@dzhechkov/harness-cli 0.3.205 → 0.3.207

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/harness-cli",
3
- "version": "0.3.205",
3
+ "version": "0.3.207",
4
4
  "description": "The dz CLI — install AI skills for Claude Code, Codex, OpenCode, Hermes, OpenClaude, GitHub Copilot. 35 commands, 13 presets, 6 platform targets.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -54,7 +54,7 @@
54
54
  "@dzhechkov/skills-reverse-engineering": "^0.1.0",
55
55
  "@dzhechkov/skills-presentation-storyteller": "^0.1.0",
56
56
  "@dzhechkov/skills-website-cloner": "^0.1.0",
57
- "@dzhechkov/harness-core": "0.3.104"
57
+ "@dzhechkov/harness-core": "0.3.106"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^25.6.0",
package/src/cli.ts CHANGED
@@ -55,6 +55,8 @@ import {
55
55
  storeStats,
56
56
  consolidateSessions,
57
57
  pruneNoisePatterns,
58
+ removePatternsByIds,
59
+ snapshotStore,
58
60
  recallHybrid,
59
61
  teachGuard,
60
62
  mirrorPatternsToVector,
@@ -74,6 +76,9 @@ import {
74
76
  summarize,
75
77
  queryBookKnowledge,
76
78
  loadStorePatternsSync,
79
+ patternRecordId,
80
+ loadStoreRecords,
81
+ recordToPattern,
77
82
  bundleSkills,
78
83
  brainHome,
79
84
  listBrain,
@@ -87,8 +92,12 @@ import {
87
92
  exportBrainSlice,
88
93
  importBrainSlice,
89
94
  registerKusToBrain,
95
+ RECALL_USAGE_LOG_RELATIVE,
96
+ RECALL_USAGE_LOG_MAX_BYTES,
97
+ parseRecallUsageLog,
98
+ buildRecallUsageReport,
90
99
  } from '@dzhechkov/harness-core';
91
- import type { ClaudeUsageModel, PatternRecord, TargetName, BookKU, HarmonizeReport, UsageCalibrationPlan, ClaimFinding } from '@dzhechkov/harness-core';
100
+ import type { ClaudeUsageModel, PatternRecord, TargetName, BookKU, HarmonizeReport, UsageCalibrationPlan, ClaimFinding, RecallUsagePatternRow } from '@dzhechkov/harness-core';
92
101
  import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
93
102
  import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
94
103
 
@@ -112,8 +121,8 @@ Usage:
112
121
  dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--project <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force] [--enrich]
113
122
  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)
114
123
  dz teach --from-json <file> [--project <dir>] [--no-mirror] (bulk-import a 'dz recall --all --json' export — share a learned store across machines)
115
- dz consolidate [--sessions-dir <dir>] [--project <dir>] [--no-mirror] [--prune-noise]
116
- dz recall "<query>" [--limit <N>] [--semantic | --no-semantic] [--books [--book <slug>]] [--project <dir>] | dz recall --all [--json]
124
+ dz consolidate [--sessions-dir <dir>] [--project <dir>] [--no-mirror] [--prune-noise [--apply]] (prune-noise: DRY-RUN by default; --apply snapshots then deletes)
125
+ 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)
117
126
  dz vector status [--project <dir>] [--json] (semantic tier: engine, mirrored vs lexical counts, pending queue)
118
127
  dz vector reindex [--project <dir>] [--json] (snapshot, re-embed learned-pattern vectors, stamp current model)
119
128
  dz vector export <path> [--project <dir>] (portable VECTOR form (.rvf, opt-in RVF engine); patterns ship via recall --all --json)
@@ -1102,7 +1111,7 @@ function cmdStatusline(
1102
1111
  return 0;
1103
1112
  }
1104
1113
 
1105
- let line = `🎓 dz: ${data.patterns} patterns · 🧠 ${data.brainSources} sources`;
1114
+ let line = `🎓 dz: ${data.patterns} patterns${data.usedPatterns !== undefined ? ` · ${data.usedPatterns} used` : ''} · 🧠 ${data.brainSources} sources`;
1106
1115
  const branch = statuslineGitBranch(projectRoot);
