@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/dist/cli.js CHANGED
@@ -3,19 +3,19 @@
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';
10
10
  import { homedir, tmpdir } from 'node:os';
11
11
  import { createRequire } from 'node:module';
12
- import { createSkill, getSkillInfo, getWorkflow, isTargetName, listSkills, runDoctor, runInit, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, TARGET_NAMES, buildParityMatrix, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_NAMES, importEcc, recordPattern, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, computeUsage, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, claimCheck, summarize, queryBookKnowledge, loadStorePatternsSync, patternRecordId, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, EVENT_CHAIN_TAIL_BYTES, EMPTY_LOG_TAIL, readTailInfo, appendChainedLines, verifyEventChainText, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters,
12
+ import { createSkill, getSkillInfo, getWorkflow, isTargetName, listSkills, runDoctor, runInit, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, TARGET_NAMES, buildParityMatrix, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_NAMES, importEcc, recordPattern, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, computeUsage, deriveCostLedger, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, claimCheck, summarize, queryBookKnowledge, loadStorePatternsSync, patternRecordId, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, EVENT_CHAIN_TAIL_BYTES, EMPTY_LOG_TAIL, readTailInfo, appendChainedLines, verifyEventChainText, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters,
13
13
  // guard-promotion (feature guard-promotion, scout idea #1)
14
14
  assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, scanSkillsLayout, parseInitFacts, verifyRegistration, buildContentProbePrompt, classifyContentProbe, renderContentProbe, findNonRegistrableSkillDirs, assembleCompoundingReport,
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)
@@ -79,6 +80,7 @@ Usage:
79
80
  dz statusline [--json] [--install] [--project <dir>] (live self-learning panel for Claude Code's status bar; reads the CC JSON payload from STDIN)
80
81
  dz statusline --fa-record --slug <s> --step "<label>" [--recalled <n>] [--stored <n>] [--mode <m>] (feature-adr: record live per-run learning state → 📐 panel segment)
81
82
  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)
83
+ 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)
82
84
  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)
83
85
  dz pretrain [--project <dir>]
84
86
  dz recommend "<task description>"
@@ -1129,6 +1131,57 @@ function cmdUsageCalibrate(options, optionLists, cwd, write) {
1129
1131
  return 0;
1130
1132
  }
1131
1133
  }
1134
+ /**
1135
+ * `dz usage --by-stage` — the per-stage cost ledger for one feature-adr run (feature `cost-ledger`).
1136
+ *
1137
+ * A run reports ONE number; this turns it into an itemized receipt keyed by the workflow's OWN
1138
+ * `stageLabel()` strings, plus the reconciliation line that guards the bookkeeping itself. Derived
1139
+ * post-hoc from the transcripts already on disk — no workflow edit, killed runs included.
1140
+ *
1141
+ * Exit code is 0 ALWAYS, matching the rest of `dz usage`; the VERDICT (`BALANCED` / `DEFECT` /
1142
+ * `INSUFFICIENT_DATA`) is the signal, and `INSUFFICIENT_DATA` is not success.
1143
+ */
1144
+ function cmdUsageByStage(options, flags, write) {
1145
+ const runId = options.get('run');
1146
+ const slug = options.get('slug');
1147
+ const epsilonRaw = options.get('epsilon');
1148
+ const epsilon = epsilonRaw === undefined ? undefined : Number(epsilonRaw);
1149
+ const report = deriveCostLedger({
1150
+ ...(runId !== undefined ? { runId } : {}),
1151
+ ...(slug !== undefined ? { slug } : {}),
1152
+ ...(epsilon !== undefined && Number.isFinite(epsilon) ? { epsilon } : {}),
1153
+ });
1154
+ if (report === null) {
1155
+ // An ABSENT run is never a balanced empty report (ADR-003).
1156
+ const detail = runId !== undefined ? `run ${runId}` : slug !== undefined ? `slug ${slug}` : 'any workflow run';
1157
+ if (flags.has('json')) {
1158
+ write(JSON.stringify({ verdict: 'INSUFFICIENT_DATA', reason: `no workflow run record found for ${detail}`, rows: [], estimated: true, scope: COST_LEDGER_SCOPE }));
1159
+ }
1160
+ else {
1161
+ write(`usage --by-stage: INSUFFICIENT_DATA — no workflow run record found for ${detail}`);
1162
+ write(`usage --by-stage: scope: ${COST_LEDGER_SCOPE}`);
1163
+ }
1164
+ return 0;
1165
+ }
1166
+ // The verifier re-derives the identities from the EMITTED report — a builder bug must surface as
1167
+ // a finding, not as a plausible table.
1168
+ const verifyDefects = verifyCostLedgerReport(report);
1169
+ const outPath = options.get('write');
1170
+ let wrote = null;
1171
+ if (outPath !== undefined && outPath.length > 0)
1172
+ wrote = writeCostLedgerJsonl(resolve(outPath), report);
1173
+ if (flags.has('json')) {
1174
+ write(JSON.stringify({ ...report, verifyDefects, ...(wrote === null ? {} : { wrote, writePath: resolve(outPath ?? '') }) }));
1175
+ return 0;
1176
+ }
1177
+ write(renderCostLedger(report));
1178
+ for (const d of verifyDefects)
1179
+ write(` verifier: ${d.kind}: ${d.detail}`);
1180
+ if (wrote !== null) {
1181
+ write(` ${wrote ? 'wrote' : 'FAILED to write'} ${resolve(outPath ?? '')} (derived report — regenerable, never read back)`);
1182
+ }
1183
+ return 0;
1184
+ }
1132
1185
  /**
1133
1186
  * `dz usage` — print an ESTIMATE of Claude session + weekly usage from fixed reset windows,
1134
1187
  * aggregated READONLY from the local transcript store (see {@link computeUsage}). `--json` emits
@@ -1154,6 +1207,8 @@ function cmdUsage(options, optionLists, flags, cwd, write) {
1154
1207
  try {
1155
1208
  if (flags.has('calibrate'))
1156
1209
  return cmdUsageCalibrate(options, optionLists, cwd, write);
1210
+ if (flags.has('by-stage'))
1211
+ return cmdUsageByStage(options, flags, write);
1157
1212
  const u = computeUsage(projectRoot);
1158
1213
  const lim = readUsageLimits(projectRoot);
1159
1214
  const modelLimits = lim.weeklyTokenLimitByModel;
@@ -1173,11 +1228,26 @@ function cmdUsage(options, optionLists, flags, cwd, write) {
1173
1228
  };
1174
1229
  if (hasModelLimits && u.weeklyByModel !== undefined)
1175
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 */ }
1176
1240
  write(JSON.stringify(payload));
