@dzhechkov/harness-cli 0.3.237 → 0.3.238

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/.dz-manifest.json CHANGED
@@ -9,7 +9,7 @@
9
9
  },
10
10
  {
11
11
  "path": "README.md",
12
- "sha256": "7e8b772a6fa9ca0f36364cafd5ea996abeb62dc894c2f67f50ea7704a7a8824d"
12
+ "sha256": "ef6603652143fc1517c81506dad9d5e7b79e5d5138aad9f6281ad77492d58b71"
13
13
  },
14
14
  {
15
15
  "path": "coverage/coverage-final.json",
@@ -41,15 +41,15 @@
41
41
  },
42
42
  {
43
43
  "path": "dist/cli.d.ts.map",
44
- "sha256": "cf6a3265890a8856ef31e3e7ac58026c15232c3c118ba0fcd1ce0d7590b84dd8"
44
+ "sha256": "8f8807920c7ddd901361eb5a602df9c85fa69726e47535a146937038eab6b281"
45
45
  },
46
46
  {
47
47
  "path": "dist/cli.js",
48
- "sha256": "b9af9be1bd31acd60e76275ff1055dd8e9b39b531b0c375467648f142b74d2c4"
48
+ "sha256": "5f5fbcc7c040a272f6af992ef1f9c39482b941d74dbcadec9f3d9008babcb08a"
49
49
  },
50
50
  {
51
51
  "path": "dist/cli.js.map",
52
- "sha256": "6a51963ede51129e43524d62f3d757255a002dfec3df2c1dfa519af6f235a667"
52
+ "sha256": "420d1178a513e54f315095a17e9c5722543ca6cbdbebd9a674d4d50ef61b8aa0"
53
53
  },
54
54
  {
55
55
  "path": "dist/index.d.ts",
@@ -73,7 +73,7 @@
73
73
  },
74
74
  {
75
75
  "path": "package.json",
76
- "sha256": "1e7ef1d6e0796144999ad64d3650aeb4a2ccc7dfc66c5c5a8b705d783a9d0801"
76
+ "sha256": "8fbaf10d29b5733cf9445e1ef5804a3e124f733c3390a99937d2d8dc3b4cf02a"
77
77
  },
78
78
  {
79
79
  "path": "src/bin.ts",
@@ -81,7 +81,7 @@
81
81
  },
82
82
  {
83
83
  "path": "src/cli.ts",
84
- "sha256": "5180330ac6612542c80a74ea8e0983f5cd231e95bb3962eccdd65fbdbf6611a8"
84
+ "sha256": "131f23a50187e1be19875d4ccd76d8c354b1fbb8d193c03e73fda16ff64f51dc"
85
85
  },
86
86
  {
87
87
  "path": "src/index.ts",
@@ -89,7 +89,7 @@
89
89
  },
90
90
  {
91
91
  "path": "test/cli.test.ts",
92
- "sha256": "461beed08ad6cfde7bbf559701fcbc6978f622c3951c80463c4e4943c538eff5"
92
+ "sha256": "3deb99917f3d8b4a5c9a3b0315bd65a3099ce2b508121cfa2bdeed19b7554d48"
93
93
  },
94
94
  {
95
95
  "path": "tsconfig.json",
@@ -101,5 +101,5 @@
101
101
  }
102
102
  ]
103
103
  },
104
- "signature": "06gdM99WpJhmarOtUFoeMAlGAfAvCboh/QGbCEIBhMAqgLn5rnSCXAb2023QdO+hZsuxrijyGzgBh62l/L+GBQ=="
104
+ "signature": "spl/yr0En3yUAxhYjhIdot8D/Tri4JeoMrweQkA9GF2F9UyzDco3FLpaqCkvwlhGvj4phKb4qB0Nm3e+GAMaDw=="
105
105
  }
package/README.md CHANGED
@@ -372,6 +372,41 @@ the release job gets a perfectly attested malicious package.
372
372
  A ready-to-install workflow is at `features/publish-provenance/07_code_changes/publish.yml`; copy it to
373
373
  `.github/workflows/` and add an `NPM_TOKEN` secret.
374
374
 
