@dzhechkov/harness-cli 0.8.5 → 0.8.9

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
@@ -14,11 +14,12 @@ import { execFile, execFileSync, execSync, spawn, spawnSync } from 'node:child_p
14
14
  import { createHash, randomBytes } from 'node:crypto';
15
15
  import { homedir, tmpdir } from 'node:os';
16
16
  import { createRequire } from 'node:module';
17
- 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,
17
+ import { isDeepStrictEqual } from 'node:util';
18
+ import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures, formatSkillApplyFailures, resolveTargetName, formatTargetProblem, formatTargetAliasNote, TARGET_NAMES_SORTED, runDoctor, runInit, discoverSkillIds, runIntegrationsVerify, resolvePackageSkillRoots, PACKAGE_SKILL_LAYOUTS, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, discoverVerifiablePackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, runSyncAgentsPolicy, runSyncCodexHooks, resolveCodexHome, withNamedLockSync,
18
19
  // dz workflow run (feature dz-workflow-run): the pure scheduler + the dispatch adapters.
19
- 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, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
20
21
  // guard-promotion (feature guard-promotion, scout idea #1)
21
- 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, 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,
22
23
  // Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
23
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
25
  // Smart Backlog (feature smart-backlog) — goal-directed idea pipeline over the Brain vector engine.
