create-cmp-cli 0.22.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/harness-region.mjs +50 -2
- 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/harness-region.mjs +50 -2
- 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
|
@@ -53,17 +53,58 @@ import path from "node:path";
|
|
|
53
53
|
*/
|
|
54
54
|
export const HARNESS_DIRS = ["qa", "qa/lib"];
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The lane's OWN tests, when a project carries them (payment-blueprint's
|
|
58
|
+
* qa/test/**, 2026-09-03). Every `.mjs` under it, recursively, is in the
|
|
59
|
+
* region: the tests that prove the gates are locked WITH the gates, or the
|
|
60
|
+
* lane can no longer tell you that the suite vouching for its verdicts is the
|
|
61
|
+
* one it was locked with — a narrower claim in the same words, which is the
|
|
62
|
+
* failure this mechanism exists to prevent. A Compose app has no qa/test and
|
|
63
|
+
* its region (and lock) is unchanged.
|
|
64
|
+
*/
|
|
65
|
+
export const HARNESS_TEST_DIR = "qa/test";
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* DECLARATIONS the lane READS to decide what it attests — locked for the same
|
|
69
|
+
* reason verify.mjs is. payment-blueprint's planted proof (2026-09-03): remove
|
|
70
|
+
* one entry from qa/verified-surface.json and 203 files — the whole backend —
|
|
71
|
+
* stop being attested; the next lane run mints a fresh receipt over the smaller
|
|
72
|
+
* surface, harnessIntegrity PASS, nothing in the chain says the coverage moved.
|
|
73
|
+
* An edited checker and an edited definition of what the checker looks at are
|
|
74
|
+
* the same attack. State a lane WRITES (approvals.json, comments.json, the
|
|
75
|
+
* journal, evidence/) stays out — that is the EXCLUDED_PREFIXES distinction.
|
|
76
|
+
* Neither file exists in a Compose app by default; its region is unchanged.
|
|
77
|
+
*/
|
|
78
|
+
export const HARNESS_DECLARATIONS = ["qa/verified-surface.json", "qa/harness-manifest.json"];
|
|
79
|
+
|
|
56
80
|
/**
|
|
57
81
|
* Is this project-relative path part of the machine-owned harness region?
|
|
58
82
|
* @param {string} relPath project-relative path, "/"-separated
|
|
59
83
|
* @returns {boolean}
|
|
60
84
|
*/
|
|
61
85
|
export function isHarnessFile(relPath) {
|
|
62
|
-
if (typeof relPath !== "string"
|
|
86
|
+
if (typeof relPath !== "string") return false;
|
|
87
|
+
if (HARNESS_DECLARATIONS.includes(relPath)) return true;
|
|
88
|
+
if (!relPath.endsWith(".mjs")) return false;
|
|
89
|
+
if (relPath.startsWith(`${HARNESS_TEST_DIR}/`)) return true;
|
|
63
90
|
const dir = relPath.includes("/") ? relPath.slice(0, relPath.lastIndexOf("/")) : "";
|
|
64
91
|
return HARNESS_DIRS.includes(dir);
|
|
65
92
|
}
|
|
66
93
|
|
|
94
|
+
function walkMjs(dirAbs, relPrefix, out) {
|
|
95
|
+
let entries;
|
|
96
|
+
try {
|
|
97
|
+
entries = fs.readdirSync(dirAbs, { withFileTypes: true });
|
|
98
|
+
} catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
for (const ent of entries) {
|
|
102
|
+
const rel = `${relPrefix}/${ent.name}`;
|
|
103
|
+
if (ent.isDirectory()) walkMjs(path.join(dirAbs, ent.name), rel, out);
|
|
104
|
+
else if (ent.isFile() && isHarnessFile(rel)) out.push(rel);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
67
108
|
/**
|
|
68
109
|
* Every machine-owned file present under `root`, as project-relative posix
|
|
69
110
|
* paths, sorted — so the list (and any hash over it) is deterministic.
|
|
@@ -85,7 +126,14 @@ export function listHarnessFiles(root) {
|
|
|
85
126
|
if (isHarnessFile(rel)) found.push(rel);
|
|
86
127
|
}
|
|
87
128
|
}
|
|
88
|
-
|
|
129
|
+
walkMjs(path.join(root, HARNESS_TEST_DIR), HARNESS_TEST_DIR, found);
|
|
130
|
+
// A declaration directly under qa/ is already seen by the scan above (it is
|
|
131
|
+
// a harness file by name); the explicit loop covers one that lives deeper.
|
|
132
|
+
// Deduplicated so no path is hashed twice.
|
|
133
|
+
for (const rel of HARNESS_DECLARATIONS) {
|
|
134
|
+
if (fs.existsSync(path.join(root, ...rel.split("/")))) found.push(rel);
|
|
135
|
+
}
|
|
136
|
+
return [...new Set(found)].sort();
|
|
89
137
|
}
|
|
90
138
|
|
|
91
139
|
/** sha256 of one file's bytes, hex. */
|
|
@@ -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",
|
|
@@ -1244,7 +1244,7 @@ const stepsForProfile = {
|
|
|
1244
1244
|
// end-to-end lane — every pure-Node step through the REAL runner, marker,
|
|
1245
1245
|
// receipt and journal, and NO Gradle, no device, no network. Its job is to
|
|
1246
1246
|
// prove the framework RETURNS, fast, in both directions, before any real
|
|
1247
|
-
// work is pointed at it.
|
|
1247
|
+
// work is pointed at it. qa/framework-check.mjs drives it: PASS on a
|
|
1248
1248
|
// fresh scaffold, then FAIL BY NAME on one planted spec edit, each bounded
|
|
1249
1249
|
// in seconds. Its receipt is refused as done-evidence (qa/receipt-check.mjs)
|
|
1250
1250
|
// exactly like --fast: it proves the instrument, never the change.
|
package/template/qa/plan.mjs
CHANGED
|
@@ -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
|
);
|
package/template/qa/verify.mjs
CHANGED
|
@@ -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
|