@dzhechkov/harness-cli 0.3.258 → 0.3.260
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 +8 -8
- package/README.md +70 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +292 -8
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
- package/sbom.json +7 -7
- package/src/cli.ts +274 -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.260",
|
|
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",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"sbom.json"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@dzhechkov/harness-core": "^0.3.
|
|
44
|
+
"@dzhechkov/harness-core": "^0.3.150",
|
|
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
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"hashes": [
|
|
26
26
|
{
|
|
27
27
|
"alg": "SHA-256",
|
|
28
|
-
"content": "
|
|
28
|
+
"content": "6b3bcc252253732b9703f1eb43ac1f0e9f0599979b09d0fea3208932fd0f33a8"
|
|
29
29
|
}
|
|
30
30
|
]
|
|
31
31
|
},
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"hashes": [
|
|
106
106
|
{
|
|
107
107
|
"alg": "SHA-256",
|
|
108
|
-
"content": "
|
|
108
|
+
"content": "4c5fa8c51cc350874413efceb0a2e4dce2f0c71f39914ddd8be425336a37ae2d"
|
|
109
109
|
}
|
|
110
110
|
]
|
|
111
111
|
},
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"hashes": [
|
|
116
116
|
{
|
|
117
117
|
"alg": "SHA-256",
|
|
118
|
-
"content": "
|
|
118
|
+
"content": "a75034dfe70f0b57811dfad831366bd5c7995583a57db0f218621b026ac8d854"
|
|
119
119
|
}
|
|
120
120
|
]
|
|
121
121
|
},
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
"hashes": [
|
|
126
126
|
{
|
|
127
127
|
"alg": "SHA-256",
|
|
128
|
-
"content": "
|
|
128
|
+
"content": "dbe8fa418aedcb77dead3a374bba7cbf0fb5e305dd9a06002946a19604279684"
|
|
129
129
|
}
|
|
130
130
|
]
|
|
131
131
|
},
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
"hashes": [
|
|
186
186
|
{
|
|
187
187
|
"alg": "SHA-256",
|
|
188
|
-
"content": "
|
|
188
|
+
"content": "72858a1f0199832c7ad781f892b7b117ec980af33ff9598587e1ed1544046a35"
|
|
189
189
|
}
|
|
190
190
|
]
|
|
191
191
|
},
|
|
@@ -205,7 +205,7 @@
|
|
|
205
205
|
"hashes": [
|
|
206
206
|
{
|
|
207
207
|
"alg": "SHA-256",
|
|
208
|
-
"content": "
|
|
208
|
+
"content": "1481238a2fc3a60051f678dcf6b415f8052ce954a9e55ee7577d8c3b2261e062"
|
|
209
209
|
}
|
|
210
210
|
]
|
|
211
211
|
},
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
"hashes": [
|
|
226
226
|
{
|
|
227
227
|
"alg": "SHA-256",
|
|
228
|
-
"content": "
|
|
228
|
+
"content": "24f2feb4c5c4fa939beb79cd658b03fbc7ce4bd87f15da563240abcb252c472c"
|
|
229
229
|
}
|
|
230
230
|
]
|
|
231
231
|
},
|
package/src/cli.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { chmodSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { appendFileSync, chmodSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { execFileSync, execSync, spawn } from 'node:child_process';
|
|
@@ -275,7 +275,17 @@ import {
|
|
|
275
275
|
makeBacklogIO,
|
|
276
276
|
harmonizeBacklog,
|
|
277
277
|
BACKLOG_BACKENDS,
|
|
278
|
+
applyDomainBoost,
|
|
279
|
+
countDisplacedByCut,
|
|
280
|
+
renderDomainBoostNote,
|
|
281
|
+
renderDomainCutNote,
|
|
282
|
+
parseReqeDebt,
|
|
283
|
+
buildReqeBrief,
|
|
284
|
+
settleReqeDebt,
|
|
285
|
+
renderReqeList,
|
|
286
|
+
REQE_SCOPE,
|
|
278
287
|
} from '@dzhechkov/harness-core';
|
|
288
|
+
import type { ReqeDebt } from '@dzhechkov/harness-core';
|
|
279
289
|
import type { IdeaRecord, IdeaStatus } from '@dzhechkov/harness-core';
|
|
280
290
|
import type { Family, ModelRung, Candidate as BtoCandidate, DimScores } from '@dzhechkov/harness-core';
|
|
281
291
|
import type { SetupSpec } from '@dzhechkov/harness-core';
|
|
@@ -314,6 +324,7 @@ Usage:
|
|
|
314
324
|
dz epoch-replay --judge <filled-work-order.json> [--out <file>] (blind judge prompts from the filled plans)
|
|
315
325
|
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)
|
|
316
326
|
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)
|
|
327
|
+
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)
|
|
317
328
|
dz backlog add "<idea>" [--effort 1-5] [--proposal <text>] [--dry-run] [--project <dir>] [--json] (capture an idea: semantic dedup against existing ideas via the Brain vector engine (DUPLICATE>=0.92 merges, RELATED links, NEW creates) + GoalMap alignment; --dry-run classifies without writing)
|
|
318
329
|
dz backlog list [--status <s>] [--goal <id>] [--project <dir>] [--json] (list captured ideas, filterable by status/goal)
|
|
319
330
|
dz backlog show <id> [--project <dir>] [--json] (full record for one idea)
|
|
@@ -326,7 +337,7 @@ Usage:
|
|
|
326
337
|
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)
|
|
327
338
|
dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
|
|
328
339
|
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)
|
|
329
|
-
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)
|
|
340
|
+
dz recall "<query>" [--limit <N>] [--domain <name>] [--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] (--domain <name> BOOSTS lessons of that domain without dropping foreign ones — a shared store keeps its cross-domain transfers; forget/promote: dry-run default; forget snapshots before removing; promote lifts lesson-quarantine)
|
|
330
341
|
dz vector status [--project <dir>] [--json] (semantic tier: engine, mirrored vs lexical counts, pending queue)
|
|
331
342
|
dz vector reindex [--project <dir>] [--json] (snapshot, re-embed learned-pattern vectors, stamp current model)
|
|
332
343
|
dz vector export <path> [--project <dir>] (portable VECTOR form (.rvf, opt-in RVF engine); patterns ship via recall --all --json)
|
|
@@ -1626,6 +1637,7 @@ function cmdUsage(
|
|
|
1626
1637
|
limits: typeof limitsPayload;
|
|
1627
1638
|
weeklyByModel?: typeof u.weeklyByModel;
|
|
1628
1639
|
estimated: true;
|
|
1640
|
+
reqeDue?: number;
|
|
1629
1641
|
} = {
|
|
1630
1642
|
sessionPct: u.sessionPct,
|
|
1631
1643
|
weeklyPct: u.weeklyPct,
|
|
@@ -1636,6 +1648,13 @@ function cmdUsage(
|
|
|
1636
1648
|
estimated: true,
|
|
1637
1649
|
};
|
|
1638
1650
|
if (hasModelLimits && u.weeklyByModel !== undefined) payload.weeklyByModel = u.weeklyByModel;
|
|
1651
|
+
// re-QE debt surfacing (backlog 6b40e667 — QE #9: the json contract must carry the debt too,
|
|
1652
|
+
// a probe is exactly the consumer that needs it). The field appears ONLY when a debt exists,
|
|
1653
|
+
// so the zero-debt contract stays byte-identical to the pinned legacy shape. Best-effort.
|
|
1654
|
+
try {
|
|
1655
|
+
const reqeCount = scanReqeDebts(resolve(cwd, options.get('project') ?? '.')).debts.length;
|
|
1656
|
+
if (reqeCount > 0) payload.reqeDue = reqeCount;
|
|
1657
|
+
} catch { /* advisory only */ }
|
|
1639
1658
|
write(
|
|
1640
1659
|
JSON.stringify(payload),
|
|
1641
1660
|
);
|
|
@@ -1645,6 +1664,10 @@ function cmdUsage(
|
|
|
1645
1664
|
write(
|
|
1646
1665
|
'usage: unconfigured — set memory.usage.sessionTokenLimit / weeklyTokenLimit in .dz/config.json (percentages are ESTIMATES calibrated from observed exhaustion)',
|
|
1647
1666
|
);
|
|
1667
|
+
try {
|
|
1668
|
+
const reqe = scanReqeDebts(resolve(cwd, options.get('project') ?? '.'));
|
|
1669
|
+
if (reqe.debts.length > 0) write('re-QE due: ' + reqe.debts.length + ' usage-switched run(s) kept same-family QE — run `dz reqe` for the cross-family pass');
|
|
1670
|
+
} catch { /* advisory only */ }
|
|
1648
1671
|
return 0;
|
|
1649
1672
|
}
|
|
1650
1673
|
// Compact human line — a short HH:MM / weekday hint on the resets, best-effort.
|
|
@@ -1660,6 +1683,12 @@ function cmdUsage(
|
|
|
1660
1683
|
const binding = hasModelLimits && u.weeklyBindingModel !== undefined ? ' ' + u.weeklyBindingModel + '-bound' : '';
|
|
1661
1684
|
const w = u.weeklyPct === null ? 'n/a' : '~' + u.weeklyPct + '%' + binding;
|
|
1662
1685
|
write('usage: session ' + s + ' (resets ' + clock(u.sessionResetsAt) + ') · week ' + w + ' (resets ' + clock(u.weeklyResetsAt) + ') · estimated');
|
|
1686
|
+
// re-QE debt surfacing (backlog 6b40e667): the moment someone checks usage is the moment a
|
|
1687
|
+
// usage-switched self-review debt should be visible. Best-effort — never breaks the contract.
|
|
1688
|
+
try {
|
|
1689
|
+
const reqe = scanReqeDebts(resolve(cwd, options.get('project') ?? '.'));
|
|
1690
|
+
if (reqe.debts.length > 0) write('re-QE due: ' + reqe.debts.length + ' usage-switched run(s) kept same-family QE — run `dz reqe` for the cross-family pass');
|
|
1691
|
+
} catch { /* advisory only */ }
|
|
1663
1692
|
return 0;
|
|
1664
1693
|
} catch {
|
|
1665
1694
|
// never let a probe see a non-zero exit — print the all-null contract and exit 0.
|
|
@@ -2216,13 +2245,31 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
2216
2245
|
: flags.has('no-semantic') || flags.has('lexical')
|
|
2217
2246
|
? ('lexical' as const)
|
|
2218
2247
|
: ('hybrid' as const);
|
|
2219
|
-
const
|
|
2248
|
+
const wantedDomain = options.get('domain');
|
|
2249
|
+
// OVER-FETCH before boosting (Codex QE #5): the boost used to run on hits ALREADY
|
|
2250
|
+
// truncated to `limit`, so an exact-domain lesson sitting at rank limit+1 could
|
|
2251
|
+
// never receive its promised lift — the feature was weakest in exactly the case it
|
|
2252
|
+
// exists for (foreign-domain dilution pushing a relevant lesson just past the cut).
|
|
2253
|
+
// Fetch a bounded surplus, re-rank, then trim to the limit the caller asked for.
|
|
2254
|
+
const fetchLimit = wantedDomain !== undefined ? Math.min(limit * 3, limit + 20) : limit;
|
|
2255
|
+
const result = await recallHybrid(projectRoot, query, { limit: fetchLimit, mode });
|
|
2256
|
+
|
|
2220
2257
|
if (mode === 'semantic' && result.vectorEngine === 'none') {
|
|
2221
2258
|
// --semantic is an explicit ask — degrading it silently would be dishonest (FR-3).
|
|
2222
2259
|
write(`dz recall --semantic: ${result.vectorReason ?? 'no vector engine available — run: dz setup --memory agentdb'}`);
|
|
2223
2260
|
return 1;
|
|
2224
2261
|
}
|
|
2225
|
-
|
|
2262
|
+
// Domain-aware re-ranking (health-advisor slice H): `--domain <name>` lifts lessons
|
|
2263
|
+
// tagged with that domain WITHOUT dropping foreign ones — a boost, not a filter, so a
|
|
2264
|
+
// shared store keeps the cross-domain transfers that make it worth more than two stores.
|
|
2265
|
+
const boost = wantedDomain !== undefined ? applyDomainBoost(result.hits, wantedDomain) : null;
|
|
2266
|
+
const hits = (boost ? boost.hits : result.hits).slice(0, limit);
|
|
2267
|
+
// The boost never drops a hit, but the CUT still can: promoting a match into the top
|
|
2268
|
+
// `limit` pushes the last one out, so a lesson visible WITHOUT --domain can vanish
|
|
2269
|
+
// WITH it. Cross-model review called this out as a lie by omission — the note said
|
|
2270
|
+
// "foreign-domain lessons kept" about the pre-cut list while the printed list was
|
|
2271
|
+
// missing one. Count it and say so; the reader can act on it (raise --limit).
|
|
2272
|
+
const displaced = boost !== null ? countDisplacedByCut(result.hits, boost.hits, limit) : 0;
|
|
2226
2273
|
if (asJson) {
|
|
2227
2274
|
// Portable contract UNCHANGED (I-7/AC-6): a plain PatternRecord[] — round-trips through
|
|
2228
2275
|
// `dz teach --from-json` regardless of which backend ranked each hit.
|
|
@@ -2233,6 +2280,10 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
2233
2280
|
if (hits.length === 0) {
|
|
2234
2281
|
write(`dz recall "${query}"`);
|
|
2235
2282
|
write(` No matching patterns (teach some with \`dz teach\`, or consolidate sessions).`);
|
|
2283
|
+
// The domain note must print here too (Codex QE #10): a --domain run with zero hits
|
|
2284
|
+
// silently said nothing about the domain, so the reader could not tell whether the
|
|
2285
|
+
// boost had been applied and found nothing, or had not run at all.
|
|
2286
|
+
if (boost !== null && wantedDomain !== undefined) write(renderDomainBoostNote(boost, wantedDomain));
|
|
2236
2287
|
return 0;
|
|
2237
2288
|
}
|
|
2238
2289
|
const vectorOn = result.vectorEngine !== 'none' && result.vectorError === undefined && mode !== 'lexical';
|
|
@@ -2244,12 +2295,25 @@ async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd:
|
|
|
2244
2295
|
const backendTag = vectorOn ? ` ⟨${h.backend}⟩` : '';
|
|
2245
2296
|
const qTag = h.quarantined === true ? ' ⚠q' : '';
|
|
2246
2297
|
if (h.quarantined === true) sawQuarantined = true;
|
|
2247
|
-
|
|
2298
|
+
// ONE line per hit, always. A lesson may contain newlines, and printing them raw
|
|
2299
|
+
// let stored CONTENT forge lines that look like the tool's own output — including
|
|
2300
|
+
// the domain-boost note that learning_bridge.py reads as a capability probe. Any
|
|
2301
|
+
// consumer that parses this output line-wise has the same exposure, so the fix
|
|
2302
|
+
// belongs at the point of rendering rather than in each reader.
|
|
2303
|
+
const oneLine = h.pattern.pattern.replace(/[\r\n]+/g, ' ⏎ ').slice(0, 80);
|
|
2304
|
+
write(` [${h.pattern.reward.toFixed(2)}] (${h.pattern.domain})${backendTag}${qTag} ${oneLine}`);
|
|
2248
2305
|
}
|
|
2249
2306
|
if (sawQuarantined) {
|
|
2250
2307
|
// The loop stays VISIBLE (ADR D2): a quarantined hit is shown, marked, and explained.
|
|
2251
2308
|
write(' ⚠q = quarantined (unproven hypothesis, rank damped) — confirm with dz teach --reinforce, or dz recall --promote <dzId> --apply');
|
|
2252
2309
|
}
|
|
2310
|
+
if (boost !== null && wantedDomain !== undefined) {
|
|
2311
|
+
// Say what the boost did — INCLUDING when it did nothing. A silent reorder would
|
|
2312
|
+
// let the reader believe the ranking was domain-aware when it had no match to work with.
|
|
2313
|
+
write(renderDomainBoostNote(boost, wantedDomain));
|
|
2314
|
+
const cutNote = renderDomainCutNote(displaced, limit);
|
|
2315
|
+
if (cutNote !== '') write(cutNote);
|
|
2316
|
+
}
|
|
2253
2317
|
if (result.vectorError !== undefined && mode !== 'lexical') {
|
|
2254
2318
|
// Engine present but the semantic leg failed/timed out — one honest line, exit 0 (05 §2.3).
|
|
2255
2319
|
write(` ℹ vector search degraded: ${result.vectorError} — showing lexical ranking`);
|
|
@@ -6213,6 +6277,194 @@ function nameFor(t: { file: string; name?: string }, outcome: 'pass' | 'fail' |
|
|
|
6213
6277
|
* `dz score --slug <feature>` — score a feature-adr RUN's process discipline from its artifacts
|
|
6214
6278
|
* (feature dz-score, Reading C). Descriptive-only: exit 0 on any score; non-zero only on usage errors.
|
|
6215
6279
|
*/
|
|
6280
|
+
/** Scan features/<slug>/.fa-state/reqe-due.json under a project root. Symlinked ANCESTOR dirs are
|
|
6281
|
+
* skipped too, not just the leaf (Codex QE #5 — a symlinked feature dir could smuggle an outside
|
|
6282
|
+
* debt); an oversized or non-regular due-file counts as MALFORMED, never a silent skip (QE #8);
|
|
6283
|
+
* a debt whose embedded slug differs from its directory is MALFORMED — identity is the directory,
|
|
6284
|
+
* the JSON only confirms it (QE #4: an embedded foreign slug must not redirect settlement). */
|
|
6285
|
+
function scanReqeDebts(root: string): { debts: Array<{ debt: ReqeDebt; duePath: string; dir: string }>; malformed: number } {
|
|
6286
|
+
const out: Array<{ debt: ReqeDebt; duePath: string; dir: string }> = [];
|
|
6287
|
+
let malformed = 0;
|
|
6288
|
+
const featuresDir = join(root, 'features');
|
|
6289
|
+
let slugs: string[] = [];
|
|
6290
|
+
try {
|
|
6291
|
+
if (lstatSync(featuresDir).isSymbolicLink()) return { debts: [], malformed: 0 }; // r2 #4: features/ itself
|
|
6292
|
+
slugs = readdirSync(featuresDir);
|
|
6293
|
+
} catch {
|
|
6294
|
+
return { debts: [], malformed: 0 };
|
|
6295
|
+
}
|
|
6296
|
+
for (const slug of slugs.sort()) {
|
|
6297
|
+
const dir = join(featuresDir, slug);
|
|
6298
|
+
const stateDir = join(dir, '.fa-state');
|
|
6299
|
+
const duePath = join(stateDir, 'reqe-due.json');
|
|
6300
|
+
try {
|
|
6301
|
+
if (lstatSync(dir).isSymbolicLink() || lstatSync(stateDir).isSymbolicLink()) continue;
|
|
6302
|
+
} catch {
|
|
6303
|
+
continue; // no feature dir / no state dir — nothing to scan
|
|
6304
|
+
}
|
|
6305
|
+
let st;
|
|
6306
|
+
try {
|
|
6307
|
+
st = lstatSync(duePath);
|
|
6308
|
+
} catch {
|
|
6309
|
+
continue; // no due-file — the common, silent case
|
|
6310
|
+
}
|
|
6311
|
+
if (!st.isFile() || st.size > 64 * 1024) {
|
|
6312
|
+
malformed++; // exists but is not a plain small file — named, never silently dropped
|
|
6313
|
+
continue;
|
|
6314
|
+
}
|
|
6315
|
+
try {
|
|
6316
|
+
const debt = parseReqeDebt(readFileSync(duePath, 'utf-8'));
|
|
6317
|
+
if (debt && debt.slug === slug) out.push({ debt, duePath, dir });
|
|
6318
|
+
else malformed++;
|
|
6319
|
+
} catch {
|
|
6320
|
+
malformed++;
|
|
6321
|
+
}
|
|
6322
|
+
}
|
|
6323
|
+
return { debts: out, malformed };
|
|
6324
|
+
}
|
|
6325
|
+
|
|
6326
|
+
/**
|
|
6327
|
+
* `dz reqe` — the re-QE debt ledger (backlog 6b40e667): list usage-switched same-family QE debts,
|
|
6328
|
+
* print the cross-family review brief, settle FAIL-CLOSED against a graded report.
|
|
6329
|
+
*/
|
|
6330
|
+
function cmdReqe(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
6331
|
+
const json = flags.has('json');
|
|
6332
|
+
if (flags.has('help')) {
|
|
6333
|
+
const usage = 'dz reqe [--slug <feature> [--done --report <file>]] [--project <dir>] [--json]';
|
|
6334
|
+
if (json) write(JSON.stringify({ help: usage, exitCode: 0 }));
|
|
6335
|
+
else {
|
|
6336
|
+
write(usage + ' — the re-QE debt ledger');
|
|
6337
|
+
write(' (no args) list unsettled debts (runs whose Step-8 QE ran on the coder’s own family under the usage override)');
|
|
6338
|
+
write(' --slug <s> print the ready cross-family review brief for one debt');
|
|
6339
|
+
write(' --slug <s> --done --report <file> settle the debt — FAIL-CLOSED: requires an existing, non-trivial, GRADED report; appends the settlement to 08_qe_report.md');
|
|
6340
|
+
write(' ' + REQE_SCOPE);
|
|
6341
|
+
}
|
|
6342
|
+
return 0;
|
|
6343
|
+
}
|
|
6344
|
+
for (const flag of flags) {
|
|
6345
|
+
if (!new Set(['json', 'help', 'done']).has(flag)) {
|
|
6346
|
+
write(json ? JSON.stringify({ error: `unknown option --${flag}`, exitCode: 1 }) : `dz reqe: unknown option --${flag}\n allowed: --slug <feature>, --done, --report <file>, --project <dir>, --json`);
|
|
6347
|
+
return 1;
|
|
6348
|
+
}
|
|
6349
|
+
}
|
|
6350
|
+
for (const key of options.keys()) {
|
|
6351
|
+
if (key.startsWith('_positional_') || !new Set(['slug', 'report', 'project']).has(key)) {
|
|
6352
|
+
const what = key.startsWith('_positional_') ? `unexpected argument "${options.get(key)}"` : `unknown option --${key}`;
|
|
6353
|
+
write(json ? JSON.stringify({ error: what, exitCode: 1 }) : `dz reqe: ${what}`);
|
|
6354
|
+
return 1;
|
|
6355
|
+
}
|
|
6356
|
+
}
|
|
6357
|
+
const root = resolve(cwd, options.get('project') ?? '.');
|
|
6358
|
+
const slug = options.get('slug') ?? '';
|
|
6359
|
+
if (slug !== '' && (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(slug) || slug.includes('..'))) {
|
|
6360
|
+
write(json ? JSON.stringify({ error: 'a plain --slug <feature> is required (no path separators)', exitCode: 1 }) : 'dz reqe: a plain --slug <feature> is required (no path separators)');
|
|
6361
|
+
return 1;
|
|
6362
|
+
}
|
|
6363
|
+
|
|
6364
|
+
const { debts, malformed } = scanReqeDebts(root);
|
|
6365
|
+
|
|
6366
|
+
if (slug === '') {
|
|
6367
|
+
if (flags.has('done')) {
|
|
6368
|
+
write(json ? JSON.stringify({ error: '--done requires --slug', exitCode: 1 }) : 'dz reqe: --done requires --slug <feature>');
|
|
6369
|
+
return 1;
|
|
6370
|
+
}
|
|
6371
|
+
if (json) write(JSON.stringify({ debts: debts.map((d) => d.debt), malformed, scope: REQE_SCOPE, exitCode: 0 }));
|
|
6372
|
+
else for (const line of renderReqeList(debts.map((d) => d.debt), malformed)) write(line);
|
|
6373
|
+
return 0;
|
|
6374
|
+
}
|
|
6375
|
+
|
|
6376
|
+
const hit = debts.find((d) => d.debt.slug === slug) ?? null;
|
|
6377
|
+
if (!hit) {
|
|
6378
|
+
// distinguish "no debt file" from "malformed debt file" — a corrupt debt must be named
|
|
6379
|
+
const duePath = join(root, 'features', slug, '.fa-state', 'reqe-due.json');
|
|
6380
|
+
const exists = existsSync(duePath);
|
|
6381
|
+
const msg = exists
|
|
6382
|
+
? `debt file at features/${slug}/.fa-state/reqe-due.json is MALFORMED — inspect it manually (a corrupt debt is named, never auto-cleared)`
|
|
6383
|
+
: `no re-QE debt recorded for "${slug}"`;
|
|
6384
|
+
write(json ? JSON.stringify({ error: msg, exitCode: 1 }) : `dz reqe: ${msg}`);
|
|
6385
|
+
return 1;
|
|
6386
|
+
}
|
|
6387
|
+
|
|
6388
|
+
if (!flags.has('done')) {
|
|
6389
|
+
const brief = buildReqeBrief(hit.debt, join('features', slug));
|
|
6390
|
+
if (json) write(JSON.stringify({ debt: hit.debt, brief, exitCode: 0 }));
|
|
6391
|
+
else {
|
|
6392
|
+
write(brief.header);
|
|
6393
|
+
for (const line of brief.instructions) write(' ' + line);
|
|
6394
|
+
if (brief.codexCmdTemplate) write(' codex template: ' + brief.codexCmdTemplate);
|
|
6395
|
+
write(' ' + REQE_SCOPE);
|
|
6396
|
+
}
|
|
6397
|
+
return 0;
|
|
6398
|
+
}
|
|
6399
|
+
|
|
6400
|
+
// --done: FAIL-CLOSED settlement
|
|
6401
|
+
const reportOpt = options.get('report') ?? '';
|
|
6402
|
+
if (reportOpt === '') {
|
|
6403
|
+
write(json ? JSON.stringify({ error: '--done requires --report <file> (the cross-family re-QE report)', exitCode: 1 }) : 'dz reqe: --done requires --report <file>');
|
|
6404
|
+
return 1;
|
|
6405
|
+
}
|
|
6406
|
+
const reportPath = resolve(root, reportOpt);
|
|
6407
|
+
const qeReportPath = join(hit.dir, '08_qe_report.md');
|
|
6408
|
+
// QE #1: the run's OWN same-family Step-8 report must never settle its own debt — the exact
|
|
6409
|
+
// laundering this ledger exists to prevent. Compared by REAL path so a symlink cannot alias it.
|
|
6410
|
+
try {
|
|
6411
|
+
const sameReal = realpathSync(reportPath) === realpathSync(qeReportPath);
|
|
6412
|
+
// r2 #1: a HARD LINK to the same report has a different path but the same inode — compare
|
|
6413
|
+
// dev+ino, not just resolved paths
|
|
6414
|
+
const rSt = statSync(reportPath);
|
|
6415
|
+
const qSt = statSync(qeReportPath);
|
|
6416
|
+
const sameInode = rSt.dev === qSt.dev && rSt.ino === qSt.ino;
|
|
6417
|
+
if (sameReal || sameInode) {
|
|
6418
|
+
write(json ? JSON.stringify({ error: 'the report IS this run’s own 08_qe_report.md (same file/inode) — the same-family review cannot settle its own debt; provide the independent cross-family report', exitCode: 1 }) : 'dz reqe: the report IS this run’s own 08_qe_report.md (same file/inode) — the same-family review cannot settle its own debt (fail-closed)');
|
|
6419
|
+
return 1;
|
|
6420
|
+
}
|
|
6421
|
+
} catch { /* one of the two does not resolve — the reads below decide */ }
|
|
6422
|
+
let reportText = '';
|
|
6423
|
+
try {
|
|
6424
|
+
const st = lstatSync(reportPath);
|
|
6425
|
+
if (!st.isFile()) throw new Error('not a regular file'); // symlinked report refused (QE #5)
|
|
6426
|
+
reportText = readFileSync(reportPath, 'utf-8');
|
|
6427
|
+
} catch {
|
|
6428
|
+
write(json ? JSON.stringify({ error: `report not readable at ${reportOpt} (must be a regular file, not a symlink) — refusing to settle`, exitCode: 1 }) : `dz reqe: report not readable at ${reportOpt} (must be a regular file, not a symlink) — refusing to settle (fail-closed)`);
|
|
6429
|
+
return 1;
|
|
6430
|
+
}
|
|
6431
|
+
const settlement = settleReqeDebt(hit.debt, reportText, reportOpt);
|
|
6432
|
+
if (!settlement.ok || settlement.epilogue === null) {
|
|
6433
|
+
write(json ? JSON.stringify({ error: settlement.error, exitCode: 1 }) : `dz reqe: ${settlement.error}`);
|
|
6434
|
+
return 1;
|
|
6435
|
+
}
|
|
6436
|
+
// QE #6 ordering: rotate the debt FIRST, append the human-readable epilogue LAST — a crash may
|
|
6437
|
+
// leave a settled debt without its epilogue (recoverable from reqe-settled.json), never an
|
|
6438
|
+
// epilogue claiming clearance while the debt is still live.
|
|
6439
|
+
const settledBase = join(hit.dir, '.fa-state', 'reqe-settled');
|
|
6440
|
+
let settledPath = settledBase + '.json';
|
|
6441
|
+
for (let n = 2; existsSync(settledPath); n++) settledPath = settledBase + '-' + n + '.json'; // never overwrite prior evidence
|
|
6442
|
+
try {
|
|
6443
|
+
const settled = { ...hit.debt, settledGrade: settlement.grade, settledReport: reportOpt };
|
|
6444
|
+
writeFileSync(settledPath, JSON.stringify(settled) + '\n', { flag: 'wx' }); // wx: never through a planted symlink
|
|
6445
|
+
try {
|
|
6446
|
+
unlinkSync(hit.duePath);
|
|
6447
|
+
} catch (e) {
|
|
6448
|
+
// r2 #5: a failed unlink must not leave a settled marker beside a live debt — roll back
|
|
6449
|
+
try { unlinkSync(settledPath); } catch { /* best-effort rollback */ }
|
|
6450
|
+
throw e;
|
|
6451
|
+
}
|
|
6452
|
+
} catch {
|
|
6453
|
+
write(json ? JSON.stringify({ error: 'could not rotate the debt file — debt NOT cleared, nothing was appended', exitCode: 1 }) : 'dz reqe: could not rotate the debt file — debt NOT cleared, nothing was appended (fail-closed)');
|
|
6454
|
+
return 1;
|
|
6455
|
+
}
|
|
6456
|
+
try {
|
|
6457
|
+
if (lstatSync(qeReportPath).isSymbolicLink()) throw new Error('symlinked 08_qe_report.md');
|
|
6458
|
+
appendFileSync(qeReportPath, settlement.epilogue);
|
|
6459
|
+
} catch {
|
|
6460
|
+
write(json ? JSON.stringify({ settled: true, grade: settlement.grade, warning: 'debt cleared (see .fa-state/reqe-settled*.json) but the epilogue could NOT be appended to 08_qe_report.md — append it manually', exitCode: 0 }) : 'dz reqe: debt cleared (evidence in .fa-state/) but the epilogue could NOT be appended to 08_qe_report.md — append it manually');
|
|
6461
|
+
return 0;
|
|
6462
|
+
}
|
|
6463
|
+
const okMsg = `debt settled: re-QE grade ${settlement.grade} (report ${reportOpt}) — settlement appended to features/${slug}/08_qe_report.md`;
|
|
6464
|
+
write(json ? JSON.stringify({ settled: true, grade: settlement.grade, exitCode: 0 }) : `dz reqe: ${okMsg}`);
|
|
6465
|
+
return 0;
|
|
6466
|
+
}
|
|
6467
|
+
|
|
6216
6468
|
function cmdScore(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
6217
6469
|
const json = flags.has('json');
|
|
6218
6470
|
if (flags.has('help')) {
|
|
@@ -6285,8 +6537,21 @@ function cmdScore(options: Map<string, string>, flags: Set<string>, cwd: string,
|
|
|
6285
6537
|
walk(dir, '', 0);
|
|
6286
6538
|
|
|
6287
6539
|
const card = scoreRun(slug, artifacts);
|
|
6288
|
-
|
|
6289
|
-
|
|
6540
|
+
// re-QE debt note (backlog 6b40e667): a scorecard that praised cross-model QE while an unsettled
|
|
6541
|
+
// same-family debt sits on disk would be half the truth. PARSED, not existence-guessed (QE #9);
|
|
6542
|
+
// a malformed debt file is named. Advisory in both modes — never moves the score or exit code.
|
|
6543
|
+
let reqeNote: string | null = null;
|
|
6544
|
+
{
|
|
6545
|
+
const scan = scanReqeDebts(root);
|
|
6546
|
+
const mine = scan.debts.find((d) => d.debt.slug === slug);
|
|
6547
|
+
if (mine) reqeNote = 'UNSETTLED re-QE debt — Step-8 ran same-family under the usage override; run `dz reqe --slug ' + slug + '`';
|
|
6548
|
+
else if (existsSync(join(dir, '.fa-state', 'reqe-due.json'))) reqeNote = 'a reqe-due.json exists but is MALFORMED/mismatched — inspect features/' + slug + '/.fa-state/ manually';
|
|
6549
|
+
}
|
|
6550
|
+
if (json) write(JSON.stringify({ ...card, reqeDebt: reqeNote, exitCode: 0 }, null, 2));
|
|
6551
|
+
else {
|
|
6552
|
+
write(renderScorecard(card));
|
|
6553
|
+
if (reqeNote) write('note: ' + reqeNote);
|
|
6554
|
+
}
|
|
6290
6555
|
return 0;
|
|
6291
6556
|
}
|
|
6292
6557
|
|
|
@@ -7815,6 +8080,8 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
7815
8080
|
return cmdEpochReplay(options, flags, cwd, write);
|
|
7816
8081
|
case 'score':
|
|
7817
8082
|
return cmdScore(options, flags, cwd, write);
|
|
8083
|
+
case 'reqe':
|
|
8084
|
+
return cmdReqe(options, flags, cwd, write);
|
|
7818
8085
|
case 'backlog':
|
|
7819
8086
|
return await cmdBacklog(options, flags, cwd, write);
|
|
7820
8087
|
case 'routing':
|