@dzhechkov/harness-cli 0.8.6 → 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, 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, 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, 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, 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
- 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, 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,
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';
@@ -40,19 +40,19 @@ export const DZ_COMMANDS = [
40
40
  'usage', 'claim-check', 'lint', 'sign', 'sbom', 'guard', 'verify-pack', 'setup',
41
41
  'pretrain', 'compose', 'diff', 'recommend', 'upgrade', 'auto-canonicalize',
42
42
  'publish', 'release', 'parity', 'registry', 'benchmark', 'mcp-scan',
43
- 'sync-upstream', 'drift-check', 'hooks-sync', 'agents-sync', 'sync-canonical',
43
+ 'sync-upstream', 'drift-check', 'hooks-sync', 'integrations-verify', 'agents-sync', 'sync-canonical',
44
44
  'plugin', 'downloads', 'stats', 'architecture', 'project-skills', 'mr-rakes',
45
45
  'retro', 'feature-adr-setup', 'challenge', 'discrimination-check',
46
46
  'mutation-gate', 'delivery-check', 'skills-verify', 'compounding', 'deadwood',
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
 
54
54
  Usage:
55
- dz init --target <name> [--skills-dir <dir>] [--project <dir>] [--preset <name>] [--select id,id,...] [--force] [--enrich] [--no-hooks] [--no-verify] (--target codex ALSO installs the user-global dz veto+recall hooks and LIVE-verifies them (ADR-001 §8); --no-hooks = skills only; --no-verify skips the live probe and can never report ready)
55
+ dz init --target <name> [--skills-dir <dir>] [--project <dir>] [--preset <name>] [--select id,id,...] [--force] [--enrich] [--allow-integrations <sha256:digest>] [--no-integrations] [--no-hooks] [--no-verify] (integration manifests require exact digest consent; --no-integrations = explicit skills-only)
56
56
  dz verify [--skills-dir <dir>] [--target <name>]
57
57
  dz sync [--canonical <dir>] [--project <dir>] [--dry-run] [--force]
58
58
  dz update (alias of sync)
@@ -81,13 +81,14 @@ Usage:
81
81
  dz delivery-check --slug <slug> [--context-only] [--findings <f.json>] [--strict] [--author <model>] [--json] (portable Step-10 Delivery Gate: prints the 4-plane review brief + artifact probes; --findings classifies a fed-back review into a fail-closed ready|blocked hand-off and writes features/<slug>/10_delivery_review.md; --strict exits 1 on blocked)
82
82
  dz challenge --plan <plan.md> [--author <model>] (the deterministic cartridge behind the challenge-panel adversarial plan-gate (R6): assembles the wide brief — plan + architecture/vision.md + testing.md + map.json + degradations.md — and prints the C1-C8 adversary prompt naming the cross-family reviewer to dispatch. exit 0 brief printed / 1 plan missing or empty)
83
83
  dz skills-verify [--dir <project>] [--expect a,b] [--static] [--strict] [--json] (does .claude/skills/ actually REGISTER? --static = instant layout scan for CI; default reads the authoritative system/init listing from a real session. exit 0 pass / 1 fail / 2 inconclusive — never a false pass)
84
- dz compounding [--project <dir>] [--json] (honest learning-loop payoff report: pool write-only ratio, guard repeat-violation trajectory, cold-vs-warm replay readiness, instrumentation health a gate without enough data says INSUFFICIENT_DATA, never a fake verdict)
84
+ dz compounding [--project <dir>] [--json] (honest learning-loop payoff report: pool/replay/guard instrumentation + monthly eligible→attempted→accepted→executions; unavailable is NOT MEASURED, and only a named empty stage after a non-empty predecessor for 3 measured months is a funnel finding)
85
85
  dz deadwood [--weeks <n>] [--json] (advisory zero-usage candidates for human deprecation review; safety-excluded surfaces carry reasons; never deletes or deprecates anything; shallow history says INSUFFICIENT_DATA)
86
86
  dz epoch-replay --mock [--n <N>] [--effect <-1..1>] [--tie-rate <0..1>] [--seed <N>] [--slice <name>] [--json] ($0 synthetic run — exercises the verdict math, NOT evidence)
87
87
  dz epoch-replay --emit [--project <dir>] [--limit <N>] [--seed <N>] [--out <file>] (cold-vs-warm work order: instances + PRE-REGISTERED blind A/B assignment; the runner never calls a model)
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)
@@ -122,7 +123,7 @@ Usage:
122
123
  dz backlog jira <id> [--project <dir>] [--json] (draft a Jira issue via the configurable adapter (backlog.jira.adapter: jira-mcp|copilot-mcp|none); none writes an auditable jira-outbox/<id>.json stub)
123
124
  dz backlog harmonize [--apply] [--threshold <0-1>] [--project <dir>] [--json] (batch semantic dedup of the backlog ideas; --dry-run default, --apply snapshots first)
124
125
  dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--project <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--no-verify] [--install-driver] [--force] [--enrich] (--target codex ALSO installs + LIVE-verifies the codex hooks; an unverified hook exits non-zero WITHOUT aborting the rest of setup)
125
- dz teach "<pattern>" [--reward <0-1>] [--domain <name>] [--type rule|success-pattern|lesson-learned] [--project <dir>] [--no-mirror] (--project pins the learned store to <dir>/.dz, not the cwd — pin to a canonical brain)
126
+ dz teach "<pattern>" [--class-form "<template with :slot>"] [--reward <0-1>] [--domain <name>] [--type rule|success-pattern|lesson-learned] [--project <dir>] [--no-mirror] (class form is optional; rejection never blocks the specific write; --project pins the learned store to <dir>/.dz)
126
127
  dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
127
128
  dz consolidate [--sessions-dir <dir>] [--project <dir>] [--no-mirror] [--prune-noise [--apply]] [--prune-quarantine [--apply]] (both prunes: DRY-RUN by default; --apply snapshots then deletes; prune-quarantine = expired unproven lessons ONLY, never coupled to noise)
128
129
  dz recall "<query>" [--limit <N>] [--domain <name>] [--semantic | --no-semantic] [--books [--book <slug>]] [--project <dir>] | dz recall --all [--json] | dz recall --usage [--json] | dz recall --forget <dzId>[,<dzId>] [--apply] | dz recall --promote <dzId>[,<dzId>] [--apply] (--domain <name> BOOSTS lessons of that domain without dropping foreign ones — a shared store keeps its cross-domain transfers; forget/promote: dry-run default; forget snapshots before removing; promote lifts lesson-quarantine)
@@ -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>]
@@ -161,6 +163,7 @@ Usage:
161
163
  dz drift-check [--json] [--project <dir>] (CI gate: exit 1 if any shared skill drifted between its monorepo copies)
162
164
  dz agents-sync [--project <dir>] [--check] [--json] (sync/verify the always-on policy fence in root AGENTS.md; exit 0 synced/written, 1 drift, 3 inconclusive)
163
165
  dz hooks-sync --target codex [--check] [--verify] [--remove] [--json] (install/verify the dz veto + recall hooks in $CODEX_HOME/hooks.json; exit 0 armed+trusted, 1 not armed/drift, 3 inconclusive)
166
+ dz integrations-verify --target <name> --component <mcp|hooks> [--project <dir>] [--json] (non-executing exact-version registration probe; exit 0 observed / 1 refused)
164
167
  dz sync-canonical <skill> [--check] [--from <dir>] [--auto] [--project <dir>] (heal every copy from skills-meta/<skill> or --from; no canonical + --check = compare copies to each other (exit 1 on drift); no canonical + write = refuse unless --auto (LOUD, picks most-complete copy); --check writes nothing)
165
168
  dz plugin [--version <ver>]
166
169
  dz downloads
