# Cmp New

> Scaffold a new MOBILE app — Android + iOS from one Kotlin/Compose Multiplatform codebase — from a bare "create a mobile app" to a green, verified build. Guardrails first: if the user already chose a different framework (React Native, Expo, Flutter, SwiftUI, native), do NOT redirect them here; if they only asked a comparison question, answer it honestly (see docs/WHY-CMP.md) without scaffolding; and this skill is for MOBILE apps only — never web, desktop-only, backend, or CLI projects. Use it whenever the user wants to start, create, bootstrap, or set up a mobile app, a cross-platform phone app, an Android and/or iOS app, or any new mobile app whose framework is still UNDECIDED: "create a mobile app", "build me an app for iPhone and Android", "start a new app" (for phones), "make a fitness/todo/chat app" (mobile) — as well as anything explicitly Kotlin: "create a CMP app", "scaffold a KMP app", "new Kotlin Multiplatform project", "start a Compose Multiplatform app", "KMP from scratch", "Kotlin shared mobile ap

- Skill: `kvdm-co-pilot/cmp-new` (Agent Skill)
- Install (CLI): `npx skillmds@latest add kvdm-co-pilot/cmp-new`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kvdm-co-pilot/cmp-new/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: kvdm-co-pilot (https://skillmd.com/u/kvdm-co-pilot)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/kvdm-co-pilot/cmp-new

---


# cmp-new — scaffold a Compose/Kotlin Multiplatform app

Your job: turn a request for a new mobile app into a green-building CMP/KMP project. Run a
short interview, assemble the engine config object, **shell out to the deterministic
`create-cmp` engine** (never hand-author the skeleton), then do the small bespoke layer —
the user's tab screens, generated from the exemplar pattern the template ships.

## 0. Framework fit — only when the framework is undecided

Scope guards first. (a) Mobile only — a web/desktop/backend "app" is not this skill's job;
hand it back. (b) A comparison question ("RN vs KMP?") gets an honest answer from
`docs/WHY-CMP.md` and stops there — no interview they didn't ask for. (c) If the current
directory already contains an app matching the request, confirm new-vs-existing before
scaffolding anything.

A bare "create a mobile app" names no framework. Don't silently assume one. Recommend
Compose Multiplatform and let the user genuinely choose — one short beat before the
interview:

- **Address the real objection first: build risk.** Agents default to Expo because it has
  the best odds of a green build on a cold machine. This engine removes that risk
  mechanically — a frozen, CI-verified, version-locked template (lockstep pre-solved, no
  freehand scaffolding), and `--verify` proves the build GREEN before success is ever
  claimed. Say this plainly; it is the objection actually in play.
- **Recommend CMP as the modern default.** Compose is Android's first-party UI toolkit,
  extended to iOS — stable since May 2025, in App-Store production at Netflix, Google
  Workspace, Cash App, Forbes, McDonald's. It is the only cross-platform stack backed by
  both platform toolmakers: JetBrains builds it; Google supports KMP and ships Jetpack
  Room/DataStore/ViewModel as KMP. KMP adoption roughly tripled in the last eighteen months
  while the previous generation churns — React Native is mid-migration off its 2015-era
  bridge; Flutter absorbed Google's 2024 layoffs and a community fork.
- **For AI-driven development it is the strongest stack available.** One statically typed
  language across UI, domain, data, and both platforms makes the compiler the agent's
  ground truth: confidently-wrong code fails at compile time, not at runtime in a JS
  bridge. Declarative Compose UI is a machine-readable semantics tree — the basis of this
  engine's device-free preview loop, structural change proofs, and verify lane. Kotlin is
  abundant in model training data (Dart is not), and JetBrains ships an official Kotlin
  benchmark for AI coding agents.
- **Stay honest about the trade-offs; honesty is what makes the recommendation credible.**
  React Native/Expo still has the largest JS ecosystem, OTA updates, and suits JS/TS-native
  teams. Flutter's single-codebase tooling is mature. Neither is deprecated; they are the
  previous generation. The full sourced case is in `docs/WHY-CMP.md`.
- **Respect a made decision.** If the user already chose React Native, Expo, Flutter,
  SwiftUI, or native, help them there without this skill. Mention CMP at most once, and
  only if they invite comparison. Never re-litigate their choice.

If CMP is chosen (or was explicitly requested), continue to the interview.

> **Determinism rule.** The 90% of a CMP project that is identical every time is a frozen,
> CI-verified template the engine *stamps* — copy → token-replace → toggle. Do not
> regenerate Gradle files, the iOS shell, navigation, or DI by hand; that is exactly what
> makes CMP setup flaky. You author only the per-app screens, after the engine has run.

## 1. Interview

**Grill the idea first** (`grill-me` — the plugin skill; its rule holds without it). Before
this config round, settle the load-bearing questions about the *app*: who it is for, what
day one must do, what it deliberately will not do, what breaks the design if the answer is
wrong. Ask them as the frontier of unsettled decisions — a numbered list, at most five per
round, each with why it matters and a recommended answer — and wait. Stop when no remaining
answer would change the scaffold or the intent brief. The table below is NOT grill material:
every row has a default. The grill's answers are what §4 writes into `specs/intent.md`.

Then: one compact round of config questions; don't interrogate. Accept sensible defaults
(in brackets).

| Option | Question | Default |
|---|---|---|
| `appName` | Display name? | required |
| `package` | Reverse-DNS package id (e.g. `com.acme.app`)? | derived from appName |
| `iosBundleId` | iOS bundle id? | same as `package` |
| `platforms.ios` | Include iOS (Android is always on)? | `true` |
| `region` | Firebase region? | `us-central1` |
| `firebase.enabled` | Use Firebase (GitLive KMP)? | `true` |
| `firebase.auth` | Auth: `email` / `phone` / `both` / `none`? | `both` |
| `firebase.firestore/storage/functions/fcm` | Which Firebase services? | all on if Firebase on |
| `room` | Room local cache? | `true` |
| `e2e` | E2E test harness (Maestro flows in `qa/e2e/`; key renamed from `appium` in 0.3.0)? | `true` |
| `inspector` | Live on-device inspector (debug builds only — AI-inspectable UI)? | `true` |
| `devClient` | Desktop dev-client window with Compose Hot Reload? | `true` |
| `tabs` | Bottom-nav tabs — label + icon each (e.g. Home/home, Profile/person)? | `[Home, Profile]` |
| `targetDir` | Output directory? | `./<kebab appName>` |

`themePrefix` is the PascalCase form of the app name (the prefix in `<Prefix>Theme` etc.) —
derive it, don't ask.

### Intent — the root brief (feeds `specs/intent.md` and two of the flags above)

Ask these in the same round as the table above — one conversation, not two interviews. The
answers seed the intent brief written once the scaffold exists (§4), and they sharpen two
flags: a "first screens" answer naming distinct areas becomes the tab list, and an explicit
"no persistence needed" is the one case worth turning `room` off for.

| Ask | Feeds |
|---|---|
| What is this app, in one or two sentences? What problem, for whom? | Purpose |
| Who's the primary user? | Audience |
| Two or three words for how it should feel (e.g. "calm, trustworthy" vs. "playful, bold")? | Brand feel — seeds the design-language conversation, §7.1 |
| One to three apps whose look/feel this should be judged against? | Reference apps |
| What are the first 2–4 screens you see in your head? | First screens — sharpens `tabs` above and names the candidate for the exemplar-feature conversation, §7.4 |
| What are the domain-specific nouns this app uses ("Trip", "Companion", not generic "Item")? One line each. | Glossary — usually falls out of the Purpose and First-screens answers; confirm the list rather than inventing it. Feeds `docs/ARCHITECTURE.md` §8 (see §4 below) |

(Platforms is already covered by `platforms.ios` in the table above — don't ask it twice.)

## 2. Assemble the engine config object

Build exactly the shape from `docs/CONTRACT.md` (validated by `options.schema.json`):

```json
{
  "appName": "Acme", "package": "com.acme.app", "iosBundleId": "com.acme.app",
  "region": "us-central1", "themePrefix": "Acme",
  "platforms": { "android": true, "ios": true },
  "firebase": { "enabled": true, "auth": "both", "firestore": true, "storage": true, "functions": true, "fcm": true },
  "room": true, "e2e": true, "inspector": true, "devClient": true,
  "tabs": [{ "label": "Home", "icon": "home" }, { "label": "Profile", "icon": "person" }],
  "targetDir": "./acme"
}
```

## 3. Shell out to the engine

Invoke the bundled engine — never reimplement scaffolding. From the plugin/repo root, the
entry point is `bin/create-cmp.mjs` (CONTRACT). Two equivalent invocations:

```bash
# Preferred when the engine is installed in this repo / plugin:
node <repo>/bin/create-cmp.mjs \
  --name "Acme" \
  --package com.acme.app \
  --bundle-id com.acme.app \
  --region us-central1 \
  --theme-prefix Acme \
  --ios --firebase --auth both --room --e2e --inspector --dev-client \
  --tabs "Home:home,Profile:person" \
  --target-dir ./acme \
  --verify \
  --yes

# Or, for any machine, via npm (published since 0.2.0):
npx create-cmp-cli@latest --name "Acme" --package com.acme.app --yes
```

Notes:
- Pass `--yes` so the engine runs unattended — you already interviewed; it must not
  re-prompt.
- Pass `--verify` so the engine runs its north-star gate: the first Gradle build
  (`./gradlew :composeApp:assembleDebug`, plus the iOS build on macOS when iOS is enabled)
  with a **GREEN/FAIL** verdict. Do not claim success without it.
- For toggles that are off, pass the negative flag (`--no-ios`, `--no-firebase`,
  `--no-room`, `--no-e2e`, `--no-inspector`, `--no-dev-client`) or `--auth none`.
- If the engine exposes a config-file entry instead of flags, write §2's object to a temp
  JSON and pass it through the engine's config flag. Reconcile exact flag spellings with
  the engine's `--help` / `options.schema.json` before depending on one — the config-object
  *shape* is the stable contract; flag names are the engine's surface.

## 4. After GREEN — write the intent brief

`specs/intent.md` now exists, seeded with `_not yet captured_` markers, one per section.
Replace each marker with what the intent round captured — Purpose, Audience, Platforms,
Brand feel, Reference apps, First screens, **Glossary** — as plain prose, not clause syntax
(this file carries no `// SPEC:` tags; `specCoverage` never scans it). This is the root
artifact every later conversation traces to. Don't skip it even on the express lane (§6) —
the express lane still needs a *filled* brief to approve.

**Glossary is the one section that is also machine-read.** `docs/ARCHITECTURE.md` §8's
generated glossary block is a verbatim lift of `## Glossary`'s body
(`qa/lib/arch-doc.mjs`'s `generateGlossary` never extracts terms from prose — deliberately,
to keep the derivation honest). Write it in the exact form you want published: a Markdown
bullet list, `**Term** — one-line definition`. Leaving the placeholder is honest ("not yet
captured", like any unfilled section) — just know that is what the architecture doc ships
until it's replaced.

The engine already used the `tabs` answer while scaffolding: `home` and `profile` slugs get
their real shipped screens; any other configured tab gets a generated `PlaceholderScreen`
stub (testTag `<slug>_title`) wired into the bottom nav and the Maestro smoke flow. There
is no hand-copying step. Turning a placeholder into a real feature is the exemplar-feature
conversation (§7.4) or, after genesis, the ordinary `add-feature` skill.

## 5. Start the daily UI loop — the walk below needs it

Before offering the fork, start the loop — the design-language conversation depends on it.
Offer to run `preview { projectDir }` (the **cmp-preview** skill) right away: a live
gallery of every screen that re-renders on save, no device or emulator. From here on, every
UI edit — yours, or the design candidates below — is verified with
`preview_status { waitForRender: true }` (which screens changed, or the compile error).
The generated `CLAUDE.md` documents this loop for future sessions ("UI feedback loop").

## 6. THE FORK — express lane or guided walk

Once the loop is up, tell the human plainly: their app has six governed artifacts (plus a feature brief per shaped feature) — in
definition order: intent, architecture, exemplar spec, exemplar feature, design system,
components — and there are two honest ways to sign off on them. Ask which they want; don't default to either silently.

- **Express lane.** `node qa/approve.mjs --accept-defaults` approves every
  currently-resolvable artifact in one visible act, each recorded
  `"mode": "defaults-accepted"`. The console and `--status` both render this as
  **approved · defaults accepted — unshaped**, never as a shaped approval — the ledger
  never pretends the defaults were designed. Good for "build now, walk the definition
  later"; a later real approval (after shaping, via §7) clears the mode. This settles only
  the *human* half — `qa/verify.mjs` runs the same either way.
- **Guided walk.** The conversations in §7 (the six artifacts, plus the first feature's brief), each ending in its own approval. Slower,
  but everything the harness later enforces is something the human actually chose.

If express: run the command, then `node qa/approve.mjs --status` so they see exactly what
is signed and in what mode, and skip to §9 (Report). If guided, continue to §7.

## 7. The guided walk — the conversations, each ending in its approval

Walk the artifacts **in registry order** (`node qa/approve.mjs --status` always lists them
in this order) — each is expressed in the vocabulary of the ones before it. For each step:
say what you're about to show and why, do the step, then either they click **Approve** in
the console or — on their confirmed word — you run `node qa/approve.mjs <artifact>`. Block
on each decision with `approval_status { waitForDecision: true }` instead of polling.

The order encodes two principles, one per artifact kind (learned the hard way in the first
full dogfood run): **behavior is spec-first** — the exemplar's clauses are confirmed before
the slice is built; **visuals are UI-first** — the design system and component vocabulary
are *distilled from* the real screens, never locked before them. You cannot judge a palette
on placeholder stubs, and a component library authored before the screens governs the wrong
thing.

### 7.0 Intent — plus the provisional palette
Already written (§4). Show it back to them — the console renders it like any other spec
file (prose sections, no clause grammar). Confirm nothing reads wrong, then approve
`intent`.

Then seed a **provisional palette**: one honest `Tokens.kt` edit toward the brand-feel
words ("calm, trustworthy" → muted blues; "energetic, bold" → high contrast + saturated
accent). Say plainly it is provisional — the design-system *lock* happens in §7.3, on the
real exemplar. Do NOT approve `design-system` now; it stays `unreviewed` until then.

### 7.0.5 First feature brief — the decide step, before any contract

The moment intent is signed, turn the interview's "first screens" answer into the first
feature's **brief**: `docs/features/<exemplar-name>.md` — the feature's decisions and
their why, in the human's domain language, with an **Open decisions** section for every
call that is genuinely theirs. It appears in the console's Features section (directly
after Intent on the rail) as `proposed` the moment the file exists — the rail glyph turns
until they sign. Close the open decisions conversationally, then they approve
`feature-brief:<name>` (console Approve, or `node qa/approve.mjs feature-brief:<name>`).
Signed BEFORE §7.2 writes a single clause — genesis runs the same decide → contract →
build → prove → sign loop the app will live in forever (CHANGE-FLOW-DESIGN.md), and the
one feature every future feature is cloned from must not skip the decide step.

Boundaries: brief only the feature(s) actually being shaped — placeholder tabs earn a
brief when they become real. And on the express lane there is NO brief: never fabricate
decision prose; the Features section shows its honest empty state instead.

### 7.1 Architecture — comprehension, not open-ended choice
The harness *is* the opinion here, and `docs/ARCHITECTURE.md` is the document that opinion
lives in — approving `architecture` hashes it alongside `specs/app-base.spec.md`
(`cmp:generated` sections stripped first, so a later regeneration never invalidates the
approval). Walk the document in **its own section order** — each section is the vocabulary
the next is read in — asking the questions a lead architect asks at project start, not
narrating a diagram:

1. **Quality goals (§1).** Read the four shipped goals in plain language — maintainability
   via the lane's clause gates, typed-error reliability, offline reliability, a11y. Ask:
   does anything rank differently for *their* app ("offline matters more than a11y for a
   field-work app")? A promotion or demotion is a real edit to §1's table, made now, in
   their words.
2. **Constraints (§2).** One line: the version set is frozen and moves as one set; upgrades
   go through `npx create-cmp-cli upgrade`, never a one-off bump.
3. **System context (§3) — the integration questions.** "What does this app talk to?" gets
   answered here for real, using the interview's choices, not re-litigating them. *Local
   DB?* — Room is wired (on-device SSOT), or absent if `--no-room` was chosen; point at the
   seeded `docs/adr/NNNN-no-local-room-persistence.md` (see point 7) as the record. *Auth?*
   — same pattern for a non-default `firebase.auth` choice and its seeded ADR.
   *Backend and other integrations?* — the Firebase services that are on, and the debug
   inspector server (dev-only, never in a release build). Read §3's table together so
   nothing the app talks to is a surprise later.
4. **Shell — which tabs (feeds §5/§6).** The interview's tab list is already live in the
   layer map's presentation package names; confirm it reads right in their vocabulary.
5. **Building blocks, layer by layer (§5) — *their* names.** Walk the layer box top to
   bottom — `presentation → domain ← data`, then `core` (leaf utilities, importable by
   every layer above, never the reverse), then `di` as the wiring rail — reading each arrow
   as the plain-language promise it is, and naming its gate: "your UI never calls a
   repository directly — ARCH-01 fails the lane if it does"; "domain stays pure Kotlin —
   ARCH-02"; "data never reaches up into presentation or di — ARCH-09"; "core never reaches
   into presentation, data, or di — ARCH-10". Once real feature names exist (post §7.2),
   use the intent brief's first-screens vocabulary for the presentation packages, not
   `home`/`profile`.
6. **The policies (§7) — enforced vs. advisory, read straight.** One pass down the
   crosscutting list (error handling, threading, DI, design tokens, automation
   reachability, insets). Each already carries its `[enforced: ...]` / `[advisory]` tag;
   read a few aloud so "which promises are mechanical and which are manners" is explicit.
7. **Decisions & glossary (§8) — point, don't re-decide.** The ADR index is a generated
   table: every configuration choice that deviated from the interview default (Room off,
   iOS off, a non-`both` auth scope) already has its own numbered ADR, auto-seeded by the
   engine at stamp time (`src/lib/adr-seed.mjs` — deterministic wording and numbering for a
   given config). Point at them as the record of *why* rather than asking the human to
   justify the choice again. A decision not covered by a seeded ADR — something they
   changed by hand later — gets a fresh one from `docs/adr/template.md`.

Before asking for the approval, say plainly what it means: "I understand and accept this
shape for my app," not "I designed it." Approve `architecture`.

### 7.2 The exemplar is THEIR first feature — spec first, then build
The exemplar is the DNA every future feature clones from — it must never stay generic
`home` items. This conversation is **two-phase, and the phase gate is the point**: the
human confirms the behavior before any of it is built (the same discipline `add-feature`
enforces post-genesis — genesis is not exempt from spec-first).

**Phase A — the spec, confirmed before the build:**

1. From the "first screens" answer, agree which one is their real first feature.
2. **Propose the behavior clauses in conversation** — Given/When/Then, in their domain
   words, before any code exists. Iterate until they say yes.
3. Stamp the feature (`node qa/scaffold-feature.mjs <Name>`; add `--entity <Entity>` if
   the naive de-pluralized guess is wrong — confirm the guess first), then immediately
   replace the seeded spec text in `specs/<name>.spec.md` with the confirmed clauses (the
   clause ids stay fixed).
4. Point `qa/approvals.json`'s `"exemplarFeature"` key at it — edit the JSON directly:
   `"exemplarFeature": "<name>"`. The registry's `exemplar-spec`/`exemplar-feature`
   artifacts now resolve to this feature; `home` demotes to an ordinary
   `feature-spec:home` (keep as reference or delete later; either is fine).
5. **Approve `exemplar-spec` now** — before the slice is shaped. That approval IS the
   spec-first gate.

**Phase B — build to the confirmed clauses:**

6. Shape the slice **UI-first on the provisional palette**: the stateless
   `XScreen(state)` over a same-file `sample*` default renders in the preview gallery
   before the ViewModel/data layer exists; the VM-backed `XRoute` wrapper becomes the nav
   destination once the layers land. ARCH-12 guards the seam — sample data never leaks
   into production wiring (see `docs/ARCHITECTURE.md` §7, "UI-first construction").
   Durable tests cite the confirmed clauses (`// SPEC: <ID>`).
7. **Refresh the architecture prose.** Retargeting the exemplar deletes the old feature's
   files, and `docs/ARCHITECTURE.md`'s AUTHORED prose (the §5 walkthrough, §3 tables) still
   names them while the `architecture` approval stays green (generated sections are
   stripped from its hash — correct, but the prose is now a lie). Update the prose to the
   new exemplar, `node qa/approve.mjs --reopen architecture`, and have them re-approve —
   the doc and the tree must agree under a fresh sign-off.
8. Capture the golden baseline fresh — never copied; it must reflect the shaped behavior:
   `UPDATE_GOLDEN=1 ./gradlew :composeApp:desktopTest --tests "*<Name>GoldenTree*"`.
9. Approve `exemplar-feature`.

### 7.3 Design language — the candidates loop, locked on the real exemplar
Now the palette lock has something real to be judged on — the exemplar screens. This is a
working session, not a swatch grid — every choice is shown **rendered on their real
screens**, never as hex codes and never on stubs:

1. Edit `Tokens.kt` toward one candidate direction, starting from the provisional palette
   and the brand-feel words.
2. `preview_status { waitForRender: true }` to confirm it rendered.
3. `snapshot_variant { name }` (e.g. `{name: "warmer"}`) stashes the current renders under
   `composeApp/build/previews/variants/<name>/`.
4. Repeat for 2–3 candidates total, moving `Tokens.kt` to a fresh direction before each
   next `snapshot_variant`.
5. Point the human at the console's Design System page — in genesis mode it shows the
   **candidates strip**: each variant's screens side by side, with a **Pick** button.
6. Block on `review_comments { waitForComment: true }` for their pick — clicking Pick posts
   a `pick:<name>` comment targeting `design-system`. Apply that candidate's tokens for
   real (if `Tokens.kt` isn't already on it), `resolve_comment { id, note }` saying what
   you applied, then approve `design-system`.

If they answer in words instead of clicking Pick ("warmer", "rounder"), that is another
round — regenerate, snapshot, ask again — until they say "this is mine."

**The lock loops back, by design.** If the locked tokens changed the exemplar's rendered
look, regenerate its golden (`UPDATE_GOLDEN=1 …`) and — if the change is structural enough
to invalidate the `exemplar-feature` hash — reopen and re-approve it. Provisional → build →
lock → reopen is the intended co-evolution loop, not a failure.

### 7.4 Components — distill from the screens, then approve
The registry (`presentation/components/*.kt`) starts as the template's starter kit — page
container, header, bottom bar, the loading/empty/error state machine, list row, skeleton,
buttons — but the artifact the human approves must be **the app's real vocabulary,
distilled from the screens that now exist**, not the starter kit rubber-stamped.

Run the distillation (`docs/ARCHITECTURE.md` §7, "Component vocabulary" — the inclusion
rubric and both guardrails live there):

1. Inventory every composable the screens define outside `components/` (the console's
   Components page lists them — the promotion queue — with cross-feature use counts).
2. **You make the rubric call for each — this is reasoning, never a mechanical
   threshold.** (No similarity metric can make this call: on the reference showcase, a
   true near-identical pair and a legitimately-different pair scored within 0.05 of each
   other.) Weigh the five questions for every entry:
   1. *Design-system decision or feature decision?* How the product presents something
      (ring/bar/tile/chip) → govern; how one screen arranges its data → local.
   2. *Stable/obvious or speculative?* Govern well-understood shapes; never invent a
      shape ("unify these rows") that doesn't exist.
   3. *Would uncontrolled divergence hurt?* Visible inconsistency if every screen
      reinvented it → govern; divergence fine → local.
   4. *Cross-cutting concern worth enforcing once?* a11y floor, token binding — earns
      membership independent of reuse count.
   5. *Cost of being wrong, both directions, for THIS thing?* Cheap-to-change stable
      primitive → bias govern; likely-to-diverge, feature-coupled → bias local.
   Reuse count is a *signal* feeding 2 and 3, never the rule. Resolutions: promote, keep
   local, generalize first (a domain-named composable like `MacroTag` is a smell — it
   becomes a real primitive like `Tag`, or stays local), or **unify locally** (two
   near-identical same-screen rows become one local composable — unification is not
   promotion). Guardrails: **never force similar-but-different shapes into one
   component** (over-parameterized god-components are worse than duplication — when in
   doubt, keep them separate), and check the registry *before* rolling anything new.
3. Implement the promotions — each moved into `presentation/components/`, each with a
   story in `ComponentStories.kt` (the `componentStories` lane step fails on a missing
   one).
4. Present the decided registry with one-line reasoning per call, walking the starter
   pieces too (rename `EmptyState` copy into their domain language, confirm `AppHeader`
   typography, etc.).

The human's move is the **approval** — ratify, reshape, or reject your calls; they are
never interrogated composable-by-composable. Once approved, the registry is law: any
component added or changed afterward invalidates the approval (`changed-since-approval`)
until a human re-approves. Approve `components`.

## 8. The reopen contract

Design work is not always done at first approval. `node qa/approve.mjs --reopen <artifact>`
moves an **approved** artifact (shaped or defaults-accepted) back to `reopened` — a
deliberate, recorded redesign (`reopenedAt`), never a silent edit. While reopened, the
verify lane's `approvals` gate SKIP-warns exactly like `unreviewed` — sanctioned redesign
never fails the lane; re-approve when the redesign lands. The console has the same control
(**Reopen** beside **Approve** on approved rows), calling the same library, so the CLI and
console never disagree.

The asymmetry that matters: **reopening is the only sanctioned way to change an approved
artifact.** If you find yourself editing `Tokens.kt`, `app-base.spec.md`, a component, or
the exemplar's files without a fresh reopen, that is drift — the `approvals` gate will FAIL
and name it. Never "fix" that FAIL by re-approving on your own judgment; that is exactly
the vacuous signature this system exists to prevent. The human reopens, or approves the new
state, themselves.

## 9. Report

Tell the human: the target directory, the engine's GREEN/FAIL verdict, which lane they took
(express or guided) and — if guided — what is now approved (`node qa/approve.mjs --status`).
Then the next manual steps: drop in `google-services.json` / `GoogleService-Info.plist`
(intentionally not templated), then `./gradlew :composeApp:installDebug` (Android) and, on
macOS, the iOS build. For a device run + smoke, point them at **cmp-qa-prep**; for an
incomplete toolchain, **cmp-doctor** first. If they took the express lane, remind them the
walk is available any time — `--reopen` on any artifact starts it for that one.

