create-cmp-cli 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,56 +2,54 @@
2
2
 
3
3
  # create-cmp
4
4
 
5
- **The AI delivery harness for Kotlin/Compose Multiplatform — scaffold a green-building app (Android + iOS) in minutes, with best practices your AI is mechanically held to.**
5
+ **The AI delivery harness for Kotlin/Compose Multiplatform.**
6
6
 
7
- Toolchain auto-bootstrapped · Navigation & insets pre-solved · Clean Architecture wired · Test pyramid + E2E flows included · AI-native inspector & verification lane.
7
+ Scaffold a green-building Android + iOS app in minutes then let AI extend it, held to a
8
+ machine-enforced definition of done.
8
9
 
9
10
  [![CI](https://github.com/kvdm-co-pilot/create-cmp/actions/workflows/ci.yml/badge.svg)](https://github.com/kvdm-co-pilot/create-cmp/actions/workflows/ci.yml)
10
11
  [![npm version](https://img.shields.io/npm/v/create-cmp-cli.svg)](https://www.npmjs.com/package/create-cmp-cli)
11
12
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
12
13
  [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING.md)
13
14
  [![Kotlin Multiplatform](https://img.shields.io/badge/Kotlin-Multiplatform-7F52FF.svg?logo=kotlin&logoColor=white)](https://kotlinlang.org/docs/multiplatform.html)
14
- [![Claude Code plugin](https://img.shields.io/badge/Claude%20Code-plugin-D97757.svg)](#use-it-from-claude-code)
15
+ [![Claude Code plugin](https://img.shields.io/badge/Claude%20Code-plugin-D97757.svg)](#the-claude-code-plugin-8-skills)
15
16
 
16
17
  </div>
17
18
 
18
19
  ---
19
20
 
20
- Starting a Compose Multiplatform project is slow and flaky — not because the knowledge is hard, but
21
- because it isn't *reproducible*. Kotlin ↔ KSP ↔ CMP ↔ Room ↔ AGP must all agree or the build dies;
22
- the iOS shell (XcodeGen + CocoaPods + Firebase pods + AppDelegate + framework embedding) is a
23
- multi-step minefield; edge-to-edge insets get re-debugged on every project; and the toolchain itself
24
- is a cliff most scaffolders leave to you. That friction is why greenfield mobile apps quietly default
25
- to React Native.
26
-
27
- **`create-cmp` removes the friction.** It *stamps* a frozen, CI-verified golden template (it does not
28
- freehand-generate your project), bootstraps the toolchain, and **proves a green build** before it
29
- reports success.
30
-
31
- And it goes further than scaffolding: `create-cmp` is an **AI delivery harness**. In the age of
32
- AI-driven development, the scarce thing isn't code it's a machine-enforceable definition of
33
- *correct*. Every generated project ships the architecture patterns as running exemplars (with
34
- tests), the best practices as executable conformance checks, generation tools so the AI extends
35
- the app right-by-construction, an AI-native structural inspector, and a verification lane that
36
- refuses "done" without evidence. See [`docs/HARNESS-PLAN.md`](./docs/HARNESS-PLAN.md) for the
37
- full product definition.
38
-
39
- ## Contents
40
-
41
- - [Quick start](#quick-start)
42
- - [Commands](#commands)
43
- - [What you get](#what-you-get-the-pre-solved-moat)
44
- - [Options](#options)
45
- - [Use it from Claude Code](#use-it-from-claude-code)
46
- - [Why CMP, not React Native](#why-cmp-not-react-native)
47
- - [How it works](#how-it-works)
48
- - [Requirements](#requirements)
49
- - [Roadmap](#roadmap)
50
- - [Contributing](#contributing)
51
-
52
- > **Full usage in one place:** [`docs/USAGE.md`](./docs/USAGE.md) is the single entry point — setup,
53
- > every CLI command, all 8 skills, the 14-tool `cmp-inspector` MCP, and the workflows that tie them
54
- > together. Read it and you can drive create-cmp end to end (agents included).
21
+ ## What is this, in plain words
22
+
23
+ **Day one, it's a scaffolder.** One command gives you a working Compose Multiplatform app
24
+ Android and iOS, navigation and insets solved, Clean Architecture wired, tests passing, build
25
+ green. It *stamps* a frozen, CI-verified template; it never asks an AI to freehand your project,
26
+ so every scaffold is identical and every scaffold builds.
27
+
28
+ **Every day after, it's a harness.** AI writes code fast, and confidently including confidently
29
+ wrong. The scarce thing is no longer code; it's a **machine-checkable definition of "correct"**.
30
+ Every project this tool generates carries that definition inside it: behavior specs, an
31
+ executable verify lane, generators that extend the app the right way by construction, and
32
+ enforcement that refuses "done" without evidence. An AI session working in your repo doesn't
33
+ *promise* the feature works it has to *prove* it, and it gets blocked when it can't.
34
+
35
+ **See it live:** [create-cmp-showcase](https://github.com/kvdm-co-pilot/create-cmp-showcase) is a
36
+ public repo built entirely by this tool every commit carries its evidence receipt, and
37
+ [PR #1](https://github.com/kvdm-co-pilot/create-cmp-showcase/pull/1) shows the harness *refusing*
38
+ a bad change and naming the exact rule it broke.
39
+
40
+ ## The core loop
41
+
42
+ ```
43
+ spec clause → generate from exemplar → verify lane (8 gates) → evidence receipt
44
+ ↑ │
45
+ └────────────── enforcement: Stop hook + CI refuse "done" without it ──┘
46
+ ```
47
+
48
+ Behavior starts as a written spec clause. Code is cloned from a proven exemplar. The verify lane
49
+ checks everything — spec coverage, build, tests, architecture, UI structure, design tokens,
50
+ accessibility, on-device E2E — and writes a receipt bound to a content hash of the code it
51
+ verified. The Stop hook and CI both check that receipt. You cannot hand-forge it, and a stale one
52
+ doesn't pass.
55
53
 
56
54
  ## Quick start
57
55
 
@@ -62,208 +60,240 @@ npx create-cmp-cli@latest
62
60
  …or non-interactively:
63
61
 
64
62
  ```bash
65
- npx create-cmp-cli@latest --name Acme --package com.acme.app --yes --verify
63
+ npx create-cmp-cli@latest my-app --name Acme --package com.acme.app --yes --verify
66
64
  ```
67
65
 
68
- The scaffolder interviews you (or takes flags), runs the toolchain doctor, stamps the template, and
69
- builds the app to prove it's green.
70
-
71
- > **On the package name:** it publishes as `create-cmp-cli` — the bare name `create-cmp` is held by
72
- > an unrelated placeholder, and `create-cmp-app` is a real, unrelated CMP generator, so we didn't
73
- > reuse either. The installed *command* is still `create-cmp`. You can also run straight from GitHub
74
- > with no install: `npx github:kvdm-co-pilot/create-cmp`, or use the
75
- > [Claude Code plugin](#use-it-from-claude-code).
66
+ It interviews you (or takes flags), checks your toolchain, stamps the template, and **builds the
67
+ app to prove it's green** before reporting success.
76
68
 
77
- ## Commands
69
+ > **Name note:** the npm package is `create-cmp-cli` (the bare `create-cmp` name was already
70
+ > squatted); the installed command is still `create-cmp`.
78
71
 
79
- `create-cmp` is useful across the whole life of a project, not just day one — and every command
80
- except `create` works on **any** KMP project, not only ones it scaffolded:
72
+ ---
81
73
 
82
- | Command | What it does |
83
- |---|---|
84
- | `create-cmp [dir]` / `create-cmp create` | Scaffold a new app from the frozen golden template (the default command). |
85
- | `create-cmp doctor [--fix]` | Toolchain preflight (JDK/SDK/Xcode/Appium — legacy driver install for the pre-Maestro path, consent-gated installs) **plus** project diagnosis when run inside a Gradle project: kotlin↔ksp lockstep, drift vs proven-green sets, the KSP2/iOS catch-22, sdk.dir, `~/.konan` bloat, disk space. `--fix` applies the safe heals. |
86
- | `create-cmp upgrade [--dry-run]` | Migrate `gradle/libs.versions.toml` to the next proven-green version set — diff table first, surgical in-place edits with `.bak-upgrade` backups, kotlin↔ksp lockstep guardrail, `--verify` to prove the result. |
87
- | `create-cmp clean` | Cache & build-output hygiene: stale `~/.konan` toolchains + project `build/`/`.gradle/` dirs (sizes shown, consent-gated); `~/.gradle/caches` is size-reported only. |
88
- | `create-cmp verify [--target-dir .]` | Run the green-build gate (Android, and iOS on macOS) against an existing project. |
89
-
90
- > **North-star (a goal, measured honestly — not a benchmark):** *time-to-green* — a running app on
91
- > the Android emulator **and** the iOS simulator, smoke-passing, with zero manual steps modulo the
92
- > one Xcode App Store install. Target: **under 5 minutes on a clean machine.** Every scaffold ends
93
- > with a `--verify` build gate, so success is *proven*, not assumed.
94
-
95
- ## What you get (the pre-solved moat)
96
-
97
- Everything below is **stamped from a template that builds green on Android + iOS** — not regenerated
98
- per run, so it can't silently drift:
99
-
100
- - **A pinned, version-locked dependency set** — Kotlin, KSP, Compose Multiplatform, Room, AGP, Koin,
101
- Ktor, GitLive Firebase KMP, Navigation Compose, Lifecycle — chosen to actually agree with each
102
- other, including the iOS Room/KSP2 catch-22 (`ksp.useKSP2=true`). Frozen and CI-gated.
103
- - **The iOS shell, in the order that builds** — `project.yml` (XcodeGen), `Podfile` (Firebase static
104
- linkage), `Info.plist`, `iOSApp.swift` (AppDelegate + `FirebaseApp.configure()` before Koin),
105
- `ContentView.swift` (ComposeUIViewController bridge).
106
- - **The Android shell** — `AndroidManifest`, `MainActivity` with `enableEdgeToEdge()`, `Application`
107
- starting Koin + Room, adaptive-icon structure.
108
- - **Navigation + insets, solved once** — a generic `BaseScreen` Scaffold owning window insets /
109
- status- and nav-bar padding, plus a data-driven `AppShell` + bottom nav fed by your `tabs` list
110
- (not role-hardcoded), with type-safe routes.
111
- - **Clean Architecture, wired** — `core / data{local,remote} / domain{model,repository,usecase} /
112
- presentation{components,theme,navigation,<feature>} / di` with Koin modules registered and **one
113
- example feature wired end-to-end** as the copy-paste pattern.
114
- - **Theme & tokens** — `<Prefix>Theme`, `<Prefix>Tokens`, `<Prefix>Colors`, DM Sans.
115
- - **The test pyramid, pre-built** — unit exemplars (Turbine + fakes), dependency-free architecture
116
- conformance gates, Compose UI Tests, golden-tree structural baselines, and Maestro E2E flows —
117
- bound to a committed behavior spec (`specs/*.spec.md`, Given/When/Then, stable clause ids) so a
118
- `specCoverage` gate catches untested or untraceable behavior — plus the verify lane
119
- (`qa/verify.mjs`) that runs it all and emits a typed PASS/FAIL/SKIP verdict + evidence receipt.
120
- - **Extend it with Claude Code, no plugin required** — every generated project ships its own
121
- `add-feature`/`add-screen`/`add-repository` skills (backed by the deterministic
122
- `qa/scaffold-feature.mjs` stamper) that clone the exemplar feature end-to-end: Screen →
123
- ViewModel → UseCase → Repository → DI → nav, tests at every layer, golden baseline included.
124
- - **Mechanically enforced, not honor-system** — a generated `.claude/settings.json` Stop hook
125
- refuses "done" unless a fresh, evidence-bound PASS receipt covers the changed surface (content-
126
- hash bound, rebase/merge-proof), and CI independently re-checks the committed receipt still
127
- attests `HEAD`. Proven against real violations: hardcoded colors, illegal layer imports, deleted
128
- spec tests, and structural regressions are each refused and named by clause, 4/4 in the
129
- repo's refusal-demo script.
130
- - **A toolchain doctor** — diagnoses *and* heals JDK, Android SDK + AVD, Xcode/CLT, CocoaPods,
131
- XcodeGen, Appium + drivers (the legacy pre-Maestro path), Node. Idempotent, OS-aware, consent-gated.
132
- The E2E flows themselves run on Maestro — install with
133
- `curl -fsSL https://get.maestro.mobile.dev | bash`.
134
- - **AI-inspectable by default** — every generated app ships a debug-only live inspector
135
- (`127.0.0.1:9500`, loopback, structurally absent from release). An agent runs `connect_live` and
136
- reads the running UI as JSON — hierarchy, geometry, resolved design tokens, real navigation
137
- state. Never screenshots. Toggle with `--no-inspector`; `create-cmp doctor` verifies it stays
138
- debug-only and that every declared theme token is served on `/inspect/design-system`.
139
- - **Live device view** — with the debug app running, open `http://127.0.0.1:9500/inspect/remote`
140
- (after `adb forward tcp:9500 tcp:9500`) to watch the real device and click the image to tap it,
141
- while the agent drives `navigate_and_inspect` on the semantics tree and proves every change with
142
- `prove_change`. Pixels for the human, structure for the AI.
143
- - **Desktop dev-client** — a phone-sized JVM window running the shared UI with Compose Hot Reload
144
- attached (`./gradlew :composeApp:hotRunDesktop --auto`); Firebase never initializes on desktop
145
- (offline DI fakes). Toggle with `--no-dev-client`.
146
-
147
- `google-services.json` / `GoogleService-Info.plist` are intentionally **not** real — you get
148
- placeholders and clear "drop your Firebase config here" instructions.
74
+ # The features, one by one
149
75
 
150
- ## Options
76
+ Three surfaces: the **CLI**, the **Claude Code plugin**, and — most importantly — **what every
77
+ generated project carries inside it**.
151
78
 
152
- | Option | Choices | Default |
153
- |---|---|---|
154
- | Platforms | Android (always) + iOS | iOS on |
155
- | App name / package / iOS bundle id | — | required / derived |
156
- | Firebase region | any Firebase region | `us-central1` |
157
- | Firebase (GitLive KMP) | on / off | on |
158
- | Auth | `email` / `phone` / `both` / `none` | `both` |
159
- | Firebase services | Firestore · Storage · Functions · FCM | all on |
160
- | Room local cache | on / off | on |
161
- | E2E flows (Maestro) | on / off | on |
162
- | Bottom-nav tabs | label + icon, any count | Home, Profile |
79
+ ## The CLI (5 commands)
163
80
 
164
- _(Web/PWA is intentionally out of scope Android + iOS only.)_
81
+ Everything except `create` works on **any** KMP project, not just ones this tool made.
165
82
 
166
- ## Use it from Claude Code
83
+ | Command | Plain-speech: what it does |
84
+ |---|---|
85
+ | `create-cmp [dir]` | Makes a new app. Asks questions (or takes flags), stamps the template, renames everything to your package, removes features you turned off, builds it, tells you GREEN or FAIL. |
86
+ | `create-cmp doctor [--fix]` | Checks your machine (JDK, Android SDK, emulator, Xcode, CocoaPods, XcodeGen, Node) **and** your project (do Kotlin and KSP versions agree? is the version catalog drifting? is `~/.konan` eating your disk?). `--fix` applies safe repairs — always asking first. |
87
+ | `create-cmp upgrade [--dry-run]` | Moves your `libs.versions.toml` to the next **proven-green** version set. Shows a diff first, edits surgically with backups, guards the Kotlin↔KSP lockstep, and can re-verify the build after. |
88
+ | `create-cmp clean` | Reclaims disk: stale Kotlin/Native toolchains, `build/` dirs. Shows sizes, asks before deleting. |
89
+ | `create-cmp verify` | Runs the green-build gate against an existing project. Exit 0 = green. Useful in scripts and CI. |
167
90
 
168
- `create-cmp` also ships as a [Claude Code](https://docs.claude.com/en/docs/claude-code) plugin one
169
- shared engine, two front doors. Install from the bundled marketplace:
91
+ ## The Claude Code plugin (8 skills)
170
92
 
171
93
  ```text
172
94
  /plugin marketplace add kvdm-co-pilot/create-cmp
173
95
  /plugin install create-cmp
174
96
  ```
175
97
 
176
- No `/plugin` UI available (headless/non-interactive sessions)? See
177
- [the settings.json fallback in `docs/USAGE.md`](./docs/USAGE.md#2-setup) — it's a real two-step
178
- install (marketplace registration, then plugin install), not a one-line tweak.
179
-
180
- It bundles eight skills (plus the `cmp-inspector` MCP server), each with a deterministic engine
181
- behind it:
182
-
183
- - **cmp-new** — conversational interview, then shells out to the same engine to scaffold, and
184
- generates your tab screens from the example-feature pattern.
185
- - **cmp-doctor** — toolchain bootstrap + project diagnosis on any KMP project (consent-gated).
186
- - **cmp-upgrade** — migrate to the next proven-green version set (diff → apply → verify).
187
- - **cmp-inspect** — see and drive a rendered Compose UI as structured JSON: hierarchy, geometry,
188
- resolved design tokens, drift diffs, golden-tree snapshots, a11y audit, live navigation
189
- (`navigate_and_inspect`), wireframe rendering, and `prove_change` — the verified dev loop.
190
- - **cmp-dev-client** — run the shared UI in a phone-sized desktop window with Compose Hot Reload
191
- (`./gradlew :composeApp:hotRunDesktop --auto`) — the Expo-style dev loop for KMP.
192
- - **cmp-firebase-connect** — post-scaffold onboarding: create/reuse a Firebase project, register
193
- the app, and replace the placeholder config via the Firebase CLI (consent-gated), proven by a
194
- green build.
195
- - **cmp-test** — generate the regression suite by *observing* the app: read the running UI's
196
- semantics tree and emit Maestro E2E flows plus golden-tree snapshots in the shipped harness style.
197
- - **cmp-qa-prep** — brings up the emulator + Maestro flow run + smoke (legacy Appium bring-up
198
- path also supported).
98
+ Same engine as the CLI, conversational front door. Each skill is a guided flow, not a wrapper:
199
99
 
200
- ## Why CMP, not React Native
100
+ | Skill | Plain-speech: what it does |
101
+ |---|---|
102
+ | `cmp-new` | "Make me an app." Interviews you in chat, scaffolds via the engine, generates your bottom-nav tab screens from the exemplar pattern, proves the build green. |
103
+ | `cmp-doctor` | "Why won't my KMP project build?" Runs the doctor, explains the findings, applies consented fixes. |
104
+ | `cmp-upgrade` | "Bump my dependencies safely." Diff → apply → verify, with the lockstep guardrails. |
105
+ | `cmp-inspect` | "What did the UI actually render?" Reads a **running** app as structured JSON — hierarchy, geometry, resolved design tokens, navigation state. Never screenshots. Can assert tokens, find drift against your design system, audit accessibility, diff before/after. |
106
+ | `cmp-dev-client` | "Let me iterate fast." Runs your shared UI in a phone-sized desktop window with hot reload — save a file, see it change. No emulator needed. Firebase stays off on desktop (offline fakes). |
107
+ | `cmp-firebase-connect` | "Wire up my real Firebase." Drives the Firebase CLI: create/reuse a project, register the app, drop the real `google-services.json` over the placeholder, prove it with a green build. Every cloud action asks first. |
108
+ | `cmp-test` | "Write tests for my app." *Observes* the running app's semantics tree — what's actually on screen, what's tappable, where navigation goes — and derives the regression suite from that. Tests come from rendered reality, not guesses. |
109
+ | `cmp-qa-prep` | "Get my test environment up." Emulator + app install + E2E smoke run, with the gotchas handled. |
201
110
 
202
- This isn't a knock on React Nativeit's about defaults. The only place CMP loses to RN on a new
203
- app is **time-to-first-green-build**, and that's a tooling problem, not a merits problem. With
204
- `create-cmp`, CMP's `npx`-and-go is competitive:
111
+ Plus the **`cmp-inspector` MCP server** (14 tools)the machine-readable window into a running
112
+ Compose UI that `cmp-inspect`, `cmp-test`, and the verified dev loop are built on. One tree
113
+ contract, three sources: render a screen headlessly, connect to the live app, or read a device
114
+ via UIAutomator.
205
115
 
206
- - **One language, real native UI.** Kotlin shared logic *and* Compose UI across Android and iOS — no
207
- JS bridge, no separate native-module dance for the common case.
208
- - **Native performance and platform access** without the RN bridge tax.
209
- - **Reproducible by construction.** A frozen, version-locked, CI-gated template means the build that
210
- was green yesterday is green today — the exact property ad-hoc CMP setups lack.
211
- - **Proven, not assumed.** Every scaffold ends on the verify lane — real build + the full JVM
212
- test tier — with a typed GREEN/FAIL verdict and an evidence receipt.
116
+ ## What every generated project carries (the harness itself)
213
117
 
214
- If `create-next-app` made React the default for the web by removing setup friction, the goal here is
215
- the same for multiplatform mobile.
118
+ This is the product. Delete the plugin, uninstall the CLI your generated repo keeps all of it.
216
119
 
217
- ## How it works
120
+ ### 1. Specs — behavior is written down first
121
+ `specs/*.spec.md` — plain Given/When/Then clauses with stable ids (`HOME-01`, `ARCH-05`). New
122
+ behavior starts as a clause; durable tests cite their clause (`// SPEC: HOME-02`). The `home`
123
+ feature ships as the fully-cited example.
218
124
 
219
- ```
220
- Front doors: npx create-cmp-cli + Claude Code plugin (cmp-new / cmp-doctor / cmp-qa-prep)
221
- │ one shared engine, two front doors
222
- Engine (Node, deterministic): copy → token-replace rename packages → toggle features → VERIFY
223
- Golden template (frozen, CI'd): pinned versions · iOS shell · nav+insets · Clean Arch · DI · test pyramid
224
- ```
125
+ ### 2. The verify lane — one command, eight gates
126
+ `node qa/verify.mjs` runs everything and writes a typed PASS/FAIL/SKIP receipt:
127
+
128
+ | Gate | Plain-speech: what it catches |
129
+ |---|---|
130
+ | `specCoverage` | Behavior nobody tests, and test citations pointing at nothing. Every clause needs a test; every citation needs a clause. |
131
+ | `build` | The app doesn't compile. |
132
+ | `unitTests` | A behavior broke. ViewModels/UseCases/Repositories, tested with hand-written fakes. |
133
+ | `conformance` | Architecture violations, **named by rule**: UI importing the data layer, hardcoded colors outside the theme, a screen without a ViewModel test. |
134
+ | `goldenTrees` | A screen's *structure* changed when you didn't mean it to. Compares the rendered semantics tree against a committed baseline — no pixels, no flake. |
135
+ | `tokenDrift` | The running app's design tokens drifting from the declared catalog — queried live from the debug inspector. Hardcode a color and it shows up here too. |
136
+ | `a11y` | Missing content descriptions, undersized touch targets. |
137
+ | `e2eSmoke` | The app doesn't actually boot and navigate on a device. Real Maestro run, hardened for slow emulators. |
138
+
139
+ No device attached? Device-dependent gates record an honest **SKIP** — never a fake green.
140
+
141
+ ### 3. Evidence — receipts you can't forge
142
+ The lane writes `qa/evidence/latest.json`: verdict, per-gate results, durations, and an
143
+ `inputs.hash` — a content hash of every file that could affect the verdict. You commit the
144
+ receipt with your change; git history becomes the audit ledger. Because validity is a *content*
145
+ hash (not a commit SHA), rebases and merges don't invalidate honest receipts — but editing the
146
+ verdict by hand, or reusing a stale receipt, fails immediately. The lane also forces test
147
+ *execution* (`--rerun`), so a receipt can never launder a cached result from a different tree.
148
+
149
+ ### 4. Enforcement — "done" is mechanical, not honor-system
150
+ - **Stop hook** (`.claude/settings.json`): when an AI session tries to end, it re-hashes the
151
+ verified surface and compares against the committed receipt. Changed code without a fresh PASS
152
+ receipt → the session is blocked, with the reason. Costs milliseconds (hashing only). Doc-only
153
+ edits never trigger it — enforcement is transparent, not hostile.
154
+ - **CI receipt gate** (`.github/workflows/verify.yml`): every push re-checks that the committed
155
+ receipt attests `HEAD`, then independently re-runs the whole lane.
156
+ - **The refusal demo** (`node qa/refusal-demo.mjs`): four staged violations — hardcoded color,
157
+ illegal layer import, deleted spec test, structural regression — each caught and **named by
158
+ clause**, 4/4. Run it to watch the harness say no.
159
+
160
+ ### 5. In-project generators — extend without the plugin
161
+ Three skills ship *inside* the generated repo (`.claude/skills/`), backed by a deterministic
162
+ stamper (`qa/scaffold-feature.mjs`):
163
+
164
+ - **`add-feature`** — a full vertical slice cloned from the `home` exemplar: Screen → ViewModel →
165
+ UseCase → Repository → DI → nav route, **with tests at every layer** and a golden baseline slot.
166
+ - **`add-screen`** — presentation only, for an entity whose data layer already exists.
167
+ - **`add-repository`** — data/domain only: model, repository interface + impl, use case, fake.
168
+
169
+ Any plain Claude Code session — no create-cmp plugin installed — finds these and extends the app
170
+ correctly by construction.
171
+
172
+ ### 6. The live inspector — AI-readable UI
173
+ Every debug build serves `127.0.0.1:9500` (loopback-only, structurally absent from release): the
174
+ UI tree as JSON, the design-token catalog, a screenshot route, a tap route, and a live device
175
+ view for humans (`/inspect/remote` — watch the real device in a browser, click to tap). Agents
176
+ read structure; humans see pixels.
177
+
178
+ ### 7. The daily-driver extras
179
+ - **Desktop dev-client** — shared UI in a phone-sized JVM window, Compose Hot Reload attached.
180
+ - **CI workflow** — Android job on every push; iOS job ready to un-comment.
181
+ - **`CLAUDE.md`** — the AI delivery contract itself, stating everything above as rules any AI
182
+ session in the repo must follow.
183
+
184
+ ---
185
+
186
+ # Workflows — how it fits together
187
+
188
+ **New app → green.** `cmp-new` (or `npx create-cmp-cli`) → interview → stamp → green build proven
189
+ → tab screens generated. Then `cmp-firebase-connect` to wire your real backend.
190
+
191
+ **The daily UI loop.** `./gradlew :composeApp:hotRunDesktop --auto` → edit Compose → save → see
192
+ it. No emulator, no Firebase, sub-second feedback.
193
+
194
+ **The verified dev loop (the flagship).** For any UI change: snapshot the live tree → make the
195
+ edit → reload → `prove_change` compares before/after structure, token drift, and a11y, and returns
196
+ a verdict. The agent doesn't say "I centered the title" — it shows *"title bounds moved, tokens
197
+ unchanged, no a11y regressions: proven clean."*
198
+
199
+ **Add a feature with AI (no plugin).** Ask any Claude Code session for a feature → it reads
200
+ `CLAUDE.md` → proposes the spec clause first → runs `add-feature` → runs the lane → commits code
201
+ + receipt together. If it violates the architecture, the gates name the broken rule; if it tries
202
+ to stop early, the Stop hook blocks it.
203
+
204
+ **Tests that write themselves.** `cmp-test` reads the running app's semantics tree and emits the
205
+ regression suite — existence, interaction, navigation, golden trees — in the shipped harness style.
206
+
207
+ **Maintenance, for the life of the repo.** `doctor` when anything misbehaves, `upgrade` when you
208
+ want newer versions without the version-matrix gamble, `clean` when disk fills, `verify` as the
209
+ standalone gate. All of it works on any KMP project.
210
+
211
+ ## Agent flows — who does what
212
+
213
+ - **A plain AI session** in a generated repo is the common case: the contract (`CLAUDE.md`), the
214
+ generators, the lane, and the hook are all local files — the session follows the loop above
215
+ with nothing installed.
216
+ - **The `cmp-orchestrator` agent** (ships with the plugin) splits bigger jobs: it delegates
217
+ generation and mechanical work to sub-agents with self-contained briefs, then **gates every
218
+ hand-off through the verify lane** before accepting it. Nothing is reported done on prose —
219
+ only on a receipt.
220
+ - **The MCP tools** are how any agent *sees*: `inspect_tree`, `get_node`, `assert_token`,
221
+ `layout_gaps`, `diff_against_design_system`, `find_drift`, `snapshot_save`, `snapshot_diff`,
222
+ `audit_a11y`, `connect_live`, `navigate_and_inspect`, `render_tree`, `render_screen`,
223
+ `prove_change`. Structure in, structure out — never pixels in model context.
224
+
225
+ ## The philosophy (why it's built this way)
226
+
227
+ 1. **Stamp, don't generate.** The skeleton comes from a frozen, CI-verified template. LLMs are
228
+ never in the hot path for code that must be identical every time. Determinism is the moat.
229
+ 2. **Evidence over claims.** "It works" is a claim. A committed receipt from an executed lane is
230
+ evidence. The whole harness exists to convert one into the other.
231
+ 3. **Specs before code.** If behavior isn't written as a clause, the coverage gate calls it
232
+ untested. New behavior starts in `specs/`, not in a diff.
233
+ 4. **Exemplars over documentation.** The `home` feature *is* the architecture guide — a running,
234
+ tested pattern that generators clone and humans copy. Patterns you can execute don't rot.
235
+ 5. **Refusal is a feature.** A green checkmark is cheap. A red one that names the violated clause
236
+ is what makes the green one mean something. The refusal demo is part of the product.
237
+ 6. **Honest SKIPs.** No device → the gate says SKIP, visibly, in the receipt. Green-with-gaps
238
+ presented as fully verified is treated as a bug — in the harness itself.
239
+ 7. **Structure, not pixels.** Golden trees, token assertions, and semantic diffs instead of
240
+ screenshot comparisons: platform-stable, flake-free, and machine-readable.
241
+ 8. **The contract lives in the project.** Everything enforcing correctness ships in the generated
242
+ repo, not the tool. Your repo stays verifiable after the tool is gone.
243
+ 9. **Enforcement must be cheap and fair.** The hook hashes files in milliseconds; doc edits never
244
+ invalidate evidence; rebases don't force re-runs. Gates that punish honest work get disabled —
245
+ so they're designed not to.
246
+ 10. **Dogfood in public.** The [showcase](https://github.com/kvdm-co-pilot/create-cmp-showcase) is
247
+ rebuilt from the published package, receipts and refusals included. Two of the last three
248
+ releases fixed bugs the dogfooding itself caught — the harness catching its own tool is the
249
+ system working.
250
+
251
+ ---
252
+
253
+ ## Options
254
+
255
+ | Option | Choices | Default |
256
+ |---|---|---|
257
+ | Platforms | Android (always) + iOS | iOS on |
258
+ | App name / package / iOS bundle id | — | required / derived |
259
+ | Firebase (GitLive KMP) | on / off | on |
260
+ | Auth | `email` / `phone` / `both` / `none` | `both` |
261
+ | Firebase region + services | any region · Firestore/Storage/Functions/FCM | `us-central1` · all on |
262
+ | Room local cache | on / off | on |
263
+ | E2E flows (Maestro) | on / off (`--e2e` / `--no-e2e`; `--appium` is a deprecated alias) | on |
264
+ | Live inspector | on / off | on |
265
+ | Desktop dev-client | on / off | on |
266
+ | Bottom-nav tabs | label + icon, any count | Home, Profile |
225
267
 
226
- The engine never puts an LLM in the hot path: it copies the template, replaces placeholders in file
227
- contents *and* paths, atomically renames package directories, toggles features (Firebase / auth type
228
- / Room / the e2e harness — feature key `e2e`, renamed from `appium` in 0.3.0; the old
229
- `--no-appium` flag still works as a deprecated alias), and runs the verify
230
- build. Determinism is the moat. See
231
- [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) for the full design, and
232
- [`docs/DOCUMENTATION.md`](./docs/DOCUMENTATION.md) for the map of every doc — what's
233
- authoritative for what, and the standards each implements.
268
+ _Web/PWA is intentionally out of scope Android + iOS only._
234
269
 
235
270
  ## Requirements
236
271
 
237
- - **Node.js ≥ 18** to run the scaffolder.
238
- - **macOS** for iOS output (Xcode, CocoaPods, XcodeGen). Android output works on macOS or Linux.
239
- - Everything else JDK 17, Android SDK + emulator, CocoaPods, XcodeGen, Appium + drivers (legacy
240
- e2e path)the built-in `doctor` detects and (with your consent) installs. The E2E flows
241
- themselves run on Maestro, installed separately with
242
- `curl -fsSL https://get.maestro.mobile.dev | bash`. Xcode itself must be installed from the App
243
- Store; the doctor surfaces that as the one manual step.
244
-
245
- ## Roadmap
246
-
247
- The full plan six pillars from scaffold to store release, and the sequencing — lives in
248
- [`docs/ROADMAP.md`](./docs/ROADMAP.md). The AI delivery harness itself (exemplars conformance
249
- gates → in-project generation → mechanical enforcement → observability) is **complete** — see
250
- [`docs/HARNESS-PLAN.md`](./docs/HARNESS-PLAN.md) for the per-layer detail. Near-term:
251
-
252
- - [x] Publish to npm as `create-cmp-cli` (`npx create-cmp-cli@latest`) **live on the registry**;
253
- `0.3.0` (the harness release) is prepared and pending its own publish.
254
- - [x] AI-native Compose inspector read a running app's hierarchy, geometry, and *resolved design
255
- tokens* as structured JSON (no screenshots), including **live on-device inspection** over a
256
- loopback debug server. See [`docs/INSPECTOR-PLAN.md`](./docs/INSPECTOR-PLAN.md).
257
- - [x] Android build matrix in CI on every push; iOS matrix runs on manual dispatch.
258
- - [ ] Publish `create-cmp-cli@0.3.0` to npm (needs an interactive OTP session) + record the
259
- asciinema demo (scaffold → `add-feature` → gate refuses a violation → fix → PASS).
260
- - [ ] More example features and nav shapes.
272
+ - **Node.js ≥ 18** for the tool itself.
273
+ - **macOS** for iOS output; Android works on macOS or Linux.
274
+ - Everything else (JDK 17, Android SDK + emulator, CocoaPods, XcodeGen) the `doctor` detects and —
275
+ with consentinstalls. Maestro installs with `curl -fsSL https://get.maestro.mobile.dev | bash`.
276
+ Xcode itself is the one manual App Store step.
277
+
278
+ ## Why CMP, not React Native
279
+
280
+ The only place CMP loses to RN on a new app is time-to-first-green-build — a tooling problem, not
281
+ a merits problem. With one language, real native UI, no bridge, and a reproducible frozen
282
+ template, CMP's `npx`-and-go is now competitive. If `create-next-app` made React the web default
283
+ by deleting setup friction, the goal here is the same for multiplatform mobile.
284
+
285
+ ## Docs
286
+
287
+ [`docs/USAGE.md`](./docs/USAGE.md) the complete usage guide (every command, skill, MCP tool,
288
+ workflow) · [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) engine design ·
289
+ [`docs/HARNESS-PLAN.md`](./docs/HARNESS-PLAN.md)the harness, layer by layer ·
290
+ [`docs/adr/`](./docs/adr/) decision records · [`docs/ROADMAP.md`](./docs/ROADMAP.md) what's next.
261
291
 
262
292
  ## Contributing
263
293
 
264
- Issues and PRs welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md) and our
294
+ Issues and PRs welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md) and the
265
295
  [Code of Conduct](./CODE_OF_CONDUCT.md). The golden template is CI-gated: an upstream version bump
266
- must fail CI, not your generated project.
296
+ must fail our CI, not your generated project.
267
297
 
268
298
  ## License
269
299
 
@@ -63,7 +63,9 @@ async function main() {
63
63
 
64
64
  function printHelp() {
65
65
  process.stdout.write(
66
- `create-cmp — scaffold & maintain Kotlin/Compose Multiplatform apps (Android + iOS)\n\n` +
66
+ `create-cmp — the AI delivery harness for Kotlin/Compose Multiplatform (Android + iOS)\n` +
67
+ `Scaffolds a green-building app in minutes; every generated project carries a spec-driven\n` +
68
+ `verify lane, evidence receipts, and mechanical enforcement of "done".\n\n` +
67
69
  `Usage:\n` +
68
70
  ` npx create-cmp [target-dir] [flags] scaffold a new app (default command)\n` +
69
71
  ` npx create-cmp create [target-dir] same, explicit\n` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-cmp-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "The AI delivery harness for Kotlin/Compose Multiplatform — scaffolds a green-building app (Android + iOS) in minutes and holds AI-driven changes to a machine-enforced verify lane with a committed evidence receipt. Installs the `create-cmp` command.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/scaffold.mjs CHANGED
@@ -181,9 +181,8 @@ function writeLocalProperties(projectDir) {
181
181
 
182
182
  /**
183
183
  * Replace the (already token-substituted) app display name with a slugified,
184
- * identifier-safe form in the few files that demand it: Gradle's
185
- * rootProject.name and the appium npm package name. Idempotent and a no-op when
186
- * the name is already slug-safe.
184
+ * identifier-safe form in the one file that demands it: Gradle's
185
+ * rootProject.name. Idempotent and a no-op when the name is already slug-safe.
187
186
  * @param {string} projectDir
188
187
  * @param {string} appName raw display name (may contain spaces)
189
188
  */
@@ -201,20 +200,6 @@ function applyAppNameSlug(projectDir, appName) {
201
200
  );
202
201
  if (out !== src) fs.writeFileSync(settings, out);
203
202
  }
204
-
205
- // qa/appium/package.json: "name": "Demo App-appium" -> "demo-app-appium"
206
- const pkg = path.join(projectDir, "qa", "appium", "package.json");
207
- if (fs.existsSync(pkg)) {
208
- try {
209
- const json = JSON.parse(fs.readFileSync(pkg, "utf8"));
210
- if (typeof json.name === "string" && json.name.includes(appName)) {
211
- json.name = json.name.split(appName).join(slug).toLowerCase();
212
- fs.writeFileSync(pkg, `${JSON.stringify(json, null, 2)}\n`);
213
- }
214
- } catch {
215
- // leave as-is on parse failure
216
- }
217
- }
218
203
  }
219
204
 
220
205
  /**
@@ -262,3 +262,16 @@ compose.desktop {
262
262
  }
263
263
  }
264
264
  // <<< cmp:feature dev-client
265
+
266
+ // Evidence integrity: golden-tree baselines (qa/golden) and the UPDATE_GOLDEN capture flag are
267
+ // REAL inputs of the JVM test tier, but Gradle can't see either on its own — baselines are read
268
+ // at runtime, not compiled, and env vars aren't tracked. Undeclared, the build cache will happily
269
+ // replay a PASS from a tree whose baselines differed (or serve an UPDATE_GOLDEN capture run from
270
+ // cache so it never writes the baseline at all). Declaring them makes caching honest; the verify
271
+ // lane additionally forces `--rerun` so evidence receipts always attest actual execution.
272
+ tasks.withType<Test>().configureEach {
273
+ inputs.files(fileTree(rootProject.layout.projectDirectory.dir("qa/golden")) { include("*.json") })
274
+ .withPropertyName("goldenBaselines")
275
+ .withPathSensitivity(PathSensitivity.RELATIVE)
276
+ inputs.property("updateGolden", System.getenv("UPDATE_GOLDEN") ?: "")
277
+ }
@@ -114,6 +114,10 @@ private fun AppBottomNav(
114
114
  }
115
115
  }
116
116
 
117
+ /** Deterministic automation tag for a nav item: `nav_` + the label lowercased with every non-[a-z0-9] run collapsed to `_` and trimmed (e.g. "My Stuff!" → `nav_my_stuff`). Mirrored by the id selectors in `qa/e2e/smoke.yaml` — keep the two in sync. */
118
+ private fun navItemTag(label: String): String =
119
+ "nav_" + label.lowercase().replace(Regex("[^a-z0-9]+"), "_").trim('_')
120
+
117
121
  @Composable
118
122
  private fun NavItem(
119
123
  label: String,
@@ -128,6 +132,8 @@ private fun NavItem(
128
132
  // a11y: guarantee the 48dp minimum touch target regardless of label width
129
133
  // (the inspector's audit_a11y flags anything smaller).
130
134
  .defaultMinSize(minWidth = 48.dp, minHeight = 48.dp)
135
+ // Durable selection handle (tests/E2E select by testTag, never display text).
136
+ .semantics { testTag = navItemTag(label) }
131
137
  .padding(horizontal = 8.dp, vertical = 4.dp),
132
138
  horizontalAlignment = Alignment.CenterHorizontally,
133
139
  verticalArrangement = Arrangement.Center,
@@ -11,7 +11,6 @@ import androidx.compose.ui.test.ExperimentalTestApi
11
11
  import androidx.compose.ui.test.assertIsDisplayed
12
12
  import androidx.compose.ui.test.hasTestTag
13
13
  import androidx.compose.ui.test.onNodeWithTag
14
- import androidx.compose.ui.test.onNodeWithText
15
14
  import androidx.compose.ui.test.performClick
16
15
  import androidx.compose.ui.test.runComposeUiTest
17
16
  import __PACKAGE__.testing.awaitNode
@@ -66,12 +65,14 @@ class AppShellTest {
66
65
 
67
66
  awaitNode(hasTestTag("tab_one_content"))
68
67
 
69
- onNodeWithText("Tab Two").performClick()
68
+ // Nav items carry a derived nav_<label-slug> tag (AppShell's navItemTag)
69
+ // select by it, never by display text.
70
+ onNodeWithTag("nav_tab_two").performClick()
70
71
  awaitNode(hasTestTag("tab_two_content"))
71
72
  onNodeWithTag("tab_two_content").assertIsDisplayed()
72
73
  onNodeWithTag("app_bottom_nav").assertIsDisplayed()
73
74
 
74
- onNodeWithText("Tab One").performClick()
75
+ onNodeWithTag("nav_tab_one").performClick()
75
76
  awaitNode(hasTestTag("tab_one_content"))
76
77
  onNodeWithTag("tab_one_content").assertIsDisplayed()
77
78
  onNodeWithTag("app_bottom_nav").assertIsDisplayed()
@@ -2,7 +2,10 @@
2
2
  .gradle
3
3
  /local.properties
4
4
  .DS_Store
5
- /build
5
+ # Gradle build outputs — unanchored so module dirs (composeApp/build/, …) match too,
6
+ # not just a root-level build/. Anchored `/build` misses composeApp/build/, which would
7
+ # then get committed and destabilise the evidence receipt's inputs hash.
8
+ build/
6
9
  /captures
7
10
  .externalNativeBuild
8
11
  .cxx
@@ -2,7 +2,9 @@
2
2
  #
3
3
  # Proves the real app boots on a device/emulator and the bottom-nav shell works.
4
4
  # Selectors go by testTag (surfaced as resource-ids on Android via TestTagAutomation),
5
- # falling back to visible text only for content assertions.
5
+ # never by display text. Nav-item ids are nav_<label-slug> — the slug rule lives in
6
+ # AppShell.kt's navItemTag (lowercase, non-[a-z0-9] runs collapsed to "_", trimmed);
7
+ # keep these ids in sync with it if the configured tabs change.
6
8
  #
7
9
  # Run: maestro test qa/e2e/smoke.yaml (device/emulator attached)
8
10
  # The verify lane's e2eSmoke step runs this automatically when maestro + a device are present.
@@ -23,13 +25,15 @@ appId: __PACKAGE__
23
25
  id: "app_bottom_nav"
24
26
 
25
27
  # SPEC: SHELL-02 — switching tabs keeps the shell
26
- - tapOn: "Profile"
28
+ - tapOn:
29
+ id: "nav_profile"
27
30
  - assertVisible:
28
31
  id: "profile_title"
29
32
  - assertVisible:
30
33
  id: "app_bottom_nav"
31
34
 
32
35
  # and back
33
- - tapOn: "Home"
36
+ - tapOn:
37
+ id: "nav_home"
34
38
  - assertVisible:
35
39
  id: "home_title"
@@ -33,9 +33,16 @@ function isExcluded(relPath) {
33
33
  return EXCLUDED_PREFIXES.some((prefix) => relPath === prefix || relPath.startsWith(`${prefix}/`));
34
34
  }
35
35
 
36
+ // The verified surface is the set of files that WILL be committed: tracked files
37
+ // PLUS untracked-but-not-ignored files (`--others --exclude-standard`). A freshly
38
+ // generated feature's files are untracked when the lane runs and the receipt is
39
+ // written, yet they land in the very same commit as the receipt — so they must be
40
+ // hashed, or the committed receipt would never attest its own commit (and CI's
41
+ // receipt-matches-HEAD gate would false-fail on every change). Gitignored scratch
42
+ // (build outputs, qa-artifacts) is still excluded via --exclude-standard.
36
43
  function tryGitLsFiles(root) {
37
44
  try {
38
- const out = execSync("git ls-files -z", { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
45
+ const out = execSync("git ls-files -z --cached --others --exclude-standard", { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
39
46
  return out.split("\0").filter(Boolean);
40
47
  } catch {
41
48
  return null;
@@ -27,7 +27,14 @@ import { fileURLToPath } from "node:url";
27
27
 
28
28
  const TEMPLATE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
29
29
  const REPO_ROOT = path.resolve(TEMPLATE_ROOT, "..");
30
- const CREATE_CMP_BIN = path.join(REPO_ROOT, "bin", "create-cmp.mjs");
30
+ // Scaffolding the throwaway app: inside the create-cmp dev tree the engine sits at
31
+ // ../bin relative to the template — use it directly (fast, offline, tests the local
32
+ // code). In a real generated repo that path doesn't exist, so fall back to the
33
+ // published CLI via npx (needs network on first run).
34
+ const LOCAL_ENGINE = path.join(REPO_ROOT, "bin", "create-cmp.mjs");
35
+ const SCAFFOLD_CMD = fs.existsSync(LOCAL_ENGINE)
36
+ ? `node "${LOCAL_ENGINE}"`
37
+ : "npx --yes create-cmp-cli@latest";
31
38
  const GRADLEW = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
32
39
 
33
40
  function log(msg) {
@@ -57,7 +64,7 @@ function scaffold() {
57
64
  log(`Scaffolding a throwaway app at ${projectDir} (never touching ${TEMPLATE_ROOT})…`);
58
65
 
59
66
  const res = sh(
60
- `node "${CREATE_CMP_BIN}" "${projectDir}" --name RefusalDemo --package com.example.refusaldemo --no-ios --yes --no-verify`,
67
+ `${SCAFFOLD_CMD} "${projectDir}" --name RefusalDemo --package com.example.refusaldemo --no-ios --yes --no-verify`,
61
68
  REPO_ROOT,
62
69
  { timeout: 5 * 60_000 },
63
70
  );
@@ -187,11 +187,11 @@ function stepBuild() {
187
187
  }
188
188
 
189
189
  // Runs a filtered slice of the JVM test tier and names the verdict after the gate it proves.
190
- // The full suite already ran in unitTests; these re-runs are cheap (compiled, cached) and give
191
- // each gate its own named verdict + failure text in the receipt.
190
+ // The full suite already ran in unitTests; the filtered slices stay cheap (compilation is
191
+ // cached) while `--rerun` forces the tests themselves to EXECUTE see stepUnitTests.
192
192
  function gradleTestStep(name, testsFilter, failHint) {
193
193
  return () => {
194
- const res = sh(`${GRADLEW} :composeApp:desktopTest --tests "${testsFilter}" --console=plain`);
194
+ const res = sh(`${GRADLEW} :composeApp:desktopTest --rerun --tests "${testsFilter}" --console=plain`);
195
195
  return {
196
196
  name,
197
197
  verdict: res.ok ? "PASS" : "FAIL",
@@ -204,7 +204,12 @@ function gradleTestStep(name, testsFilter, failHint) {
204
204
  }
205
205
 
206
206
  function stepUnitTests() {
207
- const res = sh(`${GRADLEW} :composeApp:desktopTest --console=plain`);
207
+ // `--rerun` is EVIDENCE INTEGRITY, not pedantry: without it, Gradle's build cache can
208
+ // restore a PASS recorded against a *different* tree state (deterministic re-scaffolds
209
+ // produce byte-identical sources, and golden baselines aren't compile inputs), so the
210
+ // receipt would attest tests that never executed. Compilation stays cached — only the
211
+ // test execution is forced.
212
+ const res = sh(`${GRADLEW} :composeApp:desktopTest --rerun --console=plain`);
208
213
  const summary = junitSummary(path.join(ROOT, "composeApp/build/test-results/desktopTest"));
209
214
  return {
210
215
  name: "unitTests",