@dzhechkov/harness-cli 0.8.6 → 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
@@ -15,11 +15,11 @@ import { createHash, randomBytes } from 'node:crypto';
15
15
  import { homedir, tmpdir } from 'node:os';
16
16
  import { createRequire } from 'node:module';
17
17
  import { isDeepStrictEqual } from 'node:util';
18
- import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures, formatSkillApplyFailures, resolveTargetName, formatTargetProblem, formatTargetAliasNote, TARGET_NAMES_SORTED, runDoctor, runInit, resolvePackageSkillRoots, PACKAGE_SKILL_LAYOUTS, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, discoverVerifiablePackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, runSyncAgentsPolicy, runSyncCodexHooks, resolveCodexHome, withNamedLockSync,
18
+ import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures, formatSkillApplyFailures, resolveTargetName, formatTargetProblem, formatTargetAliasNote, TARGET_NAMES_SORTED, runDoctor, runInit, discoverSkillIds, runIntegrationsVerify, resolvePackageSkillRoots, PACKAGE_SKILL_LAYOUTS, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, discoverVerifiablePackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, runSyncAgentsPolicy, runSyncCodexHooks, resolveCodexHome, withNamedLockSync,
19
19
  // dz workflow run (feature dz-workflow-run): the pure scheduler + the dispatch adapters.
20
- TRACE_RUNID_RE, WF_RUN_OWNER_HOST, preflight, runWorkflow, makeClaudePDispatcher, makeCodexExecDispatcher, NamedLockTimeoutError, NamedLockCompromisedError, POLICY_SOURCES, detectPolicyDrift, hasPolicyFence, TARGET_NAMES, buildParityMatrix, downgradeForStaleEvidence, findStaleTranscriptEvidence, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, deriveAttestation, stampAttestation, corroborate, NOT_WITNESSED, renderTimelineHtml, importEcc, recordPattern, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, computeUsage, deriveCostLedger, planLedgerBackfill, listCostLedgerRuns, resolveLedgerRunId, AMBIGUOUS, stampCheckpointLine, LEDGER_FILL_SOURCE, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, parseWeeklyResetAnchor, claimCheck, summarize, BUNDLED_SLOP_REGISTRY_URL, DEFAULT_SLOP_CONFIG, parseSlopRegistry, slopLint, validateSlopLintConfig, queryBookKnowledge, loadStorePatternsSync, patternRecordId, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, bookKbPath, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, EVENT_CHAIN_TAIL_BYTES, EMPTY_LOG_TAIL, readTailInfo, appendChainedLines, verifyEventChainText, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
20
+ TRACE_RUNID_RE, WF_RUN_OWNER_HOST, preflight, runWorkflow, makeClaudePDispatcher, makeCodexExecDispatcher, NamedLockTimeoutError, NamedLockCompromisedError, POLICY_SOURCES, detectPolicyDrift, hasPolicyFence, TARGET_NAMES, buildParityMatrix, downgradeForStaleEvidence, findStaleTranscriptEvidence, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, deriveAttestation, stampAttestation, corroborate, NOT_WITNESSED, renderTimelineHtml, importEcc, recordPattern, recordLessonForms, normalizeLessonForms, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, CHECKPOINT_STAGES, estimateEta, extractStageSamples, formatEta, parseCheckpointLines, segmentRun, computeUsage, deriveCostLedger, planLedgerBackfill, listCostLedgerRuns, resolveLedgerRunId, AMBIGUOUS, stampCheckpointLine, LEDGER_FILL_SOURCE, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, parseWeeklyResetAnchor, claimCheck, summarize, BUNDLED_SLOP_REGISTRY_URL, DEFAULT_SLOP_CONFIG, parseSlopRegistry, slopLint, validateSlopLintConfig, queryBookKnowledge, loadStorePatternsSync, patternRecordId, patternIdentityOf, mergeLessonMatchedForms, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, bookKbPath, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, EVENT_CHAIN_TAIL_BYTES, EMPTY_LOG_TAIL, readTailInfo, appendChainedLines, verifyEventChainText, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
21
21
  // guard-promotion (feature guard-promotion, scout idea #1)
22
- assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, classifyExecutionEvidence, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, harvestStageOutcomes, recommendModels, planFeed, unfedRuns, GRADE_SUCCESS_FLOOR, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, scanSkillsLayout, declaredPluginSurface, parseInitFacts, verifyRegistration, buildContentProbePrompt, classifyContentProbe, renderContentProbe, findNonRegistrableSkillDirs, assembleCompoundingReport, buildDeadwoodReport, compactCmdUsageIfNeeded, measureCmdUsageDepthDays, recordCommandInvocation, resolveCmdUsageRoot, renderDeadwoodReport, CMD_USAGE_LOG_RELATIVE, banditStats, narrowBanditReport, renderBanditHealth,
22
+ assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, recordPromotionRunEvidence, isLessonRuleContentAnchor, isOffsetIsoTimestamp, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, classifyExecutionEvidence, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, harvestStageOutcomes, recommendModels, planFeed, unfedRuns, GRADE_SUCCESS_FLOOR, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, scanSkillsLayout, declaredPluginSurface, parseInitFacts, verifyRegistration, buildContentProbePrompt, classifyContentProbe, renderContentProbe, findNonRegistrableSkillDirs, assembleCompoundingReport, buildDeadwoodReport, compactCmdUsageIfNeeded, measureCmdUsageDepthDays, recordCommandInvocation, resolveCmdUsageRoot, renderDeadwoodReport, CMD_USAGE_LOG_RELATIVE, banditStats, narrowBanditReport, renderBanditHealth,
23
23
  // Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