@@ -265,6 +268,35 @@ function discoverSkillsDirs(cwd, explicitSkillsDir) {
265
268
  async function installSkills(opts) {
266
269
  const { target, projectRoot, cwd, explicitSkillsDir, select, force, enrich } = opts;
267
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
+ }
268
300
  // agents-md and gemini are FLATTENING single-file targets: each must aggregate
269
301
  // every selected skill from ALL discovered dirs into ONE root file (AGENTS.md /
270
302
  // GEMINI.md) in a single merge. A per-dir runInit loop (like the tree targets
@@ -272,8 +304,8 @@ async function installSkills(opts) {
272
304
  // route them through one aggregation.
273
305
  if (target === 'agents-md' || target === 'gemini') {
274
306
  const report = target === 'gemini'
275
- ? runInitGeminiMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}) })
276
- : runInitAgentsMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}) });
307
+ ? runInitGeminiMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}), ...(opts.noHooks !== undefined ? { noHooks: opts.noHooks } : {}), ...(opts.noIntegrations !== undefined ? { noIntegrations: opts.noIntegrations } : {}) })
308
+ : runInitAgentsMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}), ...(opts.noHooks !== undefined ? { noHooks: opts.noHooks } : {}), ...(opts.noIntegrations !== undefined ? { noIntegrations: opts.noIntegrations } : {}) });
277
309
  const results = report.skills.map((s) => ({
278
310
  id: s.id,
279
311
  written: s.written.length,
@@ -285,12 +317,20 @@ async function installSkills(opts) {
285
317
  written += s.written;
286
318
  skipped += s.skipped;
287
319
  }
288
- return { results, dirsSearched: skillsDirs.length, written, skipped, missing: [...report.missing], failures: [...report.failures], applyFailures: [...report.applyFailures] };
320
+ return { results, dirsSearched: skillsDirs.length, written, skipped, missing: [...report.missing], failures: [...report.failures], applyFailures: [...report.applyFailures], integrations: [...report.integrations], ...(report.integrationDigest !== undefined ? { integrationDigest: report.integrationDigest } : {}) };
289
321
  }
290
322
  const results = [];
291
323
  const failures = [];
292
324
  const applyFailures = [];
293
- for (const skillsDir of skillsDirs) {
325
+ let integrations = [];
326
+ let integrationDigest;
327
+ const integrationManifestSources = skillsDirs.flatMap((skillsDir) => {
328
+ const discovered = discoverSkillIds(skillsDir);
329
+ return discovered
330
+ .filter((id) => select === undefined || select.includes(id))
331
+ .map((skillId) => ({ skillId, skillDir: skillsDir }));
332
+ });
333
+ for (const [dirIndex, skillsDir] of skillsDirs.entries()) {
294
334
  const r = await runInit({
295
335
  target,
296
336
  skillsDir,
@@ -298,12 +338,27 @@ async function installSkills(opts) {
298
338
  force,
299
339
  enrich,
300
340
  ...(select !== undefined ? { select } : {}),
341
+ ...(opts.noHooks !== undefined ? { noHooks: opts.noHooks } : {}),
342
+ ...(dirIndex === 0
343
+ ? {
344
+ integrationManifestSources,
345
+ ...(opts.noIntegrations !== undefined ? { noIntegrations: opts.noIntegrations } : {}),
346
+ }
347
+ : { noIntegrations: true }),
348
+ ...(opts.noVerify !== undefined ? { noVerify: opts.noVerify } : {}),
349
+ ...(opts.allowIntegrations !== undefined ? { allowIntegrations: opts.allowIntegrations } : {}),
301
350
  });
302
351
  for (const skill of r.skills) {
303
352
  results.push({ id: skill.id, written: skill.written.length, skipped: skill.skipped.length });
304
353
  }
305
354
  failures.push(...r.failures);
306
355
  applyFailures.push(...r.applyFailures);
356
+ if (r.integrations.some((row) => row.status !== 'not-requested'))
357
+ integrations = [...r.integrations];
358
+ else if (integrations.length === 0)
359
+ integrations = [...r.integrations];
360
+ if (r.integrationDigest !== undefined)
361
+ integrationDigest = r.integrationDigest;
307
362
  }
308
363
  let written = 0;
309
364
  let skipped = 0;
@@ -313,7 +368,7 @@ async function installSkills(opts) {
313
368
  }
314
369
  const installed = new Set(results.map((s) => s.id));
315
370
  const missing = select !== undefined ? [...select].filter((id) => !installed.has(id)) : [];
316
- return { results, dirsSearched: skillsDirs.length, written, skipped, missing, failures, applyFailures };
371
+ return { results, dirsSearched: skillsDirs.length, written, skipped, missing, failures, applyFailures, integrations, ...(integrationDigest !== undefined ? { integrationDigest } : {}) };
317
372
  }
318
373
  /** Warn about preset/select ids that weren't found in any installed pack. */
319
374
  function writeMissingSkillsHint(write, missing, presetName) {
@@ -374,19 +429,24 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
374
429
  cwd,
375
430
  explicitSkillsDir,
376
431
  select,
432
+ writeErr,
377
433
  force: flags.has('force'),
378
434
  enrich: flags.has('enrich'),
435
+ noHooks: flags.has('no-hooks'),
436
+ noIntegrations: flags.has('no-integrations'),
437
+ noVerify: flags.has('no-verify'),
438
+ ...(options.get('allow-integrations') !== undefined ? { allowIntegrations: options.get('allow-integrations') } : {}),
379
439
  });
380
- write(`dz init --target ${target}: ${r.results.length} skill(s), ${r.written} file(s) written, ${r.skipped} skipped`);
381
- if (r.dirsSearched > 1) {
382
- write(` (searched ${r.dirsSearched} skill directories)`);
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;
383
445
  }
384
- writeMissingSkillsHint(write, r.missing, presetName);
385
- // Codex-targeted init DELIVERS the hooks and verifies them (ADR-001 §8). Skills alone are not the
386
- // target's harness: the veto + recall legs are what `--target codex` promises.
387
- // `--no-hooks` is the documented escape for "skills only" (the same flag `dz setup` already
388
- // carries): hook delivery writes USER-GLOBAL config, so a command that only wants skills compiled
389
- // must be able to say so — and every test that is about skills says it.
446
+ // Codex keeps its established user-registry writer, but its result is normalized into the same
447
+ // two-outcome contract before JSON/human rendering. A write without a live ready observation is
448
+ // a refusal with applied=true, never a second success channel.
449
+ let integrationOutcomes = r.integrations;
390
450
  let codexHooksOk = true;
391
451
  if (target === 'codex' && !flags.has('no-hooks')) {
392
452
  const delivery = deliverCodexHooks({ project: projectRoot, verify: !flags.has('no-verify') }, undefined, 'dz init');
@@ -395,7 +455,30 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
395
455
  write(line);
396
456
  for (const line of delivery.stderr)
397
457
  writeErr(line);
458
+ const hookIndex = integrationOutcomes.findIndex((row) => row.component === 'hooks' && row.status !== 'not-requested');
459
+ if (hookIndex !== -1) {
460
+ const hook = normalizeCodexHookOutcome(integrationOutcomes[hookIndex], delivery, flags.has('no-verify'));
461
+ integrationOutcomes = integrationOutcomes.map((row, index) => index === hookIndex ? hook : row);
462
+ }
463
+ }
464
+ if (flags.has('json')) {
465
+ write(JSON.stringify({ target, skills: { count: r.results.length, written: r.written, skipped: r.skipped }, integrations: integrationOutcomes, integrationDigest: r.integrationDigest ?? null }));
398
466
  }
467
+ else {
468
+ write(`dz init --target ${target}: ${r.results.length} skill(s), ${r.written} file(s) written, ${r.skipped} skipped`);
469
+ if (r.dirsSearched > 1)
470
+ write(` (searched ${r.dirsSearched} skill directories)`);
471
+ for (const outcome of integrationOutcomes) {
472
+ const label = outcome.component === 'mcp' ? 'MCP' : 'Hooks';
473
+ const detail = outcome.status === 'refused'
474
+ ? `${outcome.reasonCode ?? 'LIVE_PROBE_FAILED'}${outcome.remediation ? ` — ${outcome.remediation}` : ''}`
475
+ : outcome.status === 'emitted'
476
+ ? `${outcome.carrier?.path ?? 'registered'}${outcome.registrations.some((row) => row.approval === 'pending') ? '; Pending approval; ready=false' : ''}`
477
+ : 'explicitly not requested';
478
+ write(`${label}: ${outcome.status.toUpperCase()} (${detail})`);
479
+ }
480
+ }
481
+ writeMissingSkillsHint(write, r.missing, presetName);
399
482
  // Skip-and-collect must not become skip-and-SILENCE: a skill that failed to load is
400
483
  // named on stderr and the command exits 1 (it exited 1 before too — by throwing).
401
484
  if (r.failures.length > 0 || r.applyFailures.length > 0) {
@@ -418,7 +501,38 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
418
501
  writeErr(line);
419
502
  return 1;
420
503
  }
421
- return codexHooksOk ? 0 : 1;
504
+ const integrationOk = !integrationOutcomes.some((row) => row.status === 'refused');
505
+ return codexHooksOk && integrationOk ? 0 : 1;
506
+ }
507
+ function cmdIntegrationsVerify(options, flags, cwd, write, writeErr) {
508
+ const targetInput = options.get('target');
509
+ const component = options.get('component');
510
+ if (targetInput === undefined || (component !== 'mcp' && component !== 'hooks')) {
511
+ writeErr('dz integrations-verify: requires --target <name> --component <mcp|hooks>');
512
+ return 1;
513
+ }
514
+ const resolution = resolveTargetName(targetInput);
515
+ if (resolution.kind === 'unknown') {
516
+ for (const line of formatTargetProblem('dz integrations-verify', resolution))
517
+ writeErr(line);
518
+ return 1;
519
+ }
520
+ if (resolution.via === 'alias')
521
+ writeErr(formatTargetAliasNote('dz integrations-verify', targetInput, resolution.target));
522
+ const result = runIntegrationsVerify({
523
+ target: resolution.target,
524
+ component,
525
+ projectRoot: resolve(cwd, options.get('project') ?? '.'),
526
+ });
527
+ if (flags.has('json'))
528
+ write(JSON.stringify(result));
529
+ else if (result.ok) {
530
+ write(`dz integrations-verify: ${resolution.target}/${component} registered (runtime ${result.runtimeVersion ?? 'unknown'}; ready=${result.registrations.every((row) => row.ready === true)})`);
531
+ }
532
+ else {
533
+ writeErr(`dz integrations-verify: ${resolution.target}/${component} REFUSED (${result.reasonCode ?? 'LIVE_PROBE_FAILED'}) — ${result.remediation ?? 'no qualifying receipt'}`);
534
+ }
535
+ return result.ok ? 0 : 1;
422
536
  }
423
537
  async function cmdVerify(options, cwd, write, writeErr) {
424
538
  const skillsDir = resolve(cwd, options.get('skills-dir') ?? '.claude/skills');
@@ -1870,6 +1984,150 @@ function cmdStatuslineFaRecord(options, cwd, write) {
1870
1984
  write(`dz statusline: recorded /feature-adr learning state for "${slug}" (${step}) — 🎓 ${state.pool} pool · ↑${state.recalled} used · +${state.stored} new · ↻${state.reinforced ?? 0} reinforced`);
1871
1985
  return 0;
1872
1986
  }
1987
+ function statuslineEtaStage(step) {
1988
+ const match = step.match(/\bStep\s*-?\s*(0|[1-9](?:\.5)?)(?:\b|\s)/i);
1989
+ if (match === null)
1990
+ return undefined;
1991
+ const value = Number(match[1]);
1992
+ if (value === 0)
1993
+ return 'router';
1994
+ if (value > 0 && value < 6)
1995
+ return 'design';
1996
+ if (value === 6)
1997
+ return 'plan';
1998
+ if (value === 7)
1999
+ return 'code';
2000
+ if (value === 8)
2001
+ return 'qe';
2002
+ if (value === 9)
2003
+ return 'fleet';
2004
+ return undefined;
2005
+ }
2006
+ function routerMetadata(router) {
2007
+ const result = router?.result;
2008
+ if (result === null || typeof result !== 'object')
2009
+ return {};
2010
+ const record = result;
2011
+ const tier = typeof record['tier'] === 'string' && record['tier'].length > 0 ? record['tier'] : undefined;
2012
+ const activeSteps = Array.isArray(record['activeSteps'])
2013
+ ? record['activeSteps'].filter((value) => typeof value === 'number' && Number.isFinite(value))
2014
+ : undefined;
2015
+ return {
2016
+ ...(tier !== undefined ? { tier } : {}),
2017
+ ...(activeSteps !== undefined ? { activeSteps } : {}),
2018
+ };
2019
+ }
2020
+ function activeCheckpointStages(tier, activeSteps) {
2021
+ const stages = new Set();
2022
+ if (activeSteps !== undefined) {
2023
+ for (const step of activeSteps) {
2024
+ if (step === 0)
2025
+ stages.add('router');
2026
+ else if (step > 0 && step < 6)
2027
+ stages.add('design');
2028
+ else if (step === 6)
2029
+ stages.add('plan');
2030
+ else if (step === 7)
2031
+ stages.add('code');
2032
+ else if (step === 8)
2033
+ stages.add('qe');
2034
+ else if (step === 9)
2035
+ stages.add('fleet');
2036
+ }
2037
+ }
2038
+ else if (tier !== undefined) {
2039
+ for (const stage of CHECKPOINT_STAGES) {
2040
+ if (stage !== 'fleet' || tier === 'L' || tier === 'XL')
2041
+ stages.add(stage);
2042
+ }
2043
+ }
2044
+ return CHECKPOINT_STAGES.filter((stage) => stages.has(stage));
2045
+ }
2046
+ /** All checkpoint I/O for ETA lives in this CLI-only wrapper and degrades per file. */
2047
+ function readEtaCorpus(projectRoot, currentSlug) {
2048
+ const trace = process.env['DZ_ETA_TRACE'];
2049
+ if (trace !== undefined && trace.length > 0) {
2050
+ try {
2051
+ appendFileSync(trace, `${JSON.stringify({ projectRoot, currentSlug, ts: new Date().toISOString() })}\n`);
2052
+ }
2053
+ catch { /* a test/debug receipt can never take down the statusline */ }
2054
+ }
2055
+ const samples = [];
2056
+ let currentSegments = [];
2057
+ let hasCurrentCheckpoints = false;
2058
+ let entries = [];
2059
+ try {
2060
+ entries = readdirSync(join(projectRoot, 'features'), { withFileTypes: true });
2061
+ }
2062
+ catch {
2063
+ return {
2064
+ history: [], currentRunSamples: [], currentTier: undefined, activeStages: [],
2065
+ lastCheckpointTsMs: undefined, hasCurrentCheckpoints: false,
2066
+ };
2067
+ }
2068
+ for (const entry of entries) {
2069
+ if (!entry.isDirectory())
2070
+ continue;
2071
+ const checkpointPath = join(projectRoot, 'features', entry.name, '.fa-state', 'checkpoints.jsonl');
2072
+ try {
2073
+ const text = readFileSync(checkpointPath, 'utf8');
2074
+ const observations = parseCheckpointLines(text, entry.name);
2075
+ const segments = segmentRun(observations);
2076
+ samples.push(...extractStageSamples(segments));
2077
+ if (entry.name === currentSlug) {
2078
+ hasCurrentCheckpoints = observations.length > 0;
2079
+ currentSegments = segments;
2080
+ }
2081
+ }
2082
+ catch {
2083
+ // Missing, unreadable, or racing append: this run contributes no evidence.
2084
+ }
2085
+ }
2086
+ const lastSegment = currentSegments.at(-1);
2087
+ const currentRouter = lastSegment?.router;
2088
+ const invocationSegments = currentRouter === undefined
2089
+ ? []
2090
+ : currentSegments.filter((segment) => segment.router === currentRouter);
2091
+ const invocationRunIds = new Set(invocationSegments.map((segment) => segment.runId));
2092
+ const currentRunSamples = extractStageSamples(invocationSegments);
2093
+ const metadata = routerMetadata(currentRouter);
2094
+ const currentTimestamps = invocationSegments
2095
+ .flatMap((segment) => segment.observations)
2096
+ .flatMap((observation) => observation.tsMs === undefined ? [] : [observation.tsMs]);
2097
+ return {
2098
+ history: samples.filter((sample) => !invocationRunIds.has(sample.runId)),
2099
+ currentRunSamples,
2100
+ currentTier: metadata.tier,
2101
+ activeStages: activeCheckpointStages(metadata.tier, metadata.activeSteps),
2102
+ lastCheckpointTsMs: currentTimestamps.length > 0 ? Math.max(...currentTimestamps) : undefined,
2103
+ hasCurrentCheckpoints,
2104
+ };
2105
+ }
2106
+ function statuslineEta(projectRoot, state, nowMs) {
2107
+ const currentStage = statuslineEtaStage(state.step);
2108
+ if (currentStage === undefined)
2109
+ return undefined;
2110
+ const corpus = readEtaCorpus(projectRoot, state.slug);
2111
+ const currentIndex = CHECKPOINT_STAGES.indexOf(currentStage);
2112
+ const remainingStages = corpus.activeStages.filter((stage) => CHECKPOINT_STAGES.indexOf(stage) >= currentIndex && stage !== 'router');
2113
+ // Preserve honest absence as a machine-readable union member. The text formatter intentionally
2114
+ // omits both variants, but `--json` must still distinguish no file from a file with no tier.
2115
+ const stagesForEstimate = remainingStages.length > 0
2116
+ ? remainingStages
2117
+ : (currentStage === 'router' ? [] : [currentStage]);
2118
+ if (stagesForEstimate.length === 0 && corpus.hasCurrentCheckpoints && corpus.currentTier !== undefined)
2119
+ return undefined;
2120
+ return estimateEta({
2121
+ samples: corpus.history,
2122
+ currentTier: corpus.currentTier,
2123
+ currentStage,
2124
+ remainingStages: stagesForEstimate,
2125
+ currentRunSamples: corpus.currentRunSamples,
2126
+ nowMs,
2127
+ ...(corpus.lastCheckpointTsMs !== undefined ? { lastCheckpointTsMs: corpus.lastCheckpointTsMs } : {}),
2128
+ hasCurrentCheckpoints: corpus.hasCurrentCheckpoints,
2129
+ });
2130
+ }
1873
2131
  /**
1874
2132
  * `dz statusline` — render dz's OWN self-learning counts as one compact, emoji-tagged line for
1875
2133
  * Claude Code's status bar (modeled on agentic-qe's "🎓 12 patterns"). Claude Code pipes a JSON
@@ -1889,8 +2147,23 @@ function cmdStatusline(options, flags, cwd, write, readStdin) {
1889
2147
  try {
1890
2148
  const projectRoot = statuslineProjectRoot(readStdin(), options, cwd);
1891
2149
  const data = statuslineData(projectRoot);
2150
+ const fa = data.featureAdr;
2151
+ let eta;
2152
+ let etaFragment;
2153
+ if (fa !== undefined && fa.kind !== 'loop') {
2154
+ try {
2155
+ eta = statuslineEta(projectRoot, fa, Date.now());
2156
+ if (eta !== undefined)
2157
+ etaFragment = formatEta(eta);
2158
+ }
2159
+ catch {
2160
+ // ETA is advisory: any corpus/resource failure omits only this fragment.
2161
+ eta = undefined;
2162
+ etaFragment = undefined;
2163
+ }
2164
+ }
1892
2165
  if (flags.has('json')) {
1893
- write(JSON.stringify(data));
2166
+ write(JSON.stringify({ ...data, ...(eta !== undefined ? { eta } : {}) }));
1894
2167
  return 0;
1895
2168
  }
1896
2169
  let line = `🎓 dz: ${data.patterns} patterns${data.usedPatterns !== undefined ? ` · ${data.usedPatterns} used` : ''} · 🧠 ${data.brainSources} sources`;
@@ -1900,14 +2173,13 @@ function cmdStatusline(options, flags, cwd, write, readStdin) {
1900
2173
  if (data.consolidatedAgeH !== undefined)
1901
2174
  line += ` · ⟳ ${data.consolidatedAgeH}h`;
1902
2175
  // Live /feature-adr run in flight → PREPEND the pipeline learning segment to the base dz line.
1903
- const fa = data.featureAdr;
1904
2176
  if (fa !== undefined) {
1905
2177
  // The producer marker reached data and arbitration in a previous round but not this label, so the bar asserted a pipeline that was not running.
1906
2178
  if (fa.kind === 'loop') {
1907
2179
  line = `🔁 loop ${fa.step} · ${line}`;
1908
2180
  }
1909
2181
  else {
1910
- line = `📐 feature-adr ${fa.step} · 🎓 ${fa.pool} pool · ↑${fa.recalled} used · +${fa.stored} new · ↻${fa.reinforced ?? 0} reinforced · ${line}`;
2182
+ line = `📐 feature-adr ${fa.step} · ${etaFragment !== undefined ? `${etaFragment} · ` : ''}🎓 ${fa.pool} pool · ↑${fa.recalled} used · +${fa.stored} new · ↻${fa.reinforced ?? 0} reinforced · ${line}`;
1911
2183
  }
1912
2184
  }
1913
2185
  write(line);
@@ -2494,7 +2766,12 @@ function learningStoreLine(storeRoot, projectOption, target, verb) {
2494
2766
  const reason = teachReasonPhrase(target.reason);
2495
2767
  return storeLocationLine(describeStoreLocation(storeRoot, projectOption, target.store === 'global' ? 'global' : undefined), verb) + (reason ? ' [' + reason + ']' : '');
2496
2768
  }
2497
- async function cmdTeach(options, flags, cwd, write) {
2769
+ async function runTeachGuardReinforcement(projectRoot, dzId, reward) {
2770
+ const backend = resolveLearningBackend(projectRoot);
2771
+ backend.addSample({ dzId, kind: 'reinforce', reward, ts: new Date().toISOString() });
2772
+ return backend.train();
2773
+ }
2774
+ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { console.error(line); }, interactive = false, guardRunner = teachGuard, reinforceRunner = runTeachGuardReinforcement) {
2498
2775
  // WHICH store this lesson belongs to, and WHO decided (teach-chooses-its-store).
2499
2776
  // `--to` → `DZ_LEARN` → `.dz/config.json` learning.teachTo → project. The owner asked for a
2500
2777
  // per-session choice; for a CLI every invocation is a fresh process, so the only honest session
@@ -2585,26 +2862,39 @@ async function cmdTeach(options, flags, cwd, write) {
2585
2862
  write('dz teach --from-json: expected a JSON array (produced by `dz recall --all --json`)');
2586
2863
  return 1;
2587
2864
  }
2588
- const existing = new Set(loadStorePatternsSync(storeRoot).map((p) => p.pattern));
2865
+ const importedKey = (p) => p.lessonForm !== undefined && p.lessonPairId !== undefined
2866
+ ? `${p.pattern}\u0000${p.lessonForm}\u0000${p.lessonPairId}`
2867
+ : `legacy\u0000${p.pattern}`;
2868
+ const existing = new Set(loadStorePatternsSync(storeRoot).map(importedKey));
2589
2869
  let imported = 0;
2590
2870
  let skipped = 0;
2591
2871
  const importedRecs = [];
2592
2872
  for (const item of parsed) {
2593
2873
  const p = item;
2594
- if (!p || typeof p.pattern !== 'string' || p.pattern.trim() === '' || existing.has(p.pattern)) {
2874
+ if (!p || typeof p !== 'object') {
2595
2875
  skipped += 1;
2596
2876
  continue;
2597
2877
  }
2598
- const rec = {
2599
- pattern: p.pattern,
2878
+ const pair = (p.lessonForm === 'specific' || p.lessonForm === 'class')
2879
+ && typeof p.lessonPairId === 'string' && p.lessonPairId !== ''
2880
+ ? { lessonForm: p.lessonForm, lessonPairId: p.lessonPairId }
2881
+ : {};
2882
+ const candidate = {
2883
+ pattern: typeof p.pattern === 'string' ? p.pattern : '',
2600
2884
  type: (typeof p.type === 'string' ? p.type : 'lesson-learned'),
2601
2885
  reward: typeof p.reward === 'number' ? Math.max(0, Math.min(1, p.reward)) : 0.8,
2602
2886
  domain: typeof p.domain === 'string' ? p.domain : 'general',
2603
2887
  ts: typeof p.ts === 'string' ? p.ts : new Date().toISOString(),
2604
2888
  source: 'dz-teach-import',
2889
+ ...pair,
2605
2890
  };
2891
+ if (candidate.pattern.trim() === '' || existing.has(importedKey(candidate))) {
2892
+ skipped += 1;
2893
+ continue;
2894
+ }
2895
+ const rec = candidate;
2606
2896
  await recordPattern(storeRoot, rec);
2607
- existing.add(p.pattern);
2897
+ existing.add(importedKey(rec));
2608
2898
  importedRecs.push(rec);
2609
2899
  imported += 1;
2610
2900
  }
@@ -2687,23 +2977,32 @@ async function cmdTeach(options, flags, cwd, write) {
2687
2977
  }
2688
2978
  const reward = parseFloat(options.get('reward') ?? '0.8');
2689
2979
  const domain = options.get('domain') ?? 'general';
2980
+ const classWasRequested = options.has('class-form') || flags.has('class-form');
2981
+ const lessonForms = normalizeLessonForms(pattern, options.get('class-form'));
2982
+ let guardedExisting;
2690
2983
  if (flags.has('guard')) {
2691
- const verdict = await teachGuard(storeRoot, pattern, { reward: Math.max(0, Math.min(1, reward)) });
2984
+ const verdict = await guardRunner(storeRoot, pattern, { reward: Math.max(0, Math.min(1, reward)) });
2692
2985
  if (verdict.action === 'reinforce') {
2693
- const backend = resolveLearningBackend(storeRoot);
2694
- backend.addSample({ dzId: verdict.dzId, kind: 'reinforce', reward: Math.max(0, Math.min(1, reward)), ts: new Date().toISOString() });
2695
- const trained = await backend.train();
2696
- // HIGH-fix: only claim success when the reinforce actually FLUSHED. With backend 'off'
2697
- // (NoopLearningBackend) or a flush failure, flushed === 0 — falling through to the plain
2698
- // teach below so the lesson is NEVER silently discarded (the exact silent-drop the ADR forbids).
2699
- if (trained.flushed > 0) {
2700
- write(`↳ reinforced existing pattern ${verdict.dzId} (cos=${verdict.cosine.toFixed(2)}) — not re-added`);
2701
- const clearedQ = clearAgentdbQuarantine(storeRoot, [verdict.dzId]);
2702
- if (clearedQ.cleared > 0)
2703
- write(' ↳ promoted out of quarantine (mirror updated)');
2704
- return 0;
2986
+ if (lessonForms.classForm !== undefined) {
2987
+ const existing = loadStoreRecords(storeRoot).find((record) => record.id === verdict.dzId);
2988
+ if (existing !== undefined) {
2989
+ guardedExisting = { dzId: verdict.dzId, cosine: verdict.cosine, pattern: recordToPattern(existing) };
2990
+ }
2991
+ else {
2992
+ write(`dz teach --guard: matched pattern ${verdict.dzId} was not found in the lexical store — teaching the lesson normally`);
2993
+ }
2994
+ }
2995
+ else {
2996
+ const trained = await reinforceRunner(storeRoot, verdict.dzId, Math.max(0, Math.min(1, reward)));
2997
+ if (trained.flushed > 0) {
2998
+ write(`↳ reinforced existing pattern ${verdict.dzId} (cos=${verdict.cosine.toFixed(2)}) — not re-added`);
2999
+ const clearedQ = clearAgentdbQuarantine(storeRoot, [verdict.dzId]);
3000
+ if (clearedQ.cleared > 0)
3001
+ write(' ↳ promoted out of quarantine (mirror updated)');
3002
+ return 0;
3003
+ }
3004
+ write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
2705
3005
  }
2706
- write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
2707
3006
  }
2708
3007
  }
2709
3008
  // Distill pattern into actionable rule (claude-smart inspired)
@@ -2716,8 +3015,8 @@ async function cmdTeach(options, flags, cwd, write) {
2716
3015
  // Typed through the shared schema (harness-core owns PatternRecord) so the
2717
3016
  // write side and the read side (recommend's loadPatterns) can never drift —
2718
3017
  // a field rename here is a compile error, not a silently re-muted loop (audit #2).
2719
- const entry = {
2720
- pattern,
3018
+ const entry = guardedExisting?.pattern ?? {
3019
+ pattern: lessonForms.specific,
2721
3020
  type: type,
2722
3021
  reward: Math.max(0, Math.min(1, reward)),
2723
3022
  domain,
@@ -2729,9 +3028,47 @@ async function cmdTeach(options, flags, cwd, write) {
2729
3028
  // total count. The lossy `npx agentdb add` dual-write was removed in Tier-1 (audit #6).
2730
3029
  // lesson-quarantine (opt-in): a fresh lesson is a HYPOTHESIS until it earns promotion.
2731
3030
  const quarantineOn = readMemoryLearningConfig(storeRoot).quarantine;
2732
- const count = await recordPattern(storeRoot, entry, quarantineOn ? { quarantine: true } : {});
2733
- write(`Learned: "${pattern.slice(0, 60)}${pattern.length > 60 ? '...' : ''}"`);
2734
- write(` Domain: ${domain} Reward: ${reward} Backend: memory (@dzhechkov/memory)`);
3031
+ const stored = await recordLessonForms(storeRoot, entry, lessonForms.classForm, quarantineOn ? { quarantine: true } : {});
3032
+ let recordsToMirror = stored.records;
3033
+ let commandFailed = stored.class === 'failed';
3034
+ let reinforced = false;
3035
+ if (guardedExisting !== undefined) {
3036
+ let reinforceId = guardedExisting.dzId;
3037
+ if (stored.class === 'stored') {
3038
+ const specificRow = stored.records.find((row) => row.lessonForm === 'specific');
3039
+ if (specificRow !== undefined)
3040
+ reinforceId = patternRecordId(specificRow);
3041
+ if (reinforceId !== guardedExisting.dzId) {
3042
+ const removed = removePatternsByIds(storeRoot, new Set([guardedExisting.dzId]));
3043
+ if (removed.error !== undefined || removed.removed === 0) {
3044
+ commandFailed = true;
3045
+ writeErr(`dz teach --guard: class pair stored, but old pattern cleanup failed${removed.error === undefined ? '' : ` — ${removed.error}`}`);
3046
+ }
3047
+ }
3048
+ }
3049
+ else if (stored.class === 'failed') {
3050
+ const partialSpecific = stored.records.find((row) => row.lessonForm === 'specific');
3051
+ if (partialSpecific !== undefined && patternRecordId(partialSpecific) !== guardedExisting.dzId) {
3052
+ const rolledBack = removePatternsByIds(storeRoot, new Set([patternRecordId(partialSpecific)]));
3053
+ if (rolledBack.error !== undefined)
3054
+ writeErr(`dz teach --guard: partial enrichment rollback failed — ${rolledBack.error}`);
3055
+ }
3056
+ recordsToMirror = [];
3057
+ reinforceId = guardedExisting.dzId;
3058
+ }
3059
+ if (!commandFailed) {
3060
+ const trained = await reinforceRunner(storeRoot, reinforceId, Math.max(0, Math.min(1, reward)));
3061
+ reinforced = trained.flushed > 0;
3062
+ if (reinforced) {
3063
+ const clearedQ = clearAgentdbQuarantine(storeRoot, [reinforceId]);
3064
+ if (clearedQ.cleared > 0)
3065
+ write(' ↳ promoted out of quarantine (mirror updated)');
3066
+ }
3067
+ }
3068
+ }
3069
+ const count = guardedExisting === undefined ? stored.count : loadStorePatternsSync(storeRoot).length;
3070
+ write(`${guardedExisting !== undefined && stored.class === 'stored' ? 'Enriched' : 'Learned'}: "${pattern.slice(0, 60)}${pattern.length > 60 ? '...' : ''}"`);
3071
+ write(` Domain: ${entry.domain} Reward: ${entry.reward} Backend: memory (@dzhechkov/memory)`);
2735
3072
  write(` Total patterns: ${count}`);
2736
3073
  // WHERE the write landed. MEASURED before this line existed: teach printed the pattern, the
2737
3074
  // domain, the reward and the backend — and not one word about the path, so a store written to
@@ -2741,6 +3078,24 @@ async function cmdTeach(options, flags, cwd, write) {
2741
3078
  // sees a path, cannot tell what chose it, and has no reason to question it. `default` adds
2742
3079
  // nothing, so the line stays byte-identical for everyone who set nothing.
2743
3080
  write(storeLine('written'));
3081
+ if (stored.class === 'stored')
3082
+ write(' Class form: stored separately and linked to the specific lesson');
3083
+ if (classWasRequested && stored.class === 'absent')
3084
+ write(' Class form: skipped; the specific lesson was saved');
3085
+ if (stored.class === 'rejected')
3086
+ writeErr(`dz teach: class form rejected — ${stored.reason ?? 'invalid class form'}; the specific lesson was saved`);
3087
+ if (stored.class === 'failed')
3088
+ writeErr(`dz teach: specific stored; class failed — ${stored.reason ?? 'unknown storage failure'}`);
3089
+ if (guardedExisting !== undefined && stored.class === 'stored') {
3090
+ write(commandFailed
3091
+ ? ' Guard: class enrichment stored; reinforcement skipped because old-pattern cleanup failed'
3092
+ : reinforced
3093
+ ? ` Guard: enriched existing pattern (cos=${guardedExisting.cosine.toFixed(2)}), then reinforced the linked specific form`
3094
+ : ' Guard: class enrichment stored; reinforcement did not flush');
3095
+ }
3096
+ if (!classWasRequested && interactive) {
3097
+ write(' Rule of one place or of a class? Optional: add --class-form when a future reader needs the why for structure, interfaces, or maintainability; skip when scope, risk, time, and cost are low or standards, policy, or documentation already cover it.');
3098
+ }
2744
3099
  // ADVICE, not a gate. Someone putting medical lessons in a shared store owns both
2745
3100
  // directories and this binary; refusing would be defending a user against themselves,
2746
3101
  // which this design does not attempt. Making the choice INFORMED is the part that is
@@ -2759,8 +3114,8 @@ async function cmdTeach(options, flags, cwd, write) {
2759
3114
  write(' ⚠ quarantined: excluded from auto-inject, damped in recall — promote by confirming it (dz teach --reinforce "<text>") or dz recall --promote <dzId> --apply');
2760
3115
  }
2761
3116
  // The lexical write above is durable — the vector mirror is strictly best-effort (I-3).
2762
- await emitMirrorQ(storeRoot, [entry], 'dz-teach', quarantineOn);
2763
- return 0;
3117
+ await emitMirrorQ(storeRoot, recordsToMirror, 'dz-teach', quarantineOn);
3118
+ return commandFailed ? 1 : 0;
2764
3119
  }
2765
3120
  async function cmdConsolidate(options, flags, cwd, write) {
2766
3121
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
@@ -3120,7 +3475,7 @@ async function cmdRecallPromote(options, flags, projectRoot, write) {
3120
3475
  write(` not found: ${res.notFound.join(', ')}`);
3121
3476
  return 0;
3122
3477
  }
3123
- async function cmdRecall(options, flags, cwd, write) {
3478
+ async function cmdRecall(options, flags, cwd, write, writeErr, classMatcher) {
3124
3479
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
3125
3480
  const asJson = flags.has('json');
3126
3481
  const all = flags.has('all');
@@ -3141,7 +3496,15 @@ async function cmdRecall(options, flags, cwd, write) {
3141
3496
  // review could not make correct: a tag set by the writer is decidable, prose is not.
3142
3497
  const allPatterns = loadStorePatternsSync(projectRoot);
3143
3498
  const holdout = applyExportHoldout(allPatterns, heldOutAfterOptIn(options.get('include-domain')));
3144
- const patterns = holdout.exported;
3499
+ const classByPair = new Map(holdout.exported
3500
+ .filter((row) => row.lessonForm === 'class' && row.lessonPairId !== undefined)
3501
+ .map((row) => [row.lessonPairId, row.pattern]));
3502
+ const patterns = holdout.exported.map((row) => {
3503
+ const classForm = row.lessonForm === 'specific' && row.lessonPairId !== undefined
3504
+ ? classByPair.get(row.lessonPairId)
3505
+ : undefined;
3506
+ return classForm === undefined ? row : { ...row, classForm };
3507
+ });
3145
3508
  const holdoutNote = renderHoldoutNote(holdout);
3146
3509
  // The opt-in is honoured without argument — and named out loud. A flag that silently
3147
3510
  // includes medical lessons in a portable export is a flag whose consequence the user
@@ -3171,7 +3534,15 @@ async function cmdRecall(options, flags, cwd, write) {
3171
3534
  const rawStats = storeStats(projectRoot);
3172
3535
  const topHoldout = applyExportHoldout(rawStats.topUses, heldOutAfterOptIn(options.get('include-domain')));
3173
3536
  const perDomain = Object.fromEntries(Object.entries(rawStats.perDomain).filter(([d]) => !holdout.domains.includes(canonicalDomainKey(d))));
3174
- const stats = { ...rawStats, topUses: topHoldout.exported, perDomain };
3537
+ const generalizedPairs = new Set(patterns.flatMap((p) => p.lessonPairId === undefined ? [] : [p.lessonPairId]));
3538
+ const unpairedLessons = patterns.filter((p) => p.lessonPairId === undefined).length;
3539
+ const stats = {
3540
+ ...rawStats,
3541
+ topUses: topHoldout.exported,
3542
+ perDomain,
3543
+ generalized: generalizedPairs.size,
3544
+ logicalLessons: generalizedPairs.size + unpairedLessons,
3545
+ };
3175
3546
  const backendStats = resolveLearningBackend(projectRoot).getStats();
3176
3547
  if (asJson) {
3177
3548
  write(JSON.stringify({ patterns, stats, learning: backendStats, withheld: holdout.withheld.length, withheldDomains: holdout.domains }));
@@ -3185,6 +3556,7 @@ async function cmdRecall(options, flags, cwd, write) {
3185
3556
  write(` backend: ${backendStats.backend}${backendStats.advisory !== undefined ? ` (${backendStats.advisory})` : ''}`);
3186
3557
  write(` domains: ${Object.entries(stats.perDomain).map(([k, v]) => `${k}=${v}`).join(', ') || 'none'}`);
3187
3558
  write(` exact-dup groups: ${stats.exactDupGroups}`);
3559
+ write(` generalized: ${stats.generalized} of ${stats.logicalLessons} lessons`);
3188
3560
  write(` re-teach trend: ${stats.teachEvents} teach event(s), ${stats.reinforceEvents} reinforce event(s)`);
3189
3561
  write(' top uses:');
3190
3562
  for (const row of stats.topUses)
@@ -3312,6 +3684,10 @@ async function cmdRecall(options, flags, cwd, write) {
3312
3684
  ? 'lexical'
3313
3685
  : 'hybrid';
3314
3686
  const wantedDomain = options.get('domain');
3687
+ const classRecallOptions = {
3688
+ onClassDegraded: writeErr,
3689
+ ...(classMatcher === undefined ? {} : { classMatcher }),
3690
+ };
3315
3691
  // OVER-FETCH before boosting (Codex QE #5): the boost used to run on hits ALREADY
3316
3692
  // truncated to `limit`, so an exact-domain lesson sitting at rank limit+1 could
3317
3693
  // never receive its promised lift — the feature was weakest in exactly the case it
@@ -3334,8 +3710,8 @@ async function cmdRecall(options, flags, cwd, write) {
3334
3710
  // second occurrence. Foreign stdout is routed to stderr for the duration of the engine call — our
3335
3711
  // own output is written after it returns.
3336
3712
  const result = asJson
3337
- ? await withForeignStdoutOnStderr(() => recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
3338
- : await recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
3713
+ ? await withForeignStdoutOnStderr(() => recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
3714
+ : await recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
3339
3715
  if (mode === 'semantic' && result.vectorEngine === 'none') {
3340
3716
  // --semantic is an explicit ask — degrading it silently would be dishonest (FR-3).
3341
3717
  const why = result.vectorReason ?? 'no vector engine available — run: dz setup --memory agentdb';
@@ -3369,13 +3745,27 @@ async function cmdRecall(options, flags, cwd, write) {
3369
3745
  let globalHits = [];
3370
3746
  if (readGlobal) {
3371
3747
  const g = asJson
3372
- ? await withForeignStdoutOnStderr(() => recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
3373
- : await recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
3748
+ ? await withForeignStdoutOnStderr(() => recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
3749
+ : await recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
3374
3750
  globalHits = g.hits;
3375
3751
  }
3752
+ const projectHits = boost ? boost.hits : result.hits;
3753
+ // TWO different keys on purpose, and they must not be unified. The cross-store MERGE key is the
3754
+ // lesson TEXT: the same lesson taught into the project store and the global store is one lesson
3755
+ // shown once, labelled `both`, and those two records legitimately differ in timestamp, reward and
3756
+ // domain. The matchedForm LOOKUP key may be narrower. Folding the merge key into
3757
+ // patternIdentityOf (which includes ts) made two `dz teach` calls of identical text stop
3758
+ // collapsing — it reddened the pre-existing P3 case in recall-reads-both-stores.test.ts.
3759
+ const mergeKey = (hit) => hit.pattern.pattern;
3760
+ const globalByIdentity = new Map(globalHits.map((hit) => [mergeKey(hit), hit]));
3376
3761
  const merged = readGlobal
3377
- ? mergeStoreHits(boost ? boost.hits : result.hits, globalHits, (h) => h.pattern.pattern)
3378
- : (boost ? boost.hits : result.hits);
3762
+ ? mergeStoreHits(projectHits, globalHits, mergeKey).map((hit) => {
3763
+ if (hit.origin !== 'both')
3764
+ return hit;
3765
+ const matchedForm = mergeLessonMatchedForms(hit.matchedForm, globalByIdentity.get(mergeKey(hit))?.matchedForm);
3766
+ return matchedForm === undefined ? hit : { ...hit, matchedForm };
3767
+ })
3768
+ : projectHits;
3379
3769
  const hits = merged.slice(0, limit);
3380
3770
  // Computed ONCE, honoured by EVERY return path. It used to live only on the text tail, so the two
3381
3771
  // paths that return earlier — `--json` and the zero-hits branch — still reported success. That
@@ -3422,6 +3812,7 @@ async function cmdRecall(options, flags, cwd, write) {
3422
3812
  // version emitted the number while its own comment promised null (found by independent review).
3423
3813
  write(JSON.stringify(hits.map((h) => ({
3424
3814
  ...h.pattern,
3815
+ ...(h.matchedForm === undefined ? {} : { matchedForm: h.matchedForm }),
3425
3816
  relevance: boost === null && 'score' in h && typeof h.score === 'number' ? h.score : null,
3426
3817
  // The TRUE cosine, as a second companion key — the design named it and the first ship missed
3427
3818
  // it, so a scripted consumer STILL could not threshold (found while recalibrating the floors:
@@ -3488,6 +3879,10 @@ async function cmdRecall(options, flags, cwd, write) {
3488
3879
  // guessing which second one answered.
3489
3880
  write(` store (read): ${join(globalRoot, '.dz')} [cross-project]`);
3490
3881
  }
3882
+ const classMatches = hits.filter((h) => h.matchedForm === 'class' || h.matchedForm === 'both').length;
3883
+ if (hits.some((h) => h.pattern.classForm !== undefined)) {
3884
+ write(` class-form matches: ${classMatches} of ${hits.length}`);
3885
+ }
3491
3886
  let sawQuarantined = false;
3492
3887
  for (const h of hits) {
3493
3888
  const backendTag = vectorOn ? ` ⟨${h.backend}⟩` : '';
@@ -3513,9 +3908,23 @@ async function cmdRecall(options, flags, cwd, write) {
3513
3908
  // 160, not 80: at 80 characters the evidence a reader needs to judge relevance sits in the
3514
3909
  // hidden remainder, and the cosine then appears to describe the visible fragment rather than
3515
3910
  // the whole lesson. `--full` prints it all, still on one line.
3516
- const width = flags.has('full') ? Number.POSITIVE_INFINITY : 160;
3517
- const oneLined = oneLine(h.pattern.pattern);
3518
- const shown = width === Number.POSITIVE_INFINITY ? oneLined : oneLined.slice(0, width);
3911
+ const specificText = oneLine(h.pattern.pattern);
3912
+ const classText = h.pattern.classForm === undefined ? undefined : oneLine(h.pattern.classForm);
3913
+ let shown = specificText;
3914
+ if (classText !== undefined) {
3915
+ const suffix = ` [match: ${h.matchedForm ?? 'specific'}]`;
3916
+ if (flags.has('full')) {
3917
+ shown = `specific: ${specificText} · class: ${classText}${suffix}`;
3918
+ }
3919
+ else {
3920
+ const contentWidth = Math.max(2, 160 - 'specific: '.length - ' · class: '.length - suffix.length);
3921
+ const specificWidth = Math.floor(contentWidth / 2);
3922
+ shown = `specific: ${specificText.slice(0, specificWidth)} · class: ${classText.slice(0, contentWidth - specificWidth)}${suffix}`;
3923
+ }
3924
+ }
3925
+ else if (!flags.has('full')) {
3926
+ shown = shown.slice(0, 160);
3927
+ }
3519
3928
  // WHICH store this hit came from. A merged list that does not say re-creates the fragmentation
3520
3929
  // blindness the store-location line just removed, one level down: the reader would see more
3521
3930
  // results and have no way to tell whether the global store is even connected.
@@ -4489,6 +4898,28 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4489
4898
  writeErr(formatTargetAliasNote('dz setup', targetOpt, target));
4490
4899
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
4491
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
+ }
4492
4923
  write(`\n╔══════════════════════════════════════════════════════╗`);
4493
4924
  write(`║ DZ SETUP — Full Environment ║`);
4494
4925
  write(`╠══════════════════════════════════════════════════════╣`);
@@ -4524,7 +4955,13 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4524
4955
  ? selectArg.split(',').map((s) => s.trim()).filter((s) => s.length > 0)
4525
4956
  : getPreset(preset)?.skills;
4526
4957
  const install = select !== undefined && select.length > 0
4527
- ? await installSkills({ target, projectRoot, cwd, explicitSkillsDir: options.get('skills-dir'), select, force: flags.has('force'), enrich: flags.has('enrich') })
4958
+ ? await installSkills({
4959
+ target, projectRoot, cwd, explicitSkillsDir: options.get('skills-dir'), select,
4960
+ force: flags.has('force'), enrich: flags.has('enrich'), noHooks: flags.has('no-hooks'),
4961
+ noIntegrations: flags.has('no-integrations'),
4962
+ noVerify: flags.has('no-verify'),
4963
+ ...(options.get('allow-integrations') !== undefined ? { allowIntegrations: options.get('allow-integrations') } : {}),
4964
+ })
4528
4965
  : undefined;
4529
4966
  if (install) {
4530
4967
  write(`║ ${String(install.results.length).padStart(2)} skill(s), ${String(install.written).padStart(3)} file(s) written${' '.repeat(15)}║`);
@@ -4534,6 +4971,7 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4534
4971
  }
4535
4972
  // Step 5 (ADR-001 §8): DELIVER the codex hooks and verify them live. Non-aborting — the rest of
4536
4973
  // setup has already run and the summary still prints; only the exit code carries the failure.
4974
+ let setupIntegrationOutcomes = install?.integrations ?? [];
4537
4975
  let codexHooksOk = true;
4538
4976
  if (target === 'codex' && !flags.has('no-hooks')) {
4539
4977
  write(`║ 5. Delivering codex hooks (live verify)... ║`);
@@ -4543,6 +4981,11 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4543
4981
  write(line);
4544
4982
  for (const line of delivery.stderr)
4545
4983
  writeErr(line);
4984
+ const hookIndex = setupIntegrationOutcomes.findIndex((row) => row.component === 'hooks' && row.status !== 'not-requested');
4985
+ if (hookIndex !== -1) {
4986
+ const hook = normalizeCodexHookOutcome(setupIntegrationOutcomes[hookIndex], delivery, flags.has('no-verify'));
4987
+ setupIntegrationOutcomes = setupIntegrationOutcomes.map((row, index) => index === hookIndex ? hook : row);
4988
+ }
4546
4989
  }
4547
4990
  write(`╠══════════════════════════════════════════════════════╣`);
4548
4991
  write(`║ Setup: ${String(setupResult.completed).padStart(2)} done, ${String(setupResult.skipped).padStart(2)} skipped ║`);
@@ -4560,6 +5003,9 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4560
5003
  if (install) {
4561
5004
  write(`dz setup: ${install.results.length} skill(s), ${install.written} file(s) written, ${install.skipped} skipped`
4562
5005
  + (install.dirsSearched > 1 ? ` (searched ${install.dirsSearched} skill dirs)` : ''));
5006
+ for (const outcome of setupIntegrationOutcomes) {
5007
+ write(`dz setup integration ${outcome.component}: ${outcome.status.toUpperCase()}${outcome.reasonCode ? ` (${outcome.reasonCode})` : ''}`);
5008
+ }
4563
5009
  writeMissingSkillsHint(write, install.missing, selectArg !== undefined ? undefined : preset);
4564
5010
  }
4565
5011
  // A hook that was written but never witnessed firing is NOT a completed setup (ADR-002 §5): the
@@ -4574,7 +5020,8 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4574
5020
  if (erroredSteps.length > 0) {
4575
5021
  write(`\n✗ setup reported ${erroredSteps.length} failed step(s): ${erroredSteps.join(', ')} — exit 1`);
4576
5022
  }
4577
- return codexHooksOk && erroredSteps.length === 0 ? 0 : 1;
5023
+ const integrationsOk = !setupIntegrationOutcomes.some((row) => row.status === 'refused');
5024
+ return codexHooksOk && erroredSteps.length === 0 && integrationsOk ? 0 : 1;
4578
5025
  }
4579
5026
  function cmdPretrain(options, cwd, write) {
4580
5027
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
@@ -4614,7 +5061,7 @@ function cmdPretrain(options, cwd, write) {
4614
5061
  write(`╚══════════════════════════════════════════════════════════════╝`);
4615
5062
  return 0;
4616
5063
  }
4617
- function cmdRecommend(options, cwd, write) {
5064
+ function cmdRecommend(options, flags, cwd, write) {
4618
5065
  const task = options.get('_positional_0');
4619
5066
  if (!task) {
4620
5067
  write('dz recommend: task description required');
@@ -4623,15 +5070,40 @@ function cmdRecommend(options, cwd, write) {
4623
5070
  }
4624
5071
  const registry = buildRegistry(cwd);
4625
5072
  const report = recommend(task, registry, cwd);
5073
+ if (flags.has('json')) {
5074
+ write(JSON.stringify(report, null, 2));
5075
+ return 0;
5076
+ }
4626
5077
  write(`\n╔══════════════════════════════════════════════════════════════╗`);
4627
5078
  write(`║ DZ RECOMMEND — Task Advisor ║`);
4628
5079
  write(`╠══════════════════════════════════════════════════════════════╣`);
4629
5080
  write(`║ Task: ${report.task.slice(0, 52).padEnd(52)}║`);
4630
- const topicSuffix = report.pretrainFallback ? ' (via pretrain)' : '';
4631
- 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
+ }
4632
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';
4633
5103
  if (report.presets.length > 0) {
4634
- write(`║ RECOMMENDED PRESETS ║`);
5104
+ write(stackDerived
5105
+ ? `║ PROJECT-STACK PRESETS ║`
5106
+ : `║ RECOMMENDED PRESETS ║`);
4635
5107
  for (const p of report.presets) {
4636
5108
  const matched = p.matchedSkills.length > 0 ? ` (${p.matchedSkills.slice(0, 3).join(', ')})` : '';
4637
5109
  write(`║ ${p.name.padEnd(15)} ${String(p.skills).padStart(2)} skills coverage: ${String(p.coverage).padStart(2)} topics${matched.padEnd(15)}║`);
@@ -4639,7 +5111,9 @@ function cmdRecommend(options, cwd, write) {
4639
5111
  write(`║${''.padEnd(62)}║`);
4640
5112
  }
4641
5113
  if (report.skills.length > 0) {
4642
- 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)}║`);
4643
5117
  for (const s of report.skills.slice(0, 8)) {
4644
5118
  const desc = s.description.length > 35 ? s.description.slice(0, 32) + '...' : s.description;
4645
5119
  write(`║ ${s.id.padEnd(24)} ${desc.padEnd(36)}║`);
@@ -4647,7 +5121,9 @@ function cmdRecommend(options, cwd, write) {
4647
5121
  write(`║${''.padEnd(62)}║`);
4648
5122
  }
4649
5123
  if (report.toolkits.length > 0) {
4650
- write(`║ FULL PIPELINE (npx toolkits) ║`);
5124
+ write(stackDerived
5125
+ ? `║ PROJECT-STACK PIPELINE (npx toolkits) ║`
5126
+ : `║ FULL PIPELINE (npx toolkits) ║`);
4651
5127
  for (const tk of report.toolkits) {
4652
5128
  const desc = tk.description.length > 44 ? tk.description.slice(0, 41) + '...' : tk.description;
4653
5129
  write(`║ ${tk.name.padEnd(16)} ${desc.padEnd(44)}║`);
@@ -4657,13 +5133,17 @@ function cmdRecommend(options, cwd, write) {
4657
5133
  write(`║${''.padEnd(62)}║`);
4658
5134
  }
4659
5135
  write(`╠══════════════════════════════════════════════════════════════╣`);
4660
- write(`║ STEP-BY-STEP PLAN ║`);
5136
+ write(stackDerived
5137
+ ? `║ PROJECT-STACK PLAN ║`
5138
+ : `║ STEP-BY-STEP PLAN ║`);
4661
5139
  for (const step of report.plan) {
4662
5140
  const line = step.length > 60 ? step.slice(0, 57) + '...' : step;
4663
5141
  write(`║ ${line.padEnd(58)}║`);
4664
5142
  }
4665
5143
  write(`╠══════════════════════════════════════════════════════════════╣`);
4666
- write(`║ QUICK INSTALL ║`);
5144
+ write(stackDerived
5145
+ ? `║ PROJECT-STACK QUICK INSTALL ║`
5146
+ : `║ QUICK INSTALL ║`);
4667
5147
  const cmd = report.installCommand.length > 58 ? report.installCommand.slice(0, 55) + '...' : report.installCommand;
4668
5148
  write(`║ ${cmd.padEnd(58)}║`);
4669
5149
  write(`╚══════════════════════════════════════════════════════════════╝`);
@@ -5228,6 +5708,9 @@ function cmdPublish(options, flags, cwd, write) {
5228
5708
  write(`dz publish: ⚠ guard warn — ${v.rule}: ${v.detail}`);
5229
5709
  else
5230
5710
  write('dz publish: ✓ guard pre-flight passed');
5711
+ for (const observation of guardResult.observations ?? []) {
5712
+ write(`dz publish: ℹ guard observation — ${observation.rule} ${observation.scope}: ${observation.detail} [${observation.status}]`);
5713
+ }
5231
5714
  for (const n of guardResult.notes ?? [])
5232
5715
  write(`dz publish: ℹ guard note — ${n}`); // FN-7: on the record, never blocking
5233
5716
  }
@@ -6421,6 +6904,81 @@ function looksBinaryText(text) {
6421
6904
  * per-file findings (each enriched with its `file`), and applies the exit-code contract.
6422
6905
  * `--json` ALWAYS emits valid JSON `{ok, findings, scanned}`, even on the failure path.
6423
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
+ }
6424
6982
  function cmdClaimCheck(options, _optionLists, flags, cwd, write) {
6425
6983
  // Reject (never silently coerce) an invalid --fail-on.
6426
6984
  const failOnRaw = options.get('fail-on') ?? 'high';
@@ -6904,6 +7462,26 @@ export function codexHooksSyncOptions(input) {
6904
7462
  verify: input.verify !== false,
6905
7463
  };
6906
7464
  }
7465
+ /** Map the retained Codex hook writer's one live verdict into the common integration contract. */
7466
+ export function normalizeCodexHookOutcome(base, delivery, noVerify = false) {
7467
+ if (delivery.report.ready) {
7468
+ return {
7469
+ target: 'codex', component: 'hooks', status: 'emitted',
7470
+ registrations: [{ id: 'dz-codex-hooks', scope: 'user', registered: delivery.report.installed, approval: delivery.report.trust === 'trusted' ? 'approved' : 'unknown', ready: true }],
7471
+ carrier: { scope: 'user', path: '$CODEX_HOME/hooks.json' },
7472
+ ...(delivery.report.codexVersion !== null ? { runtimeVersion: delivery.report.codexVersion } : {}),
7473
+ evidenceVersion: 'codex-hooks-live-v1',
7474
+ };
7475
+ }
7476
+ return {
7477
+ ...base,
7478
+ target: 'codex', component: 'hooks', status: 'refused',
7479
+ registrations: [{ id: 'dz-codex-hooks', scope: 'user', registered: delivery.report.installed, approval: delivery.report.trust === 'trusted' ? 'approved' : 'unknown', ready: false }],
7480
+ reasonCode: 'CURRENT_LIVE_CHECK_FAILED',
7481
+ remediation: noVerify ? '--no-verify cannot establish ready; rerun with live verification' : 'approve the managed hooks and rerun the live verification',
7482
+ applied: delivery.report.written || delivery.report.installed,
7483
+ };
7484
+ }
6907
7485
  /**
6908
7486
  * What the user is told about a sync report — the ONE place the success word can be printed.
6909
7487
  *
@@ -7221,6 +7799,430 @@ function gatherReadmeCounts(root) {
7221
7799
  sitePair('packages/@dzhechkov/sitedoc/src/content/docs/index.mdx', /^\s*tagline: "[^"]*?(\d+) commands/m, 'commands (docs-site index hero tagline vs cli All Commands)');
7222
7800
  return pairs;
7223
7801
  }
7802
+ const MAX_VOLUME_FILES_PER_SCOPE = 512;
7803
+ const MAX_VOLUME_BYTES_PER_SCOPE = 32 * 1024 * 1024;
7804
+ const CYRILLIC_CHARACTER = /\p{Script=Cyrillic}/u;
7805
+ function volumePathInside(root, candidate) {
7806
+ return candidate === root || candidate.startsWith(root + sep);
7807
+ }
7808
+ function cyrillicUtf8Bytes(buffer) {
7809
+ let bytes = 0;
7810
+ for (const character of buffer.toString('utf8')) {
7811
+ if (CYRILLIC_CHARACTER.test(character))
7812
+ bytes += Buffer.byteLength(character, 'utf8');
7813
+ }
7814
+ return bytes;
7815
+ }
7816
+ function gatherTemplateVolumeTarget(packageDir, target) {
7817
+ const templateRoot = join(packageDir, 'templates', '.claude');
7818
+ if (!existsSync(templateRoot))
7819
+ return undefined;
7820
+ const files = [];
7821
+ const realFiles = new Set();
7822
+ let totalBytes = 0;
7823
+ let failure;
7824
+ let packageReal;
7825
+ let templateReal;
7826
+ try {
7827
+ packageReal = realpathSync(packageDir);
7828
+ templateReal = realpathSync(templateRoot);
7829
+ if (!volumePathInside(packageReal, templateReal)) {
7830
+ failure = { reason: 'template-root-escape', detail: templateRoot };
7831
+ }
7832
+ }
7833
+ catch (error) {
7834
+ return {
7835
+ target,
7836
+ files,
7837
+ collection: { complete: false, reason: 'template-root-unreadable', detail: error instanceof Error ? error.message : String(error) },
7838
+ };
7839
+ }
7840
+ const fail = (reason, detail) => {
7841
+ failure ??= { reason, detail: detail.replace(/\s+/g, ' ').slice(0, 240) };
7842
+ };
7843
+ const addFile = (absolute, kind) => {
7844
+ if (failure !== undefined)
7845
+ return;
7846
+ if (files.length >= MAX_VOLUME_FILES_PER_SCOPE) {
7847
+ fail('template-file-cap-exceeded', `${MAX_VOLUME_FILES_PER_SCOPE} files`);
7848
+ return;
7849
+ }
7850
+ try {
7851
+ const stat = lstatSync(absolute);
7852
+ if (!stat.isFile()) {
7853
+ fail('template-input-not-regular', absolute);
7854
+ return;
7855
+ }
7856
+ const real = realpathSync(absolute);
7857
+ if (!volumePathInside(templateReal, real)) {
7858
+ fail('template-input-escape', absolute);
7859
+ return;
7860
+ }
7861
+ if (realFiles.has(real)) {
7862
+ fail('template-input-duplicate', absolute);
7863
+ return;
7864
+ }
7865
+ if (stat.size <= 0 || stat.size > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
7866
+ fail(stat.size <= 0 ? 'template-input-empty' : 'template-byte-cap-exceeded', absolute);
7867
+ return;
7868
+ }
7869
+ const content = readFileSync(absolute);
7870
+ if (content.byteLength <= 0 || content.byteLength > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
7871
+ fail(content.byteLength <= 0 ? 'template-input-empty' : 'template-byte-cap-exceeded', absolute);
7872
+ return;
7873
+ }
7874
+ realFiles.add(real);
7875
+ totalBytes += content.byteLength;
7876
+ files.push({
7877
+ path: relative(packageDir, absolute).split(sep).join('/'),
7878
+ kind,
7879
+ bytes: content.byteLength,
7880
+ cyrillicUtf8Bytes: cyrillicUtf8Bytes(content),
7881
+ });
7882
+ }
7883
+ catch (error) {
7884
+ fail('template-input-unreadable', `${absolute}: ${error instanceof Error ? error.message : String(error)}`);
7885
+ }
7886
+ };
7887
+ const scanFlat = (name) => {
7888
+ const dir = join(templateRoot, name);
7889
+ if (!existsSync(dir) || failure !== undefined)
7890
+ return;
7891
+ try {
7892
+ for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
7893
+ if (!entry.name.endsWith('.md'))
7894
+ continue;
7895
+ addFile(join(dir, entry.name), name);
7896
+ }
7897
+ }
7898
+ catch (error) {
7899
+ fail('template-directory-unreadable', `${dir}: ${error instanceof Error ? error.message : String(error)}`);
7900
+ }
7901
+ };
7902
+ const scanSkills = (dir) => {
7903
+ if (!existsSync(dir) || failure !== undefined)
7904
+ return;
7905
+ let entries;
7906
+ try {
7907
+ entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
7908
+ }
7909
+ catch (error) {
7910
+ fail('template-directory-unreadable', `${dir}: ${error instanceof Error ? error.message : String(error)}`);
7911
+ return;
7912
+ }
7913
+ for (const entry of entries) {
7914
+ if (failure !== undefined)
7915
+ return;
7916
+ const absolute = join(dir, entry.name);
7917
+ if (entry.isSymbolicLink()) {
7918
+ fail('template-input-not-regular', absolute);
7919
+ return;
7920
+ }
7921
+ if (entry.isDirectory())
7922
+ scanSkills(absolute);
7923
+ else if (entry.name === 'SKILL.md')
7924
+ addFile(absolute, 'skills');
7925
+ }
7926
+ };
7927
+ scanFlat('rules');
7928
+ scanFlat('commands');
7929
+ scanSkills(join(templateRoot, 'skills'));
7930
+ files.sort((a, b) => a.path.localeCompare(b.path));
7931
+ return {
7932
+ target,
7933
+ files,
7934
+ collection: failure === undefined
7935
+ ? { complete: true }
7936
+ : { complete: false, reason: failure.reason, detail: failure.detail },
7937
+ };
7938
+ }
7939
+ function volumeGitText(root, args, allowedStatuses = [0]) {
7940
+ const result = spawnSync('git', [...args], {
7941
+ cwd: root,
7942
+ encoding: 'utf8',
7943
+ maxBuffer: 32 * 1024 * 1024,
7944
+ });
7945
+ if (result.status === null || !allowedStatuses.includes(result.status)) {
7946
+ throw result.error ?? new Error(`git ${args[0] ?? ''} exited ${String(result.status)}`);
7947
+ }
7948
+ return result.stdout ?? '';
7949
+ }
7950
+ function parseGuardStatusPaths(root) {
7951
+ const text = volumeGitText(root, ['status', '--porcelain', '-uall']);
7952
+ return text.split('\n').flatMap((line) => {
7953
+ if (line.length < 4)
7954
+ return [];
7955
+ const code = line.slice(0, 2);
7956
+ const raw = line.slice(3).trim();
7957
+ const path = (raw.includes(' -> ') ? raw.split(' -> ')[1] : raw)?.trim();
7958
+ return path ? [{ code, path }] : [];
7959
+ });
7960
+ }
7961
+ function parseFeatureTier(text) {
7962
+ const match = text.match(/^##\s+Tier:\s*(S|M|L|XL)\s*$/m);
7963
+ return match?.[1];
7964
+ }
7965
+ function parseFeatureActiveSteps(text) {
7966
+ const section = text.match(/^## Active steps[^\n]*\n([\s\S]*?)(?=^## |(?![\s\S]))/mi)?.[1];
7967
+ if (section === undefined)
7968
+ return undefined;
7969
+ const steps = [...section.matchAll(/\b(?:10|[0-9](?:\.5)?)\b/g)].map((match) => match[0]);
7970
+ return [...new Set(steps)];
7971
+ }
7972
+ function featureLifecycle(root, slug, assessment) {
7973
+ if (assessment !== undefined && /^Lifecycle:\s*complete\s*$/mi.test(assessment))
7974
+ return { phase: 'complete' };
7975
+ try {
7976
+ const state = JSON.parse(readFileSync(join(root, '.dz', 'feature-adr', 'learning-state', `${slug}.json`), 'utf8'));
7977
+ const match = typeof state.step === 'string' ? state.step.match(/\bStep\s+(\d+(?:\.5)?)/i) : null;
7978
+ if (!match?.[1])
7979
+ return undefined;
7980
+ const current = Number(match[1]);
7981
+ if (!Number.isFinite(current))
7982
+ return undefined;
7983
+ return { phase: 'in-progress', completedThroughStep: current <= 0 ? 0 : Math.ceil(current) - 1 };
7984
+ }
7985
+ catch {
7986
+ return undefined;
7987
+ }
7988
+ }
7989
+ function gatherFeatureVolumeFact(root, slug) {
7990
+ const featureDir = join(root, 'features', slug);
7991
+ const artifacts = [];
7992
+ const contents = new Map();
7993
+ let totalBytes = 0;
7994
+ let failure;
7995
+ let rootReal;
7996
+ let featureReal;
7997
+ try {
7998
+ rootReal = realpathSync(root);
7999
+ featureReal = realpathSync(featureDir);
8000
+ if (!volumePathInside(rootReal, featureReal))
8001
+ failure = { reason: 'feature-root-escape', detail: featureDir };
8002
+ }
8003
+ catch (error) {
8004
+ return { fact: {
8005
+ slug,
8006
+ artifacts,
8007
+ collection: { complete: false, reason: 'feature-root-unreadable', detail: error instanceof Error ? error.message : String(error) },
8008
+ } };
8009
+ }
8010
+ const fail = (reason, detail) => {
8011
+ failure ??= { reason, detail: detail.replace(/\s+/g, ' ').slice(0, 240) };
8012
+ };
8013
+ const addArtifact = (absolute) => {
8014
+ if (failure !== undefined)
8015
+ return;
8016
+ if (artifacts.length >= MAX_VOLUME_FILES_PER_SCOPE) {
8017
+ fail('feature-file-cap-exceeded', `${MAX_VOLUME_FILES_PER_SCOPE} files`);
8018
+ return;
8019
+ }
8020
+ try {
8021
+ const stat = lstatSync(absolute);
8022
+ if (!stat.isFile()) {
8023
+ fail('feature-input-not-regular', absolute);
8024
+ return;
8025
+ }
8026
+ const real = realpathSync(absolute);
8027
+ if (!volumePathInside(featureReal, real)) {
8028
+ fail('feature-input-escape', absolute);
8029
+ return;
8030
+ }
8031
+ if (stat.size <= 0 || stat.size > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
8032
+ fail(stat.size <= 0 ? 'feature-input-empty' : 'feature-byte-cap-exceeded', absolute);
8033
+ return;
8034
+ }
8035
+ const content = readFileSync(absolute);
8036
+ if (content.byteLength <= 0 || content.byteLength > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
8037
+ fail(content.byteLength <= 0 ? 'feature-input-empty' : 'feature-byte-cap-exceeded', absolute);
8038
+ return;
8039
+ }
8040
+ const path = relative(featureDir, absolute).split(sep).join('/');
8041
+ totalBytes += content.byteLength;
8042
+ contents.set(path, content);
8043
+ artifacts.push({ path, bytes: content.byteLength });
8044
+ }
8045
+ catch (error) {
8046
+ fail('feature-input-unreadable', `${absolute}: ${error instanceof Error ? error.message : String(error)}`);
8047
+ }
8048
+ };
8049
+ const scanNested = (dir) => {
8050
+ if (!existsSync(dir) || failure !== undefined)
8051
+ return;
8052
+ let entries;
8053
+ try {
8054
+ entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
8055
+ }
8056
+ catch (error) {
8057
+ fail('feature-directory-unreadable', `${dir}: ${error instanceof Error ? error.message : String(error)}`);
8058
+ return;
8059
+ }
8060
+ for (const entry of entries) {
8061
+ if (failure !== undefined)
8062
+ return;
8063
+ const absolute = join(dir, entry.name);
8064
+ if (entry.isSymbolicLink()) {
8065
+ fail('feature-input-not-regular', absolute);
8066
+ return;
8067
+ }
8068
+ if (entry.isDirectory())
8069
+ scanNested(absolute);
8070
+ else
8071
+ addArtifact(absolute);
8072
+ }
8073
+ };
8074
+ try {
8075
+ const entries = readdirSync(featureDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
8076
+ for (const entry of entries) {
8077
+ if (failure !== undefined)
8078
+ break;
8079
+ const absolute = join(featureDir, entry.name);
8080
+ if (entry.name === '03_adr' || entry.name === '07_code_changes') {
8081
+ if (entry.isSymbolicLink() || !entry.isDirectory())
8082
+ fail('feature-input-not-regular', absolute);
8083
+ else
8084
+ scanNested(absolute);
8085
+ }
8086
+ else if (entry.name === 'README.md' || /^0[0-9](?:[_.-][^/]*)?\.md$/.test(entry.name)) {
8087
+ addArtifact(absolute);
8088
+ }
8089
+ }
8090
+ }
8091
+ catch (error) {
8092
+ fail('feature-directory-unreadable', `${featureDir}: ${error instanceof Error ? error.message : String(error)}`);
8093
+ }
8094
+ artifacts.sort((a, b) => a.path.localeCompare(b.path));
8095
+ const assessment = contents.get('00_complexity_assessment.md')?.toString('utf8');
8096
+ const tier = assessment !== undefined ? parseFeatureTier(assessment) : undefined;
8097
+ const activeSteps = assessment !== undefined ? parseFeatureActiveSteps(assessment) : undefined;
8098
+ const lifecycle = featureLifecycle(root, slug, assessment);
8099
+ const fact = {
8100
+ slug,
8101
+ ...(tier !== undefined ? { tier } : {}),
8102
+ ...(activeSteps !== undefined ? { activeSteps } : {}),
8103
+ namedConsumers: [],
8104
+ ...(lifecycle !== undefined ? { lifecycle } : {}),
8105
+ artifacts,
8106
+ collection: failure === undefined
8107
+ ? { complete: true }
8108
+ : { complete: false, reason: failure.reason, detail: failure.detail },
8109
+ };
8110
+ const manifestText = contents.get('07_code_changes/change_manifest.md')?.toString('utf8');
8111
+ return { fact, ...(manifestText !== undefined ? { manifestText } : {}) };
8112
+ }
8113
+ function manifestPaths(text, slug) {
8114
+ const paths = new Set();
8115
+ for (const line of text.split('\n')) {
8116
+ const match = line.match(/^\s*-\s+`?([^`]+?)`?(?:\s+[—-]\s+.*)?$/);
8117
+ if (!match?.[1])
8118
+ continue;
8119
+ const path = match[1].trim().replace(/\\/g, '/').replace(/^\.\//, '');
8120
+ if (path === '' || path.startsWith('/') || path.split('/').some((part) => part === '..'))
8121
+ continue;
8122
+ if (path.startsWith(`features/${slug}/`))
8123
+ continue;
8124
+ paths.add(path);
8125
+ }
8126
+ return [...paths].sort();
8127
+ }
8128
+ function attributableDiff(root, slug, feature, status, changedSlugs) {
8129
+ if (changedSlugs.length !== 1 || changedSlugs[0] !== slug) {
8130
+ return { attributable: false, reason: changedSlugs.length > 1 ? 'ambiguous-feature-attribution' : 'feature-not-attributable' };
8131
+ }
8132
+ if (feature.manifestText === undefined)
8133
+ return { attributable: false, reason: 'change-manifest-unavailable' };
8134
+ const listed = manifestPaths(feature.manifestText, slug);
8135
+ const changedByPath = new Map(status.map((item) => [item.path, item.code]));
8136
+ const changed = listed.filter((path) => changedByPath.has(path));
8137
+ if (changed.length === 0)
8138
+ return { attributable: true, bytes: 0, base: 'HEAD', head: 'working-tree', method: 'git-unified-diff-bytes/v1', excludedFeaturePath: `features/${slug}/**` };
8139
+ const tracked = [];
8140
+ const untracked = [];
8141
+ for (const path of changed) {
8142
+ const absolute = resolve(root, path);
8143
+ if (!volumePathInside(root, absolute))
8144
+ return { attributable: false, reason: 'manifest-path-escape' };
8145
+ if (changedByPath.get(path) === '??') {
8146
+ try {
8147
+ const stat = lstatSync(absolute);
8148
+ const real = realpathSync(absolute);
8149
+ if (!stat.isFile() || !volumePathInside(root, real) || stat.size > MAX_VOLUME_BYTES_PER_SCOPE) {
8150
+ return { attributable: false, reason: 'untracked-diff-input-refused' };
8151
+ }
8152
+ }
8153
+ catch {
8154
+ return { attributable: false, reason: 'untracked-diff-input-unreadable' };
8155
+ }
8156
+ untracked.push(path);
8157
+ }
8158
+ else {
8159
+ tracked.push(path);
8160
+ }
8161
+ }
8162
+ let bytes = 0;
8163
+ try {
8164
+ if (tracked.length > 0) {
8165
+ const diff = volumeGitText(root, ['diff', '--binary', '--no-ext-diff', 'HEAD', '--', ...tracked]);
8166
+ bytes += Buffer.byteLength(diff, 'utf8');
8167
+ }
8168
+ for (const path of untracked) {
8169
+ const diff = volumeGitText(root, ['diff', '--no-index', '--binary', '--', '/dev/null', resolve(root, path)], [0, 1]);
8170
+ bytes += Buffer.byteLength(diff, 'utf8');
8171
+ if (bytes > MAX_VOLUME_BYTES_PER_SCOPE)
8172
+ return { attributable: false, reason: 'diff-byte-cap-exceeded' };
8173
+ }
8174
+ }
8175
+ catch {
8176
+ return { attributable: false, reason: 'git-diff-failed' };
8177
+ }
8178
+ return {
8179
+ attributable: true,
8180
+ bytes,
8181
+ base: 'HEAD',
8182
+ head: 'working-tree',
8183
+ method: 'git-unified-diff-bytes/v1',
8184
+ excludedFeaturePath: `features/${slug}/**`,
8185
+ };
8186
+ }
8187
+ function gatherVolumeShadowFacts(root, packages) {
8188
+ const templates = packages
8189
+ .filter((item) => !item.privateFlag)
8190
+ .sort((a, b) => a.name.localeCompare(b.name) || a.dir.localeCompare(b.dir))
8191
+ .flatMap((item) => {
8192
+ const fact = gatherTemplateVolumeTarget(join(root, item.dir), item.name);
8193
+ return fact === undefined ? [] : [fact];
8194
+ });
8195
+ let status;
8196
+ try {
8197
+ status = parseGuardStatusPaths(root);
8198
+ }
8199
+ catch {
8200
+ return templates.length > 0 ? { templates } : {};
8201
+ }
8202
+ const slugs = [...new Set(status.flatMap((item) => {
8203
+ const match = item.path.match(/^features\/([^/]+)\//);
8204
+ return match?.[1] ? [match[1]] : [];
8205
+ }))].sort();
8206
+ const gathered = slugs.slice(0, 32).map((slug) => gatherFeatureVolumeFact(root, slug));
8207
+ const measuredFeatures = gathered.map((feature) => ({
8208
+ ...feature.fact,
8209
+ diff: attributableDiff(root, feature.fact.slug, feature, status, slugs),
8210
+ }));
8211
+ const cappedFeatures = slugs.slice(32).map((slug) => ({
8212
+ slug,
8213
+ artifacts: [],
8214
+ collection: {
8215
+ complete: false,
8216
+ reason: 'feature-scope-cap-exceeded',
8217
+ detail: 'only the first 32 lexicographically sorted changed feature scopes were traversed',
8218
+ },
8219
+ }));
8220
+ const features = [...measuredFeatures, ...cappedFeatures];
8221
+ return {
8222
+ ...(templates.length > 0 ? { templates } : {}),
8223
+ ...(features.length > 0 ? { features } : {}),
8224
+ };
8225
+ }
7224
8226
  /** Gather the facts one op needs. All I/O is best-effort — a missing signal skips its rule, never crashes. */
7225
8227
  function gatherGuardFacts(op, root, text, storeCap) {
7226
8228
  const facts = { op };
@@ -7355,7 +8357,7 @@ function gatherGuardFacts(op, root, text, storeCap) {
7355
8357
  const manifests = [];
7356
8358
  const located = [];
7357
8359
  try {
7358
- const out = execSync('git ls-files "packages/@dzhechkov/*/package.json"', { cwd: root, encoding: 'utf-8' });
8360
+ const out = volumeGitText(root, ['ls-files', 'packages/@dzhechkov/*/package.json']);
7359
8361
  for (const rel of out.split('\n').map((s) => s.trim()).filter(Boolean)) {
7360
8362
  try {
7361
8363
  const m = JSON.parse(readFileSync(join(root, rel), 'utf8'));
@@ -7384,6 +8386,11 @@ function gatherGuardFacts(op, root, text, storeCap) {
7384
8386
  packages.push({ name: m.name ?? '(unnamed)', deps });
7385
8387
  }
7386
8388
  facts['packages'] = packages;
8389
+ facts['volume'] = gatherVolumeShadowFacts(root, located.map(({ dir, m }) => ({
8390
+ dir,
8391
+ name: m.name ?? dir,
8392
+ privateFlag: m.private === true,
8393
+ })));
7387
8394
  // licence-hold (ADR-001, hermes-claude-adaptation): for each pack DECLARING a hold via a
7388
8395
  // `licenseHold` field, hand the raw evidence to the pure checker. Best-effort: an unreadable
7389
8396
  // LICENSE reads as absent (null), which the checker treats as a violation for a publishable
@@ -7661,6 +8668,10 @@ function runGuardEvaluation(root, op, text, overrideReason) {
7661
8668
  catch { /* audit is best-effort, never blocks the verdict */ }
7662
8669
  return result;
7663
8670
  }
8671
+ function renderGuardObservation(observation) {
8672
+ const tag = observation.status === 'unknown' ? 'note' : 'observe';
8673
+ return ` [${tag}] ${observation.rule} ${observation.scope}: ${observation.detail} [${observation.status}]`;
8674
+ }
7664
8675
  /**
7665
8676
  * The tail facts of an append-only log, read from its END — O(1) in the file size, which is what
7666
8677
  * lets the chain be extended on every append without a full-file scan (FR-2). Anything unreadable
@@ -8035,8 +9046,9 @@ function cmdGuardPromote(options, flags, root, write) {
8035
9046
  ...next,
8036
9047
  entries: Object.fromEntries(Object.entries(next.entries).map(([k, v]) => (applied.includes(k) ? [k, { ...v, appliedTs: nowTs }] : [k, v]))),
8037
9048
  };
9049
+ const withEvidence = recordPromotionRunEvidence(withApplied, report, nowTs);
8038
9050
  try {
8039
- writeJsonAtomic(join(root, PROMOTION_STATE_FILE), withApplied);
9051
+ writeJsonAtomic(join(root, PROMOTION_STATE_FILE), withEvidence);
8040
9052
  }
8041
9053
  catch { /* best-effort */ }
8042
9054
  }
@@ -8145,6 +9157,8 @@ function cmdGuard(options, flags, cwd, write) {
8145
9157
  }
8146
9158
  const glyph = result.verdict === 'block' ? '✗' : result.verdict === 'warn' ? '⚠' : '✓';
8147
9159
  write(`dz guard (${op}): ${glyph} ${result.verdict.toUpperCase()} [checked: ${result.checked.join(', ') || 'no rules for this op'}]`);
9160
+ for (const observation of result.observations ?? [])
9161
+ write(renderGuardObservation(observation));
8148
9162
  for (const v of result.violations)
8149
9163
  write(` [${v.severity === 'hard' ? 'BLOCK' : 'warn'}] ${v.rule}: ${v.detail}`);
8150
9164
  for (const n of result.notes ?? [])
@@ -8765,8 +9779,79 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
8765
9779
  // R11: a hung runner is a loud non-answer, never a pass. Same default + parse shape as mutation-gate.
8766
9780
  const timeoutOpt = Number(options.get('timeout') ?? '300000');
8767
9781
  const timeoutMs = Number.isFinite(timeoutOpt) && timeoutOpt > 0 ? timeoutOpt : 300000;
8768
- 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);
8769
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
+ }
8770
9855
  // No safe target to run → this is the existing "property untested" finding (empty propertyTests classify).
8771
9856
  const result = classifyDiscrimination({ propertyTests: [], results: [] });
8772
9857
  if (flags.has('json')) {
@@ -8871,10 +9956,24 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
8871
9956
  }
8872
9957
  // t.file + t.name already passed the engine's strict sanitation (no quotes/metacharacters/leading-dash);
8873
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);
8874
9967
  const nameArg = t.name ? ` -t '${t.name}'` : '';
8875
- const cmd = `${runner}${nameArg} -- '${t.file}'`;
8876
- const base = runCapturedTest(cmd, worktree, timeoutMs);
8877
- 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);
8878
9977
  const outcome = discriminationOutcomeOf(base.exitCode, evidence);
8879
9978
  const row = t.name !== undefined
8880
9979
  ? { file: t.file, name: t.name, outcome, evidence }
@@ -8885,8 +9984,8 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
8885
9984
  // base rows per the matrix; running it is cheap and only ever on an already-broken path.
8886
9985
  // Do NOT "simplify" this to evidenced-error-only — that silently breaks Confirmation 17.
8887
9986
  if (base.exitCode !== null && base.exitCode !== 0 && evidence.failureKind !== 'assertions') {
8888
- const tip = runCapturedTest(cmd, repoRoot, timeoutMs);
8889
- 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);
8890
9989
  row['tipOutcome'] = discriminationOutcomeOf(tip.exitCode, tipEvidence);
8891
9990
  row['tipEvidence'] = tipEvidence;
8892
9991
  // R15, named honestly: the base run is isolated in a worktree, but the tip runs in the LIVE
@@ -8995,51 +10094,6 @@ function readTipTreeConditions(repoRoot) {
8995
10094
  function nameFor(t, outcome) {
8996
10095
  return t.name !== undefined ? { file: t.file, name: t.name, outcome } : { file: t.file, outcome };
8997
10096
  }
8998
- /**
8999
- * `dz mutation-gate` — the mutation gate (feature ha-mutation-gate, SPEC at
9000
- * features/ha-mutation-gate/SPEC.md). A green test proves the code works; it does NOT prove the
9001
- * test would notice the protection being DELETED. For each entry in a declarative registry this
9002
- * gate applies the entry's exact {find, replace} mutation to a SCRATCH COPY of the package, runs
9003
- * the package's test command there, and REQUIRES a non-zero exit (red). All logic is in
9004
- * harness-core's pure engine (mutation-gate.ts); this is the I/O executor.
9005
- *
9006
- * --package <dir> the package to gate (default: cwd; must contain package.json)
9007
- * --registry <file> registry path (default: <pkg>/test/mutation-registry.json, then
9008
- * <pkg>/mutation-registry.json)
9009
- * --test-cmd '<cmd>' suite command run in the scratch copy (default: registry.testCommand,
9010
- * then `npm test`)
9011
- * --only <id[,id]> run a subset of entries (unknown id = usage error, never a silent skip)
9012
- * --timeout <ms> per-suite-run timeout (default 300000). A timed-out run is INCONCLUSIVE —
9013
- * a FAILURE, never a pass.
9014
- * --rebaseline <m> route-b guard mode: 'per-entry' (default — every red entry re-runs the
9015
- * suite on the restored tree; not green ⇒ that entry is INCONCLUSIVE) or
9016
- * 'final' (one re-run at the end; not green ⇒ every red-based verdict is
9017
- * downgraded). The gate's PROVEN now means the redness was ATTRIBUTABLE.
9018
- * --keep-scratch keep the scratch copy for inspection (default: removed in a finally)
9019
- * --json machine contract {packageDir, registryPath, testCommand, rebaselineMode,
9020
- * baseline, results, summary, warnings, exitCode}
9021
- *
9022
- * The four rules (SPEC §"Four rules") and where each is enforced:
9023
- * 1. does-not-apply = FAILURE → core classifyMutationOutcome (occurrences !== 1 ⇒ NOT_APPLIED);
9024
- * 2. green suite = FAILURE → core (exit 0 ⇒ UNDEFENDED, names the property);
9025
- * 3. never mutate the working tree → HERE: every write targets the scratch copy under tmpdir();
9026
- * the repo tree is opened read-only, and a crashed run leaves at worst a stale tmp dir;
9027
- * 4. the gate's own discrimination proof → harness-cli/test/fixtures/mutation-gate-undefended
9028
- * (the gate MUST fail on it; asserted by test/mutation-gate-cli.test.ts).
9029
- *
9030
- * Exit codes: 0 every entry PROVEN · 1 the gate ran and failed (undefended / not-applied /
9031
- * below-min / unparseable / load-fatal / over-failing / inconclusive entry) · 2 usage or setup
9032
- * error (missing registry, red BASELINE — a red unmutated copy proves nothing and must not be
9033
- * read as a mutation result — or an entry whose file RESOLVES outside the scratch copy: a
9034
- * symlink escape is refused before anything is written, SPEC rule 3).
9035
- */
9036
- /**
9037
- * Route-a guard for `dz mutation-gate`: parse-check a MUTATED file as its own language BEFORE the
9038
- * suite runs. A registry mutation must delete the protection while keeping the file loadable — a
9039
- * file that no longer parses kills the whole suite (or its import chain), and that STRUCTURAL
9040
- * redness says nothing about the named protection. Returns `{error}` when a parser ran and the
9041
- * text does not parse; `{skipped}` (reported loudly, never silently) when no parser is available.
9042
- */
9043
10097
  function parseCheckMutatedFile(absFile, text) {
9044
10098
  const ext = extname(absFile).toLowerCase();
9045
10099
  try {
@@ -9070,18 +10124,36 @@ function parseCheckMutatedFile(absFile, text) {
9070
10124
  }
9071
10125
  }
9072
10126
  if (ext === '.js' || ext === '.cjs' || ext === '.mjs' || ext === '') {
9073
- try {
9074
- // `node --check` on the file IN PLACE, so the nearest package.json decides the module goal.
9075
- execFileSync(process.execPath, ['--check', absFile], { stdio: 'pipe' });
9076
- return {};
9077
- }
9078
- catch (e) {
9079
- const err = e;
9080
- const stderrLines = String(err.stderr ?? '').split('\n').map((l) => l.trim()).filter((l) => l !== '');
9081
- // prefer the actual `SyntaxError: …` line over node's trailing version footer.
9082
- const msg = [...stderrLines].reverse().find((l) => l.includes('Error')) ?? stderrLines.at(-1) ?? 'node --check failed';
9083
- 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
+ };
9084
10153
  }
10154
+ return checked.internalRetries === 1
10155
+ ? { ...checked.value, internalAttempts: checked.attempts }
10156
+ : checked.value;
9085
10157
  }
9086
10158
  return { skipped: `no parser for '${ext}' files — parse-check unavailable` };
9087
10159
  }
@@ -9089,7 +10161,7 @@ function parseCheckMutatedFile(absFile, text) {
9089
10161
  return { skipped: `parse-check errored: ${String(e.message).slice(0, 120)}` };
9090
10162
  }
9091
10163
  }
