@dzhechkov/harness-cli 0.3.257 → 0.3.259
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 +127 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +307 -5
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
- package/sbom.json +7 -7
- package/src/cli.ts +296 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/harness-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.259",
|
|
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.149",
|
|
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';
|
|
@@ -80,6 +80,11 @@ import {
|
|
|
80
80
|
statuslineData,
|
|
81
81
|
writeFeatureAdrState,
|
|
82
82
|
computeUsage,
|
|
83
|
+
deriveCostLedger,
|
|
84
|
+
renderCostLedger,
|
|
85
|
+
verifyCostLedgerReport,
|
|
86
|
+
writeCostLedgerJsonl,
|
|
87
|
+
COST_LEDGER_SCOPE,
|
|
83
88
|
deriveUsageCalibration,
|
|
84
89
|
normalizeClaudeUsageModelKey,
|
|
85
90
|
readUsageLimits,
|
|
@@ -270,7 +275,13 @@ import {
|
|
|
270
275
|
makeBacklogIO,
|
|
271
276
|
harmonizeBacklog,
|
|
272
277
|
BACKLOG_BACKENDS,
|
|
278
|
+
parseReqeDebt,
|
|
279
|
+
buildReqeBrief,
|
|
280
|
+
settleReqeDebt,
|
|
281
|
+
renderReqeList,
|
|
282
|
+
REQE_SCOPE,
|
|
273
283
|
} from '@dzhechkov/harness-core';
|
|
284
|
+
import type { ReqeDebt } from '@dzhechkov/harness-core';
|
|
274
285
|
import type { IdeaRecord, IdeaStatus } from '@dzhechkov/harness-core';
|
|
275
286
|
import type { Family, ModelRung, Candidate as BtoCandidate, DimScores } from '@dzhechkov/harness-core';
|
|
276
287
|
import type { SetupSpec } from '@dzhechkov/harness-core';
|
|
@@ -309,6 +320,7 @@ Usage:
|
|
|
309
320
|
dz epoch-replay --judge <filled-work-order.json> [--out <file>] (blind judge prompts from the filled plans)
|
|
310
321
|
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)
|
|
311
322
|
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)
|
|
323
|
+
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)
|
|
312
324
|
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)
|
|
313
325
|
dz backlog list [--status <s>] [--goal <id>] [--project <dir>] [--json] (list captured ideas, filterable by status/goal)
|
|
314
326
|
dz backlog show <id> [--project <dir>] [--json] (full record for one idea)
|
|
@@ -340,6 +352,7 @@ Usage:
|
|
|
340
352
|
dz statusline [--json] [--install] [--project <dir>] (live self-learning panel for Claude Code's status bar; reads the CC JSON payload from STDIN)
|
|
341
353
|
dz statusline --fa-record --slug <s> --step "<label>" [--recalled <n>] [--stored <n>] [--mode <m>] (feature-adr: record live per-run learning state → 📐 panel segment)
|
|
342
354
|
dz usage [--json] [--project <dir>] | dz usage --calibrate --session <pct> --weekly <pct> [--model fable=<pct>] [--project <dir>] (ESTIMATE Claude usage from fixed reset windows; optional per-model weekly binding; exit 0 ALWAYS; pct=null when limits unconfigured)
|
|
355
|
+
dz usage --by-stage [--run <runId> | --slug <slug>] [--epsilon <0..1>] [--write <file.jsonl>] [--json] (per-stage cost ledger for ONE feature-adr run + the reconciliation invariant: accounted + unaccounted = run total; verdict BALANCED | DEFECT | INSUFFICIENT_DATA; local transcript ESTIMATES — catches ATTRIBUTION errors, not pricing errors)
|
|
343
356
|
dz claim-check [paths...] [--json] [--fail-on high|medium|none] [--project <dir>] (enforce the Integrity Rule: flag untagged/overstated accuracy claims; default scan = root README.md + every discovered package's README.md + features/*/08_qe_report.md + docs/**/*.md (historical feature artifacts are NOT scanned — pass paths explicitly); exit 1 only at/above --fail-on, default high)
|
|
344
357
|
dz pretrain [--project <dir>]
|
|
345
358
|
dz recommend "<task description>"
|
|
@@ -1509,6 +1522,64 @@ function cmdUsageCalibrate(
|
|
|
1509
1522
|
}
|
|
1510
1523
|
}
|
|
1511
1524
|
|
|
1525
|
+
/**
|
|
1526
|
+
* `dz usage --by-stage` — the per-stage cost ledger for one feature-adr run (feature `cost-ledger`).
|
|
1527
|
+
*
|
|
1528
|
+
* A run reports ONE number; this turns it into an itemized receipt keyed by the workflow's OWN
|
|
1529
|
+
* `stageLabel()` strings, plus the reconciliation line that guards the bookkeeping itself. Derived
|
|
1530
|
+
* post-hoc from the transcripts already on disk — no workflow edit, killed runs included.
|
|
1531
|
+
*
|
|
1532
|
+
* Exit code is 0 ALWAYS, matching the rest of `dz usage`; the VERDICT (`BALANCED` / `DEFECT` /
|
|
1533
|
+
* `INSUFFICIENT_DATA`) is the signal, and `INSUFFICIENT_DATA` is not success.
|
|
1534
|
+
*/
|
|
1535
|
+
function cmdUsageByStage(
|
|
1536
|
+
options: Map<string, string>,
|
|
1537
|
+
flags: Set<string>,
|
|
1538
|
+
write: Write,
|
|
1539
|
+
): number {
|
|
1540
|
+
const runId = options.get('run');
|
|
1541
|
+
const slug = options.get('slug');
|
|
1542
|
+
const epsilonRaw = options.get('epsilon');
|
|
1543
|
+
const epsilon = epsilonRaw === undefined ? undefined : Number(epsilonRaw);
|
|
1544
|
+
const report = deriveCostLedger({
|
|
1545
|
+
...(runId !== undefined ? { runId } : {}),
|
|
1546
|
+
...(slug !== undefined ? { slug } : {}),
|
|
1547
|
+
...(epsilon !== undefined && Number.isFinite(epsilon) ? { epsilon } : {}),
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
if (report === null) {
|
|
1551
|
+
// An ABSENT run is never a balanced empty report (ADR-003).
|
|
1552
|
+
const detail = runId !== undefined ? `run ${runId}` : slug !== undefined ? `slug ${slug}` : 'any workflow run';
|
|
1553
|
+
if (flags.has('json')) {
|
|
1554
|
+
write(JSON.stringify({ verdict: 'INSUFFICIENT_DATA', reason: `no workflow run record found for ${detail}`, rows: [], estimated: true, scope: COST_LEDGER_SCOPE }));
|
|
1555
|
+
} else {
|
|
1556
|
+
write(`usage --by-stage: INSUFFICIENT_DATA — no workflow run record found for ${detail}`);
|
|
1557
|
+
write(`usage --by-stage: scope: ${COST_LEDGER_SCOPE}`);
|
|
1558
|
+
}
|
|
1559
|
+
return 0;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// The verifier re-derives the identities from the EMITTED report — a builder bug must surface as
|
|
1563
|
+
// a finding, not as a plausible table.
|
|
1564
|
+
const verifyDefects = verifyCostLedgerReport(report);
|
|
1565
|
+
|
|
1566
|
+
const outPath = options.get('write');
|
|
1567
|
+
let wrote: boolean | null = null;
|
|
1568
|
+
if (outPath !== undefined && outPath.length > 0) wrote = writeCostLedgerJsonl(resolve(outPath), report);
|
|
1569
|
+
|
|
1570
|
+
if (flags.has('json')) {
|
|
1571
|
+
write(JSON.stringify({ ...report, verifyDefects, ...(wrote === null ? {} : { wrote, writePath: resolve(outPath ?? '') }) }));
|
|
1572
|
+
return 0;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
write(renderCostLedger(report));
|
|
1576
|
+
for (const d of verifyDefects) write(` verifier: ${d.kind}: ${d.detail}`);
|
|
1577
|
+
if (wrote !== null) {
|
|
1578
|
+
write(` ${wrote ? 'wrote' : 'FAILED to write'} ${resolve(outPath ?? '')} (derived report — regenerable, never read back)`);
|
|
1579
|
+
}
|
|
1580
|
+
return 0;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1512
1583
|
/**
|
|
1513
1584
|
* `dz usage` — print an ESTIMATE of Claude session + weekly usage from fixed reset windows,
|
|
1514
1585
|
* aggregated READONLY from the local transcript store (see {@link computeUsage}). `--json` emits
|
|
@@ -1540,6 +1611,7 @@ function cmdUsage(
|
|
|
1540
1611
|
});
|
|
1541
1612
|
try {
|
|
1542
1613
|
if (flags.has('calibrate')) return cmdUsageCalibrate(options, optionLists, cwd, write);
|
|
1614
|
+
if (flags.has('by-stage')) return cmdUsageByStage(options, flags, write);
|
|
1543
1615
|
|
|
1544
1616
|
const u = computeUsage(projectRoot);
|
|
1545
1617
|
const lim = readUsageLimits(projectRoot);
|
|
@@ -1561,6 +1633,7 @@ function cmdUsage(
|
|
|
1561
1633
|
limits: typeof limitsPayload;
|
|
1562
1634
|
weeklyByModel?: typeof u.weeklyByModel;
|
|
1563
1635
|
estimated: true;
|
|
1636
|
+
reqeDue?: number;
|
|
1564
1637
|
} = {
|
|
1565
1638
|
sessionPct: u.sessionPct,
|
|
1566
1639
|
weeklyPct: u.weeklyPct,
|
|
@@ -1571,6 +1644,13 @@ function cmdUsage(
|
|
|
1571
1644
|
estimated: true,
|
|
1572
1645
|
};
|
|
1573
1646
|
if (hasModelLimits && u.weeklyByModel !== undefined) payload.weeklyByModel = u.weeklyByModel;
|
|
1647
|
+
// re-QE debt surfacing (backlog 6b40e667 — QE #9: the json contract must carry the debt too,
|
|
1648
|
+
// a probe is exactly the consumer that needs it). The field appears ONLY when a debt exists,
|
|
1649
|
+
// so the zero-debt contract stays byte-identical to the pinned legacy shape. Best-effort.
|
|
1650
|
+
try {
|
|
1651
|
+
const reqeCount = scanReqeDebts(resolve(cwd, options.get('project') ?? '.')).debts.length;
|
|
1652
|
+
if (reqeCount > 0) payload.reqeDue = reqeCount;
|
|
1653
|
+
} catch { /* advisory only */ }
|
|
1574
1654
|
write(
|
|
1575
1655
|
JSON.stringify(payload),
|
|
1576
1656
|
);
|
|
@@ -1580,6 +1660,10 @@ function cmdUsage(
|
|
|
1580
1660
|
write(
|
|
1581
1661
|
'usage: unconfigured — set memory.usage.sessionTokenLimit / weeklyTokenLimit in .dz/config.json (percentages are ESTIMATES calibrated from observed exhaustion)',
|
|
1582
1662
|
);
|
|
1663
|
+
try {
|
|
1664
|
+
const reqe = scanReqeDebts(resolve(cwd, options.get('project') ?? '.'));
|
|
1665
|
+
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');
|
|
1666
|
+
} catch { /* advisory only */ }
|
|
1583
1667
|
return 0;
|
|
1584
1668
|
}
|
|
1585
1669
|
// Compact human line — a short HH:MM / weekday hint on the resets, best-effort.
|
|
@@ -1595,6 +1679,12 @@ function cmdUsage(
|
|
|
1595
1679
|
const binding = hasModelLimits && u.weeklyBindingModel !== undefined ? ' ' + u.weeklyBindingModel + '-bound' : '';
|
|
1596
1680
|
const w = u.weeklyPct === null ? 'n/a' : '~' + u.weeklyPct + '%' + binding;
|
|
1597
1681
|
write('usage: session ' + s + ' (resets ' + clock(u.sessionResetsAt) + ') · week ' + w + ' (resets ' + clock(u.weeklyResetsAt) + ') · estimated');
|
|
1682
|
+
// re-QE debt surfacing (backlog 6b40e667): the moment someone checks usage is the moment a
|
|
1683
|
+
// usage-switched self-review debt should be visible. Best-effort — never breaks the contract.
|
|
1684
|
+
try {
|
|
1685
|
+
const reqe = scanReqeDebts(resolve(cwd, options.get('project') ?? '.'));
|
|
1686
|
+
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');
|
|
1687
|
+
} catch { /* advisory only */ }
|
|
1598
1688
|
return 0;
|
|
1599
1689
|
} catch {
|
|
1600
1690
|
// never let a probe see a non-zero exit — print the all-null contract and exit 0.
|
|
@@ -6148,6 +6238,194 @@ function nameFor(t: { file: string; name?: string }, outcome: 'pass' | 'fail' |
|
|
|
6148
6238
|
* `dz score --slug <feature>` — score a feature-adr RUN's process discipline from its artifacts
|
|
6149
6239
|
* (feature dz-score, Reading C). Descriptive-only: exit 0 on any score; non-zero only on usage errors.
|
|
6150
6240
|
*/
|
|
6241
|
+
/** Scan features/<slug>/.fa-state/reqe-due.json under a project root. Symlinked ANCESTOR dirs are
|
|
6242
|
+
* skipped too, not just the leaf (Codex QE #5 — a symlinked feature dir could smuggle an outside
|
|
6243
|
+
* debt); an oversized or non-regular due-file counts as MALFORMED, never a silent skip (QE #8);
|
|
6244
|
+
* a debt whose embedded slug differs from its directory is MALFORMED — identity is the directory,
|
|
6245
|
+
* the JSON only confirms it (QE #4: an embedded foreign slug must not redirect settlement). */
|
|
6246
|
+
function scanReqeDebts(root: string): { debts: Array<{ debt: ReqeDebt; duePath: string; dir: string }>; malformed: number } {
|
|
6247
|
+
const out: Array<{ debt: ReqeDebt; duePath: string; dir: string }> = [];
|
|
6248
|
+
let malformed = 0;
|
|
6249
|
+
const featuresDir = join(root, 'features');
|
|
6250
|
+
let slugs: string[] = [];
|
|
6251
|
+
try {
|
|
6252
|
+
if (lstatSync(featuresDir).isSymbolicLink()) return { debts: [], malformed: 0 }; // r2 #4: features/ itself
|
|
6253
|
+
slugs = readdirSync(featuresDir);
|
|
6254
|
+
} catch {
|
|
6255
|
+
return { debts: [], malformed: 0 };
|
|
6256
|
+
}
|
|
6257
|
+
for (const slug of slugs.sort()) {
|
|
6258
|
+
const dir = join(featuresDir, slug);
|
|
6259
|
+
const stateDir = join(dir, '.fa-state');
|
|
6260
|
+
const duePath = join(stateDir, 'reqe-due.json');
|
|
6261
|
+
try {
|
|
6262
|
+
if (lstatSync(dir).isSymbolicLink() || lstatSync(stateDir).isSymbolicLink()) continue;
|
|
6263
|
+
} catch {
|
|
6264
|
+
continue; // no feature dir / no state dir — nothing to scan
|
|
6265
|
+
}
|
|
6266
|
+
let st;
|
|
6267
|
+
try {
|
|
6268
|
+
st = lstatSync(duePath);
|
|
6269
|
+
} catch {
|
|
6270
|
+
continue; // no due-file — the common, silent case
|
|
6271
|
+
}
|
|
6272
|
+
if (!st.isFile() || st.size > 64 * 1024) {
|
|
6273
|
+
malformed++; // exists but is not a plain small file — named, never silently dropped
|
|
6274
|
+
continue;
|
|
6275
|
+
}
|
|
6276
|
+
try {
|
|
6277
|
+
const debt = parseReqeDebt(readFileSync(duePath, 'utf-8'));
|
|
6278
|
+
if (debt && debt.slug === slug) out.push({ debt, duePath, dir });
|
|
6279
|
+
else malformed++;
|
|
6280
|
+
} catch {
|
|
6281
|
+
malformed++;
|
|
6282
|
+
}
|
|
6283
|
+
}
|
|
6284
|
+
return { debts: out, malformed };
|
|
6285
|
+
}
|
|
6286
|
+
|
|
6287
|
+
/**
|
|
6288
|
+
* `dz reqe` — the re-QE debt ledger (backlog 6b40e667): list usage-switched same-family QE debts,
|
|
6289
|
+
* print the cross-family review brief, settle FAIL-CLOSED against a graded report.
|
|
6290
|
+
*/
|
|
6291
|
+
function cmdReqe(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
6292
|
+
const json = flags.has('json');
|
|
6293
|
+
if (flags.has('help')) {
|
|
6294
|
+
const usage = 'dz reqe [--slug <feature> [--done --report <file>]] [--project <dir>] [--json]';
|
|
6295
|
+
if (json) write(JSON.stringify({ help: usage, exitCode: 0 }));
|
|
6296
|
+
else {
|
|
6297
|
+
write(usage + ' — the re-QE debt ledger');
|
|
6298
|
+
write(' (no args) list unsettled debts (runs whose Step-8 QE ran on the coder’s own family under the usage override)');
|
|
6299
|
+
write(' --slug <s> print the ready cross-family review brief for one debt');
|
|
6300
|
+
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');
|
|
6301
|
+
write(' ' + REQE_SCOPE);
|
|
6302
|
+
}
|
|
6303
|
+
return 0;
|
|
6304
|
+
}
|
|
6305
|
+
for (const flag of flags) {
|
|
6306
|
+
if (!new Set(['json', 'help', 'done']).has(flag)) {
|
|
6307
|
+
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`);
|
|
6308
|
+
return 1;
|
|
6309
|
+
}
|
|
6310
|
+
}
|
|
6311
|
+
for (const key of options.keys()) {
|
|
6312
|
+
if (key.startsWith('_positional_') || !new Set(['slug', 'report', 'project']).has(key)) {
|
|
6313
|
+
const what = key.startsWith('_positional_') ? `unexpected argument "${options.get(key)}"` : `unknown option --${key}`;
|
|
6314
|
+
write(json ? JSON.stringify({ error: what, exitCode: 1 }) : `dz reqe: ${what}`);
|
|
6315
|
+
return 1;
|
|
6316
|
+
}
|
|
6317
|
+
}
|
|
6318
|
+
const root = resolve(cwd, options.get('project') ?? '.');
|
|
6319
|
+
const slug = options.get('slug') ?? '';
|
|
6320
|
+
if (slug !== '' && (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(slug) || slug.includes('..'))) {
|
|
6321
|
+
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)');
|
|
6322
|
+
return 1;
|
|
6323
|
+
}
|
|
6324
|
+
|
|
6325
|
+
const { debts, malformed } = scanReqeDebts(root);
|
|
6326
|
+
|
|
6327
|
+
if (slug === '') {
|
|
6328
|
+
if (flags.has('done')) {
|
|
6329
|
+
write(json ? JSON.stringify({ error: '--done requires --slug', exitCode: 1 }) : 'dz reqe: --done requires --slug <feature>');
|
|
6330
|
+
return 1;
|
|
6331
|
+
}
|
|
6332
|
+
if (json) write(JSON.stringify({ debts: debts.map((d) => d.debt), malformed, scope: REQE_SCOPE, exitCode: 0 }));
|
|
6333
|
+
else for (const line of renderReqeList(debts.map((d) => d.debt), malformed)) write(line);
|
|
6334
|
+
return 0;
|
|
6335
|
+
}
|
|
6336
|
+
|
|
6337
|
+
const hit = debts.find((d) => d.debt.slug === slug) ?? null;
|
|
6338
|
+
if (!hit) {
|
|
6339
|
+
// distinguish "no debt file" from "malformed debt file" — a corrupt debt must be named
|
|
6340
|
+
const duePath = join(root, 'features', slug, '.fa-state', 'reqe-due.json');
|
|
6341
|
+
const exists = existsSync(duePath);
|
|
6342
|
+
const msg = exists
|
|
6343
|
+
? `debt file at features/${slug}/.fa-state/reqe-due.json is MALFORMED — inspect it manually (a corrupt debt is named, never auto-cleared)`
|
|
6344
|
+
: `no re-QE debt recorded for "${slug}"`;
|
|
6345
|
+
write(json ? JSON.stringify({ error: msg, exitCode: 1 }) : `dz reqe: ${msg}`);
|
|
6346
|
+
return 1;
|
|
6347
|
+
}
|
|
6348
|
+
|
|
6349
|
+
if (!flags.has('done')) {
|
|
6350
|
+
const brief = buildReqeBrief(hit.debt, join('features', slug));
|
|
6351
|
+
if (json) write(JSON.stringify({ debt: hit.debt, brief, exitCode: 0 }));
|
|
6352
|
+
else {
|
|
6353
|
+
write(brief.header);
|
|
6354
|
+
for (const line of brief.instructions) write(' ' + line);
|
|
6355
|
+
if (brief.codexCmdTemplate) write(' codex template: ' + brief.codexCmdTemplate);
|
|
6356
|
+
write(' ' + REQE_SCOPE);
|
|
6357
|
+
}
|
|
6358
|
+
return 0;
|
|
6359
|
+
}
|
|
6360
|
+
|
|
6361
|
+
// --done: FAIL-CLOSED settlement
|
|
6362
|
+
const reportOpt = options.get('report') ?? '';
|
|
6363
|
+
if (reportOpt === '') {
|
|
6364
|
+
write(json ? JSON.stringify({ error: '--done requires --report <file> (the cross-family re-QE report)', exitCode: 1 }) : 'dz reqe: --done requires --report <file>');
|
|
6365
|
+
return 1;
|
|
6366
|
+
}
|
|
6367
|
+
const reportPath = resolve(root, reportOpt);
|
|
6368
|
+
const qeReportPath = join(hit.dir, '08_qe_report.md');
|
|
6369
|
+
// QE #1: the run's OWN same-family Step-8 report must never settle its own debt — the exact
|
|
6370
|
+
// laundering this ledger exists to prevent. Compared by REAL path so a symlink cannot alias it.
|
|
6371
|
+
try {
|
|
6372
|
+
const sameReal = realpathSync(reportPath) === realpathSync(qeReportPath);
|
|
6373
|
+
// r2 #1: a HARD LINK to the same report has a different path but the same inode — compare
|
|
6374
|
+
// dev+ino, not just resolved paths
|
|
6375
|
+
const rSt = statSync(reportPath);
|
|
6376
|
+
const qSt = statSync(qeReportPath);
|
|
6377
|
+
const sameInode = rSt.dev === qSt.dev && rSt.ino === qSt.ino;
|
|
6378
|
+
if (sameReal || sameInode) {
|
|
6379
|
+
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)');
|
|
6380
|
+
return 1;
|
|
6381
|
+
}
|
|
6382
|
+
} catch { /* one of the two does not resolve — the reads below decide */ }
|
|
6383
|
+
let reportText = '';
|
|
6384
|
+
try {
|
|
6385
|
+
const st = lstatSync(reportPath);
|
|
6386
|
+
if (!st.isFile()) throw new Error('not a regular file'); // symlinked report refused (QE #5)
|
|
6387
|
+
reportText = readFileSync(reportPath, 'utf-8');
|
|
6388
|
+
} catch {
|
|
6389
|
+
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)`);
|
|
6390
|
+
return 1;
|
|
6391
|
+
}
|
|
6392
|
+
const settlement = settleReqeDebt(hit.debt, reportText, reportOpt);
|
|
6393
|
+
if (!settlement.ok || settlement.epilogue === null) {
|
|
6394
|
+
write(json ? JSON.stringify({ error: settlement.error, exitCode: 1 }) : `dz reqe: ${settlement.error}`);
|
|
6395
|
+
return 1;
|
|
6396
|
+
}
|
|
6397
|
+
// QE #6 ordering: rotate the debt FIRST, append the human-readable epilogue LAST — a crash may
|
|
6398
|
+
// leave a settled debt without its epilogue (recoverable from reqe-settled.json), never an
|
|
6399
|
+
// epilogue claiming clearance while the debt is still live.
|
|
6400
|
+
const settledBase = join(hit.dir, '.fa-state', 'reqe-settled');
|
|
6401
|
+
let settledPath = settledBase + '.json';
|
|
6402
|
+
for (let n = 2; existsSync(settledPath); n++) settledPath = settledBase + '-' + n + '.json'; // never overwrite prior evidence
|
|
6403
|
+
try {
|
|
6404
|
+
const settled = { ...hit.debt, settledGrade: settlement.grade, settledReport: reportOpt };
|
|
6405
|
+
writeFileSync(settledPath, JSON.stringify(settled) + '\n', { flag: 'wx' }); // wx: never through a planted symlink
|
|
6406
|
+
try {
|
|
6407
|
+
unlinkSync(hit.duePath);
|
|
6408
|
+
} catch (e) {
|
|
6409
|
+
// r2 #5: a failed unlink must not leave a settled marker beside a live debt — roll back
|
|
6410
|
+
try { unlinkSync(settledPath); } catch { /* best-effort rollback */ }
|
|
6411
|
+
throw e;
|
|
6412
|
+
}
|
|
6413
|
+
} catch {
|
|
6414
|
+
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)');
|
|
6415
|
+
return 1;
|
|
6416
|
+
}
|
|
6417
|
+
try {
|
|
6418
|
+
if (lstatSync(qeReportPath).isSymbolicLink()) throw new Error('symlinked 08_qe_report.md');
|
|
6419
|
+
appendFileSync(qeReportPath, settlement.epilogue);
|
|
6420
|
+
} catch {
|
|
6421
|
+
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');
|
|
6422
|
+
return 0;
|
|
6423
|
+
}
|
|
6424
|
+
const okMsg = `debt settled: re-QE grade ${settlement.grade} (report ${reportOpt}) — settlement appended to features/${slug}/08_qe_report.md`;
|
|
6425
|
+
write(json ? JSON.stringify({ settled: true, grade: settlement.grade, exitCode: 0 }) : `dz reqe: ${okMsg}`);
|
|
6426
|
+
return 0;
|
|
6427
|
+
}
|
|
6428
|
+
|
|
6151
6429
|
function cmdScore(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): number {
|
|
6152
6430
|
const json = flags.has('json');
|
|
6153
6431
|
if (flags.has('help')) {
|
|
@@ -6220,8 +6498,21 @@ function cmdScore(options: Map<string, string>, flags: Set<string>, cwd: string,
|
|
|
6220
6498
|
walk(dir, '', 0);
|
|
6221
6499
|
|
|
6222
6500
|
const card = scoreRun(slug, artifacts);
|
|
6223
|
-
|
|
6224
|
-
|
|
6501
|
+
// re-QE debt note (backlog 6b40e667): a scorecard that praised cross-model QE while an unsettled
|
|
6502
|
+
// same-family debt sits on disk would be half the truth. PARSED, not existence-guessed (QE #9);
|
|
6503
|
+
// a malformed debt file is named. Advisory in both modes — never moves the score or exit code.
|
|
6504
|
+
let reqeNote: string | null = null;
|
|
6505
|
+
{
|
|
6506
|
+
const scan = scanReqeDebts(root);
|
|
6507
|
+
const mine = scan.debts.find((d) => d.debt.slug === slug);
|
|
6508
|
+
if (mine) reqeNote = 'UNSETTLED re-QE debt — Step-8 ran same-family under the usage override; run `dz reqe --slug ' + slug + '`';
|
|
6509
|
+
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';
|
|
6510
|
+
}
|
|
6511
|
+
if (json) write(JSON.stringify({ ...card, reqeDebt: reqeNote, exitCode: 0 }, null, 2));
|
|
6512
|
+
else {
|
|
6513
|
+
write(renderScorecard(card));
|
|
6514
|
+
if (reqeNote) write('note: ' + reqeNote);
|
|
6515
|
+
}
|
|
6225
6516
|
return 0;
|
|
6226
6517
|
}
|
|
6227
6518
|
|
|
@@ -7750,6 +8041,8 @@ export async function runCli(argv: string[], io: CliIo = {}): Promise<number> {
|
|
|
7750
8041
|
return cmdEpochReplay(options, flags, cwd, write);
|
|
7751
8042
|
case 'score':
|
|
7752
8043
|
return cmdScore(options, flags, cwd, write);
|
|
8044
|
+
case 'reqe':
|
|
8045
|
+
return cmdReqe(options, flags, cwd, write);
|
|
7753
8046
|
case 'backlog':
|
|
7754
8047
|
return await cmdBacklog(options, flags, cwd, write);
|
|
7755
8048
|
case 'routing':
|