@dzhechkov/harness-cli 0.3.235 → 0.3.237

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/harness-cli",
3
- "version": "0.3.235",
3
+ "version": "0.3.237",
4
4
  "description": "The dz CLI — install AI skills for Claude Code, Codex, OpenCode, Hermes, OpenClaude, GitHub Copilot. 35 commands, 13 presets, 6 platform targets.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -57,7 +57,7 @@
57
57
  "@dzhechkov/skills-reverse-engineering": "^0.1.0",
58
58
  "@dzhechkov/skills-presentation-storyteller": "^0.1.0",
59
59
  "@dzhechkov/skills-website-cloner": "^0.1.0",
60
- "@dzhechkov/harness-core": "0.3.126"
60
+ "@dzhechkov/harness-core": "0.3.128"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/node": "^25.6.0",
package/sbom.json CHANGED
@@ -9,86 +9,6 @@
9
9
  }
10
10
  },
11
11
  "components": [
12
- {
13
- "type": "file",
14
- "name": ".agentic-qe/brain.rvf",
15
- "hashes": [
16
- {
17
- "alg": "SHA-256",
18
- "content": "8136cd4cc145d3dbe6ed3ae4489946bbc26e3e44172f0e5241ac45dde38bfead"
19
- }
20
- ]
21
- },
22
- {
23
- "type": "file",
24
- "name": ".agentic-qe/brain.rvf.lock",
25
- "hashes": [
26
- {
27
- "alg": "SHA-256",
28
- "content": "99fa5178a48b6953b8f3a6f1690204a3b5449004b7dd105e7a3155ad3658266c"
29
- }
30
- ]
31
- },
32
- {
33
- "type": "file",
34
- "name": ".agentic-qe/memory.db",
35
- "hashes": [
36
- {
37
- "alg": "SHA-256",
38
- "content": "456372cb9a8c12f98ae6378338b8469d9be6df05cbe03a99b412d83d962b3d11"
39
- }
40
- ]
41
- },
42
- {
43
- "type": "file",
44
- "name": ".agentic-qe/memory.db-shm",
45
- "hashes": [
46
- {
47
- "alg": "SHA-256",
48
- "content": "cab99dd5d4068013a0c5f4f1c2d29dbec07729dc47c1d77adba6a53b773d0026"
49
- }
50
- ]
51
- },
52
- {
53
- "type": "file",
54
- "name": ".agentic-qe/memory.db-wal",
55
- "hashes": [
56
- {
57
- "alg": "SHA-256",
58
- "content": "2cc80ec378c1f9cd2588676588ee1b73bfe4f2f7ef3b96879c295eb86dccd2c2"
59
- }
60
- ]
61
- },
62
- {
63
- "type": "file",
64
- "name": ".agentic-qe/patterns.rvf",
65
- "hashes": [
66
- {
67
- "alg": "SHA-256",
68
- "content": "9090b2e6d339eaa183672ac0cf9f625dbd5f9fc76d64afcdb34a3f5871c31c5a"
69
- }
70
- ]
71
- },
72
- {
73
- "type": "file",
74
- "name": ".agentic-qe/patterns.rvf.idmap.json",
75
- "hashes": [
76
- {
77
- "alg": "SHA-256",
78
- "content": "c2693f9a7f47a2d2bffe3004188e70ce08411bf8db47d07c60551d18ff7640c4"
79
- }
80
- ]
81
- },
82
- {
83
- "type": "file",
84
- "name": ".agentic-qe/patterns.rvf.lock",
85
- "hashes": [
86
- {
87
- "alg": "SHA-256",
88
- "content": "84842f3916c558b1f4f2da2a5b0236587f146dda1f6ab4ba4c31ed7b8b17f943"
89
- }
90
- ]
91
- },
92
12
  {
93
13
  "type": "file",
94
14
  "name": "CHANGELOG.md",
@@ -105,7 +25,7 @@
105
25
  "hashes": [
106
26
  {
107
27
  "alg": "SHA-256",
108
- "content": "378ef9c8fcde4e77594b29a5f043b57f880f5df2b40fce679addfae756670154"
28
+ "content": "7e8b772a6fa9ca0f36364cafd5ea996abeb62dc894c2f67f50ea7704a7a8824d"
109
29
  }
110
30
  ]
111
31
  },
@@ -115,7 +35,7 @@
115
35
  "hashes": [
116
36
  {
117
37
  "alg": "SHA-256",
118
- "content": "1abb00d07ab57ad413d2747baa6a68cda0876a9e0c6f75db89c205fbf4863530"
38
+ "content": "667bf610bf890a7b93480a4b1045e98dc0c765effd80b78fde4e48538f595b3e"
119
39
  }
120
40
  ]
121
41
  },