1177
1241
  return 0;
1178
1242
  }
1179
1243
  if (u.sessionPct === null && u.weeklyPct === null) {
1180
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 */ }
1181
1251
  return 0;
1182
1252
  }
1183
1253
  // Compact human line — a short HH:MM / weekday hint on the resets, best-effort.
@@ -1195,6 +1265,14 @@ function cmdUsage(options, optionLists, flags, cwd, write) {
1195
1265
  const binding = hasModelLimits && u.weeklyBindingModel !== undefined ? ' ' + u.weeklyBindingModel + '-bound' : '';
1196
1266
  const w = u.weeklyPct === null ? 'n/a' : '~' + u.weeklyPct + '%' + binding;
1197
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 */ }
1198
1276
  return 0;
1199
1277
  }
1200
1278
  catch {
@@ -5872,6 +5950,213 @@ function nameFor(t, outcome) {
5872
5950
  * `dz score --slug <feature>` — score a feature-adr RUN's process discipline from its artifacts
5873
5951
  * (feature dz-score, Reading C). Descriptive-only: exit 0 on any score; non-zero only on usage errors.
5874
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
+ }
5875
6160
  function cmdScore(options, flags, cwd, write) {
5876
6161
  const json = flags.has('json');
5877
6162
  if (flags.has('help')) {
@@ -5949,10 +6234,25 @@ function cmdScore(options, flags, cwd, write) {
5949
6234
  };
5950
6235
  walk(dir, '', 0);
5951
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
+ }
5952
6249
  if (json)
5953
- write(JSON.stringify({ ...card, exitCode: 0 }, null, 2));
5954
- else
6250
+ write(JSON.stringify({ ...card, reqeDebt: reqeNote, exitCode: 0 }, null, 2));
6251
+ else {
5955
6252
  write(renderScorecard(card));
6253
+ if (reqeNote)
6254
+ write('note: ' + reqeNote);
6255
+ }
5956
6256
  return 0;
5957
6257
  }
5958
6258
  /**
@@ -7549,6 +7849,8 @@ export async function runCli(argv, io = {}) {
7549
7849
  return cmdEpochReplay(options, flags, cwd, write);
7550
7850
  case 'score':
7551
7851
  return cmdScore(options, flags, cwd, write);
7852
+ case 'reqe':
7853
+ return cmdReqe(options, flags, cwd, write);
7552
7854
  case 'backlog':
7553
7855
  return await cmdBacklog(options, flags, cwd, write);
7554
7856
  case 'routing':