9092
- function cmdMutationGate(options, flags, cwd, write) {
10164
+ function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
9093
10165
  const json = flags.has('json');
9094
10166
  const fail = (what) => {
9095
10167
  write(json ? JSON.stringify({ error: what, exitCode: 2 }) : `dz mutation-gate: ${what}`);
@@ -9157,6 +10229,7 @@ function cmdMutationGate(options, flags, cwd, write) {
9157
10229
  const results = [];
9158
10230
  const observations = [];
9159
10231
  const warnings = [];
10232
+ const internalRetries = [];
9160
10233
  let baseline;
9161
10234
  try {
9162
10235
  if (gitTop !== null && gitTop !== pkgDir && resolve(pkgDir).startsWith(resolve(gitTop) + sep)) {
@@ -9200,7 +10273,10 @@ function cmdMutationGate(options, flags, cwd, write) {
9200
10273
  // write below is asserted to RESOLVE inside this root before it happens.
9201
10274
  const realScratchRoot = realpathSync(copyDir);
9202
10275
  const requireCompletionReceipt = parsed.registry.requireCompletionReceipt === true;
9203
- const runSuite = () => {
10276
+ const invokeSuite = () => {
10277
+ if (injectedRunner !== undefined) {
10278
+ return injectedRunner(testCmd, { cwd: copyDir, timeoutMs: timeout });
10279
+ }
9204
10280
  const run = spawnSync(testCmd, {
9205
10281
  cwd: copyDir,
9206
10282
  shell: true,
@@ -9212,6 +10288,12 @@ function cmdMutationGate(options, flags, cwd, write) {
9212
10288
  const errorCode = run.error && 'code' in run.error && typeof run.error.code === 'string'
9213
10289
  ? run.error.code
9214
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
+ }
9215
10297
  const signal = typeof run.signal === 'string' ? run.signal : undefined;
9216
10298
  let failureReason;
9217
10299
  if (typeof run.status !== 'number') {
@@ -9228,25 +10310,59 @@ function cmdMutationGate(options, flags, cwd, write) {
9228
10310
  }
9229
10311
  return {
9230
10312
  exitCode: typeof run.status === 'number' ? run.status : null,
9231
- // Receipt markers may be on stderr. Preserve both streams even on exit 0; stdout-only
9232
- // collection would silently lose a green-run marker.
9233
10313
  output: `${String(run.stdout ?? '')}\n${String(run.stderr ?? '')}`,
9234
10314
  ...(failureReason !== undefined ? { failureReason } : {}),
9235
10315
  };
9236
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
+ };
9237
10351
  // Baseline BEFORE any mutation: a red copy proves nothing, and reading it as a mutation
9238
10352
  // result would be this gate shipping the defect class it exists to catch.
9239
10353
  if (!json)
9240
10354
  write(`mutation-gate: baseline suite in scratch copy of ${pkgDir} …`);
9241
- const base = runSuite();
9242
- 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);
9243
10359
  if (!baseline.ok) {
9244
10360
  if (json) {
9245
- write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, baseline, results: [], exitCode: 2 }, null, 2));
9246
- return 2;
10361
+ write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, baseline, results: [], internalRetries, exitCode: 1 }, null, 2));
10362
+ return 1;
9247
10363
  }
9248
10364
  write(renderMutationReport([], baseline, pkgDir));
9249
- return 2;
10365
+ return 1;
9250
10366
  }
9251
10367
  for (const entry of entries) {
9252
10368
  const filePath = join(copyDir, entry.file);
@@ -9289,6 +10405,8 @@ function cmdMutationGate(options, flags, cwd, write) {
9289
10405
  write(`mutation-gate: ${entry.id} — mutating ${entry.file}, running suite …`);
9290
10406
  let run = null;
9291
10407
  let parseError;
10408
+ let parseInternalFailureReason;
10409
+ let parseInternalAttemptLog;
9292
10410
  try {
9293
10411
  writeFileSync(filePath, applied.text);
9294
10412
  // Route-a guard: the mutated file must still PARSE — a load failure reddens the whole
@@ -9299,11 +10417,18 @@ function cmdMutationGate(options, flags, cwd, write) {
9299
10417
  if (!json)
9300
10418
  write(`mutation-gate: WARNING ${entry.id}: parse-check skipped — ${check.skipped}`);
9301
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;
9302
10427
  if (check.error !== undefined) {
9303
10428
  parseError = check.error; // no suite run: the verdict is MUTATION_UNPARSEABLE regardless
9304
10429
  }
9305
- else {
9306
- run = runSuite();
10430
+ else if (parseInternalFailureReason === undefined) {
10431
+ run = runSuite('mutation', entry.id);
9307
10432
  }
9308
10433
  }
9309
10434
  finally {
@@ -9339,14 +10464,24 @@ function cmdMutationGate(options, flags, cwd, write) {
9339
10464
  // those verdicts outrank the rebaseline check, so the extra suite run would buy nothing.
9340
10465
  let rebaselineExitCode;
9341
10466
  let rebaselineFailureReason;
10467
+ let rebaselineAttribution;
10468
+ let rebaselineInternalAttemptLog;
9342
10469
  if (rebaselineMode === 'per-entry' && run !== null && run.exitCode !== null && run.exitCode !== 0
9343
10470
  && fileLoadFailure === undefined && outputUnrecognised === undefined && receiptMismatch === undefined) {
9344
10471
  if (!json)
9345
10472
  write(`mutation-gate: ${entry.id} — re-baselining the restored tree …`);
9346
- const rebaselineRun = runSuite();
10473
+ const rebaselineRun = runSuite('rebaseline', entry.id);
9347
10474
  rebaselineExitCode = rebaselineRun.exitCode;
9348
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
+ }
9349
10480
  }
10481
+ const entryRunFailureReason = run?.failureReason ?? parseInternalFailureReason;
10482
+ const entryInternalAttemptLog = [parseInternalAttemptLog, run?.internalAttemptLog, rebaselineInternalAttemptLog]
10483
+ .filter((log) => log !== undefined)
10484
+ .join('; ');
9350
10485
  const obs = {
9351
10486
  entry,
9352
10487
  occurrences: 1,
@@ -9356,9 +10491,11 @@ function cmdMutationGate(options, flags, cwd, write) {
9356
10491
  ...(fileLoadFailure !== undefined ? { fileLoadFailure } : {}),
9357
10492
  ...(outputUnrecognised !== undefined ? { outputUnrecognised } : {}),
9358
10493
  ...(receiptMismatch !== undefined ? { receiptMismatch } : {}),
9359
- ...(run?.failureReason !== undefined ? { runFailureReason: run.failureReason } : {}),
10494
+ ...(entryRunFailureReason !== undefined ? { runFailureReason: entryRunFailureReason } : {}),
10495
+ ...(entryInternalAttemptLog !== '' ? { internalAttemptLog: entryInternalAttemptLog } : {}),
9360
10496
  ...(rebaselineExitCode !== undefined ? { rebaselineExitCode } : {}),
9361
10497
  ...(rebaselineFailureReason !== undefined ? { rebaselineFailureReason } : {}),
10498
+ ...(rebaselineAttribution !== undefined ? { rebaselineAttribution } : {}),
9362
10499
  };
9363
10500
  observations.push(obs);
9364
10501
  results.push(classifyMutationOutcome(obs));
@@ -9371,7 +10508,7 @@ function cmdMutationGate(options, flags, cwd, write) {
9371
10508
  if (rebaselineMode === 'final') {
9372
10509
  if (!json)
9373
10510
  write('mutation-gate: final re-baseline of the restored tree …');
9374
- const finalRun = runSuite();
10511
+ const finalRun = runSuite('final-rebaseline');
9375
10512
  const finalExit = finalRun.exitCode;
9376
10513
  if (finalExit !== 0) {
9377
10514
  const what = finalExit === null ? `no exit code: ${finalRun.failureReason ?? 'unknown timeout / spawn failure'}` : `exit ${finalExit}`;
@@ -9382,6 +10519,12 @@ function cmdMutationGate(options, flags, cwd, write) {
9382
10519
  ...obs,
9383
10520
  rebaselineExitCode: finalExit,
9384
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
+ : {}),
9385
10528
  }));
9386
10529
  results.length = 0;
9387
10530
  results.push(...reclassified);
@@ -9401,7 +10544,7 @@ function cmdMutationGate(options, flags, cwd, write) {
9401
10544
  }
9402
10545
  const exitCode = mutationGateExitCode(results, baseline.ok);
9403
10546
  if (json) {
9404
- 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));
9405
10548
  return exitCode;
9406
10549
  }
9407
10550
  write(renderMutationReport(results, baseline, pkgDir));
@@ -11245,6 +12388,18 @@ function cmdFeatureAdrRecord(options, flags, cwd, write) {
11245
12388
  targetExists: existsSync(target),
11246
12389
  targetHasPair: flags.has('once') && existsSync(target),
11247
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
+ } })(),
11248
12403
  });
11249
12404
  if (decision.line === null)
11250
12405
  return emit(decision);
@@ -12597,6 +13752,143 @@ async function cmdQeBridge(options, flags, cwd, write) {
12597
13752
  cleanupIsolated();
12598
13753
  }
12599
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
+ }
12600
13892
  function cmdScore(options, flags, cwd, write) {
12601
13893
  const json = flags.has('json');
12602
13894
  if (flags.has('help')) {
@@ -12605,13 +13897,14 @@ function cmdScore(options, flags, cwd, write) {
12605
13897
  write(JSON.stringify({ help: usage, exitCode: 0 })); // --json stays ONE document even for help
12606
13898
  else {
12607
13899
  write(usage);
13900
+ write('dz score --all [--project <dir>] [--json] — sweep immutable score receipts into the append-only chained aggregate');
12608
13901
  write(' disciplines: ADR confirmation · discrimination · cross-model QE · live verification · README-first · learning loop · amendments');
12609
13902
  write(' descriptive-only, never a gate: a low score exits 0');
12610
13903
  }
12611
13904
  return 0;
12612
13905
  }
12613
13906
  for (const flag of flags) {
12614
- if (!new Set(['json', 'help']).has(flag)) {
13907
+ if (!new Set(['json', 'help', 'all']).has(flag)) {
12615
13908
  write(json ? JSON.stringify({ error: `unknown option --${flag}`, exitCode: 1 }) : `dz score: unknown option --${flag}\n allowed: --slug <feature>, --project <dir>, --json`);
12616
13909
  return 1;
12617
13910
  }
@@ -12623,6 +13916,8 @@ function cmdScore(options, flags, cwd, write) {
12623
13916
  return 1;
12624
13917
  }
12625
13918
  }
13919
+ if (flags.has('all'))
13920
+ return cmdScoreAll(options, flags, cwd, write);
12626
13921
  const slug = options.get('slug') ?? '';
12627
13922
  // The delivery-check traversal lesson, upgraded to a WHITELIST: `.` slipped the blacklist and
12628
13923
  // silently aggregated the entire features/ tree as one "run" (Codex QE #2).
@@ -12743,6 +14038,134 @@ function readOptionalText(path) {
12743
14038
  return '';
12744
14039
  }
12745
14040
  }
14041
+ function localEvidenceReadReason(resource, error) {
14042
+ const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
14043
+ return code === 'ENOENT' ? `${resource}-missing` : `${resource}-unreadable`;
14044
+ }
14045
+ function unavailablePromotionEvidence(reason) {
14046
+ return { source: { status: 'not-measured', reason }, acceptances: [], truncatedPeriods: [], acceptanceHistoryComplete: false };
14047
+ }
14048
+ function readPromotionRunEvidence(root) {
14049
+ let raw;
14050
+ try {
14051
+ raw = JSON.parse(readFileSync(join(root, PROMOTION_STATE_FILE), 'utf-8'));
14052
+ }
14053
+ catch (error) {
14054
+ const reason = error instanceof SyntaxError
14055
+ ? 'promotion-journal-malformed'
14056
+ : localEvidenceReadReason('promotion-journal', error);
14057
+ return unavailablePromotionEvidence(reason);
14058
+ }
14059
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
14060
+ return unavailablePromotionEvidence('promotion-journal-malformed');
14061
+ }
14062
+ const record = raw;
14063
+ if (record['version'] !== 1 || !Object.hasOwn(record, 'runs')) {
14064
+ return unavailablePromotionEvidence(record['version'] === 1 ? 'promotion-history-not-recorded' : 'promotion-journal-malformed');
14065
+ }
14066
+ if (!Array.isArray(record['runs'])) {
14067
+ return unavailablePromotionEvidence('promotion-history-malformed');
14068
+ }
14069
+ const state = normalizePromotionState(raw);
14070
+ if (state.runs === undefined || state.runs.length !== record['runs'].length) {
14071
+ return unavailablePromotionEvidence('promotion-history-malformed');
14072
+ }
14073
+ if ((Object.hasOwn(record, 'acceptances') && (!Array.isArray(record['acceptances']) || state.acceptances?.length !== record['acceptances'].length)) ||
14074
+ (Object.hasOwn(record, 'truncatedRunPeriods') && (!Array.isArray(record['truncatedRunPeriods']) || state.truncatedRunPeriods?.length !== record['truncatedRunPeriods'].length)) ||
14075
+ (Object.hasOwn(record, 'acceptanceHistoryComplete') && typeof record['acceptanceHistoryComplete'] !== 'boolean')) {
14076
+ return unavailablePromotionEvidence('promotion-history-malformed');
14077
+ }
14078
+ const derivedAcceptances = state.runs.flatMap((run) => run.candidates
14079
+ .filter((candidate) => candidate.verdict === 'promote' && candidate.ruleContentAnchor !== null)
14080
+ .map((candidate) => ({ ruleContentAnchor: candidate.ruleContentAnchor, acceptedTs: run.ts })));
14081
+ const claimedAcceptanceComplete = state.acceptanceHistoryComplete ??
14082
+ ((state.truncatedRunPeriods?.length ?? 0) === 0 && state.runs.every((run) => run.complete === true));
14083
+ if (claimedAcceptanceComplete && state.acceptances !== undefined && derivedAcceptances.some((derived) => !state.acceptances.some((stored) => stored.ruleContentAnchor === derived.ruleContentAnchor &&
14084
+ Date.parse(stored.acceptedTs) <= Date.parse(derived.acceptedTs)))) {
14085
+ return unavailablePromotionEvidence('promotion-history-malformed');
14086
+ }
14087
+ const acceptancesByAnchor = new Map();
14088
+ for (const acceptance of [...(state.acceptances ?? []), ...derivedAcceptances]) {
14089
+ const prior = acceptancesByAnchor.get(acceptance.ruleContentAnchor);
14090
+ if (prior === undefined || Date.parse(acceptance.acceptedTs) < Date.parse(prior.acceptedTs)) {
14091
+ acceptancesByAnchor.set(acceptance.ruleContentAnchor, acceptance);
14092
+ }
14093
+ }
14094
+ return {
14095
+ source: { status: 'measured', rows: state.runs },
14096
+ acceptances: [...acceptancesByAnchor.values()],
14097
+ truncatedPeriods: state.truncatedRunPeriods ?? [],
14098
+ acceptanceHistoryComplete: claimedAcceptanceComplete,
14099
+ };
14100
+ }
14101
+ function readGuardAuditEvidence(root) {
14102
+ let text;
14103
+ try {
14104
+ text = readFileSync(join(root, '.dz', 'guard-audit.jsonl'), 'utf-8');
14105
+ }
14106
+ catch (error) {
14107
+ return {
14108
+ source: { status: 'not-measured', reason: localEvidenceReadReason('guard-audit', error) },
14109
+ rows: [],
14110
+ text: null,
14111
+ };
14112
+ }
14113
+ const rows = [];
14114
+ let malformed = false;
14115
+ for (const line of text.split('\n')) {
14116
+ if (line.trim() === '')
14117
+ continue;
14118
+ try {
14119
+ const raw = JSON.parse(line);
14120
+ if (!isOffsetIsoTimestamp(raw['ts']) ||
14121
+ !['publish', 'teach', 'consolidate', 'reindex'].includes(String(raw['op'])) ||
14122
+ !['pass', 'warn', 'block'].includes(String(raw['verdict']))) {
14123
+ malformed = true;
14124
+ continue;
14125
+ }
14126
+ const violations = [];
14127
+ if (!Array.isArray(raw['violations'])) {
14128
+ malformed = true;
14129
+ continue;
14130
+ }
14131
+ for (const item of raw['violations']) {
14132
+ const rule = item && typeof item === 'object' ? item.rule : undefined;
14133
+ if (typeof rule !== 'string' || rule === '' || rule.length > 200) {
14134
+ malformed = true;
14135
+ continue;
14136
+ }
14137
+ const anchor = item.contentAnchor;
14138
+ if (anchor !== undefined && !isLessonRuleContentAnchor(anchor)) {
14139
+ malformed = true;
14140
+ continue;
14141
+ }
14142
+ violations.push({ rule, ...(typeof anchor === 'string' ? { contentAnchor: anchor } : {}) });
14143
+ }
14144
+ const verdict = raw['verdict'];
14145
+ if ((verdict === 'pass') !== (violations.length === 0))
14146
+ malformed = true;
14147
+ rows.push({
14148
+ ts: raw['ts'],
14149
+ op: raw['op'],
14150
+ verdict,
14151
+ rules: violations.map((item) => item.rule),
14152
+ violations,
14153
+ });
14154
+ }
14155
+ catch {
14156
+ malformed = true;
14157
+ }
14158
+ }
14159
+ return {
14160
+ source: malformed
14161
+ ? { status: 'not-measured', reason: 'guard-audit-malformed' }
14162
+ : !verifyEventChainText(text).ok
14163
+ ? { status: 'not-measured', reason: 'guard-audit-chain-corrupt' }
14164
+ : { status: 'measured', rows },
14165
+ rows,
14166
+ text,
14167
+ };
14168
+ }
12746
14169
  function deadwoodAllowlistText() {
12747
14170
  const require = createRequire(import.meta.url);
12748
14171
  const corePackage = require.resolve('@dzhechkov/harness-core/package.json');
@@ -12823,7 +14246,7 @@ function cmdCompounding(options, flags, cwd, write) {
12823
14246
  const json = flags.has('json');
12824
14247
  if (flags.has('help')) {
12825
14248
  write('dz compounding [--project <dir>] [--json] — honest learning-loop payoff report');
12826
- write(' pool payoff (write-only ratio) · guard repeat-violation trajectory · cold-vs-warm replay readiness · instrumentation health');
14249
+ write(' pool payoff · guard trajectory · replay readiness · instrumentation · monthly eligible→attempted→accepted→executions funnel');
12827
14250
  return 0;
12828
14251
  }
12829
14252
  for (const flag of flags) {
@@ -12852,36 +14275,14 @@ function cmdCompounding(options, flags, cwd, write) {
12852
14275
  }));
12853
14276
  // apply-leg usage events (read records only; aggregate rows carry no query by construction)
12854
14277
  const usage = readRecallUsageEvents(root);
12855
- // guard audit events
12856
- const guard = [];
12857
- try {
12858
- const text = readFileSync(join(root, '.dz', 'guard-audit.jsonl'), 'utf-8');
12859
- for (const line of text.split('\n')) {
12860
- if (line.trim() === '')
12861
- continue;
12862
- try {
12863
- const o = JSON.parse(line);
12864
- if (typeof o.ts === 'string' && typeof o.verdict === 'string') {
12865
- guard.push({
12866
- ts: o.ts,
12867
- verdict: o.verdict,
12868
- rules: Array.isArray(o.violations) ? o.violations.map((v) => (typeof v?.rule === 'string' ? v.rule : '')).filter(Boolean) : [],
12869
- });
12870
- }
12871
- }
12872
- catch {
12873
- /* skip */
12874
- }
12875
- }
12876
- }
12877
- catch {
12878
- /* no audit yet */
12879
- }
14278
+ const promotionEvidence = readPromotionRunEvidence(root);
14279
+ const guardEvidence = readGuardAuditEvidence(root);
14280
+ const guard = [...guardEvidence.rows];
12880
14281
  // The evidence logs themselves, verbatim: the report verifies their hash chains (feature
12881
14282
  // event-chain). Handing over the TEXT rather than a pre-computed verdict keeps one definition of
12882
14283
  // "the chain is intact" — a second copy here is how a gate and its report start disagreeing.
12883
14284
  const evidenceLogs = [];
12884
- for (const rel of ['.dz/recall-usage.jsonl', '.dz/guard-audit.jsonl']) {
14285
+ for (const rel of ['.dz/recall-usage.jsonl']) {
12885
14286
  try {
12886
14287
  evidenceLogs.push({ log: rel, text: readFileSync(join(root, ...rel.split('/')), 'utf-8') });
12887
14288
  }
@@ -12889,6 +14290,9 @@ function cmdCompounding(options, flags, cwd, write) {
12889
14290
  /* absent log — reported by its own gate above, not invented here */
12890
14291
  }
12891
14292
  }
14293
+ if (guardEvidence.text !== null) {
14294
+ evidenceLogs.push({ log: '.dz/guard-audit.jsonl', text: guardEvidence.text });
14295
+ }
12892
14296
  const now = new Date();
12893
14297
  const report = assembleCompoundingReport({
12894
14298
  lessons,
@@ -12897,6 +14301,13 @@ function cmdCompounding(options, flags, cwd, write) {
12897
14301
  nowTs: now.toISOString(),
12898
14302
  evidenceLogs,
12899
14303
  cmdUsageDepthDays: cmdUsageDepthDays(root, now),
14304
+ lessonToRule: {
14305
+ promotionRuns: promotionEvidence.source,
14306
+ guardAudits: guardEvidence.source,
14307
+ promotionAcceptances: promotionEvidence.acceptances,
14308
+ truncatedPromotionPeriods: promotionEvidence.truncatedPeriods,
14309
+ acceptanceHistoryComplete: promotionEvidence.acceptanceHistoryComplete,
14310
+ },
12900
14311
  });
12901
14312
  // lesson-bandit-rerank §11: the payoff axis joins THIS report rather than growing a private
12902
14313
  // dashboard — the `rewardEvents : exposureEvents` row asks exactly the question this command
@@ -14380,17 +15791,19 @@ function cmdStats(cwd, write) {
14380
15791
  }
14381
15792
  const dirs = readdirSync(baseDir, { withFileTypes: true }).filter((e) => e.isDirectory());
14382
15793
  const packages = dirs.length;
14383
- let totalSkills = 0;
14384
- let skillPacks = 0;
14385
- for (const dir of dirs) {
14386
- if (dir.name.startsWith('skills-')) {
14387
- skillPacks++;
14388
- const skillDir = join(baseDir, dir.name);
14389
- const skillDirs = readdirSync(skillDir, { withFileTypes: true })
14390
- .filter((e) => e.isDirectory() && existsSync(join(skillDir, e.name, 'SKILL.md')));
14391
- totalSkills += skillDirs.length;
14392
- }
14393
- }
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;
14394
15807
  const targets = TARGET_NAMES.length;
14395
15808
  const presets = PRESET_NAMES.length;
14396
15809
  write(`dz stats — DZ Harness Hub`);
@@ -14875,7 +16288,28 @@ export async function runCli(argv, io = {}) {
14875
16288
  // let a guard treat "I could not tell you" as "I answered you".
14876
16289
  return version === 'unknown' ? 1 : 0;
14877
16290
  }
14878
- 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))) {
14879
16313
  write(USAGE);
14880
16314
  return 0;
14881
16315
  }
@@ -14921,11 +16355,11 @@ export async function runCli(argv, io = {}) {
14921
16355
  case 'bundle':
14922
16356
  return cmdBundle(options, flags, cwd, write);
14923
16357
  case 'teach':
14924
- return await cmdTeach(options, flags, cwd, write);
16358
+ return await cmdTeach(options, flags, cwd, write, writeErr, io.interactive ?? process.stdout.isTTY === true, io.teachGuardRunner ?? teachGuard, io.teachReinforceRunner ?? runTeachGuardReinforcement);
14925
16359
  case 'consolidate':
14926
16360
  return await cmdConsolidate(options, flags, cwd, write);
14927
16361
  case 'recall':
14928
- return await cmdRecall(options, flags, cwd, write);
16362
+ return await cmdRecall(options, flags, cwd, write, writeErr, io.classMatcher);
14929
16363
  case 'vector':
14930
16364
  return await cmdVector(options, flags, cwd, write);
14931
16365
  case 'brain':
@@ -14934,6 +16368,8 @@ export async function runCli(argv, io = {}) {
14934
16368
  return cmdStatusline(options, flags, cwd, write, readStdin);
14935
16369
  case 'usage':
14936
16370
  return cmdUsage(options, optionLists, flags, cwd, write);
16371
+ case 'chain':
16372
+ return cmdChain(options, flags, cwd, write);
14937
16373
  case 'claim-check':
14938
16374
  return cmdClaimCheck(options, optionLists, flags, cwd, write);
14939
16375
  case 'lint':
@@ -14955,7 +16391,7 @@ export async function runCli(argv, io = {}) {
14955
16391
  case 'diff':
14956
16392
  return cmdDiff(options, cwd, write);
14957
16393
  case 'recommend':
14958
- return cmdRecommend(options, cwd, write);
16394
+ return cmdRecommend(options, flags, cwd, write);
14959
16395
  case 'upgrade':
14960
16396
  return cmdUpgrade(options, flags, cwd, write, writeErr);
14961
16397
  case 'auto-canonicalize':
@@ -14978,6 +16414,8 @@ export async function runCli(argv, io = {}) {
14978
16414
  return cmdDriftCheck(options, flags, cwd, write);
14979
16415
  case 'hooks-sync':
14980
16416
  return cmdHooksSync(options, flags, cwd, write, writeErr);
16417
+ case 'integrations-verify':
16418
+ return cmdIntegrationsVerify(options, flags, cwd, write, writeErr);
14981
16419
  case 'agents-sync':
14982
16420
  return cmdAgentsSync(options, flags, cwd, write, writeErr);
14983
16421
  case 'sync-canonical':
@@ -15002,8 +16440,22 @@ export async function runCli(argv, io = {}) {
15002
16440
  return cmdChallenge(options, flags, cwd, write);
15003
16441
  case 'discrimination-check':
15004
16442
  return cmdDiscriminationCheck(options, flags, cwd, write);
15005
- case 'mutation-gate':
15006
- 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
+ }
15007
16459
  case 'delivery-check':
15008
16460
  return cmdDeliveryCheck(options, flags, cwd, write);
15009
16461
  case 'skills-verify':
@@ -15057,9 +16509,8 @@ export async function runCli(argv, io = {}) {
15057
16509
  case 'import-ecc':
15058
16510
  return await cmdImportEcc(options, flags, cwd, write);
15059
16511
  default:
15060
- write(`dz: unknown command ${JSON.stringify(command)}`);
15061
- write(USAGE);
15062
- return 1;
16512
+ writeErr(`dz: unknown command ${JSON.stringify(command)} — run 'dz help' for the command list`);
16513
+ return 2;
15063
16514
  }
15064
16515
  }
15065
16516
  catch (error) {