@dzhechkov/harness-cli 0.8.9 → 0.8.10

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
@@ -12,22 +12,22 @@ import { isBooleanFlag } from './boolean-flags.js';
12
12
  import { resolveInstallSpec } from './install-spec.js';
13
13
  import { execFile, execFileSync, execSync, spawn, spawnSync } from 'node:child_process';
14
14
  import { createHash, randomBytes } from 'node:crypto';
15
- import { homedir, tmpdir } from 'node:os';
15
+ import { homedir, hostname, tmpdir } from 'node:os';
16
16
  import { createRequire } from 'node:module';
17
17
  import { isDeepStrictEqual } from 'node:util';
18
- import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures, formatSkillApplyFailures, resolveTargetName, formatTargetProblem, formatTargetAliasNote, TARGET_NAMES_SORTED, runDoctor, runInit, discoverSkillIds, runIntegrationsVerify, resolvePackageSkillRoots, PACKAGE_SKILL_LAYOUTS, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, discoverVerifiablePackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, runSyncAgentsPolicy, runSyncCodexHooks, resolveCodexHome, withNamedLockSync,
18
+ import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures, formatSkillApplyFailures, resolveTargetName, formatTargetProblem, formatTargetAliasNote, TARGET_NAMES_SORTED, runDoctor, runInit, discoverSkillIds, resolveSelection, formatSelectRefusal, runIntegrationsVerify, resolvePackageSkillRoots, PACKAGE_SKILL_LAYOUTS, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, discoverVerifiablePackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, runSyncAgentsPolicy, runSyncCodexHooks, resolveCodexHome, withNamedLockSync,
19
19
  // dz workflow run (feature dz-workflow-run): the pure scheduler + the dispatch adapters.
20
- TRACE_RUNID_RE, WF_RUN_OWNER_HOST, preflight, runWorkflow, makeClaudePDispatcher, makeCodexExecDispatcher, NamedLockTimeoutError, NamedLockCompromisedError, POLICY_SOURCES, detectPolicyDrift, hasPolicyFence, TARGET_NAMES, buildParityMatrix, downgradeForStaleEvidence, findStaleTranscriptEvidence, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, deriveAttestation, stampAttestation, corroborate, NOT_WITNESSED, renderTimelineHtml, importEcc, recordPattern, recordLessonForms, normalizeLessonForms, 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, CHECKPOINT_STAGES, estimateEta, extractStageSamples, formatEta, parseCheckpointLines, segmentRun, computeUsage, deriveCostLedger, planLedgerBackfill, listCostLedgerRuns, resolveLedgerRunId, AMBIGUOUS, stampCheckpointLine, LEDGER_FILL_SOURCE, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, parseWeeklyResetAnchor, claimCheck, summarize, BUNDLED_SLOP_REGISTRY_URL, DEFAULT_SLOP_CONFIG, parseSlopRegistry, slopLint, validateSlopLintConfig, queryBookKnowledge, loadStorePatternsSync, patternRecordId, patternIdentityOf, mergeLessonMatchedForms, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, bookKbPath, 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, scannableStubPath,
20
+ TRACE_RUNID_RE, WF_RUN_OWNER_HOST, preflight, runWorkflow, makeClaudePDispatcher, makeCodexExecDispatcher, NamedLockTimeoutError, NamedLockCompromisedError, POLICY_SOURCES, detectPolicyDrift, hasPolicyFence, TARGET_NAMES, buildParityMatrix, downgradeForStaleEvidence, findStaleTranscriptEvidence, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, deriveAttestation, stampAttestation, corroborate, NOT_WITNESSED, renderTimelineHtml, importEcc, recordPattern, recordLessonForms, normalizeLessonForms, 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, CHECKPOINT_STAGES, estimateEta, extractStageSamples, formatEta, parseCheckpointLines, segmentRun, computeUsage, deriveCostLedger, planLedgerBackfill, listCostLedgerRuns, resolveLedgerRunId, AMBIGUOUS, stampCheckpointLine, LEDGER_FILL_SOURCE, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, parseWeeklyResetAnchor, claimCheck, summarize, BUNDLED_SLOP_REGISTRY_URL, DEFAULT_SLOP_CONFIG, parseSlopRegistry, slopLint, validateSlopLintConfig, queryBookKnowledge, loadStorePatternsSync, patternRecordId, patternIdentityOf, mergeLessonMatchedForms, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, bookKbPath, 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, classifyChainDefects, CHAINED_JOURNALS, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
21
21
  // guard-promotion (feature guard-promotion, scout idea #1)
22
22
  assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, recordPromotionRunEvidence, isLessonRuleContentAnchor, isOffsetIsoTimestamp, 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, classifyExecutionEvidence, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, harvestStageOutcomes, recommendModels, planFeed, unfedRuns, GRADE_SUCCESS_FLOOR, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, scanSkillsLayout, declaredPluginSurface, parseInitFacts, verifyRegistration, buildContentProbePrompt, classifyContentProbe, renderContentProbe, findNonRegistrableSkillDirs, assembleCompoundingReport, buildDeadwoodReport, compactCmdUsageIfNeeded, measureCmdUsageDepthDays, recordCommandInvocation, resolveCmdUsageRoot, renderDeadwoodReport, CMD_USAGE_LOG_RELATIVE, banditStats, narrowBanditReport, renderBanditHealth,
23
23
  // Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
24
- replayableInstances, buildWorkOrder, buildJudgePrompts, unblindJudgments, verifyWorkOrder, isValidMargin, DIGEST_HONEST_SCOPE, scoreEpochReplay, generateMockOutcomes, renderEpochReplayResult, renderWorkOrderSummary, renderJudgePromptsSummary, WORK_ORDER_KIND, DEFAULT_MOCK_N, DEFAULT_MOCK_SEED, scoreRun, readQeGrade, recapWindow, decideHorizon, withinWindow, buildRecap, renderRecap, parseSourceManifest, tgPostHtmlIssues, tgVisibleLength, decideTgSend, TG_TEXT_LIMIT, countRecallEventsForRun, unknownFlagNotice, mirrorWriterExplanation, appendRecallUsage, closenessLine, anyAboveFloor, decideNameCheck, renderNameCheck, exportedNamesIn, dispatchedCommandsIn, decideSourceProvenance, renderSourceProvenance, REFUSED_HORIZONS, renderScorecard, renderCompoundingReport, readReinforcementState, readQuarantineState, registrationExitCode, renderRegistrationReport,
24
+ replayableInstances, buildWorkOrder, buildJudgePrompts, unblindJudgments, verifyWorkOrder, isValidMargin, DIGEST_HONEST_SCOPE, scoreEpochReplay, generateMockOutcomes, renderEpochReplayResult, renderWorkOrderSummary, renderJudgePromptsSummary, WORK_ORDER_KIND, DEFAULT_MOCK_N, DEFAULT_MOCK_SEED, scoreRun, readQeGrade, scoreReceiptToAggregateRow, readScoreAggregateRows, dedupeScoreAggregateRows, buildScoreAggregateReport, renderScoreAggregateReport, recapWindow, decideHorizon, withinWindow, buildRecap, renderRecap, parseSourceManifest, tgPostHtmlIssues, tgVisibleLength, decideTgSend, TG_TEXT_LIMIT, countRecallEventsForRun, unknownFlagNotice, mirrorWriterExplanation, appendRecallUsage, closenessLine, anyAboveFloor, decideNameCheck, renderNameCheck, exportedNamesIn, dispatchedCommandsIn, decideSourceProvenance, renderSourceProvenance, REFUSED_HORIZONS, renderScorecard, renderCompoundingReport, readReinforcementState, readQuarantineState, registrationExitCode, renderRegistrationReport,
25
25
  // Smart Backlog (feature smart-backlog) — goal-directed idea pipeline over the Brain vector engine.
26
26
  readBacklogConfig, readIdeas, writeIdeas, ideaId, dedupIdea, readGoalMap, readGoalMapDetailed, parseEffort, ensureBacklogGitignored, isSafeId, alignIdea, mirrorIdeaVector, ensureBacklogEmbedForm, readBacklogEmbedFormVersion, recordAbsorption, DEDUP_EMBED_FORM_VERSION, snapshotIdeas, spinRoulette, rankRoulette, seededRng, eligibleIdeas, stageEnrichment, buildJiraDraft, resolveJiraAdapter, makeBacklogIO, harmonizeBacklog, transitionIdeas, editIdea, clearEmbedStale, BACKLOG_BACKENDS, applyDomainBoost, DZ_OWNED_TASK_TYPES, applyExportHoldout, DEFAULT_HELD_OUT_DOMAINS, canonicalDomainKey, readAgentdbRowsByTaskType, heldOutAfterOptIn, renderHoldoutNote, renderSharedStoreAdvice, decideVectorExport, countDisplacedByCut, renderDomainBoostNote, renderDomainCutNote, parseReqeDebt,
27
27
  // qe-bridge (feature qe-bridge-claude, ADR-001): the pure half of the reverse QE bridge.
28
28
  KNOWN_CLAUDE, isSafeClaudeId, claudeProbeArgs, claudeReviewArgs, interpretClaudeProbe, modelFamily, buildBridgePrompt, parseBridgeOutput, buildBridgeFailureRecord, buildBridgeSignoffRecord, renderBridgeReport, isSafeSlug, hasUnsafePathChars, hasDotDotSegment, buildReqeBrief, settleReqeDebt, renderReqeList, REQE_SCOPE,
29
29
  // Mutation gate (feature ha-mutation-gate) — break each named protection, run the suite, require red.