24
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,
25
25
  // Smart Backlog (feature smart-backlog) — goal-directed idea pipeline over the Brain vector engine.
@@ -40,7 +40,7 @@ export const DZ_COMMANDS = [
40
40
  'usage', 'claim-check', 'lint', 'sign', 'sbom', 'guard', 'verify-pack', 'setup',
41
41
  'pretrain', 'compose', 'diff', 'recommend', 'upgrade', 'auto-canonicalize',
42
42
  'publish', 'release', 'parity', 'registry', 'benchmark', 'mcp-scan',
43
- 'sync-upstream', 'drift-check', 'hooks-sync', 'agents-sync', 'sync-canonical',
43
+ 'sync-upstream', 'drift-check', 'hooks-sync', 'integrations-verify', 'agents-sync', 'sync-canonical',
44
44
  'plugin', 'downloads', 'stats', 'architecture', 'project-skills', 'mr-rakes',
45
45
  'retro', 'feature-adr-setup', 'challenge', 'discrimination-check',
46
46
  'mutation-gate', 'delivery-check', 'skills-verify', 'compounding', 'deadwood',
@@ -52,7 +52,7 @@ export const DZ_COMMANDS = [
52
52
  const USAGE = `dz - DZ cross-platform harness CLI
53
53
 
54
54
  Usage:
55
- dz init --target <name> [--skills-dir <dir>] [--project <dir>] [--preset <name>] [--select id,id,...] [--force] [--enrich] [--no-hooks] [--no-verify] (--target codex ALSO installs the user-global dz veto+recall hooks and LIVE-verifies them (ADR-001 §8); --no-hooks = skills only; --no-verify skips the live probe and can never report ready)
55
+ dz init --target <name> [--skills-dir <dir>] [--project <dir>] [--preset <name>] [--select id,id,...] [--force] [--enrich] [--allow-integrations <sha256:digest>] [--no-integrations] [--no-hooks] [--no-verify] (integration manifests require exact digest consent; --no-integrations = explicit skills-only)
56
56
  dz verify [--skills-dir <dir>] [--target <name>]
57
57
  dz sync [--canonical <dir>] [--project <dir>] [--dry-run] [--force]
58
58
  dz update (alias of sync)
@@ -81,7 +81,7 @@ Usage:
81
81
  dz delivery-check --slug <slug> [--context-only] [--findings <f.json>] [--strict] [--author <model>] [--json] (portable Step-10 Delivery Gate: prints the 4-plane review brief + artifact probes; --findings classifies a fed-back review into a fail-closed ready|blocked hand-off and writes features/<slug>/10_delivery_review.md; --strict exits 1 on blocked)
82
82
  dz challenge --plan <plan.md> [--author <model>] (the deterministic cartridge behind the challenge-panel adversarial plan-gate (R6): assembles the wide brief — plan + architecture/vision.md + testing.md + map.json + degradations.md — and prints the C1-C8 adversary prompt naming the cross-family reviewer to dispatch. exit 0 brief printed / 1 plan missing or empty)
83
83
  dz skills-verify [--dir <project>] [--expect a,b] [--static] [--strict] [--json] (does .claude/skills/ actually REGISTER? --static = instant layout scan for CI; default reads the authoritative system/init listing from a real session. exit 0 pass / 1 fail / 2 inconclusive — never a false pass)
84
- dz compounding [--project <dir>] [--json] (honest learning-loop payoff report: pool write-only ratio, guard repeat-violation trajectory, cold-vs-warm replay readiness, instrumentation health a gate without enough data says INSUFFICIENT_DATA, never a fake verdict)
84
+ dz compounding [--project <dir>] [--json] (honest learning-loop payoff report: pool/replay/guard instrumentation + monthly eligible→attempted→accepted→executions; unavailable is NOT MEASURED, and only a named empty stage after a non-empty predecessor for 3 measured months is a funnel finding)
85
85
  dz deadwood [--weeks <n>] [--json] (advisory zero-usage candidates for human deprecation review; safety-excluded surfaces carry reasons; never deletes or deprecates anything; shallow history says INSUFFICIENT_DATA)
86
86
  dz epoch-replay --mock [--n <N>] [--effect <-1..1>] [--tie-rate <0..1>] [--seed <N>] [--slice <name>] [--json] ($0 synthetic run — exercises the verdict math, NOT evidence)
87
87
  dz epoch-replay --emit [--project <dir>] [--limit <N>] [--seed <N>] [--out <file>] (cold-vs-warm work order: instances + PRE-REGISTERED blind A/B assignment; the runner never calls a model)
@@ -122,7 +122,7 @@ Usage:
122
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)
123
123
  dz backlog harmonize [--apply] [--threshold <0-1>] [--project <dir>] [--json] (batch semantic dedup of the backlog ideas; --dry-run default, --apply snapshots first)
124
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)
125
- dz teach "<pattern>" [--reward <0-1>] [--domain <name>] [--type rule|success-pattern|lesson-learned] [--project <dir>] [--no-mirror] (--project pins the learned store to <dir>/.dz, not the cwd — pin to a canonical brain)
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)
126
126
  dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
127
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)
128
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)
@@ -161,6 +161,7 @@ Usage:
161
161
  dz drift-check [--json] [--project <dir>] (CI gate: exit 1 if any shared skill drifted between its monorepo copies)
162
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)
163
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)
164
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)
165
166
  dz plugin [--version <ver>]
166
167
  dz downloads
@@ -272,8 +273,8 @@ async function installSkills(opts) {
272
273
  // route them through one aggregation.
273
274
  if (target === 'agents-md' || target === 'gemini') {
274
275
  const report = target === 'gemini'
275
- ? runInitGeminiMd({ skillsDirs, projectRoot, ...(select !== undefined ? { select } : {}) })
276
- : 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 } : {}) });
277
278
  const results = report.skills.map((s) => ({
278
279
  id: s.id,
279
280
  written: s.written.length,
@@ -285,12 +286,20 @@ async function installSkills(opts) {
285
286
  written += s.written;
286
287
  skipped += s.skipped;
287
288
  }
288
- 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 } : {}) };
289
290
  }
290
291
  const results = [];
291
292
  const failures = [];
292
293
  const applyFailures = [];
293
- 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()) {
294
303
  const r = await runInit({
295
304
  target,
296
305
  skillsDir,
@@ -298,12 +307,27 @@ async function installSkills(opts) {
298
307
  force,
299
308
  enrich,
300
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 } : {}),
301
319
  });
302
320
  for (const skill of r.skills) {
303
321
  results.push({ id: skill.id, written: skill.written.length, skipped: skill.skipped.length });
304
322
  }
305
323
  failures.push(...r.failures);
306
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;
307
331
  }
308
332
  let written = 0;
309
333
  let skipped = 0;
@@ -313,7 +337,7 @@ async function installSkills(opts) {
313
337
  }
314
338
  const installed = new Set(results.map((s) => s.id));
315
339
  const missing = select !== undefined ? [...select].filter((id) => !installed.has(id)) : [];
316
- 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 } : {}) };
317
341
  }
318
342
  /** Warn about preset/select ids that weren't found in any installed pack. */
319
343
  function writeMissingSkillsHint(write, missing, presetName) {
@@ -376,17 +400,15 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
376
400
  select,
377
401
  force: flags.has('force'),
378
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') } : {}),
379
407
  });
380
- write(`dz init --target ${target}: ${r.results.length} skill(s), ${r.written} file(s) written, ${r.skipped} skipped`);
381
- if (r.dirsSearched > 1) {
382
- write(` (searched ${r.dirsSearched} skill directories)`);
383
- }
384
- writeMissingSkillsHint(write, r.missing, presetName);
385
- // Codex-targeted init DELIVERS the hooks and verifies them (ADR-001 §8). Skills alone are not the
386
- // target's harness: the veto + recall legs are what `--target codex` promises.
387
- // `--no-hooks` is the documented escape for "skills only" (the same flag `dz setup` already
388
- // carries): hook delivery writes USER-GLOBAL config, so a command that only wants skills compiled
389
- // must be able to say so — and every test that is about skills says it.
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;
390
412
  let codexHooksOk = true;
391
413
  if (target === 'codex' && !flags.has('no-hooks')) {
392
414
  const delivery = deliverCodexHooks({ project: projectRoot, verify: !flags.has('no-verify') }, undefined, 'dz init');
@@ -395,7 +417,30 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
395
417
  write(line);
396
418
  for (const line of delivery.stderr)
397
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
+ }
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
+ }
398
442
  }
443
+ writeMissingSkillsHint(write, r.missing, presetName);
399
444
  // Skip-and-collect must not become skip-and-SILENCE: a skill that failed to load is
400
445
  // named on stderr and the command exits 1 (it exited 1 before too — by throwing).
401
446
  if (r.failures.length > 0 || r.applyFailures.length > 0) {
@@ -418,7 +463,38 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
418
463
  writeErr(line);
419
464
  return 1;
420
465
  }
421
- 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;
422
498
  }
423
499
  async function cmdVerify(options, cwd, write, writeErr) {
424
500
  const skillsDir = resolve(cwd, options.get('skills-dir') ?? '.claude/skills');
@@ -1870,6 +1946,150 @@ function cmdStatuslineFaRecord(options, cwd, write) {
1870
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`);
1871
1947
  return 0;
1872
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
+ }
1873
2093
  /**
1874
2094
  * `dz statusline` — render dz's OWN self-learning counts as one compact, emoji-tagged line for
1875
2095
  * Claude Code's status bar (modeled on agentic-qe's "🎓 12 patterns"). Claude Code pipes a JSON
@@ -1889,8 +2109,23 @@ function cmdStatusline(options, flags, cwd, write, readStdin) {
1889
2109
  try {
1890
2110
  const projectRoot = statuslineProjectRoot(readStdin(), options, cwd);
1891
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
+ }
1892
2127
  if (flags.has('json')) {
1893
- write(JSON.stringify(data));
2128
+ write(JSON.stringify({ ...data, ...(eta !== undefined ? { eta } : {}) }));
1894
2129
  return 0;
1895
2130
  }
1896
2131
  let line = `🎓 dz: ${data.patterns} patterns${data.usedPatterns !== undefined ? ` · ${data.usedPatterns} used` : ''} · 🧠 ${data.brainSources} sources`;
@@ -1900,14 +2135,13 @@ function cmdStatusline(options, flags, cwd, write, readStdin) {
1900
2135
  if (data.consolidatedAgeH !== undefined)
1901
2136
  line += ` · ⟳ ${data.consolidatedAgeH}h`;
1902
2137
  // Live /feature-adr run in flight → PREPEND the pipeline learning segment to the base dz line.
1903
- const fa = data.featureAdr;
1904
2138
  if (fa !== undefined) {
1905
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.
1906
2140
  if (fa.kind === 'loop') {
1907
2141
  line = `🔁 loop ${fa.step} · ${line}`;
1908
2142
  }
1909
2143
  else {
1910
- 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}`;
1911
2145
  }
1912
2146
  }
1913
2147
  write(line);
@@ -2494,7 +2728,12 @@ function learningStoreLine(storeRoot, projectOption, target, verb) {
2494
2728
  const reason = teachReasonPhrase(target.reason);
2495
2729
  return storeLocationLine(describeStoreLocation(storeRoot, projectOption, target.store === 'global' ? 'global' : undefined), verb) + (reason ? ' [' + reason + ']' : '');
2496
2730
  }
2497
- 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) {
2498
2737
  // WHICH store this lesson belongs to, and WHO decided (teach-chooses-its-store).
2499
2738
  // `--to` → `DZ_LEARN` → `.dz/config.json` learning.teachTo → project. The owner asked for a
2500
2739
  // per-session choice; for a CLI every invocation is a fresh process, so the only honest session
@@ -2585,26 +2824,39 @@ async function cmdTeach(options, flags, cwd, write) {
2585
2824
  write('dz teach --from-json: expected a JSON array (produced by `dz recall --all --json`)');
2586
2825
  return 1;
2587
2826
  }
2588
- 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));
2589
2831
  let imported = 0;
2590
2832
  let skipped = 0;
2591
2833
  const importedRecs = [];
2592
2834
  for (const item of parsed) {
2593
2835
  const p = item;
2594
- if (!p || typeof p.pattern !== 'string' || p.pattern.trim() === '' || existing.has(p.pattern)) {
2836
+ if (!p || typeof p !== 'object') {
2595
2837
  skipped += 1;
2596
2838
  continue;
2597
2839
  }
2598
- const rec = {
2599
- 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 : '',
2600
2846
  type: (typeof p.type === 'string' ? p.type : 'lesson-learned'),
2601
2847
  reward: typeof p.reward === 'number' ? Math.max(0, Math.min(1, p.reward)) : 0.8,
2602
2848
  domain: typeof p.domain === 'string' ? p.domain : 'general',
2603
2849
  ts: typeof p.ts === 'string' ? p.ts : new Date().toISOString(),
2604
2850
  source: 'dz-teach-import',
2851
+ ...pair,
2605
2852
  };
2853
+ if (candidate.pattern.trim() === '' || existing.has(importedKey(candidate))) {
2854
+ skipped += 1;
2855
+ continue;
2856
+ }
2857
+ const rec = candidate;
2606
2858
  await recordPattern(storeRoot, rec);
2607
- existing.add(p.pattern);
2859
+ existing.add(importedKey(rec));
2608
2860
  importedRecs.push(rec);
2609
2861
  imported += 1;
2610
2862
  }
@@ -2687,23 +2939,32 @@ async function cmdTeach(options, flags, cwd, write) {
2687
2939
  }
2688
2940
  const reward = parseFloat(options.get('reward') ?? '0.8');
2689
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;
2690
2945
  if (flags.has('guard')) {
2691
- 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)) });
2692
2947
  if (verdict.action === 'reinforce') {
2693
- const backend = resolveLearningBackend(storeRoot);
2694
- backend.addSample({ dzId: verdict.dzId, kind: 'reinforce', reward: Math.max(0, Math.min(1, reward)), ts: new Date().toISOString() });
2695
- const trained = await backend.train();
2696
- // HIGH-fix: only claim success when the reinforce actually FLUSHED. With backend 'off'
2697
- // (NoopLearningBackend) or a flush failure, flushed === 0 — falling through to the plain
2698
- // teach below so the lesson is NEVER silently discarded (the exact silent-drop the ADR forbids).
2699
- if (trained.flushed > 0) {
2700
- write(`↳ reinforced existing pattern ${verdict.dzId} (cos=${verdict.cosine.toFixed(2)}) — not re-added`);
2701
- const clearedQ = clearAgentdbQuarantine(storeRoot, [verdict.dzId]);
2702
- if (clearedQ.cleared > 0)
2703
- write(' ↳ promoted out of quarantine (mirror updated)');
2704
- return 0;
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`);
2705
2967
  }
2706
- write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
2707
2968
  }
2708
2969
  }
2709
2970
  // Distill pattern into actionable rule (claude-smart inspired)
@@ -2716,8 +2977,8 @@ async function cmdTeach(options, flags, cwd, write) {
2716
2977
  // Typed through the shared schema (harness-core owns PatternRecord) so the
2717
2978
  // write side and the read side (recommend's loadPatterns) can never drift —
2718
2979
  // a field rename here is a compile error, not a silently re-muted loop (audit #2).
2719
- const entry = {
2720
- pattern,
2980
+ const entry = guardedExisting?.pattern ?? {
2981
+ pattern: lessonForms.specific,
2721
2982
  type: type,
2722
2983
  reward: Math.max(0, Math.min(1, reward)),
2723
2984
  domain,
@@ -2729,9 +2990,47 @@ async function cmdTeach(options, flags, cwd, write) {
2729
2990
  // total count. The lossy `npx agentdb add` dual-write was removed in Tier-1 (audit #6).
2730
2991
  // lesson-quarantine (opt-in): a fresh lesson is a HYPOTHESIS until it earns promotion.
2731
2992
  const quarantineOn = readMemoryLearningConfig(storeRoot).quarantine;
2732
- const count = await recordPattern(storeRoot, entry, quarantineOn ? { quarantine: true } : {});
2733
- write(`Learned: "${pattern.slice(0, 60)}${pattern.length > 60 ? '...' : ''}"`);
2734
- write(` Domain: ${domain} Reward: ${reward} Backend: memory (@dzhechkov/memory)`);
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)`);
2735
3034
  write(` Total patterns: ${count}`);
2736
3035
  // WHERE the write landed. MEASURED before this line existed: teach printed the pattern, the
2737
3036
  // domain, the reward and the backend — and not one word about the path, so a store written to
@@ -2741,6 +3040,24 @@ async function cmdTeach(options, flags, cwd, write) {
2741
3040
  // sees a path, cannot tell what chose it, and has no reason to question it. `default` adds
2742
3041
  // nothing, so the line stays byte-identical for everyone who set nothing.
2743
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
+ }
2744
3061
  // ADVICE, not a gate. Someone putting medical lessons in a shared store owns both
2745
3062
  // directories and this binary; refusing would be defending a user against themselves,
2746
3063
  // which this design does not attempt. Making the choice INFORMED is the part that is
@@ -2759,8 +3076,8 @@ async function cmdTeach(options, flags, cwd, write) {
2759
3076
  write(' ⚠ quarantined: excluded from auto-inject, damped in recall — promote by confirming it (dz teach --reinforce "<text>") or dz recall --promote <dzId> --apply');
2760
3077
  }
2761
3078
  // The lexical write above is durable — the vector mirror is strictly best-effort (I-3).
2762
- await emitMirrorQ(storeRoot, [entry], 'dz-teach', quarantineOn);
2763
- return 0;
3079
+ await emitMirrorQ(storeRoot, recordsToMirror, 'dz-teach', quarantineOn);
3080
+ return commandFailed ? 1 : 0;
2764
3081
  }
2765
3082
  async function cmdConsolidate(options, flags, cwd, write) {
2766
3083
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
@@ -3120,7 +3437,7 @@ async function cmdRecallPromote(options, flags, projectRoot, write) {
3120
3437
  write(` not found: ${res.notFound.join(', ')}`);
3121
3438
  return 0;
3122
3439
  }
3123
- async function cmdRecall(options, flags, cwd, write) {
3440
+ async function cmdRecall(options, flags, cwd, write, writeErr, classMatcher) {
3124
3441
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
3125
3442
  const asJson = flags.has('json');
3126
3443
  const all = flags.has('all');
@@ -3141,7 +3458,15 @@ async function cmdRecall(options, flags, cwd, write) {
3141
3458
  // review could not make correct: a tag set by the writer is decidable, prose is not.
3142
3459
  const allPatterns = loadStorePatternsSync(projectRoot);
3143
3460
  const holdout = applyExportHoldout(allPatterns, heldOutAfterOptIn(options.get('include-domain')));
3144
- 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
+ });
3145
3470
  const holdoutNote = renderHoldoutNote(holdout);
3146
3471
  // The opt-in is honoured without argument — and named out loud. A flag that silently
3147
3472
  // includes medical lessons in a portable export is a flag whose consequence the user
@@ -3171,7 +3496,15 @@ async function cmdRecall(options, flags, cwd, write) {
3171
3496
  const rawStats = storeStats(projectRoot);
3172
3497
  const topHoldout = applyExportHoldout(rawStats.topUses, heldOutAfterOptIn(options.get('include-domain')));
3173
3498
  const perDomain = Object.fromEntries(Object.entries(rawStats.perDomain).filter(([d]) => !holdout.domains.includes(canonicalDomainKey(d))));
3174
- 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
+ };
3175
3508
  const backendStats = resolveLearningBackend(projectRoot).getStats();
3176
3509
  if (asJson) {
3177
3510
  write(JSON.stringify({ patterns, stats, learning: backendStats, withheld: holdout.withheld.length, withheldDomains: holdout.domains }));
@@ -3185,6 +3518,7 @@ async function cmdRecall(options, flags, cwd, write) {
3185
3518
  write(` backend: ${backendStats.backend}${backendStats.advisory !== undefined ? ` (${backendStats.advisory})` : ''}`);
3186
3519
  write(` domains: ${Object.entries(stats.perDomain).map(([k, v]) => `${k}=${v}`).join(', ') || 'none'}`);
3187
3520
  write(` exact-dup groups: ${stats.exactDupGroups}`);
3521
+ write(` generalized: ${stats.generalized} of ${stats.logicalLessons} lessons`);
3188
3522
  write(` re-teach trend: ${stats.teachEvents} teach event(s), ${stats.reinforceEvents} reinforce event(s)`);
3189
3523
  write(' top uses:');
3190
3524
  for (const row of stats.topUses)
@@ -3312,6 +3646,10 @@ async function cmdRecall(options, flags, cwd, write) {
3312
3646
  ? 'lexical'
3313
3647
  : 'hybrid';
3314
3648
  const wantedDomain = options.get('domain');
3649
+ const classRecallOptions = {
3650
+ onClassDegraded: writeErr,
3651
+ ...(classMatcher === undefined ? {} : { classMatcher }),
3652
+ };
3315
3653
  // OVER-FETCH before boosting (Codex QE #5): the boost used to run on hits ALREADY
3316
3654
  // truncated to `limit`, so an exact-domain lesson sitting at rank limit+1 could
3317
3655
  // never receive its promised lift — the feature was weakest in exactly the case it
@@ -3334,8 +3672,8 @@ async function cmdRecall(options, flags, cwd, write) {
3334
3672
  // second occurrence. Foreign stdout is routed to stderr for the duration of the engine call — our
3335
3673
  // own output is written after it returns.
3336
3674
  const result = asJson
3337
- ? await withForeignStdoutOnStderr(() => recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
3338
- : await recallHybrid(projectRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
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 } : {}) });
3339
3677
  if (mode === 'semantic' && result.vectorEngine === 'none') {
3340
3678
  // --semantic is an explicit ask — degrading it silently would be dishonest (FR-3).
3341
3679
  const why = result.vectorReason ?? 'no vector engine available — run: dz setup --memory agentdb';
@@ -3369,13 +3707,27 @@ async function cmdRecall(options, flags, cwd, write) {
3369
3707
  let globalHits = [];
3370
3708
  if (readGlobal) {
3371
3709
  const g = asJson
3372
- ? await withForeignStdoutOnStderr(() => recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) }))
3373
- : await recallHybrid(globalRoot, query, { limit: fetchLimit, mode, deferExposures: true, ...(wantedDomain !== undefined ? { domain: wantedDomain } : {}) });
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 } : {}) });
3374
3712
  globalHits = g.hits;
3375
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]));
3376
3723
  const merged = readGlobal
3377
- ? mergeStoreHits(boost ? boost.hits : result.hits, globalHits, (h) => h.pattern.pattern)
3378
- : (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;
3379
3731
  const hits = merged.slice(0, limit);
3380
3732
  // Computed ONCE, honoured by EVERY return path. It used to live only on the text tail, so the two
3381
3733
  // paths that return earlier — `--json` and the zero-hits branch — still reported success. That
@@ -3422,6 +3774,7 @@ async function cmdRecall(options, flags, cwd, write) {
3422
3774
  // version emitted the number while its own comment promised null (found by independent review).
3423
3775
  write(JSON.stringify(hits.map((h) => ({
3424
3776
  ...h.pattern,
3777
+ ...(h.matchedForm === undefined ? {} : { matchedForm: h.matchedForm }),
3425
3778
  relevance: boost === null && 'score' in h && typeof h.score === 'number' ? h.score : null,
3426
3779
  // The TRUE cosine, as a second companion key — the design named it and the first ship missed
3427
3780
  // it, so a scripted consumer STILL could not threshold (found while recalibrating the floors:
@@ -3488,6 +3841,10 @@ async function cmdRecall(options, flags, cwd, write) {
3488
3841
  // guessing which second one answered.
3489
3842
  write(` store (read): ${join(globalRoot, '.dz')} [cross-project]`);
3490
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
+ }
3491
3848
  let sawQuarantined = false;
3492
3849
  for (const h of hits) {
3493
3850
  const backendTag = vectorOn ? ` ⟨${h.backend}⟩` : '';
@@ -3513,9 +3870,23 @@ async function cmdRecall(options, flags, cwd, write) {
3513
3870
  // 160, not 80: at 80 characters the evidence a reader needs to judge relevance sits in the
3514
3871
  // hidden remainder, and the cosine then appears to describe the visible fragment rather than
3515
3872
  // the whole lesson. `--full` prints it all, still on one line.
3516
- const width = flags.has('full') ? Number.POSITIVE_INFINITY : 160;
3517
- const oneLined = oneLine(h.pattern.pattern);
3518
- const shown = width === Number.POSITIVE_INFINITY ? oneLined : oneLined.slice(0, width);
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
+ }
3519
3890
  // WHICH store this hit came from. A merged list that does not say re-creates the fragmentation
3520
3891
  // blindness the store-location line just removed, one level down: the reader would see more
3521
3892
  // results and have no way to tell whether the global store is even connected.
@@ -4524,7 +4895,13 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4524
4895
  ? selectArg.split(',').map((s) => s.trim()).filter((s) => s.length > 0)
4525
4896
  : getPreset(preset)?.skills;
4526
4897
  const install = select !== undefined && select.length > 0
4527
- ? await installSkills({ target, projectRoot, cwd, explicitSkillsDir: options.get('skills-dir'), select, force: flags.has('force'), enrich: flags.has('enrich') })
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
+ })
4528
4905
  : undefined;
4529
4906
  if (install) {
4530
4907
  write(`║ ${String(install.results.length).padStart(2)} skill(s), ${String(install.written).padStart(3)} file(s) written${' '.repeat(15)}║`);
@@ -4534,6 +4911,7 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4534
4911
  }
4535
4912
  // Step 5 (ADR-001 §8): DELIVER the codex hooks and verify them live. Non-aborting — the rest of
4536
4913
  // setup has already run and the summary still prints; only the exit code carries the failure.
4914
+ let setupIntegrationOutcomes = install?.integrations ?? [];
4537
4915
  let codexHooksOk = true;
4538
4916
  if (target === 'codex' && !flags.has('no-hooks')) {
4539
4917
  write(`║ 5. Delivering codex hooks (live verify)... ║`);
@@ -4543,6 +4921,11 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4543
4921
  write(line);
4544
4922
  for (const line of delivery.stderr)
4545
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
+ }
4546
4929
  }
4547
4930
  write(`╠══════════════════════════════════════════════════════╣`);
4548
4931
  write(`║ Setup: ${String(setupResult.completed).padStart(2)} done, ${String(setupResult.skipped).padStart(2)} skipped ║`);
@@ -4560,6 +4943,9 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4560
4943
  if (install) {
4561
4944
  write(`dz setup: ${install.results.length} skill(s), ${install.written} file(s) written, ${install.skipped} skipped`
4562
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
+ }
4563
4949
  writeMissingSkillsHint(write, install.missing, selectArg !== undefined ? undefined : preset);
4564
4950
  }
4565
4951
  // A hook that was written but never witnessed firing is NOT a completed setup (ADR-002 §5): the
@@ -4574,7 +4960,8 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
4574
4960
  if (erroredSteps.length > 0) {
4575
4961
  write(`\n✗ setup reported ${erroredSteps.length} failed step(s): ${erroredSteps.join(', ')} — exit 1`);
4576
4962
  }
4577
- 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;
4578
4965
  }
4579
4966
  function cmdPretrain(options, cwd, write) {
4580
4967
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
@@ -5228,6 +5615,9 @@ function cmdPublish(options, flags, cwd, write) {
5228
5615
  write(`dz publish: ⚠ guard warn — ${v.rule}: ${v.detail}`);
5229
5616
  else
5230
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
+ }
5231
5621
  for (const n of guardResult.notes ?? [])
5232
5622
  write(`dz publish: ℹ guard note — ${n}`); // FN-7: on the record, never blocking
5233
5623
  }
@@ -6904,6 +7294,26 @@ export function codexHooksSyncOptions(input) {
6904
7294
  verify: input.verify !== false,
6905
7295
  };
6906
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
+ }
6907
7317
  /**
6908
7318
  * What the user is told about a sync report — the ONE place the success word can be printed.
6909
7319
  *
@@ -7221,6 +7631,430 @@ function gatherReadmeCounts(root) {
7221
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)');
7222
7632
  return pairs;
7223
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
+ }
7224
8058
  /** Gather the facts one op needs. All I/O is best-effort — a missing signal skips its rule, never crashes. */
