create-cmp-cli 0.23.0 → 0.25.0

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.
Files changed (145) hide show
  1. package/bin/create-cmp.mjs +33 -3
  2. package/package.json +10 -4
  3. package/packages/harness/package.json +18 -8
  4. package/packages/harness/src/approve.mjs +46 -18
  5. package/packages/harness/src/console/console-data.mjs +117 -0
  6. package/packages/harness/src/console/console-evidence.mjs +122 -0
  7. package/packages/harness/src/console/console-overview.mjs +642 -0
  8. package/packages/harness/src/console/console-shell.mjs +1139 -0
  9. package/packages/harness/src/console/console-tabs.mjs +2828 -0
  10. package/packages/harness/src/console/contrast.mjs +74 -0
  11. package/packages/harness/src/console/preview-service.mjs +1384 -0
  12. package/packages/harness/src/framework-check.mjs +747 -0
  13. package/packages/harness/src/lib/a11y.mjs +1 -1
  14. package/packages/harness/src/lib/affected-tests.mjs +99 -44
  15. package/packages/harness/src/lib/agent-hold.mjs +234 -0
  16. package/packages/harness/src/lib/approvals.mjs +177 -350
  17. package/packages/harness/src/lib/audit-cadence.mjs +1 -1
  18. package/packages/harness/src/lib/comments.mjs +18 -4
  19. package/packages/harness/src/lib/determinism.mjs +275 -14
  20. package/packages/harness/src/lib/evidence-badge.mjs +38 -6
  21. package/packages/harness/src/lib/evidence-ladder.mjs +202 -0
  22. package/packages/harness/src/lib/evidence-level.mjs +144 -59
  23. package/packages/harness/src/lib/feature-brief.mjs +53 -16
  24. package/packages/harness/src/lib/flight-recorder.mjs +72 -8
  25. package/packages/harness/src/lib/framework-check.mjs +778 -0
  26. package/packages/harness/src/lib/harness-lock.mjs +87 -19
  27. package/packages/harness/src/lib/harness-manifest.mjs +208 -0
  28. package/packages/harness/src/lib/harness-region.mjs +106 -2
  29. package/packages/harness/src/lib/harness-source.mjs +142 -0
  30. package/packages/harness/src/lib/inputs-hash.mjs +180 -22
  31. package/packages/harness/src/lib/lane-markers.mjs +78 -0
  32. package/packages/harness/src/lib/lane-narrator.mjs +4 -2
  33. package/packages/harness/src/lib/lane-runner.mjs +72 -6
  34. package/packages/harness/src/lib/plan.mjs +44 -11
  35. package/packages/harness/src/lib/plant-calibration.mjs +135 -0
  36. package/packages/harness/src/lib/profile-loader.mjs +323 -0
  37. package/packages/harness/src/lib/profiles/cmp/affected.mjs +70 -0
  38. package/packages/harness/src/lib/profiles/cmp/android-checks.mjs +56 -0
  39. package/packages/harness/src/lib/profiles/cmp/artifacts.mjs +381 -0
  40. package/packages/harness/src/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
  41. package/packages/harness/src/lib/profiles/cmp/console-copy.mjs +29 -0
  42. package/packages/harness/src/lib/profiles/cmp/declarations.mjs +181 -0
  43. package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
  44. package/packages/harness/src/lib/profiles/cmp/index.mjs +47 -0
  45. package/packages/harness/src/lib/profiles/cmp/ladder.mjs +67 -0
  46. package/packages/harness/src/lib/profiles/cmp/maestro.mjs +113 -0
  47. package/packages/harness/src/lib/profiles/cmp/plants.mjs +95 -0
  48. package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/reachability.mjs +1 -1
  49. package/packages/harness/src/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +230 -28
  50. package/packages/harness/src/lib/receipt-validate.mjs +84 -16
  51. package/packages/harness/src/lib/spec-coverage.mjs +179 -92
  52. package/packages/harness/src/lib/spec-model.mjs +262 -0
  53. package/packages/harness/src/lib/step-cache.mjs +11 -2
  54. package/packages/harness/src/lib/step-outcomes.mjs +84 -151
  55. package/packages/harness/src/lib/walk.mjs +1 -1
  56. package/packages/harness/src/plan.mjs +30 -1
  57. package/packages/harness/src/preview-gallery.mjs +1 -1
  58. package/packages/harness/src/receipt-check.mjs +95 -26
  59. package/packages/harness/src/record-audit.mjs +1 -1
  60. package/packages/harness/src/scaffold-feature.mjs +10 -2
  61. package/packages/harness/src/verify.mjs +222 -142
  62. package/packages/harness/src/watch.mjs +146 -33
  63. package/packages/receipts/package.json +1 -1
  64. package/packages/receipts/src/index.mjs +2 -2
  65. package/packages/receipts/src/inputs-hash.mjs +180 -22
  66. package/packages/receipts/src/receipt-validate.mjs +84 -16
  67. package/src/commands/attach.mjs +128 -4
  68. package/src/commands/harden.mjs +7 -1
  69. package/src/commands/upgrade.mjs +7 -0
  70. package/src/lib/minimal.mjs +23 -1
  71. package/src/scaffold.mjs +8 -0
  72. package/template/.claude/settings.json +2 -2
  73. package/template/.claude/skills/add-feature/SKILL.md +7 -3
  74. package/template/.claude/skills/add-repository/SKILL.md +2 -2
  75. package/template/.claude/skills/add-screen/SKILL.md +8 -4
  76. package/template/.githooks/pre-push +9 -0
  77. package/template/CLAUDE.md +55 -186
  78. package/template/docs/TESTING.md +1 -1
  79. package/template/gitignore +4 -0
  80. package/template/qa/approve.mjs +46 -18
  81. package/template/qa/comments.json +1 -1
  82. package/template/qa/evidence/schema.json +258 -34
  83. package/template/qa/framework-check.mjs +747 -0
  84. package/template/qa/harness-manifest.json +10 -0
  85. package/template/qa/lib/a11y.mjs +1 -1
  86. package/template/qa/lib/affected-tests.mjs +99 -44
  87. package/template/qa/lib/agent-hold.mjs +234 -0
  88. package/template/qa/lib/approvals.mjs +177 -350
  89. package/template/qa/lib/audit-cadence.mjs +1 -1
  90. package/template/qa/lib/comments.mjs +18 -4
  91. package/template/qa/lib/determinism.mjs +275 -14
  92. package/template/qa/lib/evidence-badge.mjs +38 -6
  93. package/template/qa/lib/evidence-ladder.mjs +202 -0
  94. package/template/qa/lib/evidence-level.mjs +144 -59
  95. package/template/qa/lib/feature-brief.mjs +53 -16
  96. package/template/qa/lib/flight-recorder.mjs +72 -8
  97. package/template/qa/lib/framework-check.mjs +778 -0
  98. package/template/qa/lib/harness-lock.mjs +87 -19
  99. package/template/qa/lib/harness-manifest.mjs +208 -0
  100. package/template/qa/lib/harness-region.mjs +106 -2
  101. package/template/qa/lib/harness-source.mjs +142 -0
  102. package/template/qa/lib/inputs-hash.mjs +180 -22
  103. package/template/qa/lib/lane-markers.mjs +78 -0
  104. package/template/qa/lib/lane-narrator.mjs +4 -2
  105. package/template/qa/lib/lane-runner.mjs +72 -6
  106. package/template/qa/lib/plan.mjs +44 -11
  107. package/template/qa/lib/plant-calibration.mjs +135 -0
  108. package/template/qa/lib/profile-loader.mjs +323 -0
  109. package/template/qa/lib/profiles/cmp/affected.mjs +70 -0
  110. package/template/qa/lib/profiles/cmp/android-checks.mjs +56 -0
  111. package/template/qa/lib/profiles/cmp/artifacts.mjs +381 -0
  112. package/template/qa/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
  113. package/template/qa/lib/profiles/cmp/console-copy.mjs +29 -0
  114. package/template/qa/lib/profiles/cmp/declarations.mjs +181 -0
  115. package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
  116. package/template/qa/lib/profiles/cmp/index.mjs +47 -0
  117. package/template/qa/lib/profiles/cmp/ladder.mjs +67 -0
  118. package/template/qa/lib/profiles/cmp/maestro.mjs +113 -0
  119. package/template/qa/lib/profiles/cmp/plants.mjs +95 -0
  120. package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/reachability.mjs +1 -1
  121. package/template/qa/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +230 -28
  122. package/template/qa/lib/receipt-validate.mjs +84 -16
  123. package/template/qa/lib/spec-coverage.mjs +179 -92
  124. package/template/qa/lib/spec-model.mjs +262 -0
  125. package/template/qa/lib/step-cache.mjs +11 -2
  126. package/template/qa/lib/step-outcomes.mjs +84 -151
  127. package/template/qa/lib/walk.mjs +1 -1
  128. package/template/qa/plan.mjs +30 -1
  129. package/template/qa/preview-gallery.mjs +1 -1
  130. package/template/qa/receipt-check.mjs +95 -26
  131. package/template/qa/record-audit.mjs +1 -1
  132. package/template/qa/scaffold-feature.mjs +10 -2
  133. package/template/qa/verified-surface.json +17 -0
  134. package/template/qa/verify.mjs +222 -142
  135. package/template/qa/watch.mjs +146 -33
  136. /package/packages/harness/src/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
  137. /package/packages/harness/src/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
  138. /package/packages/harness/src/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
  139. /package/packages/harness/src/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
  140. /package/packages/harness/src/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
  141. /package/template/qa/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
  142. /package/template/qa/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
  143. /package/template/qa/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
  144. /package/template/qa/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
  145. /package/template/qa/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