30
- parseMutationRegistry, applyMutationToText, countFailingTests, detectSuiteCompletionReceipt, detectSuiteReceiptMismatch, classifyBaseline, classifyRunFailure, classifyMutationOutcome, mutationGateExitCode, summarizeMutationResults, renderMutationReport, TRACE_BUNDLE_LEDGER_PATH, TRACE_BUNDLE_SCHEMA, TRACE_BUNDLE_RUN_META_FILE, buildBundle, serializeBundle, parseBundle, planImport, decideCheckpointWrite, amendmentSection, planSaysNoAmendments, parseAmendments, resolveAmendments, decideAmendmentOutcome, amendmentVerdictLine, amendmentsMissingFromPlan, AMENDMENT_VACUITY_NOTE, extractContractChecklist, parseContractVerdictReport, verifyContractVerdicts, decideSignableSet, signableSetLine, decideRecordWrite, decideReadBack, recordVerdictLine, buildCadenceReport, tgVisibleSha256, CADENCE_WINDOW_DAYS, readQeRounds, QE_ROUNDS_DEFAULT_CEILING, adviseRestart, describeStoreLocation, storeLocationLine, resolveTeachTarget, teachReasonPhrase, readTeachToConfig, TeachTargetError, mergeStoreHits, sameStore, globalStoreRoot, storeCountLabel,
30
+ parseMutationRegistry, applyMutationToText, attributeBaselineRedness, countFailingTests, detectSuiteCompletionReceipt, detectSuiteReceiptMismatch, classifyBaseline, classifyRunFailure, classifyMutationOutcome, mutationGateExitCode, summarizeMutationResults, renderMutationReport, runWithOneInternalRetry, TRACE_BUNDLE_LEDGER_PATH, TRACE_BUNDLE_SCHEMA, TRACE_BUNDLE_RUN_META_FILE, buildBundle, serializeBundle, parseBundle, planImport, decideCheckpointWrite, amendmentSection, planSaysNoAmendments, parseAmendments, resolveAmendments, decideAmendmentOutcome, amendmentVerdictLine, amendmentsMissingFromPlan, AMENDMENT_VACUITY_NOTE, extractContractChecklist, parseContractVerdictReport, verifyContractVerdicts, decideSignableSet, signableSetLine, decideRecordWrite, decideReadBack, recordVerdictLine, buildCadenceReport, tgVisibleSha256, CADENCE_WINDOW_DAYS, readQeRounds, QE_ROUNDS_DEFAULT_CEILING, adviseRestart, describeStoreLocation, storeLocationLine, resolveTeachTarget, teachReasonPhrase, readTeachToConfig, TeachTargetError, mergeStoreHits, sameStore, globalStoreRoot, storeCountLabel,
31
31
  // operator-profile (ADR-001): per-user 0600 store + marked block in ~/.claude/CLAUDE.md
32
32
  renderProfileBlock, readProfile, writeProfile, syncProfileBlock, checkProfileDrift, parseRegister, registerOwnerWord, profileAgeDays, parseDomainList, domainListText, parseYesNo, REGISTERS, } from '@dzhechkov/harness-core';
33
33
  import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
@@ -47,7 +47,7 @@ export const DZ_COMMANDS = [
47
47
  'epoch-replay', 'score', 'recap', 'cadence', 'qe-rounds', 'restart-advisor', 'tg-post',
48
48
  'name-check', 'provenance-check', 'feature-adr-record', 'amendment-check', 'contract-check',
49
49
  'feature-adr-checkpoint', 'profile', 'reqe', 'qe-bridge', 'backlog', 'routing',
50
- 'bto-optimize', 'dashboard', 'roam', 'import-ecc',
50
+ 'bto-optimize', 'dashboard', 'roam', 'import-ecc', 'chain',
51
51
  ];