375
+ ### Lesson quarantine — a fresh lesson is a hypothesis, not knowledge
376
+
377
+ Self-learning has a poisoning problem: the moment `dz teach` stores a lesson, it ranks alongside
378
+ patterns proven over months and can ride the auto-inject hook into your next task's context — even
379
+ if it is wrong, one-off, or junk. Quarantine (opt-in) closes the gap between COLLECT and RANK:
380
+
381
+ ```jsonc
382
+ // .dz/config.json
383
+ { "memory": { "learning": {
384
+ "quarantine": true, // fresh lessons start as quarantined hypotheses
385
+ "quarantineDamp": 0.5, // rank multiplier for ⚠q hits in recall (0..1]
386
+ "quarantineExpireDays": 30 // unconfirmed after N days ⇒ expiry CANDIDATE (report only)
387
+ } } }
388
+ ```
389
+
390
+ Three surfaces, three strictness levels — and promotion is EARNED, never automatic:
391
+
392
+ ```bash
393
+ dz teach "..." # → "⚠ quarantined: excluded from auto-inject, damped in recall"
394
+ dz recall "topic" # ⚠q hits are VISIBLE but marked + rank-damped (never hidden)
395
+ # the UserPromptSubmit auto-inject hook EXCLUDES ⚠q lessons entirely (logged, never silent)
396
+
397
+ dz teach --reinforce "<exact text>" # confirming a lesson IS its promotion
398
+ dz recall --promote <dzId> --apply # or promote explicitly (dry-run by default)
399
+
400
+ dz consolidate --prune-quarantine # report expired unconfirmed lessons (dry-run)
401
+ dz consolidate --prune-quarantine --apply # remove them (snapshots first) — a SEPARATE gate,
402
+ # never coupled to --prune-noise (unproven ≠ garbage)
403
+ ```
404
+
405
+ Absent config = zero behavior change. Existing lessons are grandfathered as promoted; a corrupt
406
+ quarantine marker reads as promoted (a metadata glitch must never isolate proven knowledge).
407
+ **When to use:** any project where subagents teach lessons unattended — the quarantine is the gate
408
+ between "an agent wrote this down" and "agents now act on it".
409
+
375
410
  ### Target parity — `dz parity` (the honest feature × target map)
376
411
 
377
412
  The harness runs on 10 targets, but not every feature runs everywhere: hooks, MCP (Model Context
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAuQH,2EAA2E;AAC3E,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAC;CAC5C;AAED,yFAAyF;AACzF,MAAM,MAAM,iBAAiB,GAAG,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,KACvD;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAo9K9E,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkJ5E"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA8QH,2EAA2E;AAC3E,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAC;CAC5C;AAED,yFAAyF;AACzF,MAAM,MAAM,iBAAiB,GAAG,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,KACvD;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAyiL9E,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkJ5E"}
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
9
9
  import { execSync } from 'node:child_process';
10
10
  import { homedir, tmpdir } from 'node:os';
11
11
  import { createRequire } from 'node:module';
12
- import { createSkill, getSkillInfo, getWorkflow, isTargetName, listSkills, runDoctor, runInit, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, TARGET_NAMES, buildParityMatrix, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_NAMES, importEcc, recordPattern, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, computeUsage, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, claimCheck, summarize, queryBookKnowledge, loadStorePatternsSync, patternRecordId, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, decideProvenance, isInsideTree, signManifest, verifyManifest, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, } from '@dzhechkov/harness-core';
12
+ import { createSkill, getSkillInfo, getWorkflow, isTargetName, listSkills, runDoctor, runInit, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, TARGET_NAMES, buildParityMatrix, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_NAMES, importEcc, recordPattern, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, computeUsage, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, claimCheck, summarize, queryBookKnowledge, loadStorePatternsSync, patternRecordId, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, decideProvenance, isInsideTree, signManifest, verifyManifest, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, } from '@dzhechkov/harness-core';
13
13
  import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
14
14
  import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