@@ -39,7 +40,7 @@ export const DZ_COMMANDS = [
39
40
  'usage', 'claim-check', 'lint', 'sign', 'sbom', 'guard', 'verify-pack', 'setup',
40
41
  'pretrain', 'compose', 'diff', 'recommend', 'upgrade', 'auto-canonicalize',
41
42
  'publish', 'release', 'parity', 'registry', 'benchmark', 'mcp-scan',
42
- 'sync-upstream', 'drift-check', 'hooks-sync', 'agents-sync', 'sync-canonical',
43
+ 'sync-upstream', 'drift-check', 'hooks-sync', 'integrations-verify', 'agents-sync', 'sync-canonical',
43
44
  'plugin', 'downloads', 'stats', 'architecture', 'project-skills', 'mr-rakes',
44
45
  'retro', 'feature-adr-setup', 'challenge', 'discrimination-check',
45
46
  'mutation-gate', 'delivery-check', 'skills-verify', 'compounding', 'deadwood',
@@ -51,7 +52,7 @@ export const DZ_COMMANDS = [
51
52
  const USAGE = `dz - DZ cross-platform harness CLI
52
53
 
53
54
  Usage:
54
- 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)
55
56
  dz verify [--skills-dir <dir>] [--target <name>]
56
57
  dz sync [--canonical <dir>] [--project <dir>] [--dry-run] [--force]
57
58
  dz update (alias of sync)
@@ -80,7 +81,7 @@ Usage:
80
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)
81
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)
82
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)
83
- 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)
84
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)
85
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)
86
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)
@@ -115,13 +116,13 @@ Usage:
115
116
  dz backlog ship <id> [<id>…] [--reason <t>] [--dry-run] [--project <dir>] [--json] (mark work DONE: new|enriched|in-progress → shipped, removing it from the roulette pool — run it after finishing a task; short id prefixes ok, ambiguous = loud error)
116
117
  dz backlog drop <id> [<id>…] [--reason <t>] [--dry-run] [--project <dir>] [--json] (retire an idea: new|enriched|in-progress → dropped)
117
118
  dz backlog edit <id> --text "<new>" | --append "<more>" [--dry-run] [--project <dir>] [--json] (rewrite ONE idea's text, preserving every other field; re-embeds the dedup vector, and on a failed re-embed MARKS the record embedStale so dedup refuses to trust it — previous text preserved in .dz/backlog/edits.jsonl)
118
- dz routing recommend [--tier <t>] [--apply] [--json] (per-stage args.models suggestion from REAL telemetry — harness records + imported run-meta sidecars — printed WITH its basis: n runs, window, the grade-floor rule, skip reasons; qe is FORCED cross-family of code; --apply feeds .dz/routing-outcomes.json idempotently by runId)
119
+ dz routing recommend [--tier <t>] [--apply] [--json] (per-stage args.models suggestion from REAL telemetry — harness records + imported run-meta sidecars — printed WITH its basis + current/STALE/UNFED store receipt; qe is FORCED cross-family of code; --apply feeds .dz/routing-outcomes.json idempotently by runId)
119
120
  dz backlog reopen <id> [<id>…] [--reason <t>] [--dry-run] [--project <dir>] [--json] (back to the pool: shipped|dropped|in-progress → new)
120
121
  dz backlog enrich <id> [--project <dir>] [--json] (stage the idea2prd input scaffold in features/<slug>/ and hand off to the idea2prd-manual skill — the CLI never fabricates a PRD)
121
122
  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)
122
123
  dz backlog harmonize [--apply] [--threshold <0-1>] [--project <dir>] [--json] (batch semantic dedup of the backlog ideas; --dry-run default, --apply snapshots first)
123
124
  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)
124
- 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)
125
+ 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)
125
126
  dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
126
127
  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)
127
128
  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)
@@ -160,6 +161,7 @@ Usage:
160
161
  dz drift-check [--json] [--project <dir>] (CI gate: exit 1 if any shared skill drifted between its monorepo copies)
161
162
  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)
162
163
  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)
164
+ dz integrations-verify --target <name> --component <mcp|hooks> [--project <dir>] [--json] (non-executing exact-version registration probe; exit 0 observed / 1 refused)
163
165
  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)
164
166
  dz plugin [--version <ver>]
165
167
  dz downloads
@@ -271,8 +273,8 @@ async function installSkills(opts) {
271
273
  // route them through one aggregation.
272
274
  if (target === 'agents-md' || target === 'gemini') {
273
275
  const report = target === 'gemini'
274
- ? runInitGeminiMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}) })
275
- : runInitAgentsMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}) });
276
+ ? runInitGeminiMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}), ...(opts.noHooks !== undefined ? { noHooks: opts.noHooks } : {}), ...(opts.noIntegrations !== undefined ? { noIntegrations: opts.noIntegrations } : {}) })
277
+ : runInitAgentsMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}), ...(opts.noHooks !== undefined ? { noHooks: opts.noHooks } : {}), ...(opts.noIntegrations !== undefined ? { noIntegrations: opts.noIntegrations } : {}) });
276
278
  const results = report.skills.map((s) => ({
277
279
  id: s.id,
278
280
  written: s.written.length,
@@ -284,12 +286,20 @@ async function installSkills(opts) {
284
286
  written += s.written;
285
287
  skipped += s.skipped;
286
288
  }
287
- return { results, dirsSearched: skillsDirs.length, written, skipped, missing: [...report.missing], failures: [...report.failures], applyFailures: [...report.applyFailures] };
289
+ 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 } : {}) };
288
290
  }
289
291
  const results = [];
290
292
  const failures = [];
291
293
  const applyFailures = [];
292
- for (const skillsDir of skillsDirs) {
294
+ let integrations = [];
295
+ let integrationDigest;
296
+ const integrationManifestSources = skillsDirs.flatMap((skillsDir) => {
297
+ const discovered = discoverSkillIds(skillsDir);
298
+ return discovered
299
+ .filter((id) => select === undefined || select.includes(id))
300
+ .map((skillId) => ({ skillId, skillDir: skillsDir }));
301
+ });
302
+ for (const [dirIndex, skillsDir] of skillsDirs.entries()) {
293
303
  const r = await runInit({
294
304
  target,
295
305
  skillsDir,
@@ -297,12 +307,27 @@ async function installSkills(opts) {
297
307
  force,
298
308
  enrich,
299
309
  ...(select !== undefined ? { select } : {}),
310
+ ...(opts.noHooks !== undefined ? { noHooks: opts.noHooks } : {}),
311
+ ...(dirIndex === 0
312
+ ? {
313
+ integrationManifestSources,
314
+ ...(opts.noIntegrations !== undefined ? { noIntegrations: opts.noIntegrations } : {}),
315
+ }
316
+ : { noIntegrations: true }),
317
+ ...(opts.noVerify !== undefined ? { noVerify: opts.noVerify } : {}),
318
+ ...(opts.allowIntegrations !== undefined ? { allowIntegrations: opts.allowIntegrations } : {}),
300
319
  });
301
320
  for (const skill of r.skills) {
302
321
  results.push({ id: skill.id, written: skill.written.length, skipped: skill.skipped.length });
303
322
  }
304
323
  failures.push(...r.failures);
305
324
  applyFailures.push(...r.applyFailures);
325
+ if (r.integrations.some((row) => row.status !== 'not-requested'))
326
+ integrations = [...r.integrations];
327
+ else if (integrations.length === 0)
328
+ integrations = [...r.integrations];
329
+ if (r.integrationDigest !== undefined)
330
+ integrationDigest = r.integrationDigest;
306
331
  }
307
332
  let written = 0;
308
333
  let skipped = 0;
@@ -312,7 +337,7 @@ async function installSkills(opts) {
312
337
  }
313
338
  const installed = new Set(results.map((s) => s.id));
314
339
  const missing = select !== undefined ? [...select].filter((id) => !installed.has(id)) : [];
315
- return { results, dirsSearched: skillsDirs.length, written, skipped, missing, failures, applyFailures };
340
+ return { results, dirsSearched: skillsDirs.length, written, skipped, missing, failures, applyFailures, integrations, ...(integrationDigest !== undefined ? { integrationDigest } : {}) };
316
341
  }
317
342
  /** Warn about preset/select ids that weren't found in any installed pack. */
318
343
  function writeMissingSkillsHint(write, missing, presetName) {
@@ -375,17 +400,15 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
375
400
  select,
376
401
  force: flags.has('force'),
377
402
  enrich: flags.has('enrich'),
403
+ noHooks: flags.has('no-hooks'),
404
+ noIntegrations: flags.has('no-integrations'),
405
+ noVerify: flags.has('no-verify'),
406
+ ...(options.get('allow-integrations') !== undefined ? { allowIntegrations: options.get('allow-integrations') } : {}),
378
407
  });
379
- write(`dz init --target ${target}: ${r.results.length} skill(s), ${r.written} file(s) written, ${r.skipped} skipped`);
380
- if (r.dirsSearched > 1) {
381
- write(` (searched ${r.dirsSearched} skill directories)`);
382
- }
383
- writeMissingSkillsHint(write, r.missing, presetName);
384
- // Codex-targeted init DELIVERS the hooks and verifies them (ADR-001 §8). Skills alone are not the
385
- // target's harness: the veto + recall legs are what `--target codex` promises.
386
- // `--no-hooks` is the documented escape for "skills only" (the same flag `dz setup` already
387
- // carries): hook delivery writes USER-GLOBAL config, so a command that only wants skills compiled
388
- // must be able to say so — and every test that is about skills says it.
408
+ // Codex keeps its established user-registry writer, but its result is normalized into the same
409
+ // two-outcome contract before JSON/human rendering. A write without a live ready observation is
410
+ // a refusal with applied=true, never a second success channel.
411
+ let integrationOutcomes = r.integrations;
389
412
  let codexHooksOk = true;
390
413
  if (target === 'codex' && !flags.has('no-hooks')) {
391
414
  const delivery = deliverCodexHooks({ project: projectRoot, verify: !flags.has('no-verify') }, undefined, 'dz init');
@@ -394,7 +417,30 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
394
417
  write(line);
395
418
  for (const line of delivery.stderr)
396
419
  writeErr(line);
420
+ const hookIndex = integrationOutcomes.findIndex((row) => row.component === 'hooks' && row.status !== 'not-requested');
421
+ if (hookIndex !== -1) {
422
+ const hook = normalizeCodexHookOutcome(integrationOutcomes[hookIndex], delivery, flags.has('no-verify'));
423
+ integrationOutcomes = integrationOutcomes.map((row, index) => index === hookIndex ? hook : row);
424
+ }
397
425
  }
426
+ if (flags.has('json')) {
427
+ write(JSON.stringify({ target, skills: { count: r.results.length, written: r.written, skipped: r.skipped }, integrations: integrationOutcomes, integrationDigest: r.integrationDigest ?? null }));
428
+ }
429
+ else {
430
+ write(`dz init --target ${target}: ${r.results.length} skill(s), ${r.written} file(s) written, ${r.skipped} skipped`);
431
+ if (r.dirsSearched > 1)
432
+ write(` (searched ${r.dirsSearched} skill directories)`);
433
+ for (const outcome of integrationOutcomes) {
434
+ const label = outcome.component === 'mcp' ? 'MCP' : 'Hooks';
435
+ const detail = outcome.status === 'refused'
436
+ ? `${outcome.reasonCode ?? 'LIVE_PROBE_FAILED'}${outcome.remediation ? ` — ${outcome.remediation}` : ''}`
437
+ : outcome.status === 'emitted'
438
+ ? `${outcome.carrier?.path ?? 'registered'}${outcome.registrations.some((row) => row.approval === 'pending') ? '; Pending approval; ready=false' : ''}`
439
+ : 'explicitly not requested';
440
+ write(`${label}: ${outcome.status.toUpperCase()} (${detail})`);
441
+ }
442
+ }
443
+ writeMissingSkillsHint(write, r.missing, presetName);
398
444
  // Skip-and-collect must not become skip-and-SILENCE: a skill that failed to load is
399
445
  // named on stderr and the command exits 1 (it exited 1 before too — by throwing).
400
446
  if (r.failures.length > 0 || r.applyFailures.length > 0) {
@@ -417,7 +463,38 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
417
463
  writeErr(line);
418
464
  return 1;
419
465
  }
420
- return codexHooksOk ? 0 : 1;
466
+ const integrationOk = !integrationOutcomes.some((row) => row.status === 'refused');
467
+ return codexHooksOk && integrationOk ? 0 : 1;
468
+ }
469
+ function cmdIntegrationsVerify(options, flags, cwd, write, writeErr) {
470
+ const targetInput = options.get('target');
471
+ const component = options.get('component');
472
+ if (targetInput === undefined || (component !== 'mcp' && component !== 'hooks')) {
473
+ writeErr('dz integrations-verify: requires --target <name> --component <mcp|hooks>');
474
+ return 1;
475
+ }
476
+ const resolution = resolveTargetName(targetInput);
477
+ if (resolution.kind === 'unknown') {
478
+ for (const line of formatTargetProblem('dz integrations-verify', resolution))
479
+ writeErr(line);
480
+ return 1;
481
+ }
482
+ if (resolution.via === 'alias')
483
+ writeErr(formatTargetAliasNote('dz integrations-verify', targetInput, resolution.target));
484
+ const result = runIntegrationsVerify({
485
+ target: resolution.target,
486
+ component,
487
+ projectRoot: resolve(cwd, options.get('project') ?? '.'),
488
+ });
489
+ if (flags.has('json'))
490
+ write(JSON.stringify(result));
491
+ else if (result.ok) {
492
+ write(`dz integrations-verify: ${resolution.target}/${component} registered (runtime ${result.runtimeVersion ?? 'unknown'}; ready=${result.registrations.every((row) => row.ready === true)})`);
493
+ }
494
+ else {
495
+ writeErr(`dz integrations-verify: ${resolution.target}/${component} REFUSED (${result.reasonCode ?? 'LIVE_PROBE_FAILED'}) — ${result.remediation ?? 'no qualifying receipt'}`);
496
+ }
497
+ return result.ok ? 0 : 1;
421
498
  }
422
499
  async function cmdVerify(options, cwd, write, writeErr) {
423
500
  const skillsDir = resolve(cwd, options.get('skills-dir') ?? '.claude/skills');
@@ -1869,6 +1946,150 @@ function cmdStatuslineFaRecord(options, cwd, write) {
1869
1946
  write(`dz statusline: recorded /feature-adr learning state for "${slug}" (${step}) — 🎓 ${state.pool} pool · ↑${state.recalled} used · +${state.stored} new · ↻${state.reinforced ?? 0} reinforced`);
1870
1947
  return 0;
1871
1948
  }
1949
+ function statuslineEtaStage(step) {
1950
+ const match = step.match(/\bStep\s*-?\s*(0|[1-9](?:\.5)?)(?:\b|\s)/i);
1951
+ if (match === null)
1952
+ return undefined;
1953
+ const value = Number(match[1]);
1954
+ if (value === 0)
1955
+ return 'router';
1956
+ if (value > 0 && value < 6)
1957
+ return 'design';
1958
+ if (value === 6)
1959
+ return 'plan';
1960
+ if (value === 7)
1961
+ return 'code';
1962
+ if (value === 8)
1963
+ return 'qe';
1964
+ if (value === 9)
1965
+ return 'fleet';
1966
+ return undefined;
1967
+ }
1968
+ function routerMetadata(router) {
1969
+ const result = router?.result;
1970
+ if (result === null || typeof result !== 'object')
1971
+ return {};
1972
+ const record = result;
1973
+ const tier = typeof record['tier'] === 'string' && record['tier'].length > 0 ? record['tier'] : undefined;
1974
+ const activeSteps = Array.isArray(record['activeSteps'])
1975
+ ? record['activeSteps'].filter((value) => typeof value === 'number' && Number.isFinite(value))
1976
+ : undefined;
1977
+ return {
1978
+ ...(tier !== undefined ? { tier } : {}),
1979
+ ...(activeSteps !== undefined ? { activeSteps } : {}),
1980
+ };
1981
+ }
1982
+ function activeCheckpointStages(tier, activeSteps) {
1983
+ const stages = new Set();
1984
+ if (activeSteps !== undefined) {
1985
+ for (const step of activeSteps) {
1986
+ if (step === 0)
1987
+ stages.add('router');
1988
+ else if (step > 0 && step < 6)
1989
+ stages.add('design');
1990
+ else if (step === 6)
1991
+ stages.add('plan');
1992
+ else if (step === 7)
1993
+ stages.add('code');
1994
+ else if (step === 8)
1995
+ stages.add('qe');
1996
+ else if (step === 9)
1997
+ stages.add('fleet');
1998
+ }
1999
+ }
2000
+ else if (tier !== undefined) {
2001
+ for (const stage of CHECKPOINT_STAGES) {
2002
+ if (stage !== 'fleet' || tier === 'L' || tier === 'XL')
2003
+ stages.add(stage);
2004
+ }
2005
+ }
2006
+ return CHECKPOINT_STAGES.filter((stage) => stages.has(stage));
2007
+ }
2008
+ /** All checkpoint I/O for ETA lives in this CLI-only wrapper and degrades per file. */
2009
+ function readEtaCorpus(projectRoot, currentSlug) {
2010
+ const trace = process.env['DZ_ETA_TRACE'];
2011
+ if (trace !== undefined && trace.length > 0) {
2012
+ try {
2013
+ appendFileSync(trace, `${JSON.stringify({ projectRoot, currentSlug, ts: new Date().toISOString() })}\n`);
2014
+ }
2015
+ catch { /* a test/debug receipt can never take down the statusline */ }
2016
+ }
2017
+ const samples = [];
2018
+ let currentSegments = [];
2019
+ let hasCurrentCheckpoints = false;
2020
+ let entries = [];
2021
+ try {
2022
+ entries = readdirSync(join(projectRoot, 'features'), { withFileTypes: true });
2023
+ }
2024
+ catch {
2025
+ return {
2026
+ history: [], currentRunSamples: [], currentTier: undefined, activeStages: [],
2027
+ lastCheckpointTsMs: undefined, hasCurrentCheckpoints: false,
2028
+ };
2029
+ }
2030
+ for (const entry of entries) {
2031
+ if (!entry.isDirectory())
2032
+ continue;
2033
+ const checkpointPath = join(projectRoot, 'features', entry.name, '.fa-state', 'checkpoints.jsonl');
2034
+ try {
2035
+ const text = readFileSync(checkpointPath, 'utf8');
2036
+ const observations = parseCheckpointLines(text, entry.name);
2037
+ const segments = segmentRun(observations);
2038
+ samples.push(...extractStageSamples(segments));
2039
+ if (entry.name === currentSlug) {
2040
+ hasCurrentCheckpoints = observations.length > 0;
2041
+ currentSegments = segments;
2042
+ }
2043
+ }
2044
+ catch {
2045
+ // Missing, unreadable, or racing append: this run contributes no evidence.
2046
+ }
2047
+ }
2048
+ const lastSegment = currentSegments.at(-1);
2049
+ const currentRouter = lastSegment?.router;
2050
+ const invocationSegments = currentRouter === undefined
2051
+ ? []
2052
+ : currentSegments.filter((segment) => segment.router === currentRouter);
2053
+ const invocationRunIds = new Set(invocationSegments.map((segment) => segment.runId));
2054
+ const currentRunSamples = extractStageSamples(invocationSegments);
2055
+ const metadata = routerMetadata(currentRouter);
2056
+ const currentTimestamps = invocationSegments
2057
+ .flatMap((segment) => segment.observations)
2058
+ .flatMap((observation) => observation.tsMs === undefined ? [] : [observation.tsMs]);
2059
+ return {
2060
+ history: samples.filter((sample) => !invocationRunIds.has(sample.runId)),
2061
+ currentRunSamples,
2062
+ currentTier: metadata.tier,
2063
+ activeStages: activeCheckpointStages(metadata.tier, metadata.activeSteps),
2064
+ lastCheckpointTsMs: currentTimestamps.length > 0 ? Math.max(...currentTimestamps) : undefined,
2065
+ hasCurrentCheckpoints,
2066
+ };
2067
+ }
2068
+ function statuslineEta(projectRoot, state, nowMs) {
2069
+ const currentStage = statuslineEtaStage(state.step);
2070
+ if (currentStage === undefined)
2071
+ return undefined;
2072
+ const corpus = readEtaCorpus(projectRoot, state.slug);
2073
+ const currentIndex = CHECKPOINT_STAGES.indexOf(currentStage);
2074
+ const remainingStages = corpus.activeStages.filter((stage) => CHECKPOINT_STAGES.indexOf(stage) >= currentIndex && stage !== 'router');
2075
+ // Preserve honest absence as a machine-readable union member. The text formatter intentionally
2076
+ // omits both variants, but `--json` must still distinguish no file from a file with no tier.
2077
+ const stagesForEstimate = remainingStages.length > 0
2078
+ ? remainingStages
2079
+ : (currentStage === 'router' ? [] : [currentStage]);
2080
+ if (stagesForEstimate.length === 0 && corpus.hasCurrentCheckpoints && corpus.currentTier !== undefined)
2081
+ return undefined;
2082
+ return estimateEta({
2083
+ samples: corpus.history,
2084
+ currentTier: corpus.currentTier,
2085
+ currentStage,
2086
+ remainingStages: stagesForEstimate,
2087
+ currentRunSamples: corpus.currentRunSamples,
2088
+ nowMs,
2089
+ ...(corpus.lastCheckpointTsMs !== undefined ? { lastCheckpointTsMs: corpus.lastCheckpointTsMs } : {}),
2090
+ hasCurrentCheckpoints: corpus.hasCurrentCheckpoints,
2091
+ });
2092
+ }
1872
2093
  /**
1873
2094
  * `dz statusline` — render dz's OWN self-learning counts as one compact, emoji-tagged line for
1874
2095
  * Claude Code's status bar (modeled on agentic-qe's "🎓 12 patterns"). Claude Code pipes a JSON
@@ -1888,8 +2109,23 @@ function cmdStatusline(options, flags, cwd, write, readStdin) {
1888
2109
  try {
1889
2110
  const projectRoot = statuslineProjectRoot(readStdin(), options, cwd);
1890
2111
  const data = statuslineData(projectRoot);
2112
+ const fa = data.featureAdr;
2113
+ let eta;
2114
+ let etaFragment;
2115
+ if (fa !== undefined && fa.kind !== 'loop') {
2116
+ try {
2117
+ eta = statuslineEta(projectRoot, fa, Date.now());
2118
+ if (eta !== undefined)
2119
+ etaFragment = formatEta(eta);
2120
+ }
2121
+ catch {
2122
+ // ETA is advisory: any corpus/resource failure omits only this fragment.
2123
+ eta = undefined;
2124
+ etaFragment = undefined;
2125
+ }
2126
+ }
1891
2127
  if (flags.has('json')) {
1892
- write(JSON.stringify(data));
2128
+ write(JSON.stringify({ ...data, ...(eta !== undefined ? { eta } : {}) }));
1893
2129
  return 0;
1894
2130
  }
1895
2131
  let line = `🎓 dz: ${data.patterns} patterns${data.usedPatterns !== undefined ? ` · ${data.usedPatterns} used` : ''} · 🧠 ${data.brainSources} sources`;
@@ -1899,14 +2135,13 @@ function cmdStatusline(options, flags, cwd, write, readStdin) {
1899
2135
  if (data.consolidatedAgeH !== undefined)
1900
2136
  line += ` · ⟳ ${data.consolidatedAgeH}h`;
1901
2137
  // Live /feature-adr run in flight → PREPEND the pipeline learning segment to the base dz line.
1902
- const fa = data.featureAdr;
1903
2138
  if (fa !== undefined) {
1904
2139
  // 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.
1905
2140
  if (fa.kind === 'loop') {
1906
2141
  line = `🔁 loop ${fa.step} · ${line}`;
1907
2142
  }
1908
2143
  else {
1909
- line = `📐 feature-adr ${fa.step} · 🎓 ${fa.pool} pool · ↑${fa.recalled} used · +${fa.stored} new · ↻${fa.reinforced ?? 0} reinforced · ${line}`;
2144
+ line = `📐 feature-adr ${fa.step} · ${etaFragment !== undefined ? `${etaFragment} · ` : ''}🎓 ${fa.pool} pool · ↑${fa.recalled} used · +${fa.stored} new · ↻${fa.reinforced ?? 0} reinforced · ${line}`;
1910
2145
  }
1911
2146
  }
1912
2147
  write(line);
@@ -2493,7 +2728,12 @@ function learningStoreLine(storeRoot, projectOption, target, verb) {
2493
2728
  const reason = teachReasonPhrase(target.reason);
2494
2729
  return storeLocationLine(describeStoreLocation(storeRoot, projectOption, target.store === 'global' ? 'global' : undefined), verb) + (reason ? ' [' + reason + ']' : '');
2495
2730
  }
2496
- async function cmdTeach(options, flags, cwd, write) {
2731
+ async function runTeachGuardReinforcement(projectRoot, dzId, reward) {
2732
+ const backend = resolveLearningBackend(projectRoot);
2733
+ backend.addSample({ dzId, kind: 'reinforce', reward, ts: new Date().toISOString() });
2734
+ return backend.train();
2735
+ }
2736
+ async function cmdTeach(options, flags, cwd, write, writeErr = (line) => { console.error(line); }, interactive = false, guardRunner = teachGuard, reinforceRunner = runTeachGuardReinforcement) {
2497
2737
  // WHICH store this lesson belongs to, and WHO decided (teach-chooses-its-store).
2498
2738
  // `--to` → `DZ_LEARN` → `.dz/config.json` learning.teachTo → project. The owner asked for a
2499
2739
  // per-session choice; for a CLI every invocation is a fresh process, so the only honest session
@@ -2584,26 +2824,39 @@ async function cmdTeach(options, flags, cwd, write) {
2584
2824
  write('dz teach --from-json: expected a JSON array (produced by `dz recall --all --json`)');
2585
2825
  return 1;
2586
2826
  }
2587
- const existing = new Set(loadStorePatternsSync(storeRoot).map((p) => p.pattern));
2827
+ const importedKey = (p) => p.lessonForm !== undefined && p.lessonPairId !== undefined
2828
+ ? `${p.pattern}\u0000${p.lessonForm}\u0000${p.lessonPairId}`
2829
+ : `legacy\u0000${p.pattern}`;
2830
+ const existing = new Set(loadStorePatternsSync(storeRoot).map(importedKey));
2588
2831
  let imported = 0;
2589
2832
  let skipped = 0;
2590
2833
  const importedRecs = [];
2591
2834
  for (const item of parsed) {
2592
2835
  const p = item;
2593
- if (!p || typeof p.pattern !== 'string' || p.pattern.trim() === '' || existing.has(p.pattern)) {
2836
+ if (!p || typeof p !== 'object') {
2594
2837
  skipped += 1;
2595
2838
  continue;
2596
2839
  }
2597
- const rec = {
2598
- pattern: p.pattern,
2840
+ const pair = (p.lessonForm === 'specific' || p.lessonForm === 'class')
2841
+ && typeof p.lessonPairId === 'string' && p.lessonPairId !== ''
2842
+ ? { lessonForm: p.lessonForm, lessonPairId: p.lessonPairId }
2843
+ : {};
2844
+ const candidate = {
2845
+ pattern: typeof p.pattern === 'string' ? p.pattern : '',
2599
2846
  type: (typeof p.type === 'string' ? p.type : 'lesson-learned'),
2600
2847
  reward: typeof p.reward === 'number' ? Math.max(0, Math.min(1, p.reward)) : 0.8,
2601
2848
  domain: typeof p.domain === 'string' ? p.domain : 'general',
2602
2849
  ts: typeof p.ts === 'string' ? p.ts : new Date().toISOString(),
2603
2850
  source: 'dz-teach-import',
2851
+ ...pair,
2604
2852
  };
2853
+ if (candidate.pattern.trim() === '' || existing.has(importedKey(candidate))) {
2854
+ skipped += 1;
2855
+ continue;
2856
+ }
2857
+ const rec = candidate;
2605
2858
  await recordPattern(storeRoot, rec);
2606
- existing.add(p.pattern);
2859
+ existing.add(importedKey(rec));
2607
2860
  importedRecs.push(rec);
2608
2861
  imported += 1;
2609
2862
  }
@@ -2686,23 +2939,32 @@ async function cmdTeach(options, flags, cwd, write) {
2686
2939
  }
2687
2940
  const reward = parseFloat(options.get('reward') ?? '0.8');
2688
2941
  const domain = options.get('domain') ?? 'general';
2942
+ const classWasRequested = options.has('class-form') || flags.has('class-form');
2943
+ const lessonForms = normalizeLessonForms(pattern, options.get('class-form'));
2944
+ let guardedExisting;
2689
2945
  if (flags.has('guard')) {
2690
- const verdict = await teachGuard(storeRoot, pattern, { reward: Math.max(0, Math.min(1, reward)) });
2946
+ const verdict = await guardRunner(storeRoot, pattern, { reward: Math.max(0, Math.min(1, reward)) });
2691
2947
  if (verdict.action === 'reinforce') {
2692
- const backend = resolveLearningBackend(storeRoot);
2693
- backend.addSample({ dzId: verdict.dzId, kind: 'reinforce', reward: Math.max(0, Math.min(1, reward)), ts: new Date().toISOString() });
2694
- const trained = await backend.train();
2695
- // HIGH-fix: only claim success when the reinforce actually FLUSHED. With backend 'off'
2696
- // (NoopLearningBackend) or a flush failure, flushed === 0 — falling through to the plain
2697
- // teach below so the lesson is NEVER silently discarded (the exact silent-drop the ADR forbids).
2698
- if (trained.flushed > 0) {
2699
- write(`↳ reinforced existing pattern ${verdict.dzId} (cos=${verdict.cosine.toFixed(2)}) — not re-added`);
2700
- const clearedQ = clearAgentdbQuarantine(storeRoot, [verdict.dzId]);
2701
- if (clearedQ.cleared > 0)
2702
- write(' ↳ promoted out of quarantine (mirror updated)');
2703
- return 0;
2948
+ if (lessonForms.classForm !== undefined) {
2949
+ const existing = loadStoreRecords(storeRoot).find((record) => record.id === verdict.dzId);
2950
+ if (existing !== undefined) {
2951
+ guardedExisting = { dzId: verdict.dzId, cosine: verdict.cosine, pattern: recordToPattern(existing) };
2952
+ }
2953
+ else {
2954
+ write(`dz teach --guard: matched pattern ${verdict.dzId} was not found in the lexical store — teaching the lesson normally`);
2955
+ }
2956
+ }
2957
+ else {
2958
+ const trained = await reinforceRunner(storeRoot, verdict.dzId, Math.max(0, Math.min(1, reward)));
2959
+ if (trained.flushed > 0) {
2960
+ write(`↳ reinforced existing pattern ${verdict.dzId} (cos=${verdict.cosine.toFixed(2)}) — not re-added`);
2961
+ const clearedQ = clearAgentdbQuarantine(storeRoot, [verdict.dzId]);
2962
+ if (clearedQ.cleared > 0)
2963
+ write(' ↳ promoted out of quarantine (mirror updated)');
2964
+ return 0;
2965
+ }
2966
+ write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
2704
2967
  }
2705
- write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
2706
2968
  }
2707
2969
  }
2708
2970
  // Distill pattern into actionable rule (claude-smart inspired)
@@ -2715,8 +2977,8 @@ async function cmdTeach(options, flags, cwd, write) {
2715
2977
  // Typed through the shared schema (harness-core owns PatternRecord) so the
2716
2978
  // write side and the read side (recommend's loadPatterns) can never drift —
2717
2979
  // a field rename here is a compile error, not a silently re-muted loop (audit #2).
2718
- const entry = {
2719
- pattern,
2980
+ const entry = guardedExisting?.pattern ?? {
2981
+ pattern: lessonForms.specific,
2720
2982
  type: type,
2721
2983
  reward: Math.max(0, Math.min(1, reward)),
2722
2984
  domain,
@@ -2728,9 +2990,47 @@ async function cmdTeach(options, flags, cwd, write) {
2728
2990
  // total count. The lossy `npx agentdb add` dual-write was removed in Tier-1 (audit #6).
2729
2991
  // lesson-quarantine (opt-in): a fresh lesson is a HYPOTHESIS until it earns promotion.
2730
2992
  const quarantineOn = readMemoryLearningConfig(storeRoot).quarantine;
2731
- const count = await recordPattern(storeRoot, entry, quarantineOn ? { quarantine: true } : {});
2732
- write(`Learned: "${pattern.slice(0, 60)}${pattern.length > 60 ? '...' : ''}"`);
2733
- write(` Domain: ${domain} Reward: ${reward} Backend: memory (@dzhechkov/memory)`);
2993
+ const stored = await recordLessonForms(storeRoot, entry, lessonForms.classForm, quarantineOn ? { quarantine: true } : {});
2994
+ let recordsToMirror = stored.records;
2995
+ let commandFailed = stored.class === 'failed';
2996
+ let reinforced = false;
2997
+ if (guardedExisting !== undefined) {
2998
+ let reinforceId = guardedExisting.dzId;
2999
+ if (stored.class === 'stored') {
3000
+ const specificRow = stored.records.find((row) => row.lessonForm === 'specific');
3001
+ if (specificRow !== undefined)
3002
+ reinforceId = patternRecordId(specificRow);
3003
+ if (reinforceId !== guardedExisting.dzId) {
3004
+ const removed = removePatternsByIds(storeRoot, new Set([guardedExisting.dzId]));
3005
+ if (removed.error !== undefined || removed.removed === 0) {
3006
+ commandFailed = true;
3007
+ writeErr(`dz teach --guard: class pair stored, but old pattern cleanup failed${removed.error === undefined ? '' : ` — ${removed.error}`}`);
3008
+ }
3009
+ }
3010
+ }
3011
+ else if (stored.class === 'failed') {
3012
+ const partialSpecific = stored.records.find((row) => row.lessonForm === 'specific');
3013
+ if (partialSpecific !== undefined && patternRecordId(partialSpecific) !== guardedExisting.dzId) {
3014
+ const rolledBack = removePatternsByIds(storeRoot, new Set([patternRecordId(partialSpecific)]));
3015
+ if (rolledBack.error !== undefined)
3016
+ writeErr(`dz teach --guard: partial enrichment rollback failed — ${rolledBack.error}`);
3017
+ }
3018
+ recordsToMirror = [];
3019
+ reinforceId = guardedExisting.dzId;
3020
+ }
3021
+ if (!commandFailed) {
3022
+ const trained = await reinforceRunner(storeRoot, reinforceId, Math.max(0, Math.min(1, reward)));
3023
+ reinforced = trained.flushed > 0;
3024
+ if (reinforced) {
3025
+ const clearedQ = clearAgentdbQuarantine(storeRoot, [reinforceId]);
3026
+ if (clearedQ.cleared > 0)
3027
+ write(' ↳ promoted out of quarantine (mirror updated)');
3028
+ }
3029
+ }
3030
+ }
3031
+ const count = guardedExisting === undefined ? stored.count : loadStorePatternsSync(storeRoot).length;
3032
+ write(`${guardedExisting !== undefined && stored.class === 'stored' ? 'Enriched' : 'Learned'}: "${pattern.slice(0, 60)}${pattern.length > 60 ? '...' : ''}"`);
3033
+ write(` Domain: ${entry.domain} Reward: ${entry.reward} Backend: memory (@dzhechkov/memory)`);
2734
3034
  write(` Total patterns: ${count}`);
2735
3035
  // WHERE the write landed. MEASURED before this line existed: teach printed the pattern, the
2736
3036
  // domain, the reward and the backend — and not one word about the path, so a store written to
@@ -2740,6 +3040,24 @@ async function cmdTeach(options, flags, cwd, write) {
2740
3040
  // sees a path, cannot tell what chose it, and has no reason to question it. `default` adds
2741
3041
  // nothing, so the line stays byte-identical for everyone who set nothing.
2742
3042
  write(storeLine('written'));
3043
+ if (stored.class === 'stored')
3044
+ write(' Class form: stored separately and linked to the specific lesson');
3045
+ if (classWasRequested && stored.class === 'absent')
3046
+ write(' Class form: skipped; the specific lesson was saved');
3047
+ if (stored.class === 'rejected')
3048
+ writeErr(`dz teach: class form rejected — ${stored.reason ?? 'invalid class form'}; the specific lesson was saved`);
3049
+ if (stored.class === 'failed')
3050
+ writeErr(`dz teach: specific stored; class failed — ${stored.reason ?? 'unknown storage failure'}`);
3051
+ if (guardedExisting !== undefined && stored.class === 'stored') {
3052
+ write(commandFailed
3053
+ ? ' Guard: class enrichment stored; reinforcement skipped because old-pattern cleanup failed'
3054
+ : reinforced
3055
+ ? ` Guard: enriched existing pattern (cos=${guardedExisting.cosine.toFixed(2)}), then reinforced the linked specific form`
3056
+ : ' Guard: class enrichment stored; reinforcement did not flush');
3057
+ }
3058
+ if (!classWasRequested && interactive) {
3059
+ 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.');
3060
+ }
2743
3061
  // ADVICE, not a gate. Someone putting medical lessons in a shared store owns both
2744
3062
  // directories and this binary; refusing would be defending a user against themselves,
2745
3063
  // which this design does not attempt. Making the choice INFORMED is the part that is
@@ -2758,8 +3076,8 @@ async function cmdTeach(options, flags, cwd, write) {
2758
3076
  write(' ⚠ quarantined: excluded from auto-inject, damped in recall — promote by confirming it (dz teach --reinforce "<text>") or dz recall --promote <dzId> --apply');
2759
3077
  }
2760
3078
  // The lexical write above is durable — the vector mirror is strictly best-effort (I-3).
2761
- await emitMirrorQ(storeRoot, [entry], 'dz-teach', quarantineOn);
2762
- return 0;
3079
+ await emitMirrorQ(storeRoot, recordsToMirror, 'dz-teach', quarantineOn);
3080
+ return commandFailed ? 1 : 0;
2763
3081
  }
2764
3082
  async function cmdConsolidate(options, flags, cwd, write) {
2765
3083
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
@@ -3119,7 +3437,7 @@ async function cmdRecallPromote(options, flags, projectRoot, write) {
3119
3437
  write(` not found: ${res.notFound.join(', ')}`);
3120
3438
  return 0;
3121
3439
  }
3122
- async function cmdRecall(options, flags, cwd, write) {
3440
+ async function cmdRecall(options, flags, cwd, write, writeErr, classMatcher) {
3123
3441
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
3124
3442
  const asJson = flags.has('json');
3125
3443
  const all = flags.has('all');
@@ -3140,7 +3458,15 @@ async function cmdRecall(options, flags, cwd, write) {
3140
3458
  // review could not make correct: a tag set by the writer is decidable, prose is not.
3141
3459
  const allPatterns = loadStorePatternsSync(projectRoot);
3142
3460
  const holdout = applyExportHoldout(allPatterns, heldOutAfterOptIn(options.get('include-domain')));
3143
- const patterns = holdout.exported;
3461
+ const classByPair = new Map(holdout.exported
3462
+ .filter((row) => row.lessonForm === 'class' && row.lessonPairId !== undefined)
3463
+ .map((row) => [row.lessonPairId, row.pattern]));
3464
+ const patterns = holdout.exported.map((row) => {
3465
+ const classForm = row.lessonForm === 'specific' && row.lessonPairId !== undefined
3466
+ ? classByPair.get(row.lessonPairId)
3467
+ : undefined;
3468
+ return classForm === undefined ? row : { ...row, classForm };
3469
+ });
3144
3470
  const holdoutNote = renderHoldoutNote(holdout);
3145
3471
  // The opt-in is honoured without argument — and named out loud. A flag that silently
3146
3472
  // includes medical lessons in a portable export is a flag whose consequence the user
@@ -3170,7 +3496,15 @@ async function cmdRecall(options, flags, cwd, write) {
3170
3496
  const rawStats = storeStats(projectRoot);
3171
3497
  const topHoldout = applyExportHoldout(rawStats.topUses, heldOutAfterOptIn(options.get('include-domain')));
3172
3498
  const perDomain = Object.fromEntries(Object.entries(rawStats.perDomain).filter(([d]) => !holdout.domains.includes(canonicalDomainKey(d))));
3173
- const stats = { ...rawStats, topUses: topHoldout.exported, perDomain };
3499
+ const generalizedPairs = new Set(patterns.flatMap((p) => p.lessonPairId === undefined ? [] : [p.lessonPairId]));
3500
+ const unpairedLessons = patterns.filter((p) => p.lessonPairId === undefined).length;
3501
+ const stats = {
3502
+ ...rawStats,
3503
+ topUses: topHoldout.exported,
3504
+ perDomain,
3505
+ generalized: generalizedPairs.size,
3506
+ logicalLessons: generalizedPairs.size + unpairedLessons,
3507
+ };
3174
3508
  const backendStats = resolveLearningBackend(projectRoot).getStats();
3175
3509
  if (asJson) {
3176
3510
  write(JSON.stringify({ patterns, stats, learning: backendStats, withheld: holdout.withheld.length, withheldDomains: holdout.domains }));
@@ -3184,6 +3518,7 @@ async function cmdRecall(options, flags, cwd, write) {
3184
3518
  write(` backend: ${backendStats.backend}${backendStats.advisory !== undefined ? ` (${backendStats.advisory})` : ''}`);
3185
3519
  write(` domains: ${Object.entries(stats.perDomain).map(([k, v]) => `${k}=${v}`).join(', ') || 'none'}`);
3186
3520
  write(` exact-dup groups: ${stats.exactDupGroups}`);
3521
+ write(` generalized: ${stats.generalized} of ${stats.logicalLessons} lessons`);
3187
3522
  write(` re-teach trend: ${stats.teachEvents} teach event(s), ${stats.reinforceEvents} reinforce event(s)`);
3188
3523
  write(' top uses:');
3189
3524
  for (const row of stats.topUses)
@@ -3311,6 +3646,10 @@ async function cmdRecall(options, flags, cwd, write) {
3311
3646
  ? 'lexical'
3312
3647
  : 'hybrid';
3313
3648
  const wantedDomain = options.get('domain');
3649
+ const classRecallOptions = {
3650
+ onClassDegraded: writeErr,
3651
+ ...(classMatcher === undefined ? {} : { classMatcher }),
3652
+ };
3314
3653
  // OVER-FETCH before boosting (Codex QE #5): the boost used to run on hits ALREADY
3315
3654
  // truncated to `limit`, so an exact-domain lesson sitting at rank limit+1 could
3316
3655
  // never receive its promised lift — the feature was weakest in exactly the case it
@@ -3333,8 +3672,8 @@ async function cmdRecall(options, flags, cwd, write) {
3333
3672
  // second occurrence. Foreign stdout is routed to stderr for the duration of the engine call — our
3334
3673
  // own output is written after it returns.
3335
3674
  const result = asJson
3336
- ? await withForeignStdoutOnStderr(() => recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
3337
- : await recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
3675
+ ? await withForeignStdoutOnStderr(() => recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
3676
+ : await recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
3338
3677
  if (mode === 'semantic' && result.vectorEngine === 'none') {
3339
3678
  // --semantic is an explicit ask — degrading it silently would be dishonest (FR-3).
3340
3679
  const why = result.vectorReason ?? 'no vector engine available — run: dz setup --memory agentdb';
@@ -3368,13 +3707,27 @@ async function cmdRecall(options, flags, cwd, write) {
3368
3707
  let globalHits = [];
3369
3708
  if (readGlobal) {
3370
3709
  const g = asJson
3371
- ? await withForeignStdoutOnStderr(() => recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
3372
- : await recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
3710
+ ? await withForeignStdoutOnStderr(() => recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
3711
+ : await recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...classRecallOptions, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
3373
3712
  globalHits = g.hits;
3374
3713
  }
3714
+ const projectHits = boost ? boost.hits : result.hits;
3715
+ // TWO different keys on purpose, and they must not be unified. The cross-store MERGE key is the
3716
+ // lesson TEXT: the same lesson taught into the project store and the global store is one lesson
3717
+ // shown once, labelled `both`, and those two records legitimately differ in timestamp, reward and
3718
+ // domain. The matchedForm LOOKUP key may be narrower. Folding the merge key into
3719
+ // patternIdentityOf (which includes ts) made two `dz teach` calls of identical text stop
3720
+ // collapsing — it reddened the pre-existing P3 case in recall-reads-both-stores.test.ts.
3721
+ const mergeKey = (hit) => hit.pattern.pattern;
3722
+ const globalByIdentity = new Map(globalHits.map((hit) => [mergeKey(hit), hit]));
3375
3723
  const merged = readGlobal
3376
- ? mergeStoreHits(boost ? boost.hits : result.hits, globalHits, (h) => h.pattern.pattern)
3377
- : (boost ? boost.hits : result.hits);
3724
+ ? mergeStoreHits(projectHits, globalHits, mergeKey).map((hit) => {
3725
+ if (hit.origin !== 'both')
3726
+ return hit;
3727
+ const matchedForm = mergeLessonMatchedForms(hit.matchedForm, globalByIdentity.get(mergeKey(hit))?.matchedForm);
3728
+ return matchedForm === undefined ? hit : { ...hit, matchedForm };
3729
+ })
3730
+ : projectHits;
3378
3731
  const hits = merged.slice(0, limit);
3379
3732
  // Computed ONCE, honoured by EVERY return path. It used to live only on the text tail, so the two
3380
3733
  // paths that return earlier — `--json` and the zero-hits branch — still reported success. That
@@ -3421,6 +3774,7 @@ async function cmdRecall(options, flags, cwd, write) {
3421
3774
  // version emitted the number while its own comment promised null (found by independent review).
3422
3775
  write(JSON.stringify(hits.map((h) => ({
3423
3776
  ...h.pattern,
3777
+ ...(h.matchedForm === undefined ? {} : { matchedForm: h.matchedForm }),
3424
3778
  relevance: boost === null && 'score' in h && typeof h.score === 'number' ? h.score : null,
3425
3779
  // The TRUE cosine, as a second companion key — the design named it and the first ship missed
3426
3780
  // it, so a scripted consumer STILL could not threshold (found while recalibrating the floors:
@@ -3487,6 +3841,10 @@ async function cmdRecall(options, flags, cwd, write) {
3487
3841
  // guessing which second one answered.
3488
3842
  write(` store (read): ${join(globalRoot, '.dz')} [cross-project]`);
3489
3843
  }
3844
+ const classMatches = hits.filter((h) => h.matchedForm === 'class' || h.matchedForm === 'both').length;
3845
+ if (hits.some((h) => h.pattern.classForm !== undefined)) {
3846
+ write(` class-form matches: ${classMatches} of ${hits.length}`);
3847
+ }
3490
3848
  let sawQuarantined = false;
3491
3849
  for (const h of hits) {
3492
3850
  const backendTag = vectorOn ? ` ⟨${h.backend}⟩` : '';
@@ -3512,9 +3870,23 @@ async function cmdRecall(options, flags, cwd, write) {
3512
3870
  // 160, not 80: at 80 characters the evidence a reader needs to judge relevance sits in the
3513
3871
  // hidden remainder, and the cosine then appears to describe the visible fragment rather than
3514
3872
  // the whole lesson. `--full` prints it all, still on one line.
3515
- const width = flags.has('full') ? Number.POSITIVE_INFINITY : 160;
3516
- const oneLined = oneLine(h.pattern.pattern);
3517
- const shown = width === Number.POSITIVE_INFINITY ? oneLined : oneLined.slice(0, width);
3873
+ const specificText = oneLine(h.pattern.pattern);
3874
+ const classText = h.pattern.classForm === undefined ? undefined : oneLine(h.pattern.classForm);
3875
+ let shown = specificText;
3876
+ if (classText !== undefined) {
3877
+ const suffix = ` [match: ${h.matchedForm ?? 'specific'}]`;
3878
+ if (flags.has('full')) {
3879
+ shown = `specific: ${specificText} · class: ${classText}${suffix}`;
3880
+ }
3881
+ else {
3882
+ const contentWidth = Math.max(2, 160 - 'specific: '.length - ' · class: '.length - suffix.length);
3883
+ const specificWidth = Math.floor(contentWidth / 2);
3884
+ shown = `specific: ${specificText.slice(0, specificWidth)} · class: ${classText.slice(0, contentWidth - specificWidth)}${suffix}`;
3885
+ }
3886
+ }
3887
+ else if (!flags.has('full')) {
3888
+ shown = shown.slice(0, 160);
3889
+ }
3518
3890
  // WHICH store this hit came from. A merged list that does not say re-creates the fragmentation
3519
3891
  // blindness the store-location line just removed, one level down: the reader would see more
3520
3892
  // results and have no way to tell whether the global store is even connected.
@@ -4523,7 +4895,13 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4523
4895
  ? selectArg.split(',').map((s) => s.trim()).filter((s) => s.length > 0)
4524
4896
  : getPreset(preset)?.skills;
4525
4897
  const install = select !== undefined && select.length > 0
4526
- ? await installSkills({ target, projectRoot, cwd, explicitSkillsDir: options.get('skills-dir'), select, force: flags.has('force'), enrich: flags.has('enrich') })
4898
+ ? await installSkills({
4899
+ target, projectRoot, cwd, explicitSkillsDir: options.get('skills-dir'), select,
4900
+ force: flags.has('force'), enrich: flags.has('enrich'), noHooks: flags.has('no-hooks'),
4901
+ noIntegrations: flags.has('no-integrations'),
4902
+ noVerify: flags.has('no-verify'),
4903
+ ...(options.get('allow-integrations') !== undefined ? { allowIntegrations: options.get('allow-integrations') } : {}),
4904
+ })
4527
4905
  : undefined;
4528
4906
  if (install) {
4529
4907
  write(`║ ${String(install.results.length).padStart(2)} skill(s), ${String(install.written).padStart(3)} file(s) written${' '.repeat(15)}║`);
@@ -4533,6 +4911,7 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4533
4911
  }
4534
4912
  // Step 5 (ADR-001 §8): DELIVER the codex hooks and verify them live. Non-aborting — the rest of
4535
4913
  // setup has already run and the summary still prints; only the exit code carries the failure.
4914
+ let setupIntegrationOutcomes = install?.integrations ?? [];
4536
4915
  let codexHooksOk = true;
4537
4916
  if (target === 'codex' && !flags.has('no-hooks')) {
4538
4917
  write(`║ 5. Delivering codex hooks (live verify)... ║`);
@@ -4542,6 +4921,11 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4542
4921
  write(line);
4543
4922
  for (const line of delivery.stderr)
4544
4923
  writeErr(line);
4924
+ const hookIndex = setupIntegrationOutcomes.findIndex((row) => row.component === 'hooks' && row.status !== 'not-requested');
4925
+ if (hookIndex !== -1) {
4926
+ const hook = normalizeCodexHookOutcome(setupIntegrationOutcomes[hookIndex], delivery, flags.has('no-verify'));
4927
+ setupIntegrationOutcomes = setupIntegrationOutcomes.map((row, index) => index === hookIndex ? hook : row);
4928
+ }
4545
4929
  }
4546
4930
  write(`╠══════════════════════════════════════════════════════╣`);
4547
4931
  write(`║ Setup: ${String(setupResult.completed).padStart(2)} done, ${String(setupResult.skipped).padStart(2)} skipped ║`);
@@ -4559,6 +4943,9 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4559
4943
  if (install) {
4560
4944
  write(`dz setup: ${install.results.length} skill(s), ${install.written} file(s) written, ${install.skipped} skipped`
4561
4945
  + (install.dirsSearched > 1 ? ` (searched ${install.dirsSearched} skill dirs)` : ''));
4946
+ for (const outcome of setupIntegrationOutcomes) {
4947
+ write(`dz setup integration ${outcome.component}: ${outcome.status.toUpperCase()}${outcome.reasonCode ? ` (${outcome.reasonCode})` : ''}`);
4948
+ }
4562
4949
  writeMissingSkillsHint(write, install.missing, selectArg !== undefined ? undefined : preset);
4563
4950
  }
4564
4951
  // A hook that was written but never witnessed firing is NOT a completed setup (ADR-002 §5): the
@@ -4573,7 +4960,8 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4573
4960
  if (erroredSteps.length > 0) {
4574
4961
  write(`\n✗ setup reported ${erroredSteps.length} failed step(s): ${erroredSteps.join(', ')} — exit 1`);
4575
4962
  }
4576
- return codexHooksOk && erroredSteps.length === 0 ? 0 : 1;
4963
+ const integrationsOk = !setupIntegrationOutcomes.some((row) => row.status === 'refused');
4964
+ return codexHooksOk && erroredSteps.length === 0 && integrationsOk ? 0 : 1;
4577
4965
  }
4578
4966
  function cmdPretrain(options, cwd, write) {
4579
4967
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
@@ -5227,6 +5615,9 @@ function cmdPublish(options, flags, cwd, write) {
5227
5615
  write(`dz publish: ⚠ guard warn — ${v.rule}: ${v.detail}`);
5228
5616
  else
5229
5617
  write('dz publish: ✓ guard pre-flight passed');
5618
+ for (const observation of guardResult.observations ?? []) {
5619
+ write(`dz publish: ℹ guard observation — ${observation.rule} ${observation.scope}: ${observation.detail} [${observation.status}]`);
5620
+ }
5230
5621
  for (const n of guardResult.notes ?? [])
5231
5622
  write(`dz publish: ℹ guard note — ${n}`); // FN-7: on the record, never blocking
5232
5623
  }
@@ -6903,6 +7294,26 @@ export function codexHooksSyncOptions(input) {
6903
7294
  verify: input.verify !== false,
6904
7295
  };
6905
7296
  }
7297
+ /** Map the retained Codex hook writer's one live verdict into the common integration contract. */
7298
+ export function normalizeCodexHookOutcome(base, delivery, noVerify = false) {
7299
+ if (delivery.report.ready) {
7300
+ return {
7301
+ target: 'codex', component: 'hooks', status: 'emitted',
7302
+ registrations: [{ id: 'dz-codex-hooks', scope: 'user', registered: delivery.report.installed, approval: delivery.report.trust === 'trusted' ? 'approved' : 'unknown', ready: true }],
7303
+ carrier: { scope: 'user', path: '$CODEX_HOME/hooks.json' },
7304
+ ...(delivery.report.codexVersion !== null ? { runtimeVersion: delivery.report.codexVersion } : {}),
7305
+ evidenceVersion: 'codex-hooks-live-v1',
7306
+ };
7307
+ }
7308
+ return {
7309
+ ...base,
7310
+ target: 'codex', component: 'hooks', status: 'refused',
7311
+ registrations: [{ id: 'dz-codex-hooks', scope: 'user', registered: delivery.report.installed, approval: delivery.report.trust === 'trusted' ? 'approved' : 'unknown', ready: false }],
7312
+ reasonCode: 'CURRENT_LIVE_CHECK_FAILED',
7313
+ remediation: noVerify ? '--no-verify cannot establish ready; rerun with live verification' : 'approve the managed hooks and rerun the live verification',
7314
+ applied: delivery.report.written || delivery.report.installed,
7315
+ };
7316
+ }
6906
7317
  /**
6907
7318
  * What the user is told about a sync report — the ONE place the success word can be printed.
6908
7319
  *
@@ -7220,10 +7631,529 @@ function gatherReadmeCounts(root) {
7220
7631
  sitePair('packages/@dzhechkov/sitedoc/src/content/docs/index.mdx', /^\s*tagline: "[^"]*?(\d+) commands/m, 'commands (docs-site index hero tagline vs cli All Commands)');
7221
7632
  return pairs;
7222
7633
  }
7634
+ const MAX_VOLUME_FILES_PER_SCOPE = 512;
7635
+ const MAX_VOLUME_BYTES_PER_SCOPE = 32 * 1024 * 1024;
7636
+ const CYRILLIC_CHARACTER = /\p{Script=Cyrillic}/u;
7637
+ function volumePathInside(root, candidate) {
7638
+ return candidate === root || candidate.startsWith(root + sep);
7639
+ }
7640
+ function cyrillicUtf8Bytes(buffer) {
7641
+ let bytes = 0;
7642
+ for (const character of buffer.toString('utf8')) {
7643
+ if (CYRILLIC_CHARACTER.test(character))
7644
+ bytes += Buffer.byteLength(character, 'utf8');
7645
+ }
7646
+ return bytes;
7647
+ }
7648
+ function gatherTemplateVolumeTarget(packageDir, target) {
7649
+ const templateRoot = join(packageDir, 'templates', '.claude');
7650
+ if (!existsSync(templateRoot))
7651
+ return undefined;
7652
+ const files = [];
7653
+ const realFiles = new Set();
7654
+ let totalBytes = 0;
7655
+ let failure;
7656
+ let packageReal;
7657
+ let templateReal;
7658
+ try {
7659
+ packageReal = realpathSync(packageDir);
7660
+ templateReal = realpathSync(templateRoot);
7661
+ if (!volumePathInside(packageReal, templateReal)) {
7662
+ failure = { reason: 'template-root-escape', detail: templateRoot };
7663
+ }
7664
+ }
7665
+ catch (error) {
7666
+ return {
7667
+ target,
7668
+ files,
7669
+ collection: { complete: false, reason: 'template-root-unreadable', detail: error instanceof Error ? error.message : String(error) },
7670
+ };
7671
+ }
7672
+ const fail = (reason, detail) => {
7673
+ failure ??= { reason, detail: detail.replace(/\s+/g, ' ').slice(0, 240) };
7674
+ };
7675
+ const addFile = (absolute, kind) => {
7676
+ if (failure !== undefined)
7677
+ return;
7678
+ if (files.length >= MAX_VOLUME_FILES_PER_SCOPE) {
7679
+ fail('template-file-cap-exceeded', `${MAX_VOLUME_FILES_PER_SCOPE} files`);
7680
+ return;
7681
+ }
7682
+ try {
7683
+ const stat = lstatSync(absolute);
7684
+ if (!stat.isFile()) {
7685
+ fail('template-input-not-regular', absolute);
7686
+ return;
7687
+ }
7688
+ const real = realpathSync(absolute);
7689
+ if (!volumePathInside(templateReal, real)) {
7690
+ fail('template-input-escape', absolute);
7691
+ return;
7692
+ }
7693
+ if (realFiles.has(real)) {
7694
+ fail('template-input-duplicate', absolute);
7695
+ return;
7696
+ }
7697
+ if (stat.size <= 0 || stat.size > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
7698
+ fail(stat.size <= 0 ? 'template-input-empty' : 'template-byte-cap-exceeded', absolute);
7699
+ return;
7700
+ }
7701
+ const content = readFileSync(absolute);
7702
+ if (content.byteLength <= 0 || content.byteLength > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
7703
+ fail(content.byteLength <= 0 ? 'template-input-empty' : 'template-byte-cap-exceeded', absolute);
7704
+ return;
7705
+ }
7706
+ realFiles.add(real);
7707
+ totalBytes += content.byteLength;
7708
+ files.push({
7709
+ path: relative(packageDir, absolute).split(sep).join('/'),
7710
+ kind,
7711
+ bytes: content.byteLength,
7712
+ cyrillicUtf8Bytes: cyrillicUtf8Bytes(content),
7713
+ });
7714
+ }
7715
+ catch (error) {
7716
+ fail('template-input-unreadable', `${absolute}: ${error instanceof Error ? error.message : String(error)}`);
7717
+ }
7718
+ };
7719
+ const scanFlat = (name) => {
7720
+ const dir = join(templateRoot, name);
7721
+ if (!existsSync(dir) || failure !== undefined)
7722
+ return;
7723
+ try {
7724
+ for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
7725
+ if (!entry.name.endsWith('.md'))
7726
+ continue;
7727
+ addFile(join(dir, entry.name), name);
7728
+ }
7729
+ }
7730
+ catch (error) {
7731
+ fail('template-directory-unreadable', `${dir}: ${error instanceof Error ? error.message : String(error)}`);
7732
+ }
7733
+ };
7734
+ const scanSkills = (dir) => {
7735
+ if (!existsSync(dir) || failure !== undefined)
7736
+ return;
7737
+ let entries;
7738
+ try {
7739
+ entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
7740
+ }
7741
+ catch (error) {
7742
+ fail('template-directory-unreadable', `${dir}: ${error instanceof Error ? error.message : String(error)}`);
7743
+ return;
7744
+ }
7745
+ for (const entry of entries) {
7746
+ if (failure !== undefined)
7747
+ return;
7748
+ const absolute = join(dir, entry.name);
7749
+ if (entry.isSymbolicLink()) {
7750
+ fail('template-input-not-regular', absolute);
7751
+ return;
7752
+ }
7753
+ if (entry.isDirectory())
7754
+ scanSkills(absolute);
7755
+ else if (entry.name === 'SKILL.md')
7756
+ addFile(absolute, 'skills');
7757
+ }
7758
+ };
7759
+ scanFlat('rules');
7760
+ scanFlat('commands');
7761
+ scanSkills(join(templateRoot, 'skills'));
7762
+ files.sort((a, b) => a.path.localeCompare(b.path));
7763
+ return {
7764
+ target,
7765
+ files,
7766
+ collection: failure === undefined
7767
+ ? { complete: true }
7768
+ : { complete: false, reason: failure.reason, detail: failure.detail },
7769
+ };
7770
+ }
7771
+ function volumeGitText(root, args, allowedStatuses = [0]) {
7772
+ const result = spawnSync('git', [...args], {
7773
+ cwd: root,
7774
+ encoding: 'utf8',
7775
+ maxBuffer: 32 * 1024 * 1024,
7776
+ });
7777
+ if (result.status === null || !allowedStatuses.includes(result.status)) {
7778
+ throw result.error ?? new Error(`git ${args[0] ?? ''} exited ${String(result.status)}`);
7779
+ }
7780
+ return result.stdout ?? '';
7781
+ }
7782
+ function parseGuardStatusPaths(root) {
7783
+ const text = volumeGitText(root, ['status', '--porcelain', '-uall']);
7784
+ return text.split('\n').flatMap((line) => {
7785
+ if (line.length < 4)
7786
+ return [];
7787
+ const code = line.slice(0, 2);
7788
+ const raw = line.slice(3).trim();
7789
+ const path = (raw.includes(' -> ') ? raw.split(' -> ')[1] : raw)?.trim();
7790
+ return path ? [{ code, path }] : [];
7791
+ });
7792
+ }
7793
+ function parseFeatureTier(text) {
7794
+ const match = text.match(/^##\s+Tier:\s*(S|M|L|XL)\s*$/m);
7795
+ return match?.[1];
7796
+ }
7797
+ function parseFeatureActiveSteps(text) {
7798
+ const section = text.match(/^## Active steps[^\n]*\n([\s\S]*?)(?=^## |(?![\s\S]))/mi)?.[1];
7799
+ if (section === undefined)
7800
+ return undefined;
7801
+ const steps = [...section.matchAll(/\b(?:10|[0-9](?:\.5)?)\b/g)].map((match) => match[0]);
7802
+ return [...new Set(steps)];
7803
+ }
7804
+ function featureLifecycle(root, slug, assessment) {
7805
+ if (assessment !== undefined && /^Lifecycle:\s*complete\s*$/mi.test(assessment))
7806
+ return { phase: 'complete' };
7807
+ try {
7808
+ const state = JSON.parse(readFileSync(join(root, '.dz', 'feature-adr', 'learning-state', `${slug}.json`), 'utf8'));
7809
+ const match = typeof state.step === 'string' ? state.step.match(/\bStep\s+(\d+(?:\.5)?)/i) : null;
7810
+ if (!match?.[1])
7811
+ return undefined;
7812
+ const current = Number(match[1]);
7813
+ if (!Number.isFinite(current))
7814
+ return undefined;
7815
+ return { phase: 'in-progress', completedThroughStep: current <= 0 ? 0 : Math.ceil(current) - 1 };
7816
+ }
7817
+ catch {
7818
+ return undefined;
7819
+ }
7820
+ }
7821
+ function gatherFeatureVolumeFact(root, slug) {
7822
+ const featureDir = join(root, 'features', slug);
7823
+ const artifacts = [];
7824
+ const contents = new Map();
7825
+ let totalBytes = 0;
7826
+ let failure;
7827
+ let rootReal;
7828
+ let featureReal;
7829
+ try {
7830
+ rootReal = realpathSync(root);
7831
+ featureReal = realpathSync(featureDir);
7832
+ if (!volumePathInside(rootReal, featureReal))
7833
+ failure = { reason: 'feature-root-escape', detail: featureDir };
7834
+ }
7835
+ catch (error) {
7836
+ return { fact: {
7837
+ slug,
7838
+ artifacts,
7839
+ collection: { complete: false, reason: 'feature-root-unreadable', detail: error instanceof Error ? error.message : String(error) },
7840
+ } };
7841
+ }
7842
+ const fail = (reason, detail) => {
7843
+ failure ??= { reason, detail: detail.replace(/\s+/g, ' ').slice(0, 240) };
7844
+ };
7845
+ const addArtifact = (absolute) => {
7846
+ if (failure !== undefined)
7847
+ return;
7848
+ if (artifacts.length >= MAX_VOLUME_FILES_PER_SCOPE) {
7849
+ fail('feature-file-cap-exceeded', `${MAX_VOLUME_FILES_PER_SCOPE} files`);
7850
+ return;
7851
+ }
7852
+ try {
7853
+ const stat = lstatSync(absolute);
7854
+ if (!stat.isFile()) {
7855
+ fail('feature-input-not-regular', absolute);
7856
+ return;
7857
+ }
7858
+ const real = realpathSync(absolute);
7859
+ if (!volumePathInside(featureReal, real)) {
7860
+ fail('feature-input-escape', absolute);
7861
+ return;
7862
+ }
7863
+ if (stat.size <= 0 || stat.size > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
7864
+ fail(stat.size <= 0 ? 'feature-input-empty' : 'feature-byte-cap-exceeded', absolute);
7865
+ return;
7866
+ }
7867
+ const content = readFileSync(absolute);
7868
+ if (content.byteLength <= 0 || content.byteLength > MAX_VOLUME_BYTES_PER_SCOPE - totalBytes) {
7869
+ fail(content.byteLength <= 0 ? 'feature-input-empty' : 'feature-byte-cap-exceeded', absolute);
7870
+ return;
7871
+ }
7872
+ const path = relative(featureDir, absolute).split(sep).join('/');
7873
+ totalBytes += content.byteLength;
7874
+ contents.set(path, content);
7875
+ artifacts.push({ path, bytes: content.byteLength });
7876
+ }
7877
+ catch (error) {
7878
+ fail('feature-input-unreadable', `${absolute}: ${error instanceof Error ? error.message : String(error)}`);
7879
+ }
7880
+ };
7881
+ const scanNested = (dir) => {
7882
+ if (!existsSync(dir) || failure !== undefined)
7883
+ return;
7884
+ let entries;
7885
+ try {
7886
+ entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
7887
+ }
7888
+ catch (error) {
7889
+ fail('feature-directory-unreadable', `${dir}: ${error instanceof Error ? error.message : String(error)}`);
7890
+ return;
7891
+ }
7892
+ for (const entry of entries) {
7893
+ if (failure !== undefined)
7894
+ return;
7895
+ const absolute = join(dir, entry.name);
7896
+ if (entry.isSymbolicLink()) {
7897
+ fail('feature-input-not-regular', absolute);
7898
+ return;
7899
+ }
7900
+ if (entry.isDirectory())
7901
+ scanNested(absolute);
7902
+ else
7903
+ addArtifact(absolute);
7904
+ }
7905
+ };
7906
+ try {
7907
+ const entries = readdirSync(featureDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
7908
+ for (const entry of entries) {
7909
+ if (failure !== undefined)
7910
+ break;
7911
+ const absolute = join(featureDir, entry.name);
7912
+ if (entry.name === '03_adr' || entry.name === '07_code_changes') {
7913
+ if (entry.isSymbolicLink() || !entry.isDirectory())
7914
+ fail('feature-input-not-regular', absolute);
7915
+ else
7916
+ scanNested(absolute);
7917
+ }
7918
+ else if (entry.name === 'README.md' || /^0[0-9](?:[_.-][^/]*)?\.md$/.test(entry.name)) {
7919
+ addArtifact(absolute);
7920
+ }
7921
+ }
7922
+ }
7923
+ catch (error) {
7924
+ fail('feature-directory-unreadable', `${featureDir}: ${error instanceof Error ? error.message : String(error)}`);
7925
+ }
7926
+ artifacts.sort((a, b) => a.path.localeCompare(b.path));
7927
+ const assessment = contents.get('00_complexity_assessment.md')?.toString('utf8');
7928
+ const tier = assessment !== undefined ? parseFeatureTier(assessment) : undefined;
7929
+ const activeSteps = assessment !== undefined ? parseFeatureActiveSteps(assessment) : undefined;
7930
+ const lifecycle = featureLifecycle(root, slug, assessment);
7931
+ const fact = {
7932
+ slug,
7933
+ ...(tier !== undefined ? { tier } : {}),
7934
+ ...(activeSteps !== undefined ? { activeSteps } : {}),
7935
+ namedConsumers: [],
7936
+ ...(lifecycle !== undefined ? { lifecycle } : {}),
7937
+ artifacts,
7938
+ collection: failure === undefined
7939
+ ? { complete: true }
7940
+ : { complete: false, reason: failure.reason, detail: failure.detail },
7941
+ };
7942
+ const manifestText = contents.get('07_code_changes/change_manifest.md')?.toString('utf8');
7943
+ return { fact, ...(manifestText !== undefined ? { manifestText } : {}) };
7944
+ }
7945
+ function manifestPaths(text, slug) {
7946
+ const paths = new Set();
7947
+ for (const line of text.split('\n')) {
7948
+ const match = line.match(/^\s*-\s+`?([^`]+?)`?(?:\s+[—-]\s+.*)?$/);
7949
+ if (!match?.[1])
7950
+ continue;
7951
+ const path = match[1].trim().replace(/\\/g, '/').replace(/^\.\//, '');
7952
+ if (path === '' || path.startsWith('/') || path.split('/').some((part) => part === '..'))
7953
+ continue;
7954
+ if (path.startsWith(`features/${slug}/`))
7955
+ continue;
7956
+ paths.add(path);
7957
+ }
7958
+ return [...paths].sort();
7959
+ }
7960
+ function attributableDiff(root, slug, feature, status, changedSlugs) {
7961
+ if (changedSlugs.length !== 1 || changedSlugs[0] !== slug) {
7962
+ return { attributable: false, reason: changedSlugs.length > 1 ? 'ambiguous-feature-attribution' : 'feature-not-attributable' };
7963
+ }
7964
+ if (feature.manifestText === undefined)
7965
+ return { attributable: false, reason: 'change-manifest-unavailable' };
7966
+ const listed = manifestPaths(feature.manifestText, slug);
7967
+ const changedByPath = new Map(status.map((item) => [item.path, item.code]));
7968
+ const changed = listed.filter((path) => changedByPath.has(path));
7969
+ if (changed.length === 0)
7970
+ return { attributable: true, bytes: 0, base: 'HEAD', head: 'working-tree', method: 'git-unified-diff-bytes/v1', excludedFeaturePath: `features/${slug}/**` };
7971
+ const tracked = [];
7972
+ const untracked = [];
7973
+ for (const path of changed) {
7974
+ const absolute = resolve(root, path);
7975
+ if (!volumePathInside(root, absolute))
7976
+ return { attributable: false, reason: 'manifest-path-escape' };
7977
+ if (changedByPath.get(path) === '??') {
7978
+ try {
7979
+ const stat = lstatSync(absolute);
7980
+ const real = realpathSync(absolute);
7981
+ if (!stat.isFile() || !volumePathInside(root, real) || stat.size > MAX_VOLUME_BYTES_PER_SCOPE) {
7982
+ return { attributable: false, reason: 'untracked-diff-input-refused' };
7983
+ }
7984
+ }
7985
+ catch {
7986
+ return { attributable: false, reason: 'untracked-diff-input-unreadable' };
7987
+ }
7988
+ untracked.push(path);
7989
+ }
7990
+ else {
7991
+ tracked.push(path);
7992
+ }
7993
+ }
7994
+ let bytes = 0;
7995
+ try {
7996
+ if (tracked.length > 0) {
7997
+ const diff = volumeGitText(root, ['diff', '--binary', '--no-ext-diff', 'HEAD', '--', ...tracked]);
7998
+ bytes += Buffer.byteLength(diff, 'utf8');
7999
+ }
8000
+ for (const path of untracked) {
8001
+ const diff = volumeGitText(root, ['diff', '--no-index', '--binary', '--', '/dev/null', resolve(root, path)], [0, 1]);
8002
+ bytes += Buffer.byteLength(diff, 'utf8');
8003
+ if (bytes > MAX_VOLUME_BYTES_PER_SCOPE)
8004
+ return { attributable: false, reason: 'diff-byte-cap-exceeded' };
8005
+ }
8006
+ }
8007
+ catch {
8008
+ return { attributable: false, reason: 'git-diff-failed' };
8009
+ }
8010
+ return {
8011
+ attributable: true,
8012
+ bytes,
8013
+ base: 'HEAD',
8014
+ head: 'working-tree',
8015
+ method: 'git-unified-diff-bytes/v1',
8016
+ excludedFeaturePath: `features/${slug}/**`,
8017
+ };
8018
+ }
8019
+ function gatherVolumeShadowFacts(root, packages) {
8020
+ const templates = packages
8021
+ .filter((item) => !item.privateFlag)
8022
+ .sort((a, b) => a.name.localeCompare(b.name) || a.dir.localeCompare(b.dir))
8023
+ .flatMap((item) => {
8024
+ const fact = gatherTemplateVolumeTarget(join(root, item.dir), item.name);
8025
+ return fact === undefined ? [] : [fact];
8026
+ });
8027
+ let status;
8028
+ try {
8029
+ status = parseGuardStatusPaths(root);
8030
+ }
8031
+ catch {
8032
+ return templates.length > 0 ? { templates } : {};
8033
+ }
8034
+ const slugs = [...new Set(status.flatMap((item) => {
8035
+ const match = item.path.match(/^features\/([^/]+)\//);
8036
+ return match?.[1] ? [match[1]] : [];
8037
+ }))].sort();
8038
+ const gathered = slugs.slice(0, 32).map((slug) => gatherFeatureVolumeFact(root, slug));
8039
+ const measuredFeatures = gathered.map((feature) => ({
8040
+ ...feature.fact,
8041
+ diff: attributableDiff(root, feature.fact.slug, feature, status, slugs),
8042
+ }));
8043
+ const cappedFeatures = slugs.slice(32).map((slug) => ({
8044
+ slug,
8045
+ artifacts: [],
8046
+ collection: {
8047
+ complete: false,
8048
+ reason: 'feature-scope-cap-exceeded',
8049
+ detail: 'only the first 32 lexicographically sorted changed feature scopes were traversed',
8050
+ },
8051
+ }));
8052
+ const features = [...measuredFeatures, ...cappedFeatures];
8053
+ return {
8054
+ ...(templates.length > 0 ? { templates } : {}),
8055
+ ...(features.length > 0 ? { features } : {}),
8056
+ };
8057
+ }
7223
8058
  /** Gather the facts one op needs. All I/O is best-effort — a missing signal skips its rule, never crashes. */
7224
8059
  function gatherGuardFacts(op, root, text, storeCap) {
7225
8060
  const facts = { op };
7226
8061
  if (op === 'publish') {
8062
+ // Advisory I/O: unreadable telemetry or fed state is absence of evidence, never a fabricated
8063
+ // stale finding and never a publish blocker.
8064
+ try {
8065
+ const routing = readRoutingTelemetry(root);
8066
+ facts['routingFreshness'] = { unfedRunIds: unfedRuns(routing.harvest.samples, routing.alreadyFed) };
8067
+ }
8068
+ catch { /* fail-open: routing-store-stale is SOFT and needs gathered evidence */ }
8069
+ // marketplace-parity: regenerate the two published showcase manifests from the LIVE registry,
8070
+ // but redirect every write to a unique temp root. Version is operator-owned, so pinning the
8071
+ // published value into generatePlugin makes the comparison composition-only by construction.
8072
+ try {
8073
+ const showcaseDir = join(root, '.claude-plugin');
8074
+ if (!existsSync(showcaseDir)) {
8075
+ facts['marketplaceParity'] = { applicable: false };
8076
+ }
8077
+ else {
8078
+ const pluginPath = join(showcaseDir, 'plugin.json');
8079
+ const marketplacePath = join(showcaseDir, 'marketplace.json');
8080
+ const hasPlugin = existsSync(pluginPath);
8081
+ const hasMarketplace = existsSync(marketplacePath);
8082
+ const manifestFailures = [];
8083
+ const readManifest = (path, file) => {
8084
+ try {
8085
+ return JSON.parse(readFileSync(path, 'utf8'));
8086
+ }
8087
+ catch (error) {
8088
+ const message = error instanceof Error ? error.message : String(error);
8089
+ manifestFailures.push({ file, error: message.replace(/\s+/g, ' ').slice(0, 240) });
8090
+ return undefined;
8091
+ }
8092
+ };
8093
+ const versionOf = (manifest) => {
8094
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest))
8095
+ return undefined;
8096
+ const version = manifest['version'];
8097
+ return typeof version === 'string' && version !== '' ? version : undefined;
8098
+ };
8099
+ const publishedPlugin = hasPlugin ? readManifest(pluginPath, 'plugin.json') : undefined;
8100
+ const publishedMarketplace = hasMarketplace ? readManifest(marketplacePath, 'marketplace.json') : undefined;
8101
+ const marketplaceRecord = publishedMarketplace && typeof publishedMarketplace === 'object' && !Array.isArray(publishedMarketplace)
8102
+ ? publishedMarketplace
8103
+ : undefined;
8104
+ const marketplacePlugins = marketplaceRecord !== undefined && Array.isArray(marketplaceRecord['plugins'])
8105
+ ? marketplaceRecord['plugins']
8106
+ : [];
8107
+ const publishedVersion = versionOf(publishedPlugin)
8108
+ ?? versionOf(publishedMarketplace)
8109
+ ?? versionOf(marketplacePlugins[0]);
8110
+ if (manifestFailures.length > 0) {
8111
+ facts['marketplaceParity'] = {
8112
+ applicable: true,
8113
+ manifestFailures,
8114
+ ...(publishedVersion !== undefined ? { publishedVersion } : {}),
8115
+ };
8116
+ }
8117
+ else if (hasPlugin !== hasMarketplace) {
8118
+ facts['marketplaceParity'] = {
8119
+ applicable: true,
8120
+ onlyOnePresent: true,
8121
+ ...(publishedVersion !== undefined ? { publishedVersion } : {}),
8122
+ };
8123
+ }
8124
+ else if (!hasPlugin) {
8125
+ facts['marketplaceParity'] = { applicable: false };
8126
+ }
8127
+ else {
8128
+ let scratch;
8129
+ try {
8130
+ const registry = buildRegistry(root);
8131
+ scratch = mkdtempSync(join(tmpdir(), 'dz-guard-marketplace-'));
8132
+ const generated = generatePlugin(scratch, registry, { version: publishedVersion });
8133
+ const freshPlugin = JSON.parse(readFileSync(generated.pluginJsonPath, 'utf8'));
8134
+ const freshMarketplace = JSON.parse(readFileSync(generated.marketplaceJsonPath, 'utf8'));
8135
+ facts['marketplaceParity'] = {
8136
+ applicable: true,
8137
+ diverged: !isDeepStrictEqual(publishedPlugin, freshPlugin)
8138
+ || !isDeepStrictEqual(publishedMarketplace, freshMarketplace),
8139
+ ...(publishedVersion !== undefined ? { publishedVersion } : {}),
8140
+ };
8141
+ }
8142
+ catch {
8143
+ facts['marketplaceParity'] = {
8144
+ applicable: true,
8145
+ regenerateFailed: true,
8146
+ ...(publishedVersion !== undefined ? { publishedVersion } : {}),
8147
+ };
8148
+ }
8149
+ finally {
8150
+ if (scratch !== undefined)
8151
+ rmSync(scratch, { recursive: true, force: true });
8152
+ }
8153
+ }
8154
+ }
8155
+ }
8156
+ catch { /* fail-open only on unexpected showcase-discovery failure; manifest read/parse errors are structured violations above */ }
7227
8157
  // agents-md-policy-sync: fixed registry, no tree walk. The pure detector
7228
8158
  // recomputes every expected hash from current source text; this gatherer
7229
8159
  // only supplies bytes. Any unexpected gather failure omits the fact, and
@@ -7259,7 +8189,7 @@ function gatherGuardFacts(op, root, text, storeCap) {
7259
8189
  const manifests = [];
7260
8190
  const located = [];
7261
8191
  try {
7262
- const out = execSync('git ls-files "packages/@dzhechkov/*/package.json"', { cwd: root, encoding: 'utf-8' });
8192
+ const out = volumeGitText(root, ['ls-files', 'packages/@dzhechkov/*/package.json']);
7263
8193
  for (const rel of out.split('\n').map((s) => s.trim()).filter(Boolean)) {
7264
8194
  try {
7265
8195
  const m = JSON.parse(readFileSync(join(root, rel), 'utf8'));
@@ -7288,6 +8218,11 @@ function gatherGuardFacts(op, root, text, storeCap) {
7288
8218
  packages.push({ name: m.name ?? '(unnamed)', deps });
7289
8219
  }
7290
8220
  facts['packages'] = packages;
8221
+ facts['volume'] = gatherVolumeShadowFacts(root, located.map(({ dir, m }) => ({
8222
+ dir,
8223
+ name: m.name ?? dir,
8224
+ privateFlag: m.private === true,
8225
+ })));
7291
8226
  // licence-hold (ADR-001, hermes-claude-adaptation): for each pack DECLARING a hold via a
7292
8227
  // `licenseHold` field, hand the raw evidence to the pure checker. Best-effort: an unreadable
7293
8228
  // LICENSE reads as absent (null), which the checker treats as a violation for a publishable
@@ -7565,6 +8500,10 @@ function runGuardEvaluation(root, op, text, overrideReason) {
7565
8500
  catch { /* audit is best-effort, never blocks the verdict */ }
7566
8501
  return result;
7567
8502
  }
8503
+ function renderGuardObservation(observation) {
8504
+ const tag = observation.status === 'unknown' ? 'note' : 'observe';
8505
+ return ` [${tag}] ${observation.rule} ${observation.scope}: ${observation.detail} [${observation.status}]`;
8506
+ }
7568
8507
  /**
7569
8508
  * The tail facts of an append-only log, read from its END — O(1) in the file size, which is what
7570
8509
  * lets the chain be extended on every append without a full-file scan (FR-2). Anything unreadable
@@ -7939,8 +8878,9 @@ function cmdGuardPromote(options, flags, root, write) {
7939
8878
  ...next,
7940
8879
  entries: Object.fromEntries(Object.entries(next.entries).map(([k, v]) => (applied.includes(k) ? [k, { ...v, appliedTs: nowTs }] : [k, v]))),
7941
8880
  };
8881
+ const withEvidence = recordPromotionRunEvidence(withApplied, report, nowTs);
7942
8882
  try {
7943
- writeJsonAtomic(join(root, PROMOTION_STATE_FILE), withApplied);
8883
+ writeJsonAtomic(join(root, PROMOTION_STATE_FILE), withEvidence);
7944
8884
  }
7945
8885
  catch { /* best-effort */ }
7946
8886
  }
@@ -8049,6 +8989,8 @@ function cmdGuard(options, flags, cwd, write) {
8049
8989
  }
8050
8990
  const glyph = result.verdict === 'block' ? '✗' : result.verdict === 'warn' ? '⚠' : '✓';
8051
8991
  write(`dz guard (${op}): ${glyph} ${result.verdict.toUpperCase()} [checked: ${result.checked.join(', ') || 'no rules for this op'}]`);
8992
+ for (const observation of result.observations ?? [])
8993
+ write(renderGuardObservation(observation));
8052
8994
  for (const v of result.violations)
8053
8995
  write(` [${v.severity === 'hard' ? 'BLOCK' : 'warn'}] ${v.rule}: ${v.detail}`);
8054
8996
  for (const n of result.notes ?? [])
@@ -12647,6 +13589,134 @@ function readOptionalText(path) {
12647
13589
  return '';
12648
13590
  }
12649
13591
  }
13592
+ function localEvidenceReadReason(resource, error) {
13593
+ const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
13594
+ return code === 'ENOENT' ? `${resource}-missing` : `${resource}-unreadable`;
13595
+ }
13596
+ function unavailablePromotionEvidence(reason) {
13597
+ return { source: { status: 'not-measured', reason }, acceptances: [], truncatedPeriods: [], acceptanceHistoryComplete: false };
13598
+ }
13599
+ function readPromotionRunEvidence(root) {
13600
+ let raw;
13601
+ try {
13602
+ raw = JSON.parse(readFileSync(join(root, PROMOTION_STATE_FILE), 'utf-8'));
13603
+ }
13604
+ catch (error) {
13605
+ const reason = error instanceof SyntaxError
13606
+ ? 'promotion-journal-malformed'
13607
+ : localEvidenceReadReason('promotion-journal', error);
13608
+ return unavailablePromotionEvidence(reason);
13609
+ }
13610
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
13611
+ return unavailablePromotionEvidence('promotion-journal-malformed');
13612
+ }
13613
+ const record = raw;
13614
+ if (record['version'] !== 1 || !Object.hasOwn(record, 'runs')) {
13615
+ return unavailablePromotionEvidence(record['version'] === 1 ? 'promotion-history-not-recorded' : 'promotion-journal-malformed');
13616
+ }
13617
+ if (!Array.isArray(record['runs'])) {
13618
+ return unavailablePromotionEvidence('promotion-history-malformed');
13619
+ }
13620
+ const state = normalizePromotionState(raw);
13621
+ if (state.runs === undefined || state.runs.length !== record['runs'].length) {
13622
+ return unavailablePromotionEvidence('promotion-history-malformed');
13623
+ }
13624
+ if ((Object.hasOwn(record, 'acceptances') && (!Array.isArray(record['acceptances']) || state.acceptances?.length !== record['acceptances'].length)) ||
13625
+ (Object.hasOwn(record, 'truncatedRunPeriods') && (!Array.isArray(record['truncatedRunPeriods']) || state.truncatedRunPeriods?.length !== record['truncatedRunPeriods'].length)) ||
13626
+ (Object.hasOwn(record, 'acceptanceHistoryComplete') && typeof record['acceptanceHistoryComplete'] !== 'boolean')) {
13627
+ return unavailablePromotionEvidence('promotion-history-malformed');
13628
+ }
13629
+ const derivedAcceptances = state.runs.flatMap((run) => run.candidates
13630
+ .filter((candidate) => candidate.verdict === 'promote' && candidate.ruleContentAnchor !== null)
13631
+ .map((candidate) => ({ ruleContentAnchor: candidate.ruleContentAnchor, acceptedTs: run.ts })));
13632
+ const claimedAcceptanceComplete = state.acceptanceHistoryComplete ??
13633
+ ((state.truncatedRunPeriods?.length ?? 0) === 0 && state.runs.every((run) => run.complete === true));
13634
+ if (claimedAcceptanceComplete && state.acceptances !== undefined && derivedAcceptances.some((derived) => !state.acceptances.some((stored) => stored.ruleContentAnchor === derived.ruleContentAnchor &&
13635
+ Date.parse(stored.acceptedTs) <= Date.parse(derived.acceptedTs)))) {
13636
+ return unavailablePromotionEvidence('promotion-history-malformed');
13637
+ }
13638
+ const acceptancesByAnchor = new Map();
13639
+ for (const acceptance of [...(state.acceptances ?? []), ...derivedAcceptances]) {
13640
+ const prior = acceptancesByAnchor.get(acceptance.ruleContentAnchor);
13641
+ if (prior === undefined || Date.parse(acceptance.acceptedTs) < Date.parse(prior.acceptedTs)) {
13642
+ acceptancesByAnchor.set(acceptance.ruleContentAnchor, acceptance);
13643
+ }
13644
+ }
13645
+ return {
13646
+ source: { status: 'measured', rows: state.runs },
13647
+ acceptances: [...acceptancesByAnchor.values()],
13648
+ truncatedPeriods: state.truncatedRunPeriods ?? [],
13649
+ acceptanceHistoryComplete: claimedAcceptanceComplete,
13650
+ };
13651
+ }
13652
+ function readGuardAuditEvidence(root) {
13653
+ let text;
13654
+ try {
13655
+ text = readFileSync(join(root, '.dz', 'guard-audit.jsonl'), 'utf-8');
13656
+ }
13657
+ catch (error) {
13658
+ return {
13659
+ source: { status: 'not-measured', reason: localEvidenceReadReason('guard-audit', error) },
13660
+ rows: [],
13661
+ text: null,
13662
+ };
13663
+ }
13664
+ const rows = [];
13665
+ let malformed = false;
13666
+ for (const line of text.split('\n')) {
13667
+ if (line.trim() === '')
13668
+ continue;
13669
+ try {
13670
+ const raw = JSON.parse(line);
13671
+ if (!isOffsetIsoTimestamp(raw['ts']) ||
13672
+ !['publish', 'teach', 'consolidate', 'reindex'].includes(String(raw['op'])) ||
13673
+ !['pass', 'warn', 'block'].includes(String(raw['verdict']))) {
13674
+ malformed = true;
13675
+ continue;
13676
+ }
13677
+ const violations = [];
13678
+ if (!Array.isArray(raw['violations'])) {
13679
+ malformed = true;
13680
+ continue;
13681
+ }
13682
+ for (const item of raw['violations']) {
13683
+ const rule = item && typeof item === 'object' ? item.rule : undefined;
13684
+ if (typeof rule !== 'string' || rule === '' || rule.length > 200) {
13685
+ malformed = true;
13686
+ continue;
13687
+ }
13688
+ const anchor = item.contentAnchor;
13689
+ if (anchor !== undefined && !isLessonRuleContentAnchor(anchor)) {
13690
+ malformed = true;
13691
+ continue;
13692
+ }
13693
+ violations.push({ rule, ...(typeof anchor === 'string' ? { contentAnchor: anchor } : {}) });
13694
+ }
13695
+ const verdict = raw['verdict'];
13696
+ if ((verdict === 'pass') !== (violations.length === 0))
13697
+ malformed = true;
13698
+ rows.push({
13699
+ ts: raw['ts'],
13700
+ op: raw['op'],
13701
+ verdict,
13702
+ rules: violations.map((item) => item.rule),
13703
+ violations,
13704
+ });
13705
+ }
13706
+ catch {
13707
+ malformed = true;
13708
+ }
13709
+ }
13710
+ return {
13711
+ source: malformed
13712
+ ? { status: 'not-measured', reason: 'guard-audit-malformed' }
13713
+ : !verifyEventChainText(text).ok
13714
+ ? { status: 'not-measured', reason: 'guard-audit-chain-corrupt' }
13715
+ : { status: 'measured', rows },
13716
+ rows,
13717
+ text,
13718
+ };
13719
+ }
12650
13720
  function deadwoodAllowlistText() {
12651
13721
  const require = createRequire(import.meta.url);
12652
13722
  const corePackage = require.resolve('@dzhechkov/harness-core/package.json');
@@ -12727,7 +13797,7 @@ function cmdCompounding(options, flags, cwd, write) {
12727
13797
  const json = flags.has('json');
12728
13798
  if (flags.has('help')) {
12729
13799
  write('dz compounding [--project <dir>] [--json] — honest learning-loop payoff report');
12730
- write(' pool payoff (write-only ratio) · guard repeat-violation trajectory · cold-vs-warm replay readiness · instrumentation health');
13800
+ write(' pool payoff · guard trajectory · replay readiness · instrumentation · monthly eligible→attempted→accepted→executions funnel');
12731
13801
  return 0;
12732
13802
  }
12733
13803
  for (const flag of flags) {
@@ -12756,36 +13826,14 @@ function cmdCompounding(options, flags, cwd, write) {
12756
13826
  }));
12757
13827
  // apply-leg usage events (read records only; aggregate rows carry no query by construction)
12758
13828
  const usage = readRecallUsageEvents(root);
12759
- // guard audit events
12760
- const guard = [];
12761
- try {
12762
- const text = readFileSync(join(root, '.dz', 'guard-audit.jsonl'), 'utf-8');
12763
- for (const line of text.split('\n')) {
12764
- if (line.trim() === '')
12765
- continue;
12766
- try {
12767
- const o = JSON.parse(line);
12768
- if (typeof o.ts === 'string' && typeof o.verdict === 'string') {
12769
- guard.push({
12770
- ts: o.ts,
12771
- verdict: o.verdict,
12772
- rules: Array.isArray(o.violations) ? o.violations.map((v) => (typeof v?.rule === 'string' ? v.rule : '')).filter(Boolean) : [],
12773
- });
12774
- }
12775
- }
12776
- catch {
12777
- /* skip */
12778
- }
12779
- }
12780
- }
12781
- catch {
12782
- /* no audit yet */
12783
- }
13829
+ const promotionEvidence = readPromotionRunEvidence(root);
13830
+ const guardEvidence = readGuardAuditEvidence(root);
13831
+ const guard = [...guardEvidence.rows];
12784
13832
  // The evidence logs themselves, verbatim: the report verifies their hash chains (feature
12785
13833
  // event-chain). Handing over the TEXT rather than a pre-computed verdict keeps one definition of
12786
13834
  // "the chain is intact" — a second copy here is how a gate and its report start disagreeing.
12787
13835
  const evidenceLogs = [];
12788
- for (const rel of ['.dz/recall-usage.jsonl', '.dz/guard-audit.jsonl']) {
13836
+ for (const rel of ['.dz/recall-usage.jsonl']) {
12789
13837
  try {
12790
13838
  evidenceLogs.push({ log: rel, text: readFileSync(join(root, ...rel.split('/')), 'utf-8') });
12791
13839
  }
@@ -12793,6 +13841,9 @@ function cmdCompounding(options, flags, cwd, write) {
12793
13841
  /* absent log — reported by its own gate above, not invented here */
12794
13842
  }
12795
13843
  }
13844
+ if (guardEvidence.text !== null) {
13845
+ evidenceLogs.push({ log: '.dz/guard-audit.jsonl', text: guardEvidence.text });
13846
+ }
12796
13847
  const now = new Date();
12797
13848
  const report = assembleCompoundingReport({
12798
13849
  lessons,
@@ -12801,6 +13852,13 @@ function cmdCompounding(options, flags, cwd, write) {
12801
13852
  nowTs: now.toISOString(),
12802
13853
  evidenceLogs,
12803
13854
  cmdUsageDepthDays: cmdUsageDepthDays(root, now),
13855
+ lessonToRule: {
13856
+ promotionRuns: promotionEvidence.source,
13857
+ guardAudits: guardEvidence.source,
13858
+ promotionAcceptances: promotionEvidence.acceptances,
13859
+ truncatedPromotionPeriods: promotionEvidence.truncatedPeriods,
13860
+ acceptanceHistoryComplete: promotionEvidence.acceptanceHistoryComplete,
13861
+ },
12804
13862
  });
12805
13863
  // lesson-bandit-rerank §11: the payoff axis joins THIS report rather than growing a private
12806
13864
  // dashboard — the `rewardEvents : exposureEvents` row asks exactly the question this command
@@ -14016,6 +15074,44 @@ async function cmdBacklog(options, flags, cwd, write) {
14016
15074
  write(' harmonize [--apply][--threshold 0-1] batch semantic dedup of the backlog');
14017
15075
  return sub === undefined ? 0 : 1;
14018
15076
  }
15077
+ /** Read the recommender's existing telemetry planes plus its idempotency receipt. One adapter is
15078
+ * shared by `recommend` and the publish advisory so their freshness definitions cannot drift. */
15079
+ function readRoutingTelemetry(repoRoot) {
15080
+ const records = readHarnessRecords(repoRoot);
15081
+ for (const base of [join(repoRoot, 'features'), join(repoRoot, '.dz', 'loop-trace')]) {
15082
+ if (!existsSync(base))
15083
+ continue;
15084
+ let names = [];
15085
+ try {
15086
+ names = readdirSync(base);
15087
+ }
15088
+ catch {
15089
+ continue;
15090
+ }
15091
+ for (const name of names) {
15092
+ const sidecar = join(base, name, 'run-meta.json');
15093
+ if (!existsSync(sidecar))
15094
+ continue;
15095
+ try {
15096
+ const meta = JSON.parse(readFileSync(sidecar, 'utf-8'));
15097
+ if (meta.runMeta?.resolved === true && Array.isArray(meta.runMeta.records))
15098
+ records.push(...meta.runMeta.records);
15099
+ }
15100
+ catch { /* an unreadable sidecar contributes no asserted record */ }
15101
+ }
15102
+ }
15103
+ let alreadyFed = [];
15104
+ const fedPath = join(repoRoot, '.dz', 'routing-fed.json');
15105
+ if (existsSync(fedPath)) {
15106
+ const parsed = JSON.parse(readFileSync(join(repoRoot, '.dz', 'routing-fed.json'), 'utf-8'));
15107
+ if (!Array.isArray(parsed))
15108
+ throw new Error('.dz/routing-fed.json must be a JSON array of runIds');
15109
+ if (parsed.some((v) => typeof v !== 'string'))
15110
+ throw new Error('.dz/routing-fed.json contains a non-string runId');
15111
+ alreadyFed = parsed;
15112
+ }
15113
+ return { harvest: harvestStageOutcomes(records), alreadyFed };
15114
+ }
14019
15115
  /**
14020
15116
  * `dz routing` — inspect the learned cost-optimal routing outcome store (feature learned-cost-routing). Shows
14021
15117
  * what `args.models[stage]='auto-cost'` currently believes per (stage, complexity-tier, model): gated
@@ -14032,40 +15128,22 @@ function cmdRouting(options, flags, cwd, write) {
14032
15128
  // ── recommend — harvest real telemetry, print per-stage picks WITH THE BASIS, optionally feed the
14033
15129
  // store (a9c3dd5c fn 3, ADR-001). Sources: live harness records + imported run-meta.json sidecars.
14034
15130
  if (options.get('_positional_0') === 'recommend') {
14035
- const records = readHarnessRecords(repoRoot);
14036
- // imported runs (trace-bundle ADR-001 D4): run-meta sidecars carry runMeta.records of the SAME shape
14037
- for (const base of [join(repoRoot, 'features'), join(repoRoot, '.dz', 'loop-trace')]) {
14038
- if (!existsSync(base))
14039
- continue;
14040
- let names = [];
14041
- try {
14042
- names = readdirSync(base);
14043
- }
14044
- catch {
14045
- continue;
14046
- }
14047
- for (const name of names) {
14048
- const sidecar = join(base, name, 'run-meta.json');
14049
- if (!existsSync(sidecar))
14050
- continue;
14051
- try {
14052
- const meta = JSON.parse(readFileSync(sidecar, 'utf-8'));
14053
- if (meta.runMeta?.resolved === true && Array.isArray(meta.runMeta.records))
14054
- records.push(...meta.runMeta.records);
14055
- }
14056
- catch { /* an unreadable sidecar contributes nothing — counted below as noResult */ }
14057
- }
15131
+ let routing;
15132
+ try {
15133
+ routing = readRoutingTelemetry(repoRoot);
15134
+ }
15135
+ catch (e) {
15136
+ write(`dz routing recommend: cannot read routing freshness state — ${e.message}`);
15137
+ return 1;
14058
15138
  }
14059
- const harvest = harvestStageOutcomes(records);
14060
- const rec = recommendModels(harvest, { ...(options.get('tier') !== undefined ? { tier: options.get('tier') } : {}) });
15139
+ const harvest = routing.harvest;
15140
+ let rec = recommendModels(harvest, {
15141
+ ...(options.get('tier') !== undefined ? { tier: options.get('tier') } : {}),
15142
+ alreadyFed: routing.alreadyFed,
15143
+ });
14061
15144
  if (flags.has('apply')) {
14062
15145
  const fedPath = join(repoRoot, '.dz', 'routing-fed.json');
14063
- let alreadyFed = [];
14064
- try {
14065
- alreadyFed = JSON.parse(readFileSync(fedPath, 'utf-8'));
14066
- }
14067
- catch { /* first apply */ }
14068
- const plan = planFeed(harvest.samples, alreadyFed);
15146
+ const plan = planFeed(harvest.samples, routing.alreadyFed);
14069
15147
  for (const sample of plan.toFeed)
14070
15148
  finalizeOutcome(repoRoot, sample.stage, sample.tier, sample.model, sample.success);
14071
15149
  try {
@@ -14076,6 +15154,10 @@ function cmdRouting(options, flags, cwd, write) {
14076
15154
  write(`dz routing recommend: fed ${plan.toFeed.length} sample(s) but could NOT persist the fed-set — a re-apply WILL double-count: ${e.message}`);
14077
15155
  return 1;
14078
15156
  }
15157
+ rec = recommendModels(harvest, {
15158
+ ...(options.get('tier') !== undefined ? { tier: options.get('tier') } : {}),
15159
+ alreadyFed: plan.fedAfter,
15160
+ });
14079
15161
  write(`dz routing recommend --apply: fed ${plan.toFeed.length} sample(s) into .dz/routing-outcomes.json${plan.skippedRuns.length > 0 ? `; skipped ${plan.skippedRuns.length} already-fed run(s) (idempotent by runId)` : ''}`);
14080
15162
  }
14081
15163
  if (flags.has('json')) {
@@ -14087,6 +15169,12 @@ function cmdRouting(options, flags, cwd, write) {
14087
15169
  write(` ${s.stage.padEnd(14)} ${s.spec.padEnd(20)} ${s.insufficientData ? '[insufficient data — cold-start pick]' : '[quality bar met]'} ${s.pick.evidence}`);
14088
15170
  }
14089
15171
  write(` basis: ${rec.basis.rule}`);
15172
+ if (rec.basis.freshness === 'current')
15173
+ write(' store: current (0 unharvested runs)');
15174
+ else if (rec.basis.freshness === 'stale')
15175
+ write(` store: STALE — ${rec.basis.unfed.count} run(s) harvested but never fed; auto-cost is routing from an older snapshot — run \`dz routing recommend --apply\``);
15176
+ else
15177
+ write(' store: UNFED — auto-cost has no learned rows; picks below are cold-start');
14090
15178
  write(` basis: skipped records — no result ${rec.basis.skipped.noResult}, no modelsUsed ${rec.basis.skipped.noModels} (predates model routing — history, not an error), no grade ${rec.basis.skipped.noGrade}, unknown model ${rec.basis.skipped.unknownModel}`);
14091
15179
  write(` basis: ${rec.basis.crossFamilyNote}`);
14092
15180
  return 0;
@@ -14795,11 +15883,11 @@ export async function runCli(argv, io = {}) {
14795
15883
  case 'bundle':
14796
15884
  return cmdBundle(options, flags, cwd, write);
14797
15885
  case 'teach':
14798
- return await cmdTeach(options, flags, cwd, write);
15886
+ return await cmdTeach(options, flags, cwd, write, writeErr, io.interactive ?? process.stdout.isTTY === true, io.teachGuardRunner ?? teachGuard, io.teachReinforceRunner ?? runTeachGuardReinforcement);
14799
15887
  case 'consolidate':
14800
15888
  return await cmdConsolidate(options, flags, cwd, write);
14801
15889
  case 'recall':
14802
- return await cmdRecall(options, flags, cwd, write);
15890
+ return await cmdRecall(options, flags, cwd, write, writeErr, io.classMatcher);
14803
15891
  case 'vector':
14804
15892
  return await cmdVector(options, flags, cwd, write);
14805
15893
  case 'brain':
@@ -14852,6 +15940,8 @@ export async function runCli(argv, io = {}) {
14852
15940
  return cmdDriftCheck(options, flags, cwd, write);
14853
15941
  case 'hooks-sync':
14854
15942
  return cmdHooksSync(options, flags, cwd, write, writeErr);
15943
+ case 'integrations-verify':
15944
+ return cmdIntegrationsVerify(options, flags, cwd, write, writeErr);
14855
15945
  case 'agents-sync':
14856
15946
  return cmdAgentsSync(options, flags, cwd, write, writeErr);
14857
15947
  case 'sync-canonical':