Communitygithub.com

kvdm-co-pilot/create-cmp

Create production mobile apps (Android + iOS, one Kotlin codebase) with AI — deterministic Compose Multiplatform scaffold, proven-green builds, machine-enforced verify lane, device-free preview loop for coding agents. CLI + Claude Code plugin.

¿Qué es create-cmp?

create-cmp is a Claude Code agent skill that create production mobile apps (Android + iOS, one Kotlin codebase) with AI — deterministic Compose Multiplatform scaffold, proven-green builds, machine-enforced verify lane, device-free preview loop for coding agents. CLI + Claude Code plugin.

Compatible conClaude Code~Codex CLI~Cursor
npx skills add kvdm-co-pilot/create-cmp

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

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

One compact round of 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
regionFirebase region?us-central1
firebase.enabledUse Firebase (GitLive KMP)?true
firebase.authAuth: email / phone / both / none?both
firebase.firestore/storage/functions/fcmWhich Firebase services?all on if Firebase on
roomRoom local cache?true
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.

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.

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 from docs/CONTRACT.md (validated by options.schema.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:

# 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:

  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>).
  2. 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.
  3. Capture the golden baseline fresh — never copied; it must reflect the shaped behavior: UPDATE_GOLDEN=1 ./gradlew :composeApp:desktopTest --tests "*<Name>GoldenTree*".
  4. 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.

Skills relacionados