@dzhechkov/harness-cli 0.5.4 → 0.6.1
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/.dz-manifest.json +30 -14
- package/README.md +52 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +322 -3
- package/dist/cli.js.map +1 -1
- package/dist/core-compat.d.ts +1 -1
- package/dist/core-compat.js +1 -1
- package/keys/README.md +44 -5
- package/keys/dz.pub +3 -0
- package/package.json +2 -2
- package/sbom.json +53 -13
- package/src/cli.ts +330 -2
- package/src/core-compat.ts +1 -1
package/dist/core-compat.d.ts
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* standing test (`test/core-import-floor.test.ts`, F1) fails if the two drift apart,
|
|
40
40
|
* because a guard that says 0.4.7 while npm may install 0.4.2 is worse than no guard.
|
|
41
41
|
*/
|
|
42
|
-
export declare const MIN_CORE = "0.
|
|
42
|
+
export declare const MIN_CORE = "0.6.1";
|
|
43
43
|
/** The npm name of the guarded package — one literal, used by every leg below. */
|
|
44
44
|
export declare const CORE_PACKAGE_NAME = "@dzhechkov/harness-core";
|
|
45
45
|
/**
|
package/dist/core-compat.js
CHANGED
|
@@ -42,7 +42,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
42
42
|
* standing test (`test/core-import-floor.test.ts`, F1) fails if the two drift apart,
|
|
43
43
|
* because a guard that says 0.4.7 while npm may install 0.4.2 is worse than no guard.
|
|
44
44
|
*/
|
|
45
|
-
export const MIN_CORE = '0.
|
|
45
|
+
export const MIN_CORE = '0.6.1';
|
|
46
46
|
/** The npm name of the guarded package — one literal, used by every leg below. */
|
|
47
47
|
export const CORE_PACKAGE_NAME = '@dzhechkov/harness-core';
|
|
48
48
|
/**
|
package/keys/README.md
CHANGED
|
@@ -1,8 +1,47 @@
|
|
|
1
1
|
# Trust root
|
|
2
2
|
|
|
3
|
-
`dz.pub` — the pinned Ed25519 public key that `dz doctor`
|
|
4
|
-
|
|
5
|
-
every pack reports `no-trust-root`, and nothing fails.
|
|
3
|
+
`dz.pub` — the pinned Ed25519 **public** key that `dz doctor`, `dz drift-check` and `dz upgrade` fall
|
|
4
|
+
back to when verifying installed skill packs against their `.dz-manifest.json`.
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
**It is here now.** Until 2026-08-21 this directory shipped without `dz.pub` — it contained only this
|
|
7
|
+
README, which claimed no project key had been generated. The key had in fact existed at the repo root
|
|
8
|
+
since 2026-07-19. Measured with one binary run from two places on the same day:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
$ dz doctor # inside the harness repo, which has its own keys/dz.pub
|
|
12
|
+
signatures: 25 verified, 0 unsigned, 1 TAMPERED, 0 unverifiable; trust root: repo (keys/dz.pub)
|
|
13
|
+
$ dz doctor --project /tmp/consumer
|
|
14
|
+
signatures: 0 verified, 0 unsigned, 0 TAMPERED, 26 unverifiable; trust root: none
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
So **every installation that relied on the packaged key** — that is, one passing no `--pubkey` and
|
|
18
|
+
holding no project-local `keys/dz.pub` — had a verifier that verified nothing, and said so in a
|
|
19
|
+
summary line that scrolls past.
|
|
20
|
+
|
|
21
|
+
## Resolution order
|
|
22
|
+
|
|
23
|
+
An explicit `--pubkey` wins; then `keys/dz.pub` inside the project being checked; then this packaged
|
|
24
|
+
key. A `--pubkey` that lives INSIDE the pack being verified is refused — an artifact must never supply
|
|
25
|
+
the key that verifies it.
|
|
26
|
+
|
|
27
|
+
## What a signature proves, and what it does not
|
|
28
|
+
|
|
29
|
+
It proves that the files **listed in that pack's `.dz-manifest.json`** still hash to the values
|
|
30
|
+
recorded when it was signed. It says nothing about any file the manifest does not list, nothing about
|
|
31
|
+
whether the skill is any good, and nothing about whether the signer deserves trust: pinning this key
|
|
32
|
+
decides which key is ACCEPTED, which is a different question from whether its holder is trustworthy.
|
|
33
|
+
|
|
34
|
+
## Diagnosing a `TAMPERED` verdict
|
|
35
|
+
|
|
36
|
+
`TAMPERED` means the bytes on disk disagree with the signed manifest. Before assuming modification,
|
|
37
|
+
check which trust root was actually used — the summary line names it, and an explicit or project-local
|
|
38
|
+
key takes precedence over this one, so a verdict may be reported against a key you did not intend.
|
|
39
|
+
|
|
40
|
+
Re-signing is a **publisher-side** operation, not a consumer repair: it is correct only when you own
|
|
41
|
+
the pack, have reviewed what changed, and are re-issuing it. The usual innocent cause is an edit made
|
|
42
|
+
after signing, and its remedy is procedural — sign LAST, immediately before packing. A consumer facing
|
|
43
|
+
`TAMPERED` should report it, not silence it.
|
|
44
|
+
|
|
45
|
+
**Never** place a private key in this directory. The signing key lives outside the repository
|
|
46
|
+
(`~/.dz/keys/dz.key`, mode 0600), `dz sign` refuses to write it anywhere inside the tree, and a test
|
|
47
|
+
asserts no private-key PEM header appears anywhere in the published tarball.
|
package/keys/dz.pub
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/harness-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "The dz CLI — install AI skills for Claude Code, Codex, OpenCode, Hermes, OpenClaude, GitHub Copilot. 67 commands, 14 presets, 10 platform targets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"sbom.json"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@dzhechkov/harness-core": "^0.
|
|
44
|
+
"@dzhechkov/harness-core": "^0.6.1",
|
|
45
45
|
"@dzhechkov/harness-presets": "^0.5.0",
|
|
46
46
|
"@dzhechkov/scout": "^0.8.0",
|
|
47
47
|
"@dzhechkov/skills-devops": "^0.3.0",
|
package/sbom.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"hashes": [
|
|
16
16
|
{
|
|
17
17
|
"alg": "SHA-256",
|
|
18
|
-
"content": "
|
|
18
|
+
"content": "b99db6be2d9e6d46f0a9d3ff0877c445efac7b9e90d5fe1db9f5bfa2cb6dc107"
|
|
19
19
|
}
|
|
20
20
|
]
|
|
21
21
|
},
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"hashes": [
|
|
26
26
|
{
|
|
27
27
|
"alg": "SHA-256",
|
|
28
|
-
"content": "
|
|
28
|
+
"content": "696aae63a95e8f8726eecc1aa0e68f20d7980e99bb4008b22f2e1058ca67c6c0"
|
|
29
29
|
}
|
|
30
30
|
]
|
|
31
31
|
},
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"hashes": [
|
|
106
106
|
{
|
|
107
107
|
"alg": "SHA-256",
|
|
108
|
-
"content": "
|
|
108
|
+
"content": "8d15372b27f7d79f54df09671b01d5944e8f3f3d87075ba4ece7f0131fc1103e"
|
|
109
109
|
}
|
|
110
110
|
]
|
|
111
111
|
},
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"hashes": [
|
|
116
116
|
{
|
|
117
117
|
"alg": "SHA-256",
|
|
118
|
-
"content": "
|
|
118
|
+
"content": "be3648deb68dca8a48cc323f34f54c77c05f0ddf0d86982ea9d6f2cc8e073349"
|
|
119
119
|
}
|
|
120
120
|
]
|
|
121
121
|
},
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"hashes": [
|
|
126
126
|
{
|
|
127
127
|
"alg": "SHA-256",
|
|
128
|
-
"content": "
|
|
128
|
+
"content": "47bcecfe2c9f1daeb73056b3a2c1e10e6b77c465c1bb0d6169690f2276a9cc77"
|
|
129
129
|
}
|
|
130
130
|
]
|
|
131
131
|
},
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"hashes": [
|
|
136
136
|
{
|
|
137
137
|
"alg": "SHA-256",
|
|
138
|
-
"content": "
|
|
138
|
+
"content": "b735a6c18bfbe5adf17bbcfe6e2eeae71477b877e958c36e0a5e73e59f100d6a"
|
|
139
139
|
}
|
|
140
140
|
]
|
|
141
141
|
},
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
"hashes": [
|
|
156
156
|
{
|
|
157
157
|
"alg": "SHA-256",
|
|
158
|
-
"content": "
|
|
158
|
+
"content": "6d052e0a7352e9824bc1d4a319d03b42320f6551210f7dfd360afbdca4e556f0"
|
|
159
159
|
}
|
|
160
160
|
]
|
|
161
161
|
},
|
|
@@ -215,7 +215,17 @@
|
|
|
215
215
|
"hashes": [
|
|
216
216
|
{
|
|
217
217
|
"alg": "SHA-256",
|
|
218
|
-
"content": "
|
|
218
|
+
"content": "142fc5d0d59003476b8e7367d6fc21fe40ce9e0bf70159318a05abdb10502b8a"
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"type": "file",
|
|
224
|
+
"name": "keys/dz.pub",
|
|
225
|
+
"hashes": [
|
|
226
|
+
{
|
|
227
|
+
"alg": "SHA-256",
|
|
228
|
+
"content": "353885160582f8e0cfb2a97ef9e51f77997f0120841f92a1ce50d202ebf5dd73"
|
|
219
229
|
}
|
|
220
230
|
]
|
|
221
231
|
},
|
|
@@ -225,7 +235,7 @@
|
|
|
225
235
|
"hashes": [
|
|
226
236
|
{
|
|
227
237
|
"alg": "SHA-256",
|
|
228
|
-
"content": "
|
|
238
|
+
"content": "a43608842dab18914486c96c1989b294031f35118bb91c1e8bd0807041a10ddd"
|
|
229
239
|
}
|
|
230
240
|
]
|
|
231
241
|
},
|
|
@@ -245,7 +255,7 @@
|
|
|
245
255
|
"hashes": [
|
|
246
256
|
{
|
|
247
257
|
"alg": "SHA-256",
|
|
248
|
-
"content": "
|
|
258
|
+
"content": "d39fa1f437401419889965674889df3e5091d4ed76242e36f8e70f18683d4e4a"
|
|
249
259
|
}
|
|
250
260
|
]
|
|
251
261
|
},
|
|
@@ -255,7 +265,7 @@
|
|
|
255
265
|
"hashes": [
|
|
256
266
|
{
|
|
257
267
|
"alg": "SHA-256",
|
|
258
|
-
"content": "
|
|
268
|
+
"content": "5fef10b3d968d299eb7463157e8b164413872f7cc8bf5e9ca1c605e63569a9f7"
|
|
259
269
|
}
|
|
260
270
|
]
|
|
261
271
|
},
|
|
@@ -279,6 +289,16 @@
|
|
|
279
289
|
}
|
|
280
290
|
]
|
|
281
291
|
},
|
|
292
|
+
{
|
|
293
|
+
"type": "file",
|
|
294
|
+
"name": "test/amendment-check-cli.test.ts",
|
|
295
|
+
"hashes": [
|
|
296
|
+
{
|
|
297
|
+
"alg": "SHA-256",
|
|
298
|
+
"content": "3dd543fd3032f1fca5c063af98a7501b4cccb9d7e42f6c307074b67f2d55d231"
|
|
299
|
+
}
|
|
300
|
+
]
|
|
301
|
+
},
|
|
282
302
|
{
|
|
283
303
|
"type": "file",
|
|
284
304
|
"name": "test/cli.test.ts",
|
|
@@ -295,7 +315,7 @@
|
|
|
295
315
|
"hashes": [
|
|
296
316
|
{
|
|
297
317
|
"alg": "SHA-256",
|
|
298
|
-
"content": "
|
|
318
|
+
"content": "d9677688116c0a35591cb7e53ab1502566e44336959ac0ea449f5e8a1c5e31e3"
|
|
299
319
|
}
|
|
300
320
|
]
|
|
301
321
|
},
|
|
@@ -349,6 +369,16 @@
|
|
|
349
369
|
}
|
|
350
370
|
]
|
|
351
371
|
},
|
|
372
|
+
{
|
|
373
|
+
"type": "file",
|
|
374
|
+
"name": "test/feature-adr-record-cli.test.ts",
|
|
375
|
+
"hashes": [
|
|
376
|
+
{
|
|
377
|
+
"alg": "SHA-256",
|
|
378
|
+
"content": "e1a2687a1ae2bc2d32cbcc852487d30f9451ce1ea4a132e188186dd4251c49d1"
|
|
379
|
+
}
|
|
380
|
+
]
|
|
381
|
+
},
|
|
352
382
|
{
|
|
353
383
|
"type": "file",
|
|
354
384
|
"name": "test/fixtures/discrimination-check/fake-runner.sh",
|
|
@@ -639,6 +669,16 @@
|
|
|
639
669
|
}
|
|
640
670
|
]
|
|
641
671
|
},
|
|
672
|
+
{
|
|
673
|
+
"type": "file",
|
|
674
|
+
"name": "test/packaged-trust-root.test.ts",
|
|
675
|
+
"hashes": [
|
|
676
|
+
{
|
|
677
|
+
"alg": "SHA-256",
|
|
678
|
+
"content": "46c0af5494ad717eb4d936489f4bde2199b40cd756ab6c8adb0cd1714a971304"
|
|
679
|
+
}
|
|
680
|
+
]
|
|
681
|
+
},
|
|
642
682
|
{
|
|
643
683
|
"type": "file",
|
|
644
684
|
"name": "test/parallel-worktree-smoke.test.ts",
|
|
@@ -655,7 +695,7 @@
|
|
|
655
695
|
"hashes": [
|
|
656
696
|
{
|
|
657
697
|
"alg": "SHA-256",
|
|
658
|
-
"content": "
|
|
698
|
+
"content": "fafa2d1ff56bd41a6496b7ce569be671f2f617deac07425f47ad6e87b3ec00ce"
|
|
659
699
|
}
|
|
660
700
|
]
|
|
661
701
|
},
|
package/src/cli.ts
CHANGED
|
@@ -399,7 +399,20 @@ import {
|
|
|
399
399
|
serializeBundle,
|
|
400
400
|
parseBundle,
|
|
401
401
|
planImport,
|
|
402
|
+
decideCheckpointWrite,
|
|
403
|
+
amendmentSection,
|
|
404
|
+
planSaysNoAmendments,
|
|
405
|
+
parseAmendments,
|
|
406
|
+
resolveAmendments,
|
|
407
|
+
decideAmendmentOutcome,
|
|
408
|
+
amendmentVerdictLine,
|
|
409
|
+
amendmentsMissingFromPlan,
|
|
410
|
+
AMENDMENT_VACUITY_NOTE,
|
|
411
|
+
decideRecordWrite,
|
|
412
|
+
decideReadBack,
|
|
413
|
+
recordVerdictLine,
|
|
402
414
|
} from '@dzhechkov/harness-core';
|
|
415
|
+
import type { RecordKind, RecordDecision } from '@dzhechkov/harness-core';
|
|
403
416
|
import type { MutationEntryResult, MutationObservation, MutationRegistryEntry } from '@dzhechkov/harness-core';
|
|
404
417
|
import type { SkillApplyFailure, SkillLoadFailure } from '@dzhechkov/harness-core';
|
|
405
418
|
import type { ReqeDebt } from '@dzhechkov/harness-core';
|
|
@@ -449,6 +462,9 @@ Usage:
|
|
|
449
462
|
dz epoch-replay --judge <filled-work-order.json> [--out <file>] (blind judge prompts from the filled plans)
|
|
450
463
|
dz epoch-replay --score <judgments.json> --work-order <file> [--slice <name>] [--json] (un-blind against the pre-registered assignment → SUPPORTED only when the two 95% Wilson CIs are DISJOINT, else FALSIFIED / INCONCLUSIVE)
|
|
451
464
|
dz score --slug <feature> [--project <dir>] [--json] (process scorecard for ONE feature-adr run, from its artifacts: ADR confirmation, discrimination, cross-model QE grade, live verification, README-first, learning loop, amendments — descriptive-only, a low score exits 0)
|
|
465
|
+
dz amendment-check --slug <slug> | --feature-dir <dir> | --all [--json] (the deterministic Step-8 amendment gate: every AM-N row must resolve to a test found INSIDE the file the row names; the PLAN is authoritative when it carries rows, and an ideation amendment the plan drops is a failure. exit 0 pass/skip, 1 fail, 3 NOT-ESTABLISHED — a section that parsed ZERO rows is never a pass. --all is a CENSUS and always exits 0. Does NOT prove non-vacuity — that is dz discrimination-check)
|
|
466
|
+
dz feature-adr-record --kind ledger|training-pair --stage <s> [--slug <s>] [--row|--pair <json>] [--mark <n>] [--once] [--json] (the witnessed writer for the run-cost ledger and training pairs: the payload arrives as an ARGUMENT, never as shell; a malformed or wrong-kind payload is REFUSED before any write; the timestamp is stamped before serialising; the append is verified by re-reading the tail. exit 0 written|duplicate|skipped, 2 refused, 3 not-verified — a record failure is never blocking)
|
|
467
|
+
dz feature-adr-checkpoint (--slug <feature> | --feature-dir <abs>) --stage <s> --input-hash <h> --result <json> [--artifact a,b] [--json] (record a pipeline stage ONLY after measuring its artifacts on disk; refuses a null result, an absent artifact, or a stage that declares none — the subagent runs a COMMAND instead of hand-writing durable state)
|
|
452
468
|
dz reqe [--slug <feature> [--done --report <f>]] [--json] (the re-QE debt ledger: a usage-switched run whose Step-8 QE ran on the coder's OWN family records a debt; list debts, print the cross-family review brief, settle FAIL-CLOSED against a graded report — the settlement lands in 08_qe_report.md)
|
|
453
469
|
dz qe-bridge --family claude --slug <feature> [--coder-family codex|claude] [--model <id>] [--files a,b] [--out <f>] [--timeout <s>] [--allow-same-family] [--json] (the REVERSE QE bridge: run an INDEPENDENT Claude reviewer over a feature's Step-8 artifacts from ANY host — a Codex session included, plain shell, no Claude agent plane needed — and land a PARSED signoff. The reviewer runs ISOLATED: an EMPTY temp cwd plus --safe-mode --strict-mcp-config --tools '' --no-session-persistence, so no CLAUDE.md/skills/plugins/hooks/MCP load, and the verdict is read from the --output-format json RESULT ENVELOPE — text a session customization printed onto the same stdout can never become a signoff. Probes the model before trusting it; sends SCOPED extracts with a loud 200k-char ceiling (never silent truncation); the grade must AGREE across three LAST-anchored channels (terminal marker line, fenced qe-bridge-signoff JSON, the report's own GRADE line) AND the marker must be the FINAL content — empty, gradeless, self-contradicting or miscounted output is one of 18 NAMED failures with an audit record under features/<slug>/.fa-state/qe-bridge/ (runId, resolved executable + binOverride, prompt sha256, channel offsets, requestedOut, reportWritten, retained raw stdout; 0600 files in a 0700 dir), never a clean review. A --coder-family that contradicts the recorded reqe debt is refused. Writes features/<slug>/08b_reqe_report.md, which dz reqe --done settles unchanged. DISCLOSURE: the extracts you scope are sent to the Claude runtime; the bridge cannot classify secrets. DZ_QE_BRIDGE_CLAUDE_BIN is a TEST SEAM, not a flag. exit 0 signoff parsed (ANY grade — it reports, it does not gate) / 1 named failure / 2 usage)
|
|
454
470
|
dz mutation-gate [--package <dir>] [--registry <file>] [--test-cmd "<cmd>"] [--only <id[,id]>] [--timeout <ms>] [--rebaseline per-entry|final] [--keep-scratch] [--json] (prove each NAMED protection has a test that DISCRIMINATES: copy the package to a scratch dir, verify the baseline suite is green, apply each registry mutation, run the suite, REQUIRE red, restore. The red must be BEHAVIOURAL: a mutation that no longer parses is MUTATION_UNPARSEABLE; a red run whose OWN output reports a test FILE failing to load (node --test file-level not-ok with exitCode, vitest Failed Suites) is MUTATION_LOAD_FATAL — the signal comes from the same run as the failing count, never from a separate isolated import; red output whose shape matches no known runner is INCONCLUSIVE (a runner-coverage gap, loud, never PROVEN); a count far above the entry's bound is OVER_FAILING; a restored tree that does not reproduce green makes the entry INCONCLUSIVE (flaky). Mutation writes are realpath-contained to the scratch copy: a symlink escape or a node_modules/ target is refused (exit 2), the real tree is never written. A mutation that does not apply, a green suite, or an inconclusive run is a FAILURE — never a skip. exit 0 all proven / 1 gate failed / 2 setup error)
|
|
@@ -4789,7 +4805,7 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
4789
4805
|
// Reject unknown flags/options so a typo (e.g. `--dry-rum`) can NEVER be
|
|
4790
4806
|
// silently swallowed and flip the command into live-publish mode.
|
|
4791
4807
|
const allowedFlags = new Set(['dry-run', 'no-dry-run', 'yes', 'confirm', 'bump-only', 'help', 'require-signing', 'provenance', 'no-provenance']);
|
|
4792
|
-
const allowedOptions = new Set(['filter', 'claim-check', 'no-guard']);
|
|
4808
|
+
const allowedOptions = new Set(['filter', 'claim-check', 'no-guard', 'sign-key']);
|
|
4793
4809
|
const allowedHelp = ' allowed: --dry-run (default), --yes/--confirm/--no-dry-run (go live), --bump-only, --filter <substr>, --claim-check <off|warn|error>, --no-guard "<reason>" (skip the guard pre-flight; logged)';
|
|
4794
4810
|
for (const flag of flags) {
|
|
4795
4811
|
if (!allowedFlags.has(flag)) {
|
|
@@ -4939,7 +4955,29 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
|
|
|
4939
4955
|
}
|
|
4940
4956
|
}
|
|
4941
4957
|
|
|
4942
|
-
|
|
4958
|
+
// A signed pack must be RE-SIGNED after publish's own bump and README sync (feature
|
|
4959
|
+
// `sign-after-bump`): publish mutates the pack, so any earlier signature describes files that no
|
|
4960
|
+
// longer exist. Default to the same path `dz sign --init` writes, so the ordinary operator needs no
|
|
4961
|
+
// new flag; `--sign-key` overrides it.
|
|
4962
|
+
const signKey = (options.get('sign-key') ?? join(homedir(), '.dz', 'keys', 'dz.key')).trim();
|
|
4963
|
+
const report = publishPackages(cwd, {
|
|
4964
|
+
provenance,
|
|
4965
|
+
dryRun,
|
|
4966
|
+
filter,
|
|
4967
|
+
bumpOnly,
|
|
4968
|
+
claimGate: claimCheckOpt,
|
|
4969
|
+
signKey: signKey === '' ? undefined : resolve(cwd, signKey),
|
|
4970
|
+
reSign: (packDir: string, keyPath: string): void => {
|
|
4971
|
+
// The same three steps `dz sign` performs, including the SBOM — a manifest refreshed without
|
|
4972
|
+
// its SBOM would leave the two describing different trees.
|
|
4973
|
+
const files = packFiles(packDir);
|
|
4974
|
+
if (files.length === 0) throw new Error(`refusing to sign an empty pack: ${packDir}`);
|
|
4975
|
+
const manifest = buildManifest(packDir, basename(packDir), files);
|
|
4976
|
+
const signed = signManifest(manifest, readFileSync(keyPath, 'utf-8'));
|
|
4977
|
+
writeFileSync(join(packDir, MANIFEST_NAME), `${JSON.stringify(signed, null, 2)}\n`);
|
|
4978
|
+
writeFileSync(join(packDir, SBOM_NAME), `${JSON.stringify(buildSbom(manifest), null, 2)}\n`);
|
|
4979
|
+
},
|
|
4980
|
+
});
|
|
4943
4981
|
|
|
4944
4982
|
write(`\ndz publish${dryRun ? ' --dry-run' : ''}${bumpOnly ? ' --bump-only' : ''}${claimCheckOpt !== 'warn' ? ` --claim-check ${claimCheckOpt}` : ''}`);
|
|
4945
4983
|
write(` Published: ${report.published} Skipped: ${report.skipped} Errors: ${report.errors}\n`);
|
|
@@ -8937,6 +8975,290 @@ function scanOneReqeRoot(
|
|
|
8937
8975
|
* `dz reqe` — the re-QE debt ledger (backlog 6b40e667): list usage-switched same-family QE debts,
|
|
8938
8976
|
* print the cross-family review brief, settle FAIL-CLOSED against a graded report.
|
|
8939
8977
|
*/
|
|
8978
|
+
/**
|
|
8979
|
+
* `dz feature-adr checkpoint` — record a pipeline stage, but only after WITNESSING its artifacts.
|
|
8980
|
+
*
|
|
8981
|
+
* Why this is a command and not a line of shell (2026-08-21). The workflow script is sandboxed with
|
|
8982
|
+
* no filesystem, so it delegated checkpoint writes to a subagent by handing it a finished JSON line
|
|
8983
|
+
* and saying "append this". The subagent verified nothing. A safety classifier read that shape as one
|
|
8984
|
+
* party instructing another to declare a verification gate complete, and blocked NINE consecutive
|
|
8985
|
+
* writes in one run — router, four design substages, plan, code, qe, and the cost-ledger row. The
|
|
8986
|
+
* measured consequence: `.fa-state/checkpoints.jsonl` was never created, resume was silently dead,
|
|
8987
|
+
* and the six-hour run still reported success.
|
|
8988
|
+
*
|
|
8989
|
+
* The classifier's premise was wrong for those particular writes — every stage had run. Its instinct
|
|
8990
|
+
* was not: the old mechanism could not tell a real completion from a fabricated one, which is exactly
|
|
8991
|
+
* what a cross-family reviewer had already filed against the `fleet` stage. So the subagent now runs
|
|
8992
|
+
* THIS, and the verification lives in code under test instead of in the wording of a prompt.
|
|
8993
|
+
*
|
|
8994
|
+
* It measures `--artifact` paths on disk itself. A caller cannot assert presence; it can only name
|
|
8995
|
+
* what must be there.
|
|
8996
|
+
*/
|
|
8997
|
+
/**
|
|
8998
|
+
* `dz amendment-check` — the deterministic half of the Step-8 amendment gate (ADR-001).
|
|
8999
|
+
*
|
|
9000
|
+
* The gate used to be prompt text asking the QE agent to confirm every `AM-N` row names a real test.
|
|
9001
|
+
* That is layer 4 on the cost-of-detection ladder, and `features/qe-scoped-review` shipped with five
|
|
9002
|
+
* dangling ids and a plan recording `## Amendments: None`. This command owns I/O and the exit code;
|
|
9003
|
+
* `harness-core/src/amendment-trace.ts` owns the grammar and the rules.
|
|
9004
|
+
*/
|
|
9005
|
+
/**
|
|
9006
|
+
* `dz feature-adr-record` — the witnessed writer for the run-cost ledger and training pairs
|
|
9007
|
+
* (ADR-001 … ADR-003). The subagent stops being a COURIER handed a shell pipeline and becomes a
|
|
9008
|
+
* CALLER handed arguments: this command owns the paths, the refusal, the append, the READ-BACK and
|
|
9009
|
+
* the exit code. A courier can neither refuse nor verify, which is how four workflow runs finished
|
|
9010
|
+
* with no cost row at all.
|
|
9011
|
+
*/
|
|
9012
|
+
function cmdFeatureAdrRecord(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
9013
|
+
const json = flags.has('json');
|
|
9014
|
+
const kind = (options.get('kind') ?? '').trim() as RecordKind;
|
|
9015
|
+
const stage = (options.get('stage') ?? '').trim();
|
|
9016
|
+
const slug = (options.get('slug') ?? '').trim();
|
|
9017
|
+
const payloadRaw = options.get('row') ?? options.get('pair') ?? '';
|
|
9018
|
+
|
|
9019
|
+
const emit = (d: RecordDecision, extra: Record<string, unknown> = {}): number => {
|
|
9020
|
+
if (json) {
|
|
9021
|
+
write(JSON.stringify({ ok: d.exit === 0, kind, stage, verdict: d.verdict, exit: d.exit, reason: d.reason, blocking: d.blocking, ...extra }));
|
|
9022
|
+
} else {
|
|
9023
|
+
write(recordVerdictLine(kind === 'ledger' || kind === 'training-pair' ? kind : 'ledger', stage, d));
|
|
9024
|
+
}
|
|
9025
|
+
return d.exit;
|
|
9026
|
+
};
|
|
9027
|
+
|
|
9028
|
+
if (kind !== 'ledger' && kind !== 'training-pair') {
|
|
9029
|
+
write('dz feature-adr-record: --kind must be ledger or training-pair');
|
|
9030
|
+
return 2;
|
|
9031
|
+
}
|
|
9032
|
+
if (payloadRaw === '') {
|
|
9033
|
+
write('dz feature-adr-record: --row (ledger) or --pair (training-pair) is required');
|
|
9034
|
+
return 2;
|
|
9035
|
+
}
|
|
9036
|
+
|
|
9037
|
+
const repo = (options.get('project') ?? cwd).trim() || cwd;
|
|
9038
|
+
const target = kind === 'ledger'
|
|
9039
|
+
? join(repo, '.dz', 'feature-adr', 'run-cost-ledger.jsonl')
|
|
9040
|
+
: join(repo, '.dz', 'fa-training', slug === '' ? 'unknown' : slug, `${stage.replace(/[^\w.-]/g, '_')}.jsonl`);
|
|
9041
|
+
const markDir = join(repo, '.dz', 'fa-training', '.backfill-marks');
|
|
9042
|
+
const markName = (options.get('mark') ?? '').trim();
|
|
9043
|
+
const markPath = markName === '' ? null : join(markDir, markName.replace(/[^\w.-]/g, '_'));
|
|
9044
|
+
|
|
9045
|
+
const decision = decideRecordWrite({
|
|
9046
|
+
kind,
|
|
9047
|
+
payloadRaw,
|
|
9048
|
+
stage,
|
|
9049
|
+
stageProducedResult: flags.has('no-result') ? false : true,
|
|
9050
|
+
markExists: markPath !== null && existsSync(markPath),
|
|
9051
|
+
targetExists: existsSync(target),
|
|
9052
|
+
targetHasPair: flags.has('once') && existsSync(target),
|
|
9053
|
+
timestamp: new Date().toISOString(),
|
|
9054
|
+
});
|
|
9055
|
+
if (decision.line === null) return emit(decision);
|
|
9056
|
+
|
|
9057
|
+
// MEASURE, never assume: an unwritable target is a LOUD refusal, because a swallowed mkdir failure
|
|
9058
|
+
// is exactly how a write becomes a silent no-op (acid case A4).
|
|
9059
|
+
try {
|
|
9060
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
9061
|
+
if (markPath !== null) {
|
|
9062
|
+
mkdirSync(markDir, { recursive: true });
|
|
9063
|
+
mkdirSync(markPath);
|
|
9064
|
+
}
|
|
9065
|
+
} catch (err) {
|
|
9066
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
9067
|
+
if (markPath !== null && code === 'EEXIST') {
|
|
9068
|
+
// A stale mark (target absent) was already decided as writable above; re-taking it is a no-op,
|
|
9069
|
+
// not a duplicate. Only a mark WITH its target means another run got here first.
|
|
9070
|
+
if (decision.staleMark !== true) {
|
|
9071
|
+
return emit({ verdict: 'duplicate', exit: 0, reason: 'a mark for this record already exists — another run captured it first', blocking: false, line: null });
|
|
9072
|
+
}
|
|
9073
|
+
}
|
|
9074
|
+
return emit({ verdict: 'refused', exit: 2, reason: `the target could not be prepared: ${(err as Error).message}`, blocking: false, line: null });
|
|
9075
|
+
}
|
|
9076
|
+
|
|
9077
|
+
try {
|
|
9078
|
+
appendFileSync(target, `${decision.line}\n`, 'utf-8');
|
|
9079
|
+
} catch (err) {
|
|
9080
|
+
return emit({ verdict: 'refused', exit: 2, reason: `the append failed: ${(err as Error).message}`, blocking: false, line: null });
|
|
9081
|
+
}
|
|
9082
|
+
|
|
9083
|
+
// ADR-002: the write is verified against the disk, never inferred from the absence of an error.
|
|
9084
|
+
let lastLine: string | null = null;
|
|
9085
|
+
try {
|
|
9086
|
+
const body = readFileSync(target, 'utf-8');
|
|
9087
|
+
const lines = body.split('\n').filter((l) => l !== '');
|
|
9088
|
+
lastLine = lines.length > 0 ? (lines[lines.length - 1] as string) : null;
|
|
9089
|
+
} catch {
|
|
9090
|
+
lastLine = null;
|
|
9091
|
+
}
|
|
9092
|
+
return emit(decideReadBack(decision.line, lastLine), { target });
|
|
9093
|
+
}
|
|
9094
|
+
|
|
9095
|
+
function cmdAmendmentCheck(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
9096
|
+
const json = flags.has('json');
|
|
9097
|
+
const readOr = (abs: string): string | null => {
|
|
9098
|
+
try {
|
|
9099
|
+
return readFileSync(abs, 'utf-8');
|
|
9100
|
+
} catch {
|
|
9101
|
+
return null;
|
|
9102
|
+
}
|
|
9103
|
+
};
|
|
9104
|
+
|
|
9105
|
+
const checkOne = (featureDir: string): { decision: ReturnType<typeof decideAmendmentOutcome>; resolutions: ReturnType<typeof resolveAmendments>; slug: string } => {
|
|
9106
|
+
const slug = basename(featureDir);
|
|
9107
|
+
const ideation = readOr(join(featureDir, '03.5_ideation_report.md'));
|
|
9108
|
+
const plan = readOr(join(featureDir, '06_implementation_plan.md'));
|
|
9109
|
+
if (ideation === null) {
|
|
9110
|
+
return {
|
|
9111
|
+
slug,
|
|
9112
|
+
resolutions: [],
|
|
9113
|
+
decision: decideAmendmentOutcome({ sectionPresent: false, rows: [], resolutions: [], planSaysNone: false }),
|
|
9114
|
+
};
|
|
9115
|
+
}
|
|
9116
|
+
const sectionPresent = amendmentSection(ideation) !== null;
|
|
9117
|
+
const ideationRows = parseAmendments(ideation);
|
|
9118
|
+
const planRows = plan === null ? [] : parseAmendments(plan);
|
|
9119
|
+
// The PLAN is authoritative when it carries rows: Step 6 owes "carry AM-N into the plan
|
|
9120
|
+
// verbatim", and the ideation report is the historical record — rewriting its rows to match
|
|
9121
|
+
// tests named later would close the trail by falsifying it. Coverage keeps that honest below.
|
|
9122
|
+
const rows = planRows.length > 0 ? planRows : ideationRows;
|
|
9123
|
+
const missingFromPlan = planRows.length > 0 ? amendmentsMissingFromPlan(ideationRows, planRows) : [];
|
|
9124
|
+
// Paths in an amendment row are repo-relative, so they resolve against the repo root — not
|
|
9125
|
+
// against the feature directory, and not against wherever the caller happened to stand.
|
|
9126
|
+
const resolutions = resolveAmendments(rows, { readFile: (rel) => readOr(resolve(cwd, rel)) });
|
|
9127
|
+
const decision = decideAmendmentOutcome({
|
|
9128
|
+
sectionPresent,
|
|
9129
|
+
rows,
|
|
9130
|
+
resolutions,
|
|
9131
|
+
planSaysNone: plan !== null && planSaysNoAmendments(plan),
|
|
9132
|
+
missingFromPlan,
|
|
9133
|
+
});
|
|
9134
|
+
return { slug, decision, resolutions };
|
|
9135
|
+
};
|
|
9136
|
+
|
|
9137
|
+
// --all is a CENSUS, not a gate (ADR-003): a gate that is red on arrival over 21 historical
|
|
9138
|
+
// features gets disabled, and then the mechanism is gone along with the debt it was to surface.
|
|
9139
|
+
if (flags.has('all')) {
|
|
9140
|
+
const featuresDir = join(cwd, 'features');
|
|
9141
|
+
let slugs: string[] = [];
|
|
9142
|
+
try {
|
|
9143
|
+
slugs = readdirSync(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
9144
|
+
} catch {
|
|
9145
|
+
if (json) write(JSON.stringify({ ok: true, mode: 'all', features: [], note: 'no features/ directory' }));
|
|
9146
|
+
else write('amendment traceability census: no features/ directory');
|
|
9147
|
+
return 0;
|
|
9148
|
+
}
|
|
9149
|
+
const rowsOut: Record<string, unknown>[] = [];
|
|
9150
|
+
for (const slug of slugs) {
|
|
9151
|
+
try {
|
|
9152
|
+
const r = checkOne(join(featuresDir, slug));
|
|
9153
|
+
rowsOut.push({ slug, outcome: r.decision.outcome, counts: r.decision.counts, reasons: r.decision.reasons.length });
|
|
9154
|
+
} catch (err) {
|
|
9155
|
+
// AM-6: one unreadable feature becomes its own row. Dropping it silently would make the
|
|
9156
|
+
// census read like coverage it does not have.
|
|
9157
|
+
rowsOut.push({ slug, outcome: 'read-error', error: (err as Error).message });
|
|
9158
|
+
}
|
|
9159
|
+
}
|
|
9160
|
+
if (json) {
|
|
9161
|
+
write(JSON.stringify({ ok: true, mode: 'all', note: AMENDMENT_VACUITY_NOTE, features: rowsOut }));
|
|
9162
|
+
} else {
|
|
9163
|
+
const tally = new Map<string, number>();
|
|
9164
|
+
for (const r of rowsOut) tally.set(String(r['outcome']), (tally.get(String(r['outcome'])) ?? 0) + 1);
|
|
9165
|
+
for (const r of rowsOut) {
|
|
9166
|
+
if (r['outcome'] !== 'pass' && r['outcome'] !== 'skip') write(` [${String(r['outcome'])}] ${String(r['slug'])}`);
|
|
9167
|
+
}
|
|
9168
|
+
write(`amendment traceability census over ${rowsOut.length} feature(s): ${[...tally].map(([k, v]) => `${v} ${k}`).join(', ')}`);
|
|
9169
|
+
write(AMENDMENT_VACUITY_NOTE);
|
|
9170
|
+
}
|
|
9171
|
+
return 0;
|
|
9172
|
+
}
|
|
9173
|
+
|
|
9174
|
+
const explicitDir = (options.get('feature-dir') ?? '').trim();
|
|
9175
|
+
const slug = (options.get('slug') ?? '').trim();
|
|
9176
|
+
if (explicitDir === '' && slug === '') {
|
|
9177
|
+
write('dz amendment-check: one of --slug / --feature-dir is required (or --all for a census)');
|
|
9178
|
+
return 2;
|
|
9179
|
+
}
|
|
9180
|
+
const featureDir = explicitDir !== '' ? resolve(cwd, explicitDir) : join(cwd, 'features', slug);
|
|
9181
|
+
const { decision, resolutions } = checkOne(featureDir);
|
|
9182
|
+
if (json) {
|
|
9183
|
+
write(JSON.stringify({
|
|
9184
|
+
ok: decision.outcome === 'pass' || decision.outcome === 'skip',
|
|
9185
|
+
slug: basename(featureDir),
|
|
9186
|
+
outcome: decision.outcome,
|
|
9187
|
+
exit: decision.exit,
|
|
9188
|
+
counts: decision.counts,
|
|
9189
|
+
reasons: decision.reasons,
|
|
9190
|
+
rows: resolutions,
|
|
9191
|
+
note: AMENDMENT_VACUITY_NOTE,
|
|
9192
|
+
}));
|
|
9193
|
+
} else {
|
|
9194
|
+
for (const r of resolutions) {
|
|
9195
|
+
if (r.verdict !== 'resolved') write(` [${r.verdict}] ${r.id}${r.testId === null ? '' : ` \`${r.testId}\``} — ${r.detail}`);
|
|
9196
|
+
}
|
|
9197
|
+
write(AMENDMENT_VACUITY_NOTE);
|
|
9198
|
+
// The verdict is the LAST line, in the K2 gate's own shape, so a caller that reads the tail of
|
|
9199
|
+
// the output reads a verdict rather than a finding.
|
|
9200
|
+
write(amendmentVerdictLine(decision));
|
|
9201
|
+
}
|
|
9202
|
+
return decision.exit;
|
|
9203
|
+
}
|
|
9204
|
+
|
|
9205
|
+
function cmdFeatureAdrCheckpoint(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
9206
|
+
const json = flags.has('json');
|
|
9207
|
+
const emit = (payload: Record<string, unknown>, human: string, code: number): number => {
|
|
9208
|
+
if (json) write(JSON.stringify(payload));
|
|
9209
|
+
else write(human);
|
|
9210
|
+
return code;
|
|
9211
|
+
};
|
|
9212
|
+
const slug = (options.get('slug') ?? '').trim();
|
|
9213
|
+
const stage = (options.get('stage') ?? '').trim();
|
|
9214
|
+
const inputHash = (options.get('input-hash') ?? '').trim();
|
|
9215
|
+
const resultRaw = options.get('result') ?? '';
|
|
9216
|
+
const artifacts = (options.get('artifact') ?? '')
|
|
9217
|
+
.split(',')
|
|
9218
|
+
.map((a) => a.trim())
|
|
9219
|
+
.filter((a) => a !== '');
|
|
9220
|
+
if ((slug === '' && (options.get('feature-dir') ?? '').trim() === '') || stage === '' || inputHash === '') {
|
|
9221
|
+
return emit({ ok: false, reason: 'usage' }, 'dz feature-adr checkpoint: --stage, --input-hash and one of --slug / --feature-dir are required', 2);
|
|
9222
|
+
}
|
|
9223
|
+
let result: unknown;
|
|
9224
|
+
try {
|
|
9225
|
+
result = JSON.parse(resultRaw);
|
|
9226
|
+
} catch {
|
|
9227
|
+
return emit({ ok: false, reason: 'result is not valid JSON' }, 'dz feature-adr checkpoint: --result must be valid JSON', 2);
|
|
9228
|
+
}
|
|
9229
|
+
// --feature-dir wins when given: the workflow knows the absolute path already, and depending on
|
|
9230
|
+
// cwd resolution there would make the command's behaviour depend on where the subagent happened to
|
|
9231
|
+
// stand. --slug stays for humans running this by hand from a repo root.
|
|
9232
|
+
const explicitDir = (options.get('feature-dir') ?? '').trim();
|
|
9233
|
+
const featureDir = explicitDir !== '' ? explicitDir : join(cwd, 'features', slug);
|
|
9234
|
+
// MEASURE, never trust: presence is established here, by this process, on this disk.
|
|
9235
|
+
const present = artifacts.filter((rel) => {
|
|
9236
|
+
const abs = join(featureDir, rel);
|
|
9237
|
+
try {
|
|
9238
|
+
return statSync(abs).isFile();
|
|
9239
|
+
} catch {
|
|
9240
|
+
return false;
|
|
9241
|
+
}
|
|
9242
|
+
});
|
|
9243
|
+
const verdict = decideCheckpointWrite({ stage, inputHash, result, artifacts, present });
|
|
9244
|
+
if (!verdict.ok) {
|
|
9245
|
+
return emit({ ok: false, stage, reason: verdict.reason }, 'dz feature-adr checkpoint: REFUSED — ' + verdict.reason, 1);
|
|
9246
|
+
}
|
|
9247
|
+
const stateDir = join(featureDir, '.fa-state');
|
|
9248
|
+
try {
|
|
9249
|
+
mkdirSync(stateDir, { recursive: true });
|
|
9250
|
+
appendFileSync(join(stateDir, 'checkpoints.jsonl'), verdict.line + '\n', 'utf-8');
|
|
9251
|
+
} catch (err) {
|
|
9252
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
9253
|
+
return emit({ ok: false, stage, reason }, 'dz feature-adr checkpoint: write failed — ' + reason, 1);
|
|
9254
|
+
}
|
|
9255
|
+
return emit(
|
|
9256
|
+
{ ok: true, stage, witnessed: verdict.witnessed },
|
|
9257
|
+
'dz feature-adr checkpoint: recorded ' + stage + ' (witnessed ' + verdict.witnessed.length + ' artifact(s))',
|
|
9258
|
+
0,
|
|
9259
|
+
);
|
|
9260
|
+
}
|
|
9261
|
+
|
|
8940
9262
|
function cmdReqe(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
8941
9263
|
const json = flags.has('json');
|
|
8942
9264
|
if (flags.has('help')) {
|
|
@@ -11697,6 +12019,12 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
11697
12019
|
return cmdEpochReplay(options, flags, cwd, write);
|
|
11698
12020
|
case 'score':
|
|
11699
12021
|
return cmdScore(options, flags, cwd, write);
|
|
12022
|
+
case 'feature-adr-record':
|
|
12023
|
+
return cmdFeatureAdrRecord(options, flags, cwd, write);
|
|
12024
|
+
case 'amendment-check':
|
|
12025
|
+
return cmdAmendmentCheck(options, flags, cwd, write);
|
|
12026
|
+
case 'feature-adr-checkpoint':
|
|
12027
|
+
return cmdFeatureAdrCheckpoint(options, flags, cwd, write);
|
|
11700
12028
|
case 'reqe':
|
|
11701
12029
|
return cmdReqe(options, flags, cwd, write);
|
|
11702
12030
|
case 'qe-bridge':
|
package/src/core-compat.ts
CHANGED
|
@@ -44,7 +44,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
44
44
|
* standing test (`test/core-import-floor.test.ts`, F1) fails if the two drift apart,
|
|
45
45
|
* because a guard that says 0.4.7 while npm may install 0.4.2 is worse than no guard.
|
|
46
46
|
*/
|
|
47
|
-
export const MIN_CORE = '0.
|
|
47
|
+
export const MIN_CORE = '0.6.1';
|
|
48
48
|
|
|
49
49
|
/** The npm name of the guarded package — one literal, used by every leg below. */
|
|
50
50
|
export const CORE_PACKAGE_NAME = '@dzhechkov/harness-core';
|