create-cmp-cli 0.18.0 → 0.20.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/README.md +3 -3
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/packages/harness/src/approve.mjs +30 -2
- package/packages/harness/src/lib/approvals.mjs +74 -10
- package/packages/harness/src/lib/evidence-level.mjs +3 -1
- package/packages/harness/src/lib/flight-recorder.mjs +47 -2
- package/packages/harness/src/lib/inputs-hash.mjs +71 -3
- package/packages/harness/src/lib/lane-narrator.mjs +97 -0
- package/packages/harness/src/lib/lane-runner.mjs +173 -0
- package/packages/harness/src/lib/plan.mjs +286 -20
- package/packages/harness/src/lib/receipt-validate.mjs +56 -1
- package/packages/harness/src/lib/spec-coverage.mjs +111 -3
- package/packages/harness/src/lib/step-cache.mjs +1 -1
- package/packages/harness/src/lib/step-outcomes.mjs +123 -0
- package/packages/harness/src/lib/steps-cmp.mjs +1284 -0
- package/packages/harness/src/lib/walk.mjs +67 -17
- package/packages/harness/src/receipt-check.mjs +80 -4
- package/packages/harness/src/verify.mjs +119 -1197
- package/packages/receipts/src/index.mjs +1 -0
- package/packages/receipts/src/inputs-hash.mjs +71 -3
- package/packages/receipts/src/receipt-validate.mjs +56 -1
- package/template/CLAUDE.md +52 -6
- package/template/composeApp/src/desktopTest/kotlin/com/example/app/conformance/ArchitectureConformanceTest.kt +1 -1
- package/template/gitignore +3 -0
- package/template/qa/approve.mjs +30 -2
- package/template/qa/lib/approvals.mjs +74 -10
- package/template/qa/lib/evidence-level.mjs +3 -1
- package/template/qa/lib/flight-recorder.mjs +47 -2
- package/template/qa/lib/inputs-hash.mjs +71 -3
- package/template/qa/lib/lane-narrator.mjs +97 -0
- package/template/qa/lib/lane-runner.mjs +173 -0
- package/template/qa/lib/plan.mjs +286 -20
- package/template/qa/lib/receipt-validate.mjs +56 -1
- package/template/qa/lib/spec-coverage.mjs +111 -3
- package/template/qa/lib/step-cache.mjs +1 -1
- package/template/qa/lib/step-outcomes.mjs +123 -0
- package/template/qa/lib/steps-cmp.mjs +1284 -0
- package/template/qa/lib/walk.mjs +67 -17
- package/template/qa/receipt-check.mjs +80 -4
- package/template/qa/verify.mjs +119 -1197
- package/template/specs/README.md +26 -0
|
@@ -145,7 +145,7 @@ function walkOfFeature(root, f, lane = null) {
|
|
|
145
145
|
// recorded history (the lane journals every run), so it says what it
|
|
146
146
|
// costs — measured, never the agent's memory of it.
|
|
147
147
|
if (s.key === "prove" && lane)
|
|
148
|
-
return { ...s, state, note:
|
|
148
|
+
return { ...s, state, note: laneCostPhrase(lane) };
|
|
149
149
|
return { ...s, state };
|
|
150
150
|
});
|
|
151
151
|
|
|
@@ -224,20 +224,54 @@ export function laneTiming(root) {
|
|
|
224
224
|
} catch {
|
|
225
225
|
return null;
|
|
226
226
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
227
|
+
// EVERY recorded full run, not just the last one. A single "last run" figure
|
|
228
|
+
// is dominated by Gradle's cache state: a run that changed nothing reports the
|
|
229
|
+
// no-op cost (a 2s releaseBuild cache hit), and the operator plans a real
|
|
230
|
+
// change around it. Observed spread on one project: last 25s, median 140s,
|
|
231
|
+
// worst 558s. The distribution is the honest answer to "what will this cost";
|
|
232
|
+
// `durationMs` stays the last run so existing callers keep their meaning.
|
|
233
|
+
const full = [];
|
|
234
|
+
for (const line of text.split("\n")) {
|
|
235
|
+
if (line.trim() === "") continue;
|
|
231
236
|
let e;
|
|
232
237
|
try {
|
|
233
238
|
e = JSON.parse(line);
|
|
234
239
|
} catch {
|
|
235
240
|
continue;
|
|
236
241
|
}
|
|
237
|
-
if (e && e.mode !== "fast" && typeof e.durationMs === "number" && e.durationMs > 0)
|
|
238
|
-
return { durationMs: e.durationMs, verdict: e.verdict ?? null };
|
|
242
|
+
if (e && e.mode !== "fast" && typeof e.durationMs === "number" && e.durationMs > 0) full.push(e);
|
|
239
243
|
}
|
|
240
|
-
return null;
|
|
244
|
+
if (full.length === 0) return null;
|
|
245
|
+
const sorted = full.map((e) => e.durationMs).sort((a, b) => a - b);
|
|
246
|
+
const last = full[full.length - 1];
|
|
247
|
+
return {
|
|
248
|
+
durationMs: last.durationMs,
|
|
249
|
+
verdict: last.verdict ?? null,
|
|
250
|
+
runs: sorted.length,
|
|
251
|
+
// Median over an even count takes the lower of the two middles — a measured
|
|
252
|
+
// run rather than an average of two, so every figure quoted is one that
|
|
253
|
+
// actually happened.
|
|
254
|
+
medianMs: sorted[Math.floor((sorted.length - 1) / 2)],
|
|
255
|
+
maxMs: sorted[sorted.length - 1],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* What a full check costs, said honestly: the last run when that is all there
|
|
261
|
+
* is, and last + typical + worst once the journal can support a spread. Never
|
|
262
|
+
* an estimate — every number here is a run that happened (walk-legibility L4).
|
|
263
|
+
* @param {{durationMs: number, medianMs?: number, maxMs?: number, runs?: number}|null} lane
|
|
264
|
+
* @returns {string|null} null when nothing was ever recorded
|
|
265
|
+
*/
|
|
266
|
+
export function laneCostPhrase(lane) {
|
|
267
|
+
if (!lane || !(lane.durationMs > 0)) return null;
|
|
268
|
+
const last = `${humanDuration(lane.durationMs)} last full run`;
|
|
269
|
+
// One or two runs is not a distribution; saying "typical" over them would be
|
|
270
|
+
// the same overclaim in a new costume.
|
|
271
|
+
if (!(lane.runs > 2) || !(lane.medianMs > 0)) return `${last} (measured)`;
|
|
272
|
+
const spread =
|
|
273
|
+
lane.maxMs > lane.medianMs ? `, typically ${humanDuration(lane.medianMs)}, worst ${humanDuration(lane.maxMs)}` : `, typically ${humanDuration(lane.medianMs)}`;
|
|
274
|
+
return `${last}${spread} (measured over ${lane.runs} full runs)`;
|
|
241
275
|
}
|
|
242
276
|
|
|
243
277
|
/** "98s" under two minutes, "~5 min" above — for humans deciding whether to wait. */
|
|
@@ -251,10 +285,17 @@ export function humanDuration(ms) {
|
|
|
251
285
|
* record the console itself writes — a CROSS-PACKAGE CONTRACT with the
|
|
252
286
|
* inspector's preview-service.mjs (consoleRegistryPath): sha1(resolved
|
|
253
287
|
* root).slice(0,12), `cmp-console-<key>.json` in os.tmpdir(), fields
|
|
254
|
-
* {pid, port, url}. pid-liveness only, no HTTP — this runs inside a
|
|
288
|
+
* {pid, port, url, buildStale}. pid-liveness only, no HTTP — this runs inside a
|
|
255
289
|
* statusline with a <300ms budget.
|
|
256
|
-
*
|
|
257
|
-
*
|
|
290
|
+
*
|
|
291
|
+
* `buildStale` is the console's OWN verdict on itself (studio-self-renewal R5):
|
|
292
|
+
* a console serving code that is no longer the code on disk. It is carried on
|
|
293
|
+
* the record rather than recomputed here on purpose — the harness cannot hash
|
|
294
|
+
* the inspector's sources (in a scaffolded app the inspector is an npm package
|
|
295
|
+
* elsewhere), and a per-prompt hook must not make an HTTP call. The process
|
|
296
|
+
* that owns the fact publishes it; every consumer stays dumb.
|
|
297
|
+
* @returns {{url: string, buildStale: boolean} | {stale: true} | null} null = no
|
|
298
|
+
* record at all (never started, or stopped cleanly — silence, not an alarm)
|
|
258
299
|
*/
|
|
259
300
|
export function consoleState(root) {
|
|
260
301
|
try {
|
|
@@ -266,7 +307,10 @@ export function consoleState(root) {
|
|
|
266
307
|
} catch (err) {
|
|
267
308
|
if (!(err && err.code === "EPERM")) return { stale: true }; // record left by a crashed console
|
|
268
309
|
}
|
|
269
|
-
return {
|
|
310
|
+
return {
|
|
311
|
+
url: typeof rec.url === "string" ? rec.url : `http://127.0.0.1:${rec.port}/`,
|
|
312
|
+
buildStale: rec.buildStale === true,
|
|
313
|
+
};
|
|
270
314
|
} catch {
|
|
271
315
|
return null;
|
|
272
316
|
}
|
|
@@ -376,7 +420,7 @@ export function renderStatusline({ available, walks, arrivals, console: consoleR
|
|
|
376
420
|
// L6: the always-visible surface reports the other surface's death. A stale
|
|
377
421
|
// record means the console CRASHED (a clean stop removes it) — the failure
|
|
378
422
|
// mode was silence, and silence is the one thing this line never does.
|
|
379
|
-
const down = consoleRec && consoleRec.stale ? " · console down" : "";
|
|
423
|
+
const down = consoleRec && consoleRec.stale ? " · console down" : consoleRec && consoleRec.buildStale ? " · console stale" : "";
|
|
380
424
|
if (w.you.turn === "you") return `■ YOUR TURN — ${w.name}: ${w.you.act}${extra}${arrived}${down}`;
|
|
381
425
|
const now =
|
|
382
426
|
w.currentStage === "build" && w.promises.total > 0
|
|
@@ -397,7 +441,7 @@ export function renderCard(w, ctx = {}) {
|
|
|
397
441
|
.join(" ");
|
|
398
442
|
const gloss = STAGE_GLOSS[w.currentStage] ? ` — ${STAGE_GLOSS[w.currentStage]}` : "";
|
|
399
443
|
const laneNote =
|
|
400
|
-
w.currentStage === "prove" && ctx.lane ? ` (
|
|
444
|
+
w.currentStage === "prove" && ctx.lane ? ` (${laneCostPhrase(ctx.lane)} here)` : "";
|
|
401
445
|
const nowLine =
|
|
402
446
|
w.currentStage === "build" && w.promises.current
|
|
403
447
|
? `Now: keeping promise ${Math.min(w.promises.kept + 1, w.promises.total)} of ${w.promises.total} — “${w.promises.current.title || w.promises.current.id}”`
|
|
@@ -426,6 +470,12 @@ export function renderCard(w, ctx = {}) {
|
|
|
426
470
|
* standing instruction, not a discovery).
|
|
427
471
|
*/
|
|
428
472
|
function studioLine(consoleRec) {
|
|
473
|
+
// Order matters: a console that is UP but drawing from old code is not
|
|
474
|
+
// healthy, and reporting it as "running" is the clean bill of health that
|
|
475
|
+
// let this go unnoticed for a whole session. It heals itself (the worker
|
|
476
|
+
// renews on quiescence), so say what is true and why it may be waiting.
|
|
477
|
+
if (consoleRec && consoleRec.url && consoleRec.buildStale)
|
|
478
|
+
return `[studio: running at ${consoleRec.url} but STALE — it is serving code older than the tree, and everything it shows was drawn by that older code. It renews itself once no render or lane is in flight; if it stays stale, say so rather than citing what it shows.]`;
|
|
429
479
|
if (consoleRec && consoleRec.url) return `[studio: running at ${consoleRec.url}]`;
|
|
430
480
|
if (consoleRec && consoleRec.stale)
|
|
431
481
|
return "[studio: DOWN — it crashed (stale registry record). Restore it now: call the cmp-inspector `preview { projectDir }` tool (it starts a detached resident console), or tell the human it is down. Do not proceed silently.]";
|
|
@@ -452,8 +502,8 @@ export function renderInject(data) {
|
|
|
452
502
|
const chainText = renderChain(chain);
|
|
453
503
|
parts.push(
|
|
454
504
|
chainText !== ""
|
|
455
|
-
? `[the chain — the current request's steps. Keep it CURRENT: advance with \`node qa/plan.mjs --step N\` as steps land, \`--done\` when the request lands. A stale chain misleads the human watching the studio.]\n${chainText}`
|
|
456
|
-
: '[no chain declared. At kickoff, declare the request\'s steps
|
|
505
|
+
? `[the chain — the current request's steps. Keep it CURRENT: advance with \`node qa/plan.mjs --step N\` as steps land, \`--done\` when the request lands. If the human redirects, re-declare (\`--set\` again) — the chain is an offer they can reshape, not an announcement. A stale chain misleads the human watching the studio.]\n${chainText}`
|
|
506
|
+
: '[no chain declared. At kickoff, declare the request\'s steps and show them in your first reply AS AN OFFER — the human can reorder or redirect, and you re-declare without ceremony; work starts immediately either way: `node qa/plan.mjs --set "step | step | …" --title "<the ask, restated>"`.]',
|
|
457
507
|
);
|
|
458
508
|
if (walks.length > 0) {
|
|
459
509
|
parts.push("[walk-status — derived from the ledgers; render this state, never your own memory of it]");
|
|
@@ -462,7 +512,7 @@ export function renderInject(data) {
|
|
|
462
512
|
for (const a of arrivals)
|
|
463
513
|
parts.push(`▲ ARRIVED, UNPLANNED — ${a.label} (${a.status}): ${a.reason ?? "no recorded reason"}. Offer: handle now, or after the current walk lands (recommended: after).`);
|
|
464
514
|
parts.push(
|
|
465
|
-
"Protocol: open every reply with the chat header line above, verbatim. Speak stages as Decide·Design·Contract·Build·Prove·Sign-off — with their plain-words gloss on first mention — and clauses as promises. Declare the chain at kickoff and advance it as you go; if the studio line above says DOWN or not running, restore it (preview tool) or surface it before proceeding. Quiet between headers (one line per stage transition). At any human gate, render the full stop card: stage, what it is in plain words, then the easiest act first (the studio console when it is up; the CLI as fallback), then what comes after. Quote the lane's cost only from the measured figure in the card — never estimate it. Never open a second walk silently.",
|
|
515
|
+
"Protocol: open every reply with the chat header line above, verbatim. Speak stages as Decide·Design·Contract·Build·Prove·Sign-off — with their plain-words gloss on first mention — and clauses as promises. Declare the chain at kickoff and advance it as you go; if the studio line above says DOWN or not running, restore it (preview tool) or surface it before proceeding. Quiet between headers (one line per stage transition). At any human gate, render the full stop card: stage, what it is in plain words, then the easiest act first (the studio console when it is up; the CLI as fallback), then what comes after. Quote the lane's cost only from the measured figure in the card — never estimate it. Never open a second walk silently. Principles that bind THIS turn (docs/PRINCIPLES.md): derived, never claimed — name the command behind any claim; the layer you changed cannot certify itself — run its consumers, stamp a fresh app for a template or harness change; never wait on nothing — every wait is bounded, and if you are blocked, say on what and stop.",
|
|
466
516
|
);
|
|
467
517
|
return parts.join("\n\n");
|
|
468
518
|
}
|
|
@@ -28,6 +28,39 @@ const args = process.argv.slice(2);
|
|
|
28
28
|
const asHook = args.includes("--hook");
|
|
29
29
|
const asJson = args.includes("--json");
|
|
30
30
|
|
|
31
|
+
// The lane's own in-flight marker (verify.mjs stamps it, rewriting it at each
|
|
32
|
+
// step start with the step name and index). Mirrors qa/watch.mjs's bound.
|
|
33
|
+
const LANE_MARKER_REL = ["composeApp", "build", ".cmp-lane-in-progress"];
|
|
34
|
+
const LANE_MARKER_STALE_MS = 30 * 60 * 1000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The full check running RIGHT NOW, or null. The gate must still refuse — a
|
|
38
|
+
* lane in flight has not produced a receipt yet — but it must not tell the
|
|
39
|
+
* agent to start one. Repeating "run the lane" at a session whose lane is
|
|
40
|
+
* already ten minutes into its release build is an instruction to do the wrong
|
|
41
|
+
* thing, and it fired ~8 times in one observed session.
|
|
42
|
+
* @returns {{step: string|null, index: number|null, total: number|null}|null}
|
|
43
|
+
*/
|
|
44
|
+
function laneInFlight() {
|
|
45
|
+
try {
|
|
46
|
+
const p = path.join(ROOT, ...LANE_MARKER_REL);
|
|
47
|
+
const st = fs.statSync(p);
|
|
48
|
+
if (Date.now() - st.mtimeMs >= LANE_MARKER_STALE_MS) return null;
|
|
49
|
+
// Content is a bonus, never a requirement: legacy markers hold "pid iso".
|
|
50
|
+
try {
|
|
51
|
+
const n = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
52
|
+
if (n && typeof n === "object") {
|
|
53
|
+
return { step: n.step ?? null, index: n.index ?? null, total: n.total ?? null };
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
/* legacy marker — its EXISTENCE is the fact that matters */
|
|
57
|
+
}
|
|
58
|
+
return { step: null, index: null, total: null };
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
31
64
|
function readStdinJson() {
|
|
32
65
|
try {
|
|
33
66
|
const raw = fs.readFileSync(0, "utf8");
|
|
@@ -56,7 +89,40 @@ function evaluate() {
|
|
|
56
89
|
profile: receipt.profile,
|
|
57
90
|
};
|
|
58
91
|
}
|
|
59
|
-
|
|
92
|
+
// A nightly receipt proves the HARNESS and the tree's invariants under a
|
|
93
|
+
// forced double-run — never a change. Refused as done-evidence for the same
|
|
94
|
+
// reason --fast is: the receipt's own stage says what it is allowed to mean.
|
|
95
|
+
if (receipt.stage === "nightly" || receipt.profile === "nightly") {
|
|
96
|
+
return {
|
|
97
|
+
valid: false,
|
|
98
|
+
reason: "the last verify run was the nightly stage (it proves the harness, not this change); run the change-stage lane (`node qa/verify.mjs`) before finishing",
|
|
99
|
+
profile: receipt.profile,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// smoke (GATE-RULES Rule 0) runs no Gradle: it proves the framework returns,
|
|
103
|
+
// never that the change is good. Refused like --fast, for the same reason.
|
|
104
|
+
if (receipt.stage === "smoke" || receipt.profile === "smoke") {
|
|
105
|
+
return {
|
|
106
|
+
valid: false,
|
|
107
|
+
reason: "the last verify run was the smoke profile (the framework check — no build, no tests; it proves the instrument, not this change); run the change-stage lane (`node qa/verify.mjs`) before finishing",
|
|
108
|
+
profile: receipt.profile,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// A surface this project cannot resolve is a REFUSAL with an explanation,
|
|
112
|
+
// never an unhandled stack trace: this runs as the Stop hook on every turn
|
|
113
|
+
// end, and a crash there reads as a broken harness rather than as the
|
|
114
|
+
// misconfiguration it is. (evidence-economics S8 follow-up: computeInputsHash
|
|
115
|
+
// now throws rather than returning a confident hash of the empty set.)
|
|
116
|
+
let result;
|
|
117
|
+
try {
|
|
118
|
+
result = evaluateReceipt(receipt, () => computeInputsHash(ROOT));
|
|
119
|
+
} catch (err) {
|
|
120
|
+
return {
|
|
121
|
+
valid: false,
|
|
122
|
+
reason: `cannot verify this receipt — ${err && err.message ? err.message : String(err)}`,
|
|
123
|
+
profile: receipt.profile,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
60
126
|
// Surface the receipt's evidence rung (the ladder — qa/lib/evidence-level.mjs)
|
|
61
127
|
// alongside the verdict: the rung is the receipt's own derived field, read
|
|
62
128
|
// verbatim, never recomputed here. Older receipts without it stay valid.
|
|
@@ -78,10 +144,20 @@ if (asHook) {
|
|
|
78
144
|
// The walk's vocabulary (walk-legibility L3): this gate IS the Prove
|
|
79
145
|
// stage refusing to close — same fact, same enforcement, words that match
|
|
80
146
|
// every other surface. The precise reason stays verbatim beneath.
|
|
147
|
+
//
|
|
148
|
+
// The REFUSAL never changes with a lane in flight — no receipt yet means
|
|
149
|
+
// not done, and that is the whole point of the gate. What changes is the
|
|
150
|
+
// INSTRUCTION: "run the lane" is wrong advice when one is already running,
|
|
151
|
+
// and a gate that tells you to do the thing you are doing trains you to
|
|
152
|
+
// stop reading it.
|
|
153
|
+
const flight = laneInFlight();
|
|
154
|
+
const act = flight
|
|
155
|
+
? `A full check is ALREADY RUNNING${flight.step ? ` (${flight.step}${flight.index && flight.total ? `, step ${flight.index} of ${flight.total}` : ""})` : ""} — ` +
|
|
156
|
+
`wait for it to finish and commit its receipt. Do NOT start a second one; two lanes fight over the same build directory.`
|
|
157
|
+
: "Run `node qa/verify.mjs` (it checks every promise and writes the receipt), " +
|
|
158
|
+
"commit the receipt, or see README §Verification enforcement to bypass.";
|
|
81
159
|
process.stderr.write(
|
|
82
|
-
`■ Prove — not done: the promises are not yet checked against this tree.
|
|
83
|
-
`${result.reason}. Run \`node qa/verify.mjs\` (it checks every promise and writes the receipt), ` +
|
|
84
|
-
`commit the receipt, or see README §Verification enforcement to bypass.\n`,
|
|
160
|
+
`■ Prove — not done: the promises are not yet checked against this tree. ${result.reason}. ${act}\n`,
|
|
85
161
|
);
|
|
86
162
|
process.exit(2);
|
|
87
163
|
}
|