<< All versions

Skill v1.0.0

currentAutomated scan100/100
kvdm-co-pilot/create-cmp/cmp-new
──Details
PublishedSeptember 28, 2026 at 09:27 AM
Content Hashsha256:8b730cc3efa17285...
Git SHA
──Files
Files (1 file, 32.0 KB)
SKILL.md32.0 KBactive
SKILL.md · 498 lines · 32.0 KB

version: "1.0.0" name: cmp-new description: >- 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 app", or framework decisions like "React Native vs KMP for my new app". When the framework is undecided, step 0 is an HONEST fit check — recommend Compose Multiplatform as the modern default (the current generation of cross-platform: Google-backed KMP, iOS stable since May 2025, the strongest stack for AI-driven development) with its real trade-offs against React Native/Flutter, and let the user genuinely choose. If the working directory already contains an app matching the request, confirm new-vs-existing before scaffolding. Once CMP is the choice: runs a short interview (platforms, app name, package, E2E tests (Maestro), bottom-nav tabs), reads the app's shape from the ask (lean — no Room, so the faster first build — unless it syncs with a backend or works offline), then stamps a frozen version-locked template via the deterministic create-cmp engine (navigation/insets pre-solved, Clean Architecture wired) and generates the requested tab screens. Proves a GREEN build before reporting success, then hands over the device-free live preview loop for all subsequent UI work. The scaffolded app carries its own verify lane and evidence contract for future AI-driven changes.


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).

OptionQuestionDefault
appNameDisplay name?required
packageReverse-DNS package id (e.g. com.acme.app)?derived from appName
iosBundleIdiOS bundle id?same as package
platforms.iosInclude iOS (Android is always on)?true
roomNot asked — set by the shape below (lean → off, full → on). A preference the user states wins.from the shape
e2eE2E test harness (Maestro flows in qa/e2e/; key renamed from appium in 0.3.0)?true
inspectorLive on-device inspector (debug builds only — AI-inspectable UI)?true
devClientDesktop dev-client window with Compose Hot Reload?true
tabsBottom-nav tabs — label + icon each (e.g. Home/home, Profile/person)?[Home, Profile]
targetDirOutput directory?./<kebab appName>

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

Firebase is not a stamp option — don't ask about it here. The stamp carries libraries and no service, so it builds with no account and no config file. If the app needs Firebase, it is added AFTER the green stamp with create-cmp add firebase (region, auth and the services are its flags); the cmp-firebase-connect skill runs that step and the console work. The engine refuses --firebase, --region, --auth and the service flags, and a config carrying firebase or region, and names that command.

The app's shape — read from the ask, never asked

Two shapes, one template (docs/proposals/LIBRARIES-IN-SERVICES-OUT.md, Decision 3). Pick one from what the user already said; do not put it to them as a question — a new choice in front of the user is the failure this rule exists to avoid.

  • `--preset lean` — the ask names no backend to sync with and no offline use of fetched data.

"Make me a todo app" is the example. Room is off, so the first build has no KSP step and the user waits through less of it.

  • `--preset full` (the default) — the ask names a backend, sync, accounts shared across

devices, or offline reading of data that came from a server. Room is on as the offline cache.

Only Room differs. Ktor, Koin, Navigation, the harness, the inspector, the dev client, the preview loop and E2E are in both — the shape is not the harness mode, which is --minimal. If an answer later in this round names sync or offline use, the shape is full. If the user states a Room preference outright, pass --room or --no-room with the preset: a stated flag wins over the preset.

What lean costs, and you say so in the report (§9): the app has no on-device database, so what a user creates lives in memory until the app adds persistence. The stamp seeds docs/adr/NNNN-no-local-room-persistence.md, which records that and what adding Room back takes.

Intent — the root brief (feeds specs/intent.md and two of the choices 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 choices: a "first screens" answer naming distinct areas becomes the tab list, and the Purpose answer is what the shape above is read from.

AskFeeds
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 the engine's options.schema.json defines — it is the options reference, and the schema the engine validates this object against:

json
{
"appName": "Acme", "package": "com.acme.app", "iosBundleId": "com.acme.app",
"themePrefix": "Acme",
"platforms": { "android": true, "ios": true },
"room": true, "e2e": true, "inspector": true, "devClient": true,
"tabs": [{ "label": "Home", "icon": "home" }, { "label": "Profile", "icon": "person" }],
"targetDir": "./acme"
}

That object is the full shape; the lean shape is the same object with "room": false.

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 \
--theme-prefix Acme \
--preset lean \
--ios --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.

  • Pass the shape (§1) as --preset lean or --preset full; the example above is lean. Do not add --room or --no-room on

top unless the user stated it — a stated flag overrides the preset, so a copied --room turns lean back into full without a word.

  • For other toggles that are off, pass the negative flag (--no-ios, --no-e2e,

--no-inspector, --no-dev-client).

  • 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.

  1. 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.

  1. 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) in the full shape, or absent in lean; point at the seeded docs/adr/NNNN-no-local-room-persistence.md (see point 7) as the record. Backend and other integrations? — none is stamped; if the app needs Firebase, say that create-cmp add firebase (via cmp-firebase-connect) adds it next. 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.

  1. 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.

  1. 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.

  1. 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.

  1. Decisions & glossary (§8) — point, don't re-decide. The ADR index is a generated

table: every configuration choice that deviated from the interview default (minimal mode, Room off, iOS off) 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.

  1. 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).

  1. 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).

  1. Approve `exemplar-spec` now — before the slice is shaped. That approval IS the

spec-first gate.

Phase B — build to the confirmed clauses:

  1. 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>).

  1. 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.

  1. Capture the golden baseline fresh — never copied; it must reflect the shaped behavior:

UPDATE_GOLDEN=1 ./gradlew :composeApp:desktopTest --tests "*<Name>GoldenTree*".

  1. 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.

  1. preview_status { waitForRender: true } to confirm it rendered.
  2. snapshot_variant { name } (e.g. {name: "warmer"}) stashes the current renders under

composeApp/build/previews/variants/<name>/.

  1. Repeat for 2–3 candidates total, moving Tokens.kt to a fresh direction before each

next snapshot_variant.

  1. 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.

  1. 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).

  1. **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.

  1. Stable/obvious or speculative? Govern well-understood shapes; never invent a

shape ("unify these rows") that doesn't exist.

  1. Would uncontrolled divergence hurt? Visible inconsistency if every screen

reinvented it → govern; divergence fine → local.

  1. Cross-cutting concern worth enforcing once? a11y floor, token binding — earns

membership independent of reuse count.

  1. 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.

  1. Implement the promotions — each moved into presentation/components/, each with a

story in ComponentStories.kt (the componentStories lane step fails on a missing one).

  1. 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, the shape and why (lean: "no Room — what the app stores lives in memory until it adds persistence"), which lane they took (express or guided) and — if guided — what is now approved (node qa/approve.mjs --status). Then the next manual steps: ./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. The app has no Firebase; if they need it, cmp-firebase-connect runs create-cmp add firebase with their own config. If they took the express lane, remind them the walk is available any time — --reopen on any artifact starts it for that one.

All versions