7225
8059
  function gatherGuardFacts(op, root, text, storeCap) {
7226
8060
  const facts = { op };
@@ -7355,7 +8189,7 @@ function gatherGuardFacts(op, root, text, storeCap) {
7355
8189
  const manifests = [];
7356
8190
  const located = [];
7357
8191
  try {
7358
- 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']);
7359
8193
  for (const rel of out.split('\n').map((s) => s.trim()).filter(Boolean)) {
7360
8194
  try {
7361
8195
  const m = JSON.parse(readFileSync(join(root, rel), 'utf8'));
@@ -7384,6 +8218,11 @@ function gatherGuardFacts(op, root, text, storeCap) {
7384
8218
  packages.push({ name: m.name ?? '(unnamed)', deps });
7385
8219
  }
7386
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
+ })));
7387
8226
  // licence-hold (ADR-001, hermes-claude-adaptation): for each pack DECLARING a hold via a
7388
8227
  // `licenseHold` field, hand the raw evidence to the pure checker. Best-effort: an unreadable
7389
8228
  // LICENSE reads as absent (null), which the checker treats as a violation for a publishable
@@ -7661,6 +8500,10 @@ function runGuardEvaluation(root, op, text, overrideReason) {
7661
8500
  catch { /* audit is best-effort, never blocks the verdict */ }
7662
8501
  return result;
7663
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
+ }
7664
8507
  /**
7665
8508
  * The tail facts of an append-only log, read from its END — O(1) in the file size, which is what
7666
8509
  * lets the chain be extended on every append without a full-file scan (FR-2). Anything unreadable
@@ -8035,8 +8878,9 @@ function cmdGuardPromote(options, flags, root, write) {
8035
8878
  ...next,
8036
8879
  entries: Object.fromEntries(Object.entries(next.entries).map(([k, v]) => (applied.includes(k) ? [k, { ...v, appliedTs: nowTs }] : [k, v]))),
8037
8880
  };
8881
+ const withEvidence = recordPromotionRunEvidence(withApplied, report, nowTs);
8038
8882
  try {
8039
- writeJsonAtomic(join(root, PROMOTION_STATE_FILE), withApplied);
8883
+ writeJsonAtomic(join(root, PROMOTION_STATE_FILE), withEvidence);
8040
8884
  }
8041
8885
  catch { /* best-effort */ }