@@ -125,7 +45,7 @@
125
45
  "hashes": [
126
46
  {
127
47
  "alg": "SHA-256",
128
- "content": "42d2a98f54c2ec773b49a0a43c959a78d070c8c427724f4affbd5bafef35621d"
48
+ "content": "593705e81f851c0e65ffa9feaa9bbd396e7a3295b9b21076cfd953ee5a05b93a"
129
49
  }
130
50
  ]
131
51
  },
@@ -185,7 +105,7 @@
185
105
  "hashes": [
186
106
  {
187
107
  "alg": "SHA-256",
188
- "content": "45c0c1328829a56cfe0ea0ebc74c15477a00bb683b496404688ab4b7b7a88ef8"
108
+ "content": "cf6a3265890a8856ef31e3e7ac58026c15232c3c118ba0fcd1ce0d7590b84dd8"
189
109
  }
190
110
  ]
191
111
  },
@@ -195,7 +115,7 @@
195
115
  "hashes": [
196
116
  {
197
117
  "alg": "SHA-256",
198
- "content": "20974a159438ac73a4c308cdb3710154dd9fe0450ed5184f6f3938792380fece"
118
+ "content": "b9af9be1bd31acd60e76275ff1055dd8e9b39b531b0c375467648f142b74d2c4"
199
119
  }
200
120
  ]
201
121
  },
@@ -205,7 +125,7 @@
205
125
  "hashes": [
206
126
  {
207
127
  "alg": "SHA-256",
208
- "content": "bfb9ecebeb1c3cc631f2c8e7f46af4318e3698789c9dda49aaebbe26fe1c7fa7"
128
+ "content": "6a51963ede51129e43524d62f3d757255a002dfec3df2c1dfa519af6f235a667"
209
129
  }
210
130
  ]
211
131
  },
@@ -265,7 +185,7 @@
265
185
  "hashes": [
266
186
  {
267
187
  "alg": "SHA-256",
268
- "content": "c03aafaeaf663bb47b756608049bf77f6460615f1e806054644ec830c524cf72"
188
+ "content": "1e7ef1d6e0796144999ad64d3650aeb4a2ccc7dfc66c5c5a8b705d783a9d0801"
269
189
  }
270
190
  ]
271
191
  },
@@ -285,7 +205,7 @@
285
205
  "hashes": [
286
206
  {
287
207
  "alg": "SHA-256",
288
- "content": "6b8d21416576a2744b5d9bf973d7b878c255495829fcf3679b183a977abc55a2"
208
+ "content": "5180330ac6612542c80a74ea8e0983f5cd231e95bb3962eccdd65fbdbf6611a8"
289
209
  }
290
210
  ]
291
211
  },
@@ -305,7 +225,7 @@
305
225
  "hashes": [
306
226
  {
307
227
  "alg": "SHA-256",
308
- "content": "09472c195254c359f14256c39e8c7afd08c0b71aadb57cd077534f0d7c482005"
228
+ "content": "461beed08ad6cfde7bbf559701fcbc6978f622c3951c80463c4e4943c538eff5"
309
229
  }
310
230
  ]
311
231
  },
