@dzhechkov/harness-cli 0.3.258 → 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 CHANGED
@@ -9,7 +9,7 @@
9
9
  },
10
10
  {
11
11
  "path": "README.md",
12
- "sha256": "a38e5ef6a741aec138af0de7bd3616d4356d613ba1616bd8b5417194c1ee5f03"
12
+ "sha256": "6b3bcc252253732b9703f1eb43ac1f0e9f0599979b09d0fea3208932fd0f33a8"
13
13
  },
14
14
  {
15
15
  "path": "coverage/coverage-final.json",
@@ -41,15 +41,15 @@
41
41
  },
42
42
  {
43
43
  "path": "dist/cli.d.ts.map",
44
- "sha256": "f954d1e2e3d9f5941481a2b2ccbf553bab892dfa9ba1e7aefb4bf7d1800e9203"
44
+ "sha256": "4c5fa8c51cc350874413efceb0a2e4dce2f0c71f39914ddd8be425336a37ae2d"
45
45
  },
46
46
  {
47
47
  "path": "dist/cli.js",
48
- "sha256": "5fb105be335591fb20401a3b23bac9829777e7ba96c2a2480c29970f4ffa2774"
48
+ "sha256": "a75034dfe70f0b57811dfad831366bd5c7995583a57db0f218621b026ac8d854"
49
49
  },
50
50
  {
51
51
  "path": "dist/cli.js.map",
52
- "sha256": "a4be3aac6dadec03c1fc85fc5354d9a286ac0a21b4983349c4e511cae49dec79"
52
+ "sha256": "dbe8fa418aedcb77dead3a374bba7cbf0fb5e305dd9a06002946a19604279684"
53
53
  },
54
54
  {
55
55
  "path": "dist/index.d.ts",
@@ -73,7 +73,7 @@
73
73
  },
74
74
  {
75
75
  "path": "package.json",
76
- "sha256": "b502ba368d29e25413ce35e8853bc5650c06f686202e51f997f67db0fac03224"
76
+ "sha256": "72858a1f0199832c7ad781f892b7b117ec980af33ff9598587e1ed1544046a35"
77
77
  },
78
78
  {
79
79
  "path": "src/bin.ts",
@@ -81,7 +81,7 @@
81
81
  },
82
82
  {
83
83
  "path": "src/cli.ts",
84
- "sha256": "a369561502f08d9dd4993a294adcb3a38349f543959fa8de31cd1580778062b0"
84
+ "sha256": "1481238a2fc3a60051f678dcf6b415f8052ce954a9e55ee7577d8c3b2261e062"
85
85
  },
86
86
  {
87
87
  "path": "src/index.ts",
@@ -89,7 +89,7 @@
89
89
  },
90
90
  {
91
91
  "path": "test/cli.test.ts",
92
- "sha256": "555aed692218a73f23a343df2f2b38ce028e694dd9d4aff37e727096f88eb50e"
92
+ "sha256": "24f2feb4c5c4fa939beb79cd658b03fbc7ce4bd87f15da563240abcb252c472c"
93
93
  },
94
94
  {
95
95
  "path": "test/epoch-replay-cli.test.ts",
@@ -109,5 +109,5 @@
109
109
  }
110
110
  ]
111
111
  },
112
- "signature": "CXaYCYQIKEVpAB29MZ5rG0/VUWtihZaeJh86r6x8uFpzKrgzHnrzmg/MmTUW3R/rqeA5w12hcfyfr4kl85ZxBA=="
112
+ "signature": "uYnxiEzDxjwdXlp1SGaC0Zv2x/MW39skbUKw3+xpvnLcoNsEV4pp8kFD5q7urBvC87fC+lRKsIpatoXCNGzwBA=="
113
113
  }