@@ -4,19 +4,20 @@
4
4
  Generated by [create-cmp](https://github.com/kvdm-co-pilot/create-cmp) with a verification
5
5
  harness. Every AI session in this repo works under this contract.
6
6
 
7
- **Principles** (the full form, with the episode behind each, is create-cmp's
8
- `docs/PRINCIPLES.md`): derived, never claimed · prove the instrument before you read it · the
9
- layer you changed cannot certify itself · proof costs what the change costs and never runs
10
- silent · never wait on nothing · a signature binds content, a decision is closed · one record,
11
- read first. These govern every rule below; when a rule below and a principle disagree, the
12
- principle wins and the rule is the bug.
7
+ **Principles** derived, never claimed · prove the instrument before you read it · the layer you
8
+ changed cannot certify itself · proof costs what the change costs · never wait on nothing · a
9
+ signature binds content, a decision is closed · one record, read first. The full form with each
10
+ episode is create-cmp's `docs/PRINCIPLES.md` (upstream, not shipped); when a rule below and a
11
+ principle disagree, the principle wins and the rule is the bug.
13
12
 
14
13
  ## Definition of done
15
14
 
16
15
  Done means `node qa/verify.mjs` reports PASS and the receipt it writes
17
- (`qa/evidence/latest.json`) is in your commit. Claiming completion without a PASS receipt is
18
- a failure. SKIPped steps are recorded in the receipt; never present green-with-gaps as fully
19
- verified.
16
+ (`qa/evidence/latest.json`) attests this tree the hook checks the working tree by hash; commit
17
+ it with your change so CI and reviewers hold the same proof. Claiming completion without a PASS receipt is
18
+ a failure. SKIPped steps are recorded in the receipt, and a step that SKIPped for an environmental reason (no
19
+ device, no bootable AVD) makes the receipt unusable as done-evidence — `qa/receipt-check.mjs` refuses
20
+ it. Never present green-with-gaps as fully verified.
20
21
 
21
22
  **Verify in two tiers — the full lane is a checkpoint, not an inner loop.** It builds,
22
23
  tests, and gates the whole tree to produce the receipt, so it is slow by design; running it
@@ -29,15 +30,16 @@ once — when you believe the change is done.
29
30
  did-I-break-anything signal is free the way an IDE's errors-on-save are free; and
30
31
  `./gradlew :composeApp:desktopTest` for the unit tests your change touches. This is where
31
32
  you catch your own mistakes.
32
- - **Checkpoint — run once, at done:** `node qa/verify.mjs`. It writes the receipt; commit
33
+ - **Checkpoint — run once, at done:** `node qa/verify.mjs`. It writes the receipt (a `--fast` run writes
34
+ `qa/evidence/latest-fast.json` instead, so the watcher can keep running); commit
33
35
  the receipt with your change. The Stop hook (`qa/receipt-check.mjs`) then confirms — with a
34
- cheap hash check, not another lane run — that a valid receipt attests your commit, and CI
36
+ cheap hash check, not another lane run — that a valid receipt attests this tree, and CI
35
37
  re-runs the full lane on push. After a green checkpoint, do not re-run the lane unless you
36
38
  change the tree again.
37
39
 
38
40
  Humans get the same gate at push time: run `node qa/setup-hooks.mjs` once (after `git init`)
39
- to enable the shipped pre-push hook. It blocks a push whose committed receipt doesn't attest
40
- HEAD — the same cheap check, before code leaves the machine (`git push --no-verify` bypasses
41
+ to enable the shipped pre-push hook. It blocks a push whose receipt doesn't attest
42
+ the tree — the same cheap check, before code leaves the machine (`git push --no-verify` bypasses
41
43
  it; CI still enforces it).
42
44
 
43
45
  ## Specifications — behavior starts here
@@ -50,7 +52,7 @@ then implement. Durable tests cite their clause (`// SPEC: HOME-02`).
50
52
  prove that test could ever *observe* the promise. Add `[tier: device]` (or `[tier: e2e]`)
51
53
  after the id when the claim is about OS facts a host JVM cannot see — lifecycle, alarms,
52
54
  notifications, permissions, real navigation. `specCoverage` then requires a citation from
53
- `androidInstrumentedTest` or `qa/e2e` and FAILS without one, rather than accepting a
55
+ `androidInstrumentedTest` or `qa/e2e` for `device`, from `qa/e2e` alone for `e2e`, and FAILS without one, rather than accepting a
54
56
  desktop test that is structurally blind to the claim.
55
57
  [`specs/app-base.spec.md`](./specs/app-base.spec.md) states the architecture and shell
56
58
  invariants the conformance gates enforce.
@@ -120,8 +122,9 @@ timezone), `DozeControl` (forced idle), `PermissionControl`, `ProcessControl`,
120
122
  the exemplar proves an `allowWhileIdle` alarm delivers from inside forced deep idle by
121
123
  nesting a clock warp in a Doze bracket. Exemplars: `PlatformBehaviorSeamTest`,
122
124
  `RuntimeStateSeamTest`. Each organ's header states what it does NOT reproduce; read it
123
- before claiming more than it proves. The lane's `androidChecks` step runs them when a
124
- device is attached; see `docs/TESTING.md`.
125
+ before claiming more than it proves. The lane's `androidChecks` step runs them the lane provisions its own device: an attached one, or a
126
+ headless AVD it boots itself. `CMP_DEVICE=none` opts out, and that SKIP is environmental, so the receipt is
127
+ then not done-evidence. See `docs/TESTING.md`.
125
128
 
126
129
  ## Evidence
127
130
 
@@ -136,9 +139,12 @@ one verified, attributed state — so committing each receipt is what builds the
136
139
 
137
140
  ## The lane is not yours to edit
138
141
 
139
- Every `.mjs` file directly under `qa/` and `qa/lib/` is **machine-owned**: harness code
140
- that is byte-identical in every create-cmp app and carries no app content at all. It
141
- belongs to `create-cmp-harness`, versioned independently of the engine that stamped this
142
+ Every `.mjs` file directly under `qa/` and `qa/lib/`, every `.mjs` under
143
+ `qa/lib/profiles/**` (this app's stack profile its steps, tiers, ladder and device
144
+ glue), and the declarations the lane reads (`qa/harness-manifest.json`,
145
+ `qa/verified-surface.json`) are **machine-owned**: harness code that is byte-identical in
146
+ every create-cmp app and carries no app content at all. It
147
+ belongs to `prooflane-harness` (the name in your `qa/harness.lock.json`), versioned independently of the engine that stamped this
142
148
  app's shape, and `qa/harness.lock.json` records a sha256 of every one of those files.
143
149
 
144
150
  `node qa/verify.mjs` checks that lock first, on every run. Editing lane code fails the
@@ -147,7 +153,9 @@ honestly vouch for itself. Without that check the receipt was unfalsifiable in o
147
153
  specific way: force every step to PASS in `qa/verify.mjs` and the receipt still validated,
148
154
  since the edited file was simply part of the hashed input surface.
149
155
 
150
- **So: do not edit `qa/*.mjs` or `qa/lib/*.mjs`.** If the lane is wrong, the fix is
156
+ **So: do not edit `qa/*.mjs`, `qa/lib/*.mjs`, `qa/lib/profiles/cmp/**`, `qa/verified-surface.json`, or
157
+ `qa/harness-manifest.json`.** (A profile you wrote yourself under `qa/lib/profiles/<id>/` is yours; after
158
+ editing it, `npx create-cmp-cli harness relock` re-takes the lock.) If the lane is wrong, the fix is
151
159
  upstream in the engine, not here. If you genuinely must fork it, know that
152
160
  `npx create-cmp-cli upgrade --harness` will replace the region and preserve your edits as
153
161
  `qa/harness-local.patch` for you to re-apply or upstream — nothing is lost, but the fork
@@ -161,7 +169,7 @@ Upgrading the lane is safe to do unattended — it touches no app content and no
161
169
  artifact:
162
170
 
163
171
  ```bash
164
- npx create-cmp-cli upgrade --harness
172
+ npx create-cmp-cli upgrade --harness --yes # without --yes it dry-runs and exits 0 — a silent no-op from a tool
165
173
  ```
166
174
 
167
175
  ## Approvals — governed artifacts need a human's sign-off
@@ -170,43 +178,17 @@ Some artifacts are **governed**: a human approves them, and the approval is boun
170
178
  artifact's content by hash (`qa/approvals.json`) — the evidence-receipt idea, applied to a
171
179
  human decision. The ordered walk is a **definition order**, not just an approval order:
172
180
  each artifact is the vocabulary the next is written in, so on a fresh app each step is a
173
- conversation that ends in an approval — the genesis walk, six conversations:
174
-
175
- The order encodes two disciplines: **behavior is spec-first** (the exemplar's clauses are
176
- confirmed before the slice is built) and **visuals are UI-first** (the design system and
177
- component vocabulary are distilled from the real screens, so they lock after the exemplar —
178
- a provisional palette carries the build until then).
179
-
180
- 0. **Intent** `specs/intent.md`, the root brief everything else traces to (purpose,
181
- audience, platforms, brand feel, reference apps, first screens, **glossary**). Filled by
182
- the `cmp-new` interview; the seed's placeholder prose is marked unfilled. Its
183
- `## Glossary` section is lifted verbatim into `docs/ARCHITECTURE.md` §8 — write it there
184
- in the exact form you want published.
185
- 0.5. **First feature brief** — `docs/features/<exemplar>.md`, drafted from the interview's
186
- "first screens" answer the moment intent is signed: the first feature's decisions and
187
- their why, signed BEFORE its spec is written. Genesis runs the same decide → contract →
188
- build loop as every later change; the app's first feature gets the same decide step
189
- every later feature gets. (Express lane: skipped honestly, never fabricated;
190
- placeholders earn a brief when they become real.)
191
- 1. **Architecture + structure** — `specs/app-base.spec.md` **and**
192
- [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) (`cmp:generated` sections stripped
193
- before hashing, so a mechanical regeneration never invalidates the approval — only an
194
- authored-prose edit does).
195
- 2. **Exemplar spec** — `specs/<exemplar>.spec.md`. Confirmed BEFORE the slice is built:
196
- propose the clauses, get the human's yes, then implement to satisfy them (the same
197
- discipline `add-feature` already enforces post-genesis).
198
- 3. **Exemplar feature** — the **configured** exemplar's file set the `add-feature`
199
- stamper clones from (see "Configurable exemplar"), built to the confirmed spec.
200
- 4. **Design system** — `presentation/theme/Theme.kt`, `presentation/theme/Tokens.kt`.
201
- Locked on the REAL exemplar: candidates render on real screens, never stubs. If the
202
- lock changes the exemplar's look, reopen → re-approve it — that loop is the design,
203
- not a failure.
204
- 5. **Components** — every `presentation/components/*.kt` (a dynamic, sorted glob),
205
- distilled from the screens per the inclusion rubric (`docs/ARCHITECTURE.md` §7). Once
206
- approved, the registry is law: adding or changing a common component invalidates the
207
- approval until a human re-approves.
208
- 6. **Per-feature spec** — `specs/<feature>.spec.md`, one governed artifact per feature,
209
- added as features land.
181
+ conversation that ends in an approval — the genesis walk:
182
+
183
+ The genesis order intent first feature brief architecture + structure → exemplar spec
184
+ exemplar feature design system components per-feature specs is driven by the `cmp-new`
185
+ interview and is over before this file is your working contract; `node qa/approve.mjs --status`
186
+ prints every artifact and its state. Two disciplines survive it: **behavior is spec-first** (clauses
187
+ confirmed before the slice is built) and **visuals are UI-first** (the design system and component
188
+ vocabulary are distilled from real screens, so they lock after the exemplar — a provisional palette
189
+ carries the build until then; if the lock changes the exemplar's look, reopen re-approve it — that
190
+ loop is the design, not a failure). Once approved, the component registry is law: adding or changing
191
+ a common component invalidates the approval until a human re-approves.
210
192
 