52
52
  const USAGE = `dz - DZ cross-platform harness CLI
53
53
 
@@ -88,6 +88,7 @@ Usage:
88
88
  dz epoch-replay --judge <filled-work-order.json> [--out <file>] (blind judge prompts from the filled plans)
89
89
  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)
90
90
  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)
91
+ dz score --all [--project <dir>] [--json] (sweep features/*/.fa-state/score-*.json into the append-only chained scorecards aggregate — descriptive-only, always exits 0)
91
92
  dz recap [--day|--week|--month] [--at <ISO date>] [--project <dir>] [--json] (what was done over a window, from records only: deliveries with the grade an independent review STATED — a report naming two grades is reported ambiguous, never guessed — registry publishes, gate verdicts, knowledge reuse. --quarter/--half-year/--year are RECOGNISED and REFUSED with the real span in days: there is one complete quarter and the longest record is 174 days. Every section carries its own data-start date, and "the source was not read" never prints as zero. Contaminated measures — commit count, lines, tokens, learning-event volume, inventory counts, lesson count — are not computed, and the report says so. exit 0 reported / 2 refused)
92
93
  dz cadence [--window day|week|month|quarter|halfyear|year] [--json] (the WHAT-SHIPPED aggregator: graded-shipment cadence by ISO week + npm-publish cadence (recap cache) + guard repeat decay on the FIXED rule set + recall reuse; a window deeper than 2× the record is REFUSED with the depth named (ADR: a cadence from one point is scale forgery); exit 0 report / 2 refused-window / 1 usage)
93
94
  dz qe-rounds (--slug <feature> | --feature-dir <abs>) [--ceiling <n>] [--project <dir>] [--json] (how many Step-8 review rounds has this feature ALREADY had? Reads what dz qe-bridge already wrote — signoff-<runId>.json and failed-*.json under features/<slug>/.fa-state/qe-bridge — and writes nothing itself, so it can answer for runs already past. A round is a runId, not a file; an attempt with no verdict is counted SEPARATELY and never merged; an unreadable record is NAMED and the count is declared a LOWER BOUND. ONE directory, never a union across checkouts. exit 0 under the ceiling / 1 at-or-over — owner decides, the command does not judge whether the rounds were warranted / 2 NOT ESTABLISHED, which is never "zero rounds")
@@ -106,7 +107,7 @@ Usage:
106
107
  dz feature-adr-checkpoint (--slug <feature> | --feature-dir <abs>) --stage <s> --input-hash <h> --result <json> [--artifact a,b] [--json] (record a pipeline stage ONLY after measuring its artifacts on disk; refuses a null result, an absent artifact, or a stage that declares none — the subagent runs a COMMAND instead of hand-writing durable state)
107
108
  dz profile [init|show|set|sync] [--json] (WHO the assistant is talking to — per-user store at ~/.dz/profile.json (0600, NEVER in a project), delivered as a marked block in ~/.claude/CLAUDE.md so it loads in EVERY project, dz installed or not. init = five questions (language, register, deep/weak domains as comma lists — "networking (CCIE; NSX)" keeps the parenthetical as the note, Enter skips — teaches y/n with one re-ask, never a silent default); show ALWAYS prints the store path + age + drift verdict + the rendered block; set register|language|teaches <v> or set deep|weak add|rm <tag> [note] — register accepts the owner's own words (профи / профи лайт / просто), an unknown value is REFUSED naming the accepted set; sync re-writes the block (runs automatically after init/set; foreign content byte-for-byte, timestamped backup before every modifying write). The register changes FORM, never FACTS, and governs dialogue only — never ADRs/commits/QE reports; both rules are baked into the rendered block at every level. exit 0 done / 1 no profile or failed / 2 refused input)
108
109
  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)
109
- dz qe-bridge --family claude --slug <feature> [--coder-family codex|claude] [--model <id>] [--files a,b] [--out <f>] [--timeout <s>] [--allow-same-family] [--json] (the REVERSE QE bridge: run an INDEPENDENT Claude reviewer over a feature's Step-8 artifacts from ANY host — a Codex session included, plain shell, no Claude agent plane needed — and land a PARSED signoff. The reviewer runs ISOLATED: an EMPTY temp cwd plus --safe-mode --strict-mcp-config --tools '' --no-session-persistence, so no CLAUDE.md/skills/plugins/hooks/MCP load, and the verdict is read from the --output-format json RESULT ENVELOPE — text a session customization printed onto the same stdout can never become a signoff. Probes the model before trusting it; sends SCOPED extracts with a loud 200k-char ceiling (never silent truncation); the grade must AGREE across three LAST-anchored channels (terminal marker line, fenced qe-bridge-signoff JSON, the report's own GRADE line) AND the marker must be the FINAL content — empty, gradeless, self-contradicting or miscounted output is one of 18 NAMED failures with an audit record under features/<slug>/.fa-state/qe-bridge/ (runId, resolved executable + binOverride, prompt sha256, channel offsets, requestedOut, reportWritten, retained raw stdout; 0600 files in a 0700 dir), never a clean review. A --coder-family that contradicts the recorded reqe debt is refused. Writes features/<slug>/08b_reqe_report.md, which dz reqe --done settles unchanged. DISCLOSURE: the extracts you scope are sent to the Claude runtime; the bridge cannot classify secrets. DZ_QE_BRIDGE_CLAUDE_BIN is a TEST SEAM, not a flag. exit 0 signoff parsed (ANY grade — it reports, it does not gate) / 1 named failure / 2 usage)
110
+ dz qe-bridge --family claude --slug <feature> [--coder-family codex|claude] [--model <id>] [--files a,b] [--out <f>] [--timeout <s>] [--allow-same-family] [--json] (the REVERSE QE bridge: run an INDEPENDENT Claude reviewer over a feature's Step-8 artifacts from ANY host — a Codex session included, plain shell, no Claude agent plane needed — and land a PARSED signoff. The reviewer runs ISOLATED: an EMPTY temp cwd plus --safe-mode --strict-mcp-config --tools '' --no-session-persistence, so no CLAUDE.md/skills/plugins/hooks/MCP load, and the verdict is read from the --output-format json RESULT ENVELOPE — text a session customization printed onto the same stdout can never become a signoff. Probes the model before trusting it; sends SCOPED extracts with a loud 200k-char ceiling (never silent truncation); the grade must AGREE across three LAST-anchored channels (terminal marker line, fenced qe-bridge-signoff JSON, the report's own GRADE line) AND the marker must be the FINAL content — empty, gradeless, self-contradicting or miscounted output is one of 17 NAMED failures with an audit record under features/<slug>/.fa-state/qe-bridge/ (runId, resolved executable + binOverride, prompt sha256, channel offsets, requestedOut, reportWritten, retained raw stdout; 0600 files in a 0700 dir), never a clean review. A --coder-family that contradicts the recorded reqe debt is refused. Writes features/<slug>/08b_reqe_report.md, which dz reqe --done settles unchanged. DISCLOSURE: the extracts you scope are sent to the Claude runtime; the bridge cannot classify secrets. DZ_QE_BRIDGE_CLAUDE_BIN is a TEST SEAM, not a flag. exit 0 signoff parsed (ANY grade — it reports, it does not gate) / 1 named failure / 2 usage)
110
111
  dz mutation-gate [--package <dir>] [--registry <file>] [--test-cmd "<cmd>"] [--only <id[,id]>] [--timeout <ms>] [--rebaseline per-entry|final] [--keep-scratch] [--json] (prove each NAMED protection has a test that DISCRIMINATES: copy the package to a scratch dir, verify the baseline suite is green, apply each registry mutation, run the suite, REQUIRE red, restore. The red must be BEHAVIOURAL: a mutation that no longer parses is MUTATION_UNPARSEABLE; a red run whose OWN output reports a test FILE failing to load (node --test file-level not-ok with exitCode, vitest Failed Suites) is MUTATION_LOAD_FATAL — the signal comes from the same run as the failing count, never from a separate isolated import; red output whose shape matches no known runner is INCONCLUSIVE (a runner-coverage gap, loud, never PROVEN); a count far above the entry's bound is OVER_FAILING; a restored tree that does not reproduce green makes the entry INCONCLUSIVE (flaky). Mutation writes are realpath-contained to the scratch copy: a symlink escape or a node_modules/ target is refused (exit 2), the real tree is never written. A mutation that does not apply, a green suite, or an inconclusive run is a FAILURE — never a skip. exit 0 all proven / 1 gate failed / 2 setup error)
111
112
  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)
112
113
  dz backlog list [--status <s>] [--goal <id>] [--project <dir>] [--json] (list captured ideas, filterable by status/goal)
@@ -145,6 +146,7 @@ Usage:
145
146
  dz statusline --fa-record --slug <s> --step "<label>" [--kind <feature-adr|loop>] [--recalled <n>] [--stored <n>] [--mode <m>] (feature-adr: record live per-run learning state → 📐 panel segment)
146
147
  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)
147
148
  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)
149
+ dz chain [--project <dir>] [--json] (verify EVERY hash-chained journal in ONE command: coverage is DERIVED from the CHAINED_JOURNALS registry, never typed, so a journal cannot be given a chain and checked by nobody. An ABSENT journal is NAMED absent, never omitted — omission and cleanliness are indistinguishable in a report. Statuses: ok | healed (defects the current unbroken run has outlived — verdicts over present records are sound) | unchained (present, no chained record yet — legal) | absent | broken | unreadable. Exit 1 on broken/unreadable: a verifier that reports damage and exits 0 is one no automation can act on)
148
150
  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)
149
151
  dz lint [paths...] [--json] [--config <file>] [--registry <file>] [--project <dir>] (advisory EN/RU prose-style lint; findings exit 0, incomplete input/policy exits 1, usage exits 2)
150
152
  dz pretrain [--project <dir>]
@@ -266,6 +268,35 @@ function discoverSkillsDirs(cwd, explicitSkillsDir) {
266
268
  async function installSkills(opts) {
267
269
  const { target, projectRoot, cwd, explicitSkillsDir, select, force, enrich } = opts;
268
270
  const skillsDirs = discoverSkillsDirs(cwd, explicitSkillsDir);
271
+ // PREFLIGHT (backlog 9d15b9b6, PR-A) — resolve the REQUEST once, before anything is written.
272
+ //
273
+ // Two defects lived in asking each root independently instead of resolving the request: a skill
274
+ // present in two roots was installed TWICE and counted twice (the field report's `2 skill(s)` was
275
+ // one skill installed twice), and a skill present in NO root produced a warning and exit 0 —
276
+ // `0 skill(s)` reading as success. Both are gone once the decision happens here.
277
+ //
278
+ // Placement is load-bearing: an exit 1 that arrives after hooks and memory are written leaves a
279
+ // half-configured project, which is worse than either clean outcome. This runs before the loop
280
+ // below and before every target adapter.
281
+ //
282
+ // Dependency closure is deliberately NOT resolved here — that is PR-B. This preflight fixes the
283
+ // count and the exit contract, and gives that work a base it can trust.
284
+ if (select !== undefined) {
285
+ const roots = skillsDirs.map((dir) => ({ dir, ids: discoverSkillIds(dir) }));
286
+ const resolution = resolveSelection(select, roots);
287
+ for (const shadow of resolution.shadowed) {
288
+ opts.writeErr?.(`dz: skill '${shadow.id}' is offered by ${shadow.alsoIn.length + 1} roots; ` +
289
+ `installing from ${shadow.chosen} (earlier root wins). Also present in: ${shadow.alsoIn.join(', ')}`);
290
+ }
291
+ const refusal = formatSelectRefusal(resolution, roots);
292
+ if (refusal !== null) {
293
+ return {
294
+ selectRefusal: refusal,
295
+ results: [], dirsSearched: skillsDirs.length, written: 0, skipped: 0,
296
+ missing: [...resolution.missing], failures: [], applyFailures: [], integrations: [],
297
+ };
298
+ }
299
+ }
269
300
  // agents-md and gemini are FLATTENING single-file targets: each must aggregate
270
301
  // every selected skill from ALL discovered dirs into ONE root file (AGENTS.md /
271
302
  // GEMINI.md) in a single merge. A per-dir runInit loop (like the tree targets
@@ -398,6 +429,7 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
398
429
  cwd,
399
430
  explicitSkillsDir,
400
431
  select,
432
+ writeErr,
401
433
  force: flags.has('force'),
402
434
  enrich: flags.has('enrich'),
403
435
  noHooks: flags.has('no-hooks'),
@@ -405,6 +437,12 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
405
437
  noVerify: flags.has('no-verify'),
406
438
  ...(options.get('allow-integrations') !== undefined ? { allowIntegrations: options.get('allow-integrations') } : {}),
407
439
  });
440
+ // PR-A: an explicit --select that named a skill no root provides is a REFUSAL, not a warning.
441
+ // Printed and returned here, before any target adapter runs — nothing has been written yet.
442
+ if (r.selectRefusal !== undefined) {
443
+ writeErr(r.selectRefusal);
444
+ return 1;
445
+ }
408
446
  // Codex keeps its established user-registry writer, but its result is normalized into the same
409
447
  // two-outcome contract before JSON/human rendering. A write without a live ready observation is
410
448
  // a refusal with applied=true, never a second success channel.
@@ -4860,6 +4898,28 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4860
4898
  writeErr(formatTargetAliasNote('dz setup', targetOpt, target));
4861
4899
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
4862
4900
  const presetName = options.get('preset');
4901
+ // PREFLIGHT BEFORE THE FIRST WRITE (backlog 9d15b9b6, PR-A). Step 3 configures the learning
4902
+ // environment and step 4 installs skills, so refusing at step 4 would leave a project that has
4903
+ // memory and hooks but not the skills the operator asked for — a half-configured state worse than
4904
+ // either clean outcome. The request is therefore resolved HERE, before the banner's first step.
4905
+ //
4906
+ // Only an EXPLICIT --select is refused. A preset names skills the package itself ships, so a gap
4907
+ // there is our packaging defect, not the operator's typo, and it is reported by the existing
4908
+ // missing-list rather than by refusing the whole run.
4909
+ const setupSelectRaw = options.get('select');
4910
+ if (setupSelectRaw !== undefined) {
4911
+ const requested = setupSelectRaw.split(',').map((x) => x.trim()).filter((x) => x.length > 0);
4912
+ const roots = discoverSkillsDirs(cwd, options.get('skills-dir')).map((dir) => ({ dir, ids: discoverSkillIds(dir) }));
4913
+ const resolution = resolveSelection(requested, roots);
4914
+ for (const shadow of resolution.shadowed) {
4915
+ writeErr(`dz: skill '${shadow.id}' is offered by ${shadow.alsoIn.length + 1} roots; installing from ${shadow.chosen} (earlier root wins). Also present in: ${shadow.alsoIn.join(', ')}`);
4916
+ }
4917
+ const refusal = formatSelectRefusal(resolution, roots);
4918
+ if (refusal !== null) {
4919
+ writeErr(refusal);
4920
+ return 1;
4921
+ }
4922
+ }
4863
4923
  write(`\n╔══════════════════════════════════════════════════════╗`);
4864
4924
  write(`║ DZ SETUP — Full Environment ║`);
4865
4925
  write(`╠══════════════════════════════════════════════════════╣`);
@@ -5001,7 +5061,7 @@ function cmdPretrain(options, cwd, write) {
5001
5061
  write(`╚══════════════════════════════════════════════════════════════╝`);
5002
5062
  return 0;
5003
5063
  }
5004
- function cmdRecommend(options, cwd, write) {
5064
+ function cmdRecommend(options, flags, cwd, write) {
5005
5065
  const task = options.get('_positional_0');
5006
5066
  if (!task) {
5007
5067
  write('dz recommend: task description required');
@@ -5010,15 +5070,40 @@ function cmdRecommend(options, cwd, write) {
5010
5070
  }
5011
5071
  const registry = buildRegistry(cwd);
5012
5072
  const report = recommend(task, registry, cwd);
5073
+ if (flags.has('json')) {
5074
+ write(JSON.stringify(report, null, 2));
5075
+ return 0;
5076
+ }
5013
5077
  write(`\n╔══════════════════════════════════════════════════════════════╗`);
5014
5078
  write(`║ DZ RECOMMEND — Task Advisor ║`);
5015
5079
  write(`╠══════════════════════════════════════════════════════════════╣`);
5016
5080
  write(`║ Task: ${report.task.slice(0, 52).padEnd(52)}║`);
5017
- const topicSuffix = report.pretrainFallback ? ' (via pretrain)' : '';
5018
- write(`║ Topics: ${(report.topics.join(', ') + topicSuffix).slice(0, 50).padEnd(50)}║`);
5081
+ if (report.topicSource === 'task') {
5082
+ write(`║ Topics: ${report.topics.join(', ').slice(0, 50).padEnd(50)}║`);
5083
+ }
5084
+ else if (report.topicSource === 'project-stack') {
5085
+ write(`║ Topics: ${'not matched in the question'.padEnd(50)}║`);
5086
+ }
5087
+ else {
5088
+ write(`║ Topics: ${'not recognized — no recommendations'.padEnd(50)}║`);
5089
+ }
5019
5090
  write(`╠══════════════════════════════════════════════════════════════╣`);
5091
+ if (report.topicSource === 'project-stack') {
5092
+ write(`⚠ Тема запроса не распознана — подбор ниже сделан по СТЕКУ ПРОЕКТА, не по вашему вопросу.`);
5093
+ write(` (topic not recognized — recommendations reflect the project stack, not the question)`);
5094
+ write(`PROJECT-STACK SUGGESTIONS`);
5095
+ }
5096
+ else if (report.topicSource === 'none') {
5097
+ write(`Тема запроса не распознана; рекомендаций нет.`);
5098
+ write(`Переформулируйте задачу или используйте dz registry search <слово> / /skill-advisor.`);
5099
+ write(`╚══════════════════════════════════════════════════════════════╝`);
5100
+ return 0;
5101
+ }
5102
+ const stackDerived = report.topicSource === 'project-stack';
5020
5103
  if (report.presets.length > 0) {
5021
- write(`║ RECOMMENDED PRESETS ║`);
5104
+ write(stackDerived
5105
+ ? `║ PROJECT-STACK PRESETS ║`
5106
+ : `║ RECOMMENDED PRESETS ║`);
5022
5107
  for (const p of report.presets) {
5023
5108
  const matched = p.matchedSkills.length > 0 ? ` (${p.matchedSkills.slice(0, 3).join(', ')})` : '';
5024
5109
  write(`║ ${p.name.padEnd(15)} ${String(p.skills).padStart(2)} skills coverage: ${String(p.coverage).padStart(2)} topics${matched.padEnd(15)}║`);
@@ -5026,7 +5111,9 @@ function cmdRecommend(options, cwd, write) {
5026
5111
  write(`║${''.padEnd(62)}║`);
5027
5112
  }
5028
5113
  if (report.skills.length > 0) {
5029
- write(`║ RECOMMENDED SKILLS (top ${Math.min(report.skills.length, 8)})${' '.repeat(35)}║`);
5114
+ write(stackDerived
5115
+ ? `║ PROJECT-STACK SKILLS (top ${Math.min(report.skills.length, 8)})${' '.repeat(35)}║`
5116
+ : `║ RECOMMENDED SKILLS (top ${Math.min(report.skills.length, 8)})${' '.repeat(35)}║`);
5030
5117
  for (const s of report.skills.slice(0, 8)) {
5031
5118
  const desc = s.description.length > 35 ? s.description.slice(0, 32) + '...' : s.description;
5032
5119
  write(`║ ${s.id.padEnd(24)} ${desc.padEnd(36)}║`);
@@ -5034,7 +5121,9 @@ function cmdRecommend(options, cwd, write) {
5034
5121
  write(`║${''.padEnd(62)}║`);
5035
5122
  }
5036
5123
  if (report.toolkits.length > 0) {
5037
- write(`║ FULL PIPELINE (npx toolkits) ║`);
5124
+ write(stackDerived
5125
+ ? `║ PROJECT-STACK PIPELINE (npx toolkits) ║`
5126
+ : `║ FULL PIPELINE (npx toolkits) ║`);
5038
5127
  for (const tk of report.toolkits) {
5039
5128
  const desc = tk.description.length > 44 ? tk.description.slice(0, 41) + '...' : tk.description;
5040
5129
  write(`║ ${tk.name.padEnd(16)} ${desc.padEnd(44)}║`);
@@ -5044,13 +5133,17 @@ function cmdRecommend(options, cwd, write) {
5044
5133
  write(`║${''.padEnd(62)}║`);
5045
5134
  }
5046
5135
  write(`╠══════════════════════════════════════════════════════════════╣`);
5047
- write(`║ STEP-BY-STEP PLAN ║`);
5136
+ write(stackDerived
5137
+ ? `║ PROJECT-STACK PLAN ║`
5138
+ : `║ STEP-BY-STEP PLAN ║`);
5048
5139
  for (const step of report.plan) {
5049
5140
  const line = step.length > 60 ? step.slice(0, 57) + '...' : step;
5050
5141
  write(`║ ${line.padEnd(58)}║`);
5051
5142
  }
5052
5143
  write(`╠══════════════════════════════════════════════════════════════╣`);
5053
- write(`║ QUICK INSTALL ║`);
5144
+ write(stackDerived
5145
+ ? `║ PROJECT-STACK QUICK INSTALL ║`
5146
+ : `║ QUICK INSTALL ║`);
5054
5147
  const cmd = report.installCommand.length > 58 ? report.installCommand.slice(0, 55) + '...' : report.installCommand;
5055
5148
  write(`║ ${cmd.padEnd(58)}║`);
5056
5149
  write(`╚══════════════════════════════════════════════════════════════╝`);
@@ -6811,6 +6904,81 @@ function looksBinaryText(text) {
6811
6904
  * per-file findings (each enriched with its `file`), and applies the exit-code contract.
6812
6905
  * `--json` ALWAYS emits valid JSON `{ok, findings, scanned}`, even on the failure path.
6813
6906
  */
6907
+ /**
6908
+ * `dz chain` — verify EVERY hash-chained journal in one command (W0-chain, backlog bc4ee35c).
6909
+ *
6910
+ * The machinery to verify a chain has worked for weeks. What was missing is the ABILITY TO ASK:
6911
+ * verification lived inside two consumers, each carrying its own hardcoded list of which files are
6912
+ * chained, so a journal could be given a chain and still be checked by nobody. Coverage here is
6913
+ * DERIVED from CHAINED_JOURNALS, never typed — adding a journal to the registry adds it to this
6914
+ * report by construction.
6915
+ *
6916
+ * An ABSENT journal is reported as `absent`, not omitted. Omission and cleanliness are
6917
+ * indistinguishable in a report, and that indistinguishability is how the original blind spot
6918
+ * survived; the same reason `broken` exits NON-ZERO rather than merely printing — a verifier that
6919
+ * reports damage and exits 0 is one no automation can act on, and this verb exists to run unattended.
6920
+ *
6921
+ * A journal that exists but carries NO chained records is `unchained`, which is legal (a log may
6922
+ * predate the chain) and therefore does not fail the command. Calling it a defect would train the
6923
+ * reader to ignore the output — the failure mode already measured once on the doctor's own line.
6924
+ */
6925
+ function cmdChain(options, flags, cwd, write) {
6926
+ const root = options.get('project') ?? cwd;
6927
+ const journals = CHAINED_JOURNALS.map((journal) => {
6928
+ const path = join(root, journal.rel);
6929
+ if (!existsSync(path)) {
6930
+ return { rel: journal.rel, decides: journal.decides, status: 'absent', chained: 0, defects: 0, detail: 'file not present' };
6931
+ }
6932
+ let text = '';
6933
+ try {
6934
+ text = readFileSync(path, 'utf-8');
6935
+ }
6936
+ catch {
6937
+ // Unreadable is NOT clean. It is the one outcome that must never be quietly folded into
6938
+ // "nothing to report": we did not look, so we know nothing.
6939
+ return { rel: journal.rel, decides: journal.decides, status: 'unreadable', chained: 0, defects: 0, detail: 'file could not be read' };
6940
+ }
6941
+ const v = verifyEventChainText(text);
6942
+ if (v.chained === 0) {
6943
+ return { rel: journal.rel, decides: journal.decides, status: 'unchained', chained: 0, defects: 0, detail: 'present, but no record carries a chain (legal — the log predates chaining)' };
6944
+ }
6945
+ const total = text.split('\n').filter((l) => l.trim() !== '').length;
6946
+ const age = classifyChainDefects(v, total);
6947
+ if (v.ok) {
6948
+ return { rel: journal.rel, decides: journal.decides, status: 'ok', chained: v.chained, defects: 0, detail: `${v.chained} chained record(s), ${v.resets} recorded restart(s)` };
6949
+ }
6950
+ // A break the current unbroken run has already outlived does not make TODAY's records unsound.
6951
+ // Reporting both alike is what made the doctor's equivalent line permanently red for four weeks.
6952
+ const historical = age.inRun.length === 0 && age.runRecords > 0;
6953
+ return {
6954
+ rel: journal.rel,
6955
+ decides: journal.decides,
6956
+ status: historical ? 'healed' : 'broken',
6957
+ chained: v.chained,
6958
+ defects: v.defects.length,
6959
+ detail: historical
6960
+ ? `${v.defects.length} defect(s), all BEFORE the current run — the last ${age.runRecords} record(s) are unbroken, so verdicts over those are sound`
6961
+ : `${v.defects.length} defect(s) with NO sound records after them: verdicts computed from this log are unsafe`,
6962
+ };
6963
+ });
6964
+ const failed = journals.filter((j) => j.status === 'broken' || j.status === 'unreadable');
6965
+ const ok = failed.length === 0;
6966
+ if (flags.has('json')) {
6967
+ write(JSON.stringify({ ok, root, journals }, null, 2));
6968
+ return ok ? 0 : 1;
6969
+ }
6970
+ write(`dz chain — ${journals.length} registered journal(s) under ${root}`);
6971
+ write('');
6972
+ const MARK = { ok: '\u2713', healed: '\u2713', unchained: '\u00b7', absent: '\u00b7', broken: '\u2717', unreadable: '\u2717' };
6973
+ for (const j of journals) {
6974
+ write(` ${MARK[j.status] ?? '?'} ${j.rel} — ${j.status}`);
6975
+ write(` ${j.detail}`);
6976
+ write(` decides: ${j.decides}`);
6977
+ }
6978
+ write('');
6979
+ write(ok ? ' all registered journals are sound for present verdicts' : ` ${failed.length} journal(s) UNSAFE — see above`);
6980
+ return ok ? 0 : 1;
6981
+ }
6814
6982
  function cmdClaimCheck(options, _optionLists, flags, cwd, write) {
6815
6983
  // Reject (never silently coerce) an invalid --fail-on.
6816
6984
  const failOnRaw = options.get('fail-on') ?? 'high';
@@ -9611,8 +9779,79 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
9611
9779
  // R11: a hung runner is a loud non-answer, never a pass. Same default + parse shape as mutation-gate.
9612
9780
  const timeoutOpt = Number(options.get('timeout') ?? '300000');
9613
9781
  const timeoutMs = Number.isFinite(timeoutOpt) && timeoutOpt > 0 ? timeoutOpt : 300000;
9614
- const plan = planDiscriminationCheck(runnerOpt !== undefined ? { baseRef, propertyTests, runner: runnerOpt } : { baseRef, propertyTests });
9782
+ // Runner honesty (feature instrument-honesty, ADR-001): the runner is selected from the TARGET
9783
+ // package's own scripts.test, never from a global default. The package dir is the nearest
9784
+ // ancestor of the FIRST named test that carries a package.json — walked here, at the seam,
9785
+ // because the pure half deliberately takes the script text as data and never touches the fs.
9786
+ let packageTestScript = null;
9787
+ let packageDevDependencies = [];
9788
+ let packageDir = repoRoot;
9789
+ {
9790
+ const firstTest = propertyTests[0]?.file;
9791
+ // QE-1 (instrument-honesty, HIGH): this walk runs on the RAW --test argument, BEFORE the
9792
+ // engine's sanitation — a `../` traversal made it read an arbitrary package.json OUTSIDE the
9793
+ // repo and echo its scripts.test verbatim into the JSON output (MEASURED with a planted
9794
+ // marker file). Containment first: a start point outside the repo root never gets walked,
9795
+ // the script stays null, and the engine's own path sanitation then refuses the test path.
9796
+ const walkStart = firstTest !== undefined ? resolve(cwd, dirname(firstTest)) : undefined;
9797
+ if (firstTest !== undefined && walkStart !== undefined
9798
+ && (walkStart === resolve(repoRoot) || walkStart.startsWith(resolve(repoRoot) + sep))) {
9799
+ let probe = walkStart;
9800
+ // walk up to the repo root looking for package.json (bounded by the fs root either way)
9801
+ for (;;) {
9802
+ if (existsSync(join(probe, 'package.json'))) {
9803
+ packageDir = probe;
9804
+ break;
9805
+ }
9806
+ const parent = dirname(probe);
9807
+ if (parent === probe || probe === repoRoot)
9808
+ break;
9809
+ probe = parent;
9810
+ }
9811
+ try {
9812
+ const pkg = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf-8'));
9813
+ packageTestScript = typeof pkg.scripts?.test === 'string' ? pkg.scripts.test : null;
9814
+ packageDevDependencies = Object.keys(pkg.devDependencies ?? {});
9815
+ }
9816
+ catch { /* unreadable package.json → selection falls through to the honest REFUSE */ }
9817
+ }
9818
+ }
9819
+ // The pure half's path sanitation expects a REPO-RELATIVE package dir ('.'-rooted), not an
9820
+ // absolute one — an absolute path is refused as unsafe-package-dir by design.
9821
+ const packageDirRel = relative(repoRoot, packageDir) || '.';
9822
+ const planInput = runnerOpt !== undefined
9823
+ ? { baseRef, propertyTests, runner: runnerOpt, packageTestScript, packageDevDependencies, packageDir: packageDirRel }
9824
+ : { baseRef, propertyTests, packageTestScript, packageDevDependencies, packageDir: packageDirRel };
9825
+ const plan = planDiscriminationCheck(planInput);
9615
9826
  if (!plan.runnable) {
9827
+ // QE-2 (instrument-honesty, MEDIUM): a runner REFUSE used to be reported through the generic
9828
+ // "no property test to check"/map-a-test framing — the operator-facing surface re-created the
9829
+ // exact "instrument gap misread as test gap" class ADR-001 names as the reason three duplicate
9830
+ // backlog entries existed. The plan's own named reason is the verdict; the generic classify
9831
+ // stays only for the genuinely-empty-target case.
9832
+ const runnerRefusal = typeof plan.reason === 'string' && plan.reason.startsWith('unsupported-runner');
9833
+ if (runnerRefusal) {
9834
+ const refusal = {
9835
+ aggregate: 'CANNOT_ISOLATE',
9836
+ measurementValid: false,
9837
+ primaryAction: plan.primaryAction ?? 'fix-runner-invocation',
9838
+ finding: {
9839
+ severity: 'high',
9840
+ verdict: 'CANNOT_ISOLATE',
9841
+ files: plan.targets.map((t) => t.file),
9842
+ detail: `runner refused: ${plan.reason} — the INSTRUMENT could not run, nothing was measured; `
9843
+ + `declare scripts.test in the target package (or pass --runner) and re-run. `
9844
+ + `This is NOT a statement about the tests.`,
9845
+ },
9846
+ };
9847
+ if (flags.has('json')) {
9848
+ write(JSON.stringify({ plan, results: [], perTest: [], ...refusal }, null, 2));
9849
+ return 0;
9850
+ }
9851
+ write(`discrimination-check: REFUSED (${plan.reason})`);
9852
+ write(` → ${refusal.finding.detail}`);
9853
+ return 0;
9854
+ }
9616
9855
  // No safe target to run → this is the existing "property untested" finding (empty propertyTests classify).
9617
9856
  const result = classifyDiscrimination({ propertyTests: [], results: [] });
9618
9857
  if (flags.has('json')) {
@@ -9717,10 +9956,24 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
9717
9956
  }
9718
9957
  // t.file + t.name already passed the engine's strict sanitation (no quotes/metacharacters/leading-dash);
9719
9958
  // still quote + `--` so a path can never be read as a runner option or split a word.
9959
+ // Runner honesty (ADR-001): the run executes FROM the target package dir with a
9960
+ // package-relative path — a root-cwd `npx vitest run packages/...` loads the ROOT config
9961
+ // (none) and reds unclassifiably, which is exactly the CANNOT_ISOLATE artifact this
9962
+ // feature removes. The plan's own commands encode the same cd; this body mirrors it.
9963
+ const pkgRel = plan.packageDir === '.' ? '' : plan.packageDir;
9964
+ const fileInPkg = pkgRel !== '' && t.file.startsWith(pkgRel + '/') ? t.file.slice(pkgRel.length + 1) : t.file;
9965
+ const execDirBase = pkgRel === '' ? worktree : join(worktree, pkgRel);
9966
+ const execDirTip = pkgRel === '' ? repoRoot : join(repoRoot, pkgRel);
9720
9967
  const nameArg = t.name ? ` -t '${t.name}'` : '';
9721
- const cmd = `${runner}${nameArg} -- '${t.file}'`;
9722
- const base = runCapturedTest(cmd, worktree, timeoutMs);
9723
- const evidence = classifyExecutionEvidence(base.output, base.exitCode, t.file);
9968
+ // NO `--` before the path: MEASURED 2026-09-02 — `npx vitest run -- 'file'` IGNORES the
9969
+ // filter and runs the whole suite (5269 tests), which is the exact whole-repo artifact
9970
+ // this feature removes (QE ha-intake-archive F5). The path is engine-sanitized (no
9971
+ // leading dash, no metacharacters), so it can never be read as an option.
9972
+ const cmd = `${runner}${nameArg} '${fileInPkg}'`;
9973
+ const base = runCapturedTest(cmd, execDirBase, timeoutMs);
9974
+ // The classifier's targetSeen is a substring probe: the run now prints PACKAGE-relative
9975
+ // paths, so it must be probed with the same form, or every hit reads as target-unseen.
9976
+ const evidence = classifyExecutionEvidence(base.output, base.exitCode, fileInPkg);
9724
9977
  const outcome = discriminationOutcomeOf(base.exitCode, evidence);
9725
9978
  const row = t.name !== undefined
9726
9979
  ? { file: t.file, name: t.name, outcome, evidence }
@@ -9731,8 +9984,8 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
9731
9984
  // base rows per the matrix; running it is cheap and only ever on an already-broken path.
9732
9985
  // Do NOT "simplify" this to evidenced-error-only — that silently breaks Confirmation 17.
9733
9986
  if (base.exitCode !== null && base.exitCode !== 0 && evidence.failureKind !== 'assertions') {
9734
- const tip = runCapturedTest(cmd, repoRoot, timeoutMs);
9735
- const tipEvidence = classifyExecutionEvidence(tip.output, tip.exitCode, t.file);
9987
+ const tip = runCapturedTest(cmd, execDirTip, timeoutMs);
9988
+ const tipEvidence = classifyExecutionEvidence(tip.output, tip.exitCode, fileInPkg);
9736
9989
  row['tipOutcome'] = discriminationOutcomeOf(tip.exitCode, tipEvidence);
9737
9990
  row['tipEvidence'] = tipEvidence;
9738
9991
  // R15, named honestly: the base run is isolated in a worktree, but the tip runs in the LIVE
@@ -9841,51 +10094,6 @@ function readTipTreeConditions(repoRoot) {
9841
10094
  function nameFor(t, outcome) {
9842
10095
  return t.name !== undefined ? { file: t.file, name: t.name, outcome } : { file: t.file, outcome };
9843
10096
  }
9844
- /**
9845
- * `dz mutation-gate` — the mutation gate (feature ha-mutation-gate, SPEC at
9846
- * features/ha-mutation-gate/SPEC.md). A green test proves the code works; it does NOT prove the
9847
- * test would notice the protection being DELETED. For each entry in a declarative registry this
9848
- * gate applies the entry's exact {find, replace} mutation to a SCRATCH COPY of the package, runs
9849
- * the package's test command there, and REQUIRES a non-zero exit (red). All logic is in
9850
- * harness-core's pure engine (mutation-gate.ts); this is the I/O executor.
9851
- *
9852
- * --package <dir> the package to gate (default: cwd; must contain package.json)
9853
- * --registry <file> registry path (default: <pkg>/test/mutation-registry.json, then
9854
- * <pkg>/mutation-registry.json)
9855
- * --test-cmd '<cmd>' suite command run in the scratch copy (default: registry.testCommand,
9856
- * then `npm test`)
9857
- * --only <id[,id]> run a subset of entries (unknown id = usage error, never a silent skip)
9858
- * --timeout <ms> per-suite-run timeout (default 300000). A timed-out run is INCONCLUSIVE —
9859
- * a FAILURE, never a pass.
9860
- * --rebaseline <m> route-b guard mode: 'per-entry' (default — every red entry re-runs the
9861
- * suite on the restored tree; not green ⇒ that entry is INCONCLUSIVE) or
9862
- * 'final' (one re-run at the end; not green ⇒ every red-based verdict is
9863
- * downgraded). The gate's PROVEN now means the redness was ATTRIBUTABLE.
9864
- * --keep-scratch keep the scratch copy for inspection (default: removed in a finally)
9865
- * --json machine contract {packageDir, registryPath, testCommand, rebaselineMode,
9866
- * baseline, results, summary, warnings, exitCode}
9867
- *
9868
- * The four rules (SPEC §"Four rules") and where each is enforced:
9869
- * 1. does-not-apply = FAILURE → core classifyMutationOutcome (occurrences !== 1 ⇒ NOT_APPLIED);
9870
- * 2. green suite = FAILURE → core (exit 0 ⇒ UNDEFENDED, names the property);
9871
- * 3. never mutate the working tree → HERE: every write targets the scratch copy under tmpdir();
9872
- * the repo tree is opened read-only, and a crashed run leaves at worst a stale tmp dir;
9873
- * 4. the gate's own discrimination proof → harness-cli/test/fixtures/mutation-gate-undefended
9874
- * (the gate MUST fail on it; asserted by test/mutation-gate-cli.test.ts).
9875
- *
9876
- * Exit codes: 0 every entry PROVEN · 1 the gate ran and failed (undefended / not-applied /
9877
- * below-min / unparseable / load-fatal / over-failing / inconclusive entry) · 2 usage or setup
9878
- * error (missing registry, red BASELINE — a red unmutated copy proves nothing and must not be
9879
- * read as a mutation result — or an entry whose file RESOLVES outside the scratch copy: a
9880
- * symlink escape is refused before anything is written, SPEC rule 3).
9881
- */
9882
- /**
9883
- * Route-a guard for `dz mutation-gate`: parse-check a MUTATED file as its own language BEFORE the
9884
- * suite runs. A registry mutation must delete the protection while keeping the file loadable — a
9885
- * file that no longer parses kills the whole suite (or its import chain), and that STRUCTURAL
9886
- * redness says nothing about the named protection. Returns `{error}` when a parser ran and the
9887
- * text does not parse; `{skipped}` (reported loudly, never silently) when no parser is available.
9888
- */
9889
10097
  function parseCheckMutatedFile(absFile, text) {
9890
10098
  const ext = extname(absFile).toLowerCase();
9891
10099
  try {
@@ -9916,18 +10124,36 @@ function parseCheckMutatedFile(absFile, text) {
9916
10124
  }
9917
10125
  }
9918
10126
  if (ext === '.js' || ext === '.cjs' || ext === '.mjs' || ext === '') {
9919
- try {
9920
- // `node --check` on the file IN PLACE, so the nearest package.json decides the module goal.
9921
- execFileSync(process.execPath, ['--check', absFile], { stdio: 'pipe' });
9922
- return {};
9923
- }
9924
- catch (e) {
9925
- const err = e;
9926
- const stderrLines = String(err.stderr ?? '').split('\n').map((l) => l.trim()).filter((l) => l !== '');
9927
- // prefer the actual `SyntaxError: …` line over node's trailing version footer.
9928
- const msg = [...stderrLines].reverse().find((l) => l.includes('Error')) ?? stderrLines.at(-1) ?? 'node --check failed';
9929
- return { error: msg.slice(0, 200) };
10127
+ const checked = runWithOneInternalRetry(() => {
10128
+ try {
10129
+ // `node --check` on the file IN PLACE, so the nearest package.json decides the module goal.
10130
+ execFileSync(process.execPath, ['--check', absFile], { stdio: 'pipe' });
10131
+ return {};
10132
+ }
10133
+ catch (e) {
10134
+ const err = e;
10135
+ // A launched parser that exits non-zero with a SyntaxError is a parse verdict. A child
10136
+ // launch/internal error (EPERM, ENOENT, Node's thrown internal) is runner infrastructure
10137
+ // and must take the bounded retry → INCONCLUSIVE route instead of masquerading as bad JS.
10138
+ if (typeof err.code === 'string' || typeof err.status !== 'number')
10139
+ throw e;
10140
+ const stderrLines = String(err.stderr ?? '').split('\n').map((line) => line.trim()).filter((line) => line !== '');
10141
+ const msg = [...stderrLines].reverse().find((line) => line.includes('Error'))
10142
+ ?? stderrLines.at(-1)
10143
+ ?? err.message
10144
+ ?? 'node --check failed';
10145
+ return { error: msg.slice(0, 200) };
10146
+ }
10147
+ });
10148
+ if (checked.value === null) {
10149
+ return {
10150
+ internalFailureReason: checked.failureReason ?? 'runner-internal-error: persistent after 2/2 attempts',
10151
+ internalAttempts: checked.attempts,
10152
+ };
9930
10153
  }
10154
+ return checked.internalRetries === 1
10155
+ ? { ...checked.value, internalAttempts: checked.attempts }
10156
+ : checked.value;
9931
10157
  }
9932
10158
  return { skipped: `no parser for '${ext}' files — parse-check unavailable` };
9933
10159
  }
@@ -9935,7 +10161,7 @@ function parseCheckMutatedFile(absFile, text) {
9935
10161
  return { skipped: `parse-check errored: ${String(e.message).slice(0, 120)}` };
9936
10162
  }
9937
10163
  }
9938
- function cmdMutationGate(options, flags, cwd, write) {
10164
+ function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
9939
10165
  const json = flags.has('json');
9940
10166
  const fail = (what) => {
9941
10167
  write(json ? JSON.stringify({ error: what, exitCode: 2 }) : `dz mutation-gate: ${what}`);
@@ -10003,6 +10229,7 @@ function cmdMutationGate(options, flags, cwd, write) {
10003
10229
  const results = [];
10004
10230
  const observations = [];
10005
10231
  const warnings = [];
10232
+ const internalRetries = [];
10006
10233
  let baseline;
10007
10234
  try {
10008
10235
  if (gitTop !== null && gitTop !== pkgDir && resolve(pkgDir).startsWith(resolve(gitTop) + sep)) {
@@ -10046,7 +10273,10 @@ function cmdMutationGate(options, flags, cwd, write) {
10046
10273
  // write below is asserted to RESOLVE inside this root before it happens.
10047
10274
  const realScratchRoot = realpathSync(copyDir);
10048
10275
  const requireCompletionReceipt = parsed.registry.requireCompletionReceipt === true;
10049
- const runSuite = () => {
10276
+ const invokeSuite = () => {
10277
+ if (injectedRunner !== undefined) {
10278
+ return injectedRunner(testCmd, { cwd: copyDir, timeoutMs: timeout });
10279
+ }
10050
10280
  const run = spawnSync(testCmd, {
10051
10281
  cwd: copyDir,
10052
10282
  shell: true,
@@ -10058,6 +10288,12 @@ function cmdMutationGate(options, flags, cwd, write) {
10058
10288
  const errorCode = run.error && 'code' in run.error && typeof run.error.code === 'string'
10059
10289
  ? run.error.code
10060
10290
  : undefined;
10291
+ // Node may populate both `error` and a numeric `status` for an internal spawn failure. The
10292
+ // error wins except for the two already-named resource observations: a status alongside
10293
+ // EPERM/Unreachable-code is not a suite verdict and takes the one-retry internal-error path.
10294
+ if (run.error !== undefined && errorCode !== 'ETIMEDOUT' && errorCode !== 'ENOBUFS') {
10295
+ throw run.error;
10296
+ }
10061
10297
  const signal = typeof run.signal === 'string' ? run.signal : undefined;
10062
10298
  let failureReason;
10063
10299
  if (typeof run.status !== 'number') {
@@ -10074,25 +10310,59 @@ function cmdMutationGate(options, flags, cwd, write) {
10074
10310
  }
10075
10311
  return {
10076
10312
  exitCode: typeof run.status === 'number' ? run.status : null,
10077
- // Receipt markers may be on stderr. Preserve both streams even on exit 0; stdout-only
10078
- // collection would silently lose a green-run marker.
10079
10313
  output: `${String(run.stdout ?? '')}\n${String(run.stderr ?? '')}`,
10080
10314
  ...(failureReason !== undefined ? { failureReason } : {}),
10081
10315
  };
10082
10316
  };
10317
+ const runSuite = (phase, entryId) => {
10318
+ const retried = runWithOneInternalRetry(invokeSuite);
10319
+ const loggedAttempts = retried.attempts.map((attempt) => {
10320
+ if (attempt.outcome !== 'completed' || retried.value === null)
10321
+ return attempt;
10322
+ const outcome = retried.value.exitCode === null
10323
+ ? `no exit code (${retried.value.failureReason ?? 'unnamed failure'})`
10324
+ : `exit ${retried.value.exitCode}`;
10325
+ return { ...attempt, detail: `attempt ${attempt.attempt}: completed — ${outcome}` };
10326
+ });
10327
+ if (retried.internalRetries === 1) {
10328
+ const record = entryId === undefined
10329
+ ? { phase, attempts: loggedAttempts }
10330
+ : { phase, entryId, attempts: loggedAttempts };
10331
+ internalRetries.push(record);
10332
+ if (!json)
10333
+ write(`mutation-gate: internal retry — ${loggedAttempts.map((attempt) => attempt.detail).join('; ')}`);
10334
+ }
10335
+ const internalAttemptLog = retried.internalRetries === 1
10336
+ ? loggedAttempts.map((attempt) => attempt.detail).join('; ')
10337
+ : undefined;
10338
+ if (retried.value !== null) {
10339
+ return {
10340
+ ...retried.value,
10341
+ ...(internalAttemptLog !== undefined ? { internalAttemptLog } : {}),
10342
+ };
10343
+ }
10344
+ return {
10345
+ exitCode: null,
10346
+ output: '',
10347
+ failureReason: retried.failureReason ?? 'runner-internal-error: persistent after 2/2 attempts',
10348
+ ...(internalAttemptLog !== undefined ? { internalAttemptLog } : {}),
10349
+ };
10350
+ };
10083
10351
  // Baseline BEFORE any mutation: a red copy proves nothing, and reading it as a mutation
10084
10352
  // result would be this gate shipping the defect class it exists to catch.
10085
10353
  if (!json)
10086
10354
  write(`mutation-gate: baseline suite in scratch copy of ${pkgDir} …`);
10087
- const base = runSuite();
10088
- baseline = classifyBaseline(base.exitCode, base.failureReason);
10355
+ const base = runSuite('baseline');
10356
+ baseline = classifyBaseline(base.exitCode, base.failureReason, base.exitCode !== null && base.exitCode !== 0
10357
+ ? attributeBaselineRedness(base.output, entries.map((entry) => entry.file))
10358
+ : undefined);
10089
10359
  if (!baseline.ok) {
10090
10360
  if (json) {
10091
- write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, baseline, results: [], exitCode: 2 }, null, 2));
10092
- return 2;
10361
+ write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, baseline, results: [], internalRetries, exitCode: 1 }, null, 2));
10362
+ return 1;
10093
10363
  }
10094
10364
  write(renderMutationReport([], baseline, pkgDir));
10095
- return 2;
10365
+ return 1;
10096
10366
  }
10097
10367
  for (const entry of entries) {
10098
10368
  const filePath = join(copyDir, entry.file);
@@ -10135,6 +10405,8 @@ function cmdMutationGate(options, flags, cwd, write) {
10135
10405
  write(`mutation-gate: ${entry.id} — mutating ${entry.file}, running suite …`);
10136
10406
  let run = null;
10137
10407
  let parseError;
10408
+ let parseInternalFailureReason;
10409
+ let parseInternalAttemptLog;
10138
10410
  try {
10139
10411
  writeFileSync(filePath, applied.text);
10140
10412
  // Route-a guard: the mutated file must still PARSE — a load failure reddens the whole
@@ -10145,11 +10417,18 @@ function cmdMutationGate(options, flags, cwd, write) {
10145
10417
  if (!json)
10146
10418
  write(`mutation-gate: WARNING ${entry.id}: parse-check skipped — ${check.skipped}`);
10147
10419
  }
10420
+ if (check.internalAttempts !== undefined) {
10421
+ internalRetries.push({ phase: 'parse-check', entryId: entry.id, attempts: check.internalAttempts });
10422
+ parseInternalAttemptLog = check.internalAttempts.map((attempt) => attempt.detail).join('; ');
10423
+ if (!json)
10424
+ write(`mutation-gate: internal retry — ${parseInternalAttemptLog}`);
10425
+ }
10426
+ parseInternalFailureReason = check.internalFailureReason;
10148
10427
  if (check.error !== undefined) {
10149
10428
  parseError = check.error; // no suite run: the verdict is MUTATION_UNPARSEABLE regardless
10150
10429
  }
10151
- else {
10152
- run = runSuite();
10430
+ else if (parseInternalFailureReason === undefined) {
10431
+ run = runSuite('mutation', entry.id);
10153
10432
  }
10154
10433
  }
10155
10434
  finally {
@@ -10185,14 +10464,24 @@ function cmdMutationGate(options, flags, cwd, write) {
10185
10464
  // those verdicts outrank the rebaseline check, so the extra suite run would buy nothing.
10186
10465
  let rebaselineExitCode;
10187
10466
  let rebaselineFailureReason;
10467
+ let rebaselineAttribution;
10468
+ let rebaselineInternalAttemptLog;
10188
10469
  if (rebaselineMode === 'per-entry' && run !== null && run.exitCode !== null && run.exitCode !== 0
10189
10470
  && fileLoadFailure === undefined && outputUnrecognised === undefined && receiptMismatch === undefined) {
10190
10471
  if (!json)
10191
10472
  write(`mutation-gate: ${entry.id} — re-baselining the restored tree …`);
10192
- const rebaselineRun = runSuite();
10473
+ const rebaselineRun = runSuite('rebaseline', entry.id);
10193
10474
  rebaselineExitCode = rebaselineRun.exitCode;
10194
10475
  rebaselineFailureReason = rebaselineRun.failureReason;
10476
+ rebaselineInternalAttemptLog = rebaselineRun.internalAttemptLog;
10477
+ if (rebaselineRun.exitCode !== null && rebaselineRun.exitCode !== 0) {
10478
+ rebaselineAttribution = attributeBaselineRedness(rebaselineRun.output, entries.map((candidate) => candidate.file));
10479
+ }
10195
10480
  }
10481
+ const entryRunFailureReason = run?.failureReason ?? parseInternalFailureReason;
10482
+ const entryInternalAttemptLog = [parseInternalAttemptLog, run?.internalAttemptLog, rebaselineInternalAttemptLog]
10483
+ .filter((log) => log !== undefined)
10484
+ .join('; ');
10196
10485
  const obs = {
10197
10486
  entry,
10198
10487
  occurrences: 1,
@@ -10202,9 +10491,11 @@ function cmdMutationGate(options, flags, cwd, write) {
10202
10491
  ...(fileLoadFailure !== undefined ? { fileLoadFailure } : {}),
10203
10492
  ...(outputUnrecognised !== undefined ? { outputUnrecognised } : {}),
10204
10493
  ...(receiptMismatch !== undefined ? { receiptMismatch } : {}),
10205
- ...(run?.failureReason !== undefined ? { runFailureReason: run.failureReason } : {}),
10494
+ ...(entryRunFailureReason !== undefined ? { runFailureReason: entryRunFailureReason } : {}),
10495
+ ...(entryInternalAttemptLog !== '' ? { internalAttemptLog: entryInternalAttemptLog } : {}),
10206
10496
  ...(rebaselineExitCode !== undefined ? { rebaselineExitCode } : {}),
10207
10497
  ...(rebaselineFailureReason !== undefined ? { rebaselineFailureReason } : {}),
10498
+ ...(rebaselineAttribution !== undefined ? { rebaselineAttribution } : {}),
10208
10499
  };
10209
10500
  observations.push(obs);
10210
10501
  results.push(classifyMutationOutcome(obs));
@@ -10217,7 +10508,7 @@ function cmdMutationGate(options, flags, cwd, write) {
10217
10508
  if (rebaselineMode === 'final') {
10218
10509
  if (!json)
10219
10510
  write('mutation-gate: final re-baseline of the restored tree …');
10220
- const finalRun = runSuite();
10511
+ const finalRun = runSuite('final-rebaseline');
10221
10512
  const finalExit = finalRun.exitCode;
10222
10513
  if (finalExit !== 0) {
10223
10514
  const what = finalExit === null ? `no exit code: ${finalRun.failureReason ?? 'unknown timeout / spawn failure'}` : `exit ${finalExit}`;
@@ -10228,6 +10519,12 @@ function cmdMutationGate(options, flags, cwd, write) {
10228
10519
  ...obs,
10229
10520
  rebaselineExitCode: finalExit,
10230
10521
  ...(finalRun.failureReason !== undefined ? { rebaselineFailureReason: finalRun.failureReason } : {}),
10522
+ ...(finalRun.internalAttemptLog !== undefined
10523
+ ? { internalAttemptLog: [obs.internalAttemptLog, finalRun.internalAttemptLog].filter((log) => log !== undefined).join('; ') }
10524
+ : {}),
10525
+ ...(finalExit !== null && finalExit !== 0
10526
+ ? { rebaselineAttribution: attributeBaselineRedness(finalRun.output, entries.map((entry) => entry.file)) }
10527
+ : {}),
10231
10528
  }));
10232
10529
  results.length = 0;
10233
10530
  results.push(...reclassified);
@@ -10247,7 +10544,7 @@ function cmdMutationGate(options, flags, cwd, write) {
10247
10544
  }
10248
10545
  const exitCode = mutationGateExitCode(results, baseline.ok);
10249
10546
  if (json) {
10250
- write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, rebaselineMode, baseline, results, summary: summarizeMutationResults(results), warnings, exitCode }, null, 2));
10547
+ write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, rebaselineMode, baseline, results, summary: summarizeMutationResults(results), warnings, internalRetries, exitCode }, null, 2));
10251
10548
  return exitCode;
10252
10549
  }
10253
10550
  write(renderMutationReport(results, baseline, pkgDir));
@@ -12091,6 +12388,18 @@ function cmdFeatureAdrRecord(options, flags, cwd, write) {
12091
12388
  targetExists: existsSync(target),
12092
12389
  targetHasPair: flags.has('once') && existsSync(target),
12093
12390
  timestamp: new Date().toISOString(),
12391
+ // WHO ran it: `--runner <id>` when the caller knows, otherwise this host. The workflow cannot
12392
+ // supply it — it has no host inside its sandbox — so the identity is resolved here, at the one
12393
+ // seam that runs outside. hostname() can throw on an exotic setup; an unresolvable runner stays
12394
+ // ABSENT rather than becoming the string 'unknown', which would later join as if it were one.
12395
+ runnerId: (options.get('runner') ?? '').trim() !== ''
12396
+ ? (options.get('runner') ?? '').trim()
12397
+ : (() => { try {
12398
+ return hostname();
12399
+ }
12400
+ catch {
12401
+ return null;
12402
+ } })(),
12094
12403
  });
12095
12404
  if (decision.line === null)
12096
12405
  return emit(decision);
@@ -13443,6 +13752,143 @@ async function cmdQeBridge(options, flags, cwd, write) {
13443
13752
  cleanupIsolated();
13444
13753
  }
13445
13754
  }
13755
+ function scoreReceiptFiles(root) {
13756
+ const featuresDir = join(root, 'features');
13757
+ let features;
13758
+ try {
13759
+ features = readdirSync(featuresDir, { withFileTypes: true });
13760
+ }
13761
+ catch {
13762
+ return [];
13763
+ }
13764
+ const receipts = [];
13765
+ for (const feature of features) {
13766
+ if (!feature.isDirectory())
13767
+ continue;
13768
+ const stateDir = join(featuresDir, feature.name, '.fa-state');
13769
+ let entries;
13770
+ try {
13771
+ if (lstatSync(stateDir).isSymbolicLink())
13772
+ continue;
13773
+ entries = readdirSync(stateDir, { withFileTypes: true });
13774
+ }
13775
+ catch {
13776
+ continue;
13777
+ }
13778
+ for (const entry of entries) {
13779
+ if (!entry.isFile())
13780
+ continue;
13781
+ const match = /^score-(.+)\.json$/.exec(entry.name);
13782
+ if (match === null || match[1] === undefined || match[1] === '')
13783
+ continue;
13784
+ const path = join(stateDir, entry.name);
13785
+ receipts.push({ path, displayPath: relative(root, path), qeHash: match[1] });
13786
+ }
13787
+ }
13788
+ return receipts.sort((a, b) => a.displayPath < b.displayPath ? -1 : a.displayPath > b.displayPath ? 1 : 0);
13789
+ }
13790
+ function scoreAggregateChainLine(text) {
13791
+ if (text === '') {
13792
+ return { line: 'chain: NOT_PRESENT — no aggregate evidence file was created', verification: null, defectAges: null };
13793
+ }
13794
+ const verification = verifyEventChainText(text);
13795
+ const defectAges = classifyChainDefects(verification, verification.lines);
13796
+ const kinds = new Map();
13797
+ for (const defect of verification.defects)
13798
+ kinds.set(defect.kind, (kinds.get(defect.kind) ?? 0) + 1);
13799
+ const kindText = [...kinds.entries()].map(([kind, count]) => `${kind}: ${count}`).join(' · ');
13800
+ const line = `chain: ${verification.ok ? 'OK' : 'FAILED'} · ${verification.chained} chained · ` +
13801
+ `${verification.resets} recorded restart(s) · before-run defects ${defectAges.beforeRun.length} · ` +
13802
+ `in-run defects ${defectAges.inRun.length} · current run ${defectAges.runRecords} record(s)` +
13803
+ (kindText === '' ? '' : ` · ${kindText}`) +
13804
+ ` — ${verification.scope}`;
13805
+ return { line, verification, defectAges };
13806
+ }
13807
+ function cmdScoreAll(options, flags, cwd, write) {
13808
+ const json = flags.has('json');
13809
+ if (options.has('slug')) {
13810
+ write(json
13811
+ ? JSON.stringify({ error: '--all and --slug are mutually exclusive', exitCode: 1 })
13812
+ : 'dz score: --all and --slug are mutually exclusive');
13813
+ return 1;
13814
+ }
13815
+ const root = resolve(cwd, options.get('project') ?? '.');
13816
+ const receiptFiles = scoreReceiptFiles(root);
13817
+ if (receiptFiles.length === 0) {
13818
+ const report = buildScoreAggregateReport([], [], 0);
13819
+ const chain = scoreAggregateChainLine('');
13820
+ if (json)
13821
+ write(JSON.stringify({ ...report, chain: null, aggregatePath: '.dz/feature-adr/scorecards.jsonl', exitCode: 0 }, null, 2));
13822
+ else {
13823
+ write(renderScoreAggregateReport(report));
13824
+ write(chain.line);
13825
+ }
13826
+ return 0;
13827
+ }
13828
+ const ts = new Date().toISOString();
13829
+ const rows = [];
13830
+ const unreadableReceipts = [];
13831
+ for (const receipt of receiptFiles) {
13832
+ try {
13833
+ rows.push(scoreReceiptToAggregateRow({
13834
+ content: readFileSync(receipt.path, 'utf8'),
13835
+ qeHash: receipt.qeHash,
13836
+ ts,
13837
+ }));
13838
+ }
13839
+ catch {
13840
+ unreadableReceipts.push(receipt.displayPath);
13841
+ }
13842
+ }
13843
+ const storeDir = join(root, '.dz', 'feature-adr');
13844
+ const aggregatePath = join(storeDir, 'scorecards.jsonl');
13845
+ let finalText = '';
13846
+ let finalRows = rows;
13847
+ let appended = 0;
13848
+ let storeError = null;
13849
+ try {
13850
+ const result = withNamedLockSync(storeDir, 'scorecards', () => {
13851
+ let existingText = '';
13852
+ try {
13853
+ existingText = readFileSync(aggregatePath, 'utf8');
13854
+ }
13855
+ catch (error) {
13856
+ if (error.code !== 'ENOENT')
13857
+ throw error;
13858
+ }
13859
+ const fresh = dedupeScoreAggregateRows(rows, readScoreAggregateRows(existingText));
13860
+ const appendText = appendChainedLines(fresh, readTailInfo(existingText));
13861
+ if (appendText !== '')
13862
+ appendFileSync(aggregatePath, appendText, { encoding: 'utf8', mode: 0o600 });
13863
+ const settledText = existingText + appendText;
13864
+ return { text: settledText, rows: readScoreAggregateRows(settledText), appended: fresh.length };
13865
+ });
13866
+ finalText = result.text;
13867
+ finalRows = result.rows;
13868
+ appended = result.appended;
13869
+ }
13870
+ catch (error) {
13871
+ storeError = error instanceof Error ? error.message : String(error);
13872
+ }
13873
+ const report = buildScoreAggregateReport(finalRows, unreadableReceipts, appended);
13874
+ const chain = scoreAggregateChainLine(finalText);
13875
+ if (json) {
13876
+ write(JSON.stringify({
13877
+ ...report,
13878
+ aggregatePath: '.dz/feature-adr/scorecards.jsonl',
13879
+ chain: chain.verification === null ? null : { verification: chain.verification, defectAges: chain.defectAges },
13880
+ storeError,
13881
+ exitCode: 0,
13882
+ }, null, 2));
13883
+ }
13884
+ else {
13885
+ write(renderScoreAggregateReport(report));
13886
+ write(chain.line);
13887
+ if (storeError !== null)
13888
+ write(`store error (nothing was claimed appended): ${storeError}`);
13889
+ }
13890
+ return 0;
13891
+ }
13446
13892
  function cmdScore(options, flags, cwd, write) {
13447
13893
  const json = flags.has('json');
13448
13894
  if (flags.has('help')) {
@@ -13451,13 +13897,14 @@ function cmdScore(options, flags, cwd, write) {
13451
13897
  write(JSON.stringify({ help: usage, exitCode: 0 })); // --json stays ONE document even for help
13452
13898
  else {
13453
13899
  write(usage);
13900
+ write('dz score --all [--project <dir>] [--json] — sweep immutable score receipts into the append-only chained aggregate');
13454
13901
  write(' disciplines: ADR confirmation · discrimination · cross-model QE · live verification · README-first · learning loop · amendments');
13455
13902
  write(' descriptive-only, never a gate: a low score exits 0');
13456
13903
  }
13457
13904
  return 0;
13458
13905
  }
13459
13906
  for (const flag of flags) {
13460
- if (!new Set(['json', 'help']).has(flag)) {
13907
+ if (!new Set(['json', 'help', 'all']).has(flag)) {
13461
13908
  write(json ? JSON.stringify({ error: `unknown option --${flag}`, exitCode: 1 }) : `dz score: unknown option --${flag}\n allowed: --slug <feature>, --project <dir>, --json`);
13462
13909
  return 1;
13463
13910
  }
@@ -13469,6 +13916,8 @@ function cmdScore(options, flags, cwd, write) {
13469
13916
  return 1;
13470
13917
  }
13471
13918
  }
13919
+ if (flags.has('all'))
13920
+ return cmdScoreAll(options, flags, cwd, write);
13472
13921
  const slug = options.get('slug') ?? '';
13473
13922
  // The delivery-check traversal lesson, upgraded to a WHITELIST: `.` slipped the blacklist and
13474
13923
  // silently aggregated the entire features/ tree as one "run" (Codex QE #2).
@@ -15342,17 +15791,19 @@ function cmdStats(cwd, write) {
15342
15791
  }
15343
15792
  const dirs = readdirSync(baseDir, { withFileTypes: true }).filter((e) => e.isDirectory());
15344
15793
  const packages = dirs.length;
15345
- let totalSkills = 0;
15346
- let skillPacks = 0;
15347
- for (const dir of dirs) {
15348
- if (dir.name.startsWith('skills-')) {
15349
- skillPacks++;
15350
- const skillDir = join(baseDir, dir.name);
15351
- const skillDirs = readdirSync(skillDir, { withFileTypes: true })
15352
- .filter((e) => e.isDirectory() && existsSync(join(skillDir, e.name, 'SKILL.md')));
15353
- totalSkills += skillDirs.length;
15354
- }
15355
- }
15794
+ // Backlog e160aeee. This used to walk the tree ITSELF, and was wrong in two independent ways:
15795
+ // it counted only packages whose NAME starts with `skills-` (health-advisor, p-replicator,
15796
+ // keysarium and trip-planner were therefore invisible), and it knew only ONE of the three skill
15797
+ // layouts. Result: 203 here against 250 from `dz registry` on the same tree — two counters of one
15798
+ // quantity, each unable to refute the other because neither knew the other existed.
15799
+ //
15800
+ // The fix is structural, not arithmetic: there is now ONE enumerator, and both commands ask it.
15801
+ // Pinned by test/stats-registry-parity.test.ts, whose red half is this exact divergence.
15802
+ // The registry already PUBLISHES these totals; recomputing them from `entries` here would be a
15803
+ // third implementation of the same count, which is the very defect being fixed.
15804
+ const registry = buildRegistry(cwd);
15805
+ const totalSkills = registry.totalSkills;
15806
+ const skillPacks = registry.totalPacks;
15356
15807
  const targets = TARGET_NAMES.length;
15357
15808
  const presets = PRESET_NAMES.length;
15358
15809
  write(`dz stats — DZ Harness Hub`);
@@ -15837,7 +16288,28 @@ export async function runCli(argv, io = {}) {
15837
16288
  // let a guard treat "I could not tell you" as "I answered you".
15838
16289
  return version === 'unknown' ? 1 : 0;
15839
16290
  }
15840
- if (command === '' || command === 'help' || flags.has('help')) {
16291
+ // `-h` is the most-typed help flag and is NOT a command: before the unknown-command contract
16292
+ // landed it fell through to the switch and still printed usage; afterwards it would have died
16293
+ // with exit 2 and an empty stdout (measured regression, cross-model QE M1). It belongs beside
16294
+ // `-v` above — an argv-level flag, resolved before command dispatch.
16295
+ if (argv[0] === '-h') {
16296
+ write(USAGE);
16297
+ return 0;
16298
+ }
16299
+ // A bare `--typo` leaves the command empty, so the usage branch reported SUCCESS on a misspelled
16300
+ // FLAG exactly as it used to on a misspelled VERB (cross-model QE M2): `dz --frobnicate` exited 0
16301
+ // with 30 KB of usage. The refusal is deliberately narrowed to the no-command case, because the
16302
+ // warn-don't-refuse decision above is measured and still stands: with a command present, an
16303
+ // unrecognised name may simply be missing from KNOWN_CLI_FLAGS and refusing would break working
16304
+ // invocations. With NO command there is nothing the flag could belong to, so it is a usage error.
16305
+ if (command === '') {
16306
+ const strayNames = unknownFlagNotice([...flags, ...options.keys()].filter((k) => !k.startsWith('_positional_')), KNOWN_CLI_FLAGS).map((n) => n.name);
16307
+ if (strayNames.length > 0) {
16308
+ writeErr(`dz: unknown option --${strayNames[0]} — run 'dz help' for usage`);
16309
+ return 2;
16310
+ }
16311
+ }
16312
+ if (command === '' || command === 'help' || (flags.has('help') && DZ_COMMANDS.includes(command))) {
15841
16313
  write(USAGE);
15842
16314
  return 0;
15843
16315
  }
@@ -15896,6 +16368,8 @@ export async function runCli(argv, io = {}) {
15896
16368
  return cmdStatusline(options, flags, cwd, write, readStdin);
15897
16369
  case 'usage':
15898
16370
  return cmdUsage(options, optionLists, flags, cwd, write);
16371
+ case 'chain':
16372
+ return cmdChain(options, flags, cwd, write);
15899
16373
  case 'claim-check':
15900
16374
  return cmdClaimCheck(options, optionLists, flags, cwd, write);
15901
16375
  case 'lint':
@@ -15917,7 +16391,7 @@ export async function runCli(argv, io = {}) {
15917
16391
  case 'diff':
15918
16392
  return cmdDiff(options, cwd, write);
15919
16393
  case 'recommend':
15920
- return cmdRecommend(options, cwd, write);
16394
+ return cmdRecommend(options, flags, cwd, write);
15921
16395
  case 'upgrade':
15922
16396
  return cmdUpgrade(options, flags, cwd, write, writeErr);
15923
16397
  case 'auto-canonicalize':
@@ -15966,8 +16440,22 @@ export async function runCli(argv, io = {}) {
15966
16440
  return cmdChallenge(options, flags, cwd, write);
15967
16441
  case 'discrimination-check':
15968
16442
  return cmdDiscriminationCheck(options, flags, cwd, write);
15969
- case 'mutation-gate':
15970
- return cmdMutationGate(options, flags, cwd, write);
16443
+ case 'mutation-gate': {
16444
+ try {
16445
+ return cmdMutationGate(options, flags, cwd, write, io.mutationGateRunner);
16446
+ }
16447
+ catch (error) {
16448
+ const raw = error instanceof Error ? error.message : String(error);
16449
+ const head = Array.from(raw.split(/\r?\n/, 1)[0]?.trim() || 'unknown internal error').slice(0, 160).join('');
16450
+ if (flags.has('json')) {
16451
+ write(JSON.stringify({ verdict: 'INCONCLUSIVE', reason: 'runner-internal-error', error: head, exitCode: 1 }));
16452
+ }
16453
+ else {
16454
+ write(`mutation-gate: INTERNAL ERROR (${head}) — verdict INCONCLUSIVE, exit 1`);
16455
+ }
16456
+ return 1;
16457
+ }
16458
+ }
15971
16459
  case 'delivery-check':
15972
16460
  return cmdDeliveryCheck(options, flags, cwd, write);
15973
16461
  case 'skills-verify':
@@ -16021,9 +16509,8 @@ export async function runCli(argv, io = {}) {
16021
16509
  case 'import-ecc':
16022
16510
  return await cmdImportEcc(options, flags, cwd, write);
16023
16511
  default:
16024
- write(`dz: unknown command ${JSON.stringify(command)}`);
16025
- write(USAGE);
16026
- return 1;
16512
+ writeErr(`dz: unknown command ${JSON.stringify(command)} — run 'dz help' for the command list`);
16513
+ return 2;
16027
16514
  }
16028
16515
  }
16029
16516
  catch (error) {