package/README.md CHANGED
@@ -132,7 +132,7 @@ point any tool at (a skill is just Markdown). Dedicated targets are on the [road
132
132
 
133
133
  ## User Journey — from install to mastery
134
134
 
135
- All 62 commands mapped to a real workflow:
135
+ All 63 commands mapped to a real workflow:
136
136
 
137
137
  ```
138
138
  DISCOVER → INSTALL → USE → CREATE → MAINTAIN → SHARE
@@ -604,6 +604,41 @@ ever enter a work order.
604
604
  **When to use:** after `dz compounding` reports the replay as READY; before claiming that recall
605
605
  "works"; and any time you want the claim re-checked as the corpus grows.
606
606
 
607
+ ### Пересмотр после аварийного само-ревью — `dz reqe`
608
+
609
+ The feature-adr pipeline's cross-model guard says *the model that writes code must not review it*.
610
+ Under limit pressure (usage-adaptive `>=70%` switch) that guard is CONSCIOUSLY suspended: coder AND
611
+ Step-8 QE both run on Codex (FR-2.9 — a Claude reviewer is exactly the agent that dies at the
612
+ limit). The rule used to say "re-review manually after limits reset" — an instruction nobody
613
+ remembers. `dz reqe` turns it into a **debt with a lifecycle**: the run records
614
+ `features/<slug>/.fa-state/reqe-due.json`, `dz usage` surfaces the count the moment you check your
615
+ freed-up limits, and settlement is FAIL-CLOSED.
616
+
617
+ ```bash
618
+ $ dz usage
619
+ usage: session ~12% (resets 03:00) · week ~41% (resets 06:00) · estimated
620
+ re-QE due: 1 usage-switched run(s) kept same-family QE — run `dz reqe` for the cross-family pass
621
+
622
+ $ dz reqe # the ledger
623
+ dz reqe — 1 unsettled re-QE debt(s):
624
+ add-x coder=openai qe=openai grade=B 2026-07-30T10:00:00Z → dz reqe --slug add-x
625
+
626
+ $ dz reqe --slug add-x # the ready cross-family review brief (the OTHER family than the coder)
627
+ $ dz reqe --slug add-x --done --report features/add-x/08b_reqe_report.md
628
+ dz reqe: debt settled: re-QE grade C (report …) — settlement appended to features/add-x/08_qe_report.md
629
+ ```
630
+
631
+ **When to use:** any time `dz usage` prints a `re-QE due` line, or a feature-adr result carried
632
+ `reqeDue: true`. **Fail-closed settlement:** the report must exist, be non-trivial, and name exactly
633
+ one line-anchored `GRADE` (the boilerplate phrase `GRADE A-F` does not count); the run's own
634
+ `08_qe_report.md` can never settle its own debt (real-path AND inode compared — a hard link doesn't
635
+ fool it); the settlement epilogue lands in `08_qe_report.md` and the due-file rotates to
636
+ `reqe-settled.json` (evidence kept, never deleted). **Honest scope** (printed by the command):
637
+ nothing re-runs QE automatically, and the validator proves the settlement is procedurally sound —
638
+ which model authored the report stays with the human running the brief. RU: гард «кодер не ревьюит
639
+ сам себя» осознанно снимается под лимитом; `dz reqe` превращает инструкцию «перепроверь потом»
640
+ в долг на диске — виден в `dz usage`, гасится только настоящим кросс-семейным отчётом с грейдом.
641
+
607
642
  ### Usage estimates you can act on — `dz usage`
608
643
 
609
644
  `dz usage` estimates how much of your Claude session (5h block) and week you have spent, from local
@@ -953,7 +988,7 @@ Get the whole set with `dz init --target claude-code --preset meta`, or pick one
953
988
 
954
989
  > **A skill and its npx toolkit are not duplicates — they're a graduation.** Several skills (e.g. `feature-adr`, `design-thinking`) exist BOTH as a skill inside a `dz` preset AND as a standalone `npx` package. The preset's SKILL.md is **fully functional on its own** (the whole methodology — modules + references — travels with it, and it auto-activates by description), and it's the only way to compile that capability to the **non-Claude platforms** (Codex/OpenCode/Hermes/OpenClaude) via `dz`. The npx package adds **project-level runtime governance** around the same skill: a slash command, governance rules, a context shard, and (for feature-adr) reward-learning + `/harvest`. So: pick the **skill/preset** for a working capability across platforms; pick the **npx toolkit** when you want it as a governed, command-driven fixture of one project.
955
990
 
956
- ## All Commands (62)
991
+ ## All Commands (63)
957
992
 
958
993
  ```
959
994
  dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force]