1107
1116
  if (branch !== undefined) line += ` · ⎇ ${branch}`;
1108
1117
  if (data.consolidatedAgeH !== undefined) line += ` · ⟳ ${data.consolidatedAgeH}h`;
@@ -1554,8 +1563,24 @@ async function cmdConsolidate(options: Map<string, string>, flags: Set<string>,
1554
1563
  // --prune-noise: RETRO-PRUNE legacy noise (tool telemetry + system-wrapper "responses") from
1555
1564
  // the lexical store AND the agentdb vector mirror BEFORE harvesting, so this run's watermark
1556
1565
  // never re-learns from junk. Best-effort — a prune error is reported, never fatal.
1566
+ //
1567
+ // DRY-RUN BY DEFAULT. The learned store lives in a git-ignored `.dz/` and has no history: a
1568
+ // wrong sweep is unrecoverable. `--prune-noise` therefore only PREVIEWS; `--apply` performs the
1569
+ // deletion, and snapshots the store to a restorable JSON export first. (Behaviour change: before
1570
+ // this, `--prune-noise` deleted immediately and silently.)
1571
+ const applyPrune = flags.has('apply');
1557
1572
  let pruned: ReturnType<typeof pruneNoisePatterns> | undefined;
1558
- if (pruneNoise) pruned = pruneNoisePatterns(projectRoot);
1573
+ let pruneSnapshot: string | undefined;
1574
+ if (pruneNoise) {
1575
+ if (applyPrune) {
1576
+ const dest = join(projectRoot, '.dz', `patterns-pre-prune-${Date.now()}.json`);
1577
+ const snap = snapshotStore(projectRoot, dest);
1578
+ if (snap.error === undefined) pruneSnapshot = snap.path;
1579
+ else write(` ⚠ snapshot failed (${snap.error}) — prune SKIPPED, the store is not versioned`);
1580
+ if (snap.error !== undefined) return 1; // never delete what we could not back up
1581
+ }
1582
+ pruned = pruneNoisePatterns(projectRoot, { dryRun: !applyPrune });
1583
+ }
1559
1584
  // Throw-safety (QE P3): this command also runs DETACHED from the SessionEnd hook (stdio
1560
1585
  // ignored) — an uncaught store/watermark error would be fully invisible there. Catch, leave
1561
1586
  // an observable note next to the session markers, and report on stdout for manual runs.
@@ -1585,7 +1610,17 @@ async function cmdConsolidate(options: Map<string, string>, flags: Set<string>,
1585
1610
  // Echo active flags in the header (learned pattern: flags must be visible in command output).
1586
1611
  write(`dz consolidate${pruneNoise ? ' (--prune-noise)' : ''}`);
1587
1612
  if (pruned !== undefined) {
1588
- write(` pruned: ${pruned.lexicalRemoved} lexical, ${pruned.vectorRemoved} vector`);
1613
+ const verb = pruned.dryRun === true ? 'would prune' : 'pruned';
1614
+ write(` ${verb}: ${pruned.lexicalRemoved} lexical, ${pruned.vectorRemoved} vector`);
1615
+ for (const c of (pruned.candidates ?? []).slice(0, 10)) {
1616
+ write(` ${c.id} ${c.text.replace(/\s+/g, ' ').slice(0, 72)}`);
1617
+ }
1618
+ const extra = (pruned.candidates?.length ?? 0) - 10;
1619
+ if (extra > 0) write(` … and ${extra} more`);
1620
+ if (pruned.dryRun === true && pruned.lexicalRemoved + pruned.vectorRemoved > 0) {
1621
+ write(' DRY RUN — nothing deleted. Re-run with --apply to remove (snapshots first).');
1622
+ }
1623
+ if (pruneSnapshot !== undefined) write(` snapshot: ${pruneSnapshot}`);
1589
1624
  if (pruned.error !== undefined) write(` Prune: ${pruned.error}`);
1590
1625
  }
1591
1626
  write(` Sessions dir: ${result.sessionsDir} (${sourceLabel[result.source]})`);
@@ -1606,10 +1641,132 @@ async function cmdConsolidate(options: Map<string, string>, flags: Set<string>,
1606
1641
  return 0;
1607
1642
  }
1608
1643
 
1644
+ function recallUsagePatternRefs(projectRoot: string): { dzId: string; pattern: string; domain: string; reward: number }[] {
1645
+ return loadStoreRecords(projectRoot).map((record) => {
1646
+ const pattern = recordToPattern(record);
1647
+ return { dzId: record.id, pattern: pattern.pattern, domain: pattern.domain, reward: pattern.reward };
1648
+ });
1649
+ }
1650
+
1651
+ function readRecallUsageReport(projectRoot: string) {
1652
+ let text = '';
1653
+ const logPath = join(projectRoot, RECALL_USAGE_LOG_RELATIVE);
1654
+ try {
1655
+ if (existsSync(logPath)) text = readFileSync(logPath, 'utf-8');
1656
+ } catch {
1657
+ text = '';
1658
+ }
1659
+ return buildRecallUsageReport(recallUsagePatternRefs(projectRoot), parseRecallUsageLog(text));
1660
+ }
1661
+
1662
+ function fmtUsageRow(row: RecallUsagePatternRow): string {
1663
+ const last = row.lastReadAt !== undefined ? ` last=${row.lastReadAt}` : '';
1664
+ const score = row.avgScore !== undefined ? ` avg=${row.avgScore.toFixed(3)}` : '';
1665
+ const domain = row.domain !== undefined ? ` (${row.domain})` : '';
1666
+ return `${row.reads}×${last}${score} ${row.dzId}${domain} ${row.pattern.slice(0, 100)}`;
1667
+ }
1668
+
1669
+ function cmdRecallUsage(options: Map<string, string>, flags: Set<string>, projectRoot: string, write: Write): number {
1670
+ const report = readRecallUsageReport(projectRoot);
1671
+ if (flags.has('json')) {
1672
+ write(JSON.stringify({
1673
+ ...report,
1674
+ log: join(projectRoot, RECALL_USAGE_LOG_RELATIVE),
1675
+ retention: { maxBytes: RECALL_USAGE_LOG_MAX_BYTES },
1676
+ }));
1677
+ return 0;
1678
+ }
1679
+
1680
+ const displayLimit = Math.max(1, parseInt(options.get('limit') ?? '20', 10) || 20);
1681
+ write(`dz recall --usage — ${report.usedPatterns}/${report.totalPatterns} pattern(s) read, ${report.totalReads} read event(s)`);
1682
+ write(` log: ${RECALL_USAGE_LOG_RELATIVE} (bounded at ${RECALL_USAGE_LOG_MAX_BYTES} bytes)`);
1683
+ if (report.invalidLines > 0) write(` skipped invalid/torn line(s): ${report.invalidLines}`);
1684
+ if (report.unknownReadPatterns > 0) write(` usage rows for missing pattern id(s): ${report.unknownReadPatterns}`);
1685
+
1686
+ write(' most read:');
1687
+ if (report.top.length === 0) {
1688
+ write(' none yet');
1689
+ } else {
1690
+ for (const row of report.top.slice(0, displayLimit)) write(` ${fmtUsageRow(row)}`);
1691
+ if (report.top.length > displayLimit) write(` ... ${report.top.length - displayLimit} more (use --json for the full list)`);
1692
+ }
1693
+
1694
+ write(' never read:');
1695
+ if (report.neverRead.length === 0) {
1696
+ write(' none');
1697
+ } else {
1698
+ for (const row of report.neverRead.slice(0, displayLimit)) write(` ${row.dzId}${row.domain !== undefined ? ` (${row.domain})` : ''} ${row.pattern.slice(0, 100)}`);
1699
+ if (report.neverRead.length > displayLimit) write(` ... ${report.neverRead.length - displayLimit} more (use --json for the full list)`);
1700
+ }
1701
+ return 0;
1702
+ }
1703
+
1704
+ /**
1705
+ * `dz recall --forget <dzId>[,<dzId>…] [--apply]` — remove NAMED records from the learned store.
1706
+ *
1707
+ * `--prune-noise` only removes what `isNoiseInsight` recognises. It cannot reach the other junk
1708
+ * class: records that are structurally fine but were never lessons. This repo has two of them —
1709
+ * `mismatch probe A` / `match probe B`, written into the LIVE store on 2026-07-07 by the
1710
+ * `dz teach --project <relative>` path bug fixed in `83b1bac` (the relative root resolved to the
1711
+ * cwd store instead of the temp project). The bug is gone; its residue is not, and no predicate
1712
+ * will ever classify it.
1713
+ *
1714
+ * DRY-RUN BY DEFAULT, and it snapshots before deleting: `.dz/` is git-ignored and unversioned, so a
1715
+ * mistaken id is unrecoverable. Use `dz recall --all --json` to find ids.
1716
+ */
1717
+ async function cmdRecallForget(
1718
+ options: Map<string, string>,
1719
+ flags: Set<string>,
1720
+ projectRoot: string,
1721
+ write: Write,
1722
+ ): Promise<number> {
1723
+ const raw = options.get('forget') ?? '';
1724
+ const ids = new Set(raw.split(',').map((s) => s.trim()).filter((s) => s !== ''));
1725
+ if (ids.size === 0) {
1726
+ write('dz recall --forget: no ids given (comma-separated dzIds; find them with `dz recall --all --json`)');
1727
+ return 1;
1728
+ }
1729
+
1730
+ // Use the CANONICAL store ids, not `patternRecordId(pattern)`. `removePatternsByIds` matches on
1731
+ // `MemoryRecord.id`, and the two coincide only for records dz itself wrote (`dz teach` stores the
1732
+ // hash as the id). A record seeded or migrated with a different id would be reported as "found",
1733
+ // then silently NOT removed — the command would claim success while deleting nothing.
1734
+ const known = new Map(loadStoreRecords(projectRoot).map((r) => [r.id, r.text]));
1735
+ const found = [...ids].filter((id) => known.has(id));
1736
+ const missing = [...ids].filter((id) => !known.has(id));
1737
+
1738
+ for (const id of found) write(` ${id} ${(known.get(id) ?? '').replace(/\s+/g, ' ').slice(0, 72)}`);
1739
+ for (const id of missing) write(` ${id} (not in the store — nothing to forget)`);
1740
+
1741
+ if (found.length === 0) {
1742
+ write('dz recall --forget: nothing matched.');
1743
+ return 1;
1744
+ }
1745
+ if (!flags.has('apply')) {
1746
+ write(`dz recall --forget: DRY RUN — ${found.length} record(s) would be removed. Re-run with --apply.`);
1747
+ return 0;
1748
+ }
1749
+
1750
+ const dest = join(projectRoot, '.dz', `patterns-pre-forget-${Date.now()}.json`);
1751
+ const snap = snapshotStore(projectRoot, dest);
1752
+ if (snap.error !== undefined) {
1753
+ write(`dz recall --forget: snapshot failed (${snap.error}) — nothing removed; the store is not versioned`);
1754
+ return 1;
1755
+ }
1756
+ const result = removePatternsByIds(projectRoot, new Set(found));
1757
+ write(`dz recall --forget: removed ${result.removed} record(s)`);
1758
+ write(` snapshot: ${snap.path} (${snap.count} record(s))`);
1759
+ if (result.error !== undefined) write(` ⚠ ${result.error}`);
1760
+ write(' the vector mirror still holds them — run `dz vector reindex` to resync');
1761
+ return 0;
1762
+ }
1763
+
1609
1764
  async function cmdRecall(options: Map<string, string>, flags: Set<string>, cwd: string, write: Write): Promise<number> {
1610
1765
  const projectRoot = resolve(cwd, options.get('project') ?? '.');
1611
1766
  const asJson = flags.has('json');
1612
1767
  const all = flags.has('all');
1768
+ if (flags.has('usage')) return cmdRecallUsage(options, flags, projectRoot, write);
1769
+ if (options.has('forget')) return cmdRecallForget(options, flags, projectRoot, write);
1613
1770
 
1614
1771
  // --all: dump the entire learned store (backend-agnostic, via loadStorePatternsSync).
1615
1772
  // With --json this is the portable export the agentdb-memory MCP bridge consumes.