Phase A, step 5 — build the Expo app/game in 3 gated sub-phases (scaffold+core → systems+features → UI+content+polish), branched by kind (app|game). Each sub-phase ends with a HARD GATE (tsc --noEmit + expo lint + jest) before advancing; round N>1 fixes only the listed evaluator failures.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add SummerRiversound/expo-launchpad --skill expo-launchpad-generator --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Expo Launchpad Generator?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/summerriversound-expo-launchpad-generator)More formats (shields.io, HTML) on the badges page.
---
name: expo-launchpad-generator
description: Phase A, step 5 — build the Expo app/game in 3 gated sub-phases (scaffold+core → systems+features → UI+content+polish), branched by kind (app|game). Each sub-phase ends with a HARD GATE (tsc --noEmit + expo lint + jest) before advancing; round N>1 fixes only the listed evaluator failures.
argument-hint: ""
allowed-tools: [Agent, Read, Write, Edit, Bash, Glob, Grep]
---
# expo-launchpad-generator
Phase A, step 5 of the expo-launchpad pipeline. Builds the Expo (React Native + TypeScript) app or
game in three gated sub-phases (6a → 6b → 6c), branching every step by `config.md`'s `kind`
(`app` | `game`). Each sub-phase ends with a HARD GATE (`npx tsc --noEmit && npx expo lint && npx
jest`, all green) before the next sub-phase begins. On round N > 1, reads the previous evaluator
feedback and fixes only the listed failures.
All file schemas (`config.md`, `state.md`, `contract.md`, `handoff/round-N-gen.md`,
`feedback/round-N-qa.md`, the log tables) and the phase transition table are defined in
`docs/harness-protocol.md` — that document is the single source of truth (§2 for `state.md`
schema; §4 for handoff layout; §6 for log tables; §7 for the `generator → evaluator` transition).
Do not redefine schemas here. Every platform-robustness pattern (audio, haptics, lifecycle,
performance, persistence, accessibility, build/platform) is defined once in
`docs/app-gotchas.md` (R1–R11) — cite it; do not restate the patterns.
---
## Round Handling and Feedback Intake
### Workspace root
`docs/harness/` is bootstrapped by the orchestrator in the directory the pipeline was invoked
from — call this the **workspace root**. It stays there for the **entire pipeline** and is never
moved or copied into the generated project. Before any other step (including the round-1/round-N
reads below), capture the workspace root once:
```bash
ROOT="$(pwd)"
```
From this point on, **every harness file** (`config.md`, `state.md`, `contract.md`, `plans/*`,
`handoff/*`, `feedback/*`, `pipeline-log.md`, `build-log.md`, `screenshots/*`) — whether read or
written via a Bash redirect, or read/written via the `Write`/`Edit` tools — uses the root-absolute
path `"$ROOT/docs/harness/..."`, never a path relative to whatever directory the shell happens to
be in at the time. `ROOT` is fixed once, the same way `<app_slug>` and `<N>` are: treat it as a
value substituted verbatim into every command below, not a live shell variable that needs to
survive between separate tool invocations. This is what keeps `--resume` and the status/resume
skills working: they read `docs/harness/state.md` at a fixed path relative to the invocation
directory, forever.
The generated Expo project lives in its own subdirectory, `"$ROOT/<app_slug>/"`, and is its own git
repository (6a.9). Sub-phase 6a.1 `cd`s into `<app_slug>/` so the build/test commands
(`create-expo-app`, `tsc --noEmit`, `expo lint`, `jest`, `expo run:ios`, and the app's own git
commits) run from inside it — that `cd` is scoped to those commands only and never carries over to
where harness files are written. Never write into `<app_slug>/docs/harness/`.
### Determine current round
Read `"$ROOT/docs/harness/state.md"`. Extract `current_round` (integer, ≥ 1).
### Round 1 — build from scratch
When `current_round` is 1:
1. Read `"$ROOT/docs/harness/config.md"` (extract `app_name`, `app_slug`, `bundle_id`, `kind`,
`engine`, `default_language`, `orientation`).
2. Read the latest PRD (`"$ROOT/docs/harness/plans/*-prd.md"`, sort descending, take first).
3. Read the latest design doc (`"$ROOT/docs/harness/plans/*-design.md"`, sort descending, take
first).
4. Read `"$ROOT/docs/harness/contract.md"`. Confirm `## Status: AGREED` is present; abort with
`expo-launchpad-generator: contract not AGREED — run expo-launchpad-contract first` if missing.
5. Read `checkpoint` from `"$ROOT/docs/harness/state.md"` (`docs/harness-protocol.md` §2).
**Re-entry:** if a prior run crashed/paused mid-phase, skip every sub-phase `≤ checkpoint` (its
HARD GATE already passed and its output exists) and resume at the next one — e.g.
`checkpoint: 6a` → start at 6b. If `checkpoint` is `""`, start at 6a. If `checkpoint` is already
`6c`, the build is complete for this round; skip straight to **Self-evaluation and Handoff**.
This makes the generator safe to re-run without redoing (or colliding with) completed work.
### Round N > 1 — fix only listed failures (feedback intake)
When `current_round` is greater than 1:
1. Read `"$ROOT/docs/harness/feedback/round-<N-1>-qa.md"` (per `docs/harness-protocol.md` §5
layout).
2. Parse the `## Failed Criteria` section to extract each failing criterion and its prescribed fix.
3. Map each failure to the sub-phase that owns it (6a scaffold/core loop, 6b systems/data/features,
6c UI/content/tests/polish) and edit the smallest set of files needed. Do NOT redesign or
refactor areas that were not listed as failures.
4. If `"$ROOT/docs/harness/feedback/round-<N-1>-qa.md"` does not exist, abort with:
`expo-launchpad-generator: feedback file for round <N-1> not found — cannot determine fixes`.
5. Apply each fix, then run the full HARD GATE (`npx tsc --noEmit && npx expo lint && npx jest`)
before writing the handoff. `checkpoint` was reset to `""` by the evaluator on FAIL — after a
successful round N>1 gate, write `checkpoint: 6c` again to `"$ROOT/docs/harness/state.md"` (the
project is fully built, just patched) before writing the handoff.
---
## Sub-phase 6a — Scaffold + Core
### 6a.1 Create the Expo project (idempotency guard)
Run from the workspace root:
```bash
if [ -d "<app_slug>/app" ]; then
echo "project already exists — reusing (skipping create-expo-app)"
else
npx create-expo-app@latest <app_slug>
fi
cd <app_slug>
```
The current default template is TypeScript + Expo Router (matches `docs/harness-protocol.md`'s
project layout, which puts routes under `app/`). Verify `tsconfig.json` and `app/_layout.tsx`
exist after creation; if an older `create-expo-app` version produced a plain JavaScript or
non-router template instead, remove the directory and retry passing the Expo Router TypeScript
template explicitly (consult `npx create-expo-app@latest --help` for the current flag name — the
template name/flag has changed across Expo SDK releases).
**Idempotency:** `create-expo-app` fails if the target directory is non-empty, which would break a
resume or a re-run of round 1. The guard above makes 6a.1 safe to re-enter — it reuses an existing
scaffold instead of hard-failing.
### 6a.2 Project identity — package.json and app.json
Set in `package.json`:
- `name`: `<app_slug>` (from `config.md`)
- `version`: `1.0.0`
Set in `app.json` (`expo` block):
- `name`: `<app_name>` (from `config.md`) — the display name, not the slug
- `slug`: `<app_slug>`
- `orientation`: `<orientation>` (from `config.md` — `portrait` or `landscape`)
### 6a.3 Bundle id — identical across platforms
**Set it explicitly and IDENTICALLY on both platforms** (per `docs/app-gotchas.md` R6). In
`app.json`:
```json
{
"expo": {
"ios": { "bundleIdentifier": "<bundle_id>" },
"android": { "package": "<bundle_id>" }
}
}
```
Verify `ios.bundleIdentifier` == `android.package` == `config.md`'s `bundle_id`, **byte-for-byte**
(lowercase `[a-z0-9.]` only, no `_`/`-`/uppercase). A default `create-expo-app` scaffold ships no
bundle id at all — this step adds both explicitly; never leave one platform unset while the other
is configured.
### 6a.4 Remove default template boilerplate
Delete the generated demo screens/tests so the project starts from a clean slate:
```bash
rm -rf app/(tabs) components/HelloWave.tsx components/ParallaxScrollView.tsx \
components/Collapsible.tsx __tests__/
```
(Adjust the exact list to whatever the installed template version actually scaffolded — the goal
is an empty `app/` route tree and no leftover demo components before 6a.6/6a.7 write the real
ones.) Keep `app/_layout.tsx` — it is rewritten, not deleted.
### 6a.5 Copy shared config templates
Copy from this plugin's `templates/` directory (the plugin's own `templates/`, not a directory
inside the generated project):
```bash
cp <plugin-templates>/tsconfig.json.template tsconfig.json
cp <plugin-templates>/jest.config.js.template jest.config.js
mkdir -p .github/workflows && cp <plugin-templates>/ci.yml.template .github/workflows/ci.yml
mkdir -p config
```
Then, branching on `kind`:
- **`kind: game`**: `cp <plugin-templates>/gameConfig.ts.template config/gameConfig.ts`, replacing
`<APP_NAME>` with `<app_name>`. Fill every constant from the PRD's mechanics/content sections
(world size, player speed, scoring, difficulty ramp, audio volume caps). No magic numbers are
permitted anywhere else in `game/` or `app/` — every tunable value must reference
`GameConfig`.
- **`kind: app`**: `cp <plugin-templates>/appConfig.ts.template config/appConfig.ts`, replacing
`<APP_NAME>` with `<app_name>`. Fill every constant from the PRD's feature list (pagination,
network timeouts/retries, feature flags). No magic numbers are permitted anywhere else in `app/`
or `components/`.
Install dependencies used from 6a onward (both kinds): `expo-secure-store`,
`@react-native-async-storage/async-storage`, `expo-screen-orientation`, `i18next`,
`react-i18next`, `expo-localization` — via `npx expo install <package>` (resolves to the SDK-
compatible version, unlike a bare `npm install`). Install kind-specific packages in the steps
below where they're first used.
### 6a.6 `kind: game` — Skia canvas, loop, state machine, input
1. **Dependencies**: `npx expo install @shopify/react-native-skia react-native-gesture-handler`.
2. **Game-state type**: create `game/scene/gameState.ts`:
```ts
export type GameState = 'menu' | 'playing' | 'paused' | 'gameOver';
// Add states as the PRD requires (e.g. 'levelComplete', 'shop').
```
3. **Root layout**: `react-native-gesture-handler` requires the app root wrapped in
`GestureHandlerRootView` — do this once in `app/_layout.tsx`, wrapping the `<Stack>`/router
outlet. Omitting this wrapper makes every gesture silently fail to register.
4. **Scene**: create `game/scene/GameCanvas.tsx`. It renders a `<Canvas>` (from
`@shopify/react-native-skia`) sized to `GameConfig.world`, drives the loop via
`useFrameCallback` (per `docs/app-gotchas.md` R4 — cite it; do not restate the
no-per-frame-allocation rule), and exposes `startGame()` / `pauseGame()` / `resumeGame()` /
`endGame()` transition functions that update the `GameState` (lifted `useState` or a small
store) consumed by `app/game.tsx` to decide which overlay to render.
5. **Input**: wire the control method the PRD's §3.1 Controls table specifies using
`react-native-gesture-handler`'s `Gesture.Tap()` / `Gesture.Pan()` / `Gesture.LongPress()`
inside a `GestureDetector` wrapping the `<Canvas>` — never the raw `PanResponder`/`onTouch*`
APIs (`docs/app-gotchas.md`, Input & UI).
### 6a.7 `kind: app` — Expo Router route tree, navigation, state layer
1. Create one route file under `app/` per row of the PRD's §6 Navigation Map (file-based routing —
route path maps directly to the file path/name, e.g. `app/settings.tsx` → `/settings`).
2. Create `app/_layout.tsx` as the root `Stack`/`Tabs` navigator matching the PRD's navigation
structure; every route from §6 must be reachable from the paths the map specifies.
3. Create a `state/` directory holding the app's shared state layer (React Context, or a small
store if the PRD's state complexity warrants one) — feature logic never lives inline in `app/`
route files; routes only read from `state/`/`services/` and render.
4. Each route implements the Loading/Empty/Error states its PRD §5 States row specifies (even as a
minimal placeholder at this stage — full implementations land in 6b/6c).
### 6a.8 Minimal passing test
Branching on `kind`:
- **`kind: game`**: create `config/gameConfig.test.ts` asserting `GameConfig`'s core constants are
positive / within range (world dimensions > 0, volume caps in `[0, 1]`, etc.).
- **`kind: app`**: create `config/appConfig.test.ts` asserting `AppConfig`'s core constants are
positive / within range (page size > 0, timeout > 0, etc.).
### 6a.9 Initialize version control
The generated project is its own git repository — initialize it and commit as the build
progresses, so the work has atomic history from the start (and the user can diff/revert per
sub-phase). `create-expo-app` already writes a suitable `.gitignore` (`node_modules/`, `.expo/`,
`dist/`, etc.) — do not overwrite it; Phase A collects no signing credentials or secrets to
additionally exclude (per `config.md`'s `credentials_dir` placeholder note in
`docs/harness-protocol.md` §1), so no extra excludes are needed here.
```bash
git init -q
git add -A && git commit -q -m "chore: scaffold <app_name> (expo-launchpad 6a)"
```
Use **Conventional Commits** and **never add AI-authorship trailers** (no `Co-Authored-By`, no
"Generated with…" line). Make one atomic commit at each sub-phase gate (6a/6b/6c) and one per
discrete fix on round N > 1.
### 6a HARD GATE
After completing steps 6a.1–6a.9, run and confirm all three exit 0 before proceeding to
Sub-phase 6b:
```bash
npx tsc --noEmit
npx expo lint
npx jest
```
`tsc --noEmit` must report **zero errors**. `expo lint` must report **zero errors**. `jest` must
report **zero failures**. When green, commit:
`git add -A && git commit -q -m "feat: scaffold and core loop pass tsc+lint+jest (6a)"`, then write
`checkpoint: 6a` to `"$ROOT/docs/harness/state.md"` (atomically) so a later re-entry skips 6a.
**If any command fails, fix all reported issues before proceeding. Do not start 6b until this
gate is green.**
---
## Sub-phase 6b — Systems and Features
Sub-phase 6b implements all game entities/systems (or app feature logic/data/services) as specified
in the PRD. Start only after the 6a HARD GATE passes.
### `kind: game`
#### 6b.1 Entities
Create one file per entity under `game/entities/` (e.g. `Player.ts`, `Enemy.ts`, `Obstacle.ts` —
name them after the PRD's actual content). Each entity:
- Holds its own position/velocity/size state and a `draw(canvas, paint)` method using Skia
primitives (or a shared drawing helper) styled from `ui/designTokens.ts` colours (added in 6c;
reference the token names now, fill values are already known from the design doc).
- Reads every tunable value (speed, size, score value) from `GameConfig` — no hardcoded numbers.
- Exposes a hitbox/bounds check if the PRD requires collision detection.
#### 6b.2 Systems — spawn / collision / score / difficulty
Create one file per system under `game/systems/`:
- `spawnSystem.ts` — reads spawn intervals and difficulty parameters from `GameConfig`; draws
entity definitions from the data catalog (6b.3), never inline literals.
- `collisionSystem.ts` — player-vs-enemy/obstacle (game-over or health reduction, per the PRD) and
player-vs-collectible (score increment + SFX trigger) checks, run once per frame from the scene's
`useFrameCallback`.
- `scoreSystem.ts` — maintains the current score (`addScore(points)`, `score` getter), notifies the
HUD (added in 6c) on change, and persists the high score through `saveRepository.ts` (added in
6c — do not call `AsyncStorage`/`SecureStore` directly from here; that lands once the durable
save module exists).
- `difficultySystem.ts` — tracks elapsed play time and ramps spawn interval / entity speed using
`GameConfig.difficulty`, never dropping below `GameConfig.difficulty.minSpawnInterval`.
#### 6b.3 Data catalogs
Create one file per data-driven element the PRD's §4 Content Metrics defines (enemies, levels,
waves, collectibles) under `data/` (e.g. `data/enemies.ts`, `data/levels.ts`). Each catalog exports
a typed array of plain data objects. No enemy/level/wave values may be hardcoded in
`game/entities/` or `game/systems/` — all such values must come from a catalog here. This satisfies
Hard Gate 5 (`docs/harness-protocol.md` §3).
#### 6b.4 Audio pool
**R1 — Audio safe (cite `docs/app-gotchas.md`; do not restate).** `expo-audio`'s API is
`createAudioPlayer(source)` (or the `useAudioPlayer(source)` hook), returning an `AudioPlayer`
(`.play()`, `.pause()`, `.seekTo()`, `.volume`, `.remove()`) — there is **no built-in pool**.
Never reach for the legacy `expo-av` sound-object API — this project only uses `expo-audio`.
1. `npx expo install expo-audio`.
2. Create `game/systems/audioSystem.ts`. For each **frequent** SFX, pre-load 1–3 `AudioPlayer`
instances via `createAudioPlayer` up front (in a setup effect, not per-trigger) and cycle
through them on each `playSfx(name)` call. Rare one-offs may load on demand.
3. Throttle repeated SFX (~70 ms per key) using a `Map<string, number>` of last-played timestamps.
4. Wrap every load/play/BGM call in try/catch, logging via `console.warn` on failure — a
missing/bad audio asset must never crash; playback is skipped silently.
5. Start BGM only in the `'playing'` state; stop it on game-over, on `AppState` background (see
6b.6/R3), and on unmount.
6. Apply per-channel volume caps from `GameConfig.audio` (`bgmVolume`, `sfxVolume` as safe caps —
a user volume slider, if any, is a fraction of the cap).
#### 6b.5 Haptics helper
**R2 — Haptics safe (cite `docs/app-gotchas.md`; do not restate).**
1. `npx expo install expo-haptics`.
2. Create `game/systems/haptics.ts`. Gameplay code never calls `Haptics.*` directly — only through
this helper, which:
- No-ops unless `Platform.OS === 'ios' || Platform.OS === 'android'`.
- Enforces a global throttle (~60 ms minimum gap).
- Exposes a persisted `enabled` toggle (via `AsyncStorage`) and intent methods (`light()`,
`medium()`, `heavy()`) over `expo-haptics`.
- Wraps every call in try/catch (simulators/unsupported devices throw).
#### 6b.6 App lifecycle wiring
**R3 — App lifecycle safe (cite `docs/app-gotchas.md`; do not restate).** Subscribe to
`AppState.addEventListener('change', ...)` in the scene (`game/scene/GameCanvas.tsx`) or its root
route: on `background`/`inactive`, cancel the frame loop and pause BGM; on `active`, reverse both.
Gate input behind a `canInput` flag during the transition. Clean up the subscription (and all
timers) on unmount.
#### 6b.7 Tests for systems
Write **at least 3** focused unit tests covering the game's actual systems, e.g.:
- `scoreSystem.addScore` increments correctly (and a `reset` zeroes it).
- `difficultySystem` increases spawn rate over time but never below
`GameConfig.difficulty.minSpawnInterval`.
- Data catalog entries are non-empty and every numeric field is positive.
These are the minimum — add one per non-trivial system the PRD defines. Contract gate R11 requires
≥3 system unit tests.
### `kind: app`
#### 6b.1 Feature logic and typed data models
Create one typed model per PRD §4 Data Model entity under `data/` (e.g. `data/order.ts`) — no
untyped `any` payloads. Implement the feature logic each PRD §3 Feature List (MVP) row requires.
#### 6b.2 Service/data layer
Create the API/data-fetching layer under `services/` (e.g. `services/orderService.ts`) —
per the PRD §4, either a mock in-memory/local dataset or a real API client. Every network call has
an explicit timeout/retry from `AppConfig.network` (no magic numbers). No feature logic or
network/storage calls live inline in `app/` route files — only in `services/`/`state/`.
#### 6b.3 Hooks
Create a `hooks/` directory (a natural addition to the PRD §9 map, wrapping `state/`/`services/`
into screen-consumable hooks — analogous to how design added `ui/designTokens.ts` as a single-file
addition). E.g. `hooks/useOrders.ts` composing `services/orderService.ts` + `state/` for a route to
consume with one call.
#### 6b.4 Tests for features
Write **at least 3** focused unit tests covering real logic, e.g.:
- A service function returns the expected shape/error for both success and failure paths.
- A typed data model's parsing/validation rejects malformed input.
- A hook's derived state (e.g. a computed total, a filtered list) is correct for a given input.
Contract gate R11 requires ≥3 unit tests.
### 6b HARD GATE
After completing the `kind`-appropriate steps above, run:
```bash
npx tsc --noEmit
npx expo lint
npx jest
```
All three must report **zero errors/failures**. When green, commit:
`git add -A && git commit -q -m "feat: systems and features pass tsc+lint+jest (6b)"`, then write
`checkpoint: 6b` to `"$ROOT/docs/harness/state.md"`.
**If any command fails, fix all reported issues before proceeding. Do not start 6c until this
gate is green.**
---
## Sub-phase 6c — UI, Content, and Polish
Sub-phase 6c wires up all screens/overlays, applies design tokens, adds localization, completes
durable-save persistence, and applies accessibility/branding/native-config polish. Start only
after the 6b HARD GATE passes.
### 6c.1 Design tokens
Create `ui/designTokens.ts` from the design doc's `## Design tokens` section, using the exact file
template it specifies (`Colors`/`Spacing`/`Radius`/`Typography`, each `as const`). Every colour,
spacing, radius, and font value used anywhere in `app/`, `components/`, `game/`, or `ui/` must
come from this file — no raw hex strings or bare numeric literals in UI code.
### 6c.2 Screens and overlays
**`kind: game`** — for each screen/overlay the PRD requires, create a file. A typical set (add or
remove per the PRD):
| File | Rendered | Trigger |
|---|---|---|
| `app/index.tsx` | main-menu route | app launch |
| `app/game.tsx` | hosts `game/scene/GameCanvas.tsx` | Play tapped |
| `ui/hud.tsx` | overlay inside `app/game.tsx` | `gameState === 'playing'` |
| `ui/pauseOverlay.tsx` | overlay inside `app/game.tsx` | pause button tapped |
| `ui/gameOverOverlay.tsx` | overlay inside `app/game.tsx` | player loses |
| `app/settings.tsx` | settings route | settings button tapped |
`ui/hud.tsx` shows the current score, health/lives (if the PRD defines them), and a pause button.
The game-over overlay shows the final score, best score (with a new-record indicator), a restart
control, and a main-menu control. All sizes/colours use `ui/designTokens.ts`.
**`kind: app`** — for each screen the PRD's §2 Primary Journeys / §6 Navigation Map defines, fill
in the full implementation (the route files created in 6a.7 held placeholders): Loading/Empty/Error
states per PRD §5, styled exclusively from `ui/designTokens.ts`, using shared building blocks
under `components/` (buttons, cards, lists, form fields) so no screen duplicates the same markup.
**Accessibility baseline (gate R10 — cite `docs/app-gotchas.md`; do not restate).** Every tappable
control on these screens/overlays must be **≥44×44 pt** and carry an `accessibilityLabel` /
`accessibilityRole` — especially icon-only buttons (pause, settings, restart). Read
`AccessibilityInfo.isReduceMotionEnabled()` and route it through one flag so gameplay/transition
effects (screen shake, big transitions, flashing) can damp or skip themselves when it's `true`;
never flash faster than 3×/second.
### 6c.3 Localization (i18n)
1. `cp <plugin-templates>/i18n.ts.template i18n/i18n.ts`.
2. For every locale `config.md`'s `default_language` requires (plus `en` as the template's
fallback, if `default_language` isn't already `en`), add a `resources` entry with every
user-visible string used across every screen/overlay/component — no string literals in UI
code, only `t('key')` calls (`react-i18next`'s `useTranslation`).
3. Verify completeness: every locale's key set must exactly match every other configured locale's
key set (no missing/extra keys) — write or run a small script comparing the `resources` objects
and fail loudly if any key is missing in any locale. This satisfies Hard Gate 6
(`docs/harness-protocol.md` §3).
### 6c.4 Durable save — persistence (default ON)
**R9 — Persistence safe (cite `docs/app-gotchas.md`; do not restate).**
1. `cp <plugin-templates>/saveRepository.ts.template data/saveRepository.ts`, replacing
`__SAVE_KEY__` with `<app_slug>_save_v1`. It mirrors one JSON blob across **`expo-secure-store`**
(durable-first read) and **`AsyncStorage`** (cache), every call wrapped in try/catch.
2. Keep the blob small — `expo-secure-store` caps values at ~2 KB per key on iOS/Android. Persist
only essential fields (high score/progress, economy totals, settings, a `save_v1` version key).
If more must be stored, split across additional `saveRepository` keys or keep bulk data in
`AsyncStorage` only, with just a pointer/critical subset going through `saveRepository`.
3. Route every persisted read/write through `saveRepository.ts` — no direct `AsyncStorage`/
`expo-secure-store` calls anywhere else. `kind: game`'s `scoreSystem.ts` (6b.2) wires through it;
`kind: app`'s `state/` layer loads it once at startup into an in-memory shape and writes through
it on every mutation.
4. Migrate the audio/haptics `enabled` toggles from the direct `AsyncStorage` stopgap (6b.4/6b.5) to
route through `saveRepository.ts` — replace the direct `AsyncStorage` calls in
`game/systems/haptics.ts` and `game/systems/audioSystem.ts` with calls into `saveRepository.ts`,
and verify no direct `AsyncStorage` calls remain outside `saveRepository.ts`.
5. Write critical values synchronously at run-end (final score, currency, progress) rather than
relying only on a later async flush.
### 6c.5 App branding — icon, splash, display name
**R5 — Branding safe (cite `docs/app-gotchas.md`'s Store rejections section; do not restate).** A
shipped app must not have the default Expo icon, default splash, or a slug-ish display name.
1. **Icon + splash art.** Default: code-drawn, from the design doc's Asset plan (palette + a
monogram/mark) — no external art dependency. Add `sharp` as a dev dependency
(`npm install --save-dev sharp`) and create `scripts/gen-icon.js`: build a small inline SVG
string per asset (a solid `Colors.primary`-filled 1024×1024 square with a centred monogram — the
first letter of `app_name` — in `Colors.onBackground` for the icon; a `Colors.background`-filled
square with a smaller centred mark for the splash) and rasterize each with
`sharp(Buffer.from(svg)).png().toFile(...)`. Run it once (`node scripts/gen-icon.js`) to write:
- `assets/images/icon.png` (1024×1024, **opaque — flatten any alpha with `.flatten({ background:
... })`**; a transparent iOS icon is rejected)
- `assets/images/adaptive-icon.png` (Android foreground layer, same mark)
- `assets/images/splash-icon.png`
If the design doc's Asset plan instead chose sourced/AI-generated art, use those images at the
same paths (still flattened opaque for the icon).
2. **Wire `app.json`** per the design doc's App icon and splash screen template: `expo.icon`,
`expo.android.adaptiveIcon` (`foregroundImage` + `backgroundColor: <Colors.primary>`), and the
`expo-splash-screen` plugin entry (`image`, `imageWidth: 200`, `resizeMode: "contain"`,
`backgroundColor: <Colors.background>`).
3. **Display name** — `app.json`'s `expo.name` was already set to `<app_name>` in 6a.2; confirm it
still reads as the human display name, not the slug (this is what both platforms show as the
home-screen label in a managed Expo build).
### 6c.6 Native platform config — orientation lock
**R6 — Native config safe (cite `docs/app-gotchas.md`; do not restate).** `app.json`'s
`expo.orientation` was set in 6a.2; reinforce it at runtime so the app can't rotate into the unused
orientation even under a dev client or a config that doesn't fully honour `app.json`:
1. `npx expo install expo-screen-orientation`.
2. In the root layout (`app/_layout.tsx`), on mount call
`ScreenOrientation.lockAsync(...)` with the lock constant matching `config.md`'s `orientation`
(`PORTRAIT_UP` or `LANDSCAPE`), wrapped in try/catch (unsupported on web).
### 6c.7 Anti-stub verification
Before the final HARD GATE, run one directory-agnostic scan over every TypeScript source in the
project (covers `app/`, `game/`, `components/`, `config/`, `data/`, `services/`, `state/`, `hooks/`,
`ui/`, `i18n/` and any other source dir — no need to enumerate them, and nothing is missed if a
dir was added in 6b/6c that an enumerated list would omit). Run from inside `<app_slug>/` (the
working directory since 6a.1):
```bash
grep -rniE "TODO|stub|placeholder|미구현" . --include='*.ts' --include='*.tsx' | grep -vE 'node_modules|/\.expo/|/dist/'
```
If this command returns any output, fix or remove every match. The contract requires zero stubs in
app/game logic (`docs/harness-protocol.md` §3, Hard Gate 3).
### 6c.8 Full tests — component/RTL + Maestro flow
Contract gate R11 requires **≥1 component test + ≥1 Maestro flow**, beyond the ≥3 unit tests from
6b.
1. **Component test** — `npm install --save-dev @testing-library/react-native`. Create a test for
one menu/overlay/screen (e.g. `ui/hud.test.tsx` or `app/index.test.tsx`) asserting it renders
and that pressing its primary control (accessibility-labelled, ≥44pt per R10) fires the expected
handler.
2. **Maestro flow** — create `.maestro/core-loop.yaml` (game: boot → play → win/lose → restart) or
`.maestro/primary-journey.yaml` (app: the PRD's primary journey, screen by screen), each step
keyed off the `accessibilityLabel`s set in 6c.2. `appId: <bundle_id>`. (Maestro itself is an
external CLI, not an npm dependency — the evaluator phase runs `maestro test .maestro/<flow>.yaml`
against the built app.)
3. Expand coverage per kind:
- **`kind: game`**: game-over overlay shows the correct final/best score; `saveRepository`
read/write round-trips (score, settings) using a mocked `expo-secure-store`/`AsyncStorage`.
- **`kind: app`**: every screen's Loading/Empty/Error state renders correctly for a given input;
`saveRepository` read/write round-trips using the same mocks.
- Both: localization — every configured locale's `resources` key set matches (6c.3's check,
turned into an actual `jest` test rather than a one-off script).
### 6c.9 README + LICENSE
A shipped project has a `README.md` and a `LICENSE`.
- **`README.md`** — `cp <plugin-templates>/README.md.template README.md`, then fill:
`<APP_NAME>` → `app_name`; `<ONE_LINE_PITCH>` → the PRD's tagline; `<TECH_STACK>` → the actual
stack used (**`kind: game`**: Skia `<Canvas>` + `useFrameCallback` game loop, `expo-audio`,
`expo-haptics`, i18next, `expo-secure-store`+`AsyncStorage` durable save; **`kind: app`**: Expo
Router navigation, the `state/`/`services/` layer, i18next, `expo-secure-store`+`AsyncStorage`
durable save). Author it in `default_language` (add an English section too when it isn't `en`).
- **`LICENSE`** — `cp <plugin-templates>/LICENSE.template LICENSE`, filling `<YEAR>` with the
current year and `<HOLDER>` with `<app_name>` (Phase A collects no developer/company identity —
same placeholder rationale as the `bundle_id` `example` segment, `docs/harness-protocol.md` §1).
Commit them with the 6c gate commit below.
### 6c HARD GATE
After completing steps 6c.1–6c.9, run:
```bash
npx tsc --noEmit
npx expo lint
npx jest
```
All three must report **zero errors/failures**. Also confirm branding (6c.5): a **custom** icon +
splash were generated (not the default Expo art), the icon is **opaque (no alpha)**, and the
display name equals `app_name` (not the slug). And native config (6c.6): orientation locked to
`config.orientation` both statically (`app.json`) and at runtime (`expo-screen-orientation`). And
assets/CI (6a.9/6c.5): every asset referenced in code exists on disk (no dangling paths), and
`.github/workflows/ci.yml` is present.
**This is the final gate. Do not write the handoff until all three commands pass. If any fails,
fix all reported issues and re-run all three.** When green, commit:
`git add -A && git commit -q -m "feat: UI, content, branding and native config (6c)"`, then write
`checkpoint: 6c` to `"$ROOT/docs/harness/state.md"` before writing the handoff.
---
## Self-evaluation and Handoff
After the 6c HARD GATE passes, write the generator handoff and update pipeline state.
### Write handoff/round-N-gen.md
Create `"$ROOT/docs/harness/handoff/round-<N>-gen.md"`, following the layout defined in
`docs/harness-protocol.md` §4:
```markdown
# Round <N> — Generator Handoff
## What Was Built
<!-- Bullet list of features implemented (round 1) or bugs fixed (round N>1). -->
## Contract Self-Assessment
| Criterion | Status | Evidence |
|---|---|---|
| Hard Gate 1 (tsc --noEmit + expo lint) | ✅/❌ | <command output ref> |
| Hard Gate 2 (jest all pass) | ✅/❌ | <command output ref> |
| Hard Gate 3 (no TODO/stub/placeholder) | ✅/❌ | <grep output ref> |
| Hard Gate 4 (tuning centralized in config/) | ✅/❌ | <file ref> |
| Hard Gate 5 (content defined as data) | ✅/❌ | <file ref> |
| Hard Gate 6 (l10n complete, no missing keys) | ✅/❌ | <check ref> |
| Hard Gate 7 (core loop / primary journey end-to-end) | ✅/❌ | <test/flow ref> |
| Hard Gate 8 (zero crashes/console errors on simulator) | ✅/❌ | <deferred to evaluator's live run> |
| Platform-Robustness Gates R1–R11 | ✅/❌ | <brief per-gate note> |
| <PRD-specific criterion> | ✅/❌ | <evidence> |
## Test Results
- `tsc --noEmit`: <pass/fail summary>
- `expo lint`: <pass/fail summary>
- `jest`: <pass/fail summary, test count>
## Environment
- `node`: <output of `node --version`>
- `expo`: <the `expo` version pinned in package.json>
- `npx expo --version`: <output of that command>
## Known Issues
<!-- List any known issues or deferred items. State "none" if clean. -->
```
Fill every section with actual output and real assessments. Do not leave placeholder text.
### Update state.md
Update `"$ROOT/docs/harness/state.md"` per `docs/harness-protocol.md` §2 and the
`generator → evaluator` transition in §7. Per §7 rule 2, a successful phase completion sets
`status: running` in the same atomic write:
```yaml
status: running
current_phase: generator
next_role: evaluator
updated_at: "<ISO-8601 UTC now>"
```
Leave `current_round`, `created_at`, `resume_attempts`, and all other keys unchanged (`checkpoint`
was already written as `6c` at the end of the 6c HARD GATE, above). Use `Edit` for a targeted
update.
> **Note:** `current_round` is incremented by the evaluator when it returns FAIL; the generator
> reads it but does not write it.
### Append to build-log.md
Append one row to `"$ROOT/docs/harness/build-log.md"` per `docs/harness-protocol.md` §6:
```
| <N> | generator | <ISO-8601 UTC now> | handoff | round <N> built; tsc 0; lint 0; jest 0 failures |
```
### Append to pipeline-log.md
Append one row to `"$ROOT/docs/harness/pipeline-log.md"` per `docs/harness-protocol.md` §6:
```
| <ISO-8601 UTC now> | generator | handoff | evaluator |
```
---
## Error handling
- If `contract.md` is missing or does not contain `## Status: AGREED`, abort immediately.
- If any HARD GATE fails (non-zero `tsc --noEmit`/`expo lint` errors, or failing `jest` tests),
stop at that sub-phase, fix the failures, and re-run the gate. Do not advance to the next
sub-phase.
- If the feedback file for round N-1 is missing on a round > 1 run, abort with a clear message and
set `"$ROOT/docs/harness/state.md"` to `status: paused`, `pause_reason: manual_action`.
- If `create-expo-app` fails, abort immediately and do not proceed to 6a.2.
- If a dependency install fails, check for SDK version conflicts (`npx expo install --check`) and
resolve before continuing.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!