@@ -1017,6 +1052,7 @@ dz epoch-replay --emit [--project <dir>] [--limit <N>] [--seed <N>] [--margin <
1017
1052
  dz epoch-replay --judge <filled-work-order.json> [--out <file>] [--json] # blind judge prompts from the filled plans — the file carries {id, prompt} ONLY (no assignment, no arm names); refuses an order whose digest or seed-derived assignment does not check out; half-pairs skipped WITH A REASON, on stdout
1018
1053
  dz epoch-replay --score <judgments.json> --work-order <file> [--slice <name>] [--json] # un-blind against the VERIFIED pre-registered assignment; ONE paired binomial over DECISIVE pairs (ties excluded, reported) → SUPPORTED only when the lift interval (2p−1) lies entirely above zero; FALSIFIED only on harm or a passed non-superiority test (lift upper bound below the margin PRE-REGISTERED in the work order, default 0.05, at 10+ decisive pairs); else INCONCLUSIVE (min 5 decisive pairs). Refuses a forged work order, a --margin flag, or duplicate judgement ids; the verdict is data, not an exit code
1019
1054
  dz score --slug <feature> [--project <dir>] [--json] # process scorecard for ONE feature-adr run, from its artifacts: ADR confirmation, discrimination proof, cross-model QE grade, live verification, README-first, learning loop, amendments — DESCRIPTIVE-ONLY (a low score exits 0); evidence lines are shown so the reader judges the heuristics
1055
+ dz reqe [--slug <feature> [--done --report <f>]] [--project <dir>] [--json] # the re-QE debt ledger: a usage-switched feature-adr run whose Step-8 QE ran on the coder's OWN family (cross-model guard suspended, FR-2.9) records a debt; list debts (also surfaced by dz usage), print the cross-family review brief, settle FAIL-CLOSED against an existing GRADED report (the run's own 08_qe_report.md — even hard-linked — can never settle its own debt); settlement lands in 08_qe_report.md, evidence rotates to reqe-settled.json
1020
1056
  dz backlog <sub> add "<idea>" | list | show <id> | goals [--validate] | roulette [--seed n] [--commit <id>] | enrich <id> | jira <id> | harmonize [--apply] # brain-backed idea backlog: capture an idea → semantic dedup against past ideas/features via the REUSED agentdb vector engine (DUPLICATE≥0.92 / RELATED / NEW) + GoalMap alignment ("map+compass") → weighted seeded roulette picks one to work on → enrich STAGES an idea2prd hand-off → jira writes an auditable outbox via a configurable MCP adapter seam (jira-mcp|copilot-mcp|none). No 2nd vector store; without agentdb it degrades to exact-text dedup (honest)
1021
1057
  dz sign --init --out <path> | --pack <dir> --key <path> # --init: generate the Ed25519 keypair (private OUTSIDE the repo, prints the public key for keys/dz.pub); else sign a pack's manifest + CycloneDX SBOM
1022
1058
  dz sbom --pack <dir> [--out <file>] # emit the CycloneDX 1.5 SBOM for a pack standalone (file-level bill of materials); print to stdout or write to a file
@@ -2729,7 +2765,7 @@ npx @dzhechkov/p-replicator init
2729
2765
 
2730
2766
  ## Status
2731
2767
 
2732
- `v0.3.236` — published on npm. Also available as [Claude Plugin](#claude-plugin). Part of [DZ Harness Hub](https://github.com/djd1m/dz-harness-hub).
2768
+ `v0.3.259` — published on npm. Also available as [Claude Plugin](#claude-plugin). Part of [DZ Harness Hub](https://github.com/djd1m/dz-harness-hub).
2733
2769
 
2734
2770
  ## Claude Plugin
2735
2771
 
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAmXH,2EAA2E;AAC3E,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAC;CAC5C;AAED,yFAAyF;AACzF,MAAM,MAAM,iBAAiB,GAAG,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,KACvD;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAonO9E,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA4J5E"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA0XH,2EAA2E;AAC3E,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAC;CAC5C;AAED,yFAAyF;AACzF,MAAM,MAAM,iBAAiB,GAAG,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,KACvD;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AA+0O9E,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA8J5E"}
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @packageDocumentation
5
5
  */
6
- import { chmodSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
6
+ 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';
7
7
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { execFileSync, execSync, spawn } from 'node:child_process';
@@ -15,7 +15,7 @@ assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotio
15
15
  // Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
16
16
  replayableInstances, buildWorkOrder, buildJudgePrompts, unblindJudgments, verifyWorkOrder, isValidMargin, DIGEST_HONEST_SCOPE, scoreEpochReplay, generateMockOutcomes, renderEpochReplayResult, renderWorkOrderSummary, renderJudgePromptsSummary, WORK_ORDER_KIND, DEFAULT_MOCK_N, DEFAULT_MOCK_SEED, scoreRun, renderScorecard, renderCompoundingReport, readReinforcementState, readQuarantineState, registrationExitCode, renderRegistrationReport,
17
17
  // Smart Backlog (feature smart-backlog) — goal-directed idea pipeline over the Brain vector engine.
18
- readBacklogConfig, readIdeas, writeIdeas, ideaId, dedupIdea, readGoalMap, readGoalMapDetailed, parseEffort, ensureBacklogGitignored, isSafeId, alignIdea, mirrorIdeaVector, snapshotIdeas, spinRoulette, rankRoulette, seededRng, eligibleIdeas, stageEnrichment, buildJiraDraft, resolveJiraAdapter, makeBacklogIO, harmonizeBacklog, BACKLOG_BACKENDS, } from '@dzhechkov/harness-core';
18
+ readBacklogConfig, readIdeas, writeIdeas, ideaId, dedupIdea, readGoalMap, readGoalMapDetailed, parseEffort, ensureBacklogGitignored, isSafeId, alignIdea, mirrorIdeaVector, snapshotIdeas, spinRoulette, rankRoulette, seededRng, eligibleIdeas, stageEnrichment, buildJiraDraft, resolveJiraAdapter, makeBacklogIO, harmonizeBacklog, BACKLOG_BACKENDS, parseReqeDebt, buildReqeBrief, settleReqeDebt, renderReqeList, REQE_SCOPE, } from '@dzhechkov/harness-core';
19
19
  import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
20
20
  import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
21
21
  const USAGE = `dz - DZ cross-platform harness CLI
@@ -48,6 +48,7 @@ Usage:
48
48
  dz epoch-replay --judge <filled-work-order.json> [--out <file>] (blind judge prompts from the filled plans)
49
49
  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)
50
50
  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)
51
+ 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)
51
52
  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)
52
53
  dz backlog list [--status <s>] [--goal <id>] [--project <dir>] [--json] (list captured ideas, filterable by status/goal)
53
54
  dz backlog show <id> [--project <dir>] [--json] (full record for one idea)
@@ -1227,11 +1228,26 @@ function cmdUsage(options, optionLists, flags, cwd, write) {
1227
1228
  };
1228
1229
  if (hasModelLimits && u.weeklyByModel !== undefined)
1229
1230
  payload.weeklyByModel = u.weeklyByModel;
1231
+ // re-QE debt surfacing (backlog 6b40e667 — QE #9: the json contract must carry the debt too,
1232
+ // a probe is exactly the consumer that needs it). The field appears ONLY when a debt exists,
1233
+ // so the zero-debt contract stays byte-identical to the pinned legacy shape. Best-effort.
1234
+ try {
1235
+ const reqeCount = scanReqeDebts(resolve(cwd, options.get('project') ?? '.')).debts.length;
1236
+ if (reqeCount > 0)
1237
+ payload.reqeDue = reqeCount;
1238
+ }
1239
+ catch { /* advisory only */ }
1230
1240
  write(JSON.stringify(payload));
1231
1241
  return 0;
1232
1242
  }
1233
1243
  if (u.sessionPct === null && u.weeklyPct === null) {
1234
1244
  write('usage: unconfigured — set memory.usage.sessionTokenLimit / weeklyTokenLimit in .dz/config.json (percentages are ESTIMATES calibrated from observed exhaustion)');
1245
+ try {
1246
+ const reqe = scanReqeDebts(resolve(cwd, options.get('project') ?? '.'));
1247
+ if (reqe.debts.length > 0)
1248
+ write('re-QE due: ' + reqe.debts.length + ' usage-switched run(s) kept same-family QE — run `dz reqe` for the cross-family pass');
1249
+ }
1250
+ catch { /* advisory only */ }
1235
1251
  return 0;
1236
1252
  }
1237
1253
  // Compact human line — a short HH:MM / weekday hint on the resets, best-effort.
@@ -1249,6 +1265,14 @@ function cmdUsage(options, optionLists, flags, cwd, write) {
1249
1265
  const binding = hasModelLimits && u.weeklyBindingModel !== undefined ? ' ' + u.weeklyBindingModel + '-bound' : '';
1250
1266
  const w = u.weeklyPct === null ? 'n/a' : '~' + u.weeklyPct + '%' + binding;
1251
1267
  write('usage: session ' + s + ' (resets ' + clock(u.sessionResetsAt) + ') · week ' + w + ' (resets ' + clock(u.weeklyResetsAt) + ') · estimated');
1268
+ // re-QE debt surfacing (backlog 6b40e667): the moment someone checks usage is the moment a
1269
+ // usage-switched self-review debt should be visible. Best-effort — never breaks the contract.
1270
+ try {
1271
+ const reqe = scanReqeDebts(resolve(cwd, options.get('project') ?? '.'));
1272
+ if (reqe.debts.length > 0)
1273
+ write('re-QE due: ' + reqe.debts.length + ' usage-switched run(s) kept same-family QE — run `dz reqe` for the cross-family pass');
1274
+ }
1275
+ catch { /* advisory only */ }
1252
1276
  return 0;
1253
1277
  }
1254
1278
  catch {
@@ -5926,6 +5950,213 @@ function nameFor(t, outcome) {
5926
5950
  * `dz score --slug <feature>` — score a feature-adr RUN's process discipline from its artifacts
5927
5951
  * (feature dz-score, Reading C). Descriptive-only: exit 0 on any score; non-zero only on usage errors.
5928
5952
  */
5953
+ /** Scan features/<slug>/.fa-state/reqe-due.json under a project root. Symlinked ANCESTOR dirs are
5954
+ * skipped too, not just the leaf (Codex QE #5 — a symlinked feature dir could smuggle an outside
5955
+ * debt); an oversized or non-regular due-file counts as MALFORMED, never a silent skip (QE #8);
5956
+ * a debt whose embedded slug differs from its directory is MALFORMED — identity is the directory,
5957
+ * the JSON only confirms it (QE #4: an embedded foreign slug must not redirect settlement). */
5958
+ function scanReqeDebts(root) {
5959
+ const out = [];
5960
+ let malformed = 0;
5961
+ const featuresDir = join(root, 'features');
5962
+ let slugs = [];
5963
+ try {
5964
+ if (lstatSync(featuresDir).isSymbolicLink())
5965
+ return { debts: [], malformed: 0 }; // r2 #4: features/ itself
5966
+ slugs = readdirSync(featuresDir);
5967
+ }
5968
+ catch {
5969
+ return { debts: [], malformed: 0 };
5970
+ }
5971
+ for (const slug of slugs.sort()) {
5972
+ const dir = join(featuresDir, slug);
5973
+ const stateDir = join(dir, '.fa-state');
5974
+ const duePath = join(stateDir, 'reqe-due.json');
5975
+ try {
5976
+ if (lstatSync(dir).isSymbolicLink() || lstatSync(stateDir).isSymbolicLink())
5977
+ continue;
5978
+ }
5979
+ catch {
5980
+ continue; // no feature dir / no state dir — nothing to scan
5981
+ }
5982
+ let st;
5983
+ try {
5984
+ st = lstatSync(duePath);
5985
+ }
5986
+ catch {
5987
+ continue; // no due-file — the common, silent case
5988
+ }
5989
+ if (!st.isFile() || st.size > 64 * 1024) {
5990
+ malformed++; // exists but is not a plain small file — named, never silently dropped
5991
+ continue;
5992
+ }
5993
+ try {
5994
+ const debt = parseReqeDebt(readFileSync(duePath, 'utf-8'));
5995
+ if (debt && debt.slug === slug)
5996
+ out.push({ debt, duePath, dir });
5997
+ else
5998
+ malformed++;
5999
+ }
6000
+ catch {
6001
+ malformed++;
6002
+ }
6003
+ }
6004
+ return { debts: out, malformed };
6005
+ }
6006
+ /**
6007
+ * `dz reqe` — the re-QE debt ledger (backlog 6b40e667): list usage-switched same-family QE debts,
6008
+ * print the cross-family review brief, settle FAIL-CLOSED against a graded report.
6009
+ */
6010
+ function cmdReqe(options, flags, cwd, write) {
6011
+ const json = flags.has('json');
6012
+ if (flags.has('help')) {
6013
+ const usage = 'dz reqe [--slug <feature> [--done --report <file>]] [--project <dir>] [--json]';
6014
+ if (json)
6015
+ write(JSON.stringify({ help: usage, exitCode: 0 }));
6016
+ else {
6017
+ write(usage + ' — the re-QE debt ledger');
6018
+ write(' (no args) list unsettled debts (runs whose Step-8 QE ran on the coder’s own family under the usage override)');
6019
+ write(' --slug <s> print the ready cross-family review brief for one debt');
6020
+ 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');
6021
+ write(' ' + REQE_SCOPE);
6022
+ }
6023
+ return 0;
6024
+ }
6025
+ for (const flag of flags) {
6026
+ if (!new Set(['json', 'help', 'done']).has(flag)) {
6027
+ 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`);
6028
+ return 1;
6029
+ }
6030
+ }
6031
+ for (const key of options.keys()) {
6032
+ if (key.startsWith('_positional_') || !new Set(['slug', 'report', 'project']).has(key)) {
6033
+ const what = key.startsWith('_positional_') ? `unexpected argument "${options.get(key)}"` : `unknown option --${key}`;
6034
+ write(json ? JSON.stringify({ error: what, exitCode: 1 }) : `dz reqe: ${what}`);
6035
+ return 1;
6036
+ }
6037
+ }
6038
+ const root = resolve(cwd, options.get('project') ?? '.');
6039
+ const slug = options.get('slug') ?? '';
6040
+ if (slug !== '' && (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(slug) || slug.includes('..'))) {
6041
+ 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)');
6042
+ return 1;
6043
+ }
6044
+ const { debts, malformed } = scanReqeDebts(root);
6045
+ if (slug === '') {
6046
+ if (flags.has('done')) {
6047
+ write(json ? JSON.stringify({ error: '--done requires --slug', exitCode: 1 }) : 'dz reqe: --done requires --slug <feature>');
6048
+ return 1;
6049
+ }
6050
+ if (json)
6051
+ write(JSON.stringify({ debts: debts.map((d) => d.debt), malformed, scope: REQE_SCOPE, exitCode: 0 }));
6052
+ else
6053
+ for (const line of renderReqeList(debts.map((d) => d.debt), malformed))
6054
+ write(line);
6055
+ return 0;
6056
+ }
6057
+ const hit = debts.find((d) => d.debt.slug === slug) ?? null;
6058
+ if (!hit) {
6059
+ // distinguish "no debt file" from "malformed debt file" — a corrupt debt must be named
6060
+ const duePath = join(root, 'features', slug, '.fa-state', 'reqe-due.json');
6061
+ const exists = existsSync(duePath);
6062
+ const msg = exists
6063
+ ? `debt file at features/${slug}/.fa-state/reqe-due.json is MALFORMED — inspect it manually (a corrupt debt is named, never auto-cleared)`
6064
+ : `no re-QE debt recorded for "${slug}"`;
6065
+ write(json ? JSON.stringify({ error: msg, exitCode: 1 }) : `dz reqe: ${msg}`);
6066
+ return 1;
6067
+ }
6068
+ if (!flags.has('done')) {
6069
+ const brief = buildReqeBrief(hit.debt, join('features', slug));
6070
+ if (json)
6071
+ write(JSON.stringify({ debt: hit.debt, brief, exitCode: 0 }));
6072
+ else {
6073
+ write(brief.header);
6074
+ for (const line of brief.instructions)
6075
+ write(' ' + line);
6076
+ if (brief.codexCmdTemplate)
6077
+ write(' codex template: ' + brief.codexCmdTemplate);
6078
+ write(' ' + REQE_SCOPE);
6079
+ }
6080
+ return 0;
6081
+ }
6082
+ // --done: FAIL-CLOSED settlement
6083
+ const reportOpt = options.get('report') ?? '';
6084
+ if (reportOpt === '') {
6085
+ write(json ? JSON.stringify({ error: '--done requires --report <file> (the cross-family re-QE report)', exitCode: 1 }) : 'dz reqe: --done requires --report <file>');
6086
+ return 1;
6087
+ }
6088
+ const reportPath = resolve(root, reportOpt);
6089
+ const qeReportPath = join(hit.dir, '08_qe_report.md');
6090
+ // QE #1: the run's OWN same-family Step-8 report must never settle its own debt — the exact
6091
+ // laundering this ledger exists to prevent. Compared by REAL path so a symlink cannot alias it.
6092
+ try {
6093
+ const sameReal = realpathSync(reportPath) === realpathSync(qeReportPath);
6094
+ // r2 #1: a HARD LINK to the same report has a different path but the same inode — compare
6095
+ // dev+ino, not just resolved paths
6096
+ const rSt = statSync(reportPath);
6097
+ const qSt = statSync(qeReportPath);
6098
+ const sameInode = rSt.dev === qSt.dev && rSt.ino === qSt.ino;
6099
+ if (sameReal || sameInode) {
6100
+ 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)');
6101
+ return 1;
6102
+ }
6103
+ }
6104
+ catch { /* one of the two does not resolve — the reads below decide */ }
6105
+ let reportText = '';
6106
+ try {
6107
+ const st = lstatSync(reportPath);
6108
+ if (!st.isFile())
6109
+ throw new Error('not a regular file'); // symlinked report refused (QE #5)
6110
+ reportText = readFileSync(reportPath, 'utf-8');
6111
+ }
6112
+ catch {
6113
+ 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)`);
6114
+ return 1;
6115
+ }
6116
+ const settlement = settleReqeDebt(hit.debt, reportText, reportOpt);
6117
+ if (!settlement.ok || settlement.epilogue === null) {
6118
+ write(json ? JSON.stringify({ error: settlement.error, exitCode: 1 }) : `dz reqe: ${settlement.error}`);
6119
+ return 1;
6120
+ }
6121
+ // QE #6 ordering: rotate the debt FIRST, append the human-readable epilogue LAST — a crash may
6122
+ // leave a settled debt without its epilogue (recoverable from reqe-settled.json), never an
6123
+ // epilogue claiming clearance while the debt is still live.
6124
+ const settledBase = join(hit.dir, '.fa-state', 'reqe-settled');
6125
+ let settledPath = settledBase + '.json';
6126
+ for (let n = 2; existsSync(settledPath); n++)
6127
+ settledPath = settledBase + '-' + n + '.json'; // never overwrite prior evidence
6128
+ try {
6129
+ const settled = { ...hit.debt, settledGrade: settlement.grade, settledReport: reportOpt };
6130
+ writeFileSync(settledPath, JSON.stringify(settled) + '\n', { flag: 'wx' }); // wx: never through a planted symlink
6131
+ try {
6132
+ unlinkSync(hit.duePath);
6133
+ }
6134
+ catch (e) {
6135
+ // r2 #5: a failed unlink must not leave a settled marker beside a live debt — roll back
6136
+ try {
6137
+ unlinkSync(settledPath);
6138
+ }
6139
+ catch { /* best-effort rollback */ }
6140
+ throw e;
6141
+ }
6142
+ }
6143
+ catch {
6144
+ 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)');
6145
+ return 1;
6146
+ }
6147
+ try {
6148
+ if (lstatSync(qeReportPath).isSymbolicLink())
6149
+ throw new Error('symlinked 08_qe_report.md');
6150
+ appendFileSync(qeReportPath, settlement.epilogue);
6151
+ }
6152
+ catch {
6153
+ 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');
6154
+ return 0;
6155
+ }
6156
+ const okMsg = `debt settled: re-QE grade ${settlement.grade} (report ${reportOpt}) — settlement appended to features/${slug}/08_qe_report.md`;
6157
+ write(json ? JSON.stringify({ settled: true, grade: settlement.grade, exitCode: 0 }) : `dz reqe: ${okMsg}`);
6158
+ return 0;
6159
+ }
5929
6160
  function cmdScore(options, flags, cwd, write) {
5930
6161
  const json = flags.has('json');
5931
6162
  if (flags.has('help')) {
@@ -6003,10 +6234,25 @@ function cmdScore(options, flags, cwd, write) {
6003
6234
  };
6004
6235
  walk(dir, '', 0);
6005
6236
  const card = scoreRun(slug, artifacts);
6237
+ // re-QE debt note (backlog 6b40e667): a scorecard that praised cross-model QE while an unsettled
6238
+ // same-family debt sits on disk would be half the truth. PARSED, not existence-guessed (QE #9);
6239
+ // a malformed debt file is named. Advisory in both modes — never moves the score or exit code.
6240
+ let reqeNote = null;
6241
+ {
6242
+ const scan = scanReqeDebts(root);
6243
+ const mine = scan.debts.find((d) => d.debt.slug === slug);
6244
+ if (mine)
6245
+ reqeNote = 'UNSETTLED re-QE debt — Step-8 ran same-family under the usage override; run `dz reqe --slug ' + slug + '`';
6246
+ else if (existsSync(join(dir, '.fa-state', 'reqe-due.json')))
6247
+ reqeNote = 'a reqe-due.json exists but is MALFORMED/mismatched — inspect features/' + slug + '/.fa-state/ manually';
6248
+ }
6006
6249
  if (json)
6007
- write(JSON.stringify({ ...card, exitCode: 0 }, null, 2));
6008
- else
6250
+ write(JSON.stringify({ ...card, reqeDebt: reqeNote, exitCode: 0 }, null, 2));
6251
+ else {
6009
6252
  write(renderScorecard(card));
6253
+ if (reqeNote)
6254
+ write('note: ' + reqeNote);
6255
+ }
6010
6256
  return 0;
6011
6257
  }
6012
6258
  /**
@@ -7603,6 +7849,8 @@ export async function runCli(argv, io = {}) {
7603
7849
  return cmdEpochReplay(options, flags, cwd, write);
7604
7850
  case 'score':
7605
7851
  return cmdScore(options, flags, cwd, write);
7852
+ case 'reqe':
7853
+ return cmdReqe(options, flags, cwd, write);
7606
7854
  case 'backlog':
7607
7855
  return await cmdBacklog(options, flags, cwd, write);
7608
7856
  case 'routing':