@dzhechkov/harness-cli 0.3.236 → 0.3.238

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.236",
3
+ "version": "0.3.238",
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.127"
60
+ "@dzhechkov/harness-core": "0.3.129"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/node": "^25.6.0",
package/sbom.json CHANGED
@@ -25,7 +25,7 @@
25
25
  "hashes": [
26
26
  {
27
27
  "alg": "SHA-256",
28
- "content": "1e36d7e9e6022d412c030975ec686b1ab91c61c64c807ef2e9ff3fa75ac8917c"
28
+ "content": "ef6603652143fc1517c81506dad9d5e7b79e5d5138aad9f6281ad77492d58b71"
29
29
  }
30
30
  ]
31
31
  },
@@ -35,7 +35,7 @@
35
35
  "hashes": [
36
36
  {
37
37
  "alg": "SHA-256",
38
- "content": "1abb00d07ab57ad413d2747baa6a68cda0876a9e0c6f75db89c205fbf4863530"
38
+ "content": "667bf610bf890a7b93480a4b1045e98dc0c765effd80b78fde4e48538f595b3e"
39
39
  }
40
40
  ]
41
41
  },
@@ -45,7 +45,7 @@
45
45
  "hashes": [
46
46
  {
47
47
  "alg": "SHA-256",
48
- "content": "42d2a98f54c2ec773b49a0a43c959a78d070c8c427724f4affbd5bafef35621d"
48
+ "content": "593705e81f851c0e65ffa9feaa9bbd396e7a3295b9b21076cfd953ee5a05b93a"
49
49
  }
50
50
  ]
51
51
  },
@@ -105,7 +105,7 @@
105
105
  "hashes": [
106
106
  {
107
107
  "alg": "SHA-256",
108
- "content": "8a00bf14c3fdd437b7e482da8c4018366041755714ed1791b1c523f9dee5de6c"
108
+ "content": "8f8807920c7ddd901361eb5a602df9c85fa69726e47535a146937038eab6b281"
109
109
  }
110
110
  ]
111
111
  },
@@ -115,7 +115,7 @@
115
115
  "hashes": [
116
116
  {
117
117
  "alg": "SHA-256",
118
- "content": "dea4446db1109285c1fc5f5283d7263462cb446f12b0d9b1e1e821374d0ca57d"
118
+ "content": "5f5fbcc7c040a272f6af992ef1f9c39482b941d74dbcadec9f3d9008babcb08a"
119
119
  }
120
120
  ]
121
121
  },
@@ -125,7 +125,7 @@
125
125
  "hashes": [
126
126
  {
127
127
  "alg": "SHA-256",
128
- "content": "a253625c94835ef94bbd141b1dd759578433423bd098314fd977c515f140dafe"
128
+ "content": "420d1178a513e54f315095a17e9c5722543ca6cbdbebd9a674d4d50ef61b8aa0"
129
129
  }
130
130
  ]
131
131
  },
@@ -185,7 +185,7 @@
185
185
  "hashes": [
186
186
  {
187
187
  "alg": "SHA-256",
188
- "content": "543319797d47d599a646c7d695f82aa31491ffd97579edd691b6a9cb99af76ef"
188
+ "content": "8fbaf10d29b5733cf9445e1ef5804a3e124f733c3390a99937d2d8dc3b4cf02a"
189
189
  }
190
190
  ]
191
191
  },
@@ -205,7 +205,7 @@
205
205
  "hashes": [
206
206
  {
207
207
  "alg": "SHA-256",
208
- "content": "c5d4902f9b0c4133318b15e757d07856872177dc92752d5996440524342b7f88"
208
+ "content": "131f23a50187e1be19875d4ccd76d8c354b1fbb8d193c03e73fda16ff64f51dc"
209
209
  }
210
210
  ]
211
211
  },
@@ -225,7 +225,7 @@
225
225
  "hashes": [
226
226
  {
227
227
  "alg": "SHA-256",
228
- "content": "0538f3cfd8eb880305c08fef05a78601263003c54a6f1b44f97cd7ebd7bd37f9"
228
+ "content": "3deb99917f3d8b4a5c9a3b0315bd65a3099ce2b508121cfa2bdeed19b7554d48"
229
229
  }
230
230
  ]