package/src/cli.ts CHANGED
@@ -48,6 +48,9 @@ import {
48
48
  runInitAgentsMd,
49
49
  runInitGeminiMd,
50
50
  TARGET_NAMES,
51
+ buildParityMatrix,
52
+ TARGET_CAPABILITIES,
53
+ TARGET_SHORT_LABELS,
51
54
  WORKFLOW_NAMES,
52
55
  importEcc,
53
56
  recordPattern,
@@ -176,6 +179,12 @@ import {
176
179
  renderProseDiff,
177
180
  readScenarioIds,
178
181
  DEFAULT_MAX_JUDGE_RUNS,
182
+ collectDeliveryFacts,
183
+ planDeliveryCheck,
184
+ renderDeliveryBrief,
185
+ classifyDelivery,
186
+ isUsablePlaneResult,
187
+ renderDeliveryReview,
179
188
  } from '@dzhechkov/harness-core';
180
189
  import type { Family, ModelRung, Candidate as BtoCandidate, DimScores } from '@dzhechkov/harness-core';
181
190
  import type { SetupSpec } from '@dzhechkov/harness-core';
@@ -204,6 +213,8 @@ Usage:
204
213
  dz verify-pack --pack <dir> [--pubkey <path>] (signature check; fail-closed; key from the repo, never the pack)
205
214
  dz publish [--filter <name>] [--bump-only] [--claim-check <off|warn|error>] [--require-signing] [--provenance|--no-provenance] (dry-run by default; pass --yes/--confirm/--no-dry-run to go live; claim-check gate default warn — surfaces README claim findings, never blocks; error fails an offending package)
206
215
  dz release [--filter <name>] [--tag] [--publish] [--json] [--dry-run] [--no-issue] (VERIFIED release: 4 HARD gates in FRONT of dz publish — full package test suites, audit >=high, node --check of every dist/bin file, bin smoke-boot via "node <bin> --help" — any red gate STOPS the release (exit 1) + best-effort gh issue; all green ⇒ re-sign reminder, then prints the ready dz publish command (or chains with --publish); never duplicates publish's own gates)
216
+ dz parity [--target <name>] [--json] (the honest feature×target map, COMPUTED from the capability model — which harness feature is full / manual / absent on each of the ${TARGET_NAMES.length} targets, and via which form)
217
+ dz delivery-check --slug <slug> [--context-only] [--findings <f.json>] [--strict] [--author <model>] [--json] (portable Step-10 Delivery Gate: prints the 4-plane review brief + artifact probes; --findings classifies a fed-back review into a fail-closed ready|blocked hand-off and writes features/<slug>/10_delivery_review.md; --strict exits 1 on blocked)
207
218
  dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--project <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force] [--enrich]
208
219
  dz teach "<pattern>" [--reward <0-1>] [--domain <name>] [--type rule|success-pattern|lesson-learned] [--project <dir>] [--no-mirror] (--project pins the learned store to <dir>/.dz, not the cwd — pin to a canonical brain)
209
220
  dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
@@ -3571,6 +3582,88 @@ function cmdPublish(options: Map<string, string>, flags: Set<string>, cwd: strin
3571
3582
  return report.errors > 0 ? 1 : 0;
3572
3583
  }
3573
3584
 
3585
+ /* ------------------------------------------------------------------ */
3586
+ /* dz parity — the honest feature×target map (target-parity-matrix, */
3587
+ /* ADR-001): computed from the declarative model, never hand-written */
3588
+ /* ------------------------------------------------------------------ */
3589
+
3590
+ function cmdParity(options: Map<string, string>, flags: Set<string>, write: Write): number {
3591
+ const json = flags.has('json');
3592
+ if (flags.has('help')) {
3593
+ write('dz parity [--target <name>] [--json] — the computed feature×target map (never hand-written)');
3594
+ write(` targets: ${TARGET_NAMES.join(', ')}`);
3595
+ return 0;
3596
+ }
3597
+ const allowedFlags = new Set(['json', 'help']);
3598
+ const allowedOptions = new Set(['target']);
3599
+ // A stray positional is a typo'd invocation, not something to silently ignore (Codex QE gap 7).
3600
+ if (options.has('_positional_0')) {
3601
+ write(json ? JSON.stringify({ error: `unexpected argument "${options.get('_positional_0')}"`, exitCode: 1 }) : `dz parity: unexpected argument "${options.get('_positional_0')}"\n allowed: --target <name>, --json`);
3602
+ return 1;
3603
+ }
3604
+ for (const flag of flags) {
3605
+ if (!allowedFlags.has(flag)) {
3606
+ write(json ? JSON.stringify({ error: `unknown option --${flag}`, exitCode: 1 }) : `dz parity: unknown option --${flag}\n allowed: --target <name>, --json`);
3607
+ return 1;
3608
+ }
3609
+ }
3610
+ for (const key of options.keys()) {
3611
+ if (key.startsWith('_positional_')) continue;
3612
+ if (!allowedOptions.has(key)) {
3613
+ write(json ? JSON.stringify({ error: `unknown option --${key}`, exitCode: 1 }) : `dz parity: unknown option --${key}\n allowed: --target <name>, --json`);
3614
+ return 1;
3615
+ }
3616
+ }
3617
+
3618
+ const matrix = buildParityMatrix();
3619
+ const target = options.get('target');
3620
+ if (target !== undefined && !TARGET_NAMES.includes(target as (typeof TARGET_NAMES)[number])) {
3621
+ if (json) write(JSON.stringify({ error: `unknown target "${target}"`, targets: TARGET_NAMES, exitCode: 1 }, null, 2));
3622
+ else write(`dz parity: unknown target "${target}" — one of: ${TARGET_NAMES.join(', ')}`);
3623
+ return 1;
3624
+ }
3625
+
3626
+ if (json) {
3627
+ const rows = matrix.map((r) => ({
3628
+ id: r.feature.id,
3629
+ title: r.feature.title,
3630
+ cells: target !== undefined ? { [target]: r.cells[target as (typeof TARGET_NAMES)[number]] } : r.cells,
3631
+ }));
3632
+ // A filtered response stays internally consistent: capabilities are filtered too (Codex QE gap 9).
3633
+ const caps = target !== undefined ? { [target]: TARGET_CAPABILITIES[target as (typeof TARGET_NAMES)[number]] } : TARGET_CAPABILITIES;
3634
+ write(JSON.stringify({ targets: target !== undefined ? [target] : TARGET_NAMES, capabilities: caps, features: rows }, null, 2));
3635
+ return 0;
3636
+ }
3637
+
3638
+ if (target !== undefined) {
3639
+ const t = target as (typeof TARGET_NAMES)[number];
3640
+ write(`\ndz parity — ${t} (capabilities: ${TARGET_CAPABILITIES[t].join(', ')})\n`);
3641
+ for (const r of matrix) {
3642
+ const c = r.cells[t];
3643
+ const icon = c.level === 'full' ? '✓' : c.level === 'manual' ? '◐' : '—';
3644
+ write(` ${icon} ${r.feature.title.padEnd(58)} ${c.level === 'none' ? 'not available on this target' : `via ${c.via ?? ''}`}`);
3645
+ }
3646
+ write('\n ✓ full (the complete experience) ◐ manual (works, you drive it by hand) — not available');
3647
+ return 0;
3648
+ }
3649
+
3650
+ // Grid: one column per target, short header, one row per feature. Computed, never hand-written;
3651
+ // the labels live NEXT TO the model (a local map here would be a second, unchecked registry).
3652
+ const short: Record<string, string> = TARGET_SHORT_LABELS;
3653
+ write('\ndz parity — feature × target (computed from the model; dz parity --target <name> for detail)\n');
3654
+ write(` ${'feature'.padEnd(52)} ${TARGET_NAMES.map((t) => (short[t] ?? t).padStart(4)).join('')}`);
3655
+ for (const r of matrix) {
3656
+ const cells = TARGET_NAMES.map((t) => {
3657
+ const c = r.cells[t];
3658
+ return (c.level === 'full' ? '✓' : c.level === 'manual' ? '◐' : '—').padStart(4);
3659
+ }).join('');
3660
+ write(` ${r.feature.title.slice(0, 52).padEnd(52)} ${cells}`);
3661
+ }
3662
+ write('\n ✓ full ◐ manual (available, driven by hand) — not available');
3663
+ write(` columns: ${TARGET_NAMES.map((t) => `${short[t] ?? t}=${t}`).join(' ')}`);
3664
+ return 0;
3665
+ }
3666
+
3574
3667
  /* ------------------------------------------------------------------ */
3575
3668
  /* dz release — verified-release conveyor (feature release-verified, */
3576
3669
  /* ADR-001): 4 HARD gates in FRONT of the untouched dz publish. */
@@ -5115,8 +5208,18 @@ function cmdFeatureAdrSetup(options: Map<string, string>, flags: Set<string>, cw
5115
5208
  return 1;
5116
5209
  }
5117
5210
 
5211
+ // portable-gates (direction b): --gates scaffolds the zero-config architecture/gates/delivery-check.md.
5212
+ // Its "runnable here" list is computed for --target (default agents-md, the AGENTS.md-class target class).
5213
+ const wantGates = flags.has('gates');
5214
+ const targetOpt = options.get('target');
5215
+ if (targetOpt !== undefined && !isTargetName(targetOpt)) {
5216
+ write(`dz feature-adr-setup: --target must be one of: ${TARGET_NAMES.join(', ')}`);
5217
+ return 1;
5218
+ }
5219
+ const gatesTarget: TargetName = isTargetName(targetOpt ?? '') ? (targetOpt as TargetName) : 'agents-md';
5220
+
5118
5221
  const specPath = options.get('from-spec');
5119
- if (specPath === undefined && !wantGuards) {
5222
+ if (specPath === undefined && !wantGuards && !wantGates) {
5120
5223
  // default + --plan: the read-only "which documents, and where?" answer.
5121
5224
  const plan = buildSetupPlan(scanForSetup(repoRoot));
5122
5225
  if (flags.has('json')) { write(JSON.stringify(plan, null, 2)); return 0; }
@@ -5138,8 +5241,9 @@ function cmdFeatureAdrSetup(options: Map<string, string>, flags: Set<string>, cw
5138
5241
  spec = {}; // --guards standalone: scaffold just the guard set
5139
5242
  }
5140
5243
  if (wantGuards) spec = { ...spec, guards: locCap !== undefined ? { locCap } : true };
5244
+ if (wantGates) spec = { ...spec, gates: true };
5141
5245
 
5142
- const result = scaffoldFromSpec(spec, readExistingForScaffold(repoRoot));
5246
+ const result = scaffoldFromSpec(spec, readExistingForScaffold(repoRoot), gatesTarget);
5143
5247
  if (flags.has('json') && !flags.has('apply')) { write(JSON.stringify(result, null, 2)); return 0; }
5144
5248
  write(renderScaffoldPreview(result));
5145
5249
 
@@ -5342,6 +5446,137 @@ function nameFor(t: { file: string; name?: string }, outcome: 'pass' | 'fail' |
5342
5446
  return t.name !== undefined ? { file: t.file, name: t.name, outcome } : { file: t.file, outcome };
5343
5447
  }
5344
5448
 
5449
+ /**
5450
+ * `dz delivery-check` — the portable Step-10 Delivery Gate (feature portable-gates). The `manual` form that
5451
+ * travels to every `shell` target: the deterministic parts (artifact probes, hand-off arithmetic,
5452
+ * cross-validation bookkeeping) run IN the CLI; the semantic 4-plane review is DISPATCHED to the caller's own
5453
+ * agent runtime — the `dz challenge` cartridge shape. All logic lives in harness-core's pure engine; this is
5454
+ * the executor. Advisory exit codes (0 by default; `--strict` ⇒ 1 on `blocked`).
5455
+ *
5456
+ * --slug <slug> the feature under features/<slug> (required; featureDir derived internally)
5457
+ * --context-only default mode: print artifact probes + the 4-plane review brief (no verdict)
5458
+ * --findings <f.json> classify a fed-back findings array → write 10_delivery_review.md + print ready/blocked
5459
+ * --strict with --findings: exit 1 iff handoff === 'blocked' (CI gate)
5460
+ * --author <model> cosmetic: the reviewer to dispatch, printed alongside the brief
5461
+ * --json machine contract { planesChecked, planesSkipped, findings, handoff, artifact }
5462
+ */
5463
+ function cmdDeliveryCheck(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
5464
+ let repoRoot = cwd;
5465
+ try { repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || cwd; } catch { /* not git */ }
5466
+
5467
+ const json = flags.has('json');
5468
+ const errOut = (error: string): number => {
5469
+ if (json) write(JSON.stringify({ error, planesChecked: [], planesSkipped: [], findings: [], handoff: null, artifact: 'not-written' }, null, 2));
5470
+ else { write(`dz delivery-check: ${error}`); write(' allowed: --slug <slug>, --findings <f.json>, --author <model>, --context-only, --strict, --json'); }
5471
+ return 1;
5472
+ };
5473
+ // Strict allowlist (delivery finding: a typo'd --findings silently downgraded the --strict CI
5474
+ // gate to a vacuous exit-0 pass — a mistyped option must be an ERROR, never a mode change).
5475
+ const allowedFlags = new Set(['context-only', 'strict', 'json', 'help']);
5476
+ const allowedOptions = new Set(['slug', 'findings', 'author']);
5477
+ for (const flag of flags) if (!allowedFlags.has(flag)) return errOut(`unknown option --${flag}`);
5478
+ for (const key of options.keys()) {
5479
+ if (key.startsWith('_positional_')) return errOut(`unexpected argument "${options.get(key)}"`);
5480
+ if (!allowedOptions.has(key)) return errOut(`unknown option --${key}`);
5481
+ }
5482
+
5483
+ const slug = options.get('slug');
5484
+ if (slug === undefined || slug.trim() === '') return errOut('pass --slug <slug> (the feature under features/<slug>)');
5485
+ // Slug convention is a SECURITY boundary here (delivery finding, CONFIRMED traversal reproducer:
5486
+ // --slug '../../outside/evil' wrote 10_delivery_review.md outside the repo). kebab-case Latin,
5487
+ // <=40 chars — the repo's own slug rule as a layer-1 three-line check.
5488
+ const slugTrimmed = slug.trim();
5489
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(slugTrimmed) || slugTrimmed.length > 40) {
5490
+ return errOut(`invalid --slug "${slugTrimmed}" — kebab-case Latin, max 40 chars (a path-like slug would redirect the report write)`);
5491
+ }
5492
+ const featureDir = join(repoRoot, 'features', slugTrimmed);
5493
+
5494
+ // --context-only is a real mode flag, not decoration: combining it with --findings contradicts
5495
+ // itself (delivery finding — the flag was accepted and silently ignored).
5496
+ if (flags.has('context-only') && options.get('findings') !== undefined) {
5497
+ return errOut('--context-only and --findings are mutually exclusive (context-only prints the brief; findings classifies the verdict)');
5498
+ }
5499
+
5500
+ // git status --porcelain — CLI layer only (the `no child_process in core` contract); a non-git target
5501
+ // degrades to an empty list, which is honest (AM-10: changed-files is informational, never a required gate).
5502
+ let changedFiles: string[] = [];
5503
+ try {
5504
+ const out = execSync('git status --porcelain', { cwd: repoRoot, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
5505
+ changedFiles = out.split('\n').map((l) => l.slice(3).trim()).filter((s) => s.length > 0);
5506
+ } catch { /* non-git / unavailable → empty (honest) */ }
5507
+
5508
+ const facts = collectDeliveryFacts(featureDir, { changedFiles, repoRoot });
5509
+ const plan = planDeliveryCheck(facts);
5510
+ const author = options.get('author');
5511
+
5512
+ const findingsPath = options.get('findings');
5513
+ if (findingsPath === undefined) {
5514
+ // Default / --context-only: emit the led protocol (probes + brief). No verdict — the CLI has no model.
5515
+ if (json) {
5516
+ write(JSON.stringify({
5517
+ planesChecked: [],
5518
+ planesSkipped: plan.planes.map((p) => p.id),
5519
+ findings: [],
5520
+ handoff: null,
5521
+ artifact: 'not-written',
5522
+ probes: plan.probes,
5523
+ }, null, 2));
5524
+ return 0;
5525
+ }
5526
+ write(`dz delivery-check — ${slug} (portable 4-plane hand-off protocol; you drive the review)`);
5527
+ for (const p of plan.probes) {
5528
+ const mark = p.passed === true ? '✓' : p.passed === false ? '✗' : '○';
5529
+ write(` ${mark} ${p.description}${p.required ? ' [required]' : ' [informational]'}`);
5530
+ }
5531
+ write('');
5532
+ write(renderDeliveryBrief(plan, facts));
5533
+ // The hand-off criterion the verdict will be judged by, shown UP FRONT (this also makes
5534
+ // plan.criterionTemplate a consumed surface, not dead product code — delivery finding).
5535
+ write('\n── hand-off criterion (all must PASS for ready) ──');
5536
+ for (const c of plan.criterionTemplate) write(` ○ ${c.label}`);
5537
+ if (author !== undefined && author.trim() !== '') write(`\n── reviewer to dispatch ──\n${author.trim()}`);
5538
+ write('\n(next: run the four planes, then `dz delivery-check --slug ' + slug + ' --findings findings.json` for the verdict)');
5539
+ return 0;
5540
+ }
5541
+
5542
+ // --findings mode: parse (malformed/missing ⇒ [], a failed-plane input — fail-closed, never a throw).
5543
+ let reviewResults: unknown[] = [];
5544
+ try {
5545
+ const parsed: unknown = JSON.parse(readFileSync(resolve(cwd, findingsPath), 'utf-8'));
5546
+ if (Array.isArray(parsed)) reviewResults = parsed;
5547
+ else if (parsed !== null && typeof parsed === 'object' && Array.isArray((parsed as { planes?: unknown }).planes)) reviewResults = (parsed as { planes: unknown[] }).planes;
5548
+ else reviewResults = [];
5549
+ } catch { reviewResults = []; }
5550
+
5551
+ const verdict = classifyDelivery(plan, reviewResults as ({ findings?: readonly unknown[] } | null | undefined)[]);
5552
+ // The SAME predicate as the fail-closed verdict (core-exported) — a local copy was a drift channel.
5553
+ const planesChecked = plan.planes.filter((_, i) => isUsablePlaneResult(reviewResults[i])).map((p) => p.id);
5554
+ const planesSkipped = plan.planes.filter((_, i) => !isUsablePlaneResult(reviewResults[i])).map((p) => p.id);
5555
+
5556
+ // Write the regenerable report (NOT augment-never-clobber — deliberately overwritten each run).
5557
+ let artifact: 'written' | 'not-written' = 'not-written';
5558
+ try {
5559
+ const reviewPath = join(featureDir, '10_delivery_review.md');
5560
+ mkdirSync(dirname(reviewPath), { recursive: true });
5561
+ writeFileSync(reviewPath, renderDeliveryReview(verdict, facts));
5562
+ artifact = 'written';
5563
+ } catch { /* best-effort — the verdict is still returned */ }
5564
+
5565
+ if (json) {
5566
+ write(JSON.stringify({ planesChecked, planesSkipped, findings: verdict.findings, handoff: verdict.handoff, artifact }, null, 2));
5567
+ } else {
5568
+ write(`dz delivery-check — ${slug}: ${verdict.handoff}`);
5569
+ for (const c of verdict.criterion) {
5570
+ const mark = c.status === 'PASS' ? '✓' : c.status === 'FAIL' ? '✗' : '○';
5571
+ write(` ${mark} ${c.label}: ${c.status}${c.detail ? ` — ${c.detail}` : ''}`);
5572
+ }
5573
+ write(artifact === 'written' ? `\n↳ wrote features/${slug}/10_delivery_review.md` : '\n↳ 10_delivery_review.md NOT written (verdict above is authoritative)');
5574
+ }
5575
+
5576
+ if (flags.has('strict') && verdict.handoff === 'blocked') return 1;
5577
+ return 0;
5578
+ }
5579
+
5345
5580
  /**
5346
5581
  * `dz routing` — inspect the learned cost-optimal routing outcome store (feature learned-cost-routing). Shows
5347
5582
  * what `args.models[stage]='auto-cost'` currently believes per (stage, complexity-tier, model): gated
@@ -5732,6 +5967,8 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
5732
5967
  return cmdPublish(options, flags, cwd, write);
5733
5968
  case 'release':
5734
5969
  return cmdRelease(options, flags, cwd, write, io.releaseRunner);
5970
+ case 'parity':
5971
+ return cmdParity(options, flags, write);
5735
5972
  case 'registry':
5736
5973
  return cmdRegistry(options, cwd, write);
5737
5974
  case 'benchmark':
@@ -5764,6 +6001,8 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
5764
6001
  return cmdChallenge(options, flags, cwd, write);
5765
6002
  case 'discrimination-check':
5766
6003
  return cmdDiscriminationCheck(options, flags, cwd, write);
6004
+ case 'delivery-check':
6005
+ return cmdDeliveryCheck(options, flags, cwd, write);
5767
6006
  case 'routing':
5768
6007
  return cmdRouting(options, flags, cwd, write);
5769
6008
  case 'bto-optimize':