@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/.dz-manifest.json +10 -10
- package/README.md +79 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +259 -8
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
- package/sbom.json +9 -9
- package/src/cli.ts +250 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/harness-cli",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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]] (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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':
|