@hiveai/cli 0.7.1 → 0.8.0

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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command39 } from "commander";
4
+ import { Command as Command40 } from "commander";
5
5
 
6
6
  // src/commands/briefing.ts
7
7
  import { existsSync } from "fs";
@@ -14,6 +14,7 @@ import {
14
14
  literalMatchesAnyToken,
15
15
  loadCodeMap,
16
16
  loadMemoriesFromDir,
17
+ loadUsageIndex,
17
18
  memoryMatchesAnchorPaths,
18
19
  queryCodeMap,
19
20
  resolveHaivePaths,
@@ -51,11 +52,240 @@ var ui = {
51
52
  }
52
53
  };
53
54
 
55
+ // src/utils/briefing-radar.ts
56
+ import { execFile } from "child_process";
57
+ import { promisify } from "util";
58
+ var exec = promisify(execFile);
59
+ var DEFAULT_DAYS_BACK = 14;
60
+ var DEFAULT_MAX_COMMITS = 5;
61
+ var DEFAULT_MAX_TODOS = 8;
62
+ var DEFAULT_MAX_HOT_FILES = 5;
63
+ var TODO_RE = /\b(?:TODO|FIXME|HACK|XXX)\b[: ]?(.{0,120})/i;
64
+ var SOURCE_GLOBS = [
65
+ "*.ts",
66
+ "*.tsx",
67
+ "*.js",
68
+ "*.jsx",
69
+ "*.py",
70
+ "*.go",
71
+ "*.rs",
72
+ "*.java",
73
+ "*.kt",
74
+ "*.swift",
75
+ "*.rb",
76
+ "*.php",
77
+ "*.cs",
78
+ "*.cpp",
79
+ "*.c",
80
+ "*.h"
81
+ ];
82
+ async function isGitRepo(root) {
83
+ try {
84
+ await exec("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root });
85
+ return true;
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
90
+ async function getRecentCommits(root, daysBack, maxCommits, taskTokens, filePaths) {
91
+ try {
92
+ const { stdout } = await exec(
93
+ "git",
94
+ [
95
+ "log",
96
+ `--since=${daysBack}.days.ago`,
97
+ "--name-only",
98
+ "--pretty=format:%x1f%h%x1f%ad%x1f%s",
99
+ "--date=short",
100
+ "-n",
101
+ "60"
102
+ ],
103
+ { cwd: root, maxBuffer: 4 * 1024 * 1024 }
104
+ );
105
+ const blocks = stdout.split("").filter((b) => b.trim().length > 0);
106
+ const commits = [];
107
+ for (let i = 0; i + 2 < blocks.length; i += 3) {
108
+ const sha = blocks[i].trim();
109
+ const date = blocks[i + 1].trim();
110
+ const tail = blocks[i + 2];
111
+ const lines = tail.split("\n").map((l) => l.trim()).filter(Boolean);
112
+ const subject = lines.shift() ?? "";
113
+ const files = lines;
114
+ commits.push({ sha, date, subject, files });
115
+ }
116
+ const lowerTokens = taskTokens?.map((t) => t.toLowerCase()) ?? [];
117
+ const lowerPaths = filePaths.map((p) => p.toLowerCase());
118
+ const scored = commits.map((c) => {
119
+ let score = 0;
120
+ const haystack = (c.subject + " " + c.files.join(" ")).toLowerCase();
121
+ for (const t of lowerTokens) if (haystack.includes(t)) score += 2;
122
+ for (const p of lowerPaths) if (c.files.some((f) => f.toLowerCase().includes(p))) score += 3;
123
+ return { c, score };
124
+ });
125
+ if (lowerTokens.length === 0 && lowerPaths.length === 0) {
126
+ return commits.slice(0, maxCommits);
127
+ }
128
+ return scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, maxCommits).map((s) => s.c);
129
+ } catch {
130
+ return [];
131
+ }
132
+ }
133
+ async function getOpenTodos(root, maxTodos, taskTokens, filePaths) {
134
+ try {
135
+ const includeArgs = SOURCE_GLOBS.flatMap((g) => ["--include", g]);
136
+ const { stdout } = await exec(
137
+ "grep",
138
+ [
139
+ "-rnE",
140
+ "--exclude-dir=node_modules",
141
+ "--exclude-dir=.git",
142
+ "--exclude-dir=dist",
143
+ "--exclude-dir=build",
144
+ "--exclude-dir=.next",
145
+ "--exclude-dir=coverage",
146
+ ...includeArgs,
147
+ "\\b(TODO|FIXME|HACK|XXX)\\b",
148
+ "."
149
+ ],
150
+ { cwd: root, maxBuffer: 4 * 1024 * 1024 }
151
+ ).catch((err) => ({ stdout: err.stdout ?? "" }));
152
+ const lines = stdout.split("\n").filter(Boolean);
153
+ const parsed = [];
154
+ for (const line of lines) {
155
+ const m = line.match(/^([^:]+):(\d+):(.*)$/);
156
+ if (!m) continue;
157
+ const [, file, lineNoStr, rest] = m;
158
+ const todoMatch = rest.match(TODO_RE);
159
+ if (!todoMatch) continue;
160
+ const text = (todoMatch[1] ?? "").trim() || rest.trim().slice(0, 120);
161
+ parsed.push({ file: file.replace(/^\.\//, ""), line: Number(lineNoStr), text });
162
+ }
163
+ const lowerTokens = taskTokens?.map((t) => t.toLowerCase()) ?? [];
164
+ const lowerPaths = filePaths.map((p) => p.toLowerCase());
165
+ if (lowerTokens.length === 0 && lowerPaths.length === 0) {
166
+ return parsed.slice(0, maxTodos);
167
+ }
168
+ const scored = parsed.map((t) => {
169
+ let score = 0;
170
+ const hay = (t.file + " " + t.text).toLowerCase();
171
+ for (const tok of lowerTokens) if (hay.includes(tok)) score += 1;
172
+ for (const p of lowerPaths) if (t.file.toLowerCase().includes(p)) score += 2;
173
+ return { t, score };
174
+ });
175
+ return scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, maxTodos).map((s) => s.t);
176
+ } catch {
177
+ return [];
178
+ }
179
+ }
180
+ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
181
+ try {
182
+ const { stdout } = await exec(
183
+ "git",
184
+ [
185
+ "log",
186
+ `--since=${daysBack * 6}.days.ago`,
187
+ "--name-only",
188
+ "--pretty=format:"
189
+ ],
190
+ { cwd: root, maxBuffer: 4 * 1024 * 1024 }
191
+ );
192
+ const counts = /* @__PURE__ */ new Map();
193
+ for (const raw of stdout.split("\n")) {
194
+ const f = raw.trim();
195
+ if (!f) continue;
196
+ counts.set(f, (counts.get(f) ?? 0) + 1);
197
+ }
198
+ let entries = [...counts.entries()].map(([path37, changes]) => ({ path: path37, changes }));
199
+ const lowerPaths = filePaths.map((p) => p.toLowerCase());
200
+ if (lowerPaths.length > 0) {
201
+ entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
202
+ }
203
+ return entries.sort((a, b) => b.changes - a.changes).slice(0, maxHotFiles);
204
+ } catch {
205
+ return [];
206
+ }
207
+ }
208
+ async function buildRadar(opts) {
209
+ const inside = await isGitRepo(opts.root);
210
+ if (!inside) {
211
+ return { recentCommits: [], openTodos: [], hotFiles: [], insideGitRepo: false };
212
+ }
213
+ const daysBack = opts.daysBack ?? DEFAULT_DAYS_BACK;
214
+ const [recentCommits, openTodos, hotFiles] = await Promise.all([
215
+ getRecentCommits(opts.root, daysBack, opts.maxCommits ?? DEFAULT_MAX_COMMITS, opts.taskTokens, opts.filePaths),
216
+ getOpenTodos(opts.root, opts.maxTodos ?? DEFAULT_MAX_TODOS, opts.taskTokens, opts.filePaths),
217
+ getHotFiles(opts.root, daysBack, opts.maxHotFiles ?? DEFAULT_MAX_HOT_FILES, opts.filePaths)
218
+ ]);
219
+ return { recentCommits, openTodos, hotFiles, insideGitRepo: true };
220
+ }
221
+ function radarHasContent(r) {
222
+ return r.recentCommits.length > 0 || r.openTodos.length > 0 || r.hotFiles.length > 0;
223
+ }
224
+
54
225
  // src/commands/briefing.ts
226
+ var RADAR_AUTO_THRESHOLD = 3;
227
+ var CHARS_PER_TOKEN = 4;
228
+ function printRadar(radar, out, reason) {
229
+ if (!radar.insideGitRepo) return;
230
+ if (!radarHasContent(radar)) return;
231
+ const header = reason === "low-memory-signal" ? "=== Project Radar (few relevant memories \u2014 surfacing live signals) ===" : "=== Project Radar ===";
232
+ out(`${ui.bold(header)}
233
+ `);
234
+ if (radar.recentCommits.length > 0) {
235
+ out(ui.bold("Recent commits:"));
236
+ for (const c of radar.recentCommits) {
237
+ const filesBlurb = c.files.slice(0, 3).join(", ");
238
+ const more = c.files.length > 3 ? ` (+${c.files.length - 3})` : "";
239
+ out(` ${ui.dim(c.date)} ${c.sha} ${c.subject}`);
240
+ if (filesBlurb) out(ui.dim(` ${filesBlurb}${more}`));
241
+ }
242
+ out("");
243
+ }
244
+ if (radar.openTodos.length > 0) {
245
+ out(ui.bold("Open TODOs/FIXMEs:"));
246
+ for (const t of radar.openTodos) {
247
+ out(` ${ui.dim(t.file + ":" + t.line)} ${t.text}`);
248
+ }
249
+ out("");
250
+ }
251
+ if (radar.hotFiles.length > 0) {
252
+ out(ui.bold("Hot files (most modified recently):"));
253
+ for (const f of radar.hotFiles) {
254
+ out(` ${f.changes}\xD7 ${ui.dim(f.path)}`);
255
+ }
256
+ out("");
257
+ }
258
+ }
259
+ var TokenBudgetWriter = class {
260
+ constructor(budgetChars) {
261
+ this.budgetChars = budgetChars;
262
+ }
263
+ budgetChars;
264
+ used = 0;
265
+ truncated = false;
266
+ write(text) {
267
+ if (this.truncated) return false;
268
+ const next = this.used + text.length + 1;
269
+ if (next > this.budgetChars) {
270
+ console.log(ui.dim(`... [briefing truncated to fit --max-tokens budget \xB7 ${Math.round(this.used / CHARS_PER_TOKEN)} tokens used]`));
271
+ this.truncated = true;
272
+ return false;
273
+ }
274
+ console.log(text);
275
+ this.used = next;
276
+ return true;
277
+ }
278
+ isTruncated() {
279
+ return this.truncated;
280
+ }
281
+ remainingChars() {
282
+ return Math.max(0, this.budgetChars - this.used);
283
+ }
284
+ };
55
285
  function registerBriefing(program2) {
56
286
  program2.command("briefing").description(
57
287
  'Print the full project briefing: last session recap + project context + relevant memories.\n Equivalent to calling get_briefing via MCP. Run before starting any task.\n\n Examples:\n haive briefing\n haive briefing --task "add Stripe payment" --files src/payments/PaymentService.ts\n haive briefing --symbols PaymentService,TenantFilter # look up where symbols live\n'
58
- ).option("--task <text>", "what you are about to do \u2014 filters memories by relevance").option("--files <csv>", "comma-separated file paths being worked on (surfaces anchored memories)").option("--symbols <csv>", "symbol names to look up in the code-map (e.g. PaymentService,TenantFilter) \u2014 requires haive index code").option("--max-memories <n>", "cap on memories surfaced", "10").option(
288
+ ).option("--task <text>", "what you are about to do \u2014 filters memories by relevance").option("--files <csv>", "comma-separated file paths being worked on (surfaces anchored memories)").option("--symbols <csv>", "symbol names to look up in the code-map (e.g. PaymentService,TenantFilter) \u2014 requires haive index code").option("--max-memories <n>", "cap on memories surfaced", "10").option("--max-tokens <n>", "approximate token budget for the entire briefing (truncates if exceeded)").option("--explain-source", "annotate each memory with [source: <relative-path> \xB7 anchors: <files>] for traceable citations").option("--radar", "force project radar (recent commits, open TODOs, hot files) even when memories are plentiful").option("--no-radar", "disable the project radar even when memories are scarce").option(
59
289
  "--scope <scope>",
60
290
  "personal | team | shared | all (default: all \u2014 includes team + shared cross-repo memories)",
61
291
  "all"
@@ -67,14 +297,29 @@ function registerBriefing(program2) {
67
297
  ).option("-d, --dir <dir>", "project root").action(async (opts) => {
68
298
  const root = findProjectRoot(opts.dir);
69
299
  const paths = resolveHaivePaths(root);
300
+ const budgetTokens = opts.maxTokens ? Math.max(100, Number(opts.maxTokens)) : null;
301
+ const writer = budgetTokens ? new TokenBudgetWriter(budgetTokens * CHARS_PER_TOKEN) : null;
302
+ const out = (text) => {
303
+ if (writer) return writer.write(text);
304
+ console.log(text);
305
+ return true;
306
+ };
307
+ const stopped = () => writer?.isTruncated() ?? false;
70
308
  if (!existsSync(paths.memoriesDir)) {
71
309
  if (existsSync(paths.projectContext)) {
72
- console.log(`${ui.bold("=== Project Context ===")}
310
+ out(`${ui.bold("=== Project Context ===")}
73
311
  `);
74
- console.log((await readFile(paths.projectContext, "utf8")).trim());
312
+ out((await readFile(paths.projectContext, "utf8")).trim());
313
+ out("");
75
314
  } else {
76
315
  ui.warn("No project-context.md found. Run `haive init` and the `bootstrap_project` MCP prompt to set it up.");
77
316
  }
317
+ if (opts.radar !== false && !stopped()) {
318
+ const filePathsEarly = parseCsv(opts.files);
319
+ const tokensEarly = opts.task ? tokenizeQuery(opts.task) : null;
320
+ const radar = await buildRadar({ root, taskTokens: tokensEarly, filePaths: filePathsEarly });
321
+ printRadar(radar, out, "low-memory-signal");
322
+ }
78
323
  return;
79
324
  }
80
325
  const ownMemories = await loadMemoriesFromDir(paths.memoriesDir);
@@ -112,17 +357,17 @@ function registerBriefing(program2) {
112
357
  const recaps = all.filter(({ memory: mem }) => mem.frontmatter.type === "session_recap").sort(
113
358
  (a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
114
359
  );
115
- if (recaps.length > 0) {
360
+ if (recaps.length > 0 && !stopped()) {
116
361
  const recap = recaps[0];
117
362
  const fm = recap.memory.frontmatter;
118
363
  const rev = fm.revision_count ? ` \xB7 revision #${fm.revision_count}` : "";
119
- console.log(`${ui.bold("=== Last Session Recap ===")}
364
+ out(`${ui.bold("=== Last Session Recap ===")}
120
365
  `);
121
- console.log(ui.dim(`${fm.id} (${fm.scope}${rev})`));
122
- console.log(recap.memory.body.trim());
123
- console.log();
366
+ out(ui.dim(`${fm.id} (${fm.scope}${rev})`));
367
+ out(recap.memory.body.trim());
368
+ out("");
124
369
  }
125
- if (existsSync(paths.projectContext)) {
370
+ if (existsSync(paths.projectContext) && !stopped()) {
126
371
  const ctx = await readFile(paths.projectContext, "utf8");
127
372
  const isTemplate = ctx.includes("TODO \u2014 high-level overview") || ctx.includes("Generated by `haive init`");
128
373
  if (isTemplate) {
@@ -132,14 +377,14 @@ function registerBriefing(program2) {
132
377
  ui.warn(
133
378
  "Fix: in your AI client, invoke the MCP prompt bootstrap_project to auto-fill it from your codebase."
134
379
  );
135
- console.log();
380
+ out("");
136
381
  } else {
137
- console.log(`${ui.bold("=== Project Context ===")}
382
+ out(`${ui.bold("=== Project Context ===")}
138
383
  `);
139
- console.log(ctx.trim());
140
- console.log();
384
+ out(ctx.trim());
385
+ out("");
141
386
  }
142
- } else {
387
+ } else if (!existsSync(paths.projectContext)) {
143
388
  ui.warn(
144
389
  "No project-context.md found. Run `haive init` then invoke the bootstrap_project MCP prompt."
145
390
  );
@@ -177,54 +422,85 @@ function registerBriefing(program2) {
177
422
  if (draftCount > 0) {
178
423
  ui.info(`(${draftCount} draft memories excluded \u2014 use --include-draft to show)`);
179
424
  }
425
+ if (opts.radar !== false && !stopped()) {
426
+ const radar = await buildRadar({ root, taskTokens: tokens, filePaths });
427
+ out("");
428
+ printRadar(radar, out, "low-memory-signal");
429
+ }
180
430
  return;
181
431
  }
182
- console.log(`${ui.bold("=== Relevant Memories ===")}
432
+ if (stopped()) return;
433
+ const usageIndex = await loadUsageIndex(paths).catch(() => null);
434
+ out(`${ui.bold("=== Relevant Memories ===")}
183
435
  `);
184
436
  for (const item of top) {
437
+ if (stopped()) break;
185
438
  const fm = item.memory.frontmatter;
186
439
  const badge = ui.statusBadge(fm.status);
187
440
  const draftMarker = fm.status === "draft" ? ui.yellow(" [DRAFT]") : "";
188
441
  const unverifiedMarker = fm.status === "proposed" ? ui.yellow(" [UNVERIFIED]") : "";
189
442
  const originMarker = item.origin ? ` ${ui.yellow("[from " + item.origin + "]")}` : "";
190
- console.log(
191
- `${ui.bold(fm.id)} ${ui.dim(fm.scope + "/" + fm.type)} ${badge}${draftMarker}${unverifiedMarker}${originMarker}`
443
+ const reads = usageIndex?.by_id[fm.id]?.read_count ?? 0;
444
+ const hitMarker = reads > 0 ? ` ${ui.dim("\xB7 " + reads + "\xD7 read")}` : "";
445
+ out(
446
+ `${ui.bold(fm.id)} ${ui.dim(fm.scope + "/" + fm.type)} ${badge}${draftMarker}${unverifiedMarker}${originMarker}${hitMarker}`
192
447
  );
193
- console.log(item.memory.body.trim());
194
- console.log();
448
+ if (opts.explainSource) {
449
+ const relPath = path.relative(root, item.filePath);
450
+ const anchorPaths = fm.anchor?.paths ?? [];
451
+ const anchorSymbols = fm.anchor?.symbols ?? [];
452
+ const parts = [`source: ${relPath}`];
453
+ if (anchorPaths.length > 0) parts.push(`paths: ${anchorPaths.join(", ")}`);
454
+ if (anchorSymbols.length > 0) parts.push(`symbols: ${anchorSymbols.join(", ")}`);
455
+ out(ui.dim(` [${parts.join(" \xB7 ")}]`));
456
+ }
457
+ out(item.memory.body.trim());
458
+ out("");
195
459
  }
196
- console.log(ui.dim(`${top.length} memor${top.length === 1 ? "y" : "ies"} surfaced`));
460
+ if (!stopped()) out(ui.dim(`${top.length} memor${top.length === 1 ? "y" : "ies"} surfaced`));
197
461
  const ids = top.map(({ memory: mem }) => mem.frontmatter.id);
198
462
  if (ids.length > 0) {
199
463
  await trackReads(paths, ids).catch(() => {
200
464
  });
201
465
  }
466
+ const radarForced = opts.radar === true;
467
+ const radarAuto = opts.radar !== false && top.length < RADAR_AUTO_THRESHOLD;
468
+ if ((radarForced || radarAuto) && !stopped()) {
469
+ const radar = await buildRadar({ root, taskTokens: tokens, filePaths });
470
+ if (radarHasContent(radar)) {
471
+ out("");
472
+ printRadar(radar, out, radarForced ? "forced" : "low-memory-signal");
473
+ }
474
+ }
202
475
  const requestedSymbols = (opts.symbols ?? "").split(",").map((s) => s.trim()).filter(Boolean);
203
- if (requestedSymbols.length > 0) {
476
+ if (requestedSymbols.length > 0 && !stopped()) {
204
477
  const codeMap = await loadCodeMap(paths);
205
478
  if (!codeMap) {
206
479
  ui.warn("No code-map found. Run `haive index code` first to enable symbol lookup.");
207
480
  } else {
208
- console.log(`
481
+ out(`
209
482
  ${ui.bold("=== Symbol Locations ===")}
210
483
  `);
211
484
  for (const sym of requestedSymbols) {
485
+ if (stopped()) break;
212
486
  const { files } = queryCodeMap(codeMap, { symbol: sym });
213
487
  if (files.length === 0) {
214
- console.log(`${ui.dim(sym)} (not found in code-map)`);
488
+ out(`${ui.dim(sym)} (not found in code-map)`);
215
489
  } else {
216
490
  for (const f of files) {
491
+ if (stopped()) break;
217
492
  const exports = f.entry.exports.filter(
218
493
  (e) => e.name.toLowerCase().includes(sym.toLowerCase())
219
494
  );
220
495
  for (const e of exports) {
496
+ if (stopped()) break;
221
497
  const desc = e.description ? ` \u2014 ${e.description}` : "";
222
- console.log(`${ui.bold(e.name)} ${ui.dim(f.path + ":" + e.line)} [${e.kind}]${desc}`);
498
+ out(`${ui.bold(e.name)} ${ui.dim(f.path + ":" + e.line)} [${e.kind}]${desc}`);
223
499
  }
224
500
  }
225
501
  }
226
502
  }
227
- console.log();
503
+ out("");
228
504
  }
229
505
  }
230
506
  });
@@ -610,12 +886,15 @@ function inferModuleDescriptions(dirs, frameworks = []) {
610
886
  "shared": "shared code across modules"
611
887
  };
612
888
  const isNestJS = frameworks.includes("NestJS");
889
+ const isSrcBased = isNestJS || frameworks.some((f) => ["Express", "Fastify", "Hono", "tRPC", "Apollo", "GraphQL"].includes(f));
890
+ const isFrontend = frameworks.some((f) => ["React", "Vue", "Svelte", "SvelteKit", "Astro"].includes(f));
613
891
  const srcSubdirs = dirs.filter((d) => d.startsWith("src/") && d.split("/").length === 2);
614
- if (isNestJS && srcSubdirs.length >= 2) {
615
- const result = [`- \`src/\` \u2014 main source (NestJS feature modules)`];
892
+ if (isSrcBased && srcSubdirs.length >= 2 || isFrontend && srcSubdirs.length >= 3) {
893
+ const label = isNestJS ? "main source (NestJS feature modules)" : "main source";
894
+ const result = [`- \`src/\` \u2014 ${label}`];
616
895
  for (const d of srcSubdirs.slice(0, 12)) {
617
896
  const name = d.split("/")[1];
618
- const desc = known[name.toLowerCase()] ?? "feature module";
897
+ const desc = known[name.toLowerCase()] ?? "module";
619
898
  result.push(` - \`${name}/\` \u2014 ${desc}`);
620
899
  }
621
900
  const otherTopLevel = dirs.filter((d) => !d.includes("/") && d !== "src").slice(0, 6);
@@ -1668,18 +1947,21 @@ jobs:
1668
1947
  `;
1669
1948
  function registerInit(program2) {
1670
1949
  program2.command("init").description(
1671
- "Initialize a hAIve project \u2014 autopilot mode ON by default (zero human intervention).\n Add --manual if you want to control memory approval and session recaps yourself."
1950
+ "Initialize a hAIve project \u2014 autopilot mode ON by default (zero human intervention).\n Auto-bootstraps project-context.md from local files and seeds detected stack packs.\n Add --manual to control memory approval and session recaps yourself.\n Add --no-bootstrap and --stack none to disable the auto-features."
1672
1951
  ).option("-d, --dir <dir>", "project root", process.cwd()).option("--no-bridges", "do not generate CLAUDE.md / .cursorrules / copilot-instructions.md").option("--with-ci", "write a GitHub Actions workflow (.github/workflows/haive-sync.yml) \u2014 included automatically in autopilot mode").option(
1673
1952
  "--manual",
1674
1953
  "opt out of autopilot: memories require manual approval, no auto-session recap, no auto-context"
1675
1954
  ).option(
1676
1955
  "--bootstrap",
1677
- "auto-generate .ai/project-context.md from package.json, README, and directory structure (no AI needed)"
1956
+ "auto-generate .ai/project-context.md from package.json, README, and directory structure (ON by default in autopilot)"
1957
+ ).option(
1958
+ "--no-bootstrap",
1959
+ "skip the project-context auto-generation (only the default template is written)"
1678
1960
  ).option(
1679
1961
  "--stack <stacks>",
1680
1962
  `pre-seed validated memory packs for the given stacks (comma-separated).
1681
1963
  Supported: ${SUPPORTED_STACKS.join(", ")}.
1682
- Use 'auto' to detect from package.json automatically.`
1964
+ Defaults to 'auto' in autopilot mode (detects from package.json). Pass 'none' to disable.`
1683
1965
  ).option(
1684
1966
  "--no-mcp-setup",
1685
1967
  "skip auto-configuring haive-mcp in Cursor / VS Code / Claude Code"
@@ -1687,6 +1969,8 @@ function registerInit(program2) {
1687
1969
  const root = path7.resolve(opts.dir);
1688
1970
  const paths = resolveHaivePaths4(root);
1689
1971
  const autopilot = opts.manual !== true;
1972
+ const wantBootstrap = opts.bootstrap === void 0 ? autopilot : opts.bootstrap;
1973
+ const wantStack = opts.stack === void 0 ? autopilot ? "auto" : void 0 : opts.stack === "none" ? void 0 : opts.stack;
1690
1974
  if (existsSync6(paths.haiveDir)) {
1691
1975
  ui.warn(`.ai/ already exists at ${paths.haiveDir} \u2014 leaving existing files in place.`);
1692
1976
  }
@@ -1695,7 +1979,7 @@ function registerInit(program2) {
1695
1979
  await mkdir3(paths.moduleDir, { recursive: true });
1696
1980
  await mkdir3(paths.modulesContextDir, { recursive: true });
1697
1981
  if (!existsSync6(paths.projectContext)) {
1698
- if (opts.bootstrap) {
1982
+ if (wantBootstrap) {
1699
1983
  ui.info("Bootstrapping project context from local files\u2026");
1700
1984
  try {
1701
1985
  const context = await generateBootstrapContext(root);
@@ -1724,7 +2008,7 @@ function registerInit(program2) {
1724
2008
  await writeBridge(root, ".cursorrules");
1725
2009
  await writeBridge(root, path7.join(".github", "copilot-instructions.md"));
1726
2010
  }
1727
- const stacksToSeed = await resolveStacksToSeed(root, opts.stack);
2011
+ const stacksToSeed = await resolveStacksToSeed(root, wantStack);
1728
2012
  if (stacksToSeed.length > 0) {
1729
2013
  let totalSeeded = 0;
1730
2014
  for (const stack of stacksToSeed) {
@@ -1866,11 +2150,106 @@ async function writeBridge(root, relPath) {
1866
2150
  }
1867
2151
 
1868
2152
  // src/commands/install-hooks.ts
1869
- import { mkdir as mkdir4, writeFile as writeFile4, chmod, readFile as readFile5 } from "fs/promises";
1870
- import { existsSync as existsSync7 } from "fs";
1871
- import path8 from "path";
2153
+ import { mkdir as mkdir5, writeFile as writeFile5, chmod, readFile as readFile6 } from "fs/promises";
2154
+ import { existsSync as existsSync8 } from "fs";
2155
+ import path9 from "path";
1872
2156
  import "commander";
1873
2157
  import { findProjectRoot as findProjectRoot6 } from "@hiveai/core";
2158
+
2159
+ // src/utils/claude-hooks.ts
2160
+ import { existsSync as existsSync7 } from "fs";
2161
+ import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
2162
+ import path8 from "path";
2163
+ var HAIVE_HOOK_TAG = "haive-passive-capture";
2164
+ var POST_TOOL_USE_GROUP = {
2165
+ matcher: "Edit|Write|Bash",
2166
+ hooks: [
2167
+ {
2168
+ type: "command",
2169
+ command: "haive observe",
2170
+ haive_tag: HAIVE_HOOK_TAG
2171
+ }
2172
+ ]
2173
+ };
2174
+ var SESSION_END_GROUP = {
2175
+ hooks: [
2176
+ {
2177
+ type: "command",
2178
+ command: "haive session end --quiet --auto",
2179
+ haive_tag: HAIVE_HOOK_TAG
2180
+ }
2181
+ ]
2182
+ };
2183
+ function dropHaiveGroups(groups) {
2184
+ return groups.filter(
2185
+ (g) => !g.hooks.some((h) => h.haive_tag === HAIVE_HOOK_TAG)
2186
+ );
2187
+ }
2188
+ function patchClaudeSettings(input) {
2189
+ const settings = input ? { ...input } : {};
2190
+ const hooks = settings.hooks ? { ...settings.hooks } : {};
2191
+ hooks.PostToolUse = [
2192
+ ...dropHaiveGroups(hooks.PostToolUse ?? []),
2193
+ POST_TOOL_USE_GROUP
2194
+ ];
2195
+ hooks.SessionEnd = [
2196
+ ...dropHaiveGroups(hooks.SessionEnd ?? []),
2197
+ SESSION_END_GROUP
2198
+ ];
2199
+ settings.hooks = hooks;
2200
+ return settings;
2201
+ }
2202
+ function unpatchClaudeSettings(input) {
2203
+ const settings = input ? { ...input } : {};
2204
+ if (!settings.hooks) return settings;
2205
+ const hooks = { ...settings.hooks };
2206
+ for (const [event, groups] of Object.entries(hooks)) {
2207
+ const cleaned = dropHaiveGroups(groups);
2208
+ if (cleaned.length === 0) {
2209
+ delete hooks[event];
2210
+ } else {
2211
+ hooks[event] = cleaned;
2212
+ }
2213
+ }
2214
+ settings.hooks = hooks;
2215
+ if (Object.keys(hooks).length === 0) delete settings.hooks;
2216
+ return settings;
2217
+ }
2218
+ async function installClaudeHooksAtPath(settingsPath) {
2219
+ let raw = null;
2220
+ let created = false;
2221
+ if (existsSync7(settingsPath)) {
2222
+ try {
2223
+ raw = JSON.parse(await readFile5(settingsPath, "utf8"));
2224
+ } catch {
2225
+ throw new Error(`${settingsPath} exists but is not valid JSON. Fix it manually first.`);
2226
+ }
2227
+ } else {
2228
+ created = true;
2229
+ }
2230
+ const patched = patchClaudeSettings(raw);
2231
+ await mkdir4(path8.dirname(settingsPath), { recursive: true });
2232
+ await writeFile4(settingsPath, JSON.stringify(patched, null, 2) + "\n", "utf8");
2233
+ return { settingsPath, created };
2234
+ }
2235
+ async function uninstallClaudeHooksAtPath(settingsPath) {
2236
+ if (!existsSync7(settingsPath)) {
2237
+ return { settingsPath, created: false };
2238
+ }
2239
+ const raw = JSON.parse(await readFile5(settingsPath, "utf8"));
2240
+ const cleaned = unpatchClaudeSettings(raw);
2241
+ await writeFile4(settingsPath, JSON.stringify(cleaned, null, 2) + "\n", "utf8");
2242
+ return { settingsPath, created: false };
2243
+ }
2244
+ function defaultClaudeSettingsPath(scope, projectRoot) {
2245
+ if (scope === "user") {
2246
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
2247
+ return path8.join(home, ".claude", "settings.json");
2248
+ }
2249
+ return path8.join(projectRoot, ".claude", "settings.local.json");
2250
+ }
2251
+
2252
+ // src/commands/install-hooks.ts
1874
2253
  var HOOK_MARKER = "# hAIve auto-generated";
1875
2254
  var POST_MERGE_BODY = `#!/bin/sh
1876
2255
  ${HOOK_MARKER} \u2014 keep this block to allow upgrades. Hand-edit anything outside it.
@@ -1919,50 +2298,192 @@ var HOOKS = [
1919
2298
  { name: "post-rewrite", body: POST_MERGE_BODY },
1920
2299
  { name: "pre-push", body: PRE_PUSH_BODY }
1921
2300
  ];
2301
+ async function installGitHooks(opts) {
2302
+ const root = findProjectRoot6(opts.dir);
2303
+ const gitDir = path9.join(root, ".git");
2304
+ if (!existsSync8(gitDir)) {
2305
+ ui.error(`No .git directory at ${root}.`);
2306
+ process.exitCode = 1;
2307
+ return;
2308
+ }
2309
+ const hooksDir = path9.join(gitDir, "hooks");
2310
+ await mkdir5(hooksDir, { recursive: true });
2311
+ let installed = 0;
2312
+ let skipped = 0;
2313
+ for (const { name, body } of HOOKS) {
2314
+ const file = path9.join(hooksDir, name);
2315
+ if (existsSync8(file) && !opts.force) {
2316
+ const existing = await readFile6(file, "utf8");
2317
+ if (!existing.includes(HOOK_MARKER)) {
2318
+ ui.warn(`${name} already exists and was not written by hAIve. Re-run with --force to overwrite.`);
2319
+ skipped++;
2320
+ continue;
2321
+ }
2322
+ }
2323
+ await writeFile5(file, body, "utf8");
2324
+ await chmod(file, 493);
2325
+ installed++;
2326
+ }
2327
+ ui.success(`Installed ${installed} git hook(s) in .git/hooks/${skipped ? `, skipped ${skipped}` : ""}`);
2328
+ ui.info("post-merge: haive sync runs after every pull/merge.");
2329
+ ui.info("pre-push: haive precommit runs before every push (advisory, never blocks).");
2330
+ ui.info(" Set HAIVE_BLOCK=1 in your shell to make pre-push blocking.");
2331
+ }
2332
+ async function installClaudeHooks(opts) {
2333
+ const root = findProjectRoot6(opts.dir);
2334
+ const scope = opts.scope ?? "user";
2335
+ const settingsPath = opts.settings ?? defaultClaudeSettingsPath(scope, root);
2336
+ if (opts.uninstall) {
2337
+ const result = await uninstallClaudeHooksAtPath(settingsPath);
2338
+ ui.success(`Removed hAIve hooks from ${result.settingsPath}`);
2339
+ return;
2340
+ }
2341
+ try {
2342
+ const result = await installClaudeHooksAtPath(settingsPath);
2343
+ if (result.created) {
2344
+ ui.success(`Created ${result.settingsPath} with hAIve passive-capture hooks`);
2345
+ } else {
2346
+ ui.success(`Patched ${result.settingsPath} (existing user hooks preserved)`);
2347
+ }
2348
+ } catch (err) {
2349
+ ui.error(err instanceof Error ? err.message : String(err));
2350
+ process.exitCode = 1;
2351
+ return;
2352
+ }
2353
+ ui.info("PostToolUse hook: `haive observe` runs after every Edit/Write/Bash");
2354
+ ui.info(" (appends a JSON line to .ai/.cache/observations.jsonl)");
2355
+ ui.info("SessionEnd hook: `haive session end --auto --quiet` distills observations");
2356
+ ui.info(" into a session_recap memory at session close");
2357
+ ui.info("Restart Claude Code (or open a new conversation) for the hooks to take effect.");
2358
+ ui.info(`Run \`haive install-hooks claude --uninstall\` to remove.`);
2359
+ }
1922
2360
  function registerInstallHooks(program2) {
1923
- program2.command("install-hooks").description(
1924
- "Install git hooks so haive sync runs automatically after every pull or merge.\n\n Installs:\n post-merge / post-rewrite \u2014 runs haive sync after every pull/merge\n pre-push \u2014 runs haive precommit before every push (advisory)\n\n Installed automatically by haive init (autopilot mode).\n Use --force to overwrite existing hooks.\n"
1925
- ).option("-d, --dir <dir>", "project root").option("--force", "overwrite existing hooks").action(async (opts) => {
1926
- const root = findProjectRoot6(opts.dir);
1927
- const gitDir = path8.join(root, ".git");
1928
- if (!existsSync7(gitDir)) {
1929
- ui.error(`No .git directory at ${root}.`);
2361
+ program2.command("install-hooks [target]").description(
2362
+ "Install hAIve hooks. Targets:\n\n git (default) post-merge / post-rewrite / pre-push for haive sync + precommit\n claude PostToolUse + SessionEnd hooks in ~/.claude/settings.json\n for passive observation capture (Claude Code only)\n\n Examples:\n haive install-hooks # git hooks (legacy default)\n haive install-hooks git\n haive install-hooks claude\n haive install-hooks claude --scope project\n haive install-hooks claude --uninstall\n"
2363
+ ).option("-d, --dir <dir>", "project root").option("--force", "overwrite existing hooks (git target only)").option("--scope <scope>", "claude target: 'user' (~/.claude) or 'project' (.claude/)", "user").option("--uninstall", "remove previously installed hAIve hooks (claude target only)").option("--settings <path>", "explicit path to settings.json (claude target only)").action(async (target, opts) => {
2364
+ const t = (target ?? "git").toLowerCase();
2365
+ if (t === "git") {
2366
+ await installGitHooks(opts);
2367
+ } else if (t === "claude") {
2368
+ await installClaudeHooks(opts);
2369
+ } else {
2370
+ ui.error(`Unknown target: ${target}. Available: git, claude`);
1930
2371
  process.exitCode = 1;
1931
- return;
1932
2372
  }
1933
- const hooksDir = path8.join(gitDir, "hooks");
1934
- await mkdir4(hooksDir, { recursive: true });
1935
- let installed = 0;
1936
- let skipped = 0;
1937
- for (const { name, body } of HOOKS) {
1938
- const file = path8.join(hooksDir, name);
1939
- if (existsSync7(file) && !opts.force) {
1940
- const existing = await readFile5(file, "utf8");
1941
- if (!existing.includes(HOOK_MARKER)) {
1942
- ui.warn(`${name} already exists and was not written by hAIve. Re-run with --force to overwrite.`);
1943
- skipped++;
1944
- continue;
1945
- }
2373
+ });
2374
+ }
2375
+
2376
+ // src/commands/observe.ts
2377
+ import { appendFile, mkdir as mkdir6 } from "fs/promises";
2378
+ import { existsSync as existsSync9 } from "fs";
2379
+ import path10 from "path";
2380
+ import "commander";
2381
+ import { findProjectRoot as findProjectRoot7, resolveHaivePaths as resolveHaivePaths5 } from "@hiveai/core";
2382
+ var MAX_STDIN_BYTES = 256 * 1024;
2383
+ var TRUNCATE_FIELD = 800;
2384
+ function truncate(s, max = TRUNCATE_FIELD) {
2385
+ if (s == null) return "";
2386
+ const str = typeof s === "string" ? s : JSON.stringify(s);
2387
+ return str.length > max ? str.slice(0, max) + "\u2026" : str;
2388
+ }
2389
+ function extractFiles(payload) {
2390
+ const files = /* @__PURE__ */ new Set();
2391
+ const input = payload.tool_input ?? {};
2392
+ for (const k of ["file_path", "path", "notebook_path"]) {
2393
+ const v = input[k];
2394
+ if (typeof v === "string") files.add(v);
2395
+ }
2396
+ const cmd = input["command"];
2397
+ if (typeof cmd === "string") {
2398
+ const matches = cmd.match(/[\w./-]+\.(?:ts|tsx|js|jsx|py|go|rs|java|kt|swift|rb|php|cs|cpp|c|h|md|json|yml|yaml)\b/g);
2399
+ if (matches) for (const m of matches) files.add(m);
2400
+ }
2401
+ return [...files].slice(0, 8);
2402
+ }
2403
+ function buildSummary(payload) {
2404
+ const tool = payload.tool_name ?? "?";
2405
+ const input = payload.tool_input ?? {};
2406
+ if (tool === "Bash") return `Bash: ${truncate(input["command"], 200)}`;
2407
+ if (tool === "Edit") return `Edit ${truncate(input["file_path"], 200)}`;
2408
+ if (tool === "Write") return `Write ${truncate(input["file_path"], 200)}`;
2409
+ return `${tool}: ${truncate(input, 200)}`;
2410
+ }
2411
+ async function readStdin(maxBytes) {
2412
+ if (process.stdin.isTTY) return "";
2413
+ return await new Promise((resolve) => {
2414
+ const chunks = [];
2415
+ let total = 0;
2416
+ let done = false;
2417
+ const finish = () => {
2418
+ if (done) return;
2419
+ done = true;
2420
+ resolve(Buffer.concat(chunks).toString("utf8"));
2421
+ };
2422
+ process.stdin.on("data", (c) => {
2423
+ total += c.length;
2424
+ if (total > maxBytes) {
2425
+ process.stdin.destroy();
2426
+ finish();
2427
+ return;
2428
+ }
2429
+ chunks.push(c);
2430
+ });
2431
+ process.stdin.on("end", finish);
2432
+ process.stdin.on("error", finish);
2433
+ setTimeout(finish, 2e3);
2434
+ });
2435
+ }
2436
+ function registerObserve(program2) {
2437
+ program2.command("observe").description(
2438
+ "Passive-capture endpoint for Claude Code PostToolUse hooks.\n\n Reads a JSON payload on stdin and appends an observation record to\n .ai/.cache/observations.jsonl. Always exits 0; never blocks the agent.\n Wired up automatically by `haive install-hooks claude`."
2439
+ ).option("-d, --dir <dir>", "project root").action(async (opts) => {
2440
+ try {
2441
+ const raw = await readStdin(MAX_STDIN_BYTES);
2442
+ if (!raw.trim()) return;
2443
+ let payload;
2444
+ try {
2445
+ payload = JSON.parse(raw);
2446
+ } catch {
2447
+ return;
1946
2448
  }
1947
- await writeFile4(file, body, "utf8");
1948
- await chmod(file, 493);
1949
- installed++;
1950
- }
1951
- ui.success(`Installed ${installed} hook(s) in .git/hooks/${skipped ? `, skipped ${skipped}` : ""}`);
1952
- ui.info("post-merge: haive sync runs after every pull/merge.");
1953
- ui.info("pre-push: haive precommit runs before every push (advisory, never blocks).");
1954
- ui.info(" Set HAIVE_BLOCK=1 in your shell to make pre-push blocking.");
2449
+ const root = (() => {
2450
+ try {
2451
+ return findProjectRoot7(opts.dir ?? payload.cwd);
2452
+ } catch {
2453
+ return null;
2454
+ }
2455
+ })();
2456
+ if (!root) return;
2457
+ const paths = resolveHaivePaths5(root);
2458
+ if (!existsSync9(paths.haiveDir)) return;
2459
+ const observation = {
2460
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2461
+ session_id: payload.session_id,
2462
+ cwd: payload.cwd,
2463
+ tool: payload.tool_name ?? "?",
2464
+ summary: buildSummary(payload),
2465
+ files: extractFiles(payload)
2466
+ };
2467
+ const cacheDir = path10.join(paths.haiveDir, ".cache");
2468
+ await mkdir6(cacheDir, { recursive: true });
2469
+ await appendFile(
2470
+ path10.join(cacheDir, "observations.jsonl"),
2471
+ JSON.stringify(observation) + "\n",
2472
+ "utf8"
2473
+ );
2474
+ } catch {
2475
+ }
1955
2476
  });
1956
2477
  }
1957
2478
 
1958
2479
  // src/commands/mcp.ts
1959
2480
  import { spawn } from "child_process";
1960
- import { existsSync as existsSync8 } from "fs";
2481
+ import { existsSync as existsSync10 } from "fs";
1961
2482
  import { createRequire } from "module";
1962
- import path9 from "path";
2483
+ import path11 from "path";
1963
2484
  import { fileURLToPath } from "url";
1964
2485
  import "commander";
1965
- import { findProjectRoot as findProjectRoot7 } from "@hiveai/core";
2486
+ import { findProjectRoot as findProjectRoot8 } from "@hiveai/core";
1966
2487
  var require2 = createRequire(import.meta.url);
1967
2488
  function registerMcp(program2) {
1968
2489
  program2.command("mcp").description(
@@ -1979,7 +2500,7 @@ function registerMcp(program2) {
1979
2500
  VS Code: code --add-mcp '{"name":"haive","command":"haive-mcp",...}'
1980
2501
  `
1981
2502
  ).option("-d, --dir <dir>", "project root (defaults to nearest .ai/ or .git/)").action((opts) => {
1982
- const root = findProjectRoot7(opts.dir);
2503
+ const root = findProjectRoot8(opts.dir);
1983
2504
  const bin = locateMcpBin();
1984
2505
  if (!bin) {
1985
2506
  ui.error(
@@ -1997,36 +2518,36 @@ function registerMcp(program2) {
1997
2518
  function locateMcpBin() {
1998
2519
  try {
1999
2520
  const pkgPath = require2.resolve("@hiveai/mcp/package.json");
2000
- const pkgDir = path9.dirname(pkgPath);
2001
- const candidate = path9.join(pkgDir, "dist", "index.js");
2002
- if (existsSync8(candidate)) return candidate;
2521
+ const pkgDir = path11.dirname(pkgPath);
2522
+ const candidate = path11.join(pkgDir, "dist", "index.js");
2523
+ if (existsSync10(candidate)) return candidate;
2003
2524
  } catch {
2004
2525
  }
2005
- const here = path9.dirname(fileURLToPath(import.meta.url));
2006
- const sibling = path9.resolve(here, "..", "..", "..", "mcp", "dist", "index.js");
2007
- if (existsSync8(sibling)) return sibling;
2526
+ const here = path11.dirname(fileURLToPath(import.meta.url));
2527
+ const sibling = path11.resolve(here, "..", "..", "..", "mcp", "dist", "index.js");
2528
+ if (existsSync10(sibling)) return sibling;
2008
2529
  return null;
2009
2530
  }
2010
2531
 
2011
2532
  // src/commands/sync.ts
2012
2533
  import { spawnSync as spawnSync2 } from "child_process";
2013
- import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
2014
- import { existsSync as existsSync9 } from "fs";
2015
- import path10 from "path";
2534
+ import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7 } from "fs/promises";
2535
+ import { existsSync as existsSync11 } from "fs";
2536
+ import path12 from "path";
2016
2537
  import "commander";
2017
2538
  import {
2018
2539
  DEFAULT_AUTO_PROMOTE_RULE,
2019
2540
  buildFrontmatter as buildFrontmatter2,
2020
- findProjectRoot as findProjectRoot8,
2541
+ findProjectRoot as findProjectRoot9,
2021
2542
  getUsage,
2022
2543
  isAutoPromoteEligible,
2023
2544
  isDecaying,
2024
2545
  loadCodeMap as loadCodeMap2,
2025
2546
  loadConfig,
2026
2547
  loadMemoriesFromDir as loadMemoriesFromDir2,
2027
- loadUsageIndex,
2548
+ loadUsageIndex as loadUsageIndex2,
2028
2549
  pullCrossRepoSources,
2029
- resolveHaivePaths as resolveHaivePaths5,
2550
+ resolveHaivePaths as resolveHaivePaths6,
2030
2551
  resolveManifestFiles,
2031
2552
  serializeMemory as serializeMemory2,
2032
2553
  trackDependencies,
@@ -2045,9 +2566,9 @@ function registerSync(program2) {
2045
2566
  "--inject-bridge",
2046
2567
  "inject top validated memories into CLAUDE.md (or --bridge-file) between <!-- haive:memories-start/end --> markers"
2047
2568
  ).option("--bridge-file <path>", "bridge file to inject into (default: CLAUDE.md)").option("--bridge-max-memories <n>", "max memories to inject into bridge file", "5").option("--embed", "rebuild embeddings index after sync (requires @haive/embeddings)").option("--no-cross-repo", "skip cross-repo memory pull even if crossRepoSources is configured").option("--no-deps", "skip dependency version tracking").option("--no-contracts", "skip contract file diff checking").action(async (opts) => {
2048
- const root = findProjectRoot8(opts.dir);
2049
- const paths = resolveHaivePaths5(root);
2050
- if (!existsSync9(paths.memoriesDir)) {
2569
+ const root = findProjectRoot9(opts.dir);
2570
+ const paths = resolveHaivePaths6(root);
2571
+ if (!existsSync11(paths.memoriesDir)) {
2051
2572
  if (!opts.quiet) ui.warn(`No .ai/memories at ${root}. Run \`haive init\` first.`);
2052
2573
  process.exitCode = 1;
2053
2574
  return;
@@ -2067,7 +2588,7 @@ function registerSync(program2) {
2067
2588
  for (const { memory: memory2, filePath } of memories) {
2068
2589
  if (memory2.frontmatter.type === "session_recap") {
2069
2590
  if (memory2.frontmatter.status === "stale") {
2070
- await writeFile5(
2591
+ await writeFile6(
2071
2592
  filePath,
2072
2593
  serializeMemory2({
2073
2594
  frontmatter: {
@@ -2090,7 +2611,7 @@ function registerSync(program2) {
2090
2611
  const verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
2091
2612
  if (result.stale) {
2092
2613
  if (memory2.frontmatter.status !== "stale") {
2093
- await writeFile5(
2614
+ await writeFile6(
2094
2615
  filePath,
2095
2616
  serializeMemory2({
2096
2617
  frontmatter: {
@@ -2106,7 +2627,7 @@ function registerSync(program2) {
2106
2627
  staleMarked++;
2107
2628
  }
2108
2629
  } else if (memory2.frontmatter.status === "stale") {
2109
- await writeFile5(
2630
+ await writeFile6(
2110
2631
  filePath,
2111
2632
  serializeMemory2({
2112
2633
  frontmatter: {
@@ -2125,7 +2646,7 @@ function registerSync(program2) {
2125
2646
  }
2126
2647
  if (opts.promote !== false) {
2127
2648
  const memories = await loadMemoriesFromDir2(paths.memoriesDir);
2128
- const usage = await loadUsageIndex(paths);
2649
+ const usage = await loadUsageIndex2(paths);
2129
2650
  const nowMs = Date.now();
2130
2651
  for (const { memory: memory2, filePath } of memories) {
2131
2652
  const fm = memory2.frontmatter;
@@ -2134,7 +2655,7 @@ function registerSync(program2) {
2134
2655
  minReads: autoPromoteMinReads,
2135
2656
  maxRejections: DEFAULT_AUTO_PROMOTE_RULE.maxRejections
2136
2657
  })) {
2137
- await writeFile5(
2658
+ await writeFile6(
2138
2659
  filePath,
2139
2660
  serializeMemory2({ frontmatter: { ...fm, status: "validated" }, body: memory2.body }),
2140
2661
  "utf8"
@@ -2145,7 +2666,7 @@ function registerSync(program2) {
2145
2666
  if (autoApproveDelayHours !== null && fm.status === "proposed" && fm.scope === "team") {
2146
2667
  const ageHours = (nowMs - new Date(fm.created_at).getTime()) / (1e3 * 60 * 60);
2147
2668
  if (ageHours >= autoApproveDelayHours) {
2148
- await writeFile5(
2669
+ await writeFile6(
2149
2670
  filePath,
2150
2671
  serializeMemory2({
2151
2672
  frontmatter: {
@@ -2179,7 +2700,7 @@ function registerSync(program2) {
2179
2700
  );
2180
2701
  }
2181
2702
  if (opts.injectBridge) {
2182
- const bridgeFile = opts.bridgeFile ? path10.resolve(opts.bridgeFile) : path10.join(root, "CLAUDE.md");
2703
+ const bridgeFile = opts.bridgeFile ? path12.resolve(opts.bridgeFile) : path12.join(root, "CLAUDE.md");
2183
2704
  const maxInject = Math.max(1, Number(opts.bridgeMaxMemories ?? 5));
2184
2705
  await injectBridge(bridgeFile, paths.memoriesDir, maxInject, root, opts.quiet);
2185
2706
  }
@@ -2199,7 +2720,7 @@ function registerSync(program2) {
2199
2720
  }
2200
2721
  if (!opts.quiet) {
2201
2722
  const allForDecay = await loadMemoriesFromDir2(paths.memoriesDir);
2202
- const usageForDecay = await loadUsageIndex(paths);
2723
+ const usageForDecay = await loadUsageIndex2(paths);
2203
2724
  const decaying = allForDecay.filter(({ memory: memory2 }) => {
2204
2725
  const fm = memory2.frontmatter;
2205
2726
  if (fm.status === "rejected" || fm.status === "deprecated" || fm.status === "stale") return false;
@@ -2285,10 +2806,10 @@ Attends une **confirmation explicite** avant d'agir.
2285
2806
  paths: [result.file],
2286
2807
  topic: `dep-bump-${slugParts}`
2287
2808
  });
2288
- const teamDir = path10.join(paths.memoriesDir, "team");
2289
- await mkdir5(teamDir, { recursive: true });
2290
- await writeFile5(
2291
- path10.join(teamDir, `${fm.id}.md`),
2809
+ const teamDir = path12.join(paths.memoriesDir, "team");
2810
+ await mkdir7(teamDir, { recursive: true });
2811
+ await writeFile6(
2812
+ path12.join(teamDir, `${fm.id}.md`),
2292
2813
  serializeMemory2({ frontmatter: { ...fm, requires_human_approval: true }, body }),
2293
2814
  "utf8"
2294
2815
  );
@@ -2352,10 +2873,10 @@ Attends une **confirmation explicite** avant d'agir.
2352
2873
  paths: [diff.file],
2353
2874
  topic: `contract-breaking-${diff.contract}`
2354
2875
  });
2355
- const teamDir = path10.join(paths.memoriesDir, "team");
2356
- await mkdir5(teamDir, { recursive: true });
2357
- await writeFile5(
2358
- path10.join(teamDir, `${fm.id}.md`),
2876
+ const teamDir = path12.join(paths.memoriesDir, "team");
2877
+ await mkdir7(teamDir, { recursive: true });
2878
+ await writeFile6(
2879
+ path12.join(teamDir, `${fm.id}.md`),
2359
2880
  serializeMemory2({ frontmatter: { ...fm, requires_human_approval: true }, body }),
2360
2881
  "utf8"
2361
2882
  );
@@ -2416,7 +2937,7 @@ Attends une **confirmation explicite** avant d'agir.
2416
2937
  });
2417
2938
  }
2418
2939
  async function injectBridge(bridgeFile, memoriesDir, maxMemories, root, quiet) {
2419
- if (!existsSync9(memoriesDir)) return;
2940
+ if (!existsSync11(memoriesDir)) return;
2420
2941
  const all = await loadMemoriesFromDir2(memoriesDir);
2421
2942
  const top = all.filter(({ memory: memory2 }) => {
2422
2943
  const s = memory2.frontmatter.status;
@@ -2441,17 +2962,17 @@ ${m.memory.body.trim()}`;
2441
2962
  ` + block + `
2442
2963
 
2443
2964
  ${BRIDGE_END}`;
2444
- const fileExists = existsSync9(bridgeFile);
2445
- let existing = fileExists ? await readFile6(bridgeFile, "utf8") : "";
2965
+ const fileExists = existsSync11(bridgeFile);
2966
+ let existing = fileExists ? await readFile7(bridgeFile, "utf8") : "";
2446
2967
  existing = existing.replace(/\r\n/g, "\n");
2447
2968
  const startIdx = existing.indexOf(BRIDGE_START);
2448
2969
  const endIdx = existing.indexOf(BRIDGE_END);
2449
2970
  if (startIdx !== -1 && endIdx === -1) {
2450
- ui.warn(`${path10.relative(root, bridgeFile)}: found ${BRIDGE_START} without ${BRIDGE_END}. Fix the file manually before running --inject-bridge.`);
2971
+ ui.warn(`${path12.relative(root, bridgeFile)}: found ${BRIDGE_START} without ${BRIDGE_END}. Fix the file manually before running --inject-bridge.`);
2451
2972
  return;
2452
2973
  }
2453
2974
  if (startIdx === -1 && endIdx !== -1) {
2454
- ui.warn(`${path10.relative(root, bridgeFile)}: found ${BRIDGE_END} without ${BRIDGE_START}. Fix the file manually before running --inject-bridge.`);
2975
+ ui.warn(`${path12.relative(root, bridgeFile)}: found ${BRIDGE_END} without ${BRIDGE_START}. Fix the file manually before running --inject-bridge.`);
2455
2976
  return;
2456
2977
  }
2457
2978
  let updated;
@@ -2459,14 +2980,14 @@ ${BRIDGE_END}`;
2459
2980
  updated = existing.slice(0, startIdx) + injected + existing.slice(endIdx + BRIDGE_END.length);
2460
2981
  } else {
2461
2982
  if (!fileExists && !quiet) {
2462
- ui.info(`Creating ${path10.relative(root, bridgeFile)} with haive memory block.`);
2983
+ ui.info(`Creating ${path12.relative(root, bridgeFile)} with haive memory block.`);
2463
2984
  }
2464
2985
  updated = existing + (existing.endsWith("\n") ? "" : "\n") + "\n" + injected + "\n";
2465
2986
  }
2466
- await writeFile5(bridgeFile, updated, "utf8");
2987
+ await writeFile6(bridgeFile, updated, "utf8");
2467
2988
  if (!quiet) {
2468
2989
  console.log(
2469
- ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${path10.relative(root, bridgeFile)}`)
2990
+ ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${path12.relative(root, bridgeFile)}`)
2470
2991
  );
2471
2992
  }
2472
2993
  }
@@ -2491,17 +3012,17 @@ function collectSinceChanges(root, ref) {
2491
3012
 
2492
3013
  // src/commands/memory-add.ts
2493
3014
  import { createHash } from "crypto";
2494
- import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
2495
- import { existsSync as existsSync10 } from "fs";
2496
- import path11 from "path";
3015
+ import { mkdir as mkdir8, readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
3016
+ import { existsSync as existsSync12 } from "fs";
3017
+ import path13 from "path";
2497
3018
  import "commander";
2498
3019
  import {
2499
3020
  buildFrontmatter as buildFrontmatter3,
2500
- findProjectRoot as findProjectRoot9,
3021
+ findProjectRoot as findProjectRoot10,
2501
3022
  inferModulesFromPaths,
2502
3023
  loadMemoriesFromDir as loadMemoriesFromDir3,
2503
3024
  memoryFilePath as memoryFilePath2,
2504
- resolveHaivePaths as resolveHaivePaths6,
3025
+ resolveHaivePaths as resolveHaivePaths7,
2505
3026
  serializeMemory as serializeMemory3
2506
3027
  } from "@hiveai/core";
2507
3028
  function registerMemoryAdd(memory2) {
@@ -2529,9 +3050,9 @@ function registerMemoryAdd(memory2) {
2529
3050
  --scope team --body "Never modify existing migrations. Create V{n+1}__desc.sql."
2530
3051
  `
2531
3052
  ).requiredOption("--type <type>", "convention | decision | gotcha | architecture | glossary | attempt").requiredOption("--slug <slug>", "short kebab-case identifier used in the file name").option("--title <text>", "memory title \u2014 becomes the first heading of the body").option("--scope <scope>", "personal | team | module (default: personal, or team in autopilot)", "personal").option("--module <name>", "module name (required when scope=module)").option("--tags <csv>", "comma-separated tags for easier retrieval").option("--domain <domain>", "domain (e.g. transactions)").option("--author <author>", "author email or handle").option("--paths <csv>", "anchor to source files \u2014 used for staleness detection by haive sync").option("--symbols <csv>", "anchor to specific symbols (class/function names)").option("--commit <sha>", "anchor to a specific commit SHA").option("--body <text>", "memory body content (Markdown) \u2014 overrides --title default body").option("--body-file <path>", "read memory body from a Markdown file \u2014 for long content").option("--no-auto-tag", "disable automatic tag suggestions inferred from anchor paths").option("--topic <key>", "stable key for upsert: if a memory with this topic+scope already exists, update it in-place (revision_count++)").option("-d, --dir <dir>", "project root").action(async (opts) => {
2532
- const root = findProjectRoot9(opts.dir);
2533
- const paths = resolveHaivePaths6(root);
2534
- if (!existsSync10(paths.haiveDir)) {
3053
+ const root = findProjectRoot10(opts.dir);
3054
+ const paths = resolveHaivePaths7(root);
3055
+ if (!existsSync12(paths.haiveDir)) {
2535
3056
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
2536
3057
  process.exitCode = 1;
2537
3058
  return;
@@ -2542,7 +3063,7 @@ function registerMemoryAdd(memory2) {
2542
3063
  const inferredTags = autoTagsEnabled ? inferModulesFromPaths(anchorPaths) : [];
2543
3064
  const mergedTags = Array.from(/* @__PURE__ */ new Set([...userTags, ...inferredTags]));
2544
3065
  if (anchorPaths.length > 0) {
2545
- const missing = anchorPaths.filter((p) => !existsSync10(path11.resolve(root, p)));
3066
+ const missing = anchorPaths.filter((p) => !existsSync12(path13.resolve(root, p)));
2546
3067
  if (missing.length > 0) {
2547
3068
  ui.warn(`Anchor path${missing.length > 1 ? "s" : ""} not found in project:`);
2548
3069
  for (const p of missing) ui.warn(` \u2717 ${p}`);
@@ -2554,12 +3075,12 @@ function registerMemoryAdd(memory2) {
2554
3075
  const title = opts.title ?? opts.slug;
2555
3076
  let body;
2556
3077
  if (opts.bodyFile !== void 0) {
2557
- if (!existsSync10(opts.bodyFile)) {
3078
+ if (!existsSync12(opts.bodyFile)) {
2558
3079
  ui.error(`--body-file not found: ${opts.bodyFile}`);
2559
3080
  process.exitCode = 1;
2560
3081
  return;
2561
3082
  }
2562
- const fileContent = await readFile7(opts.bodyFile, "utf8");
3083
+ const fileContent = await readFile8(opts.bodyFile, "utf8");
2563
3084
  body = opts.title ? `# ${opts.title}
2564
3085
 
2565
3086
  ${fileContent.trim()}
@@ -2575,7 +3096,7 @@ TODO \u2014 write the memory body.
2575
3096
  `;
2576
3097
  }
2577
3098
  const scope = opts.scope ?? "personal";
2578
- if (existsSync10(paths.memoriesDir)) {
3099
+ if (existsSync12(paths.memoriesDir)) {
2579
3100
  const incomingHash = createHash("sha256").update(body.trim()).digest("hex").slice(0, 12);
2580
3101
  const allForHash = await loadMemoriesFromDir3(paths.memoriesDir);
2581
3102
  const hashDup = allForHash.find(
@@ -2588,7 +3109,7 @@ TODO \u2014 write the memory body.
2588
3109
  return;
2589
3110
  }
2590
3111
  }
2591
- if (opts.topic && existsSync10(paths.memoriesDir)) {
3112
+ if (opts.topic && existsSync12(paths.memoriesDir)) {
2592
3113
  const existing = await loadMemoriesFromDir3(paths.memoriesDir);
2593
3114
  const topicMatch = existing.find(
2594
3115
  ({ memory: memory3 }) => memory3.frontmatter.topic === opts.topic && memory3.frontmatter.scope === scope && (!opts.module || memory3.frontmatter.module === opts.module)
@@ -2606,8 +3127,8 @@ TODO \u2014 write the memory body.
2606
3127
  symbols: parseCsv2(opts.symbols).length ? parseCsv2(opts.symbols) : fm.anchor.symbols
2607
3128
  }
2608
3129
  };
2609
- await writeFile6(topicMatch.filePath, serializeMemory3({ frontmatter: newFrontmatter, body }), "utf8");
2610
- ui.success(`Updated (topic upsert) ${path11.relative(root, topicMatch.filePath)}`);
3130
+ await writeFile7(topicMatch.filePath, serializeMemory3({ frontmatter: newFrontmatter, body }), "utf8");
3131
+ ui.success(`Updated (topic upsert) ${path13.relative(root, topicMatch.filePath)}`);
2611
3132
  ui.info(`id=${fm.id} revision=${revisionCount}`);
2612
3133
  return;
2613
3134
  }
@@ -2626,13 +3147,13 @@ TODO \u2014 write the memory body.
2626
3147
  topic: opts.topic
2627
3148
  });
2628
3149
  const file = memoryFilePath2(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
2629
- await mkdir6(path11.dirname(file), { recursive: true });
2630
- if (existsSync10(file)) {
3150
+ await mkdir8(path13.dirname(file), { recursive: true });
3151
+ if (existsSync12(file)) {
2631
3152
  ui.error(`Memory already exists at ${file}`);
2632
3153
  process.exitCode = 1;
2633
3154
  return;
2634
3155
  }
2635
- if (existsSync10(paths.memoriesDir)) {
3156
+ if (existsSync12(paths.memoriesDir)) {
2636
3157
  const existing = await loadMemoriesFromDir3(paths.memoriesDir);
2637
3158
  const slugTokens = opts.slug.toLowerCase().split(/[-_\s]+/).filter(Boolean);
2638
3159
  const similar = existing.filter(({ memory: memory3 }) => {
@@ -2644,8 +3165,8 @@ TODO \u2014 write the memory body.
2644
3165
  ui.warn("Consider updating one of these with `haive memory update` instead.");
2645
3166
  }
2646
3167
  }
2647
- await writeFile6(file, serializeMemory3({ frontmatter, body }), "utf8");
2648
- ui.success(`Created ${path11.relative(root, file)}`);
3168
+ await writeFile7(file, serializeMemory3({ frontmatter, body }), "utf8");
3169
+ ui.success(`Created ${path13.relative(root, file)}`);
2649
3170
  ui.info(`id=${frontmatter.id} scope=${frontmatter.scope} status=${frontmatter.status}`);
2650
3171
  if (inferredTags.length > 0) {
2651
3172
  ui.info(`auto-tagged: ${inferredTags.join(", ")} (use --no-auto-tag to disable)`);
@@ -2675,10 +3196,10 @@ function parseCsv2(value) {
2675
3196
  }
2676
3197
 
2677
3198
  // src/commands/memory-list.ts
2678
- import { existsSync as existsSync11 } from "fs";
2679
- import path12 from "path";
3199
+ import { existsSync as existsSync13 } from "fs";
3200
+ import path14 from "path";
2680
3201
  import "commander";
2681
- import { findProjectRoot as findProjectRoot10, resolveHaivePaths as resolveHaivePaths7 } from "@hiveai/core";
3202
+ import { findProjectRoot as findProjectRoot11, resolveHaivePaths as resolveHaivePaths8 } from "@hiveai/core";
2682
3203
 
2683
3204
  // src/utils/fs.ts
2684
3205
  import {
@@ -2690,9 +3211,9 @@ import {
2690
3211
  // src/commands/memory-list.ts
2691
3212
  function registerMemoryList(memory2) {
2692
3213
  memory2.command("list").description("List memories with optional filters").option("--scope <scope>", "personal | team | module").option("--type <type>", "filter by type").option("--tag <tag>", "filter by tag").option("--module <name>", "filter by module name").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected,deprecated)").option("--show-rejected", "include rejected memories (hidden by default)").option("-d, --dir <dir>", "project root").action(async (opts) => {
2693
- const root = findProjectRoot10(opts.dir);
2694
- const paths = resolveHaivePaths7(root);
2695
- if (!existsSync11(paths.memoriesDir)) {
3214
+ const root = findProjectRoot11(opts.dir);
3215
+ const paths = resolveHaivePaths8(root);
3216
+ if (!existsSync13(paths.memoriesDir)) {
2696
3217
  ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
2697
3218
  process.exitCode = 1;
2698
3219
  return;
@@ -2724,7 +3245,7 @@ function registerMemoryList(memory2) {
2724
3245
  console.log(
2725
3246
  `${ui.bold(fm.id)} ${ui.dim(fm.scope)}/${ui.dim(fm.type)} ${statusBadge}${moduleStr}${tagStr}`
2726
3247
  );
2727
- console.log(` ${ui.dim(path12.relative(root, filePath))}`);
3248
+ console.log(` ${ui.dim(path14.relative(root, filePath))}`);
2728
3249
  }
2729
3250
  console.log(ui.dim(`
2730
3251
  ${filtered.length} memor${filtered.length === 1 ? "y" : "ies"}`));
@@ -2759,21 +3280,21 @@ function matchesFilters(loaded, opts) {
2759
3280
  }
2760
3281
 
2761
3282
  // src/commands/memory-promote.ts
2762
- import { mkdir as mkdir7, unlink, writeFile as writeFile7 } from "fs/promises";
2763
- import { existsSync as existsSync12 } from "fs";
2764
- import path13 from "path";
3283
+ import { mkdir as mkdir9, unlink, writeFile as writeFile8 } from "fs/promises";
3284
+ import { existsSync as existsSync14 } from "fs";
3285
+ import path15 from "path";
2765
3286
  import "commander";
2766
3287
  import {
2767
- findProjectRoot as findProjectRoot11,
3288
+ findProjectRoot as findProjectRoot12,
2768
3289
  memoryFilePath as memoryFilePath3,
2769
- resolveHaivePaths as resolveHaivePaths8,
3290
+ resolveHaivePaths as resolveHaivePaths9,
2770
3291
  serializeMemory as serializeMemory4
2771
3292
  } from "@hiveai/core";
2772
3293
  function registerMemoryPromote(memory2) {
2773
3294
  memory2.command("promote <id>").description("Promote a personal memory to team scope (status -> proposed)").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
2774
- const root = findProjectRoot11(opts.dir);
2775
- const paths = resolveHaivePaths8(root);
2776
- if (!existsSync12(paths.memoriesDir)) {
3295
+ const root = findProjectRoot12(opts.dir);
3296
+ const paths = resolveHaivePaths9(root);
3297
+ if (!existsSync14(paths.memoriesDir)) {
2777
3298
  ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
2778
3299
  process.exitCode = 1;
2779
3300
  return;
@@ -2808,30 +3329,30 @@ function registerMemoryPromote(memory2) {
2808
3329
  body: found.memory.body
2809
3330
  };
2810
3331
  const newPath = memoryFilePath3(paths, "team", updated.frontmatter.id);
2811
- await mkdir7(path13.dirname(newPath), { recursive: true });
2812
- await writeFile7(newPath, serializeMemory4(updated), "utf8");
3332
+ await mkdir9(path15.dirname(newPath), { recursive: true });
3333
+ await writeFile8(newPath, serializeMemory4(updated), "utf8");
2813
3334
  await unlink(found.filePath);
2814
3335
  ui.success(`Promoted ${id} to team scope (status=proposed)`);
2815
- ui.info(`Now at ${path13.relative(root, newPath)}`);
3336
+ ui.info(`Now at ${path15.relative(root, newPath)}`);
2816
3337
  console.log(ui.dim(`\u2192 next: haive memory approve ${id} (validate for team use)`));
2817
3338
  });
2818
3339
  }
2819
3340
 
2820
3341
  // src/commands/memory-approve.ts
2821
- import { existsSync as existsSync13 } from "fs";
2822
- import { writeFile as writeFile8 } from "fs/promises";
2823
- import path14 from "path";
3342
+ import { existsSync as existsSync15 } from "fs";
3343
+ import { writeFile as writeFile9 } from "fs/promises";
3344
+ import path16 from "path";
2824
3345
  import "commander";
2825
3346
  import {
2826
- findProjectRoot as findProjectRoot12,
2827
- resolveHaivePaths as resolveHaivePaths9,
3347
+ findProjectRoot as findProjectRoot13,
3348
+ resolveHaivePaths as resolveHaivePaths10,
2828
3349
  serializeMemory as serializeMemory5
2829
3350
  } from "@hiveai/core";
2830
3351
  function registerMemoryApprove(memory2) {
2831
3352
  memory2.command("approve [id]").description("Mark a memory as 'validated'. Use --all to bulk-approve all proposed/draft memories.").option("--all", "approve all proposed and draft memories at once").option("--pending", "approve all memories with status 'proposed'").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
2832
- const root = findProjectRoot12(opts.dir);
2833
- const paths = resolveHaivePaths9(root);
2834
- if (!existsSync13(paths.memoriesDir)) {
3353
+ const root = findProjectRoot13(opts.dir);
3354
+ const paths = resolveHaivePaths10(root);
3355
+ if (!existsSync15(paths.memoriesDir)) {
2835
3356
  ui.error(`No .ai/memories at ${root}.`);
2836
3357
  process.exitCode = 1;
2837
3358
  return;
@@ -2853,7 +3374,7 @@ function registerMemoryApprove(memory2) {
2853
3374
  frontmatter: { ...found2.memory.frontmatter, status: "validated" },
2854
3375
  body: found2.memory.body
2855
3376
  };
2856
- await writeFile8(found2.filePath, serializeMemory5(next2), "utf8");
3377
+ await writeFile9(found2.filePath, serializeMemory5(next2), "utf8");
2857
3378
  count++;
2858
3379
  }
2859
3380
  ui.success(`Approved ${count} memor${count === 1 ? "y" : "ies"} (status=validated)`);
@@ -2882,27 +3403,27 @@ function registerMemoryApprove(memory2) {
2882
3403
  frontmatter: { ...found.memory.frontmatter, status: "validated" },
2883
3404
  body: found.memory.body
2884
3405
  };
2885
- await writeFile8(found.filePath, serializeMemory5(next), "utf8");
3406
+ await writeFile9(found.filePath, serializeMemory5(next), "utf8");
2886
3407
  ui.success(`Approved ${id} (status=validated)`);
2887
- ui.info(path14.relative(root, found.filePath));
3408
+ ui.info(path16.relative(root, found.filePath));
2888
3409
  });
2889
3410
  }
2890
3411
 
2891
3412
  // src/commands/memory-update.ts
2892
- import { writeFile as writeFile9 } from "fs/promises";
2893
- import { existsSync as existsSync14 } from "fs";
2894
- import path15 from "path";
3413
+ import { writeFile as writeFile10 } from "fs/promises";
3414
+ import { existsSync as existsSync16 } from "fs";
3415
+ import path17 from "path";
2895
3416
  import "commander";
2896
3417
  import {
2897
- findProjectRoot as findProjectRoot13,
2898
- resolveHaivePaths as resolveHaivePaths10,
3418
+ findProjectRoot as findProjectRoot14,
3419
+ resolveHaivePaths as resolveHaivePaths11,
2899
3420
  serializeMemory as serializeMemory6
2900
3421
  } from "@hiveai/core";
2901
3422
  function registerMemoryUpdate(memory2) {
2902
3423
  memory2.command("update <id>").description("Update body, tags, or anchor of an existing memory (preserves id and usage history)").option("--title <text>", "new title \u2014 replaces the first heading of the body").option("--body <text>", "new Markdown body \u2014 replaces the existing body").option("--tags <csv>", "new tags, comma-separated \u2014 fully replaces existing tags").option("--paths <csv>", "new anchor paths, comma-separated").option("--symbols <csv>", "new anchor symbols, comma-separated").option("--commit <sha>", "new anchor commit SHA").option("--domain <domain>", "new domain label").option("--author <author>", "new author handle or email").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
2903
- const root = findProjectRoot13(opts.dir);
2904
- const paths = resolveHaivePaths10(root);
2905
- if (!existsSync14(paths.memoriesDir)) {
3424
+ const root = findProjectRoot14(opts.dir);
3425
+ const paths = resolveHaivePaths11(root);
3426
+ if (!existsSync16(paths.memoriesDir)) {
2906
3427
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
2907
3428
  process.exitCode = 1;
2908
3429
  return;
@@ -2949,12 +3470,12 @@ function registerMemoryUpdate(memory2) {
2949
3470
  ui.warn("Nothing to update \u2014 provide at least one option.");
2950
3471
  return;
2951
3472
  }
2952
- await writeFile9(
3473
+ await writeFile10(
2953
3474
  loaded.filePath,
2954
3475
  serializeMemory6({ frontmatter: newFrontmatter, body: newBody }),
2955
3476
  "utf8"
2956
3477
  );
2957
- ui.success(`Updated ${path15.relative(root, loaded.filePath)}`);
3478
+ ui.success(`Updated ${path17.relative(root, loaded.filePath)}`);
2958
3479
  ui.info(`fields: ${updated.join(", ")}`);
2959
3480
  });
2960
3481
  }
@@ -2973,17 +3494,17 @@ function parseCsv3(value) {
2973
3494
  }
2974
3495
 
2975
3496
  // src/commands/memory-auto-promote.ts
2976
- import { writeFile as writeFile10 } from "fs/promises";
2977
- import { existsSync as existsSync15 } from "fs";
2978
- import path16 from "path";
3497
+ import { writeFile as writeFile11 } from "fs/promises";
3498
+ import { existsSync as existsSync17 } from "fs";
3499
+ import path18 from "path";
2979
3500
  import "commander";
2980
3501
  import {
2981
3502
  DEFAULT_AUTO_PROMOTE_RULE as DEFAULT_AUTO_PROMOTE_RULE2,
2982
- findProjectRoot as findProjectRoot14,
3503
+ findProjectRoot as findProjectRoot15,
2983
3504
  getUsage as getUsage2,
2984
3505
  isAutoPromoteEligible as isAutoPromoteEligible2,
2985
- loadUsageIndex as loadUsageIndex2,
2986
- resolveHaivePaths as resolveHaivePaths11,
3506
+ loadUsageIndex as loadUsageIndex3,
3507
+ resolveHaivePaths as resolveHaivePaths12,
2987
3508
  serializeMemory as serializeMemory7
2988
3509
  } from "@hiveai/core";
2989
3510
  function registerMemoryAutoPromote(memory2) {
@@ -2992,9 +3513,9 @@ function registerMemoryAutoPromote(memory2) {
2992
3513
  "memories with more rejections than this are skipped",
2993
3514
  String(DEFAULT_AUTO_PROMOTE_RULE2.maxRejections)
2994
3515
  ).option("--apply", "actually write status=validated to disk (default: dry-run)").option("-d, --dir <dir>", "project root").action(async (opts) => {
2995
- const root = findProjectRoot14(opts.dir);
2996
- const paths = resolveHaivePaths11(root);
2997
- if (!existsSync15(paths.memoriesDir)) {
3516
+ const root = findProjectRoot15(opts.dir);
3517
+ const paths = resolveHaivePaths12(root);
3518
+ if (!existsSync17(paths.memoriesDir)) {
2998
3519
  ui.error(`No .ai/memories at ${root}.`);
2999
3520
  process.exitCode = 1;
3000
3521
  return;
@@ -3004,7 +3525,7 @@ function registerMemoryAutoPromote(memory2) {
3004
3525
  maxRejections: Number(opts.maxRejections ?? DEFAULT_AUTO_PROMOTE_RULE2.maxRejections)
3005
3526
  };
3006
3527
  const memories = await loadMemoriesFromDir4(paths.memoriesDir);
3007
- const usage = await loadUsageIndex2(paths);
3528
+ const usage = await loadUsageIndex3(paths);
3008
3529
  const eligible = memories.filter(
3009
3530
  ({ memory: memory3 }) => isAutoPromoteEligible2(memory3.frontmatter, getUsage2(usage, memory3.frontmatter.id), rule)
3010
3531
  );
@@ -3020,13 +3541,13 @@ function registerMemoryAutoPromote(memory2) {
3020
3541
  console.log(
3021
3542
  `${ui.bold(opts.apply ? "PROMOTE" : "would promote")} ${mem.frontmatter.id} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
3022
3543
  );
3023
- console.log(` ${ui.dim(path16.relative(root, filePath))}`);
3544
+ console.log(` ${ui.dim(path18.relative(root, filePath))}`);
3024
3545
  if (opts.apply) {
3025
3546
  const next = {
3026
3547
  frontmatter: { ...mem.frontmatter, status: "validated" },
3027
3548
  body: mem.body
3028
3549
  };
3029
- await writeFile10(filePath, serializeMemory7(next), "utf8");
3550
+ await writeFile11(filePath, serializeMemory7(next), "utf8");
3030
3551
  written++;
3031
3552
  }
3032
3553
  }
@@ -3037,20 +3558,20 @@ function registerMemoryAutoPromote(memory2) {
3037
3558
 
3038
3559
  // src/commands/memory-edit.ts
3039
3560
  import { spawn as spawn2 } from "child_process";
3040
- import { existsSync as existsSync16 } from "fs";
3041
- import { readFile as readFile8 } from "fs/promises";
3042
- import path17 from "path";
3561
+ import { existsSync as existsSync18 } from "fs";
3562
+ import { readFile as readFile9 } from "fs/promises";
3563
+ import path19 from "path";
3043
3564
  import "commander";
3044
3565
  import {
3045
- findProjectRoot as findProjectRoot15,
3566
+ findProjectRoot as findProjectRoot16,
3046
3567
  parseMemory,
3047
- resolveHaivePaths as resolveHaivePaths12
3568
+ resolveHaivePaths as resolveHaivePaths13
3048
3569
  } from "@hiveai/core";
3049
3570
  function registerMemoryEdit(memory2) {
3050
3571
  memory2.command("edit <id>").description("Open a memory in $EDITOR and re-validate when you save").option("-e, --editor <cmd>", "editor command (defaults to $EDITOR or 'vi')").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
3051
- const root = findProjectRoot15(opts.dir);
3052
- const paths = resolveHaivePaths12(root);
3053
- if (!existsSync16(paths.memoriesDir)) {
3572
+ const root = findProjectRoot16(opts.dir);
3573
+ const paths = resolveHaivePaths13(root);
3574
+ if (!existsSync18(paths.memoriesDir)) {
3054
3575
  ui.error(`No .ai/memories at ${root}.`);
3055
3576
  process.exitCode = 1;
3056
3577
  return;
@@ -3063,13 +3584,13 @@ function registerMemoryEdit(memory2) {
3063
3584
  return;
3064
3585
  }
3065
3586
  const editor = opts.editor ?? process.env.EDITOR ?? process.env.VISUAL ?? "vi";
3066
- ui.info(`Opening ${path17.relative(root, found.filePath)} with ${editor}\u2026`);
3587
+ ui.info(`Opening ${path19.relative(root, found.filePath)} with ${editor}\u2026`);
3067
3588
  const code = await runEditor(editor, found.filePath);
3068
3589
  if (code !== 0) {
3069
3590
  ui.warn(`Editor exited with status ${code}.`);
3070
3591
  }
3071
3592
  try {
3072
- const fresh = await readFile8(found.filePath, "utf8");
3593
+ const fresh = await readFile9(found.filePath, "utf8");
3073
3594
  parseMemory(fresh);
3074
3595
  ui.success("Memory still parses cleanly.");
3075
3596
  } catch (err) {
@@ -3090,29 +3611,29 @@ function runEditor(editor, file) {
3090
3611
  }
3091
3612
 
3092
3613
  // src/commands/memory-for-files.ts
3093
- import { existsSync as existsSync17 } from "fs";
3094
- import path18 from "path";
3095
- import "commander";
3614
+ import { existsSync as existsSync19 } from "fs";
3615
+ import path20 from "path";
3616
+ import "commander";
3096
3617
  import {
3097
3618
  deriveConfidence,
3098
- findProjectRoot as findProjectRoot16,
3619
+ findProjectRoot as findProjectRoot17,
3099
3620
  getUsage as getUsage3,
3100
3621
  inferModulesFromPaths as inferModulesFromPaths2,
3101
- loadUsageIndex as loadUsageIndex3,
3622
+ loadUsageIndex as loadUsageIndex4,
3102
3623
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
3103
- resolveHaivePaths as resolveHaivePaths13
3624
+ resolveHaivePaths as resolveHaivePaths14
3104
3625
  } from "@hiveai/core";
3105
3626
  function registerMemoryForFiles(memory2) {
3106
3627
  memory2.command("for-files <files...>").description("Show memories relevant to the given files (anchor overlap, module, domain)").option("-d, --dir <dir>", "project root").action(async (files, opts) => {
3107
- const root = findProjectRoot16(opts.dir);
3108
- const paths = resolveHaivePaths13(root);
3109
- if (!existsSync17(paths.memoriesDir)) {
3628
+ const root = findProjectRoot17(opts.dir);
3629
+ const paths = resolveHaivePaths14(root);
3630
+ if (!existsSync19(paths.memoriesDir)) {
3110
3631
  ui.error(`No .ai/memories at ${root}.`);
3111
3632
  process.exitCode = 1;
3112
3633
  return;
3113
3634
  }
3114
3635
  const all = await loadMemoriesFromDir4(paths.memoriesDir);
3115
- const usage = await loadUsageIndex3(paths);
3636
+ const usage = await loadUsageIndex4(paths);
3116
3637
  const inferred = inferModulesFromPaths2(files);
3117
3638
  const byAnchor = [];
3118
3639
  const byModule = [];
@@ -3213,32 +3734,32 @@ function printGroup(root, label, loaded, usage) {
3213
3734
  const u = getUsage3(usage, fm.id);
3214
3735
  const conf = deriveConfidence(fm, u);
3215
3736
  console.log(`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(conf)}`);
3216
- console.log(` ${ui.dim(path18.relative(root, filePath))}`);
3737
+ console.log(` ${ui.dim(path20.relative(root, filePath))}`);
3217
3738
  }
3218
3739
  }
3219
3740
 
3220
3741
  // src/commands/memory-hot.ts
3221
- import { existsSync as existsSync18 } from "fs";
3222
- import path19 from "path";
3742
+ import { existsSync as existsSync20 } from "fs";
3743
+ import path21 from "path";
3223
3744
  import "commander";
3224
3745
  import {
3225
- findProjectRoot as findProjectRoot17,
3746
+ findProjectRoot as findProjectRoot18,
3226
3747
  getUsage as getUsage4,
3227
- loadUsageIndex as loadUsageIndex4,
3228
- resolveHaivePaths as resolveHaivePaths14
3748
+ loadUsageIndex as loadUsageIndex5,
3749
+ resolveHaivePaths as resolveHaivePaths15
3229
3750
  } from "@hiveai/core";
3230
3751
  function registerMemoryHot(memory2) {
3231
3752
  memory2.command("hot").description("List memories actively used but not yet validated (good promotion candidates)").option("--threshold <n>", "minimum read_count to qualify", "3").option("--status <status>", "limit to one status (default: draft + proposed)").option("-d, --dir <dir>", "project root").action(async (opts) => {
3232
- const root = findProjectRoot17(opts.dir);
3233
- const paths = resolveHaivePaths14(root);
3234
- if (!existsSync18(paths.memoriesDir)) {
3753
+ const root = findProjectRoot18(opts.dir);
3754
+ const paths = resolveHaivePaths15(root);
3755
+ if (!existsSync20(paths.memoriesDir)) {
3235
3756
  ui.error(`No .ai/memories at ${root}.`);
3236
3757
  process.exitCode = 1;
3237
3758
  return;
3238
3759
  }
3239
3760
  const threshold = Math.max(1, Number(opts.threshold ?? 3));
3240
3761
  const all = await loadMemoriesFromDir4(paths.memoriesDir);
3241
- const usage = await loadUsageIndex4(paths);
3762
+ const usage = await loadUsageIndex5(paths);
3242
3763
  const candidates = all.filter(({ memory: mem }) => {
3243
3764
  const fm = mem.frontmatter;
3244
3765
  if (opts.status && fm.status !== opts.status) return false;
@@ -3259,7 +3780,7 @@ function registerMemoryHot(memory2) {
3259
3780
  console.log(
3260
3781
  `${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(fm.status)} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
3261
3782
  );
3262
- console.log(` ${ui.dim(path19.relative(root, filePath))}`);
3783
+ console.log(` ${ui.dim(path21.relative(root, filePath))}`);
3263
3784
  }
3264
3785
  ui.info(
3265
3786
  `${candidates.length} hot \u2014 promote drafts with \`haive memory promote <id>\`, then \`haive memory auto-promote --apply\`.`
@@ -3268,15 +3789,15 @@ function registerMemoryHot(memory2) {
3268
3789
  }
3269
3790
 
3270
3791
  // src/commands/memory-tried.ts
3271
- import { mkdir as mkdir8, writeFile as writeFile11 } from "fs/promises";
3272
- import { existsSync as existsSync19 } from "fs";
3273
- import path20 from "path";
3792
+ import { mkdir as mkdir10, writeFile as writeFile12 } from "fs/promises";
3793
+ import { existsSync as existsSync21 } from "fs";
3794
+ import path22 from "path";
3274
3795
  import "commander";
3275
3796
  import {
3276
3797
  buildFrontmatter as buildFrontmatter4,
3277
- findProjectRoot as findProjectRoot18,
3798
+ findProjectRoot as findProjectRoot19,
3278
3799
  memoryFilePath as memoryFilePath4,
3279
- resolveHaivePaths as resolveHaivePaths15,
3800
+ resolveHaivePaths as resolveHaivePaths16,
3280
3801
  serializeMemory as serializeMemory8
3281
3802
  } from "@hiveai/core";
3282
3803
  function registerMemoryTried(memory2) {
@@ -3296,9 +3817,9 @@ function registerMemoryTried(memory2) {
3296
3817
  --paths packages/cli/src/index.ts
3297
3818
  `
3298
3819
  ).requiredOption("--what <text>", "what approach was tried (short, descriptive title)").requiredOption("--why-failed <text>", "why it failed or should NOT be used (include the exact error if possible)").option("--instead <text>", "the correct approach to use instead").option("--scope <scope>", "personal | team | module (default: personal)", "personal").option("--module <name>", "module name (required when scope=module)").option("--tags <csv>", "comma-separated tags").option("--paths <csv>", "anchor paths, comma-separated").option("--author <author>", "author email or handle").option("-d, --dir <dir>", "project root").action(async (opts) => {
3299
- const root = findProjectRoot18(opts.dir);
3300
- const paths = resolveHaivePaths15(root);
3301
- if (!existsSync19(paths.haiveDir)) {
3820
+ const root = findProjectRoot19(opts.dir);
3821
+ const paths = resolveHaivePaths16(root);
3822
+ if (!existsSync21(paths.haiveDir)) {
3302
3823
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
3303
3824
  process.exitCode = 1;
3304
3825
  return;
@@ -3321,14 +3842,14 @@ function registerMemoryTried(memory2) {
3321
3842
  }
3322
3843
  const body = lines.join("\n") + "\n";
3323
3844
  const file = memoryFilePath4(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
3324
- await mkdir8(path20.dirname(file), { recursive: true });
3325
- if (existsSync19(file)) {
3845
+ await mkdir10(path22.dirname(file), { recursive: true });
3846
+ if (existsSync21(file)) {
3326
3847
  ui.error(`Memory already exists at ${file}`);
3327
3848
  process.exitCode = 1;
3328
3849
  return;
3329
3850
  }
3330
- await writeFile11(file, serializeMemory8({ frontmatter, body }), "utf8");
3331
- ui.success(`Recorded: ${path20.relative(root, file)}`);
3851
+ await writeFile12(file, serializeMemory8({ frontmatter, body }), "utf8");
3852
+ ui.success(`Recorded: ${path22.relative(root, file)}`);
3332
3853
  ui.info(`id=${frontmatter.id} type=attempt status=validated (auto-approved)`);
3333
3854
  });
3334
3855
  }
@@ -3338,26 +3859,26 @@ function parseCsv4(value) {
3338
3859
  }
3339
3860
 
3340
3861
  // src/commands/memory-pending.ts
3341
- import { existsSync as existsSync20 } from "fs";
3342
- import path21 from "path";
3862
+ import { existsSync as existsSync22 } from "fs";
3863
+ import path23 from "path";
3343
3864
  import "commander";
3344
3865
  import {
3345
- findProjectRoot as findProjectRoot19,
3866
+ findProjectRoot as findProjectRoot20,
3346
3867
  getUsage as getUsage5,
3347
- loadUsageIndex as loadUsageIndex5,
3348
- resolveHaivePaths as resolveHaivePaths16
3868
+ loadUsageIndex as loadUsageIndex6,
3869
+ resolveHaivePaths as resolveHaivePaths17
3349
3870
  } from "@hiveai/core";
3350
3871
  function registerMemoryPending(memory2) {
3351
3872
  memory2.command("pending").description("List 'proposed' memories awaiting review (sorted by reads desc)").option("--scope <scope>", "filter by scope (personal | team | module)").option("-d, --dir <dir>", "project root").action(async (opts) => {
3352
- const root = findProjectRoot19(opts.dir);
3353
- const paths = resolveHaivePaths16(root);
3354
- if (!existsSync20(paths.memoriesDir)) {
3873
+ const root = findProjectRoot20(opts.dir);
3874
+ const paths = resolveHaivePaths17(root);
3875
+ if (!existsSync22(paths.memoriesDir)) {
3355
3876
  ui.error(`No .ai/memories at ${root}.`);
3356
3877
  process.exitCode = 1;
3357
3878
  return;
3358
3879
  }
3359
3880
  const all = await loadMemoriesFromDir4(paths.memoriesDir);
3360
- const usage = await loadUsageIndex5(paths);
3881
+ const usage = await loadUsageIndex6(paths);
3361
3882
  const proposed = all.filter(({ memory: mem }) => {
3362
3883
  if (mem.frontmatter.status !== "proposed") return false;
3363
3884
  if (opts.scope && mem.frontmatter.scope !== opts.scope) return false;
@@ -3379,31 +3900,31 @@ function registerMemoryPending(memory2) {
3379
3900
  console.log(
3380
3901
  `${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.dim(`age=${ageStr} reads=${u.read_count} rejections=${u.rejected_count}`)}`
3381
3902
  );
3382
- console.log(` ${ui.dim(path21.relative(root, filePath))}`);
3903
+ console.log(` ${ui.dim(path23.relative(root, filePath))}`);
3383
3904
  }
3384
3905
  ui.info(`${proposed.length} pending`);
3385
3906
  });
3386
3907
  }
3387
3908
 
3388
3909
  // src/commands/memory-query.ts
3389
- import { existsSync as existsSync21 } from "fs";
3390
- import path22 from "path";
3910
+ import { existsSync as existsSync23 } from "fs";
3911
+ import path24 from "path";
3391
3912
  import "commander";
3392
3913
  import {
3393
3914
  extractSnippet,
3394
- findProjectRoot as findProjectRoot20,
3915
+ findProjectRoot as findProjectRoot21,
3395
3916
  literalMatchesAllTokens as literalMatchesAllTokens2,
3396
3917
  literalMatchesAnyToken as literalMatchesAnyToken2,
3397
3918
  pickSnippetNeedle,
3398
- resolveHaivePaths as resolveHaivePaths17,
3919
+ resolveHaivePaths as resolveHaivePaths18,
3399
3920
  tokenizeQuery as tokenizeQuery2,
3400
3921
  trackReads as trackReads2
3401
3922
  } from "@hiveai/core";
3402
3923
  function registerMemoryQuery(memory2) {
3403
- memory2.command("query <text>").description("Search memories by id, tag, or substring (AND, OR fallback)").option("-d, --dir <dir>", "project root").option("--limit <n>", "max results", "20").option("--scope <scope>", "personal | team | module").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected)").option("--show-rejected", "include rejected memories (hidden by default)").action(async (text, opts) => {
3404
- const root = findProjectRoot20(opts.dir);
3405
- const paths = resolveHaivePaths17(root);
3406
- if (!existsSync21(paths.memoriesDir)) {
3924
+ memory2.command("query <text>").alias("search").description("Search memories by id, tag, or substring (AND, OR fallback). Alias: search").option("-d, --dir <dir>", "project root").option("--limit <n>", "max results", "20").option("--scope <scope>", "personal | team | module").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected)").option("--show-rejected", "include rejected memories (hidden by default)").action(async (text, opts) => {
3925
+ const root = findProjectRoot21(opts.dir);
3926
+ const paths = resolveHaivePaths18(root);
3927
+ if (!existsSync23(paths.memoriesDir)) {
3407
3928
  ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
3408
3929
  process.exitCode = 1;
3409
3930
  return;
@@ -3444,7 +3965,7 @@ function registerMemoryQuery(memory2) {
3444
3965
  const fm = mem.frontmatter;
3445
3966
  const statusBadge = ui.statusBadge(fm.status);
3446
3967
  console.log(`${ui.bold(fm.id)} ${ui.dim(fm.scope)} ${statusBadge}`);
3447
- console.log(` ${ui.dim(path22.relative(root, filePath))}`);
3968
+ console.log(` ${ui.dim(path24.relative(root, filePath))}`);
3448
3969
  const snippet = extractSnippet(mem.body, snippetNeedle);
3449
3970
  if (snippet) console.log(` ${snippet}`);
3450
3971
  }
@@ -3461,22 +3982,22 @@ ${top.length} of ${matches.length} match${matches.length === 1 ? "" : "es"}`)
3461
3982
  }
3462
3983
 
3463
3984
  // src/commands/memory-reject.ts
3464
- import { writeFile as writeFile12 } from "fs/promises";
3465
- import { existsSync as existsSync22 } from "fs";
3985
+ import { writeFile as writeFile13 } from "fs/promises";
3986
+ import { existsSync as existsSync24 } from "fs";
3466
3987
  import "commander";
3467
3988
  import {
3468
- findProjectRoot as findProjectRoot21,
3469
- loadUsageIndex as loadUsageIndex6,
3989
+ findProjectRoot as findProjectRoot22,
3990
+ loadUsageIndex as loadUsageIndex7,
3470
3991
  recordRejection,
3471
- resolveHaivePaths as resolveHaivePaths18,
3992
+ resolveHaivePaths as resolveHaivePaths19,
3472
3993
  saveUsageIndex,
3473
3994
  serializeMemory as serializeMemory9
3474
3995
  } from "@hiveai/core";
3475
3996
  function registerMemoryReject(memory2) {
3476
3997
  memory2.command("reject <id>").description("Record a rejection (blocks auto-promotion and lowers confidence)").option("-r, --reason <reason>", "why this memory is being rejected").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
3477
- const root = findProjectRoot21(opts.dir);
3478
- const paths = resolveHaivePaths18(root);
3479
- if (!existsSync22(paths.memoriesDir)) {
3998
+ const root = findProjectRoot22(opts.dir);
3999
+ const paths = resolveHaivePaths19(root);
4000
+ if (!existsSync24(paths.memoriesDir)) {
3480
4001
  ui.error(`No .ai/memories at ${root}.`);
3481
4002
  process.exitCode = 1;
3482
4003
  return;
@@ -3488,7 +4009,7 @@ function registerMemoryReject(memory2) {
3488
4009
  process.exitCode = 1;
3489
4010
  return;
3490
4011
  }
3491
- await writeFile12(
4012
+ await writeFile13(
3492
4013
  loaded.filePath,
3493
4014
  serializeMemory9({
3494
4015
  frontmatter: {
@@ -3500,7 +4021,7 @@ function registerMemoryReject(memory2) {
3500
4021
  }),
3501
4022
  "utf8"
3502
4023
  );
3503
- const idx = await loadUsageIndex6(paths);
4024
+ const idx = await loadUsageIndex7(paths);
3504
4025
  recordRejection(idx, id, opts.reason ?? null);
3505
4026
  await saveUsageIndex(paths, idx);
3506
4027
  const u = idx.by_id[id];
@@ -3512,22 +4033,22 @@ function registerMemoryReject(memory2) {
3512
4033
  }
3513
4034
 
3514
4035
  // src/commands/memory-rm.ts
3515
- import { existsSync as existsSync23 } from "fs";
4036
+ import { existsSync as existsSync25 } from "fs";
3516
4037
  import { unlink as unlink2 } from "fs/promises";
3517
- import path23 from "path";
4038
+ import path25 from "path";
3518
4039
  import { createInterface } from "readline/promises";
3519
4040
  import "commander";
3520
4041
  import {
3521
- findProjectRoot as findProjectRoot22,
3522
- loadUsageIndex as loadUsageIndex7,
3523
- resolveHaivePaths as resolveHaivePaths19,
4042
+ findProjectRoot as findProjectRoot23,
4043
+ loadUsageIndex as loadUsageIndex8,
4044
+ resolveHaivePaths as resolveHaivePaths20,
3524
4045
  saveUsageIndex as saveUsageIndex2
3525
4046
  } from "@hiveai/core";
3526
4047
  function registerMemoryRm(memory2) {
3527
4048
  memory2.command("rm <id>").description("Delete a memory file (and its usage entry by default)").option("-y, --yes", "skip the confirmation prompt").option("--keep-usage", "do not remove the usage.json entry").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
3528
- const root = findProjectRoot22(opts.dir);
3529
- const paths = resolveHaivePaths19(root);
3530
- if (!existsSync23(paths.memoriesDir)) {
4049
+ const root = findProjectRoot23(opts.dir);
4050
+ const paths = resolveHaivePaths20(root);
4051
+ if (!existsSync25(paths.memoriesDir)) {
3531
4052
  ui.error(`No .ai/memories at ${root}.`);
3532
4053
  process.exitCode = 1;
3533
4054
  return;
@@ -3539,7 +4060,7 @@ function registerMemoryRm(memory2) {
3539
4060
  process.exitCode = 1;
3540
4061
  return;
3541
4062
  }
3542
- const rel = path23.relative(root, found.filePath);
4063
+ const rel = path25.relative(root, found.filePath);
3543
4064
  if (!opts.yes) {
3544
4065
  const rl = createInterface({ input: process.stdin, output: process.stdout });
3545
4066
  const answer = (await rl.question(`Delete ${rel}? [y/N] `)).trim().toLowerCase();
@@ -3552,7 +4073,7 @@ function registerMemoryRm(memory2) {
3552
4073
  await unlink2(found.filePath);
3553
4074
  ui.success(`Deleted ${rel}`);
3554
4075
  if (!opts.keepUsage) {
3555
- const idx = await loadUsageIndex7(paths);
4076
+ const idx = await loadUsageIndex8(paths);
3556
4077
  if (idx.by_id[id]) {
3557
4078
  delete idx.by_id[id];
3558
4079
  await saveUsageIndex2(paths, idx);
@@ -3563,22 +4084,22 @@ function registerMemoryRm(memory2) {
3563
4084
  }
3564
4085
 
3565
4086
  // src/commands/memory-show.ts
3566
- import { existsSync as existsSync24 } from "fs";
3567
- import { readFile as readFile9 } from "fs/promises";
3568
- import path24 from "path";
4087
+ import { existsSync as existsSync26 } from "fs";
4088
+ import { readFile as readFile10 } from "fs/promises";
4089
+ import path26 from "path";
3569
4090
  import "commander";
3570
4091
  import {
3571
4092
  deriveConfidence as deriveConfidence2,
3572
- findProjectRoot as findProjectRoot23,
4093
+ findProjectRoot as findProjectRoot24,
3573
4094
  getUsage as getUsage6,
3574
- loadUsageIndex as loadUsageIndex8,
3575
- resolveHaivePaths as resolveHaivePaths20
4095
+ loadUsageIndex as loadUsageIndex9,
4096
+ resolveHaivePaths as resolveHaivePaths21
3576
4097
  } from "@hiveai/core";
3577
4098
  function registerMemoryShow(memory2) {
3578
4099
  memory2.command("show <id>").description("Print a memory's frontmatter, body, and confidence/usage").option("--raw", "print the raw file contents instead of a summary").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
3579
- const root = findProjectRoot23(opts.dir);
3580
- const paths = resolveHaivePaths20(root);
3581
- if (!existsSync24(paths.memoriesDir)) {
4100
+ const root = findProjectRoot24(opts.dir);
4101
+ const paths = resolveHaivePaths21(root);
4102
+ if (!existsSync26(paths.memoriesDir)) {
3582
4103
  ui.error(`No .ai/memories at ${root}.`);
3583
4104
  process.exitCode = 1;
3584
4105
  return;
@@ -3591,11 +4112,11 @@ function registerMemoryShow(memory2) {
3591
4112
  return;
3592
4113
  }
3593
4114
  if (opts.raw) {
3594
- console.log(await readFile9(found.filePath, "utf8"));
4115
+ console.log(await readFile10(found.filePath, "utf8"));
3595
4116
  return;
3596
4117
  }
3597
4118
  const fm = found.memory.frontmatter;
3598
- const usage = await loadUsageIndex8(paths);
4119
+ const usage = await loadUsageIndex9(paths);
3599
4120
  const u = getUsage6(usage, fm.id);
3600
4121
  const conf = deriveConfidence2(fm, u);
3601
4122
  console.log(ui.bold(fm.id));
@@ -3607,7 +4128,7 @@ function registerMemoryShow(memory2) {
3607
4128
  if (fm.verified_at) console.log(`${ui.dim("verified:")} ${fm.verified_at}`);
3608
4129
  if (fm.stale_reason) console.log(`${ui.dim("stale:")} ${fm.stale_reason}`);
3609
4130
  console.log(`${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`);
3610
- console.log(`${ui.dim("file:")} ${path24.relative(root, found.filePath)}`);
4131
+ console.log(`${ui.dim("file:")} ${path26.relative(root, found.filePath)}`);
3611
4132
  if (fm.anchor.paths.length || fm.anchor.symbols.length) {
3612
4133
  console.log(ui.dim("anchor:"));
3613
4134
  if (fm.anchor.commit) console.log(` ${ui.dim("commit:")} ${fm.anchor.commit}`);
@@ -3622,27 +4143,27 @@ function registerMemoryShow(memory2) {
3622
4143
  }
3623
4144
 
3624
4145
  // src/commands/memory-stats.ts
3625
- import { existsSync as existsSync25 } from "fs";
3626
- import path25 from "path";
4146
+ import { existsSync as existsSync27 } from "fs";
4147
+ import path27 from "path";
3627
4148
  import "commander";
3628
4149
  import {
3629
4150
  deriveConfidence as deriveConfidence3,
3630
- findProjectRoot as findProjectRoot24,
4151
+ findProjectRoot as findProjectRoot25,
3631
4152
  getUsage as getUsage7,
3632
- loadUsageIndex as loadUsageIndex9,
3633
- resolveHaivePaths as resolveHaivePaths21
4153
+ loadUsageIndex as loadUsageIndex10,
4154
+ resolveHaivePaths as resolveHaivePaths22
3634
4155
  } from "@hiveai/core";
3635
4156
  function registerMemoryStats(memory2) {
3636
4157
  memory2.command("stats").description("Show usage stats and confidence levels per memory").option("--id <id>", "show stats for a single memory id").option("-d, --dir <dir>", "project root").action(async (opts) => {
3637
- const root = findProjectRoot24(opts.dir);
3638
- const paths = resolveHaivePaths21(root);
3639
- if (!existsSync25(paths.memoriesDir)) {
4158
+ const root = findProjectRoot25(opts.dir);
4159
+ const paths = resolveHaivePaths22(root);
4160
+ if (!existsSync27(paths.memoriesDir)) {
3640
4161
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
3641
4162
  process.exitCode = 1;
3642
4163
  return;
3643
4164
  }
3644
4165
  const all = await loadMemoriesFromDir4(paths.memoriesDir);
3645
- const usage = await loadUsageIndex9(paths);
4166
+ const usage = await loadUsageIndex10(paths);
3646
4167
  const target = opts.id ? all.filter((m) => m.memory.frontmatter.id === opts.id) : all;
3647
4168
  if (target.length === 0) {
3648
4169
  ui.info(opts.id ? `No memory with id "${opts.id}".` : "No memories.");
@@ -3661,19 +4182,19 @@ function registerMemoryStats(memory2) {
3661
4182
  console.log(
3662
4183
  ` ${ui.dim("status:")} ${fm.status} ${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`
3663
4184
  );
3664
- console.log(` ${ui.dim(path25.relative(root, filePath))}`);
4185
+ console.log(` ${ui.dim(path27.relative(root, filePath))}`);
3665
4186
  }
3666
4187
  });
3667
4188
  }
3668
4189
 
3669
4190
  // src/commands/memory-verify.ts
3670
- import { writeFile as writeFile13 } from "fs/promises";
3671
- import { existsSync as existsSync26 } from "fs";
3672
- import path26 from "path";
4191
+ import { writeFile as writeFile14 } from "fs/promises";
4192
+ import { existsSync as existsSync28 } from "fs";
4193
+ import path28 from "path";
3673
4194
  import "commander";
3674
4195
  import {
3675
- findProjectRoot as findProjectRoot25,
3676
- resolveHaivePaths as resolveHaivePaths22,
4196
+ findProjectRoot as findProjectRoot26,
4197
+ resolveHaivePaths as resolveHaivePaths23,
3677
4198
  serializeMemory as serializeMemory10,
3678
4199
  verifyAnchor as verifyAnchor2
3679
4200
  } from "@hiveai/core";
@@ -3681,9 +4202,9 @@ function registerMemoryVerify(memory2) {
3681
4202
  memory2.command("verify").description(
3682
4203
  "Check that memory anchor paths still exist in the current codebase.\n\n A memory is 'stale' when its anchored file or symbol was moved, deleted, or renamed.\n Stale memories are shown with a warning in get_briefing and should be updated or deleted.\n\n haive sync runs this automatically. Use this command for on-demand checks or in CI.\n\n CI recommendation: add 'haive memory verify' to your haive-sync.yml PR check job\n to catch stale memories before they reach main.\n\n Examples:\n haive memory verify # check all, report only\n haive memory verify --update # mark stale/fresh on disk\n haive memory verify --id 2026-04-28-gotcha-x # check one memory\n"
3683
4204
  ).option("--id <id>", "verify a single memory by id").option("--all", "verify every memory (default if --id is omitted)").option("--update", "write status=stale or status=validated back to disk").option("-d, --dir <dir>", "project root").action(async (opts) => {
3684
- const root = findProjectRoot25(opts.dir);
3685
- const paths = resolveHaivePaths22(root);
3686
- if (!existsSync26(paths.memoriesDir)) {
4205
+ const root = findProjectRoot26(opts.dir);
4206
+ const paths = resolveHaivePaths23(root);
4207
+ if (!existsSync28(paths.memoriesDir)) {
3687
4208
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
3688
4209
  process.exitCode = 1;
3689
4210
  return;
@@ -3706,7 +4227,7 @@ function registerMemoryVerify(memory2) {
3706
4227
  anchorlessIds.push(mem.frontmatter.id);
3707
4228
  continue;
3708
4229
  }
3709
- const rel = path26.relative(root, filePath);
4230
+ const rel = path28.relative(root, filePath);
3710
4231
  if (result.stale) {
3711
4232
  staleCount++;
3712
4233
  console.log(`${ui.bold("STALE")} ${mem.frontmatter.id}`);
@@ -3721,7 +4242,7 @@ function registerMemoryVerify(memory2) {
3721
4242
  }
3722
4243
  if (opts.update) {
3723
4244
  const next = applyVerification(mem, result);
3724
- await writeFile13(filePath, serializeMemory10(next), "utf8");
4245
+ await writeFile14(filePath, serializeMemory10(next), "utf8");
3725
4246
  updated++;
3726
4247
  }
3727
4248
  }
@@ -3769,30 +4290,30 @@ function applyVerification(mem, result) {
3769
4290
  }
3770
4291
 
3771
4292
  // src/commands/memory-import.ts
3772
- import { readFile as readFile10 } from "fs/promises";
3773
- import { existsSync as existsSync27 } from "fs";
4293
+ import { readFile as readFile11 } from "fs/promises";
4294
+ import { existsSync as existsSync29 } from "fs";
3774
4295
  import "commander";
3775
4296
  import {
3776
- findProjectRoot as findProjectRoot26,
3777
- resolveHaivePaths as resolveHaivePaths23
4297
+ findProjectRoot as findProjectRoot27,
4298
+ resolveHaivePaths as resolveHaivePaths24
3778
4299
  } from "@hiveai/core";
3779
4300
  function registerMemoryImport(memory2) {
3780
4301
  memory2.command("import").description(
3781
4302
  "Parse a Markdown file and suggest memories via the import_docs MCP prompt (prints a ready-to-use prompt invocation)"
3782
4303
  ).requiredOption("--from <file>", "Markdown/text file to import from").option("--scope <scope>", "personal | team (default: team)", "team").option("-d, --dir <dir>", "project root").action(async (opts) => {
3783
- const root = findProjectRoot26(opts.dir);
3784
- const paths = resolveHaivePaths23(root);
3785
- if (!existsSync27(paths.haiveDir)) {
4304
+ const root = findProjectRoot27(opts.dir);
4305
+ const paths = resolveHaivePaths24(root);
4306
+ if (!existsSync29(paths.haiveDir)) {
3786
4307
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
3787
4308
  process.exitCode = 1;
3788
4309
  return;
3789
4310
  }
3790
- if (!existsSync27(opts.from)) {
4311
+ if (!existsSync29(opts.from)) {
3791
4312
  ui.error(`File not found: ${opts.from}`);
3792
4313
  process.exitCode = 1;
3793
4314
  return;
3794
4315
  }
3795
- const content = await readFile10(opts.from, "utf8");
4316
+ const content = await readFile11(opts.from, "utf8");
3796
4317
  const scope = opts.scope ?? "team";
3797
4318
  ui.info(`Preparing import from: ${opts.from} (scope=${scope})`);
3798
4319
  ui.info(`Content length: ${content.length} chars`);
@@ -3820,14 +4341,14 @@ function registerMemoryImport(memory2) {
3820
4341
  }
3821
4342
 
3822
4343
  // src/commands/memory-import-changelog.ts
3823
- import { existsSync as existsSync28 } from "fs";
3824
- import { readFile as readFile11, mkdir as mkdir9, writeFile as writeFile14 } from "fs/promises";
3825
- import path27 from "path";
4344
+ import { existsSync as existsSync30 } from "fs";
4345
+ import { readFile as readFile12, mkdir as mkdir11, writeFile as writeFile15 } from "fs/promises";
4346
+ import path29 from "path";
3826
4347
  import "commander";
3827
4348
  import {
3828
4349
  buildFrontmatter as buildFrontmatter5,
3829
- findProjectRoot as findProjectRoot27,
3830
- resolveHaivePaths as resolveHaivePaths24,
4350
+ findProjectRoot as findProjectRoot28,
4351
+ resolveHaivePaths as resolveHaivePaths25,
3831
4352
  serializeMemory as serializeMemory11
3832
4353
  } from "@hiveai/core";
3833
4354
  function parseChangelog(content) {
@@ -3891,15 +4412,15 @@ function registerMemoryImportChangelog(memory2) {
3891
4412
  "--versions <csv>",
3892
4413
  "only import specific versions (comma-separated), or 'latest' for the most recent breaking version"
3893
4414
  ).option("-d, --dir <dir>", "project root").action(async (opts) => {
3894
- const root = findProjectRoot27(opts.dir);
3895
- const paths = resolveHaivePaths24(root);
3896
- const changelogPath = path27.resolve(root, opts.fromChangelog);
3897
- if (!existsSync28(changelogPath)) {
4415
+ const root = findProjectRoot28(opts.dir);
4416
+ const paths = resolveHaivePaths25(root);
4417
+ const changelogPath = path29.resolve(root, opts.fromChangelog);
4418
+ if (!existsSync30(changelogPath)) {
3898
4419
  ui.error(`CHANGELOG not found: ${changelogPath}`);
3899
4420
  process.exitCode = 1;
3900
4421
  return;
3901
4422
  }
3902
- const content = await readFile11(changelogPath, "utf8");
4423
+ const content = await readFile12(changelogPath, "utf8");
3903
4424
  let entries = parseChangelog(content);
3904
4425
  if (entries.length === 0) {
3905
4426
  ui.warn("No breaking changes, deprecations, or removals found in the CHANGELOG.");
@@ -3913,10 +4434,10 @@ function registerMemoryImportChangelog(memory2) {
3913
4434
  entries = entries.filter((e) => requested.includes(e.version));
3914
4435
  }
3915
4436
  }
3916
- const pkgName = opts.package ?? path27.basename(path27.dirname(changelogPath));
4437
+ const pkgName = opts.package ?? path29.basename(path29.dirname(changelogPath));
3917
4438
  const scope = opts.scope ?? "team";
3918
- const teamDir = path27.join(paths.memoriesDir, scope);
3919
- await mkdir9(teamDir, { recursive: true });
4439
+ const teamDir = path29.join(paths.memoriesDir, scope);
4440
+ await mkdir11(teamDir, { recursive: true });
3920
4441
  let saved = 0;
3921
4442
  for (const entry of entries) {
3922
4443
  const lines = [];
@@ -3938,7 +4459,7 @@ function registerMemoryImportChangelog(memory2) {
3938
4459
  lines.push("");
3939
4460
  }
3940
4461
  lines.push(
3941
- `**Source:** \`${path27.relative(root, changelogPath)}\`
4462
+ `**Source:** \`${path29.relative(root, changelogPath)}\`
3942
4463
  **Action:** Update all usages of ${pkgName} if they rely on any of the above.`
3943
4464
  );
3944
4465
  const slug = `changelog-${pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase()}-v${entry.version.replace(/\./g, "-")}`;
@@ -3953,11 +4474,11 @@ function registerMemoryImportChangelog(memory2) {
3953
4474
  pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase(),
3954
4475
  `v${entry.version}`
3955
4476
  ],
3956
- paths: [path27.relative(root, changelogPath)],
4477
+ paths: [path29.relative(root, changelogPath)],
3957
4478
  topic: `changelog-${pkgName}-${entry.version}`
3958
4479
  });
3959
- await writeFile14(
3960
- path27.join(teamDir, `${fm.id}.md`),
4480
+ await writeFile15(
4481
+ path29.join(teamDir, `${fm.id}.md`),
3961
4482
  serializeMemory11({ frontmatter: fm, body: lines.join("\n") }),
3962
4483
  "utf8"
3963
4484
  );
@@ -3980,17 +4501,17 @@ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${p
3980
4501
  }
3981
4502
 
3982
4503
  // src/commands/memory-digest.ts
3983
- import { existsSync as existsSync29 } from "fs";
3984
- import { writeFile as writeFile15 } from "fs/promises";
3985
- import path28 from "path";
4504
+ import { existsSync as existsSync31 } from "fs";
4505
+ import { writeFile as writeFile16 } from "fs/promises";
4506
+ import path30 from "path";
3986
4507
  import "commander";
3987
4508
  import {
3988
4509
  deriveConfidence as deriveConfidence4,
3989
- findProjectRoot as findProjectRoot28,
4510
+ findProjectRoot as findProjectRoot29,
3990
4511
  getUsage as getUsage8,
3991
4512
  loadMemoriesFromDir as loadMemoriesFromDir5,
3992
- loadUsageIndex as loadUsageIndex10,
3993
- resolveHaivePaths as resolveHaivePaths25
4513
+ loadUsageIndex as loadUsageIndex11,
4514
+ resolveHaivePaths as resolveHaivePaths26
3994
4515
  } from "@hiveai/core";
3995
4516
  var CONFIDENCE_EMOJI = {
3996
4517
  unverified: "\u2B1C",
@@ -4003,9 +4524,9 @@ function registerMemoryDigest(program2) {
4003
4524
  program2.command("digest").description(
4004
4525
  "Generate a Markdown review digest of recently added or updated memories.\n\n Groups memories by type, shows confidence, status, read count, and anchor info.\n Each memory has action checkboxes (approve / reject / keep as-is) for peer review.\n\n Use this to do a bulk weekly review of team memories, or share with teammates\n as a pull-request attachment so humans can validate what the AI captured.\n\n Examples:\n haive memory digest # last 7 days, team scope\n haive memory digest --days 30 --scope all # last 30 days, all scopes\n haive memory digest --out review.md # write to file\n"
4005
4526
  ).option("--days <n>", "look-back window in days (default: 7)", "7").option("--scope <scope>", "personal | team | module | all (default: team)", "team").option("--out <file>", "write digest to a file instead of stdout").option("-d, --dir <dir>", "project root").action(async (opts) => {
4006
- const root = findProjectRoot28(opts.dir);
4007
- const paths = resolveHaivePaths25(root);
4008
- if (!existsSync29(paths.memoriesDir)) {
4527
+ const root = findProjectRoot29(opts.dir);
4528
+ const paths = resolveHaivePaths26(root);
4529
+ if (!existsSync31(paths.memoriesDir)) {
4009
4530
  ui.error("No .ai/memories found. Run `haive init` first.");
4010
4531
  process.exitCode = 1;
4011
4532
  return;
@@ -4014,7 +4535,7 @@ function registerMemoryDigest(program2) {
4014
4535
  const scopeFilter = opts.scope ?? "team";
4015
4536
  const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
4016
4537
  const all = await loadMemoriesFromDir5(paths.memoriesDir);
4017
- const usage = await loadUsageIndex10(paths);
4538
+ const usage = await loadUsageIndex11(paths);
4018
4539
  const recent = all.filter(({ memory: mem }) => {
4019
4540
  const fm = mem.frontmatter;
4020
4541
  if (fm.type === "session_recap") return false;
@@ -4077,8 +4598,8 @@ function registerMemoryDigest(program2) {
4077
4598
  );
4078
4599
  const digest = lines.join("\n");
4079
4600
  if (opts.out) {
4080
- const outPath = path28.resolve(process.cwd(), opts.out);
4081
- await writeFile15(outPath, digest, "utf8");
4601
+ const outPath = path30.resolve(process.cwd(), opts.out);
4602
+ await writeFile16(outPath, digest, "utf8");
4082
4603
  ui.success(`Digest written to ${opts.out} (${recent.length} memor${recent.length === 1 ? "y" : "ies"})`);
4083
4604
  } else {
4084
4605
  console.log(digest);
@@ -4087,18 +4608,52 @@ function registerMemoryDigest(program2) {
4087
4608
  }
4088
4609
 
4089
4610
  // src/commands/session-end.ts
4090
- import { writeFile as writeFile16, mkdir as mkdir10 } from "fs/promises";
4091
- import { existsSync as existsSync30 } from "fs";
4092
- import path29 from "path";
4611
+ import { writeFile as writeFile17, mkdir as mkdir12, readFile as readFile13, rm } from "fs/promises";
4612
+ import { existsSync as existsSync32 } from "fs";
4613
+ import path31 from "path";
4093
4614
  import "commander";
4094
4615
  import {
4095
4616
  buildFrontmatter as buildFrontmatter6,
4096
- findProjectRoot as findProjectRoot29,
4617
+ findProjectRoot as findProjectRoot30,
4097
4618
  loadMemoriesFromDir as loadMemoriesFromDir6,
4098
4619
  memoryFilePath as memoryFilePath5,
4099
- resolveHaivePaths as resolveHaivePaths26,
4620
+ resolveHaivePaths as resolveHaivePaths27,
4100
4621
  serializeMemory as serializeMemory12
4101
4622
  } from "@hiveai/core";
4623
+ async function buildAutoRecap(paths) {
4624
+ const obsFile = path31.join(paths.haiveDir, ".cache", "observations.jsonl");
4625
+ if (!existsSync32(obsFile)) return null;
4626
+ const raw = await readFile13(obsFile, "utf8").catch(() => "");
4627
+ if (!raw.trim()) return null;
4628
+ const lines = raw.split("\n").filter(Boolean);
4629
+ const obs = [];
4630
+ for (const line of lines) {
4631
+ try {
4632
+ obs.push(JSON.parse(line));
4633
+ } catch {
4634
+ }
4635
+ }
4636
+ if (obs.length === 0) return null;
4637
+ const toolCounts = /* @__PURE__ */ new Map();
4638
+ const fileCounts = /* @__PURE__ */ new Map();
4639
+ const summaries = [];
4640
+ for (const o of obs) {
4641
+ toolCounts.set(o.tool, (toolCounts.get(o.tool) ?? 0) + 1);
4642
+ for (const f of o.files ?? []) fileCounts.set(f, (fileCounts.get(f) ?? 0) + 1);
4643
+ if (summaries.length < 10) summaries.push(`- ${o.summary}`);
4644
+ }
4645
+ const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([t, c]) => `${t} \xD7${c}`).join(", ");
4646
+ const topFiles = [...fileCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8);
4647
+ const goal = `Auto-captured session \u2014 ${obs.length} tool calls (${topTools})`;
4648
+ const accomplished = summaries.length ? `Recent activity:
4649
+ ${summaries.join("\n")}` : `Activity captured but no parseable summaries.`;
4650
+ return {
4651
+ goal,
4652
+ accomplished,
4653
+ files: topFiles.map(([f]) => f),
4654
+ rawCount: obs.length
4655
+ };
4656
+ }
4102
4657
  function buildRecapBody(opts) {
4103
4658
  const lines = [];
4104
4659
  lines.push(`## Goal
@@ -4145,24 +4700,53 @@ function registerSessionEnd(session2) {
4145
4700
  --files src/payments/WebhookController.ts,src/payments/WebhookService.ts \\\\
4146
4701
  --next "Add integration tests for webhook signature validation"
4147
4702
  `
4148
- ).requiredOption("--goal <text>", "what you were trying to accomplish (1\u20132 sentences)").requiredOption("--accomplished <text>", "what was actually done (bullet list recommended)").option("--discoveries <text>", "bugs, surprises, or inconsistencies found during this session").option("--files <csv>", "key files touched, comma-separated (used as anchor for staleness detection)").option("--next <text>", "what should happen next (for the next session or a teammate)").option("--scope <scope>", "personal | team | module (default: personal)", "personal").option("--module <name>", "module name (required when scope=module)").option("-d, --dir <dir>", "project root").action(async (opts) => {
4149
- const root = findProjectRoot29(opts.dir);
4150
- const paths = resolveHaivePaths26(root);
4151
- if (!existsSync30(paths.haiveDir)) {
4703
+ ).option("--goal <text>", "what you were trying to accomplish (1\u20132 sentences)").option("--accomplished <text>", "what was actually done (bullet list recommended)").option("--discoveries <text>", "bugs, surprises, or inconsistencies found during this session").option("--files <csv>", "key files touched, comma-separated (used as anchor for staleness detection)").option("--next <text>", "what should happen next (for the next session or a teammate)").option("--scope <scope>", "personal | team | module (default: personal)", "personal").option("--module <name>", "module name (required when scope=module)").option("--auto", "synthesize the recap from .ai/.cache/observations.jsonl (used by Claude Code SessionEnd hook)").option("--quiet", "suppress non-error output (for hook use)").option("-d, --dir <dir>", "project root").action(async (opts) => {
4704
+ const root = findProjectRoot30(opts.dir);
4705
+ const paths = resolveHaivePaths27(root);
4706
+ if (!existsSync32(paths.haiveDir)) {
4707
+ if (opts.auto || opts.quiet) return;
4152
4708
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
4153
4709
  process.exitCode = 1;
4154
4710
  return;
4155
4711
  }
4712
+ let resolvedFiles = opts.files;
4713
+ let goal = opts.goal;
4714
+ let accomplished = opts.accomplished;
4715
+ if (opts.auto) {
4716
+ const synth = await buildAutoRecap(paths);
4717
+ if (!synth) return;
4718
+ goal = goal ?? synth.goal;
4719
+ accomplished = accomplished ?? synth.accomplished;
4720
+ if (!resolvedFiles && synth.files.length) resolvedFiles = synth.files.join(",");
4721
+ }
4722
+ if (!goal || !accomplished) {
4723
+ if (opts.quiet) return;
4724
+ ui.error("session-end requires --goal and --accomplished (or pass --auto with captured observations).");
4725
+ process.exitCode = 1;
4726
+ return;
4727
+ }
4156
4728
  const scope = opts.scope ?? "personal";
4157
- const body = buildRecapBody(opts);
4729
+ const body = buildRecapBody({
4730
+ goal,
4731
+ accomplished,
4732
+ discoveries: opts.discoveries,
4733
+ files: resolvedFiles,
4734
+ next: opts.next
4735
+ });
4158
4736
  const topic = recapTopic(scope, opts.module);
4159
- const filesTouched = parseCsv5(opts.files);
4160
- const missingPaths = filesTouched.filter((p) => !existsSync30(path29.resolve(root, p)));
4161
- if (missingPaths.length > 0) {
4737
+ const filesTouched = parseCsv5(resolvedFiles);
4738
+ const missingPaths = filesTouched.filter((p) => !existsSync32(path31.resolve(root, p)));
4739
+ if (missingPaths.length > 0 && !opts.quiet) {
4162
4740
  ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
4163
4741
  for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
4164
4742
  }
4165
- if (existsSync30(paths.memoriesDir)) {
4743
+ const cleanupObservations = async () => {
4744
+ if (!opts.auto) return;
4745
+ const obsFile = path31.join(paths.haiveDir, ".cache", "observations.jsonl");
4746
+ if (existsSync32(obsFile)) await rm(obsFile).catch(() => {
4747
+ });
4748
+ };
4749
+ if (existsSync32(paths.memoriesDir)) {
4166
4750
  const existing = await loadMemoriesFromDir6(paths.memoriesDir);
4167
4751
  const topicMatch = existing.find(
4168
4752
  ({ memory: memory2 }) => memory2.frontmatter.topic === topic && memory2.frontmatter.scope === scope && (!opts.module || memory2.frontmatter.module === opts.module)
@@ -4178,9 +4762,12 @@ function registerSessionEnd(session2) {
4178
4762
  paths: filesTouched.length ? filesTouched : fm.anchor.paths
4179
4763
  }
4180
4764
  };
4181
- await writeFile16(topicMatch.filePath, serializeMemory12({ frontmatter: newFrontmatter, body }), "utf8");
4182
- ui.success(`Session recap updated (revision #${revisionCount})`);
4183
- ui.info(`id=${fm.id} file=${path29.relative(root, topicMatch.filePath)}`);
4765
+ await writeFile17(topicMatch.filePath, serializeMemory12({ frontmatter: newFrontmatter, body }), "utf8");
4766
+ await cleanupObservations();
4767
+ if (!opts.quiet) {
4768
+ ui.success(`Session recap updated (revision #${revisionCount})`);
4769
+ ui.info(`id=${fm.id} file=${path31.relative(root, topicMatch.filePath)}`);
4770
+ }
4184
4771
  return;
4185
4772
  }
4186
4773
  }
@@ -4195,11 +4782,14 @@ function registerSessionEnd(session2) {
4195
4782
  status: "validated"
4196
4783
  });
4197
4784
  const file = memoryFilePath5(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
4198
- await mkdir10(path29.dirname(file), { recursive: true });
4199
- await writeFile16(file, serializeMemory12({ frontmatter, body }), "utf8");
4200
- ui.success(`Session recap created`);
4201
- ui.info(`id=${frontmatter.id} scope=${scope} file=${path29.relative(root, file)}`);
4202
- ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
4785
+ await mkdir12(path31.dirname(file), { recursive: true });
4786
+ await writeFile17(file, serializeMemory12({ frontmatter, body }), "utf8");
4787
+ await cleanupObservations();
4788
+ if (!opts.quiet) {
4789
+ ui.success(`Session recap created`);
4790
+ ui.info(`id=${frontmatter.id} scope=${scope} file=${path31.relative(root, file)}`);
4791
+ ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
4792
+ }
4203
4793
  });
4204
4794
  }
4205
4795
  function parseCsv5(value) {
@@ -4208,15 +4798,15 @@ function parseCsv5(value) {
4208
4798
  }
4209
4799
 
4210
4800
  // src/commands/snapshot.ts
4211
- import { existsSync as existsSync31 } from "fs";
4801
+ import { existsSync as existsSync33 } from "fs";
4212
4802
  import { readdir as readdir2 } from "fs/promises";
4213
- import path30 from "path";
4803
+ import path32 from "path";
4214
4804
  import "commander";
4215
4805
  import {
4216
4806
  diffContract,
4217
- findProjectRoot as findProjectRoot30,
4807
+ findProjectRoot as findProjectRoot31,
4218
4808
  loadConfig as loadConfig2,
4219
- resolveHaivePaths as resolveHaivePaths27,
4809
+ resolveHaivePaths as resolveHaivePaths28,
4220
4810
  snapshotContract
4221
4811
  } from "@hiveai/core";
4222
4812
  function registerSnapshot(program2) {
@@ -4241,16 +4831,16 @@ function registerSnapshot(program2) {
4241
4831
  "--format <format>",
4242
4832
  "contract format: openapi | graphql | proto | typescript | json-schema (auto-detected if omitted)"
4243
4833
  ).option("--diff", "compare the contract against its stored snapshot").option("--list", "list all stored contract snapshots").option("-d, --dir <dir>", "project root").action(async (opts) => {
4244
- const root = findProjectRoot30(opts.dir);
4245
- const paths = resolveHaivePaths27(root);
4246
- if (!existsSync31(paths.haiveDir)) {
4834
+ const root = findProjectRoot31(opts.dir);
4835
+ const paths = resolveHaivePaths28(root);
4836
+ if (!existsSync33(paths.haiveDir)) {
4247
4837
  ui.error("No .ai/ found. Run `haive init` first.");
4248
4838
  process.exitCode = 1;
4249
4839
  return;
4250
4840
  }
4251
4841
  if (opts.list) {
4252
- const contractsDir = path30.join(paths.haiveDir, "contracts");
4253
- if (!existsSync31(contractsDir)) {
4842
+ const contractsDir = path32.join(paths.haiveDir, "contracts");
4843
+ if (!existsSync33(contractsDir)) {
4254
4844
  console.log(ui.dim("No contract snapshots found."));
4255
4845
  return;
4256
4846
  }
@@ -4305,7 +4895,7 @@ function registerSnapshot(program2) {
4305
4895
  return;
4306
4896
  }
4307
4897
  const contractPath = opts.contract;
4308
- const name = opts.name ?? path30.basename(contractPath, path30.extname(contractPath));
4898
+ const name = opts.name ?? path32.basename(contractPath, path32.extname(contractPath));
4309
4899
  const format = opts.format ?? detectFormat(contractPath) ?? "openapi";
4310
4900
  const contract = { name, path: contractPath, format };
4311
4901
  try {
@@ -4360,8 +4950,8 @@ async function runDiff(root, haiveDir, contract) {
4360
4950
  }
4361
4951
  }
4362
4952
  function detectFormat(filePath) {
4363
- const ext = path30.extname(filePath).toLowerCase();
4364
- const base = path30.basename(filePath).toLowerCase();
4953
+ const ext = path32.extname(filePath).toLowerCase();
4954
+ const base = path32.basename(filePath).toLowerCase();
4365
4955
  if (ext === ".yaml" || ext === ".yml" || ext === ".json") {
4366
4956
  if (base.includes("openapi") || base.includes("swagger")) return "openapi";
4367
4957
  if (base.includes("schema") || base.includes("graphql")) return "graphql";
@@ -4374,16 +4964,16 @@ function detectFormat(filePath) {
4374
4964
  }
4375
4965
 
4376
4966
  // src/commands/hub.ts
4377
- import { existsSync as existsSync32 } from "fs";
4378
- import { mkdir as mkdir11, readFile as readFile12, writeFile as writeFile17, copyFile } from "fs/promises";
4379
- import path31 from "path";
4967
+ import { existsSync as existsSync34 } from "fs";
4968
+ import { mkdir as mkdir13, readFile as readFile14, writeFile as writeFile18, copyFile } from "fs/promises";
4969
+ import path33 from "path";
4380
4970
  import { spawnSync as spawnSync3 } from "child_process";
4381
4971
  import "commander";
4382
4972
  import {
4383
- findProjectRoot as findProjectRoot31,
4973
+ findProjectRoot as findProjectRoot32,
4384
4974
  loadConfig as loadConfig3,
4385
4975
  loadMemoriesFromDir as loadMemoriesFromDir7,
4386
- resolveHaivePaths as resolveHaivePaths28,
4976
+ resolveHaivePaths as resolveHaivePaths29,
4387
4977
  saveConfig as saveConfig2,
4388
4978
  serializeMemory as serializeMemory13
4389
4979
  } from "@hiveai/core";
@@ -4395,8 +4985,8 @@ function registerHub(program2) {
4395
4985
  hub.command("init <hubPath>").description(
4396
4986
  "Initialize a new team-knowledge hub repo at <hubPath>.\n\n Creates a git repo with a .ai/ directory structure ready for shared memories.\n\n Example:\n haive hub init ../team-hub\n haive hub init /srv/git/team-knowledge\n"
4397
4987
  ).action(async (hubPath) => {
4398
- const absPath = path31.resolve(hubPath);
4399
- await mkdir11(absPath, { recursive: true });
4988
+ const absPath = path33.resolve(hubPath);
4989
+ await mkdir13(absPath, { recursive: true });
4400
4990
  const gitCheck = spawnSync3("git", ["rev-parse", "--git-dir"], { cwd: absPath });
4401
4991
  if (gitCheck.status !== 0) {
4402
4992
  const init = spawnSync3("git", ["init"], { cwd: absPath, encoding: "utf8" });
@@ -4406,10 +4996,10 @@ function registerHub(program2) {
4406
4996
  return;
4407
4997
  }
4408
4998
  }
4409
- const sharedDir = path31.join(absPath, ".ai", "memories", "shared");
4410
- await mkdir11(sharedDir, { recursive: true });
4411
- await writeFile17(
4412
- path31.join(absPath, ".ai", "README.md"),
4999
+ const sharedDir = path33.join(absPath, ".ai", "memories", "shared");
5000
+ await mkdir13(sharedDir, { recursive: true });
5001
+ await writeFile18(
5002
+ path33.join(absPath, ".ai", "README.md"),
4413
5003
  `# hAIve Team Knowledge Hub
4414
5004
 
4415
5005
  This repo is a shared knowledge hub for hAIve.
@@ -4430,8 +5020,8 @@ haive hub pull # import into a project
4430
5020
  `,
4431
5021
  "utf8"
4432
5022
  );
4433
- await writeFile17(
4434
- path31.join(absPath, ".gitignore"),
5023
+ await writeFile18(
5024
+ path33.join(absPath, ".gitignore"),
4435
5025
  ".ai/.cache/\n.ai/memories/personal/\n",
4436
5026
  "utf8"
4437
5027
  );
@@ -4446,7 +5036,7 @@ haive hub pull # import into a project
4446
5036
  `
4447
5037
  Next steps:
4448
5038
  1. Add hubPath to your project's .ai/haive.config.json:
4449
- { "hubPath": "${path31.relative(process.cwd(), absPath)}" }
5039
+ { "hubPath": "${path33.relative(process.cwd(), absPath)}" }
4450
5040
  2. Run \`haive hub push\` to publish your shared memories
4451
5041
  3. Share ${absPath} with teammates (git remote, NFS, etc.)
4452
5042
  `
@@ -4465,8 +5055,8 @@ Next steps:
4465
5055
  haive hub push --commit --message "feat: add payment API contract memories"
4466
5056
  `
4467
5057
  ).option("-d, --dir <dir>", "project root").option("--commit", "auto-commit to the hub repo after pushing").option("--message <msg>", "commit message for the hub (used with --commit)").action(async (opts) => {
4468
- const root = findProjectRoot31(opts.dir);
4469
- const paths = resolveHaivePaths28(root);
5058
+ const root = findProjectRoot32(opts.dir);
5059
+ const paths = resolveHaivePaths29(root);
4470
5060
  const config = await loadConfig3(paths);
4471
5061
  if (!config.hubPath) {
4472
5062
  ui.error(
@@ -4475,15 +5065,15 @@ Next steps:
4475
5065
  process.exitCode = 1;
4476
5066
  return;
4477
5067
  }
4478
- const hubRoot = path31.resolve(root, config.hubPath);
4479
- if (!existsSync32(hubRoot)) {
5068
+ const hubRoot = path33.resolve(root, config.hubPath);
5069
+ if (!existsSync34(hubRoot)) {
4480
5070
  ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
4481
5071
  process.exitCode = 1;
4482
5072
  return;
4483
5073
  }
4484
- const projectName = path31.basename(root);
4485
- const destDir = path31.join(hubRoot, ".ai", "memories", "shared", projectName);
4486
- await mkdir11(destDir, { recursive: true });
5074
+ const projectName = path33.basename(root);
5075
+ const destDir = path33.join(hubRoot, ".ai", "memories", "shared", projectName);
5076
+ await mkdir13(destDir, { recursive: true });
4487
5077
  const all = await loadMemoriesFromDir7(paths.memoriesDir);
4488
5078
  const shared = all.filter(
4489
5079
  ({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && // Don't push imported memories (avoid echo loops)
@@ -4501,15 +5091,15 @@ Next steps:
4501
5091
  for (const { memory: memory2 } of shared) {
4502
5092
  const fm = memory2.frontmatter;
4503
5093
  const fileName = `${fm.id}.md`;
4504
- const destPath = path31.join(destDir, fileName);
4505
- await writeFile17(destPath, serializeMemory13(memory2), "utf8");
5094
+ const destPath = path33.join(destDir, fileName);
5095
+ await writeFile18(destPath, serializeMemory13(memory2), "utf8");
4506
5096
  pushed++;
4507
5097
  }
4508
5098
  console.log(ui.green(`\u2713 Pushed ${pushed} shared memor${pushed === 1 ? "y" : "ies"} to hub`));
4509
5099
  console.log(ui.dim(` Location: ${destDir}`));
4510
5100
  if (opts.commit) {
4511
5101
  const message = opts.message ?? `haive: sync shared memories from ${projectName} (${pushed} memories)`;
4512
- spawnSync3("git", ["add", path31.join(".ai", "memories", "shared", projectName)], {
5102
+ spawnSync3("git", ["add", path33.join(".ai", "memories", "shared", projectName)], {
4513
5103
  cwd: hubRoot
4514
5104
  });
4515
5105
  const commit = spawnSync3("git", ["commit", "-m", message], {
@@ -4534,8 +5124,8 @@ Next steps:
4534
5124
  hub.command("pull").description(
4535
5125
  "Pull shared memories from the hub into this project.\n\n Imports all memories from hub/.ai/memories/shared/ EXCEPT this project's own.\n Imported memories land in .ai/memories/shared/<source-project-name>/.\n\n Examples:\n haive hub pull\n"
4536
5126
  ).option("-d, --dir <dir>", "project root").action(async (opts) => {
4537
- const root = findProjectRoot31(opts.dir);
4538
- const paths = resolveHaivePaths28(root);
5127
+ const root = findProjectRoot32(opts.dir);
5128
+ const paths = resolveHaivePaths29(root);
4539
5129
  const config = await loadConfig3(paths);
4540
5130
  if (!config.hubPath) {
4541
5131
  ui.error(
@@ -4544,13 +5134,13 @@ Next steps:
4544
5134
  process.exitCode = 1;
4545
5135
  return;
4546
5136
  }
4547
- const hubRoot = path31.resolve(root, config.hubPath);
4548
- const hubSharedDir = path31.join(hubRoot, ".ai", "memories", "shared");
4549
- if (!existsSync32(hubSharedDir)) {
5137
+ const hubRoot = path33.resolve(root, config.hubPath);
5138
+ const hubSharedDir = path33.join(hubRoot, ".ai", "memories", "shared");
5139
+ if (!existsSync34(hubSharedDir)) {
4550
5140
  ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
4551
5141
  return;
4552
5142
  }
4553
- const projectName = path31.basename(root);
5143
+ const projectName = path33.basename(root);
4554
5144
  const { readdir: readdir3 } = await import("fs/promises");
4555
5145
  const projectDirs = (await readdir3(hubSharedDir, { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== projectName).map((d) => d.name);
4556
5146
  if (projectDirs.length === 0) {
@@ -4560,17 +5150,17 @@ Next steps:
4560
5150
  let totalImported = 0;
4561
5151
  let totalUpdated = 0;
4562
5152
  for (const sourceName of projectDirs) {
4563
- const sourceDir = path31.join(hubSharedDir, sourceName);
4564
- const destDir = path31.join(paths.memoriesDir, "shared", sourceName);
4565
- await mkdir11(destDir, { recursive: true });
5153
+ const sourceDir = path33.join(hubSharedDir, sourceName);
5154
+ const destDir = path33.join(paths.memoriesDir, "shared", sourceName);
5155
+ await mkdir13(destDir, { recursive: true });
4566
5156
  const sourceFiles = (await readdir3(sourceDir)).filter((f) => f.endsWith(".md"));
4567
5157
  const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
4568
5158
  const existingInDest = await loadDir(destDir);
4569
5159
  const existingIds = new Set(existingInDest.map(({ memory: memory2 }) => memory2.frontmatter.id));
4570
5160
  for (const file of sourceFiles) {
4571
- const srcPath = path31.join(sourceDir, file);
4572
- const destPath = path31.join(destDir, file);
4573
- const fileContent = await readFile12(srcPath, "utf8");
5161
+ const srcPath = path33.join(sourceDir, file);
5162
+ const destPath = path33.join(destDir, file);
5163
+ const fileContent = await readFile14(srcPath, "utf8");
4574
5164
  const alreadyTagged = fileContent.includes(`cross-repo:${sourceName}`);
4575
5165
  if (!alreadyTagged) {
4576
5166
  await copyFile(srcPath, destPath);
@@ -4593,21 +5183,21 @@ Next steps:
4593
5183
  );
4594
5184
  });
4595
5185
  hub.command("status").description("Show hub sync status.").option("-d, --dir <dir>", "project root").action(async (opts) => {
4596
- const root = findProjectRoot31(opts.dir);
4597
- const paths = resolveHaivePaths28(root);
5186
+ const root = findProjectRoot32(opts.dir);
5187
+ const paths = resolveHaivePaths29(root);
4598
5188
  const config = await loadConfig3(paths);
4599
5189
  console.log(ui.bold("Hub status"));
4600
5190
  console.log(
4601
5191
  ` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
4602
5192
  );
4603
- const sharedDir = path31.join(paths.memoriesDir, "shared");
4604
- if (existsSync32(sharedDir)) {
5193
+ const sharedDir = path33.join(paths.memoriesDir, "shared");
5194
+ if (existsSync34(sharedDir)) {
4605
5195
  const { readdir: readdir3 } = await import("fs/promises");
4606
5196
  const sources = (await readdir3(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
4607
5197
  console.log(`
4608
5198
  Imported from ${sources.length} source(s):`);
4609
5199
  for (const src of sources) {
4610
- const files = (await readdir3(path31.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
5200
+ const files = (await readdir3(path33.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
4611
5201
  console.log(` ${src}: ${files.length} memor${files.length === 1 ? "y" : "ies"}`);
4612
5202
  }
4613
5203
  } else {
@@ -4622,8 +5212,8 @@ Next steps:
4622
5212
  if (outgoing.length > 0) {
4623
5213
  console.log(ui.dim(" Run `haive hub push` to publish them to the hub."));
4624
5214
  }
4625
- void readFile12;
4626
- void writeFile17;
5215
+ void readFile14;
5216
+ void writeFile18;
4627
5217
  void saveConfig2;
4628
5218
  });
4629
5219
  }
@@ -4632,16 +5222,21 @@ Next steps:
4632
5222
  import "commander";
4633
5223
  import {
4634
5224
  aggregateUsage,
4635
- findProjectRoot as findProjectRoot32,
5225
+ findProjectRoot as findProjectRoot33,
5226
+ loadUsageIndex as loadUsageIndex12,
4636
5227
  parseSince,
4637
5228
  readUsageEvents,
4638
- resolveHaivePaths as resolveHaivePaths29,
5229
+ resolveHaivePaths as resolveHaivePaths30,
4639
5230
  usageLogSize
4640
5231
  } from "@hiveai/core";
4641
5232
  function registerStats(program2) {
4642
- program2.command("stats").description("Show MCP tool-usage stats over a window (e.g. --since 7d).").option("--since <window>", "ISO date or relative (e.g. '7d', '24h', '30m')", "30d").option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
4643
- const root = findProjectRoot32(opts.dir);
4644
- const paths = resolveHaivePaths29(root);
5233
+ program2.command("stats").description("Show MCP tool-usage stats over a window (e.g. --since 7d).").option("--since <window>", "ISO date or relative (e.g. '7d', '24h', '30m')", "30d").option("--json", "emit JSON instead of human-readable output", false).option("--memory-hits", "show top-read memories (which mems are actually being used)", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
5234
+ const root = findProjectRoot33(opts.dir);
5235
+ const paths = resolveHaivePaths30(root);
5236
+ if (opts.memoryHits) {
5237
+ await renderMemoryHits(paths, opts);
5238
+ return;
5239
+ }
4645
5240
  const size = await usageLogSize(paths);
4646
5241
  if (!size.exists) {
4647
5242
  if (opts.json) {
@@ -4686,14 +5281,57 @@ function registerStats(program2) {
4686
5281
  }
4687
5282
  });
4688
5283
  }
5284
+ async function renderMemoryHits(paths, opts) {
5285
+ const index = await loadUsageIndex12(paths);
5286
+ const since = parseSince(opts.since ?? "30d");
5287
+ const sinceMs = since ? new Date(since).getTime() : null;
5288
+ const entries = Object.entries(index.by_id).map(([id, usage]) => ({ id, ...usage })).filter((e) => e.read_count > 0).filter((e) => {
5289
+ if (!sinceMs || !e.last_read_at) return !sinceMs;
5290
+ return new Date(e.last_read_at).getTime() >= sinceMs;
5291
+ }).sort((a, b) => b.read_count - a.read_count);
5292
+ if (opts.json) {
5293
+ console.log(JSON.stringify({
5294
+ window: opts.since ?? "30d",
5295
+ total_mems_with_hits: entries.length,
5296
+ top: entries.slice(0, 50)
5297
+ }, null, 2));
5298
+ return;
5299
+ }
5300
+ const window = opts.since ?? "30d";
5301
+ console.log(ui.bold(`Memory hits (${window})`));
5302
+ if (entries.length === 0) {
5303
+ ui.info(
5304
+ `No memory reads recorded in window. Reads are logged when \`haive briefing\` or \`haive memory query\` surface a memory.`
5305
+ );
5306
+ return;
5307
+ }
5308
+ console.log(
5309
+ ` ${ui.dim("memories with hits:")} ${entries.length} ${ui.dim("total reads:")} ${entries.reduce((a, e) => a + e.read_count, 0)}`
5310
+ );
5311
+ console.log();
5312
+ console.log(ui.bold("Top read memories:"));
5313
+ const maxCount = entries[0].read_count;
5314
+ for (const e of entries.slice(0, 25)) {
5315
+ const bar = "\u2588".repeat(Math.max(1, Math.round(e.read_count / maxCount * 20)));
5316
+ const lastRead = e.last_read_at?.slice(0, 10) ?? "?";
5317
+ console.log(
5318
+ ` ${ui.bold(String(e.read_count).padStart(4))} ${ui.green(bar.padEnd(20))} ${e.id} ${ui.dim(`last: ${lastRead}`)}`
5319
+ );
5320
+ }
5321
+ const dead = Object.keys(index.by_id).length - entries.length;
5322
+ if (dead > 0) {
5323
+ console.log();
5324
+ ui.info(`${dead} memor${dead === 1 ? "y" : "ies"} never read in window \u2014 candidates for cleanup (run \`haive doctor\`).`);
5325
+ }
5326
+ }
4689
5327
 
4690
5328
  // src/commands/bench.ts
4691
5329
  import { performance } from "perf_hooks";
4692
5330
  import "commander";
4693
5331
  import {
4694
5332
  estimateTokens,
4695
- findProjectRoot as findProjectRoot33,
4696
- resolveHaivePaths as resolveHaivePaths30
5333
+ findProjectRoot as findProjectRoot34,
5334
+ resolveHaivePaths as resolveHaivePaths31
4697
5335
  } from "@hiveai/core";
4698
5336
  import {
4699
5337
  antiPatternsCheck,
@@ -4705,8 +5343,8 @@ import {
4705
5343
  } from "@hiveai/mcp";
4706
5344
  function registerBench(program2) {
4707
5345
  program2.command("bench").description("Self-test the local hAIve setup: runs core MCP tools against this project and reports latency + payload size.").option("-t, --task <task>", "task description for ranking-aware tools", "audit dependencies for security risks").option("--json", "emit JSON instead of a table", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
4708
- const root = findProjectRoot33(opts.dir);
4709
- const paths = resolveHaivePaths30(root);
5346
+ const root = findProjectRoot34(opts.dir);
5347
+ const paths = resolveHaivePaths31(root);
4710
5348
  const ctx = { paths };
4711
5349
  const task = opts.task ?? "audit dependencies for security risks";
4712
5350
  const scenarios = [
@@ -4825,19 +5463,19 @@ function summarize(name, t0, payload, notes) {
4825
5463
  }
4826
5464
 
4827
5465
  // src/commands/memory-suggest.ts
4828
- import { mkdir as mkdir12, writeFile as writeFile18 } from "fs/promises";
4829
- import { existsSync as existsSync33 } from "fs";
4830
- import path32 from "path";
5466
+ import { mkdir as mkdir14, writeFile as writeFile19 } from "fs/promises";
5467
+ import { existsSync as existsSync35 } from "fs";
5468
+ import path34 from "path";
4831
5469
  import "commander";
4832
5470
  import {
4833
5471
  aggregateUsage as aggregateUsage2,
4834
5472
  buildFrontmatter as buildFrontmatter7,
4835
- findProjectRoot as findProjectRoot34,
5473
+ findProjectRoot as findProjectRoot35,
4836
5474
  loadMemoriesFromDir as loadMemoriesFromDir8,
4837
5475
  memoryFilePath as memoryFilePath6,
4838
5476
  parseSince as parseSince2,
4839
5477
  readUsageEvents as readUsageEvents2,
4840
- resolveHaivePaths as resolveHaivePaths31,
5478
+ resolveHaivePaths as resolveHaivePaths32,
4841
5479
  serializeMemory as serializeMemory14
4842
5480
  } from "@hiveai/core";
4843
5481
  var SEARCH_TOOLS = /* @__PURE__ */ new Set([
@@ -4850,8 +5488,8 @@ function registerMemorySuggest(memory2) {
4850
5488
  memory2.command("suggest").description(
4851
5489
  "Suggest memories to create based on recurring search queries in the usage log.\n\n Use --auto-save to draft the top-N suggestions as draft memories. They land\n in personal scope by default with status=draft, ready for you to edit and promote."
4852
5490
  ).option("--since <window>", "ISO date or relative (e.g. '7d', '24h')", "30d").option("--min <count>", "minimum repeat count to surface a query", "2").option("--top-n <n>", "with --auto-save, draft this many top suggestions", "3").option("--scope <scope>", "with --auto-save, scope of drafted memories (personal | team)", "personal").option("--auto-save", "draft top-N suggestions as draft memories on disk", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
4853
- const root = findProjectRoot34(opts.dir);
4854
- const paths = resolveHaivePaths31(root);
5491
+ const root = findProjectRoot35(opts.dir);
5492
+ const paths = resolveHaivePaths32(root);
4855
5493
  const events = await readUsageEvents2(paths);
4856
5494
  if (events.length === 0) {
4857
5495
  if (opts.json) {
@@ -4897,7 +5535,7 @@ function registerMemorySuggest(memory2) {
4897
5535
  }
4898
5536
  const created = [];
4899
5537
  const skipped = [];
4900
- const existing = existsSync33(paths.memoriesDir) ? await loadMemoriesFromDir8(paths.memoriesDir) : [];
5538
+ const existing = existsSync35(paths.memoriesDir) ? await loadMemoriesFromDir8(paths.memoriesDir) : [];
4901
5539
  for (const s of top) {
4902
5540
  const slug = slugify(s.query);
4903
5541
  if (!slug) {
@@ -4920,13 +5558,13 @@ function registerMemorySuggest(memory2) {
4920
5558
  fm.status = "draft";
4921
5559
  const body = renderTemplate(s);
4922
5560
  const file = memoryFilePath6(paths, fm.scope, fm.id, fm.module);
4923
- await mkdir12(path32.dirname(file), { recursive: true });
4924
- if (existsSync33(file)) {
4925
- skipped.push({ query: s.query, reason: `file already exists at ${path32.relative(root, file)}` });
5561
+ await mkdir14(path34.dirname(file), { recursive: true });
5562
+ if (existsSync35(file)) {
5563
+ skipped.push({ query: s.query, reason: `file already exists at ${path34.relative(root, file)}` });
4926
5564
  continue;
4927
5565
  }
4928
- await writeFile18(file, serializeMemory14({ frontmatter: fm, body }), "utf8");
4929
- created.push({ id: fm.id, file: path32.relative(root, file), query: s.query });
5566
+ await writeFile19(file, serializeMemory14({ frontmatter: fm, body }), "utf8");
5567
+ created.push({ id: fm.id, file: path34.relative(root, file), query: s.query });
4930
5568
  }
4931
5569
  if (opts.json) {
4932
5570
  console.log(JSON.stringify({ created, skipped }, null, 2));
@@ -4934,10 +5572,10 @@ function registerMemorySuggest(memory2) {
4934
5572
  }
4935
5573
  for (const c of created) {
4936
5574
  ui.success(`Drafted ${c.id} \u2192 ${c.file}`);
4937
- console.log(` ${ui.dim("from query:")} ${truncate(c.query, 60)}`);
5575
+ console.log(` ${ui.dim("from query:")} ${truncate2(c.query, 60)}`);
4938
5576
  }
4939
5577
  for (const s of skipped) {
4940
- ui.warn(`Skipped: ${truncate(s.query, 50)} \u2014 ${s.reason}`);
5578
+ ui.warn(`Skipped: ${truncate2(s.query, 50)} \u2014 ${s.reason}`);
4941
5579
  }
4942
5580
  if (created.length > 0) {
4943
5581
  console.log();
@@ -4961,7 +5599,7 @@ function registerMemorySuggest(memory2) {
4961
5599
  }
4962
5600
  for (const s of suggestions.slice(0, 30)) {
4963
5601
  console.log(
4964
- ` ${ui.bold(`\xD7${s.count}`)} ${ui.dim(`[${s.tools.join(",")}]`)} ${truncate(s.query, 70)}`
5602
+ ` ${ui.bold(`\xD7${s.count}`)} ${ui.dim(`[${s.tools.join(",")}]`)} ${truncate2(s.query, 70)}`
4965
5603
  );
4966
5604
  console.log(` ${ui.dim("\u2192")} ${s.reason}`);
4967
5605
  }
@@ -5007,28 +5645,28 @@ function renderTemplate(s) {
5007
5645
  `- **Why** \u2014 the rationale or root cause`,
5008
5646
  `- **How to apply** \u2014 what an agent should do when this comes up again`,
5009
5647
  ``,
5010
- `Then run \`haive memory promote ${truncate(s.query, 30)}\` to mark it validated.`
5648
+ `Then run \`haive memory promote ${truncate2(s.query, 30)}\` to mark it validated.`
5011
5649
  ].join("\n");
5012
5650
  }
5013
5651
  function slugify(s) {
5014
5652
  return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
5015
5653
  }
5016
- function truncate(text, max) {
5654
+ function truncate2(text, max) {
5017
5655
  if (text.length <= max) return text;
5018
5656
  return text.slice(0, max - 1) + "\u2026";
5019
5657
  }
5020
5658
 
5021
5659
  // src/commands/memory-archive.ts
5022
- import { existsSync as existsSync34 } from "fs";
5023
- import { writeFile as writeFile19 } from "fs/promises";
5024
- import path33 from "path";
5660
+ import { existsSync as existsSync36 } from "fs";
5661
+ import { writeFile as writeFile20 } from "fs/promises";
5662
+ import path35 from "path";
5025
5663
  import "commander";
5026
5664
  import {
5027
- findProjectRoot as findProjectRoot35,
5665
+ findProjectRoot as findProjectRoot36,
5028
5666
  getUsage as getUsage9,
5029
5667
  loadMemoriesFromDir as loadMemoriesFromDir9,
5030
- loadUsageIndex as loadUsageIndex11,
5031
- resolveHaivePaths as resolveHaivePaths32,
5668
+ loadUsageIndex as loadUsageIndex13,
5669
+ resolveHaivePaths as resolveHaivePaths33,
5032
5670
  serializeMemory as serializeMemory15
5033
5671
  } from "@hiveai/core";
5034
5672
  var MS_PER_DAY = 24 * 60 * 60 * 1e3;
@@ -5036,9 +5674,9 @@ function registerMemoryArchive(memory2) {
5036
5674
  memory2.command("archive").description(
5037
5675
  "Archive obsolete memories: marks status='deprecated' for memories not read in N days\n whose anchored paths have all disappeared (or have no anchor at all).\n\n Defaults to a DRY RUN \u2014 pass --apply to actually rewrite files.\n Targets `attempt` memories by default since they age the fastest.\n\n Recover later with `haive memory edit <id>` to set status back to validated."
5038
5676
  ).option("--since <window>", "minimum age since last read (e.g. '180d', '6m')", "180d").option("--type <type>", "limit to a memory type (default 'attempt'). Pass 'all' to scan all types.", "attempt").option("--apply", "actually rewrite files (default: dry run)", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
5039
- const root = findProjectRoot35(opts.dir);
5040
- const paths = resolveHaivePaths32(root);
5041
- if (!existsSync34(paths.memoriesDir)) {
5677
+ const root = findProjectRoot36(opts.dir);
5678
+ const paths = resolveHaivePaths33(root);
5679
+ if (!existsSync36(paths.memoriesDir)) {
5042
5680
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
5043
5681
  process.exitCode = 1;
5044
5682
  return;
@@ -5051,7 +5689,7 @@ function registerMemoryArchive(memory2) {
5051
5689
  }
5052
5690
  const cutoff = Date.now() - minDays * MS_PER_DAY;
5053
5691
  const all = await loadMemoriesFromDir9(paths.memoriesDir);
5054
- const usage = await loadUsageIndex11(paths);
5692
+ const usage = await loadUsageIndex13(paths);
5055
5693
  const typeFilter = opts.type === "all" ? null : opts.type ?? "attempt";
5056
5694
  const candidates = [];
5057
5695
  for (const { memory: mem, filePath } of all) {
@@ -5059,7 +5697,7 @@ function registerMemoryArchive(memory2) {
5059
5697
  if (typeFilter && fm.type !== typeFilter) continue;
5060
5698
  if (fm.status === "deprecated" || fm.status === "rejected") continue;
5061
5699
  const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
5062
- const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync34(path33.join(paths.root, p)));
5700
+ const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync36(path35.join(paths.root, p)));
5063
5701
  const isAnchorless = !hasAnyAnchor;
5064
5702
  if (!isAnchorless && !allPathsGone) continue;
5065
5703
  const u = getUsage9(usage, fm.id);
@@ -5107,7 +5745,7 @@ function registerMemoryArchive(memory2) {
5107
5745
  if (!found) continue;
5108
5746
  const fm = { ...found.memory.frontmatter, status: "deprecated" };
5109
5747
  try {
5110
- await writeFile19(c.filePath, serializeMemory15({ frontmatter: fm, body: found.memory.body }), "utf8");
5748
+ await writeFile20(c.filePath, serializeMemory15({ frontmatter: fm, body: found.memory.body }), "utf8");
5111
5749
  archived++;
5112
5750
  } catch (err) {
5113
5751
  if (!opts.json) {
@@ -5133,30 +5771,30 @@ function parseDays(input) {
5133
5771
  }
5134
5772
 
5135
5773
  // src/commands/doctor.ts
5136
- import { existsSync as existsSync35 } from "fs";
5774
+ import { existsSync as existsSync37 } from "fs";
5137
5775
  import { stat } from "fs/promises";
5138
5776
  import "path";
5139
5777
  import "commander";
5140
5778
  import {
5141
5779
  codeMapPath as codeMapPath2,
5142
- findProjectRoot as findProjectRoot36,
5780
+ findProjectRoot as findProjectRoot37,
5143
5781
  getUsage as getUsage10,
5144
5782
  loadCodeMap as loadCodeMap3,
5145
5783
  loadConfig as loadConfig4,
5146
5784
  loadMemoriesFromDir as loadMemoriesFromDir10,
5147
- loadUsageIndex as loadUsageIndex12,
5785
+ loadUsageIndex as loadUsageIndex14,
5148
5786
  readUsageEvents as readUsageEvents3,
5149
- resolveHaivePaths as resolveHaivePaths33
5787
+ resolveHaivePaths as resolveHaivePaths34
5150
5788
  } from "@hiveai/core";
5151
5789
  var MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
5152
5790
  function registerDoctor(program2) {
5153
5791
  program2.command("doctor").description(
5154
5792
  "Analyze the local hAIve setup and emit actionable recommendations.\n\n Inspects: project-context status, memory health (stale/anchorless/decay/pending),\n code-map freshness, usage log signals (low-hit briefings, repeated empty searches).\n\n Read-only by default. Pass --fix to suggest commands you can copy-paste."
5155
5793
  ).option("--json", "emit JSON instead of human-readable output", false).option("--fix", "include suggested fix commands in human output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
5156
- const root = findProjectRoot36(opts.dir);
5157
- const paths = resolveHaivePaths33(root);
5794
+ const root = findProjectRoot37(opts.dir);
5795
+ const paths = resolveHaivePaths34(root);
5158
5796
  const findings = [];
5159
- if (!existsSync35(paths.haiveDir)) {
5797
+ if (!existsSync37(paths.haiveDir)) {
5160
5798
  findings.push({
5161
5799
  severity: "error",
5162
5800
  code: "not-initialized",
@@ -5165,7 +5803,7 @@ function registerDoctor(program2) {
5165
5803
  });
5166
5804
  return emit(findings, opts);
5167
5805
  }
5168
- if (!existsSync35(paths.projectContext)) {
5806
+ if (!existsSync37(paths.projectContext)) {
5169
5807
  findings.push({
5170
5808
  severity: "warn",
5171
5809
  code: "no-project-context",
@@ -5173,8 +5811,8 @@ function registerDoctor(program2) {
5173
5811
  fix: "haive init"
5174
5812
  });
5175
5813
  } else {
5176
- const { readFile: readFile13 } = await import("fs/promises");
5177
- const content = await readFile13(paths.projectContext, "utf8");
5814
+ const { readFile: readFile15 } = await import("fs/promises");
5815
+ const content = await readFile15(paths.projectContext, "utf8");
5178
5816
  const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
5179
5817
  if (isTemplate) {
5180
5818
  findings.push({
@@ -5185,7 +5823,7 @@ function registerDoctor(program2) {
5185
5823
  });
5186
5824
  }
5187
5825
  }
5188
- const memories = existsSync35(paths.memoriesDir) ? await loadMemoriesFromDir10(paths.memoriesDir) : [];
5826
+ const memories = existsSync37(paths.memoriesDir) ? await loadMemoriesFromDir10(paths.memoriesDir) : [];
5189
5827
  const now = Date.now();
5190
5828
  if (memories.length === 0) {
5191
5829
  findings.push({
@@ -5194,7 +5832,7 @@ function registerDoctor(program2) {
5194
5832
  message: "No memories yet. Capture knowledge as agents work via mem_save / mem_observe / mem_tried."
5195
5833
  });
5196
5834
  } else {
5197
- const usage = await loadUsageIndex12(paths);
5835
+ const usage = await loadUsageIndex14(paths);
5198
5836
  const stale = memories.filter((m) => m.memory.frontmatter.status === "stale");
5199
5837
  if (stale.length > 0) {
5200
5838
  findings.push({
@@ -5339,22 +5977,22 @@ function isSearchTool(name) {
5339
5977
  }
5340
5978
 
5341
5979
  // src/commands/playback.ts
5342
- import { existsSync as existsSync36 } from "fs";
5980
+ import { existsSync as existsSync38 } from "fs";
5343
5981
  import "commander";
5344
5982
  import {
5345
- findProjectRoot as findProjectRoot37,
5983
+ findProjectRoot as findProjectRoot38,
5346
5984
  loadMemoriesFromDir as loadMemoriesFromDir11,
5347
5985
  parseSince as parseSince3,
5348
5986
  readUsageEvents as readUsageEvents4,
5349
- resolveHaivePaths as resolveHaivePaths34
5987
+ resolveHaivePaths as resolveHaivePaths35
5350
5988
  } from "@hiveai/core";
5351
5989
  var MS_PER_MINUTE = 6e4;
5352
5990
  function registerPlayback(program2) {
5353
5991
  program2.command("playback").description(
5354
5992
  "Replay past sessions from the usage log. For each session, show:\n - tool calls (what kind, how many)\n - briefing tasks asked\n - memories that have been created since then (that the session didn't have)\n\n Useful to ask 'would today's haive have helped past me on this task?'"
5355
5993
  ).option("--since <window>", "limit to events in this window (e.g. '7d')", "30d").option("--session-gap <minutes>", "minutes of inactivity that splits a session", "30").option("--limit <n>", "show at most this many sessions (newest first)", "10").option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
5356
- const root = findProjectRoot37(opts.dir);
5357
- const paths = resolveHaivePaths34(root);
5994
+ const root = findProjectRoot38(opts.dir);
5995
+ const paths = resolveHaivePaths35(root);
5358
5996
  const events = await readUsageEvents4(paths);
5359
5997
  if (events.length === 0) {
5360
5998
  if (opts.json) {
@@ -5369,7 +6007,7 @@ function registerPlayback(program2) {
5369
6007
  const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
5370
6008
  const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
5371
6009
  const sessions = bucketSessions(filtered, gapMs);
5372
- const all = existsSync36(paths.memoriesDir) ? await loadMemoriesFromDir11(paths.memoriesDir) : [];
6010
+ const all = existsSync38(paths.memoriesDir) ? await loadMemoriesFromDir11(paths.memoriesDir) : [];
5373
6011
  const memByCreatedAt = all.filter(({ memory: memory2 }) => memory2.frontmatter.type !== "session_recap").map(({ memory: memory2 }) => ({ id: memory2.frontmatter.id, at: Date.parse(memory2.frontmatter.created_at) })).sort((a, b) => a.at - b.at);
5374
6012
  const enriched = sessions.map((s, i) => {
5375
6013
  const startMs = Date.parse(s.start);
@@ -5409,7 +6047,7 @@ function registerPlayback(program2) {
5409
6047
  if (s.briefing_tasks.length > 0) {
5410
6048
  console.log(` ${ui.dim("briefings asked:")}`);
5411
6049
  for (const t of s.briefing_tasks) {
5412
- console.log(` \u2022 ${truncate2(t, 80)}`);
6050
+ console.log(` \u2022 ${truncate3(t, 80)}`);
5413
6051
  }
5414
6052
  }
5415
6053
  if (s.memories_created_since > 0) {
@@ -5450,7 +6088,7 @@ function countTools(events) {
5450
6088
  for (const e of events) out[e.tool] = (out[e.tool] ?? 0) + 1;
5451
6089
  return out;
5452
6090
  }
5453
- function truncate2(text, max) {
6091
+ function truncate3(text, max) {
5454
6092
  if (text.length <= max) return text;
5455
6093
  return text.slice(0, max - 1) + "\u2026";
5456
6094
  }
@@ -5459,8 +6097,8 @@ function truncate2(text, max) {
5459
6097
  import { spawn as spawn3 } from "child_process";
5460
6098
  import "commander";
5461
6099
  import {
5462
- findProjectRoot as findProjectRoot38,
5463
- resolveHaivePaths as resolveHaivePaths35
6100
+ findProjectRoot as findProjectRoot39,
6101
+ resolveHaivePaths as resolveHaivePaths36
5464
6102
  } from "@hiveai/core";
5465
6103
  import { preCommitCheck } from "@hiveai/mcp";
5466
6104
  function registerPrecommit(program2) {
@@ -5471,8 +6109,8 @@ function registerPrecommit(program2) {
5471
6109
  "'any' | 'high-confidence' (default) | 'never' (report only)",
5472
6110
  "high-confidence"
5473
6111
  ).option("--no-semantic", "disable semantic search in anti-patterns matching").option("--json", "emit JSON instead of human-readable output", false).option("--paths <paths...>", "explicit paths to check (skips git diff)").option("-d, --dir <dir>", "project root").action(async (opts) => {
5474
- const root = findProjectRoot38(opts.dir);
5475
- const paths = resolveHaivePaths35(root);
6112
+ const root = findProjectRoot39(opts.dir);
6113
+ const paths = resolveHaivePaths36(root);
5476
6114
  const ctx = { paths };
5477
6115
  let diff = "";
5478
6116
  let touchedPaths = opts.paths ?? [];
@@ -5564,8 +6202,8 @@ function runCommand(cmd, args, cwd) {
5564
6202
  }
5565
6203
 
5566
6204
  // src/index.ts
5567
- var program = new Command39();
5568
- program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.7.1");
6205
+ var program = new Command40();
6206
+ program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.8.0");
5569
6207
  registerInit(program);
5570
6208
  registerMcp(program);
5571
6209
  registerBriefing(program);
@@ -5573,6 +6211,7 @@ registerTui(program);
5573
6211
  registerEmbeddings(program);
5574
6212
  registerSync(program);
5575
6213
  registerInstallHooks(program);
6214
+ registerObserve(program);
5576
6215
  registerIndexCode(program);
5577
6216
  var memory = program.command("memory").description("Manage memory entries");
5578
6217
  registerMemoryAdd(memory);