create-cmp-cli 0.23.0 → 0.24.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.
- package/package.json +1 -1
- package/packages/harness/package.json +1 -1
- package/packages/harness/src/approve.mjs +37 -8
- package/packages/harness/src/framework-check.mjs +513 -0
- package/packages/harness/src/lib/agent-hold.mjs +234 -0
- package/packages/harness/src/lib/evidence-badge.mjs +4 -4
- package/packages/harness/src/lib/framework-check.mjs +397 -0
- package/packages/harness/src/lib/inputs-hash.mjs +5 -0
- package/packages/harness/src/lib/steps-cmp.mjs +1 -1
- package/packages/harness/src/plan.mjs +30 -1
- package/packages/harness/src/receipt-check.mjs +15 -2
- package/packages/harness/src/verify.mjs +1 -1
- package/packages/receipts/src/inputs-hash.mjs +5 -0
- package/template/gitignore +1 -0
- package/template/qa/approve.mjs +37 -8
- package/template/qa/framework-check.mjs +513 -0
- package/template/qa/lib/agent-hold.mjs +234 -0
- package/template/qa/lib/evidence-badge.mjs +4 -4
- package/template/qa/lib/framework-check.mjs +397 -0
- package/template/qa/lib/inputs-hash.mjs +5 -0
- package/template/qa/lib/steps-cmp.mjs +1 -1
- package/template/qa/plan.mjs +30 -1
- package/template/qa/receipt-check.mjs +15 -2
- package/template/qa/verify.mjs +1 -1
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
// node qa/plan.mjs --done # close the chain
|
|
11
11
|
// node qa/plan.mjs --clear # a landed request leaves no stale windshield
|
|
12
12
|
//
|
|
13
|
+
// node qa/plan.mjs --hold "adopting the ports" [--as <name>] # I am working in this tree
|
|
14
|
+
// node qa/plan.mjs --beat ["what I am on now"] # still here (every few minutes)
|
|
15
|
+
// node qa/plan.mjs --release # done, or gone
|
|
16
|
+
//
|
|
17
|
+
// The hold is LIVENESS, not a lock: nothing waits on it and it gates nothing.
|
|
18
|
+
// It exists so "is the agent working or wedged?" is `tail` on a file instead of
|
|
19
|
+
// filesystem archaeology — the question that got a healthy agent killed
|
|
20
|
+
// mid-proof on 2026-09-04 — and so the Stop hook can say "wait for it" instead
|
|
21
|
+
// of "run the lane" at a tree that would not compile. A heartbeat older than
|
|
22
|
+
// five minutes is a crashed writer; a hold older than the ceiling is a wedge.
|
|
23
|
+
//
|
|
13
24
|
// Unlike walk-status (a fail-open status surface), this CLI is a WRITER the
|
|
14
25
|
// agent invokes deliberately: bad input gets a refusal and exit 1, because a
|
|
15
26
|
// silently-dropped declaration would leave the surfaces lying about position.
|
|
@@ -18,6 +29,7 @@ import path from "node:path";
|
|
|
18
29
|
import { fileURLToPath } from "node:url";
|
|
19
30
|
|
|
20
31
|
import { deriveChain, markStep, readPlan, renderChain, setPlan, clearPlan } from "./lib/plan.mjs";
|
|
32
|
+
import { assessHold, beatHold, claimHold, describeHold, readHold, releaseHold } from "./lib/agent-hold.mjs";
|
|
21
33
|
|
|
22
34
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
23
35
|
const args = process.argv.slice(2);
|
|
@@ -37,7 +49,24 @@ function out(result) {
|
|
|
37
49
|
process.exit(0);
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
|
|
52
|
+
function holdOut(result) {
|
|
53
|
+
if (!result.ok) {
|
|
54
|
+
process.stderr.write(`plan: ${result.error}\n`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const line = describeHold(assessHold(readHold(ROOT)));
|
|
58
|
+
process.stdout.write(`${line ?? "No agent holds this tree."}\n`);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (args.includes("--hold")) {
|
|
63
|
+
holdOut(claimHold(ROOT, { holder: valueOf("--as") ?? process.env.CMP_AGENT ?? "an agent", note: valueOf("--hold") ?? "" }));
|
|
64
|
+
} else if (args.includes("--beat")) {
|
|
65
|
+
const note = valueOf("--beat");
|
|
66
|
+
holdOut(beatHold(ROOT, note === null ? {} : { note }));
|
|
67
|
+
} else if (args.includes("--release")) {
|
|
68
|
+
holdOut(releaseHold(ROOT));
|
|
69
|
+
} else if (args.includes("--set")) {
|
|
41
70
|
const spec = valueOf("--set");
|
|
42
71
|
if (spec === null) {
|
|
43
72
|
process.stderr.write('plan: --set needs a value: --set "step | step | …"\n');
|
|
@@ -21,6 +21,7 @@ import { fileURLToPath } from "node:url";
|
|
|
21
21
|
|
|
22
22
|
import { computeInputsHash } from "./lib/inputs-hash.mjs";
|
|
23
23
|
import { evaluateReceipt, readReceipt } from "./lib/receipt-validate.mjs";
|
|
24
|
+
import { readHold, assessHold, describeHold, holdExplains } from "./lib/agent-hold.mjs";
|
|
24
25
|
|
|
25
26
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
26
27
|
|
|
@@ -175,12 +176,24 @@ if (asHook) {
|
|
|
175
176
|
// INSTRUCTION: "run the lane" is wrong advice when one is already running,
|
|
176
177
|
// and a gate that tells you to do the thing you are doing trains you to
|
|
177
178
|
// stop reading it.
|
|
179
|
+
//
|
|
180
|
+
// An agent HOLDING the tree is the same shape of correction one level out:
|
|
181
|
+
// a lane in flight explains a receipt that is about to arrive, a hold
|
|
182
|
+
// explains a tree that is mid-edit and would not compile if you ran one.
|
|
183
|
+
// Reported ~15 false alarms in one evening at payment-blueprint, every one
|
|
184
|
+
// of which should have said "wait". It only ever changes the instruction —
|
|
185
|
+
// and only for the two refusals a working agent legitimately causes
|
|
186
|
+
// (holdExplains), never for a FAIL, a forgery or a skipped device tier,
|
|
187
|
+
// where the hold is not the cause and offering it would misdirect.
|
|
178
188
|
const flight = laneInFlight();
|
|
189
|
+
const hold = holdExplains(result, readReceipt(ROOT)) ? assessHold(readHold(ROOT)) : null;
|
|
179
190
|
const act = flight
|
|
180
191
|
? `A full check is ALREADY RUNNING${flight.step ? ` (${flight.step}${flight.index && flight.total ? `, step ${flight.index} of ${flight.total}` : ""})` : ""} — ` +
|
|
181
192
|
`wait for it to finish and commit its receipt. Do NOT start a second one; two lanes fight over the same build directory.`
|
|
182
|
-
:
|
|
183
|
-
|
|
193
|
+
: hold?.held
|
|
194
|
+
? describeHold(hold)
|
|
195
|
+
: "Run `node qa/verify.mjs` (it checks every promise and writes the receipt), " +
|
|
196
|
+
"commit the receipt, or see README §Verification enforcement to bypass.";
|
|
184
197
|
process.stderr.write(
|
|
185
198
|
`■ Prove — not done: the promises are not yet checked against this tree. ${result.reason}. ${act}\n`,
|
|
186
199
|
);
|
|
@@ -104,7 +104,7 @@ Profiles:
|
|
|
104
104
|
smoke the smallest end-to-end lane: every pure-Node gate through the real
|
|
105
105
|
runner, receipt and journal — no Gradle, no device. Seconds. Proves the
|
|
106
106
|
FRAMEWORK returns, both ways; never the change (its receipt is refused
|
|
107
|
-
as done-evidence). Driven by
|
|
107
|
+
as done-evidence). Driven by qa/framework-check.mjs.
|
|
108
108
|
scaffold spec coverage + build + unit tests (what \`create-cmp --verify\`
|
|
109
109
|
proves at stamp time)
|
|
110
110
|
local everything; device-dependent steps SKIP when no device is
|
|
@@ -69,6 +69,11 @@ const EXCLUDED_PREFIXES = [
|
|
|
69
69
|
"qa/.plan.json",
|
|
70
70
|
"qa/.request.json",
|
|
71
71
|
"qa/.plan-history.jsonl",
|
|
72
|
+
// Who is typing right now (qa/lib/agent-hold.mjs). A liveness declaration
|
|
73
|
+
// that could invalidate a receipt would mean an agent saying "I am here"
|
|
74
|
+
// un-proves the tree — the exact inversion this family of exclusions exists
|
|
75
|
+
// to prevent.
|
|
76
|
+
"qa/.agent-hold.json",
|
|
72
77
|
"qa/evidence",
|
|
73
78
|
"qa-artifacts",
|
|
74
79
|
"qa/comments.json",
|
package/template/gitignore
CHANGED
|
@@ -44,6 +44,7 @@ qa/.plan.json
|
|
|
44
44
|
# The closed-chain trail (drive-narration N5): local because it carries raw
|
|
45
45
|
# human prompts — the committed journal for lane runs stays qa/flight-recorder.jsonl.
|
|
46
46
|
qa/.plan-history.jsonl
|
|
47
|
+
qa/.agent-hold.json
|
|
47
48
|
|
|
48
49
|
# Android signing. The keystore IS the app's identity on Android — it cannot be reissued —
|
|
49
50
|
# and keystore.properties holds its passwords. Never committed, never in a sidecar diff.
|
package/template/qa/approve.mjs
CHANGED
|
@@ -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>
|
|
5
|
-
//
|
|
4
|
+
// node qa/approve.mjs <artifact> [<artifact> …]
|
|
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
|
|
@@ -251,7 +255,27 @@ if (args.length === 0) {
|
|
|
251
255
|
|
|
252
256
|
refuseIfUnresolvable();
|
|
253
257
|
|
|
254
|
-
|
|
258
|
+
// EVERY artifact named in one command — one write, one invalidation.
|
|
259
|
+
//
|
|
260
|
+
// A signature changes qa/approvals.json's `status`, which the approvals gate
|
|
261
|
+
// reads, so it legitimately moves the receipt's input hash. Signed one at a
|
|
262
|
+
// time AFTER a green lane, that means N signatures invalidate the receipt N
|
|
263
|
+
// times and cost N lane re-runs and N bookkeeping commits. Measured in
|
|
264
|
+
// payment-blueprint's log on 2026-09-04: of 21 commits in one session, 8 were
|
|
265
|
+
// "sign the approval" / "bind the receipt" with no product content, and every
|
|
266
|
+
// code change cost two to three commits. It compounds as a repo accumulates
|
|
267
|
+
// governed artifacts, which is what makes a session start fast and grind later.
|
|
268
|
+
//
|
|
269
|
+
// Two halves to the fix: sign them together (here), and sign BEFORE the final
|
|
270
|
+
// lane run so the receipt you keep is already the one that covers the
|
|
271
|
+
// signatures. `--reopen-feature` established the idiom — one recorded change,
|
|
272
|
+
// not N commands.
|
|
273
|
+
const artifactIds = args.filter((a, i) => !a.startsWith("--") && (i === 0 || !args[i - 1].startsWith("--")));
|
|
274
|
+
// An unrecognised flag must still be REFUSED by name, never silently skipped:
|
|
275
|
+
// filtering flags out is how `node qa/approve.mjs --delivr meal` would quietly
|
|
276
|
+
// approve nothing and exit 0. Falling back to the first argument reproduces the
|
|
277
|
+
// exact refusal an unknown verb has always produced.
|
|
278
|
+
if (artifactIds.length === 0) artifactIds.push(args[0]);
|
|
255
279
|
// The signer is REQUIRED, not optional: see approveArtifact's refusal. Parsed
|
|
256
280
|
// here rather than defaulted from git config on purpose — `git config user.name`
|
|
257
281
|
// is whatever the machine says, and an agent running on a developer's laptop
|
|
@@ -267,9 +291,14 @@ if (!approvedBy || approvedBy.startsWith("--")) {
|
|
|
267
291
|
);
|
|
268
292
|
process.exit(1);
|
|
269
293
|
}
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
294
|
+
const results = artifactIds.map((id) => ({ id, result: approveArtifact(ROOT, id, { via: "cli", approvedBy }) }));
|
|
295
|
+
const failed = results.filter((r) => !r.result.ok);
|
|
296
|
+
for (const { id, result } of results) {
|
|
297
|
+
if (result.ok) console.log(`✓ approved ${result.artifact} — hash ${shortHash(result.hash)}, at ${result.approvedAt}`);
|
|
298
|
+
else console.error(`error: ${id}: ${result.reason}`);
|
|
299
|
+
}
|
|
300
|
+
if (results.length > 1) {
|
|
301
|
+
const signed = results.length - failed.length;
|
|
302
|
+
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
303
|
}
|
|
275
|
-
|
|
304
|
+
if (failed.length) process.exit(1);
|