231
231
  },
package/src/cli.ts CHANGED
@@ -64,6 +64,13 @@ import {
64
64
  recallHybrid,
65
65
  teachGuard,
66
66
  mirrorPatternsToVector,
67
+ mirrorEntriesToVector,
68
+ patternVectorEntry,
69
+ readMemoryLearningConfig,
70
+ promotePatterns,
71
+ quarantineExpiryCandidates,
72
+ pruneQuarantinePatterns,
73
+ clearAgentdbQuarantine,
67
74
  vectorMirrorEnabled,
68
75
  vectorTierStatus,
69
76
  resolveVectorEngine,
@@ -179,6 +186,12 @@ import {
179
186
  renderProseDiff,
180
187
  readScenarioIds,
181
188
  DEFAULT_MAX_JUDGE_RUNS,
189
+ collectDeliveryFacts,
190
+ planDeliveryCheck,
191
+ renderDeliveryBrief,
192
+ classifyDelivery,
193
+ isUsablePlaneResult,
194
+ renderDeliveryReview,
182
195
  } from '@dzhechkov/harness-core';
183
196
  import type { Family, ModelRung, Candidate as BtoCandidate, DimScores } from '@dzhechkov/harness-core';
184
197
  import type { SetupSpec } from '@dzhechkov/harness-core';
@@ -208,11 +221,12 @@ Usage:
208
221
  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)
209
222
  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)
210
223
  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)
224
+ 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)
211
225
  dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--project <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force] [--enrich]
212
226
  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)
213
227
  dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
214
- dz consolidate [--sessions-dir <dir>] [--project <dir>] [--no-mirror] [--prune-noise [--apply]] (prune-noise: DRY-RUN by default; --apply snapshots then deletes)
215
- dz recall "<query>" [--limit <N>] [--semantic | --no-semantic] [--books [--book <slug>]] [--project <dir>] | dz recall --all [--json] | dz recall --usage [--json] | dz recall --forget <dzId>[,<dzId>] [--apply] (forget: dry-run default; snapshots before removing)
228
+ dz consolidate [--sessions-dir <dir>] [--project <dir>] [--no-mirror] [--prune-noise [--apply]] [--prune-quarantine [--apply]] (both prunes: DRY-RUN by default; --apply snapshots then deletes; prune-quarantine = expired unproven lessons ONLY, never coupled to noise)
229
+ dz recall "<query>" [--limit <N>] [--semantic | --no-semantic] [--books [--book <slug>]] [--project <dir>] | dz recall --all [--json] | dz recall --usage [--json] | dz recall --forget <dzId>[,<dzId>] [--apply] | dz recall --promote <dzId>[,<dzId>] [--apply] (forget/promote: dry-run default; forget snapshots before removing; promote lifts lesson-quarantine)
216
230
  dz vector status [--project <dir>] [--json] (semantic tier: engine, mirrored vs lexical counts, pending queue)
217
231
  dz vector reindex [--project <dir>] [--json] (snapshot, re-embed learned-pattern vectors, stamp current model)
218
232
  dz vector export <path> [--project <dir>] (portable VECTOR form (.rvf, opt-in RVF engine); patterns ship via recall --all --json)
@@ -1508,6 +1522,16 @@ async function cmdTeach(options: Map<string, string>, flags: Set<string>, cwd: s
1508
1522
  const receipt = await mirrorPatternsToVector(root, records, source);
1509
1523
  if (receipt.mirrored > 0) write(` ↳ mirrored to vector tier (${receipt.engine ?? 'vector'})`);
1510
1524
  };