15
15
  const USAGE = `dz - DZ cross-platform harness CLI
@@ -38,8 +38,8 @@ Usage:
38
38
  dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--project <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force] [--enrich]
39
39
  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)
40
40
  dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
41
- dz consolidate [--sessions-dir <dir>] [--project <dir>] [--no-mirror] [--prune-noise [--apply]] (prune-noise: DRY-RUN by default; --apply snapshots then deletes)
42
- dz recall "<query>" [--limit <N>] [--semantic | --no-semantic] [--books [--book <slug>]] [--project <dir>] | dz recall --all [--json] | dz recall --usage [--json] | dz recall --forget <dzId>[,<dzId>] [--apply] (forget: dry-run default; snapshots before removing)
41
+ 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)
42
+ dz recall "<query>" [--limit <N>] [--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] (forget/promote: dry-run default; forget snapshots before removing; promote lifts lesson-quarantine)
43
43
  dz vector status [--project <dir>] [--json] (semantic tier: engine, mirrored vs lexical counts, pending queue)
44
44
  dz vector reindex [--project <dir>] [--json] (snapshot, re-embed learned-pattern vectors, stamp current model)
45
45
  dz vector export <path> [--project <dir>] (portable VECTOR form (.rvf, opt-in RVF engine); patterns ship via recall --all --json)
@@ -1199,6 +1199,18 @@ async function cmdTeach(options, flags, cwd, write) {
1199
1199
  if (receipt.mirrored > 0)
1200
1200
  write(` ↳ mirrored to vector tier (${receipt.engine ?? 'vector'})`);
1201
1201
  };
1202
+ // lesson-quarantine FR-8: the fresh-teach mirror carries the qStatus marker so the hook daemon
1203
+ // (which reads only the mirror's metadata) can exclude unproven lessons from auto-inject.
1204
+ const emitMirrorQ = async (root, records, source, quarantined) => {
1205
+ if (flags.has('no-mirror') || records.length === 0 || !vectorMirrorEnabled(root))
1206
+ return;
1207
+ const entries = records
1208
+ .map((r) => patternVectorEntry(r, source, quarantined ? { quarantined: true } : {}))
1209
+ .filter((e) => e !== undefined);
1210
+ const receipt = await mirrorEntriesToVector(root, entries);
1211
+ if (receipt.mirrored > 0)
1212
+ write(` ↳ mirrored to vector tier (${receipt.engine ?? 'vector'})${quarantined ? ' [quarantined]' : ''}`);
1213
+ };
1202
1214
  // `dz teach --harmonize` — documented ALIAS of `dz vector harmonize`: SEMANTIC dedup of the
1203
1215
  // learned store. ONE implementation (harmonizeVectorStore), two entry points (QR-6). Routed
1204
1216
  // BEFORE the --from-json / single-teach paths. Dry-run by default; --apply after a backup.
@@ -1278,6 +1290,10 @@ async function cmdTeach(options, flags, cwd, write) {
1278
1290
  const trained = await backend.train();
1279
1291
  if (trained.flushed > 0) {
1280
1292
  write(`↳ reinforced ${reinforce}`);
1293
+ // lesson-quarantine: reinforcement IS promotion — keep the hook daemon's mirror in step.
1294
+ const clearedQ = clearAgentdbQuarantine(projectRoot, [reinforce]);
1295
+ if (clearedQ.cleared > 0)
1296
+ write(` ↳ promoted out of quarantine (mirror updated)`);
1281
1297
  return 0;
1282
1298
  }
1283
1299
  // HIGH-fix: a no-match must NOT auto-teach the raw argument — callers pass dzIds or truncated
@@ -1309,6 +1325,9 @@ async function cmdTeach(options, flags, cwd, write) {
1309
1325
  // teach below so the lesson is NEVER silently discarded (the exact silent-drop the ADR forbids).
1310
1326
  if (trained.flushed > 0) {
1311
1327
  write(`↳ reinforced existing pattern ${verdict.dzId} (cos=${verdict.cosine.toFixed(2)}) — not re-added`);
1328
+ const clearedQ = clearAgentdbQuarantine(projectRoot, [verdict.dzId]);
1329
+ if (clearedQ.cleared > 0)
1330
+ write(' ↳ promoted out of quarantine (mirror updated)');
1312
1331
  return 0;
1313
1332
  }
1314
1333
  write(`dz teach --guard: reinforce of ${verdict.dzId} did not flush (backend off or write failure) — teaching the lesson normally instead`);
@@ -1335,18 +1354,45 @@ async function cmdTeach(options, flags, cwd, write) {
1335
1354
  // Tier-2 (ADR-005): persist through the unified @dzhechkov/memory store. recordPattern
1336
1355
  // folds any legacy .dz/patterns.jsonl into the backend (idempotent) and returns the
1337
1356
  // total count. The lossy `npx agentdb add` dual-write was removed in Tier-1 (audit #6).
1338
- const count = await recordPattern(projectRoot, entry);
1357
+ // lesson-quarantine (opt-in): a fresh lesson is a HYPOTHESIS until it earns promotion.
1358
+ const quarantineOn = readMemoryLearningConfig(projectRoot).quarantine;
1359
+ const count = await recordPattern(projectRoot, entry, quarantineOn ? { quarantine: true } : {});
1339
1360
  write(`Learned: "${pattern.slice(0, 60)}${pattern.length > 60 ? '...' : ''}"`);
1340
1361
  write(` Domain: ${domain} Reward: ${reward} Backend: memory (@dzhechkov/memory)`);
1341
1362
  write(` Total patterns: ${count}`);
1363
+ if (quarantineOn) {
1364
+ write(' ⚠ quarantined: excluded from auto-inject, damped in recall — promote by confirming it (dz teach --reinforce "<text>") or dz recall --promote <dzId> --apply');
1365
+ }
1342
1366
  // The lexical write above is durable — the vector mirror is strictly best-effort (I-3).
1343
- await emitMirror(projectRoot, [entry], 'dz-teach');
1367
+ await emitMirrorQ(projectRoot, [entry], 'dz-teach', quarantineOn);
1344
1368
  return 0;
1345
1369
  }
1346
1370
  async function cmdConsolidate(options, flags, cwd, write) {
1347
1371
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
1348
1372
  const sessionsDirOpt = options.get('sessions-dir');
1349
1373
  const pruneNoise = flags.has('prune-noise');
1374
+ // lesson-quarantine FR-7: expiry is a SEPARATE, explicit surface — never coupled to prune-noise
1375
+ // (the recalled decay-vs-noise lesson: valid-but-unproven is not garbage). Dry-run by default.
1376
+ if (flags.has('prune-quarantine')) {
1377
+ const cfg = readMemoryLearningConfig(projectRoot);
1378
+ const res = pruneQuarantinePatterns(projectRoot, { dryRun: !flags.has('apply'), expireDays: cfg.quarantineExpireDays });
1379
+ if (!flags.has('apply')) {
1380
+ write(`dz consolidate --prune-quarantine: DRY RUN — ${res.candidates.length} expired quarantined lesson(s) (> ${cfg.quarantineExpireDays}d, never reinforced)`);
1381
+ for (const c of res.candidates)
1382
+ write(` [${c.ageDays < 0 ? '?' : c.ageDays}d] ${c.dzId} ${c.text.slice(0, 70)}`);
1383
+ if (res.candidates.length > 0)
1384
+ write(' → re-run with --apply to remove (snapshots first); or promote keepers: dz recall --promote <dzId> --apply');
1385
+ return 0;
1386
+ }
1387
+ if (res.error !== undefined) {
1388
+ write(`dz consolidate --prune-quarantine: ${res.error}`);
1389
+ return 1;
1390
+ }
1391
+ write(`dz consolidate --prune-quarantine: removed ${res.removed} expired quarantined lesson(s)`);
1392
+ if (res.snapshot !== undefined)
1393
+ write(` snapshot: ${res.snapshot}`);
1394
+ return 0;
1395
+ }
1350
1396
  // --prune-noise: RETRO-PRUNE legacy noise (tool telemetry + system-wrapper "responses") from
1351
1397
  // the lexical store AND the agentdb vector mirror BEFORE harvesting, so this run's watermark
1352
1398
  // never re-learns from junk. Best-effort — a prune error is reported, never fatal.
@@ -1572,6 +1618,44 @@ async function cmdRecallForget(options, flags, projectRoot, write) {
1572
1618
  write(' the vector mirror still holds them — run `dz vector reindex` to resync');
1573
1619
  return 0;
1574
1620
  }
1621
+ /**
1622
+ * `dz recall --promote <dzId>[,<dzId>…] [--apply]` — lift quarantine from NAMED records
1623
+ * (lesson-quarantine FR-6b). Dry-run by default, the --forget symmetry. Also clears the
1624
+ * agentdb mirror's qStatus (best-effort) so the hook daemon stops excluding promoted lessons.
1625
+ */
1626
+ async function cmdRecallPromote(options, flags, projectRoot, write) {
1627
+ const ids = (options.get('promote') ?? '').split(',').map((s) => s.trim()).filter((s) => s.length > 0);
1628
+ if (ids.length === 0) {
1629
+ write('dz recall --promote: no ids given (comma-separated dzIds; quarantined ones are marked ⚠q in recall)');
1630
+ return 1;
1631
+ }
1632
+ if (!flags.has('apply')) {
1633
+ const records = loadStoreRecords(projectRoot);
1634
+ const found = ids.filter((id) => records.some((r) => r.id === id));
1635
+ write(`dz recall --promote: DRY RUN — ${found.length}/${ids.length} id(s) match the store. Re-run with --apply to promote.`);
1636
+ return 0;
1637
+ }
1638
+ const res = await promotePatterns(projectRoot, ids);
1639
+ // Codex-QE finding 5: even on a mid-batch failure, the ALREADY-promoted records must get their
1640
+ // mirror rows cleared — otherwise the canonical store and the hook-visible mirror split-brain.
1641
+ if (res.promoted.length > 0) {
1642
+ const cleared = clearAgentdbQuarantine(projectRoot, res.promoted);
1643
+ if (cleared.cleared > 0)
1644
+ write(` ↳ mirror updated (${cleared.cleared} row(s) un-quarantined in agentdb)`);
1645
+ else if (cleared.error !== undefined)
1646
+ write(` ⚠ mirror not updated (${cleared.error}) — run dz vector reindex to resync`);
1647
+ }
1648
+ if (!res.ok) {
1649
+ write(`dz recall --promote: failed — ${res.error ?? 'unknown error'} (promoted so far: ${res.promoted.length})`);
1650
+ return 1;
1651
+ }
1652
+ write(`dz recall --promote: promoted ${res.promoted.length} record(s)`);
1653
+ if (res.notQuarantined.length > 0)
1654
+ write(` already promoted (not quarantined): ${res.notQuarantined.join(', ')}`);
1655
+ if (res.notFound.length > 0)
1656
+ write(` not found: ${res.notFound.join(', ')}`);
1657
+ return 0;
1658
+ }
1575
1659
  async function cmdRecall(options, flags, cwd, write) {
1576
1660
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
1577
1661
  const asJson = flags.has('json');
@@ -1580,6 +1664,8 @@ async function cmdRecall(options, flags, cwd, write) {
1580
1664
  return cmdRecallUsage(options, flags, projectRoot, write);
1581
1665
  if (options.has('forget'))
1582
1666
  return cmdRecallForget(options, flags, projectRoot, write);
1667
+ if (options.has('promote'))
1668
+ return cmdRecallPromote(options, flags, projectRoot, write);
1583
1669
  // --all: dump the entire learned store (backend-agnostic, via loadStorePatternsSync).
1584
1670
  // With --json this is the portable export the agentdb-memory MCP bridge consumes.
1585
1671
  if (all) {
@@ -1679,9 +1765,17 @@ async function cmdRecall(options, flags, cwd, write) {
1679
1765
  const lexLabel = result.lexicalBackend === 'sqlite' ? 'SQLite FTS5' : 'keyword (JSON)';
1680
1766
  const ranking = vectorOn ? `${lexLabel} + vector (${result.vectorEngine}) ranking` : `${lexLabel} ranking (lexical)`;
1681
1767
  write(`dz recall "${query}" — ${hits.length} hit(s), ${ranking}`);
1768
+ let sawQuarantined = false;
1682
1769
  for (const h of hits) {
1683
1770
  const backendTag = vectorOn ? ` ⟨${h.backend}⟩` : '';
1684
- write(` [${h.pattern.reward.toFixed(2)}] (${h.pattern.domain})${backendTag} ${h.pattern.pattern.slice(0, 80)}`);
1771
+ const qTag = h.quarantined === true ? ' ⚠q' : '';
1772
+ if (h.quarantined === true)
1773
+ sawQuarantined = true;
1774
+ write(` [${h.pattern.reward.toFixed(2)}] (${h.pattern.domain})${backendTag}${qTag} ${h.pattern.pattern.slice(0, 80)}`);
1775
+ }
1776
+ if (sawQuarantined) {
1777
+ // The loop stays VISIBLE (ADR D2): a quarantined hit is shown, marked, and explained.
1778
+ write(' ⚠q = quarantined (unproven hypothesis, rank damped) — confirm with dz teach --reinforce, or dz recall --promote <dzId> --apply');
1685
1779
  }
1686
1780
  if (result.vectorError !== undefined && mode !== 'lexical') {
1687
1781
  // Engine present but the semantic leg failed/timed out — one honest line, exit 0 (05 §2.3).