8042
8886
  }
@@ -8145,6 +8989,8 @@ function cmdGuard(options, flags, cwd, write) {
8145
8989
  }
8146
8990
  const glyph = result.verdict === 'block' ? '✗' : result.verdict === 'warn' ? '⚠' : '✓';
8147
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));
8148
8994
  for (const v of result.violations)
8149
8995
  write(` [${v.severity === 'hard' ? 'BLOCK' : 'warn'}] ${v.rule}: ${v.detail}`);
8150
8996
  for (const n of result.notes ?? [])
@@ -12743,6 +13589,134 @@ function readOptionalText(path) {
12743
13589
  return '';
12744
13590
  }
12745
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
+ }
12746
13720
  function deadwoodAllowlistText() {
12747
13721
  const require = createRequire(import.meta.url);
12748
13722
  const corePackage = require.resolve('@dzhechkov/harness-core/package.json');
@@ -12823,7 +13797,7 @@ function cmdCompounding(options, flags, cwd, write) {
12823
13797
  const json = flags.has('json');
12824
13798
  if (flags.has('help')) {
12825
13799
  write('dz compounding [--project <dir>] [--json] — honest learning-loop payoff report');
12826
- write(' pool payoff (write-only ratio) · guard repeat-violation trajectory · cold-vs-warm replay readiness · instrumentation health');
13800
+ write(' pool payoff · guard trajectory · replay readiness · instrumentation · monthly eligible→attempted→accepted→executions funnel');
12827
13801
  return 0;
12828
13802
  }
12829
13803
  for (const flag of flags) {
@@ -12852,36 +13826,14 @@ function cmdCompounding(options, flags, cwd, write) {
12852
13826
  }));
12853
13827
  // apply-leg usage events (read records only; aggregate rows carry no query by construction)
12854
13828
  const usage = readRecallUsageEvents(root);
12855
- // guard audit events
12856
- const guard = [];
12857
- try {
12858
- const text = readFileSync(join(root, '.dz', 'guard-audit.jsonl'), 'utf-8');
12859
- for (const line of text.split('\n')) {
12860
- if (line.trim() === '')
12861
- continue;
12862
- try {
12863
- const o = JSON.parse(line);
12864
- if (typeof o.ts === 'string' && typeof o.verdict === 'string') {
12865
- guard.push({
12866
- ts: o.ts,
12867
- verdict: o.verdict,
12868
- rules: Array.isArray(o.violations) ? o.violations.map((v) => (typeof v?.rule === 'string' ? v.rule : '')).filter(Boolean) : [],
12869
- });
12870
- }
12871
- }
12872
- catch {
12873
- /* skip */
12874
- }
12875
- }
12876
- }
12877
- catch {
12878
- /* no audit yet */
12879
- }
13829
+ const promotionEvidence = readPromotionRunEvidence(root);
13830
+ const guardEvidence = readGuardAuditEvidence(root);
13831
+ const guard = [...guardEvidence.rows];
12880
13832
  // The evidence logs themselves, verbatim: the report verifies their hash chains (feature
12881
13833
  // event-chain). Handing over the TEXT rather than a pre-computed verdict keeps one definition of
12882
13834
  // "the chain is intact" — a second copy here is how a gate and its report start disagreeing.
12883
13835
  const evidenceLogs = [];
12884
- for (const rel of ['.dz/recall-usage.jsonl', '.dz/guard-audit.jsonl']) {
13836
+ for (const rel of ['.dz/recall-usage.jsonl']) {
12885
13837
  try {
12886
13838
  evidenceLogs.push({ log: rel, text: readFileSync(join(root, ...rel.split('/')), 'utf-8') });
12887
13839
  }
@@ -12889,6 +13841,9 @@ function cmdCompounding(options, flags, cwd, write) {
12889
13841
  /* absent log — reported by its own gate above, not invented here */
12890
13842
  }
12891
13843
  }
13844
+ if (guardEvidence.text !== null) {
13845
+ evidenceLogs.push({ log: '.dz/guard-audit.jsonl', text: guardEvidence.text });
13846
+ }
12892
13847
  const now = new Date();
12893
13848
  const report = assembleCompoundingReport({
12894
13849
  lessons,
@@ -12897,6 +13852,13 @@ function cmdCompounding(options, flags, cwd, write) {
12897
13852
  nowTs: now.toISOString(),
12898
13853
  evidenceLogs,
12899
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
+ },
12900
13862
  });
12901
13863
  // lesson-bandit-rerank §11: the payoff axis joins THIS report rather than growing a private
12902
13864
  // dashboard — the `rewardEvents : exposureEvents` row asks exactly the question this command
@@ -14921,11 +15883,11 @@ export async function runCli(argv, io = {}) {
14921
15883
  case 'bundle':
14922
15884
  return cmdBundle(options, flags, cwd, write);
14923
15885
  case 'teach':
14924
- 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);
14925
15887
  case 'consolidate':
14926
15888
  return await cmdConsolidate(options, flags, cwd, write);
14927
15889
  case 'recall':
14928
- return await cmdRecall(options, flags, cwd, write);
15890
+ return await cmdRecall(options, flags, cwd, write, writeErr, io.classMatcher);
14929
15891
  case 'vector':
14930
15892
  return await cmdVector(options, flags, cwd, write);
14931
15893
  case 'brain':
@@ -14978,6 +15940,8 @@ export async function runCli(argv, io = {}) {
14978
15940
  return cmdDriftCheck(options, flags, cwd, write);
14979
15941
  case 'hooks-sync':
14980
15942
  return cmdHooksSync(options, flags, cwd, write, writeErr);
15943
+ case 'integrations-verify':
15944
+ return cmdIntegrationsVerify(options, flags, cwd, write, writeErr);
14981
15945
  case 'agents-sync':
14982
15946
  return cmdAgentsSync(options, flags, cwd, write, writeErr);
14983
15947
  case 'sync-canonical':