1525
+ // lesson-quarantine FR-8: the fresh-teach mirror carries the qStatus marker so the hook daemon
1526
+ // (which reads only the mirror's metadata) can exclude unproven lessons from auto-inject.
1527
+ const emitMirrorQ = async (root: string, records: readonly PatternRecord[], source: string, quarantined: boolean): Promise<void> => {
1528
+ if (flags.has('no-mirror') || records.length === 0 || !vectorMirrorEnabled(root)) return;
1529
+ const entries = records
1530
+ .map((r) => patternVectorEntry(r, source, quarantined ? { quarantined: true } : {}))
1531
+ .filter((e): e is NonNullable<typeof e> => e !== undefined);
1532
+ const receipt = await mirrorEntriesToVector(root, entries);
1533
+ if (receipt.mirrored > 0) write(` ↳ mirrored to vector tier (${receipt.engine ?? 'vector'})${quarantined ? ' [quarantined]' : ''}`);
1534
+ };
1511
1535
 
1512
1536
  // `dz teach --harmonize` — documented ALIAS of `dz vector harmonize`: SEMANTIC dedup of the
1513
1537
  // learned store. ONE implementation (harmonizeVectorStore), two entry points (QR-6). Routed
@@ -1588,6 +1612,9 @@ async function cmdTeach(options: Map<string, string>, flags: Set<string>, cwd: s
1588
1612
  const trained = await backend.train();
1589
1613
  if (trained.flushed > 0) {
1590
1614
  write(`↳ reinforced ${reinforce}`);
1615
+ // lesson-quarantine: reinforcement IS promotion — keep the hook daemon's mirror in step.
1616
+ const clearedQ = clearAgentdbQuarantine(projectRoot, [reinforce]);
1617
+ if (clearedQ.cleared > 0) write(` ↳ promoted out of quarantine (mirror updated)`);
1591
1618
  return 0;
1592
1619
  }
1593
1620
  // HIGH-fix: a no-match must NOT auto-teach the raw argument — callers pass dzIds or truncated
@@ -1622,6 +1649,8 @@ async function cmdTeach(options: Map<string, string>, flags: Set<string>, cwd: s
1622
1649
  // teach below so the lesson is NEVER silently discarded (the exact silent-drop the ADR forbids).
1623
1650
  if (trained.flushed > 0) {
1624
1651
  write(`↳ reinforced existing pattern ${verdict.dzId} (cos=${verdict.cosine.toFixed(2)}) — not re-added`);
1652
+ const clearedQ = clearAgentdbQuarantine(projectRoot, [verdict.dzId]);
1653
+ if (clearedQ.cleared > 0) write(' ↳ promoted out of quarantine (mirror updated)');
1625
1654
  return 0;
1626
1655
  }
1627
1656
  write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
@@ -1651,13 +1680,18 @@ async function cmdTeach(options: Map<string, string>, flags: Set<string>, cwd: s
1651
1680
  // Tier-2 (ADR-005): persist through the unified @dzhechkov/memory store. recordPattern
1652
1681
  // folds any legacy .dz/patterns.jsonl into the backend (idempotent) and returns the
1653
1682
  // total count. The lossy `npx agentdb add` dual-write was removed in Tier-1 (audit #6).
1654
- const count = await recordPattern(projectRoot, entry);
1683
+ // lesson-quarantine (opt-in): a fresh lesson is a HYPOTHESIS until it earns promotion.
1684
+ const quarantineOn = readMemoryLearningConfig(projectRoot).quarantine;
1685
+ const count = await recordPattern(projectRoot, entry, quarantineOn ? { quarantine: true } : {});
1655
1686
 
1656
1687
  write(`Learned: "${pattern.slice(0, 60)}${pattern.length > 60 ? '...' : ''}"`);
1657
1688
  write(` Domain: ${domain} Reward: ${reward} Backend: memory (@dzhechkov/memory)`);
1658
1689
  write(` Total patterns: ${count}`);
1690
+ if (quarantineOn) {
1691
+ write(' ⚠ quarantined: excluded from auto-inject, damped in recall — promote by confirming it (dz teach --reinforce "<text>") or dz recall --promote <dzId> --apply');
1692
+ }
1659
1693
  // The lexical write above is durable — the vector mirror is strictly best-effort (I-3).
1660
- await emitMirror(projectRoot, [entry], 'dz-teach');
1694
+ await emitMirrorQ(projectRoot, [entry], 'dz-teach', quarantineOn);
1661
1695
  return 0;
1662
1696
  }
1663
1697
 
@@ -1666,6 +1700,23 @@ async function cmdConsolidate(options: Map<string, string>, flags: Set<string>,
1666
1700
  const sessionsDirOpt = options.get('sessions-dir');
1667
1701
  const pruneNoise = flags.has('prune-noise');
1668
1702
 
1703
+ // lesson-quarantine FR-7: expiry is a SEPARATE, explicit surface — never coupled to prune-noise
1704
+ // (the recalled decay-vs-noise lesson: valid-but-unproven is not garbage). Dry-run by default.
1705
+ if (flags.has('prune-quarantine')) {
1706
+ const cfg = readMemoryLearningConfig(projectRoot);
1707
+ const res = pruneQuarantinePatterns(projectRoot, { dryRun: !flags.has('apply'), expireDays: cfg.quarantineExpireDays });
1708
+ if (!flags.has('apply')) {
1709
+ write(`dz consolidate --prune-quarantine: DRY RUN — ${res.candidates.length} expired quarantined lesson(s) (> ${cfg.quarantineExpireDays}d, never reinforced)`);
1710
+ for (const c of res.candidates) write(` [${c.ageDays < 0 ? '?' : c.ageDays}d] ${c.dzId} ${c.text.slice(0, 70)}`);
1711
+ if (res.candidates.length > 0) write(' → re-run with --apply to remove (snapshots first); or promote keepers: dz recall --promote <dzId> --apply');
1712
+ return 0;
1713
+ }
1714
+ if (res.error !== undefined) { write(`dz consolidate --prune-quarantine: ${res.error}`); return 1; }
1715
+ write(`dz consolidate --prune-quarantine: removed ${res.removed} expired quarantined lesson(s)`);
1716
+ if (res.snapshot !== undefined) write(` snapshot: ${res.snapshot}`);
1717
+ return 0;
1718
+ }
1719
+
1669
1720
  // --prune-noise: RETRO-PRUNE legacy noise (tool telemetry + system-wrapper "responses") from
1670
1721
  // the lexical store AND the agentdb vector mirror BEFORE harvesting, so this run's watermark
1671
1722
  // never re-learns from junk. Best-effort — a prune error is reported, never fatal.
@@ -1883,12 +1934,53 @@ async function cmdRecallForget(
1883
1934
  return 0;
1884
1935
  }
1885
1936
 
1937
+ /**
1938
+ * `dz recall --promote <dzId>[,<dzId>…] [--apply]` — lift quarantine from NAMED records
1939
+ * (lesson-quarantine FR-6b). Dry-run by default, the --forget symmetry. Also clears the
1940
+ * agentdb mirror's qStatus (best-effort) so the hook daemon stops excluding promoted lessons.
1941
+ */
1942
+ async function cmdRecallPromote(
1943
+ options: Map<string, string>,
1944
+ flags: Set<string>,
1945
+ projectRoot: string,
1946
+ write: Write,
1947
+ ): Promise<number> {
1948
+ const ids = (options.get('promote') ?? '').split(',').map((s) => s.trim()).filter((s) => s.length > 0);
1949
+ if (ids.length === 0) {
1950
+ write('dz recall --promote: no ids given (comma-separated dzIds; quarantined ones are marked ⚠q in recall)');
1951
+ return 1;
1952
+ }
1953
+ if (!flags.has('apply')) {
1954
+ const records = loadStoreRecords(projectRoot);
1955
+ const found = ids.filter((id) => records.some((r) => r.id === id));
1956
+ write(`dz recall --promote: DRY RUN — ${found.length}/${ids.length} id(s) match the store. Re-run with --apply to promote.`);
1957
+ return 0;
1958
+ }
1959
+ const res = await promotePatterns(projectRoot, ids);
1960
+ // Codex-QE finding 5: even on a mid-batch failure, the ALREADY-promoted records must get their
1961
+ // mirror rows cleared — otherwise the canonical store and the hook-visible mirror split-brain.
1962
+ if (res.promoted.length > 0) {
1963
+ const cleared = clearAgentdbQuarantine(projectRoot, res.promoted);
1964
+ if (cleared.cleared > 0) write(` ↳ mirror updated (${cleared.cleared} row(s) un-quarantined in agentdb)`);
1965
+ else if (cleared.error !== undefined) write(` ⚠ mirror not updated (${cleared.error}) — run dz vector reindex to resync`);
1966
+ }
1967
+ if (!res.ok) {
1968
+ write(`dz recall --promote: failed — ${res.error ?? 'unknown error'} (promoted so far: ${res.promoted.length})`);
1969
+ return 1;
1970
+ }
1971
+ write(`dz recall --promote: promoted ${res.promoted.length} record(s)`);
1972
+ if (res.notQuarantined.length > 0) write(` already promoted (not quarantined): ${res.notQuarantined.join(', ')}`);
1973
+ if (res.notFound.length > 0) write(` not found: ${res.notFound.join(', ')}`);
1974
+ return 0;
1975
+ }
1976
+
1886
1977
  async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): Promise<number> {
1887
1978
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
1888
1979
  const asJson = flags.has('json');
1889
1980
  const all = flags.has('all');
1890
1981
  if (flags.has('usage')) return cmdRecallUsage(options, flags, projectRoot, write);
1891
1982
  if (options.has('forget')) return cmdRecallForget(options, flags, projectRoot, write);
1983
+ if (options.has('promote')) return cmdRecallPromote(options, flags, projectRoot, write);
1892
1984
 
1893
1985
  // --all: dump the entire learned store (backend-agnostic, via loadStorePatternsSync).
1894
1986
  // With --json this is the portable export the agentdb-memory MCP bridge consumes.
@@ -1987,9 +2079,16 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
1987
2079
  const lexLabel = result.lexicalBackend === 'sqlite' ? 'SQLite FTS5' : 'keyword (JSON)';
1988
2080
  const ranking = vectorOn ? `${lexLabel} + vector (${result.vectorEngine}) ranking` : `${lexLabel} ranking (lexical)`;
1989
2081
  write(`dz recall "${query}" — ${hits.length} hit(s), ${ranking}`);
2082
+ let sawQuarantined = false;
1990
2083
  for (const h of hits) {
1991
2084
  const backendTag = vectorOn ? ` ⟨${h.backend}⟩` : '';
1992
- write(` [${h.pattern.reward.toFixed(2)}] (${h.pattern.domain})${backendTag} ${h.pattern.pattern.slice(0, 80)}`);
2085
+ const qTag = h.quarantined === true ? ' ⚠q' : '';
2086
+ if (h.quarantined === true) sawQuarantined = true;
2087
+ write(` [${h.pattern.reward.toFixed(2)}] (${h.pattern.domain})${backendTag}${qTag} ${h.pattern.pattern.slice(0, 80)}`);
2088
+ }
2089
+ if (sawQuarantined) {
2090
+ // The loop stays VISIBLE (ADR D2): a quarantined hit is shown, marked, and explained.
2091
+ write(' ⚠q = quarantined (unproven hypothesis, rank damped) — confirm with dz teach --reinforce, or dz recall --promote <dzId> --apply');
1993
2092
  }
1994
2093
  if (result.vectorError !== undefined && mode !== 'lexical') {
1995
2094
  // Engine present but the semantic leg failed/timed out — one honest line, exit 0 (05 §2.3).
@@ -5201,8 +5300,18 @@ function cmdFeatureAdrSetup(options: Map<string, string>, flags: Set<string>, cw
5201
5300
  return 1;
5202
5301
  }
5203
5302
 
5303
+ // portable-gates (direction b): --gates scaffolds the zero-config architecture/gates/delivery-check.md.
5304
+ // Its "runnable here" list is computed for --target (default agents-md, the AGENTS.md-class target class).
5305
+ const wantGates = flags.has('gates');
5306
+ const targetOpt = options.get('target');
5307
+ if (targetOpt !== undefined && !isTargetName(targetOpt)) {
5308
+ write(`dz feature-adr-setup: --target must be one of: ${TARGET_NAMES.join(', ')}`);
5309
+ return 1;
5310
+ }
5311
+ const gatesTarget: TargetName = isTargetName(targetOpt ?? '') ? (targetOpt as TargetName) : 'agents-md';
5312
+
5204
5313
  const specPath = options.get('from-spec');
5205
- if (specPath === undefined && !wantGuards) {
5314
+ if (specPath === undefined && !wantGuards && !wantGates) {
5206
5315
  // default + --plan: the read-only "which documents, and where?" answer.
5207
5316
  const plan = buildSetupPlan(scanForSetup(repoRoot));
5208
5317
  if (flags.has('json')) { write(JSON.stringify(plan, null, 2)); return 0; }
@@ -5224,8 +5333,9 @@ function cmdFeatureAdrSetup(options: Map<string, string>, flags: Set<string>, cw
5224
5333
  spec = {}; // --guards standalone: scaffold just the guard set
5225
5334
  }
5226
5335
  if (wantGuards) spec = { ...spec, guards: locCap !== undefined ? { locCap } : true };
5336
+ if (wantGates) spec = { ...spec, gates: true };
5227
5337
 
5228
- const result = scaffoldFromSpec(spec, readExistingForScaffold(repoRoot));
5338
+ const result = scaffoldFromSpec(spec, readExistingForScaffold(repoRoot), gatesTarget);
5229
5339
  if (flags.has('json') && !flags.has('apply')) { write(JSON.stringify(result, null, 2)); return 0; }
5230
5340
  write(renderScaffoldPreview(result));
5231
5341
 
@@ -5428,6 +5538,137 @@ function nameFor(t: { file: string; name?: string }, outcome: 'pass' | 'fail' |
5428
5538
  return t.name !== undefined ? { file: t.file, name: t.name, outcome } : { file: t.file, outcome };
5429
5539
  }
5430
5540
 
5541
+ /**
5542
+ * `dz delivery-check` — the portable Step-10 Delivery Gate (feature portable-gates). The `manual` form that
5543
+ * travels to every `shell` target: the deterministic parts (artifact probes, hand-off arithmetic,
5544
+ * cross-validation bookkeeping) run IN the CLI; the semantic 4-plane review is DISPATCHED to the caller's own
5545
+ * agent runtime — the `dz challenge` cartridge shape. All logic lives in harness-core's pure engine; this is
5546
+ * the executor. Advisory exit codes (0 by default; `--strict` ⇒ 1 on `blocked`).
5547
+ *
5548
+ * --slug <slug> the feature under features/<slug> (required; featureDir derived internally)
5549
+ * --context-only default mode: print artifact probes + the 4-plane review brief (no verdict)
5550
+ * --findings <f.json> classify a fed-back findings array → write 10_delivery_review.md + print ready/blocked
5551
+ * --strict with --findings: exit 1 iff handoff === 'blocked' (CI gate)
5552
+ * --author <model> cosmetic: the reviewer to dispatch, printed alongside the brief
5553
+ * --json machine contract { planesChecked, planesSkipped, findings, handoff, artifact }
5554
+ */
5555
+ function cmdDeliveryCheck(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
5556
+ let repoRoot = cwd;
5557
+ try { repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || cwd; } catch { /* not git */ }
5558
+
5559
+ const json = flags.has('json');
5560
+ const errOut = (error: string): number => {
5561
+ if (json) write(JSON.stringify({ error, planesChecked: [], planesSkipped: [], findings: [], handoff: null, artifact: 'not-written' }, null, 2));
5562
+ else { write(`dz delivery-check: ${error}`); write(' allowed: --slug <slug>, --findings <f.json>, --author <model>, --context-only, --strict, --json'); }
5563
+ return 1;
5564
+ };
5565
+ // Strict allowlist (delivery finding: a typo'd --findings silently downgraded the --strict CI
5566
+ // gate to a vacuous exit-0 pass — a mistyped option must be an ERROR, never a mode change).
5567
+ const allowedFlags = new Set(['context-only', 'strict', 'json', 'help']);
5568
+ const allowedOptions = new Set(['slug', 'findings', 'author']);
5569
+ for (const flag of flags) if (!allowedFlags.has(flag)) return errOut(`unknown option --${flag}`);
5570
+ for (const key of options.keys()) {
5571
+ if (key.startsWith('_positional_')) return errOut(`unexpected argument "${options.get(key)}"`);
5572
+ if (!allowedOptions.has(key)) return errOut(`unknown option --${key}`);
5573
+ }
5574
+
5575
+ const slug = options.get('slug');
5576
+ if (slug === undefined || slug.trim() === '') return errOut('pass --slug <slug> (the feature under features/<slug>)');
5577
+ // Slug convention is a SECURITY boundary here (delivery finding, CONFIRMED traversal reproducer:
5578
+ // --slug '../../outside/evil' wrote 10_delivery_review.md outside the repo). kebab-case Latin,
5579
+ // <=40 chars — the repo's own slug rule as a layer-1 three-line check.
5580
+ const slugTrimmed = slug.trim();
5581
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(slugTrimmed) || slugTrimmed.length > 40) {
5582
+ return errOut(`invalid --slug "${slugTrimmed}" — kebab-case Latin, max 40 chars (a path-like slug would redirect the report write)`);
5583
+ }
5584
+ const featureDir = join(repoRoot, 'features', slugTrimmed);
5585
+
5586
+ // --context-only is a real mode flag, not decoration: combining it with --findings contradicts
5587
+ // itself (delivery finding — the flag was accepted and silently ignored).
5588
+ if (flags.has('context-only') && options.get('findings') !== undefined) {
5589
+ return errOut('--context-only and --findings are mutually exclusive (context-only prints the brief; findings classifies the verdict)');
5590
+ }
5591
+
5592
+ // git status --porcelain — CLI layer only (the `no child_process in core` contract); a non-git target
5593
+ // degrades to an empty list, which is honest (AM-10: changed-files is informational, never a required gate).
5594
+ let changedFiles: string[] = [];
5595
+ try {
5596
+ const out = execSync('git status --porcelain', { cwd: repoRoot, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
5597
+ changedFiles = out.split('\n').map((l) => l.slice(3).trim()).filter((s) => s.length > 0);
5598
+ } catch { /* non-git / unavailable → empty (honest) */ }
5599
+
5600
+ const facts = collectDeliveryFacts(featureDir, { changedFiles, repoRoot });
5601
+ const plan = planDeliveryCheck(facts);
5602
+ const author = options.get('author');
5603
+
5604
+ const findingsPath = options.get('findings');
5605
+ if (findingsPath === undefined) {
5606
+ // Default / --context-only: emit the led protocol (probes + brief). No verdict — the CLI has no model.
5607
+ if (json) {
5608
+ write(JSON.stringify({
5609
+ planesChecked: [],
5610
+ planesSkipped: plan.planes.map((p) => p.id),
5611
+ findings: [],
5612
+ handoff: null,
5613
+ artifact: 'not-written',
5614
+ probes: plan.probes,
5615
+ }, null, 2));
5616
+ return 0;
5617
+ }
5618
+ write(`dz delivery-check — ${slug} (portable 4-plane hand-off protocol; you drive the review)`);
5619
+ for (const p of plan.probes) {
5620
+ const mark = p.passed === true ? '✓' : p.passed === false ? '✗' : '○';
5621
+ write(` ${mark} ${p.description}${p.required ? ' [required]' : ' [informational]'}`);
5622
+ }
5623
+ write('');
5624
+ write(renderDeliveryBrief(plan, facts));
5625
+ // The hand-off criterion the verdict will be judged by, shown UP FRONT (this also makes
5626
+ // plan.criterionTemplate a consumed surface, not dead product code — delivery finding).
5627
+ write('\n── hand-off criterion (all must PASS for ready) ──');
5628
+ for (const c of plan.criterionTemplate) write(` ○ ${c.label}`);
5629
+ if (author !== undefined && author.trim() !== '') write(`\n── reviewer to dispatch ──\n${author.trim()}`);
5630
+ write('\n(next: run the four planes, then `dz delivery-check --slug ' + slug + ' --findings findings.json` for the verdict)');
5631
+ return 0;
5632
+ }
5633
+
5634
+ // --findings mode: parse (malformed/missing ⇒ [], a failed-plane input — fail-closed, never a throw).
5635
+ let reviewResults: unknown[] = [];
5636
+ try {
5637
+ const parsed: unknown = JSON.parse(readFileSync(resolve(cwd, findingsPath), 'utf-8'));
5638
+ if (Array.isArray(parsed)) reviewResults = parsed;
5639
+ else if (parsed !== null && typeof parsed === 'object' && Array.isArray((parsed as { planes?: unknown }).planes)) reviewResults = (parsed as { planes: unknown[] }).planes;
5640
+ else reviewResults = [];
5641
+ } catch { reviewResults = []; }
5642
+
5643
+ const verdict = classifyDelivery(plan, reviewResults as ({ findings?: readonly unknown[] } | null | undefined)[]);
5644
+ // The SAME predicate as the fail-closed verdict (core-exported) — a local copy was a drift channel.
5645
+ const planesChecked = plan.planes.filter((_, i) => isUsablePlaneResult(reviewResults[i])).map((p) => p.id);
5646
+ const planesSkipped = plan.planes.filter((_, i) => !isUsablePlaneResult(reviewResults[i])).map((p) => p.id);
5647
+
5648
+ // Write the regenerable report (NOT augment-never-clobber — deliberately overwritten each run).
5649
+ let artifact: 'written' | 'not-written' = 'not-written';
5650
+ try {
5651
+ const reviewPath = join(featureDir, '10_delivery_review.md');
5652
+ mkdirSync(dirname(reviewPath), { recursive: true });
5653
+ writeFileSync(reviewPath, renderDeliveryReview(verdict, facts));
5654
+ artifact = 'written';
5655
+ } catch { /* best-effort — the verdict is still returned */ }
5656
+
5657
+ if (json) {
5658
+ write(JSON.stringify({ planesChecked, planesSkipped, findings: verdict.findings, handoff: verdict.handoff, artifact }, null, 2));
5659
+ } else {
5660
+ write(`dz delivery-check — ${slug}: ${verdict.handoff}`);
5661
+ for (const c of verdict.criterion) {
5662
+ const mark = c.status === 'PASS' ? '✓' : c.status === 'FAIL' ? '✗' : '○';
5663
+ write(` ${mark} ${c.label}: ${c.status}${c.detail ? ` — ${c.detail}` : ''}`);
5664
+ }
5665
+ write(artifact === 'written' ? `\n↳ wrote features/${slug}/10_delivery_review.md` : '\n↳ 10_delivery_review.md NOT written (verdict above is authoritative)');
5666
+ }
5667
+
5668
+ if (flags.has('strict') && verdict.handoff === 'blocked') return 1;
5669
+ return 0;
5670
+ }
5671
+
5431
5672
  /**
5432
5673
  * `dz routing` — inspect the learned cost-optimal routing outcome store (feature learned-cost-routing). Shows
5433
5674
  * what `args.models[stage]='auto-cost'` currently believes per (stage, complexity-tier, model): gated
@@ -5852,6 +6093,8 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
5852
6093
  return cmdChallenge(options, flags, cwd, write);
5853
6094
  case 'discrimination-check':
5854
6095
  return cmdDiscriminationCheck(options, flags, cwd, write);
6096
+ case 'delivery-check':
6097
+ return cmdDeliveryCheck(options, flags, cwd, write);
5855
6098
  case 'routing':
5856
6099
  return cmdRouting(options, flags, cwd, write);
5857
6100
  case 'bto-optimize':