211
193
  ### After genesis — every change is the same loop
212
194
 
@@ -219,114 +201,11 @@ understood the change to be, which lane it takes, and why, before any tool runs.
219
201
  can overrule the lane in a word; a silent route is a routing error even when the lane was
220
202
  right.
221
203
 
222
- **Grill before the brief** (the `grill-me` plugin skill; the rule holds without the plugin):
223
- on the brief lane, after the triage restatement and before a word of the brief is drafted,
224
- settle the load-bearing questions. Read what the repo already answers first a signed brief
225
- or spec is a CLOSED decision: cite it, never re-ask it. Then ask the frontier of unsettled
226
- decisions as a numbered list, at most five per round, each with why it matters and a
227
- recommended answer — and WAIT for the answers before anything else. Stop when no remaining
228
- question would change the work; three rounds is the ceiling (more means the request needs
229
- splitting). Answers land in the brief — settled calls become **Decisions** with their why,
230
- the human's own calls the **Open decisions** section; the brief's signature closes them. The
231
- direct lane is not grilled (one inline question at most, only when the restatement cannot
232
- be made unambiguous); a bug fix or an emergency fix, never. While the grill is open, the
233
- chain's first step reads `settle the open questions` (declare it before the first round;
234
- re-declare when the answers reshape the steps).
235
-
236
- **Brief lane** — when the change carries **decisions a future contributor could plausibly
237
- "simplify" away** ("the day boundary is configurable, default 04:00 — not midnight") OR
238
- **blast radius into other governed artifacts**. After naming the lane:
239
-
240
- | Step | What |
241
- |---|---|
242
- | 1 | **Feature brief** — `docs/features/<name>.md`: the decisions with their why, research, rejected options, an **Open decisions** section until the human closes each. Signed BEFORE code. |
243
- | 2 | **Design** — iff the feature has a UI surface (`"screens": true`, or screen files exist): draft the screens on STUB data, register them in the PreviewRegistry, render, and STOP. The human judges the rendered screens and signs `feature-design:<name>`. Never ask a human to approve a described UI. |
244
- | 3 | **Contract** — reopen any signed spec the brief amends (`--reopen feature-spec:<surface> --reason "…"`); write the clauses where the behavior lives — about the form that now exists; the human signs |
245
- | 4 | Build the slice (`add-feature` / preview loop). Declared blast lands "as declared"; re-approve touched visual artifacts on rendered output (wiring the signed screens from stub to real state drifts `feature-design:<name>` — its re-approval is that pass) |
246
- | 5 | Prove — nothing to do: the lane's gates + receipt ARE the proof |
247
- | 6 | The human's `--accept` — enabled only at provenDone AND a signed design |
248
-
249
- **Direct lane** — everything else (bug fix, copy edit, tweak): confirm in chat, reopen →
250
- amend clause → re-approve if a signed contract is touched, build, lane once at done.
251
- Legacy features never get retro-briefs; spikes are ungoverned until they become real.
252
-
253
- **A brief's LOCATION is the governance opt-in**: every `docs/features/*.md` is a governed
254
- `feature-brief:<name>` artifact (hash-bound at signing; `<name>` pairs with
255
- `specs/<name>.spec.md`). `docs/proposals/` stays ungoverned. The brief carries at most one
256
- machine-read block, and it **declares — it never gates**:
257
-
258
- ```json cmp:feature
259
- { "touches": ["components", "design-system"], "screens": true }
260
- ```
261
-
262
- `touches` is the declared blast radius — the artifact hashes already enforce; declaring
263
- lets the console show "components re-approval, as planned" instead of an unexplained
264
- failure, and surface **undeclared blast** when something drifted that no open brief
265
- accounted for. `screens: true` declares a UI surface: it holds the design gate
266
- (`feature-design:<name>` — the feature's own `presentation/<name>/*Screen.kt`, signed on
267
- rendered output) before any screen file exists; once files exist, disk is ground truth
268
- regardless. Both declare — neither gates.
269
-
270
- **Doneness is DERIVED, never claimed.** There is no `--deliver` and no checks block —
271
- deliberately (they existed and were removed as a weaker parallel truth). A feature is
272
- `provenDone` when, mechanically: its spec has live clauses **and** every one is cited by a
273
- test **and** the latest receipt is PASS **and** the receipt's `inputs.hash` attests the
274
- tree as it stands. `--status` and the console print the same one-line `doneReason` either
275
- way. What remains for humans is judgment: signing the brief (before code) and accepting
276
- the feature (after proof — "the proven thing is what I wanted"); `--accept` is refused
277
- until the derivation holds. Acceptance lives on the ledger row, never in the doc (the
278
- signed bytes must not move when the human accepts).
279
-
280
- | Command | What |
281
- |---|---|
282
- | `node qa/approve.mjs feature-brief:<name>` | the human signs the brief (before code) |
283
- | `node qa/approve.mjs --accept <name>` | the human's bookend; refused until provenDone |
284
-
285
- Editing a feature is the same brief **reopened** — `--reopen-feature <name> --reason "…"`
286
- reopens the brief + its spec + its design + the set it declared, as ONE recorded change;
287
- re-approval is one walk back. Every reopen **requires a `--reason`** (it walks back a
288
- signature; the signer reads why from the ledger itself) and is journaled with `via` — the
289
- append-only journal (`qa/approvals.log.jsonl`, printed by `--log`, shown in the console
290
- strip's History) is how "what happened while I was away" stays answerable.
291
-
292
- | Command | What |
293
- |---|---|
294
- | `node qa/approve.mjs --status` | Every governed artifact with live state (`unreviewed` / `approved` / `changed-since-approval` / `reopened`), short hash, mode badge |
295
- | `node qa/approve.mjs <artifact>` | Record approval — hashes the artifact's files now, stamps the time; also clears a `defaults-accepted` mode |
296
- | `node qa/approve.mjs --accept-defaults` | **Express lane**: approve every currently-resolvable artifact in one visible act, each stamped `"mode": "defaults-accepted"` — build now, walk the definition later. Unresolvable artifacts are skipped with the standard refusal printed. The ledger never pretends the defaults were designed. |
297
- | `node qa/approve.mjs --reopen <artifact> --reason "…"` | Move an *approved* artifact (shaped or defaults-accepted) back to `reopened` for deliberate redesign. `--reason` is REQUIRED and recorded (`reopenedAt`, `via`, `reason` — on the row and in the journal). Refuses unknown ids and anything not currently approved. |
298
- | `node qa/approve.mjs --reopen-feature <name> --reason "…"` | ONE recorded change: reopens the brief + `feature-spec:<name>` + `feature-design:<name>` + every declared `touches` artifact (each only if currently approved), all under one reason. |
299
- | `node qa/approve.mjs --log` | The governance journal — every approve/reopen/accept with when, which surface (`via`), and why. |
300
-
301
- With the create-cmp plugin, the same decisions can be made from the preview console
302
- (`preview {projectDir}`'s URL) — it calls the same library, so the CLI and the console
303
- never disagree. The console's rail is the human's work queue (colour = something waits
304
- on them), and a drifted artifact shows, in its own section, exactly **what changed
305
- against the signed bytes and which files are still exactly as signed**, with re-approval
306
- in place. An agent blocks on a pending decision with
307
- `approval_status {waitForDecision:true}`.
308
-
309
- The verify lane's `approvals` gate (a step like any other, in every profile) resolves each
310
- artifact's live status against `qa/approvals.json`:
311
-
312
- - **`unreviewed`** → SKIP with a warning line. Nothing fails until a human opts in by
313
- approving.
314
- - **`reopened`** → SKIP with a warning line, exactly like `unreviewed`. Sanctioned redesign
315
- never trips the gate; edits made while reopened are never drift. Re-approve when the
316
- redesign lands.
317
- - **`approved`, hash still matches** → PASS.
318
- - **`approved`, hash no longer matches** → FAIL, naming the artifact and the re-approval
319
- command. The artifact changed after sign-off — re-approve it or revert the change.
320
- Invalidation is mechanical, like golden-tree drift, not a judgment call.
321
-
322
- That asymmetry is the point: **redesign is a decision; drift is an accident** — the ledger
323
- records which was which. A run with one reopened artifact and one drifted artifact FAILs
324
- naming only the drifted one.
325
-
326
- A gate FAIL fails the lane verdict, which fails `qa/receipt-check.mjs` (the Stop hook) by
327
- the same mechanism as any other FAIL — no separate enforcement to maintain. `add-feature`
328
- seeds each new feature's spec as `unreviewed` and prints the approval reminder; it never
329
- refuses to stamp over this.
204
+ **Grill before the brief** (the `grill-me` plugin skill carries the procedure; the rule holds without the plugin):
205
+ on the brief lane, after the triage restatement and before a word of the brief, settle
206
+ the load-bearing questions a signed brief or spec is a CLOSED decision: cite it, never re-ask it;
207
+ ask the unsettled ones as a numbered list with a recommended answer each, and WAIT. Answers land in the brief's **Decisions** and
208
+ **Open decisions**; the signature closes them. The direct lane is not grilled.
330
209
 
331
210
  ### Configurable exemplar — the DNA features are cloned from
332
211
 
@@ -381,19 +260,12 @@ which is worse than no chain. While the full check runs, the chain's observed li
381
260
  narrates the lane's own position (step, elapsed, usual cost) — quote THAT, never an
382
261
  estimate. The chain gates nothing; the walk stays the truth for doneness.
383
262
 
384
- **The studio is a standing check:** every injected context opens with a `[studio: …]`
385
- line. If it says DOWN or not running, restore it before proceeding call the
386
- cmp-inspector `preview { projectDir }` tool (it starts a detached resident console
387
- that survives the session) or, if the tools are absent, tell the human once. A
388
- missing window is a fault to heal, never something to work silently past.
389
-
390
- **While working — the header, then quiet:** open EVERY reply with the walk's one-line
391
- header — the exact `[chat header]` line the per-prompt inject delivers. Paste it
392
- verbatim, never compose it: it is the derivation's own string, so it cannot drift, and
393
- it persists in the transcript, which the statusline beneath the input box never does.
394
- After the header: one line per stage transition, nothing per-file. Stages carry their
395
- plain-words gloss on first mention ("Contract — agreeing what it promises"); quote the
396
- lane's cost only from the measured figure in the injected card, never an estimate.
263
+ **While a walk is open, the per-prompt inject carries the running protocol** the `[studio: …]`
264
+ line (restore it if DOWN: the cmp-inspector `preview { projectDir }` tool, or tell the human once),
265
+ the `[chat header]` to open every reply with, verbatim, and the `▲ ARRIVED, UNPLANNED` line for work
266
+ that belongs to no open walk (default: after the current walk lands; one walk at a time). Render the
267
+ injected lines, never your memory of them; with no walk open it delivers nothing and you write none
268
+ of it. After the header: one line per stage transition, nothing per-file.
397
269
 
398
270
  **At every human gate — loud:** a full stop card, never a bare question — and the
399
271
  easiest act leads:
@@ -404,12 +276,6 @@ easiest act leads:
404
276
  → CLI fallback: <the command> (or "reply approve" when no console is up)
405
277
  After this: <the remaining stages, and which ones stop for the human>
406
278
 
407
- **Arrivals:** work that belongs to no open walk (undeclared drift, a harness
408
- upgrade's rule-change reopens) is NEVER silently interleaved. Render
409
- `▲ ARRIVED, UNPLANNED — <what> · now, or after <current walk> lands?` and default to
410
- after. One walk at a time unless the human chooses otherwise — three unframed
411
- interleaved walks is precisely the session this rule exists to prevent.
412
-
413
279
  ## Comments — review feedback flows back through the agent
414
280
 
415
281
  Approvals are binding (they gate the verify lane); **comments are advisory** — a human's
@@ -523,14 +389,17 @@ conventions) · [`CONTRIBUTING.md`](./CONTRIBUTING.md) (workflow, Conventional C
523
389
  | `node qa/setup-hooks.mjs` | Enable the pre-push receipt gate (one-time, after `git init`) |
524
390
  | `./gradlew :composeApp:assembleDebug` | Android debug build |
525
391
  | `./gradlew :composeApp:assembleRelease` | Android release build — R8 + `lintVital`, the variant the lane's `releaseBuild` step proves. Produces an **unsigned** APK; signing needs a keystore, which is yours to create and keep out of the repo. |
526
- | `./gradlew :composeApp:hotRunDesktop --auto` | Desktop dev-client with hot reload |
527
392
  | `./gradlew :composeApp:connectedDebugAndroidTest` | Instrumented behavior tests on the attached device (the lane's `androidChecks` step) |
393
+ | `node qa/framework-check.mjs` | GATE-RULES Rule 0: proves the lane returns both ways — PASS on the clean tree, FAIL by name on planted violations — in seconds, restoring every byte. Run it first in a repo whose harness is new or freshly upgraded. |
528
394
  | `node qa/verify.mjs --profile smoke` | The smallest end-to-end lane: every pure-Node gate, no Gradle, no device — seconds. Proves the framework *returns*, never the change (its receipt is refused as done-evidence). Run it first in any repo whose harness is new or freshly upgraded |
529
395
  | `node qa/verify.mjs --profile nightly` | Scheduled stage: everything `ci` proves with the determinism probe forced on. Proves the harness, never a change — its receipt (`stage: "nightly"`) is refused as done-evidence, exactly like `--fast`. Schedule it; never wait on it |
530
396
  | `node qa/verify.mjs --profile release` | Ship-time lane: everything `ci` proves plus the audit-cadence report (`auditCadence` — which androidMain subsystems changed since their last recorded `cmp-audit`; a nudge, never a gate) and the release-APK Maestro smoke (`releaseSmoke`) |
531
397
  | `node qa/verify.mjs --determinism` | Timezone determinism probe, alone: runs the JVM test tier twice under UTC-12 and UTC+14 and FAILs naming any test whose outcome differs — the dynamic net behind ARCH-13's static one. Opt-in inside a lane via `--profile ci --determinism`; never with `--fast`; writes no receipt on its own |
532
398
  | `node qa/record-audit.mjs <subsystem>` | Record that a `cmp-audit` of an androidMain subsystem happened (appends subsystem + HEAD sha + timestamp to `qa/audits.jsonl`; refuses dirty/unknown targets). `--list` shows every derived subsystem and its audit status |
533
399
  | `node qa/retrospective.mjs` | How this project actually uses its harness, from `qa/flight-recorder.jsonl` (appended by every lane run): fast vs full ratio, verbatim SKIP reasons grouped, whether the device tier is ever reached, longest stretch with no full lane. States only what the journal recorded |
400
+ <!-- >>> cmp:feature dev-client -->
401
+ | `./gradlew :composeApp:hotRunDesktop --auto` | Desktop dev-client with hot reload |
402
+ <!-- <<< cmp:feature dev-client -->
534
403
  <!-- <<< cmp:feature harness -->
535
404
  <!-- >>> cmp:feature !harness -->
536
405
  # __APP_NAME__ — working guide
@@ -198,7 +198,7 @@ verbatim — the errors-on-save loop an IDE gives a human, for the agent. What i
198
198
  evidence. It runs the fast tier only, so nothing it produces can satisfy the done-gate,
199
199
  and every run says so in a standing footer; the checkpoint remains one deliberate full
200
200
  `node qa/verify.mjs` run. Coordination: it never launches while a verify lane
201
- (`.cmp-lane-in-progress`) or a preview-daemon render (`.cmp-render-in-progress`) holds the
201
+ (`qa/.lane-in-progress`) or a preview-daemon render (`composeApp/build/.cmp-render-in-progress`) holds the
202
202
  project — it waits and coalesces, so watch mode, the preview daemon, and the lane can all
203
203
  be resident on one project without two Gradle invocations ever colliding. `--once` runs a
204
204
  single coordinated pass for scripting; `--json` emits one JSON object per run for
@@ -16,6 +16,8 @@ build/
16
16
 
17
17
  # Verify-lane binary evidence (screenshots, reports, page sources). The RECEIPT
18
18
  # (qa/evidence/latest.json) IS committed — these artifacts are hashed into it instead.
19
+ # A --fast run writes latest-fast.json: inner-loop feedback, never evidence, never committed.
20
+ qa/evidence/latest-fast.json
19
21
  qa-artifacts/
20
22
 
21
23
  # iOS / CocoaPods / XcodeGen
@@ -44,6 +46,8 @@ qa/.plan.json
44
46
  # The closed-chain trail (drive-narration N5): local because it carries raw
45
47
  # human prompts — the committed journal for lane runs stays qa/flight-recorder.jsonl.
46
48
  qa/.plan-history.jsonl
49
+ qa/.agent-hold.json
50
+ qa/.lane-in-progress
47
51
 
48
52
  # Android signing. The keystore IS the app's identity on Android — it cannot be reissued —
49
53
  # and keystore.properties holds its passwords. Never committed, never in a sidecar diff.
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  // The approvals CLI — thin shell over qa/lib/approvals.mjs.
3
3
  //
4
- // node qa/approve.mjs <artifact> records approval (recomputes the artifact's
5
- // hash now, stamps the time, writes qa/approvals.json)
4
+ // node qa/approve.mjs <artifact> [<artifact> …] --as "Name <email>"
5
+ // records approval (recomputes each artifact's
6
+ // hash now, stamps the time, writes qa/approvals.json).
7
+ // Sign every pending artifact in ONE command: each
8
+ // signature moves the receipt's input hash, so N
9
+ // separate signings cost N lane re-runs.
6
10
  // node qa/approve.mjs --status lists every governed artifact + live state
7
11
  // (unreviewed / approved / changed-since-approval /
8
12
  // reopened, + mode when set) + short hash
@@ -44,7 +48,7 @@ import {
44
48
  approveArtifact,
45
49
  getApprovalStatuses,
46
50
  getFeatureBoard,
47
- isPackageResolvable,
51
+ isProjectGovernable,
48
52
  listGovernedArtifacts,
49
53
  readJournal,
50
54
  reopenArtifact,
@@ -118,17 +122,16 @@ if (args.includes("--status")) {
118
122
  process.exit(0);
119
123
  }
120
124
 
121
- // Write guard: refuse to RECORD approvals in a tree whose package is not
122
- // resolvable (the raw template / a pre-stamp tree). Approvals belong to a
123
- // generated project; writing qa/approvals.json into the template pollutes the
125
+ // Write guard: refuse to RECORD approvals in a tree that cannot be governed —
126
+ // no manifest, no loadable profile, or a tree the profile itself refuses (the
127
+ // raw template, whose package is still a placeholder). Approvals belong to a
128
+ // real project; writing qa/approvals.json into the template pollutes the
124
129
  // template itself. Read-only --status (above) stays available anywhere. Applies
125
130
  // to every write operation below (single approve, express lane, reopen).
126
131
  function refuseIfUnresolvable() {
127
- if (isPackageResolvable(ROOT)) return;
128
- console.error(
129
- "error: this tree's package is not resolvable (composeApp/build.gradle.kts namespace is missing or still a placeholder) — " +
130
- "this looks like the raw template or a pre-stamp tree. Approvals are recorded in a generated project; refusing to write qa/approvals.json here.",
131
- );
132
+ const v = isProjectGovernable(ROOT);
133
+ if (v.ok) return;
134
+ console.error(`error: ${v.reason}`);
132
135
  process.exit(1);
133
136
  }
134
137
 
@@ -243,7 +246,7 @@ if (acceptFlagIdx !== -1) {
243
246
  if (args.length === 0) {
244
247
  const ids = listGovernedArtifacts(ROOT).map((a) => a.id);
245
248
  console.error(
246
- 'usage: node qa/approve.mjs <artifact> | --status | --log | --accept-defaults | --reopen <artifact> --reason "…" | --reopen-feature <name> --reason "…" | --accept <name>\n' +
249
+ 'usage: node qa/approve.mjs <artifact> --as "Name <email>" | --status | --log | --accept-defaults | --reopen <artifact> --reason "…" | --reopen-feature <name> --reason "…" | --accept <name>\n' +
247
250
  ` valid artifacts: ${ids.length > 0 ? ids.join(", ") : "(none resolved in this project)"}`,
248
251
  );
249
252
  process.exit(1);
@@ -251,7 +254,27 @@ if (args.length === 0) {
251
254
 
252
255
  refuseIfUnresolvable();
253
256
 
254
- const artifactId = args[0];
257
+ // EVERY artifact named in one command — one write, one invalidation.
258
+ //
259
+ // A signature changes qa/approvals.json's `status`, which the approvals gate
260
+ // reads, so it legitimately moves the receipt's input hash. Signed one at a
261
+ // time AFTER a green lane, that means N signatures invalidate the receipt N
262
+ // times and cost N lane re-runs and N bookkeeping commits. Measured in
263
+ // payment-blueprint's log on 2026-09-04: of 21 commits in one session, 8 were
264
+ // "sign the approval" / "bind the receipt" with no product content, and every
265
+ // code change cost two to three commits. It compounds as a repo accumulates
266
+ // governed artifacts, which is what makes a session start fast and grind later.
267
+ //
268
+ // Two halves to the fix: sign them together (here), and sign BEFORE the final
269
+ // lane run so the receipt you keep is already the one that covers the
270
+ // signatures. `--reopen-feature` established the idiom — one recorded change,
271
+ // not N commands.
272
+ const artifactIds = args.filter((a, i) => !a.startsWith("--") && (i === 0 || !args[i - 1].startsWith("--")));
273
+ // An unrecognised flag must still be REFUSED by name, never silently skipped:
274
+ // filtering flags out is how `node qa/approve.mjs --delivr meal` would quietly
275
+ // approve nothing and exit 0. Falling back to the first argument reproduces the
276
+ // exact refusal an unknown verb has always produced.
277
+ if (artifactIds.length === 0) artifactIds.push(args[0]);
255
278
  // The signer is REQUIRED, not optional: see approveArtifact's refusal. Parsed
256
279
  // here rather than defaulted from git config on purpose — `git config user.name`
257
280
  // is whatever the machine says, and an agent running on a developer's laptop
@@ -267,9 +290,14 @@ if (!approvedBy || approvedBy.startsWith("--")) {
267
290
  );
268
291
  process.exit(1);
269
292
  }
270
- const result = approveArtifact(ROOT, artifactId, { via: "cli", approvedBy });
271
- if (!result.ok) {
272
- console.error(`error: ${result.reason}`);
273
- process.exit(1);
293
+ const results = artifactIds.map((id) => ({ id, result: approveArtifact(ROOT, id, { via: "cli", approvedBy }) }));
294
+ const failed = results.filter((r) => !r.result.ok);
295
+ for (const { id, result } of results) {
296
+ if (result.ok) console.log(`✓ approved ${result.artifact} — hash ${shortHash(result.hash)}, at ${result.approvedAt}`);
297
+ else console.error(`error: ${id}: ${result.reason}`);
298
+ }
299
+ if (results.length > 1) {
300
+ const signed = results.length - failed.length;
301
+ console.log(`\n${signed} signature${signed === 1 ? "" : "s"} in one write. Run the lane AFTER signing, not before — a signature moves the receipt's input hash, so signing after a green run costs you that run.`);
274
302
  }
275
- console.log(`✓ approved ${result.artifact} — hash ${shortHash(result.hash)}, at ${result.approvedAt}`);
303
+ if (failed.length) process.exit(1);
@@ -1,4 +1,4 @@
1
1
  {
2
- "schema": "cmp-comments/1",
2
+ "schema": "prooflane-comments/1",
3
3
  "comments": []
4
4
  }