@hiveai/cli 0.7.2 → 0.9.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
  });
@@ -759,6 +1035,13 @@ var HAIVE_MCP_ENTRY = {
759
1035
  command: "haive-mcp",
760
1036
  args: []
761
1037
  };
1038
+ function projectMcpEntry(root) {
1039
+ return {
1040
+ command: "haive-mcp",
1041
+ args: [],
1042
+ env: { HAIVE_PROJECT_ROOT: root }
1043
+ };
1044
+ }
762
1045
  function cursorMcpPath() {
763
1046
  return path5.join(HOME, ".cursor", "mcp.json");
764
1047
  }
@@ -877,6 +1160,61 @@ async function autoConfigureMcpClients() {
877
1160
  }
878
1161
  return results;
879
1162
  }
1163
+ async function configureProjectMcpClients(root) {
1164
+ const entry = projectMcpEntry(root);
1165
+ const results = [];
1166
+ try {
1167
+ const cursorPath = path5.join(root, ".cursor", "mcp.json");
1168
+ let config = {};
1169
+ if (existsSync4(cursorPath)) {
1170
+ try {
1171
+ config = JSON.parse(await readFile3(cursorPath, "utf8"));
1172
+ } catch {
1173
+ }
1174
+ }
1175
+ config.mcpServers ??= {};
1176
+ config.mcpServers["haive"] = entry;
1177
+ await mkdir(path5.dirname(cursorPath), { recursive: true });
1178
+ await writeFile(cursorPath, JSON.stringify(config, null, 2) + "\n", "utf8");
1179
+ results.push({ client: "Cursor (project)", status: "configured", path: cursorPath });
1180
+ } catch (err) {
1181
+ results.push({ client: "Cursor (project)", status: "error", error: String(err) });
1182
+ }
1183
+ try {
1184
+ const vscodePath = path5.join(root, ".vscode", "mcp.json");
1185
+ let config = {};
1186
+ if (existsSync4(vscodePath)) {
1187
+ try {
1188
+ config = JSON.parse(await readFile3(vscodePath, "utf8"));
1189
+ } catch {
1190
+ }
1191
+ }
1192
+ config.servers ??= {};
1193
+ config.servers["haive"] = { ...entry, type: "stdio" };
1194
+ await mkdir(path5.dirname(vscodePath), { recursive: true });
1195
+ await writeFile(vscodePath, JSON.stringify(config, null, 2) + "\n", "utf8");
1196
+ results.push({ client: "VS Code (workspace)", status: "configured", path: vscodePath });
1197
+ } catch (err) {
1198
+ results.push({ client: "VS Code (workspace)", status: "error", error: String(err) });
1199
+ }
1200
+ try {
1201
+ const mcpPath = path5.join(root, ".mcp.json");
1202
+ let config = {};
1203
+ if (existsSync4(mcpPath)) {
1204
+ try {
1205
+ config = JSON.parse(await readFile3(mcpPath, "utf8"));
1206
+ } catch {
1207
+ }
1208
+ }
1209
+ config.mcpServers ??= {};
1210
+ config.mcpServers["haive"] = { ...entry, type: "stdio" };
1211
+ await writeFile(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf8");
1212
+ results.push({ client: "Claude Code (project)", status: "configured", path: mcpPath });
1213
+ } catch (err) {
1214
+ results.push({ client: "Claude Code (project)", status: "error", error: String(err) });
1215
+ }
1216
+ return results;
1217
+ }
880
1218
 
881
1219
  // src/commands/init-stack-packs.ts
882
1220
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
@@ -1671,18 +2009,21 @@ jobs:
1671
2009
  `;
1672
2010
  function registerInit(program2) {
1673
2011
  program2.command("init").description(
1674
- "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."
2012
+ "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."
1675
2013
  ).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(
1676
2014
  "--manual",
1677
2015
  "opt out of autopilot: memories require manual approval, no auto-session recap, no auto-context"
1678
2016
  ).option(
1679
2017
  "--bootstrap",
1680
- "auto-generate .ai/project-context.md from package.json, README, and directory structure (no AI needed)"
2018
+ "auto-generate .ai/project-context.md from package.json, README, and directory structure (ON by default in autopilot)"
2019
+ ).option(
2020
+ "--no-bootstrap",
2021
+ "skip the project-context auto-generation (only the default template is written)"
1681
2022
  ).option(
1682
2023
  "--stack <stacks>",
1683
2024
  `pre-seed validated memory packs for the given stacks (comma-separated).
1684
2025
  Supported: ${SUPPORTED_STACKS.join(", ")}.
1685
- Use 'auto' to detect from package.json automatically.`
2026
+ Defaults to 'auto' in autopilot mode (detects from package.json). Pass 'none' to disable.`
1686
2027
  ).option(
1687
2028
  "--no-mcp-setup",
1688
2029
  "skip auto-configuring haive-mcp in Cursor / VS Code / Claude Code"
@@ -1690,6 +2031,8 @@ function registerInit(program2) {
1690
2031
  const root = path7.resolve(opts.dir);
1691
2032
  const paths = resolveHaivePaths4(root);
1692
2033
  const autopilot = opts.manual !== true;
2034
+ const wantBootstrap = opts.bootstrap === void 0 ? autopilot : opts.bootstrap;
2035
+ const wantStack = opts.stack === void 0 ? autopilot ? "auto" : void 0 : opts.stack === "none" ? void 0 : opts.stack;
1693
2036
  if (existsSync6(paths.haiveDir)) {
1694
2037
  ui.warn(`.ai/ already exists at ${paths.haiveDir} \u2014 leaving existing files in place.`);
1695
2038
  }
@@ -1698,7 +2041,7 @@ function registerInit(program2) {
1698
2041
  await mkdir3(paths.moduleDir, { recursive: true });
1699
2042
  await mkdir3(paths.modulesContextDir, { recursive: true });
1700
2043
  if (!existsSync6(paths.projectContext)) {
1701
- if (opts.bootstrap) {
2044
+ if (wantBootstrap) {
1702
2045
  ui.info("Bootstrapping project context from local files\u2026");
1703
2046
  try {
1704
2047
  const context = await generateBootstrapContext(root);
@@ -1727,7 +2070,7 @@ function registerInit(program2) {
1727
2070
  await writeBridge(root, ".cursorrules");
1728
2071
  await writeBridge(root, path7.join(".github", "copilot-instructions.md"));
1729
2072
  }
1730
- const stacksToSeed = await resolveStacksToSeed(root, opts.stack);
2073
+ const stacksToSeed = await resolveStacksToSeed(root, wantStack);
1731
2074
  if (stacksToSeed.length > 0) {
1732
2075
  let totalSeeded = 0;
1733
2076
  for (const stack of stacksToSeed) {
@@ -1792,7 +2135,18 @@ function registerInit(program2) {
1792
2135
  "No supported AI client detected (Cursor, VS Code, Claude Code, Windsurf).\n Configure manually: add haive-mcp to your client's MCP config.\n See: https://github.com/Doucs91/hAIve#mcp-setup"
1793
2136
  );
1794
2137
  }
1795
- if (configured.length > 0) {
2138
+ const projectMcpResults = await configureProjectMcpClients(root);
2139
+ for (const r of projectMcpResults) {
2140
+ if (r.status === "configured") {
2141
+ ui.success(`haive-mcp project config written (${path7.relative(root, r.path)})`);
2142
+ }
2143
+ }
2144
+ await ensureGitignoreEntries(root, [
2145
+ ".cursor/mcp.json",
2146
+ ".vscode/mcp.json",
2147
+ ".mcp.json"
2148
+ ]);
2149
+ if (configured.length > 0 || projectMcpResults.some((r) => r.status === "configured")) {
1796
2150
  ui.info(
1797
2151
  ui.dim(" \u2192 Restart your AI client for MCP changes to take effect.")
1798
2152
  );
@@ -1867,13 +2221,123 @@ async function writeBridge(root, relPath) {
1867
2221
  await writeFile3(target, BRIDGE_BODY, "utf8");
1868
2222
  ui.success(`Created bridge ${relPath}`);
1869
2223
  }
2224
+ async function ensureGitignoreEntries(root, patterns) {
2225
+ try {
2226
+ const gitignorePath = path7.join(root, ".gitignore");
2227
+ let existing = "";
2228
+ if (existsSync6(gitignorePath)) {
2229
+ existing = await readFile4(gitignorePath, "utf8");
2230
+ }
2231
+ const lines = existing.split("\n");
2232
+ const missing = patterns.filter((p) => !lines.some((l) => l.trim() === p));
2233
+ if (missing.length === 0) return;
2234
+ const toAppend = (existing.endsWith("\n") || existing === "" ? "" : "\n") + "# hAIve project-level MCP configs (machine-specific absolute paths)\n" + missing.join("\n") + "\n";
2235
+ await writeFile3(gitignorePath, existing + toAppend, "utf8");
2236
+ } catch {
2237
+ }
2238
+ }
1870
2239
 
1871
2240
  // src/commands/install-hooks.ts
1872
- import { mkdir as mkdir4, writeFile as writeFile4, chmod, readFile as readFile5 } from "fs/promises";
1873
- import { existsSync as existsSync7 } from "fs";
1874
- import path8 from "path";
2241
+ import { mkdir as mkdir5, writeFile as writeFile5, chmod, readFile as readFile6 } from "fs/promises";
2242
+ import { existsSync as existsSync8 } from "fs";
2243
+ import path9 from "path";
1875
2244
  import "commander";
1876
2245
  import { findProjectRoot as findProjectRoot6 } from "@hiveai/core";
2246
+
2247
+ // src/utils/claude-hooks.ts
2248
+ import { existsSync as existsSync7 } from "fs";
2249
+ import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
2250
+ import path8 from "path";
2251
+ var HAIVE_HOOK_TAG = "haive-passive-capture";
2252
+ var POST_TOOL_USE_GROUP = {
2253
+ matcher: "Edit|Write|Bash",
2254
+ hooks: [
2255
+ {
2256
+ type: "command",
2257
+ command: "haive observe",
2258
+ haive_tag: HAIVE_HOOK_TAG
2259
+ }
2260
+ ]
2261
+ };
2262
+ var SESSION_END_GROUP = {
2263
+ hooks: [
2264
+ {
2265
+ type: "command",
2266
+ command: "haive session end --quiet --auto",
2267
+ haive_tag: HAIVE_HOOK_TAG
2268
+ }
2269
+ ]
2270
+ };
2271
+ function dropHaiveGroups(groups) {
2272
+ return groups.filter(
2273
+ (g) => !g.hooks.some((h) => h.haive_tag === HAIVE_HOOK_TAG)
2274
+ );
2275
+ }
2276
+ function patchClaudeSettings(input) {
2277
+ const settings = input ? { ...input } : {};
2278
+ const hooks = settings.hooks ? { ...settings.hooks } : {};
2279
+ hooks.PostToolUse = [
2280
+ ...dropHaiveGroups(hooks.PostToolUse ?? []),
2281
+ POST_TOOL_USE_GROUP
2282
+ ];
2283
+ hooks.SessionEnd = [
2284
+ ...dropHaiveGroups(hooks.SessionEnd ?? []),
2285
+ SESSION_END_GROUP
2286
+ ];
2287
+ settings.hooks = hooks;
2288
+ return settings;
2289
+ }
2290
+ function unpatchClaudeSettings(input) {
2291
+ const settings = input ? { ...input } : {};
2292
+ if (!settings.hooks) return settings;
2293
+ const hooks = { ...settings.hooks };
2294
+ for (const [event, groups] of Object.entries(hooks)) {
2295
+ const cleaned = dropHaiveGroups(groups);
2296
+ if (cleaned.length === 0) {
2297
+ delete hooks[event];
2298
+ } else {
2299
+ hooks[event] = cleaned;
2300
+ }
2301
+ }
2302
+ settings.hooks = hooks;
2303
+ if (Object.keys(hooks).length === 0) delete settings.hooks;
2304
+ return settings;
2305
+ }
2306
+ async function installClaudeHooksAtPath(settingsPath) {
2307
+ let raw = null;
2308
+ let created = false;
2309
+ if (existsSync7(settingsPath)) {
2310
+ try {
2311
+ raw = JSON.parse(await readFile5(settingsPath, "utf8"));
2312
+ } catch {
2313
+ throw new Error(`${settingsPath} exists but is not valid JSON. Fix it manually first.`);
2314
+ }
2315
+ } else {
2316
+ created = true;
2317
+ }
2318
+ const patched = patchClaudeSettings(raw);
2319
+ await mkdir4(path8.dirname(settingsPath), { recursive: true });
2320
+ await writeFile4(settingsPath, JSON.stringify(patched, null, 2) + "\n", "utf8");
2321
+ return { settingsPath, created };
2322
+ }
2323
+ async function uninstallClaudeHooksAtPath(settingsPath) {
2324
+ if (!existsSync7(settingsPath)) {
2325
+ return { settingsPath, created: false };
2326
+ }
2327
+ const raw = JSON.parse(await readFile5(settingsPath, "utf8"));
2328
+ const cleaned = unpatchClaudeSettings(raw);
2329
+ await writeFile4(settingsPath, JSON.stringify(cleaned, null, 2) + "\n", "utf8");
2330
+ return { settingsPath, created: false };
2331
+ }
2332
+ function defaultClaudeSettingsPath(scope, projectRoot) {
2333
+ if (scope === "user") {
2334
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
2335
+ return path8.join(home, ".claude", "settings.json");
2336
+ }
2337
+ return path8.join(projectRoot, ".claude", "settings.local.json");
2338
+ }
2339
+
2340
+ // src/commands/install-hooks.ts
1877
2341
  var HOOK_MARKER = "# hAIve auto-generated";
1878
2342
  var POST_MERGE_BODY = `#!/bin/sh
1879
2343
  ${HOOK_MARKER} \u2014 keep this block to allow upgrades. Hand-edit anything outside it.
@@ -1922,50 +2386,192 @@ var HOOKS = [
1922
2386
  { name: "post-rewrite", body: POST_MERGE_BODY },
1923
2387
  { name: "pre-push", body: PRE_PUSH_BODY }
1924
2388
  ];
2389
+ async function installGitHooks(opts) {
2390
+ const root = findProjectRoot6(opts.dir);
2391
+ const gitDir = path9.join(root, ".git");
2392
+ if (!existsSync8(gitDir)) {
2393
+ ui.error(`No .git directory at ${root}.`);
2394
+ process.exitCode = 1;
2395
+ return;
2396
+ }
2397
+ const hooksDir = path9.join(gitDir, "hooks");
2398
+ await mkdir5(hooksDir, { recursive: true });
2399
+ let installed = 0;
2400
+ let skipped = 0;
2401
+ for (const { name, body } of HOOKS) {
2402
+ const file = path9.join(hooksDir, name);
2403
+ if (existsSync8(file) && !opts.force) {
2404
+ const existing = await readFile6(file, "utf8");
2405
+ if (!existing.includes(HOOK_MARKER)) {
2406
+ ui.warn(`${name} already exists and was not written by hAIve. Re-run with --force to overwrite.`);
2407
+ skipped++;
2408
+ continue;
2409
+ }
2410
+ }
2411
+ await writeFile5(file, body, "utf8");
2412
+ await chmod(file, 493);
2413
+ installed++;
2414
+ }
2415
+ ui.success(`Installed ${installed} git hook(s) in .git/hooks/${skipped ? `, skipped ${skipped}` : ""}`);
2416
+ ui.info("post-merge: haive sync runs after every pull/merge.");
2417
+ ui.info("pre-push: haive precommit runs before every push (advisory, never blocks).");
2418
+ ui.info(" Set HAIVE_BLOCK=1 in your shell to make pre-push blocking.");
2419
+ }
2420
+ async function installClaudeHooks(opts) {
2421
+ const root = findProjectRoot6(opts.dir);
2422
+ const scope = opts.scope ?? "user";
2423
+ const settingsPath = opts.settings ?? defaultClaudeSettingsPath(scope, root);
2424
+ if (opts.uninstall) {
2425
+ const result = await uninstallClaudeHooksAtPath(settingsPath);
2426
+ ui.success(`Removed hAIve hooks from ${result.settingsPath}`);
2427
+ return;
2428
+ }
2429
+ try {
2430
+ const result = await installClaudeHooksAtPath(settingsPath);
2431
+ if (result.created) {
2432
+ ui.success(`Created ${result.settingsPath} with hAIve passive-capture hooks`);
2433
+ } else {
2434
+ ui.success(`Patched ${result.settingsPath} (existing user hooks preserved)`);
2435
+ }
2436
+ } catch (err) {
2437
+ ui.error(err instanceof Error ? err.message : String(err));
2438
+ process.exitCode = 1;
2439
+ return;
2440
+ }
2441
+ ui.info("PostToolUse hook: `haive observe` runs after every Edit/Write/Bash");
2442
+ ui.info(" (appends a JSON line to .ai/.cache/observations.jsonl)");
2443
+ ui.info("SessionEnd hook: `haive session end --auto --quiet` distills observations");
2444
+ ui.info(" into a session_recap memory at session close");
2445
+ ui.info("Restart Claude Code (or open a new conversation) for the hooks to take effect.");
2446
+ ui.info(`Run \`haive install-hooks claude --uninstall\` to remove.`);
2447
+ }
1925
2448
  function registerInstallHooks(program2) {
1926
- program2.command("install-hooks").description(
1927
- "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"
1928
- ).option("-d, --dir <dir>", "project root").option("--force", "overwrite existing hooks").action(async (opts) => {
1929
- const root = findProjectRoot6(opts.dir);
1930
- const gitDir = path8.join(root, ".git");
1931
- if (!existsSync7(gitDir)) {
1932
- ui.error(`No .git directory at ${root}.`);
2449
+ program2.command("install-hooks [target]").description(
2450
+ "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"
2451
+ ).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) => {
2452
+ const t = (target ?? "git").toLowerCase();
2453
+ if (t === "git") {
2454
+ await installGitHooks(opts);
2455
+ } else if (t === "claude") {
2456
+ await installClaudeHooks(opts);
2457
+ } else {
2458
+ ui.error(`Unknown target: ${target}. Available: git, claude`);
1933
2459
  process.exitCode = 1;
1934
- return;
1935
2460
  }
1936
- const hooksDir = path8.join(gitDir, "hooks");
1937
- await mkdir4(hooksDir, { recursive: true });
1938
- let installed = 0;
1939
- let skipped = 0;
1940
- for (const { name, body } of HOOKS) {
1941
- const file = path8.join(hooksDir, name);
1942
- if (existsSync7(file) && !opts.force) {
1943
- const existing = await readFile5(file, "utf8");
1944
- if (!existing.includes(HOOK_MARKER)) {
1945
- ui.warn(`${name} already exists and was not written by hAIve. Re-run with --force to overwrite.`);
1946
- skipped++;
1947
- continue;
1948
- }
2461
+ });
2462
+ }
2463
+
2464
+ // src/commands/observe.ts
2465
+ import { appendFile, mkdir as mkdir6 } from "fs/promises";
2466
+ import { existsSync as existsSync9 } from "fs";
2467
+ import path10 from "path";
2468
+ import "commander";
2469
+ import { findProjectRoot as findProjectRoot7, resolveHaivePaths as resolveHaivePaths5 } from "@hiveai/core";
2470
+ var MAX_STDIN_BYTES = 256 * 1024;
2471
+ var TRUNCATE_FIELD = 800;
2472
+ function truncate(s, max = TRUNCATE_FIELD) {
2473
+ if (s == null) return "";
2474
+ const str = typeof s === "string" ? s : JSON.stringify(s);
2475
+ return str.length > max ? str.slice(0, max) + "\u2026" : str;
2476
+ }
2477
+ function extractFiles(payload) {
2478
+ const files = /* @__PURE__ */ new Set();
2479
+ const input = payload.tool_input ?? {};
2480
+ for (const k of ["file_path", "path", "notebook_path"]) {
2481
+ const v = input[k];
2482
+ if (typeof v === "string") files.add(v);
2483
+ }
2484
+ const cmd = input["command"];
2485
+ if (typeof cmd === "string") {
2486
+ 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);
2487
+ if (matches) for (const m of matches) files.add(m);
2488
+ }
2489
+ return [...files].slice(0, 8);
2490
+ }
2491
+ function buildSummary(payload) {
2492
+ const tool = payload.tool_name ?? "?";
2493
+ const input = payload.tool_input ?? {};
2494
+ if (tool === "Bash") return `Bash: ${truncate(input["command"], 200)}`;
2495
+ if (tool === "Edit") return `Edit ${truncate(input["file_path"], 200)}`;
2496
+ if (tool === "Write") return `Write ${truncate(input["file_path"], 200)}`;
2497
+ return `${tool}: ${truncate(input, 200)}`;
2498
+ }
2499
+ async function readStdin(maxBytes) {
2500
+ if (process.stdin.isTTY) return "";
2501
+ return await new Promise((resolve) => {
2502
+ const chunks = [];
2503
+ let total = 0;
2504
+ let done = false;
2505
+ const finish = () => {
2506
+ if (done) return;
2507
+ done = true;
2508
+ resolve(Buffer.concat(chunks).toString("utf8"));
2509
+ };
2510
+ process.stdin.on("data", (c) => {
2511
+ total += c.length;
2512
+ if (total > maxBytes) {
2513
+ process.stdin.destroy();
2514
+ finish();
2515
+ return;
2516
+ }
2517
+ chunks.push(c);
2518
+ });
2519
+ process.stdin.on("end", finish);
2520
+ process.stdin.on("error", finish);
2521
+ setTimeout(finish, 2e3);
2522
+ });
2523
+ }
2524
+ function registerObserve(program2) {
2525
+ program2.command("observe").description(
2526
+ "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`."
2527
+ ).option("-d, --dir <dir>", "project root").action(async (opts) => {
2528
+ try {
2529
+ const raw = await readStdin(MAX_STDIN_BYTES);
2530
+ if (!raw.trim()) return;
2531
+ let payload;
2532
+ try {
2533
+ payload = JSON.parse(raw);
2534
+ } catch {
2535
+ return;
1949
2536
  }
1950
- await writeFile4(file, body, "utf8");
1951
- await chmod(file, 493);
1952
- installed++;
1953
- }
1954
- ui.success(`Installed ${installed} hook(s) in .git/hooks/${skipped ? `, skipped ${skipped}` : ""}`);
1955
- ui.info("post-merge: haive sync runs after every pull/merge.");
1956
- ui.info("pre-push: haive precommit runs before every push (advisory, never blocks).");
1957
- ui.info(" Set HAIVE_BLOCK=1 in your shell to make pre-push blocking.");
2537
+ const root = (() => {
2538
+ try {
2539
+ return findProjectRoot7(opts.dir ?? payload.cwd);
2540
+ } catch {
2541
+ return null;
2542
+ }
2543
+ })();
2544
+ if (!root) return;
2545
+ const paths = resolveHaivePaths5(root);
2546
+ if (!existsSync9(paths.haiveDir)) return;
2547
+ const observation = {
2548
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2549
+ session_id: payload.session_id,
2550
+ cwd: payload.cwd,
2551
+ tool: payload.tool_name ?? "?",
2552
+ summary: buildSummary(payload),
2553
+ files: extractFiles(payload)
2554
+ };
2555
+ const cacheDir = path10.join(paths.haiveDir, ".cache");
2556
+ await mkdir6(cacheDir, { recursive: true });
2557
+ await appendFile(
2558
+ path10.join(cacheDir, "observations.jsonl"),
2559
+ JSON.stringify(observation) + "\n",
2560
+ "utf8"
2561
+ );
2562
+ } catch {
2563
+ }
1958
2564
  });
1959
2565
  }
1960
2566
 
1961
2567
  // src/commands/mcp.ts
1962
2568
  import { spawn } from "child_process";
1963
- import { existsSync as existsSync8 } from "fs";
2569
+ import { existsSync as existsSync10 } from "fs";
1964
2570
  import { createRequire } from "module";
1965
- import path9 from "path";
2571
+ import path11 from "path";
1966
2572
  import { fileURLToPath } from "url";
1967
2573
  import "commander";
1968
- import { findProjectRoot as findProjectRoot7 } from "@hiveai/core";
2574
+ import { findProjectRoot as findProjectRoot8 } from "@hiveai/core";
1969
2575
  var require2 = createRequire(import.meta.url);
1970
2576
  function registerMcp(program2) {
1971
2577
  program2.command("mcp").description(
@@ -1982,7 +2588,7 @@ function registerMcp(program2) {
1982
2588
  VS Code: code --add-mcp '{"name":"haive","command":"haive-mcp",...}'
1983
2589
  `
1984
2590
  ).option("-d, --dir <dir>", "project root (defaults to nearest .ai/ or .git/)").action((opts) => {
1985
- const root = findProjectRoot7(opts.dir);
2591
+ const root = findProjectRoot8(opts.dir);
1986
2592
  const bin = locateMcpBin();
1987
2593
  if (!bin) {
1988
2594
  ui.error(
@@ -2000,36 +2606,36 @@ function registerMcp(program2) {
2000
2606
  function locateMcpBin() {
2001
2607
  try {
2002
2608
  const pkgPath = require2.resolve("@hiveai/mcp/package.json");
2003
- const pkgDir = path9.dirname(pkgPath);
2004
- const candidate = path9.join(pkgDir, "dist", "index.js");
2005
- if (existsSync8(candidate)) return candidate;
2609
+ const pkgDir = path11.dirname(pkgPath);
2610
+ const candidate = path11.join(pkgDir, "dist", "index.js");
2611
+ if (existsSync10(candidate)) return candidate;
2006
2612
  } catch {
2007
2613
  }
2008
- const here = path9.dirname(fileURLToPath(import.meta.url));
2009
- const sibling = path9.resolve(here, "..", "..", "..", "mcp", "dist", "index.js");
2010
- if (existsSync8(sibling)) return sibling;
2614
+ const here = path11.dirname(fileURLToPath(import.meta.url));
2615
+ const sibling = path11.resolve(here, "..", "..", "..", "mcp", "dist", "index.js");
2616
+ if (existsSync10(sibling)) return sibling;
2011
2617
  return null;
2012
2618
  }
2013
2619
 
2014
2620
  // src/commands/sync.ts
2015
2621
  import { spawnSync as spawnSync2 } from "child_process";
2016
- import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
2017
- import { existsSync as existsSync9 } from "fs";
2018
- import path10 from "path";
2622
+ import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7 } from "fs/promises";
2623
+ import { existsSync as existsSync11 } from "fs";
2624
+ import path12 from "path";
2019
2625
  import "commander";
2020
2626
  import {
2021
2627
  DEFAULT_AUTO_PROMOTE_RULE,
2022
2628
  buildFrontmatter as buildFrontmatter2,
2023
- findProjectRoot as findProjectRoot8,
2629
+ findProjectRoot as findProjectRoot9,
2024
2630
  getUsage,
2025
2631
  isAutoPromoteEligible,
2026
2632
  isDecaying,
2027
2633
  loadCodeMap as loadCodeMap2,
2028
2634
  loadConfig,
2029
2635
  loadMemoriesFromDir as loadMemoriesFromDir2,
2030
- loadUsageIndex,
2636
+ loadUsageIndex as loadUsageIndex2,
2031
2637
  pullCrossRepoSources,
2032
- resolveHaivePaths as resolveHaivePaths5,
2638
+ resolveHaivePaths as resolveHaivePaths6,
2033
2639
  resolveManifestFiles,
2034
2640
  serializeMemory as serializeMemory2,
2035
2641
  trackDependencies,
@@ -2048,9 +2654,9 @@ function registerSync(program2) {
2048
2654
  "--inject-bridge",
2049
2655
  "inject top validated memories into CLAUDE.md (or --bridge-file) between <!-- haive:memories-start/end --> markers"
2050
2656
  ).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) => {
2051
- const root = findProjectRoot8(opts.dir);
2052
- const paths = resolveHaivePaths5(root);
2053
- if (!existsSync9(paths.memoriesDir)) {
2657
+ const root = findProjectRoot9(opts.dir);
2658
+ const paths = resolveHaivePaths6(root);
2659
+ if (!existsSync11(paths.memoriesDir)) {
2054
2660
  if (!opts.quiet) ui.warn(`No .ai/memories at ${root}. Run \`haive init\` first.`);
2055
2661
  process.exitCode = 1;
2056
2662
  return;
@@ -2070,7 +2676,7 @@ function registerSync(program2) {
2070
2676
  for (const { memory: memory2, filePath } of memories) {
2071
2677
  if (memory2.frontmatter.type === "session_recap") {
2072
2678
  if (memory2.frontmatter.status === "stale") {
2073
- await writeFile5(
2679
+ await writeFile6(
2074
2680
  filePath,
2075
2681
  serializeMemory2({
2076
2682
  frontmatter: {
@@ -2093,7 +2699,7 @@ function registerSync(program2) {
2093
2699
  const verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
2094
2700
  if (result.stale) {
2095
2701
  if (memory2.frontmatter.status !== "stale") {
2096
- await writeFile5(
2702
+ await writeFile6(
2097
2703
  filePath,
2098
2704
  serializeMemory2({
2099
2705
  frontmatter: {
@@ -2109,7 +2715,7 @@ function registerSync(program2) {
2109
2715
  staleMarked++;
2110
2716
  }
2111
2717
  } else if (memory2.frontmatter.status === "stale") {
2112
- await writeFile5(
2718
+ await writeFile6(
2113
2719
  filePath,
2114
2720
  serializeMemory2({
2115
2721
  frontmatter: {
@@ -2128,7 +2734,7 @@ function registerSync(program2) {
2128
2734
  }
2129
2735
  if (opts.promote !== false) {
2130
2736
  const memories = await loadMemoriesFromDir2(paths.memoriesDir);
2131
- const usage = await loadUsageIndex(paths);
2737
+ const usage = await loadUsageIndex2(paths);
2132
2738
  const nowMs = Date.now();
2133
2739
  for (const { memory: memory2, filePath } of memories) {
2134
2740
  const fm = memory2.frontmatter;
@@ -2137,7 +2743,7 @@ function registerSync(program2) {
2137
2743
  minReads: autoPromoteMinReads,
2138
2744
  maxRejections: DEFAULT_AUTO_PROMOTE_RULE.maxRejections
2139
2745
  })) {
2140
- await writeFile5(
2746
+ await writeFile6(
2141
2747
  filePath,
2142
2748
  serializeMemory2({ frontmatter: { ...fm, status: "validated" }, body: memory2.body }),
2143
2749
  "utf8"
@@ -2148,7 +2754,7 @@ function registerSync(program2) {
2148
2754
  if (autoApproveDelayHours !== null && fm.status === "proposed" && fm.scope === "team") {
2149
2755
  const ageHours = (nowMs - new Date(fm.created_at).getTime()) / (1e3 * 60 * 60);
2150
2756
  if (ageHours >= autoApproveDelayHours) {
2151
- await writeFile5(
2757
+ await writeFile6(
2152
2758
  filePath,
2153
2759
  serializeMemory2({
2154
2760
  frontmatter: {
@@ -2182,7 +2788,7 @@ function registerSync(program2) {
2182
2788
  );
2183
2789
  }
2184
2790
  if (opts.injectBridge) {
2185
- const bridgeFile = opts.bridgeFile ? path10.resolve(opts.bridgeFile) : path10.join(root, "CLAUDE.md");
2791
+ const bridgeFile = opts.bridgeFile ? path12.resolve(opts.bridgeFile) : path12.join(root, "CLAUDE.md");
2186
2792
  const maxInject = Math.max(1, Number(opts.bridgeMaxMemories ?? 5));
2187
2793
  await injectBridge(bridgeFile, paths.memoriesDir, maxInject, root, opts.quiet);
2188
2794
  }
@@ -2202,7 +2808,7 @@ function registerSync(program2) {
2202
2808
  }
2203
2809
  if (!opts.quiet) {
2204
2810
  const allForDecay = await loadMemoriesFromDir2(paths.memoriesDir);
2205
- const usageForDecay = await loadUsageIndex(paths);
2811
+ const usageForDecay = await loadUsageIndex2(paths);
2206
2812
  const decaying = allForDecay.filter(({ memory: memory2 }) => {
2207
2813
  const fm = memory2.frontmatter;
2208
2814
  if (fm.status === "rejected" || fm.status === "deprecated" || fm.status === "stale") return false;
@@ -2288,10 +2894,10 @@ Attends une **confirmation explicite** avant d'agir.
2288
2894
  paths: [result.file],
2289
2895
  topic: `dep-bump-${slugParts}`
2290
2896
  });
2291
- const teamDir = path10.join(paths.memoriesDir, "team");
2292
- await mkdir5(teamDir, { recursive: true });
2293
- await writeFile5(
2294
- path10.join(teamDir, `${fm.id}.md`),
2897
+ const teamDir = path12.join(paths.memoriesDir, "team");
2898
+ await mkdir7(teamDir, { recursive: true });
2899
+ await writeFile6(
2900
+ path12.join(teamDir, `${fm.id}.md`),
2295
2901
  serializeMemory2({ frontmatter: { ...fm, requires_human_approval: true }, body }),
2296
2902
  "utf8"
2297
2903
  );
@@ -2355,10 +2961,10 @@ Attends une **confirmation explicite** avant d'agir.
2355
2961
  paths: [diff.file],
2356
2962
  topic: `contract-breaking-${diff.contract}`
2357
2963
  });
2358
- const teamDir = path10.join(paths.memoriesDir, "team");
2359
- await mkdir5(teamDir, { recursive: true });
2360
- await writeFile5(
2361
- path10.join(teamDir, `${fm.id}.md`),
2964
+ const teamDir = path12.join(paths.memoriesDir, "team");
2965
+ await mkdir7(teamDir, { recursive: true });
2966
+ await writeFile6(
2967
+ path12.join(teamDir, `${fm.id}.md`),
2362
2968
  serializeMemory2({ frontmatter: { ...fm, requires_human_approval: true }, body }),
2363
2969
  "utf8"
2364
2970
  );
@@ -2419,7 +3025,7 @@ Attends une **confirmation explicite** avant d'agir.
2419
3025
  });
2420
3026
  }
2421
3027
  async function injectBridge(bridgeFile, memoriesDir, maxMemories, root, quiet) {
2422
- if (!existsSync9(memoriesDir)) return;
3028
+ if (!existsSync11(memoriesDir)) return;
2423
3029
  const all = await loadMemoriesFromDir2(memoriesDir);
2424
3030
  const top = all.filter(({ memory: memory2 }) => {
2425
3031
  const s = memory2.frontmatter.status;
@@ -2444,17 +3050,17 @@ ${m.memory.body.trim()}`;
2444
3050
  ` + block + `
2445
3051
 
2446
3052
  ${BRIDGE_END}`;
2447
- const fileExists = existsSync9(bridgeFile);
2448
- let existing = fileExists ? await readFile6(bridgeFile, "utf8") : "";
3053
+ const fileExists = existsSync11(bridgeFile);
3054
+ let existing = fileExists ? await readFile7(bridgeFile, "utf8") : "";
2449
3055
  existing = existing.replace(/\r\n/g, "\n");
2450
3056
  const startIdx = existing.indexOf(BRIDGE_START);
2451
3057
  const endIdx = existing.indexOf(BRIDGE_END);
2452
3058
  if (startIdx !== -1 && endIdx === -1) {
2453
- ui.warn(`${path10.relative(root, bridgeFile)}: found ${BRIDGE_START} without ${BRIDGE_END}. Fix the file manually before running --inject-bridge.`);
3059
+ ui.warn(`${path12.relative(root, bridgeFile)}: found ${BRIDGE_START} without ${BRIDGE_END}. Fix the file manually before running --inject-bridge.`);
2454
3060
  return;
2455
3061
  }
2456
3062
  if (startIdx === -1 && endIdx !== -1) {
2457
- ui.warn(`${path10.relative(root, bridgeFile)}: found ${BRIDGE_END} without ${BRIDGE_START}. Fix the file manually before running --inject-bridge.`);
3063
+ ui.warn(`${path12.relative(root, bridgeFile)}: found ${BRIDGE_END} without ${BRIDGE_START}. Fix the file manually before running --inject-bridge.`);
2458
3064
  return;
2459
3065
  }
2460
3066
  let updated;
@@ -2462,14 +3068,14 @@ ${BRIDGE_END}`;
2462
3068
  updated = existing.slice(0, startIdx) + injected + existing.slice(endIdx + BRIDGE_END.length);
2463
3069
  } else {
2464
3070
  if (!fileExists && !quiet) {
2465
- ui.info(`Creating ${path10.relative(root, bridgeFile)} with haive memory block.`);
3071
+ ui.info(`Creating ${path12.relative(root, bridgeFile)} with haive memory block.`);
2466
3072
  }
2467
3073
  updated = existing + (existing.endsWith("\n") ? "" : "\n") + "\n" + injected + "\n";
2468
3074
  }
2469
- await writeFile5(bridgeFile, updated, "utf8");
3075
+ await writeFile6(bridgeFile, updated, "utf8");
2470
3076
  if (!quiet) {
2471
3077
  console.log(
2472
- ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${path10.relative(root, bridgeFile)}`)
3078
+ ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${path12.relative(root, bridgeFile)}`)
2473
3079
  );
2474
3080
  }
2475
3081
  }
@@ -2494,17 +3100,17 @@ function collectSinceChanges(root, ref) {
2494
3100
 
2495
3101
  // src/commands/memory-add.ts
2496
3102
  import { createHash } from "crypto";
2497
- import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
2498
- import { existsSync as existsSync10 } from "fs";
2499
- import path11 from "path";
3103
+ import { mkdir as mkdir8, readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
3104
+ import { existsSync as existsSync12 } from "fs";
3105
+ import path13 from "path";
2500
3106
  import "commander";
2501
3107
  import {
2502
3108
  buildFrontmatter as buildFrontmatter3,
2503
- findProjectRoot as findProjectRoot9,
3109
+ findProjectRoot as findProjectRoot10,
2504
3110
  inferModulesFromPaths,
2505
3111
  loadMemoriesFromDir as loadMemoriesFromDir3,
2506
3112
  memoryFilePath as memoryFilePath2,
2507
- resolveHaivePaths as resolveHaivePaths6,
3113
+ resolveHaivePaths as resolveHaivePaths7,
2508
3114
  serializeMemory as serializeMemory3
2509
3115
  } from "@hiveai/core";
2510
3116
  function registerMemoryAdd(memory2) {
@@ -2532,9 +3138,9 @@ function registerMemoryAdd(memory2) {
2532
3138
  --scope team --body "Never modify existing migrations. Create V{n+1}__desc.sql."
2533
3139
  `
2534
3140
  ).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) => {
2535
- const root = findProjectRoot9(opts.dir);
2536
- const paths = resolveHaivePaths6(root);
2537
- if (!existsSync10(paths.haiveDir)) {
3141
+ const root = findProjectRoot10(opts.dir);
3142
+ const paths = resolveHaivePaths7(root);
3143
+ if (!existsSync12(paths.haiveDir)) {
2538
3144
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
2539
3145
  process.exitCode = 1;
2540
3146
  return;
@@ -2545,7 +3151,7 @@ function registerMemoryAdd(memory2) {
2545
3151
  const inferredTags = autoTagsEnabled ? inferModulesFromPaths(anchorPaths) : [];
2546
3152
  const mergedTags = Array.from(/* @__PURE__ */ new Set([...userTags, ...inferredTags]));
2547
3153
  if (anchorPaths.length > 0) {
2548
- const missing = anchorPaths.filter((p) => !existsSync10(path11.resolve(root, p)));
3154
+ const missing = anchorPaths.filter((p) => !existsSync12(path13.resolve(root, p)));
2549
3155
  if (missing.length > 0) {
2550
3156
  ui.warn(`Anchor path${missing.length > 1 ? "s" : ""} not found in project:`);
2551
3157
  for (const p of missing) ui.warn(` \u2717 ${p}`);
@@ -2557,12 +3163,12 @@ function registerMemoryAdd(memory2) {
2557
3163
  const title = opts.title ?? opts.slug;
2558
3164
  let body;
2559
3165
  if (opts.bodyFile !== void 0) {
2560
- if (!existsSync10(opts.bodyFile)) {
3166
+ if (!existsSync12(opts.bodyFile)) {
2561
3167
  ui.error(`--body-file not found: ${opts.bodyFile}`);
2562
3168
  process.exitCode = 1;
2563
3169
  return;
2564
3170
  }
2565
- const fileContent = await readFile7(opts.bodyFile, "utf8");
3171
+ const fileContent = await readFile8(opts.bodyFile, "utf8");
2566
3172
  body = opts.title ? `# ${opts.title}
2567
3173
 
2568
3174
  ${fileContent.trim()}
@@ -2578,7 +3184,7 @@ TODO \u2014 write the memory body.
2578
3184
  `;
2579
3185
  }
2580
3186
  const scope = opts.scope ?? "personal";
2581
- if (existsSync10(paths.memoriesDir)) {
3187
+ if (existsSync12(paths.memoriesDir)) {
2582
3188
  const incomingHash = createHash("sha256").update(body.trim()).digest("hex").slice(0, 12);
2583
3189
  const allForHash = await loadMemoriesFromDir3(paths.memoriesDir);
2584
3190
  const hashDup = allForHash.find(
@@ -2591,7 +3197,7 @@ TODO \u2014 write the memory body.
2591
3197
  return;
2592
3198
  }
2593
3199
  }
2594
- if (opts.topic && existsSync10(paths.memoriesDir)) {
3200
+ if (opts.topic && existsSync12(paths.memoriesDir)) {
2595
3201
  const existing = await loadMemoriesFromDir3(paths.memoriesDir);
2596
3202
  const topicMatch = existing.find(
2597
3203
  ({ memory: memory3 }) => memory3.frontmatter.topic === opts.topic && memory3.frontmatter.scope === scope && (!opts.module || memory3.frontmatter.module === opts.module)
@@ -2609,8 +3215,8 @@ TODO \u2014 write the memory body.
2609
3215
  symbols: parseCsv2(opts.symbols).length ? parseCsv2(opts.symbols) : fm.anchor.symbols
2610
3216
  }
2611
3217
  };
2612
- await writeFile6(topicMatch.filePath, serializeMemory3({ frontmatter: newFrontmatter, body }), "utf8");
2613
- ui.success(`Updated (topic upsert) ${path11.relative(root, topicMatch.filePath)}`);
3218
+ await writeFile7(topicMatch.filePath, serializeMemory3({ frontmatter: newFrontmatter, body }), "utf8");
3219
+ ui.success(`Updated (topic upsert) ${path13.relative(root, topicMatch.filePath)}`);
2614
3220
  ui.info(`id=${fm.id} revision=${revisionCount}`);
2615
3221
  return;
2616
3222
  }
@@ -2629,13 +3235,13 @@ TODO \u2014 write the memory body.
2629
3235
  topic: opts.topic
2630
3236
  });
2631
3237
  const file = memoryFilePath2(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
2632
- await mkdir6(path11.dirname(file), { recursive: true });
2633
- if (existsSync10(file)) {
3238
+ await mkdir8(path13.dirname(file), { recursive: true });
3239
+ if (existsSync12(file)) {
2634
3240
  ui.error(`Memory already exists at ${file}`);
2635
3241
  process.exitCode = 1;
2636
3242
  return;
2637
3243
  }
2638
- if (existsSync10(paths.memoriesDir)) {
3244
+ if (existsSync12(paths.memoriesDir)) {
2639
3245
  const existing = await loadMemoriesFromDir3(paths.memoriesDir);
2640
3246
  const slugTokens = opts.slug.toLowerCase().split(/[-_\s]+/).filter(Boolean);
2641
3247
  const similar = existing.filter(({ memory: memory3 }) => {
@@ -2647,8 +3253,8 @@ TODO \u2014 write the memory body.
2647
3253
  ui.warn("Consider updating one of these with `haive memory update` instead.");
2648
3254
  }
2649
3255
  }
2650
- await writeFile6(file, serializeMemory3({ frontmatter, body }), "utf8");
2651
- ui.success(`Created ${path11.relative(root, file)}`);
3256
+ await writeFile7(file, serializeMemory3({ frontmatter, body }), "utf8");
3257
+ ui.success(`Created ${path13.relative(root, file)}`);
2652
3258
  ui.info(`id=${frontmatter.id} scope=${frontmatter.scope} status=${frontmatter.status}`);
2653
3259
  if (inferredTags.length > 0) {
2654
3260
  ui.info(`auto-tagged: ${inferredTags.join(", ")} (use --no-auto-tag to disable)`);
@@ -2678,10 +3284,10 @@ function parseCsv2(value) {
2678
3284
  }
2679
3285
 
2680
3286
  // src/commands/memory-list.ts
2681
- import { existsSync as existsSync11 } from "fs";
2682
- import path12 from "path";
3287
+ import { existsSync as existsSync13 } from "fs";
3288
+ import path14 from "path";
2683
3289
  import "commander";
2684
- import { findProjectRoot as findProjectRoot10, resolveHaivePaths as resolveHaivePaths7 } from "@hiveai/core";
3290
+ import { findProjectRoot as findProjectRoot11, resolveHaivePaths as resolveHaivePaths8 } from "@hiveai/core";
2685
3291
 
2686
3292
  // src/utils/fs.ts
2687
3293
  import {
@@ -2693,9 +3299,9 @@ import {
2693
3299
  // src/commands/memory-list.ts
2694
3300
  function registerMemoryList(memory2) {
2695
3301
  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) => {
2696
- const root = findProjectRoot10(opts.dir);
2697
- const paths = resolveHaivePaths7(root);
2698
- if (!existsSync11(paths.memoriesDir)) {
3302
+ const root = findProjectRoot11(opts.dir);
3303
+ const paths = resolveHaivePaths8(root);
3304
+ if (!existsSync13(paths.memoriesDir)) {
2699
3305
  ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
2700
3306
  process.exitCode = 1;
2701
3307
  return;
@@ -2727,7 +3333,7 @@ function registerMemoryList(memory2) {
2727
3333
  console.log(
2728
3334
  `${ui.bold(fm.id)} ${ui.dim(fm.scope)}/${ui.dim(fm.type)} ${statusBadge}${moduleStr}${tagStr}`
2729
3335
  );
2730
- console.log(` ${ui.dim(path12.relative(root, filePath))}`);
3336
+ console.log(` ${ui.dim(path14.relative(root, filePath))}`);
2731
3337
  }
2732
3338
  console.log(ui.dim(`
2733
3339
  ${filtered.length} memor${filtered.length === 1 ? "y" : "ies"}`));
@@ -2762,21 +3368,21 @@ function matchesFilters(loaded, opts) {
2762
3368
  }
2763
3369
 
2764
3370
  // src/commands/memory-promote.ts
2765
- import { mkdir as mkdir7, unlink, writeFile as writeFile7 } from "fs/promises";
2766
- import { existsSync as existsSync12 } from "fs";
2767
- import path13 from "path";
3371
+ import { mkdir as mkdir9, unlink, writeFile as writeFile8 } from "fs/promises";
3372
+ import { existsSync as existsSync14 } from "fs";
3373
+ import path15 from "path";
2768
3374
  import "commander";
2769
3375
  import {
2770
- findProjectRoot as findProjectRoot11,
3376
+ findProjectRoot as findProjectRoot12,
2771
3377
  memoryFilePath as memoryFilePath3,
2772
- resolveHaivePaths as resolveHaivePaths8,
3378
+ resolveHaivePaths as resolveHaivePaths9,
2773
3379
  serializeMemory as serializeMemory4
2774
3380
  } from "@hiveai/core";
2775
3381
  function registerMemoryPromote(memory2) {
2776
3382
  memory2.command("promote <id>").description("Promote a personal memory to team scope (status -> proposed)").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
2777
- const root = findProjectRoot11(opts.dir);
2778
- const paths = resolveHaivePaths8(root);
2779
- if (!existsSync12(paths.memoriesDir)) {
3383
+ const root = findProjectRoot12(opts.dir);
3384
+ const paths = resolveHaivePaths9(root);
3385
+ if (!existsSync14(paths.memoriesDir)) {
2780
3386
  ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
2781
3387
  process.exitCode = 1;
2782
3388
  return;
@@ -2811,30 +3417,30 @@ function registerMemoryPromote(memory2) {
2811
3417
  body: found.memory.body
2812
3418
  };
2813
3419
  const newPath = memoryFilePath3(paths, "team", updated.frontmatter.id);
2814
- await mkdir7(path13.dirname(newPath), { recursive: true });
2815
- await writeFile7(newPath, serializeMemory4(updated), "utf8");
3420
+ await mkdir9(path15.dirname(newPath), { recursive: true });
3421
+ await writeFile8(newPath, serializeMemory4(updated), "utf8");
2816
3422
  await unlink(found.filePath);
2817
3423
  ui.success(`Promoted ${id} to team scope (status=proposed)`);
2818
- ui.info(`Now at ${path13.relative(root, newPath)}`);
3424
+ ui.info(`Now at ${path15.relative(root, newPath)}`);
2819
3425
  console.log(ui.dim(`\u2192 next: haive memory approve ${id} (validate for team use)`));
2820
3426
  });
2821
3427
  }
2822
3428
 
2823
3429
  // src/commands/memory-approve.ts
2824
- import { existsSync as existsSync13 } from "fs";
2825
- import { writeFile as writeFile8 } from "fs/promises";
2826
- import path14 from "path";
3430
+ import { existsSync as existsSync15 } from "fs";
3431
+ import { writeFile as writeFile9 } from "fs/promises";
3432
+ import path16 from "path";
2827
3433
  import "commander";
2828
3434
  import {
2829
- findProjectRoot as findProjectRoot12,
2830
- resolveHaivePaths as resolveHaivePaths9,
3435
+ findProjectRoot as findProjectRoot13,
3436
+ resolveHaivePaths as resolveHaivePaths10,
2831
3437
  serializeMemory as serializeMemory5
2832
3438
  } from "@hiveai/core";
2833
3439
  function registerMemoryApprove(memory2) {
2834
3440
  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) => {
2835
- const root = findProjectRoot12(opts.dir);
2836
- const paths = resolveHaivePaths9(root);
2837
- if (!existsSync13(paths.memoriesDir)) {
3441
+ const root = findProjectRoot13(opts.dir);
3442
+ const paths = resolveHaivePaths10(root);
3443
+ if (!existsSync15(paths.memoriesDir)) {
2838
3444
  ui.error(`No .ai/memories at ${root}.`);
2839
3445
  process.exitCode = 1;
2840
3446
  return;
@@ -2856,7 +3462,7 @@ function registerMemoryApprove(memory2) {
2856
3462
  frontmatter: { ...found2.memory.frontmatter, status: "validated" },
2857
3463
  body: found2.memory.body
2858
3464
  };
2859
- await writeFile8(found2.filePath, serializeMemory5(next2), "utf8");
3465
+ await writeFile9(found2.filePath, serializeMemory5(next2), "utf8");
2860
3466
  count++;
2861
3467
  }
2862
3468
  ui.success(`Approved ${count} memor${count === 1 ? "y" : "ies"} (status=validated)`);
@@ -2885,27 +3491,27 @@ function registerMemoryApprove(memory2) {
2885
3491
  frontmatter: { ...found.memory.frontmatter, status: "validated" },
2886
3492
  body: found.memory.body
2887
3493
  };
2888
- await writeFile8(found.filePath, serializeMemory5(next), "utf8");
3494
+ await writeFile9(found.filePath, serializeMemory5(next), "utf8");
2889
3495
  ui.success(`Approved ${id} (status=validated)`);
2890
- ui.info(path14.relative(root, found.filePath));
3496
+ ui.info(path16.relative(root, found.filePath));
2891
3497
  });
2892
3498
  }
2893
3499
 
2894
3500
  // src/commands/memory-update.ts
2895
- import { writeFile as writeFile9 } from "fs/promises";
2896
- import { existsSync as existsSync14 } from "fs";
2897
- import path15 from "path";
3501
+ import { writeFile as writeFile10 } from "fs/promises";
3502
+ import { existsSync as existsSync16 } from "fs";
3503
+ import path17 from "path";
2898
3504
  import "commander";
2899
3505
  import {
2900
- findProjectRoot as findProjectRoot13,
2901
- resolveHaivePaths as resolveHaivePaths10,
3506
+ findProjectRoot as findProjectRoot14,
3507
+ resolveHaivePaths as resolveHaivePaths11,
2902
3508
  serializeMemory as serializeMemory6
2903
3509
  } from "@hiveai/core";
2904
3510
  function registerMemoryUpdate(memory2) {
2905
3511
  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) => {
2906
- const root = findProjectRoot13(opts.dir);
2907
- const paths = resolveHaivePaths10(root);
2908
- if (!existsSync14(paths.memoriesDir)) {
3512
+ const root = findProjectRoot14(opts.dir);
3513
+ const paths = resolveHaivePaths11(root);
3514
+ if (!existsSync16(paths.memoriesDir)) {
2909
3515
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
2910
3516
  process.exitCode = 1;
2911
3517
  return;
@@ -2952,12 +3558,12 @@ function registerMemoryUpdate(memory2) {
2952
3558
  ui.warn("Nothing to update \u2014 provide at least one option.");
2953
3559
  return;
2954
3560
  }
2955
- await writeFile9(
3561
+ await writeFile10(
2956
3562
  loaded.filePath,
2957
3563
  serializeMemory6({ frontmatter: newFrontmatter, body: newBody }),
2958
3564
  "utf8"
2959
3565
  );
2960
- ui.success(`Updated ${path15.relative(root, loaded.filePath)}`);
3566
+ ui.success(`Updated ${path17.relative(root, loaded.filePath)}`);
2961
3567
  ui.info(`fields: ${updated.join(", ")}`);
2962
3568
  });
2963
3569
  }
@@ -2976,17 +3582,17 @@ function parseCsv3(value) {
2976
3582
  }
2977
3583
 
2978
3584
  // src/commands/memory-auto-promote.ts
2979
- import { writeFile as writeFile10 } from "fs/promises";
2980
- import { existsSync as existsSync15 } from "fs";
2981
- import path16 from "path";
3585
+ import { writeFile as writeFile11 } from "fs/promises";
3586
+ import { existsSync as existsSync17 } from "fs";
3587
+ import path18 from "path";
2982
3588
  import "commander";
2983
3589
  import {
2984
3590
  DEFAULT_AUTO_PROMOTE_RULE as DEFAULT_AUTO_PROMOTE_RULE2,
2985
- findProjectRoot as findProjectRoot14,
3591
+ findProjectRoot as findProjectRoot15,
2986
3592
  getUsage as getUsage2,
2987
3593
  isAutoPromoteEligible as isAutoPromoteEligible2,
2988
- loadUsageIndex as loadUsageIndex2,
2989
- resolveHaivePaths as resolveHaivePaths11,
3594
+ loadUsageIndex as loadUsageIndex3,
3595
+ resolveHaivePaths as resolveHaivePaths12,
2990
3596
  serializeMemory as serializeMemory7
2991
3597
  } from "@hiveai/core";
2992
3598
  function registerMemoryAutoPromote(memory2) {
@@ -2995,9 +3601,9 @@ function registerMemoryAutoPromote(memory2) {
2995
3601
  "memories with more rejections than this are skipped",
2996
3602
  String(DEFAULT_AUTO_PROMOTE_RULE2.maxRejections)
2997
3603
  ).option("--apply", "actually write status=validated to disk (default: dry-run)").option("-d, --dir <dir>", "project root").action(async (opts) => {
2998
- const root = findProjectRoot14(opts.dir);
2999
- const paths = resolveHaivePaths11(root);
3000
- if (!existsSync15(paths.memoriesDir)) {
3604
+ const root = findProjectRoot15(opts.dir);
3605
+ const paths = resolveHaivePaths12(root);
3606
+ if (!existsSync17(paths.memoriesDir)) {
3001
3607
  ui.error(`No .ai/memories at ${root}.`);
3002
3608
  process.exitCode = 1;
3003
3609
  return;
@@ -3007,7 +3613,7 @@ function registerMemoryAutoPromote(memory2) {
3007
3613
  maxRejections: Number(opts.maxRejections ?? DEFAULT_AUTO_PROMOTE_RULE2.maxRejections)
3008
3614
  };
3009
3615
  const memories = await loadMemoriesFromDir4(paths.memoriesDir);
3010
- const usage = await loadUsageIndex2(paths);
3616
+ const usage = await loadUsageIndex3(paths);
3011
3617
  const eligible = memories.filter(
3012
3618
  ({ memory: memory3 }) => isAutoPromoteEligible2(memory3.frontmatter, getUsage2(usage, memory3.frontmatter.id), rule)
3013
3619
  );
@@ -3023,13 +3629,13 @@ function registerMemoryAutoPromote(memory2) {
3023
3629
  console.log(
3024
3630
  `${ui.bold(opts.apply ? "PROMOTE" : "would promote")} ${mem.frontmatter.id} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
3025
3631
  );
3026
- console.log(` ${ui.dim(path16.relative(root, filePath))}`);
3632
+ console.log(` ${ui.dim(path18.relative(root, filePath))}`);
3027
3633
  if (opts.apply) {
3028
3634
  const next = {
3029
3635
  frontmatter: { ...mem.frontmatter, status: "validated" },
3030
3636
  body: mem.body
3031
3637
  };
3032
- await writeFile10(filePath, serializeMemory7(next), "utf8");
3638
+ await writeFile11(filePath, serializeMemory7(next), "utf8");
3033
3639
  written++;
3034
3640
  }
3035
3641
  }
@@ -3040,20 +3646,20 @@ function registerMemoryAutoPromote(memory2) {
3040
3646
 
3041
3647
  // src/commands/memory-edit.ts
3042
3648
  import { spawn as spawn2 } from "child_process";
3043
- import { existsSync as existsSync16 } from "fs";
3044
- import { readFile as readFile8 } from "fs/promises";
3045
- import path17 from "path";
3649
+ import { existsSync as existsSync18 } from "fs";
3650
+ import { readFile as readFile9 } from "fs/promises";
3651
+ import path19 from "path";
3046
3652
  import "commander";
3047
3653
  import {
3048
- findProjectRoot as findProjectRoot15,
3654
+ findProjectRoot as findProjectRoot16,
3049
3655
  parseMemory,
3050
- resolveHaivePaths as resolveHaivePaths12
3656
+ resolveHaivePaths as resolveHaivePaths13
3051
3657
  } from "@hiveai/core";
3052
3658
  function registerMemoryEdit(memory2) {
3053
3659
  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) => {
3054
- const root = findProjectRoot15(opts.dir);
3055
- const paths = resolveHaivePaths12(root);
3056
- if (!existsSync16(paths.memoriesDir)) {
3660
+ const root = findProjectRoot16(opts.dir);
3661
+ const paths = resolveHaivePaths13(root);
3662
+ if (!existsSync18(paths.memoriesDir)) {
3057
3663
  ui.error(`No .ai/memories at ${root}.`);
3058
3664
  process.exitCode = 1;
3059
3665
  return;
@@ -3066,13 +3672,13 @@ function registerMemoryEdit(memory2) {
3066
3672
  return;
3067
3673
  }
3068
3674
  const editor = opts.editor ?? process.env.EDITOR ?? process.env.VISUAL ?? "vi";
3069
- ui.info(`Opening ${path17.relative(root, found.filePath)} with ${editor}\u2026`);
3675
+ ui.info(`Opening ${path19.relative(root, found.filePath)} with ${editor}\u2026`);
3070
3676
  const code = await runEditor(editor, found.filePath);
3071
3677
  if (code !== 0) {
3072
3678
  ui.warn(`Editor exited with status ${code}.`);
3073
3679
  }
3074
3680
  try {
3075
- const fresh = await readFile8(found.filePath, "utf8");
3681
+ const fresh = await readFile9(found.filePath, "utf8");
3076
3682
  parseMemory(fresh);
3077
3683
  ui.success("Memory still parses cleanly.");
3078
3684
  } catch (err) {
@@ -3093,29 +3699,29 @@ function runEditor(editor, file) {
3093
3699
  }
3094
3700
 
3095
3701
  // src/commands/memory-for-files.ts
3096
- import { existsSync as existsSync17 } from "fs";
3097
- import path18 from "path";
3702
+ import { existsSync as existsSync19 } from "fs";
3703
+ import path20 from "path";
3098
3704
  import "commander";
3099
3705
  import {
3100
3706
  deriveConfidence,
3101
- findProjectRoot as findProjectRoot16,
3707
+ findProjectRoot as findProjectRoot17,
3102
3708
  getUsage as getUsage3,
3103
3709
  inferModulesFromPaths as inferModulesFromPaths2,
3104
- loadUsageIndex as loadUsageIndex3,
3710
+ loadUsageIndex as loadUsageIndex4,
3105
3711
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
3106
- resolveHaivePaths as resolveHaivePaths13
3712
+ resolveHaivePaths as resolveHaivePaths14
3107
3713
  } from "@hiveai/core";
3108
3714
  function registerMemoryForFiles(memory2) {
3109
3715
  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) => {
3110
- const root = findProjectRoot16(opts.dir);
3111
- const paths = resolveHaivePaths13(root);
3112
- if (!existsSync17(paths.memoriesDir)) {
3716
+ const root = findProjectRoot17(opts.dir);
3717
+ const paths = resolveHaivePaths14(root);
3718
+ if (!existsSync19(paths.memoriesDir)) {
3113
3719
  ui.error(`No .ai/memories at ${root}.`);
3114
3720
  process.exitCode = 1;
3115
3721
  return;
3116
3722
  }
3117
3723
  const all = await loadMemoriesFromDir4(paths.memoriesDir);
3118
- const usage = await loadUsageIndex3(paths);
3724
+ const usage = await loadUsageIndex4(paths);
3119
3725
  const inferred = inferModulesFromPaths2(files);
3120
3726
  const byAnchor = [];
3121
3727
  const byModule = [];
@@ -3216,32 +3822,32 @@ function printGroup(root, label, loaded, usage) {
3216
3822
  const u = getUsage3(usage, fm.id);
3217
3823
  const conf = deriveConfidence(fm, u);
3218
3824
  console.log(`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(conf)}`);
3219
- console.log(` ${ui.dim(path18.relative(root, filePath))}`);
3825
+ console.log(` ${ui.dim(path20.relative(root, filePath))}`);
3220
3826
  }
3221
3827
  }
3222
3828
 
3223
3829
  // src/commands/memory-hot.ts
3224
- import { existsSync as existsSync18 } from "fs";
3225
- import path19 from "path";
3830
+ import { existsSync as existsSync20 } from "fs";
3831
+ import path21 from "path";
3226
3832
  import "commander";
3227
3833
  import {
3228
- findProjectRoot as findProjectRoot17,
3834
+ findProjectRoot as findProjectRoot18,
3229
3835
  getUsage as getUsage4,
3230
- loadUsageIndex as loadUsageIndex4,
3231
- resolveHaivePaths as resolveHaivePaths14
3836
+ loadUsageIndex as loadUsageIndex5,
3837
+ resolveHaivePaths as resolveHaivePaths15
3232
3838
  } from "@hiveai/core";
3233
3839
  function registerMemoryHot(memory2) {
3234
3840
  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) => {
3235
- const root = findProjectRoot17(opts.dir);
3236
- const paths = resolveHaivePaths14(root);
3237
- if (!existsSync18(paths.memoriesDir)) {
3841
+ const root = findProjectRoot18(opts.dir);
3842
+ const paths = resolveHaivePaths15(root);
3843
+ if (!existsSync20(paths.memoriesDir)) {
3238
3844
  ui.error(`No .ai/memories at ${root}.`);
3239
3845
  process.exitCode = 1;
3240
3846
  return;
3241
3847
  }
3242
3848
  const threshold = Math.max(1, Number(opts.threshold ?? 3));
3243
3849
  const all = await loadMemoriesFromDir4(paths.memoriesDir);
3244
- const usage = await loadUsageIndex4(paths);
3850
+ const usage = await loadUsageIndex5(paths);
3245
3851
  const candidates = all.filter(({ memory: mem }) => {
3246
3852
  const fm = mem.frontmatter;
3247
3853
  if (opts.status && fm.status !== opts.status) return false;
@@ -3262,7 +3868,7 @@ function registerMemoryHot(memory2) {
3262
3868
  console.log(
3263
3869
  `${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(fm.status)} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
3264
3870
  );
3265
- console.log(` ${ui.dim(path19.relative(root, filePath))}`);
3871
+ console.log(` ${ui.dim(path21.relative(root, filePath))}`);
3266
3872
  }
3267
3873
  ui.info(
3268
3874
  `${candidates.length} hot \u2014 promote drafts with \`haive memory promote <id>\`, then \`haive memory auto-promote --apply\`.`
@@ -3271,15 +3877,15 @@ function registerMemoryHot(memory2) {
3271
3877
  }
3272
3878
 
3273
3879
  // src/commands/memory-tried.ts
3274
- import { mkdir as mkdir8, writeFile as writeFile11 } from "fs/promises";
3275
- import { existsSync as existsSync19 } from "fs";
3276
- import path20 from "path";
3880
+ import { mkdir as mkdir10, writeFile as writeFile12 } from "fs/promises";
3881
+ import { existsSync as existsSync21 } from "fs";
3882
+ import path22 from "path";
3277
3883
  import "commander";
3278
3884
  import {
3279
3885
  buildFrontmatter as buildFrontmatter4,
3280
- findProjectRoot as findProjectRoot18,
3886
+ findProjectRoot as findProjectRoot19,
3281
3887
  memoryFilePath as memoryFilePath4,
3282
- resolveHaivePaths as resolveHaivePaths15,
3888
+ resolveHaivePaths as resolveHaivePaths16,
3283
3889
  serializeMemory as serializeMemory8
3284
3890
  } from "@hiveai/core";
3285
3891
  function registerMemoryTried(memory2) {
@@ -3299,9 +3905,9 @@ function registerMemoryTried(memory2) {
3299
3905
  --paths packages/cli/src/index.ts
3300
3906
  `
3301
3907
  ).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) => {
3302
- const root = findProjectRoot18(opts.dir);
3303
- const paths = resolveHaivePaths15(root);
3304
- if (!existsSync19(paths.haiveDir)) {
3908
+ const root = findProjectRoot19(opts.dir);
3909
+ const paths = resolveHaivePaths16(root);
3910
+ if (!existsSync21(paths.haiveDir)) {
3305
3911
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
3306
3912
  process.exitCode = 1;
3307
3913
  return;
@@ -3324,14 +3930,14 @@ function registerMemoryTried(memory2) {
3324
3930
  }
3325
3931
  const body = lines.join("\n") + "\n";
3326
3932
  const file = memoryFilePath4(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
3327
- await mkdir8(path20.dirname(file), { recursive: true });
3328
- if (existsSync19(file)) {
3933
+ await mkdir10(path22.dirname(file), { recursive: true });
3934
+ if (existsSync21(file)) {
3329
3935
  ui.error(`Memory already exists at ${file}`);
3330
3936
  process.exitCode = 1;
3331
3937
  return;
3332
3938
  }
3333
- await writeFile11(file, serializeMemory8({ frontmatter, body }), "utf8");
3334
- ui.success(`Recorded: ${path20.relative(root, file)}`);
3939
+ await writeFile12(file, serializeMemory8({ frontmatter, body }), "utf8");
3940
+ ui.success(`Recorded: ${path22.relative(root, file)}`);
3335
3941
  ui.info(`id=${frontmatter.id} type=attempt status=validated (auto-approved)`);
3336
3942
  });
3337
3943
  }
@@ -3341,26 +3947,26 @@ function parseCsv4(value) {
3341
3947
  }
3342
3948
 
3343
3949
  // src/commands/memory-pending.ts
3344
- import { existsSync as existsSync20 } from "fs";
3345
- import path21 from "path";
3950
+ import { existsSync as existsSync22 } from "fs";
3951
+ import path23 from "path";
3346
3952
  import "commander";
3347
3953
  import {
3348
- findProjectRoot as findProjectRoot19,
3954
+ findProjectRoot as findProjectRoot20,
3349
3955
  getUsage as getUsage5,
3350
- loadUsageIndex as loadUsageIndex5,
3351
- resolveHaivePaths as resolveHaivePaths16
3956
+ loadUsageIndex as loadUsageIndex6,
3957
+ resolveHaivePaths as resolveHaivePaths17
3352
3958
  } from "@hiveai/core";
3353
3959
  function registerMemoryPending(memory2) {
3354
3960
  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) => {
3355
- const root = findProjectRoot19(opts.dir);
3356
- const paths = resolveHaivePaths16(root);
3357
- if (!existsSync20(paths.memoriesDir)) {
3961
+ const root = findProjectRoot20(opts.dir);
3962
+ const paths = resolveHaivePaths17(root);
3963
+ if (!existsSync22(paths.memoriesDir)) {
3358
3964
  ui.error(`No .ai/memories at ${root}.`);
3359
3965
  process.exitCode = 1;
3360
3966
  return;
3361
3967
  }
3362
3968
  const all = await loadMemoriesFromDir4(paths.memoriesDir);
3363
- const usage = await loadUsageIndex5(paths);
3969
+ const usage = await loadUsageIndex6(paths);
3364
3970
  const proposed = all.filter(({ memory: mem }) => {
3365
3971
  if (mem.frontmatter.status !== "proposed") return false;
3366
3972
  if (opts.scope && mem.frontmatter.scope !== opts.scope) return false;
@@ -3382,31 +3988,31 @@ function registerMemoryPending(memory2) {
3382
3988
  console.log(
3383
3989
  `${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.dim(`age=${ageStr} reads=${u.read_count} rejections=${u.rejected_count}`)}`
3384
3990
  );
3385
- console.log(` ${ui.dim(path21.relative(root, filePath))}`);
3991
+ console.log(` ${ui.dim(path23.relative(root, filePath))}`);
3386
3992
  }
3387
3993
  ui.info(`${proposed.length} pending`);
3388
3994
  });
3389
3995
  }
3390
3996
 
3391
3997
  // src/commands/memory-query.ts
3392
- import { existsSync as existsSync21 } from "fs";
3393
- import path22 from "path";
3998
+ import { existsSync as existsSync23 } from "fs";
3999
+ import path24 from "path";
3394
4000
  import "commander";
3395
4001
  import {
3396
4002
  extractSnippet,
3397
- findProjectRoot as findProjectRoot20,
4003
+ findProjectRoot as findProjectRoot21,
3398
4004
  literalMatchesAllTokens as literalMatchesAllTokens2,
3399
4005
  literalMatchesAnyToken as literalMatchesAnyToken2,
3400
4006
  pickSnippetNeedle,
3401
- resolveHaivePaths as resolveHaivePaths17,
4007
+ resolveHaivePaths as resolveHaivePaths18,
3402
4008
  tokenizeQuery as tokenizeQuery2,
3403
4009
  trackReads as trackReads2
3404
4010
  } from "@hiveai/core";
3405
4011
  function registerMemoryQuery(memory2) {
3406
- 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) => {
3407
- const root = findProjectRoot20(opts.dir);
3408
- const paths = resolveHaivePaths17(root);
3409
- if (!existsSync21(paths.memoriesDir)) {
4012
+ 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) => {
4013
+ const root = findProjectRoot21(opts.dir);
4014
+ const paths = resolveHaivePaths18(root);
4015
+ if (!existsSync23(paths.memoriesDir)) {
3410
4016
  ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
3411
4017
  process.exitCode = 1;
3412
4018
  return;
@@ -3447,7 +4053,7 @@ function registerMemoryQuery(memory2) {
3447
4053
  const fm = mem.frontmatter;
3448
4054
  const statusBadge = ui.statusBadge(fm.status);
3449
4055
  console.log(`${ui.bold(fm.id)} ${ui.dim(fm.scope)} ${statusBadge}`);
3450
- console.log(` ${ui.dim(path22.relative(root, filePath))}`);
4056
+ console.log(` ${ui.dim(path24.relative(root, filePath))}`);
3451
4057
  const snippet = extractSnippet(mem.body, snippetNeedle);
3452
4058
  if (snippet) console.log(` ${snippet}`);
3453
4059
  }
@@ -3464,22 +4070,22 @@ ${top.length} of ${matches.length} match${matches.length === 1 ? "" : "es"}`)
3464
4070
  }
3465
4071
 
3466
4072
  // src/commands/memory-reject.ts
3467
- import { writeFile as writeFile12 } from "fs/promises";
3468
- import { existsSync as existsSync22 } from "fs";
4073
+ import { writeFile as writeFile13 } from "fs/promises";
4074
+ import { existsSync as existsSync24 } from "fs";
3469
4075
  import "commander";
3470
4076
  import {
3471
- findProjectRoot as findProjectRoot21,
3472
- loadUsageIndex as loadUsageIndex6,
4077
+ findProjectRoot as findProjectRoot22,
4078
+ loadUsageIndex as loadUsageIndex7,
3473
4079
  recordRejection,
3474
- resolveHaivePaths as resolveHaivePaths18,
4080
+ resolveHaivePaths as resolveHaivePaths19,
3475
4081
  saveUsageIndex,
3476
4082
  serializeMemory as serializeMemory9
3477
4083
  } from "@hiveai/core";
3478
4084
  function registerMemoryReject(memory2) {
3479
4085
  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) => {
3480
- const root = findProjectRoot21(opts.dir);
3481
- const paths = resolveHaivePaths18(root);
3482
- if (!existsSync22(paths.memoriesDir)) {
4086
+ const root = findProjectRoot22(opts.dir);
4087
+ const paths = resolveHaivePaths19(root);
4088
+ if (!existsSync24(paths.memoriesDir)) {
3483
4089
  ui.error(`No .ai/memories at ${root}.`);
3484
4090
  process.exitCode = 1;
3485
4091
  return;
@@ -3491,7 +4097,7 @@ function registerMemoryReject(memory2) {
3491
4097
  process.exitCode = 1;
3492
4098
  return;
3493
4099
  }
3494
- await writeFile12(
4100
+ await writeFile13(
3495
4101
  loaded.filePath,
3496
4102
  serializeMemory9({
3497
4103
  frontmatter: {
@@ -3503,7 +4109,7 @@ function registerMemoryReject(memory2) {
3503
4109
  }),
3504
4110
  "utf8"
3505
4111
  );
3506
- const idx = await loadUsageIndex6(paths);
4112
+ const idx = await loadUsageIndex7(paths);
3507
4113
  recordRejection(idx, id, opts.reason ?? null);
3508
4114
  await saveUsageIndex(paths, idx);
3509
4115
  const u = idx.by_id[id];
@@ -3515,22 +4121,22 @@ function registerMemoryReject(memory2) {
3515
4121
  }
3516
4122
 
3517
4123
  // src/commands/memory-rm.ts
3518
- import { existsSync as existsSync23 } from "fs";
4124
+ import { existsSync as existsSync25 } from "fs";
3519
4125
  import { unlink as unlink2 } from "fs/promises";
3520
- import path23 from "path";
4126
+ import path25 from "path";
3521
4127
  import { createInterface } from "readline/promises";
3522
4128
  import "commander";
3523
4129
  import {
3524
- findProjectRoot as findProjectRoot22,
3525
- loadUsageIndex as loadUsageIndex7,
3526
- resolveHaivePaths as resolveHaivePaths19,
4130
+ findProjectRoot as findProjectRoot23,
4131
+ loadUsageIndex as loadUsageIndex8,
4132
+ resolveHaivePaths as resolveHaivePaths20,
3527
4133
  saveUsageIndex as saveUsageIndex2
3528
4134
  } from "@hiveai/core";
3529
4135
  function registerMemoryRm(memory2) {
3530
4136
  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) => {
3531
- const root = findProjectRoot22(opts.dir);
3532
- const paths = resolveHaivePaths19(root);
3533
- if (!existsSync23(paths.memoriesDir)) {
4137
+ const root = findProjectRoot23(opts.dir);
4138
+ const paths = resolveHaivePaths20(root);
4139
+ if (!existsSync25(paths.memoriesDir)) {
3534
4140
  ui.error(`No .ai/memories at ${root}.`);
3535
4141
  process.exitCode = 1;
3536
4142
  return;
@@ -3542,7 +4148,7 @@ function registerMemoryRm(memory2) {
3542
4148
  process.exitCode = 1;
3543
4149
  return;
3544
4150
  }
3545
- const rel = path23.relative(root, found.filePath);
4151
+ const rel = path25.relative(root, found.filePath);
3546
4152
  if (!opts.yes) {
3547
4153
  const rl = createInterface({ input: process.stdin, output: process.stdout });
3548
4154
  const answer = (await rl.question(`Delete ${rel}? [y/N] `)).trim().toLowerCase();
@@ -3555,7 +4161,7 @@ function registerMemoryRm(memory2) {
3555
4161
  await unlink2(found.filePath);
3556
4162
  ui.success(`Deleted ${rel}`);
3557
4163
  if (!opts.keepUsage) {
3558
- const idx = await loadUsageIndex7(paths);
4164
+ const idx = await loadUsageIndex8(paths);
3559
4165
  if (idx.by_id[id]) {
3560
4166
  delete idx.by_id[id];
3561
4167
  await saveUsageIndex2(paths, idx);
@@ -3566,22 +4172,22 @@ function registerMemoryRm(memory2) {
3566
4172
  }
3567
4173
 
3568
4174
  // src/commands/memory-show.ts
3569
- import { existsSync as existsSync24 } from "fs";
3570
- import { readFile as readFile9 } from "fs/promises";
3571
- import path24 from "path";
4175
+ import { existsSync as existsSync26 } from "fs";
4176
+ import { readFile as readFile10 } from "fs/promises";
4177
+ import path26 from "path";
3572
4178
  import "commander";
3573
4179
  import {
3574
4180
  deriveConfidence as deriveConfidence2,
3575
- findProjectRoot as findProjectRoot23,
4181
+ findProjectRoot as findProjectRoot24,
3576
4182
  getUsage as getUsage6,
3577
- loadUsageIndex as loadUsageIndex8,
3578
- resolveHaivePaths as resolveHaivePaths20
4183
+ loadUsageIndex as loadUsageIndex9,
4184
+ resolveHaivePaths as resolveHaivePaths21
3579
4185
  } from "@hiveai/core";
3580
4186
  function registerMemoryShow(memory2) {
3581
4187
  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) => {
3582
- const root = findProjectRoot23(opts.dir);
3583
- const paths = resolveHaivePaths20(root);
3584
- if (!existsSync24(paths.memoriesDir)) {
4188
+ const root = findProjectRoot24(opts.dir);
4189
+ const paths = resolveHaivePaths21(root);
4190
+ if (!existsSync26(paths.memoriesDir)) {
3585
4191
  ui.error(`No .ai/memories at ${root}.`);
3586
4192
  process.exitCode = 1;
3587
4193
  return;
@@ -3594,11 +4200,11 @@ function registerMemoryShow(memory2) {
3594
4200
  return;
3595
4201
  }
3596
4202
  if (opts.raw) {
3597
- console.log(await readFile9(found.filePath, "utf8"));
4203
+ console.log(await readFile10(found.filePath, "utf8"));
3598
4204
  return;
3599
4205
  }
3600
4206
  const fm = found.memory.frontmatter;
3601
- const usage = await loadUsageIndex8(paths);
4207
+ const usage = await loadUsageIndex9(paths);
3602
4208
  const u = getUsage6(usage, fm.id);
3603
4209
  const conf = deriveConfidence2(fm, u);
3604
4210
  console.log(ui.bold(fm.id));
@@ -3610,7 +4216,7 @@ function registerMemoryShow(memory2) {
3610
4216
  if (fm.verified_at) console.log(`${ui.dim("verified:")} ${fm.verified_at}`);
3611
4217
  if (fm.stale_reason) console.log(`${ui.dim("stale:")} ${fm.stale_reason}`);
3612
4218
  console.log(`${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`);
3613
- console.log(`${ui.dim("file:")} ${path24.relative(root, found.filePath)}`);
4219
+ console.log(`${ui.dim("file:")} ${path26.relative(root, found.filePath)}`);
3614
4220
  if (fm.anchor.paths.length || fm.anchor.symbols.length) {
3615
4221
  console.log(ui.dim("anchor:"));
3616
4222
  if (fm.anchor.commit) console.log(` ${ui.dim("commit:")} ${fm.anchor.commit}`);
@@ -3625,27 +4231,27 @@ function registerMemoryShow(memory2) {
3625
4231
  }
3626
4232
 
3627
4233
  // src/commands/memory-stats.ts
3628
- import { existsSync as existsSync25 } from "fs";
3629
- import path25 from "path";
4234
+ import { existsSync as existsSync27 } from "fs";
4235
+ import path27 from "path";
3630
4236
  import "commander";
3631
4237
  import {
3632
4238
  deriveConfidence as deriveConfidence3,
3633
- findProjectRoot as findProjectRoot24,
4239
+ findProjectRoot as findProjectRoot25,
3634
4240
  getUsage as getUsage7,
3635
- loadUsageIndex as loadUsageIndex9,
3636
- resolveHaivePaths as resolveHaivePaths21
4241
+ loadUsageIndex as loadUsageIndex10,
4242
+ resolveHaivePaths as resolveHaivePaths22
3637
4243
  } from "@hiveai/core";
3638
4244
  function registerMemoryStats(memory2) {
3639
4245
  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) => {
3640
- const root = findProjectRoot24(opts.dir);
3641
- const paths = resolveHaivePaths21(root);
3642
- if (!existsSync25(paths.memoriesDir)) {
4246
+ const root = findProjectRoot25(opts.dir);
4247
+ const paths = resolveHaivePaths22(root);
4248
+ if (!existsSync27(paths.memoriesDir)) {
3643
4249
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
3644
4250
  process.exitCode = 1;
3645
4251
  return;
3646
4252
  }
3647
4253
  const all = await loadMemoriesFromDir4(paths.memoriesDir);
3648
- const usage = await loadUsageIndex9(paths);
4254
+ const usage = await loadUsageIndex10(paths);
3649
4255
  const target = opts.id ? all.filter((m) => m.memory.frontmatter.id === opts.id) : all;
3650
4256
  if (target.length === 0) {
3651
4257
  ui.info(opts.id ? `No memory with id "${opts.id}".` : "No memories.");
@@ -3664,19 +4270,19 @@ function registerMemoryStats(memory2) {
3664
4270
  console.log(
3665
4271
  ` ${ui.dim("status:")} ${fm.status} ${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`
3666
4272
  );
3667
- console.log(` ${ui.dim(path25.relative(root, filePath))}`);
4273
+ console.log(` ${ui.dim(path27.relative(root, filePath))}`);
3668
4274
  }
3669
4275
  });
3670
4276
  }
3671
4277
 
3672
4278
  // src/commands/memory-verify.ts
3673
- import { writeFile as writeFile13 } from "fs/promises";
3674
- import { existsSync as existsSync26 } from "fs";
3675
- import path26 from "path";
4279
+ import { writeFile as writeFile14 } from "fs/promises";
4280
+ import { existsSync as existsSync28 } from "fs";
4281
+ import path28 from "path";
3676
4282
  import "commander";
3677
4283
  import {
3678
- findProjectRoot as findProjectRoot25,
3679
- resolveHaivePaths as resolveHaivePaths22,
4284
+ findProjectRoot as findProjectRoot26,
4285
+ resolveHaivePaths as resolveHaivePaths23,
3680
4286
  serializeMemory as serializeMemory10,
3681
4287
  verifyAnchor as verifyAnchor2
3682
4288
  } from "@hiveai/core";
@@ -3684,9 +4290,9 @@ function registerMemoryVerify(memory2) {
3684
4290
  memory2.command("verify").description(
3685
4291
  "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"
3686
4292
  ).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) => {
3687
- const root = findProjectRoot25(opts.dir);
3688
- const paths = resolveHaivePaths22(root);
3689
- if (!existsSync26(paths.memoriesDir)) {
4293
+ const root = findProjectRoot26(opts.dir);
4294
+ const paths = resolveHaivePaths23(root);
4295
+ if (!existsSync28(paths.memoriesDir)) {
3690
4296
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
3691
4297
  process.exitCode = 1;
3692
4298
  return;
@@ -3709,7 +4315,7 @@ function registerMemoryVerify(memory2) {
3709
4315
  anchorlessIds.push(mem.frontmatter.id);
3710
4316
  continue;
3711
4317
  }
3712
- const rel = path26.relative(root, filePath);
4318
+ const rel = path28.relative(root, filePath);
3713
4319
  if (result.stale) {
3714
4320
  staleCount++;
3715
4321
  console.log(`${ui.bold("STALE")} ${mem.frontmatter.id}`);
@@ -3724,7 +4330,7 @@ function registerMemoryVerify(memory2) {
3724
4330
  }
3725
4331
  if (opts.update) {
3726
4332
  const next = applyVerification(mem, result);
3727
- await writeFile13(filePath, serializeMemory10(next), "utf8");
4333
+ await writeFile14(filePath, serializeMemory10(next), "utf8");
3728
4334
  updated++;
3729
4335
  }
3730
4336
  }
@@ -3772,30 +4378,30 @@ function applyVerification(mem, result) {
3772
4378
  }
3773
4379
 
3774
4380
  // src/commands/memory-import.ts
3775
- import { readFile as readFile10 } from "fs/promises";
3776
- import { existsSync as existsSync27 } from "fs";
4381
+ import { readFile as readFile11 } from "fs/promises";
4382
+ import { existsSync as existsSync29 } from "fs";
3777
4383
  import "commander";
3778
4384
  import {
3779
- findProjectRoot as findProjectRoot26,
3780
- resolveHaivePaths as resolveHaivePaths23
4385
+ findProjectRoot as findProjectRoot27,
4386
+ resolveHaivePaths as resolveHaivePaths24
3781
4387
  } from "@hiveai/core";
3782
4388
  function registerMemoryImport(memory2) {
3783
4389
  memory2.command("import").description(
3784
4390
  "Parse a Markdown file and suggest memories via the import_docs MCP prompt (prints a ready-to-use prompt invocation)"
3785
4391
  ).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) => {
3786
- const root = findProjectRoot26(opts.dir);
3787
- const paths = resolveHaivePaths23(root);
3788
- if (!existsSync27(paths.haiveDir)) {
4392
+ const root = findProjectRoot27(opts.dir);
4393
+ const paths = resolveHaivePaths24(root);
4394
+ if (!existsSync29(paths.haiveDir)) {
3789
4395
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
3790
4396
  process.exitCode = 1;
3791
4397
  return;
3792
4398
  }
3793
- if (!existsSync27(opts.from)) {
4399
+ if (!existsSync29(opts.from)) {
3794
4400
  ui.error(`File not found: ${opts.from}`);
3795
4401
  process.exitCode = 1;
3796
4402
  return;
3797
4403
  }
3798
- const content = await readFile10(opts.from, "utf8");
4404
+ const content = await readFile11(opts.from, "utf8");
3799
4405
  const scope = opts.scope ?? "team";
3800
4406
  ui.info(`Preparing import from: ${opts.from} (scope=${scope})`);
3801
4407
  ui.info(`Content length: ${content.length} chars`);
@@ -3823,14 +4429,14 @@ function registerMemoryImport(memory2) {
3823
4429
  }
3824
4430
 
3825
4431
  // src/commands/memory-import-changelog.ts
3826
- import { existsSync as existsSync28 } from "fs";
3827
- import { readFile as readFile11, mkdir as mkdir9, writeFile as writeFile14 } from "fs/promises";
3828
- import path27 from "path";
4432
+ import { existsSync as existsSync30 } from "fs";
4433
+ import { readFile as readFile12, mkdir as mkdir11, writeFile as writeFile15 } from "fs/promises";
4434
+ import path29 from "path";
3829
4435
  import "commander";
3830
4436
  import {
3831
4437
  buildFrontmatter as buildFrontmatter5,
3832
- findProjectRoot as findProjectRoot27,
3833
- resolveHaivePaths as resolveHaivePaths24,
4438
+ findProjectRoot as findProjectRoot28,
4439
+ resolveHaivePaths as resolveHaivePaths25,
3834
4440
  serializeMemory as serializeMemory11
3835
4441
  } from "@hiveai/core";
3836
4442
  function parseChangelog(content) {
@@ -3894,15 +4500,15 @@ function registerMemoryImportChangelog(memory2) {
3894
4500
  "--versions <csv>",
3895
4501
  "only import specific versions (comma-separated), or 'latest' for the most recent breaking version"
3896
4502
  ).option("-d, --dir <dir>", "project root").action(async (opts) => {
3897
- const root = findProjectRoot27(opts.dir);
3898
- const paths = resolveHaivePaths24(root);
3899
- const changelogPath = path27.resolve(root, opts.fromChangelog);
3900
- if (!existsSync28(changelogPath)) {
4503
+ const root = findProjectRoot28(opts.dir);
4504
+ const paths = resolveHaivePaths25(root);
4505
+ const changelogPath = path29.resolve(root, opts.fromChangelog);
4506
+ if (!existsSync30(changelogPath)) {
3901
4507
  ui.error(`CHANGELOG not found: ${changelogPath}`);
3902
4508
  process.exitCode = 1;
3903
4509
  return;
3904
4510
  }
3905
- const content = await readFile11(changelogPath, "utf8");
4511
+ const content = await readFile12(changelogPath, "utf8");
3906
4512
  let entries = parseChangelog(content);
3907
4513
  if (entries.length === 0) {
3908
4514
  ui.warn("No breaking changes, deprecations, or removals found in the CHANGELOG.");
@@ -3916,10 +4522,10 @@ function registerMemoryImportChangelog(memory2) {
3916
4522
  entries = entries.filter((e) => requested.includes(e.version));
3917
4523
  }
3918
4524
  }
3919
- const pkgName = opts.package ?? path27.basename(path27.dirname(changelogPath));
4525
+ const pkgName = opts.package ?? path29.basename(path29.dirname(changelogPath));
3920
4526
  const scope = opts.scope ?? "team";
3921
- const teamDir = path27.join(paths.memoriesDir, scope);
3922
- await mkdir9(teamDir, { recursive: true });
4527
+ const teamDir = path29.join(paths.memoriesDir, scope);
4528
+ await mkdir11(teamDir, { recursive: true });
3923
4529
  let saved = 0;
3924
4530
  for (const entry of entries) {
3925
4531
  const lines = [];
@@ -3941,7 +4547,7 @@ function registerMemoryImportChangelog(memory2) {
3941
4547
  lines.push("");
3942
4548
  }
3943
4549
  lines.push(
3944
- `**Source:** \`${path27.relative(root, changelogPath)}\`
4550
+ `**Source:** \`${path29.relative(root, changelogPath)}\`
3945
4551
  **Action:** Update all usages of ${pkgName} if they rely on any of the above.`
3946
4552
  );
3947
4553
  const slug = `changelog-${pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase()}-v${entry.version.replace(/\./g, "-")}`;
@@ -3956,11 +4562,11 @@ function registerMemoryImportChangelog(memory2) {
3956
4562
  pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase(),
3957
4563
  `v${entry.version}`
3958
4564
  ],
3959
- paths: [path27.relative(root, changelogPath)],
4565
+ paths: [path29.relative(root, changelogPath)],
3960
4566
  topic: `changelog-${pkgName}-${entry.version}`
3961
4567
  });
3962
- await writeFile14(
3963
- path27.join(teamDir, `${fm.id}.md`),
4568
+ await writeFile15(
4569
+ path29.join(teamDir, `${fm.id}.md`),
3964
4570
  serializeMemory11({ frontmatter: fm, body: lines.join("\n") }),
3965
4571
  "utf8"
3966
4572
  );
@@ -3983,17 +4589,17 @@ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${p
3983
4589
  }
3984
4590
 
3985
4591
  // src/commands/memory-digest.ts
3986
- import { existsSync as existsSync29 } from "fs";
3987
- import { writeFile as writeFile15 } from "fs/promises";
3988
- import path28 from "path";
4592
+ import { existsSync as existsSync31 } from "fs";
4593
+ import { writeFile as writeFile16 } from "fs/promises";
4594
+ import path30 from "path";
3989
4595
  import "commander";
3990
4596
  import {
3991
4597
  deriveConfidence as deriveConfidence4,
3992
- findProjectRoot as findProjectRoot28,
4598
+ findProjectRoot as findProjectRoot29,
3993
4599
  getUsage as getUsage8,
3994
4600
  loadMemoriesFromDir as loadMemoriesFromDir5,
3995
- loadUsageIndex as loadUsageIndex10,
3996
- resolveHaivePaths as resolveHaivePaths25
4601
+ loadUsageIndex as loadUsageIndex11,
4602
+ resolveHaivePaths as resolveHaivePaths26
3997
4603
  } from "@hiveai/core";
3998
4604
  var CONFIDENCE_EMOJI = {
3999
4605
  unverified: "\u2B1C",
@@ -4006,9 +4612,9 @@ function registerMemoryDigest(program2) {
4006
4612
  program2.command("digest").description(
4007
4613
  "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"
4008
4614
  ).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) => {
4009
- const root = findProjectRoot28(opts.dir);
4010
- const paths = resolveHaivePaths25(root);
4011
- if (!existsSync29(paths.memoriesDir)) {
4615
+ const root = findProjectRoot29(opts.dir);
4616
+ const paths = resolveHaivePaths26(root);
4617
+ if (!existsSync31(paths.memoriesDir)) {
4012
4618
  ui.error("No .ai/memories found. Run `haive init` first.");
4013
4619
  process.exitCode = 1;
4014
4620
  return;
@@ -4017,7 +4623,7 @@ function registerMemoryDigest(program2) {
4017
4623
  const scopeFilter = opts.scope ?? "team";
4018
4624
  const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
4019
4625
  const all = await loadMemoriesFromDir5(paths.memoriesDir);
4020
- const usage = await loadUsageIndex10(paths);
4626
+ const usage = await loadUsageIndex11(paths);
4021
4627
  const recent = all.filter(({ memory: mem }) => {
4022
4628
  const fm = mem.frontmatter;
4023
4629
  if (fm.type === "session_recap") return false;
@@ -4080,8 +4686,8 @@ function registerMemoryDigest(program2) {
4080
4686
  );
4081
4687
  const digest = lines.join("\n");
4082
4688
  if (opts.out) {
4083
- const outPath = path28.resolve(process.cwd(), opts.out);
4084
- await writeFile15(outPath, digest, "utf8");
4689
+ const outPath = path30.resolve(process.cwd(), opts.out);
4690
+ await writeFile16(outPath, digest, "utf8");
4085
4691
  ui.success(`Digest written to ${opts.out} (${recent.length} memor${recent.length === 1 ? "y" : "ies"})`);
4086
4692
  } else {
4087
4693
  console.log(digest);
@@ -4090,18 +4696,52 @@ function registerMemoryDigest(program2) {
4090
4696
  }
4091
4697
 
4092
4698
  // src/commands/session-end.ts
4093
- import { writeFile as writeFile16, mkdir as mkdir10 } from "fs/promises";
4094
- import { existsSync as existsSync30 } from "fs";
4095
- import path29 from "path";
4699
+ import { writeFile as writeFile17, mkdir as mkdir12, readFile as readFile13, rm } from "fs/promises";
4700
+ import { existsSync as existsSync32 } from "fs";
4701
+ import path31 from "path";
4096
4702
  import "commander";
4097
4703
  import {
4098
4704
  buildFrontmatter as buildFrontmatter6,
4099
- findProjectRoot as findProjectRoot29,
4705
+ findProjectRoot as findProjectRoot30,
4100
4706
  loadMemoriesFromDir as loadMemoriesFromDir6,
4101
4707
  memoryFilePath as memoryFilePath5,
4102
- resolveHaivePaths as resolveHaivePaths26,
4708
+ resolveHaivePaths as resolveHaivePaths27,
4103
4709
  serializeMemory as serializeMemory12
4104
4710
  } from "@hiveai/core";
4711
+ async function buildAutoRecap(paths) {
4712
+ const obsFile = path31.join(paths.haiveDir, ".cache", "observations.jsonl");
4713
+ if (!existsSync32(obsFile)) return null;
4714
+ const raw = await readFile13(obsFile, "utf8").catch(() => "");
4715
+ if (!raw.trim()) return null;
4716
+ const lines = raw.split("\n").filter(Boolean);
4717
+ const obs = [];
4718
+ for (const line of lines) {
4719
+ try {
4720
+ obs.push(JSON.parse(line));
4721
+ } catch {
4722
+ }
4723
+ }
4724
+ if (obs.length === 0) return null;
4725
+ const toolCounts = /* @__PURE__ */ new Map();
4726
+ const fileCounts = /* @__PURE__ */ new Map();
4727
+ const summaries = [];
4728
+ for (const o of obs) {
4729
+ toolCounts.set(o.tool, (toolCounts.get(o.tool) ?? 0) + 1);
4730
+ for (const f of o.files ?? []) fileCounts.set(f, (fileCounts.get(f) ?? 0) + 1);
4731
+ if (summaries.length < 10) summaries.push(`- ${o.summary}`);
4732
+ }
4733
+ const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([t, c]) => `${t} \xD7${c}`).join(", ");
4734
+ const topFiles = [...fileCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8);
4735
+ const goal = `Auto-captured session \u2014 ${obs.length} tool calls (${topTools})`;
4736
+ const accomplished = summaries.length ? `Recent activity:
4737
+ ${summaries.join("\n")}` : `Activity captured but no parseable summaries.`;
4738
+ return {
4739
+ goal,
4740
+ accomplished,
4741
+ files: topFiles.map(([f]) => f),
4742
+ rawCount: obs.length
4743
+ };
4744
+ }
4105
4745
  function buildRecapBody(opts) {
4106
4746
  const lines = [];
4107
4747
  lines.push(`## Goal
@@ -4148,24 +4788,53 @@ function registerSessionEnd(session2) {
4148
4788
  --files src/payments/WebhookController.ts,src/payments/WebhookService.ts \\\\
4149
4789
  --next "Add integration tests for webhook signature validation"
4150
4790
  `
4151
- ).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) => {
4152
- const root = findProjectRoot29(opts.dir);
4153
- const paths = resolveHaivePaths26(root);
4154
- if (!existsSync30(paths.haiveDir)) {
4791
+ ).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) => {
4792
+ const root = findProjectRoot30(opts.dir);
4793
+ const paths = resolveHaivePaths27(root);
4794
+ if (!existsSync32(paths.haiveDir)) {
4795
+ if (opts.auto || opts.quiet) return;
4155
4796
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
4156
4797
  process.exitCode = 1;
4157
4798
  return;
4158
4799
  }
4800
+ let resolvedFiles = opts.files;
4801
+ let goal = opts.goal;
4802
+ let accomplished = opts.accomplished;
4803
+ if (opts.auto) {
4804
+ const synth = await buildAutoRecap(paths);
4805
+ if (!synth) return;
4806
+ goal = goal ?? synth.goal;
4807
+ accomplished = accomplished ?? synth.accomplished;
4808
+ if (!resolvedFiles && synth.files.length) resolvedFiles = synth.files.join(",");
4809
+ }
4810
+ if (!goal || !accomplished) {
4811
+ if (opts.quiet) return;
4812
+ ui.error("session-end requires --goal and --accomplished (or pass --auto with captured observations).");
4813
+ process.exitCode = 1;
4814
+ return;
4815
+ }
4159
4816
  const scope = opts.scope ?? "personal";
4160
- const body = buildRecapBody(opts);
4817
+ const body = buildRecapBody({
4818
+ goal,
4819
+ accomplished,
4820
+ discoveries: opts.discoveries,
4821
+ files: resolvedFiles,
4822
+ next: opts.next
4823
+ });
4161
4824
  const topic = recapTopic(scope, opts.module);
4162
- const filesTouched = parseCsv5(opts.files);
4163
- const missingPaths = filesTouched.filter((p) => !existsSync30(path29.resolve(root, p)));
4164
- if (missingPaths.length > 0) {
4825
+ const filesTouched = parseCsv5(resolvedFiles);
4826
+ const missingPaths = filesTouched.filter((p) => !existsSync32(path31.resolve(root, p)));
4827
+ if (missingPaths.length > 0 && !opts.quiet) {
4165
4828
  ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
4166
4829
  for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
4167
4830
  }
4168
- if (existsSync30(paths.memoriesDir)) {
4831
+ const cleanupObservations = async () => {
4832
+ if (!opts.auto) return;
4833
+ const obsFile = path31.join(paths.haiveDir, ".cache", "observations.jsonl");
4834
+ if (existsSync32(obsFile)) await rm(obsFile).catch(() => {
4835
+ });
4836
+ };
4837
+ if (existsSync32(paths.memoriesDir)) {
4169
4838
  const existing = await loadMemoriesFromDir6(paths.memoriesDir);
4170
4839
  const topicMatch = existing.find(
4171
4840
  ({ memory: memory2 }) => memory2.frontmatter.topic === topic && memory2.frontmatter.scope === scope && (!opts.module || memory2.frontmatter.module === opts.module)
@@ -4181,9 +4850,12 @@ function registerSessionEnd(session2) {
4181
4850
  paths: filesTouched.length ? filesTouched : fm.anchor.paths
4182
4851
  }
4183
4852
  };
4184
- await writeFile16(topicMatch.filePath, serializeMemory12({ frontmatter: newFrontmatter, body }), "utf8");
4185
- ui.success(`Session recap updated (revision #${revisionCount})`);
4186
- ui.info(`id=${fm.id} file=${path29.relative(root, topicMatch.filePath)}`);
4853
+ await writeFile17(topicMatch.filePath, serializeMemory12({ frontmatter: newFrontmatter, body }), "utf8");
4854
+ await cleanupObservations();
4855
+ if (!opts.quiet) {
4856
+ ui.success(`Session recap updated (revision #${revisionCount})`);
4857
+ ui.info(`id=${fm.id} file=${path31.relative(root, topicMatch.filePath)}`);
4858
+ }
4187
4859
  return;
4188
4860
  }
4189
4861
  }
@@ -4198,11 +4870,14 @@ function registerSessionEnd(session2) {
4198
4870
  status: "validated"
4199
4871
  });
4200
4872
  const file = memoryFilePath5(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
4201
- await mkdir10(path29.dirname(file), { recursive: true });
4202
- await writeFile16(file, serializeMemory12({ frontmatter, body }), "utf8");
4203
- ui.success(`Session recap created`);
4204
- ui.info(`id=${frontmatter.id} scope=${scope} file=${path29.relative(root, file)}`);
4205
- ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
4873
+ await mkdir12(path31.dirname(file), { recursive: true });
4874
+ await writeFile17(file, serializeMemory12({ frontmatter, body }), "utf8");
4875
+ await cleanupObservations();
4876
+ if (!opts.quiet) {
4877
+ ui.success(`Session recap created`);
4878
+ ui.info(`id=${frontmatter.id} scope=${scope} file=${path31.relative(root, file)}`);
4879
+ ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
4880
+ }
4206
4881
  });
4207
4882
  }
4208
4883
  function parseCsv5(value) {
@@ -4211,15 +4886,15 @@ function parseCsv5(value) {
4211
4886
  }
4212
4887
 
4213
4888
  // src/commands/snapshot.ts
4214
- import { existsSync as existsSync31 } from "fs";
4889
+ import { existsSync as existsSync33 } from "fs";
4215
4890
  import { readdir as readdir2 } from "fs/promises";
4216
- import path30 from "path";
4891
+ import path32 from "path";
4217
4892
  import "commander";
4218
4893
  import {
4219
4894
  diffContract,
4220
- findProjectRoot as findProjectRoot30,
4895
+ findProjectRoot as findProjectRoot31,
4221
4896
  loadConfig as loadConfig2,
4222
- resolveHaivePaths as resolveHaivePaths27,
4897
+ resolveHaivePaths as resolveHaivePaths28,
4223
4898
  snapshotContract
4224
4899
  } from "@hiveai/core";
4225
4900
  function registerSnapshot(program2) {
@@ -4244,16 +4919,16 @@ function registerSnapshot(program2) {
4244
4919
  "--format <format>",
4245
4920
  "contract format: openapi | graphql | proto | typescript | json-schema (auto-detected if omitted)"
4246
4921
  ).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) => {
4247
- const root = findProjectRoot30(opts.dir);
4248
- const paths = resolveHaivePaths27(root);
4249
- if (!existsSync31(paths.haiveDir)) {
4922
+ const root = findProjectRoot31(opts.dir);
4923
+ const paths = resolveHaivePaths28(root);
4924
+ if (!existsSync33(paths.haiveDir)) {
4250
4925
  ui.error("No .ai/ found. Run `haive init` first.");
4251
4926
  process.exitCode = 1;
4252
4927
  return;
4253
4928
  }
4254
4929
  if (opts.list) {
4255
- const contractsDir = path30.join(paths.haiveDir, "contracts");
4256
- if (!existsSync31(contractsDir)) {
4930
+ const contractsDir = path32.join(paths.haiveDir, "contracts");
4931
+ if (!existsSync33(contractsDir)) {
4257
4932
  console.log(ui.dim("No contract snapshots found."));
4258
4933
  return;
4259
4934
  }
@@ -4308,7 +4983,7 @@ function registerSnapshot(program2) {
4308
4983
  return;
4309
4984
  }
4310
4985
  const contractPath = opts.contract;
4311
- const name = opts.name ?? path30.basename(contractPath, path30.extname(contractPath));
4986
+ const name = opts.name ?? path32.basename(contractPath, path32.extname(contractPath));
4312
4987
  const format = opts.format ?? detectFormat(contractPath) ?? "openapi";
4313
4988
  const contract = { name, path: contractPath, format };
4314
4989
  try {
@@ -4363,8 +5038,8 @@ async function runDiff(root, haiveDir, contract) {
4363
5038
  }
4364
5039
  }
4365
5040
  function detectFormat(filePath) {
4366
- const ext = path30.extname(filePath).toLowerCase();
4367
- const base = path30.basename(filePath).toLowerCase();
5041
+ const ext = path32.extname(filePath).toLowerCase();
5042
+ const base = path32.basename(filePath).toLowerCase();
4368
5043
  if (ext === ".yaml" || ext === ".yml" || ext === ".json") {
4369
5044
  if (base.includes("openapi") || base.includes("swagger")) return "openapi";
4370
5045
  if (base.includes("schema") || base.includes("graphql")) return "graphql";
@@ -4377,16 +5052,16 @@ function detectFormat(filePath) {
4377
5052
  }
4378
5053
 
4379
5054
  // src/commands/hub.ts
4380
- import { existsSync as existsSync32 } from "fs";
4381
- import { mkdir as mkdir11, readFile as readFile12, writeFile as writeFile17, copyFile } from "fs/promises";
4382
- import path31 from "path";
5055
+ import { existsSync as existsSync34 } from "fs";
5056
+ import { mkdir as mkdir13, readFile as readFile14, writeFile as writeFile18, copyFile } from "fs/promises";
5057
+ import path33 from "path";
4383
5058
  import { spawnSync as spawnSync3 } from "child_process";
4384
5059
  import "commander";
4385
5060
  import {
4386
- findProjectRoot as findProjectRoot31,
5061
+ findProjectRoot as findProjectRoot32,
4387
5062
  loadConfig as loadConfig3,
4388
5063
  loadMemoriesFromDir as loadMemoriesFromDir7,
4389
- resolveHaivePaths as resolveHaivePaths28,
5064
+ resolveHaivePaths as resolveHaivePaths29,
4390
5065
  saveConfig as saveConfig2,
4391
5066
  serializeMemory as serializeMemory13
4392
5067
  } from "@hiveai/core";
@@ -4398,8 +5073,8 @@ function registerHub(program2) {
4398
5073
  hub.command("init <hubPath>").description(
4399
5074
  "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"
4400
5075
  ).action(async (hubPath) => {
4401
- const absPath = path31.resolve(hubPath);
4402
- await mkdir11(absPath, { recursive: true });
5076
+ const absPath = path33.resolve(hubPath);
5077
+ await mkdir13(absPath, { recursive: true });
4403
5078
  const gitCheck = spawnSync3("git", ["rev-parse", "--git-dir"], { cwd: absPath });
4404
5079
  if (gitCheck.status !== 0) {
4405
5080
  const init = spawnSync3("git", ["init"], { cwd: absPath, encoding: "utf8" });
@@ -4409,10 +5084,10 @@ function registerHub(program2) {
4409
5084
  return;
4410
5085
  }
4411
5086
  }
4412
- const sharedDir = path31.join(absPath, ".ai", "memories", "shared");
4413
- await mkdir11(sharedDir, { recursive: true });
4414
- await writeFile17(
4415
- path31.join(absPath, ".ai", "README.md"),
5087
+ const sharedDir = path33.join(absPath, ".ai", "memories", "shared");
5088
+ await mkdir13(sharedDir, { recursive: true });
5089
+ await writeFile18(
5090
+ path33.join(absPath, ".ai", "README.md"),
4416
5091
  `# hAIve Team Knowledge Hub
4417
5092
 
4418
5093
  This repo is a shared knowledge hub for hAIve.
@@ -4433,8 +5108,8 @@ haive hub pull # import into a project
4433
5108
  `,
4434
5109
  "utf8"
4435
5110
  );
4436
- await writeFile17(
4437
- path31.join(absPath, ".gitignore"),
5111
+ await writeFile18(
5112
+ path33.join(absPath, ".gitignore"),
4438
5113
  ".ai/.cache/\n.ai/memories/personal/\n",
4439
5114
  "utf8"
4440
5115
  );
@@ -4449,7 +5124,7 @@ haive hub pull # import into a project
4449
5124
  `
4450
5125
  Next steps:
4451
5126
  1. Add hubPath to your project's .ai/haive.config.json:
4452
- { "hubPath": "${path31.relative(process.cwd(), absPath)}" }
5127
+ { "hubPath": "${path33.relative(process.cwd(), absPath)}" }
4453
5128
  2. Run \`haive hub push\` to publish your shared memories
4454
5129
  3. Share ${absPath} with teammates (git remote, NFS, etc.)
4455
5130
  `
@@ -4468,8 +5143,8 @@ Next steps:
4468
5143
  haive hub push --commit --message "feat: add payment API contract memories"
4469
5144
  `
4470
5145
  ).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) => {
4471
- const root = findProjectRoot31(opts.dir);
4472
- const paths = resolveHaivePaths28(root);
5146
+ const root = findProjectRoot32(opts.dir);
5147
+ const paths = resolveHaivePaths29(root);
4473
5148
  const config = await loadConfig3(paths);
4474
5149
  if (!config.hubPath) {
4475
5150
  ui.error(
@@ -4478,15 +5153,15 @@ Next steps:
4478
5153
  process.exitCode = 1;
4479
5154
  return;
4480
5155
  }
4481
- const hubRoot = path31.resolve(root, config.hubPath);
4482
- if (!existsSync32(hubRoot)) {
5156
+ const hubRoot = path33.resolve(root, config.hubPath);
5157
+ if (!existsSync34(hubRoot)) {
4483
5158
  ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
4484
5159
  process.exitCode = 1;
4485
5160
  return;
4486
5161
  }
4487
- const projectName = path31.basename(root);
4488
- const destDir = path31.join(hubRoot, ".ai", "memories", "shared", projectName);
4489
- await mkdir11(destDir, { recursive: true });
5162
+ const projectName = path33.basename(root);
5163
+ const destDir = path33.join(hubRoot, ".ai", "memories", "shared", projectName);
5164
+ await mkdir13(destDir, { recursive: true });
4490
5165
  const all = await loadMemoriesFromDir7(paths.memoriesDir);
4491
5166
  const shared = all.filter(
4492
5167
  ({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && // Don't push imported memories (avoid echo loops)
@@ -4504,15 +5179,15 @@ Next steps:
4504
5179
  for (const { memory: memory2 } of shared) {
4505
5180
  const fm = memory2.frontmatter;
4506
5181
  const fileName = `${fm.id}.md`;
4507
- const destPath = path31.join(destDir, fileName);
4508
- await writeFile17(destPath, serializeMemory13(memory2), "utf8");
5182
+ const destPath = path33.join(destDir, fileName);
5183
+ await writeFile18(destPath, serializeMemory13(memory2), "utf8");
4509
5184
  pushed++;
4510
5185
  }
4511
5186
  console.log(ui.green(`\u2713 Pushed ${pushed} shared memor${pushed === 1 ? "y" : "ies"} to hub`));
4512
5187
  console.log(ui.dim(` Location: ${destDir}`));
4513
5188
  if (opts.commit) {
4514
5189
  const message = opts.message ?? `haive: sync shared memories from ${projectName} (${pushed} memories)`;
4515
- spawnSync3("git", ["add", path31.join(".ai", "memories", "shared", projectName)], {
5190
+ spawnSync3("git", ["add", path33.join(".ai", "memories", "shared", projectName)], {
4516
5191
  cwd: hubRoot
4517
5192
  });
4518
5193
  const commit = spawnSync3("git", ["commit", "-m", message], {
@@ -4537,8 +5212,8 @@ Next steps:
4537
5212
  hub.command("pull").description(
4538
5213
  "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"
4539
5214
  ).option("-d, --dir <dir>", "project root").action(async (opts) => {
4540
- const root = findProjectRoot31(opts.dir);
4541
- const paths = resolveHaivePaths28(root);
5215
+ const root = findProjectRoot32(opts.dir);
5216
+ const paths = resolveHaivePaths29(root);
4542
5217
  const config = await loadConfig3(paths);
4543
5218
  if (!config.hubPath) {
4544
5219
  ui.error(
@@ -4547,13 +5222,13 @@ Next steps:
4547
5222
  process.exitCode = 1;
4548
5223
  return;
4549
5224
  }
4550
- const hubRoot = path31.resolve(root, config.hubPath);
4551
- const hubSharedDir = path31.join(hubRoot, ".ai", "memories", "shared");
4552
- if (!existsSync32(hubSharedDir)) {
5225
+ const hubRoot = path33.resolve(root, config.hubPath);
5226
+ const hubSharedDir = path33.join(hubRoot, ".ai", "memories", "shared");
5227
+ if (!existsSync34(hubSharedDir)) {
4553
5228
  ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
4554
5229
  return;
4555
5230
  }
4556
- const projectName = path31.basename(root);
5231
+ const projectName = path33.basename(root);
4557
5232
  const { readdir: readdir3 } = await import("fs/promises");
4558
5233
  const projectDirs = (await readdir3(hubSharedDir, { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== projectName).map((d) => d.name);
4559
5234
  if (projectDirs.length === 0) {
@@ -4563,17 +5238,17 @@ Next steps:
4563
5238
  let totalImported = 0;
4564
5239
  let totalUpdated = 0;
4565
5240
  for (const sourceName of projectDirs) {
4566
- const sourceDir = path31.join(hubSharedDir, sourceName);
4567
- const destDir = path31.join(paths.memoriesDir, "shared", sourceName);
4568
- await mkdir11(destDir, { recursive: true });
5241
+ const sourceDir = path33.join(hubSharedDir, sourceName);
5242
+ const destDir = path33.join(paths.memoriesDir, "shared", sourceName);
5243
+ await mkdir13(destDir, { recursive: true });
4569
5244
  const sourceFiles = (await readdir3(sourceDir)).filter((f) => f.endsWith(".md"));
4570
5245
  const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
4571
5246
  const existingInDest = await loadDir(destDir);
4572
5247
  const existingIds = new Set(existingInDest.map(({ memory: memory2 }) => memory2.frontmatter.id));
4573
5248
  for (const file of sourceFiles) {
4574
- const srcPath = path31.join(sourceDir, file);
4575
- const destPath = path31.join(destDir, file);
4576
- const fileContent = await readFile12(srcPath, "utf8");
5249
+ const srcPath = path33.join(sourceDir, file);
5250
+ const destPath = path33.join(destDir, file);
5251
+ const fileContent = await readFile14(srcPath, "utf8");
4577
5252
  const alreadyTagged = fileContent.includes(`cross-repo:${sourceName}`);
4578
5253
  if (!alreadyTagged) {
4579
5254
  await copyFile(srcPath, destPath);
@@ -4596,21 +5271,21 @@ Next steps:
4596
5271
  );
4597
5272
  });
4598
5273
  hub.command("status").description("Show hub sync status.").option("-d, --dir <dir>", "project root").action(async (opts) => {
4599
- const root = findProjectRoot31(opts.dir);
4600
- const paths = resolveHaivePaths28(root);
5274
+ const root = findProjectRoot32(opts.dir);
5275
+ const paths = resolveHaivePaths29(root);
4601
5276
  const config = await loadConfig3(paths);
4602
5277
  console.log(ui.bold("Hub status"));
4603
5278
  console.log(
4604
5279
  ` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
4605
5280
  );
4606
- const sharedDir = path31.join(paths.memoriesDir, "shared");
4607
- if (existsSync32(sharedDir)) {
5281
+ const sharedDir = path33.join(paths.memoriesDir, "shared");
5282
+ if (existsSync34(sharedDir)) {
4608
5283
  const { readdir: readdir3 } = await import("fs/promises");
4609
5284
  const sources = (await readdir3(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
4610
5285
  console.log(`
4611
5286
  Imported from ${sources.length} source(s):`);
4612
5287
  for (const src of sources) {
4613
- const files = (await readdir3(path31.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
5288
+ const files = (await readdir3(path33.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
4614
5289
  console.log(` ${src}: ${files.length} memor${files.length === 1 ? "y" : "ies"}`);
4615
5290
  }
4616
5291
  } else {
@@ -4625,8 +5300,8 @@ Next steps:
4625
5300
  if (outgoing.length > 0) {
4626
5301
  console.log(ui.dim(" Run `haive hub push` to publish them to the hub."));
4627
5302
  }
4628
- void readFile12;
4629
- void writeFile17;
5303
+ void readFile14;
5304
+ void writeFile18;
4630
5305
  void saveConfig2;
4631
5306
  });
4632
5307
  }
@@ -4635,16 +5310,21 @@ Next steps:
4635
5310
  import "commander";
4636
5311
  import {
4637
5312
  aggregateUsage,
4638
- findProjectRoot as findProjectRoot32,
5313
+ findProjectRoot as findProjectRoot33,
5314
+ loadUsageIndex as loadUsageIndex12,
4639
5315
  parseSince,
4640
5316
  readUsageEvents,
4641
- resolveHaivePaths as resolveHaivePaths29,
5317
+ resolveHaivePaths as resolveHaivePaths30,
4642
5318
  usageLogSize
4643
5319
  } from "@hiveai/core";
4644
5320
  function registerStats(program2) {
4645
- 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) => {
4646
- const root = findProjectRoot32(opts.dir);
4647
- const paths = resolveHaivePaths29(root);
5321
+ 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) => {
5322
+ const root = findProjectRoot33(opts.dir);
5323
+ const paths = resolveHaivePaths30(root);
5324
+ if (opts.memoryHits) {
5325
+ await renderMemoryHits(paths, opts);
5326
+ return;
5327
+ }
4648
5328
  const size = await usageLogSize(paths);
4649
5329
  if (!size.exists) {
4650
5330
  if (opts.json) {
@@ -4689,14 +5369,57 @@ function registerStats(program2) {
4689
5369
  }
4690
5370
  });
4691
5371
  }
5372
+ async function renderMemoryHits(paths, opts) {
5373
+ const index = await loadUsageIndex12(paths);
5374
+ const since = parseSince(opts.since ?? "30d");
5375
+ const sinceMs = since ? new Date(since).getTime() : null;
5376
+ const entries = Object.entries(index.by_id).map(([id, usage]) => ({ id, ...usage })).filter((e) => e.read_count > 0).filter((e) => {
5377
+ if (!sinceMs || !e.last_read_at) return !sinceMs;
5378
+ return new Date(e.last_read_at).getTime() >= sinceMs;
5379
+ }).sort((a, b) => b.read_count - a.read_count);
5380
+ if (opts.json) {
5381
+ console.log(JSON.stringify({
5382
+ window: opts.since ?? "30d",
5383
+ total_mems_with_hits: entries.length,
5384
+ top: entries.slice(0, 50)
5385
+ }, null, 2));
5386
+ return;
5387
+ }
5388
+ const window = opts.since ?? "30d";
5389
+ console.log(ui.bold(`Memory hits (${window})`));
5390
+ if (entries.length === 0) {
5391
+ ui.info(
5392
+ `No memory reads recorded in window. Reads are logged when \`haive briefing\` or \`haive memory query\` surface a memory.`
5393
+ );
5394
+ return;
5395
+ }
5396
+ console.log(
5397
+ ` ${ui.dim("memories with hits:")} ${entries.length} ${ui.dim("total reads:")} ${entries.reduce((a, e) => a + e.read_count, 0)}`
5398
+ );
5399
+ console.log();
5400
+ console.log(ui.bold("Top read memories:"));
5401
+ const maxCount = entries[0].read_count;
5402
+ for (const e of entries.slice(0, 25)) {
5403
+ const bar = "\u2588".repeat(Math.max(1, Math.round(e.read_count / maxCount * 20)));
5404
+ const lastRead = e.last_read_at?.slice(0, 10) ?? "?";
5405
+ console.log(
5406
+ ` ${ui.bold(String(e.read_count).padStart(4))} ${ui.green(bar.padEnd(20))} ${e.id} ${ui.dim(`last: ${lastRead}`)}`
5407
+ );
5408
+ }
5409
+ const dead = Object.keys(index.by_id).length - entries.length;
5410
+ if (dead > 0) {
5411
+ console.log();
5412
+ ui.info(`${dead} memor${dead === 1 ? "y" : "ies"} never read in window \u2014 candidates for cleanup (run \`haive doctor\`).`);
5413
+ }
5414
+ }
4692
5415
 
4693
5416
  // src/commands/bench.ts
4694
5417
  import { performance } from "perf_hooks";
4695
5418
  import "commander";
4696
5419
  import {
4697
5420
  estimateTokens,
4698
- findProjectRoot as findProjectRoot33,
4699
- resolveHaivePaths as resolveHaivePaths30
5421
+ findProjectRoot as findProjectRoot34,
5422
+ resolveHaivePaths as resolveHaivePaths31
4700
5423
  } from "@hiveai/core";
4701
5424
  import {
4702
5425
  antiPatternsCheck,
@@ -4708,8 +5431,8 @@ import {
4708
5431
  } from "@hiveai/mcp";
4709
5432
  function registerBench(program2) {
4710
5433
  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) => {
4711
- const root = findProjectRoot33(opts.dir);
4712
- const paths = resolveHaivePaths30(root);
5434
+ const root = findProjectRoot34(opts.dir);
5435
+ const paths = resolveHaivePaths31(root);
4713
5436
  const ctx = { paths };
4714
5437
  const task = opts.task ?? "audit dependencies for security risks";
4715
5438
  const scenarios = [
@@ -4828,19 +5551,19 @@ function summarize(name, t0, payload, notes) {
4828
5551
  }
4829
5552
 
4830
5553
  // src/commands/memory-suggest.ts
4831
- import { mkdir as mkdir12, writeFile as writeFile18 } from "fs/promises";
4832
- import { existsSync as existsSync33 } from "fs";
4833
- import path32 from "path";
5554
+ import { mkdir as mkdir14, writeFile as writeFile19 } from "fs/promises";
5555
+ import { existsSync as existsSync35 } from "fs";
5556
+ import path34 from "path";
4834
5557
  import "commander";
4835
5558
  import {
4836
5559
  aggregateUsage as aggregateUsage2,
4837
5560
  buildFrontmatter as buildFrontmatter7,
4838
- findProjectRoot as findProjectRoot34,
5561
+ findProjectRoot as findProjectRoot35,
4839
5562
  loadMemoriesFromDir as loadMemoriesFromDir8,
4840
5563
  memoryFilePath as memoryFilePath6,
4841
5564
  parseSince as parseSince2,
4842
5565
  readUsageEvents as readUsageEvents2,
4843
- resolveHaivePaths as resolveHaivePaths31,
5566
+ resolveHaivePaths as resolveHaivePaths32,
4844
5567
  serializeMemory as serializeMemory14
4845
5568
  } from "@hiveai/core";
4846
5569
  var SEARCH_TOOLS = /* @__PURE__ */ new Set([
@@ -4853,8 +5576,8 @@ function registerMemorySuggest(memory2) {
4853
5576
  memory2.command("suggest").description(
4854
5577
  "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."
4855
5578
  ).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) => {
4856
- const root = findProjectRoot34(opts.dir);
4857
- const paths = resolveHaivePaths31(root);
5579
+ const root = findProjectRoot35(opts.dir);
5580
+ const paths = resolveHaivePaths32(root);
4858
5581
  const events = await readUsageEvents2(paths);
4859
5582
  if (events.length === 0) {
4860
5583
  if (opts.json) {
@@ -4900,7 +5623,7 @@ function registerMemorySuggest(memory2) {
4900
5623
  }
4901
5624
  const created = [];
4902
5625
  const skipped = [];
4903
- const existing = existsSync33(paths.memoriesDir) ? await loadMemoriesFromDir8(paths.memoriesDir) : [];
5626
+ const existing = existsSync35(paths.memoriesDir) ? await loadMemoriesFromDir8(paths.memoriesDir) : [];
4904
5627
  for (const s of top) {
4905
5628
  const slug = slugify(s.query);
4906
5629
  if (!slug) {
@@ -4923,13 +5646,13 @@ function registerMemorySuggest(memory2) {
4923
5646
  fm.status = "draft";
4924
5647
  const body = renderTemplate(s);
4925
5648
  const file = memoryFilePath6(paths, fm.scope, fm.id, fm.module);
4926
- await mkdir12(path32.dirname(file), { recursive: true });
4927
- if (existsSync33(file)) {
4928
- skipped.push({ query: s.query, reason: `file already exists at ${path32.relative(root, file)}` });
5649
+ await mkdir14(path34.dirname(file), { recursive: true });
5650
+ if (existsSync35(file)) {
5651
+ skipped.push({ query: s.query, reason: `file already exists at ${path34.relative(root, file)}` });
4929
5652
  continue;
4930
5653
  }
4931
- await writeFile18(file, serializeMemory14({ frontmatter: fm, body }), "utf8");
4932
- created.push({ id: fm.id, file: path32.relative(root, file), query: s.query });
5654
+ await writeFile19(file, serializeMemory14({ frontmatter: fm, body }), "utf8");
5655
+ created.push({ id: fm.id, file: path34.relative(root, file), query: s.query });
4933
5656
  }
4934
5657
  if (opts.json) {
4935
5658
  console.log(JSON.stringify({ created, skipped }, null, 2));
@@ -4937,10 +5660,10 @@ function registerMemorySuggest(memory2) {
4937
5660
  }
4938
5661
  for (const c of created) {
4939
5662
  ui.success(`Drafted ${c.id} \u2192 ${c.file}`);
4940
- console.log(` ${ui.dim("from query:")} ${truncate(c.query, 60)}`);
5663
+ console.log(` ${ui.dim("from query:")} ${truncate2(c.query, 60)}`);
4941
5664
  }
4942
5665
  for (const s of skipped) {
4943
- ui.warn(`Skipped: ${truncate(s.query, 50)} \u2014 ${s.reason}`);
5666
+ ui.warn(`Skipped: ${truncate2(s.query, 50)} \u2014 ${s.reason}`);
4944
5667
  }
4945
5668
  if (created.length > 0) {
4946
5669
  console.log();
@@ -4964,7 +5687,7 @@ function registerMemorySuggest(memory2) {
4964
5687
  }
4965
5688
  for (const s of suggestions.slice(0, 30)) {
4966
5689
  console.log(
4967
- ` ${ui.bold(`\xD7${s.count}`)} ${ui.dim(`[${s.tools.join(",")}]`)} ${truncate(s.query, 70)}`
5690
+ ` ${ui.bold(`\xD7${s.count}`)} ${ui.dim(`[${s.tools.join(",")}]`)} ${truncate2(s.query, 70)}`
4968
5691
  );
4969
5692
  console.log(` ${ui.dim("\u2192")} ${s.reason}`);
4970
5693
  }
@@ -5010,28 +5733,28 @@ function renderTemplate(s) {
5010
5733
  `- **Why** \u2014 the rationale or root cause`,
5011
5734
  `- **How to apply** \u2014 what an agent should do when this comes up again`,
5012
5735
  ``,
5013
- `Then run \`haive memory promote ${truncate(s.query, 30)}\` to mark it validated.`
5736
+ `Then run \`haive memory promote ${truncate2(s.query, 30)}\` to mark it validated.`
5014
5737
  ].join("\n");
5015
5738
  }
5016
5739
  function slugify(s) {
5017
5740
  return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
5018
5741
  }
5019
- function truncate(text, max) {
5742
+ function truncate2(text, max) {
5020
5743
  if (text.length <= max) return text;
5021
5744
  return text.slice(0, max - 1) + "\u2026";
5022
5745
  }
5023
5746
 
5024
5747
  // src/commands/memory-archive.ts
5025
- import { existsSync as existsSync34 } from "fs";
5026
- import { writeFile as writeFile19 } from "fs/promises";
5027
- import path33 from "path";
5748
+ import { existsSync as existsSync36 } from "fs";
5749
+ import { writeFile as writeFile20 } from "fs/promises";
5750
+ import path35 from "path";
5028
5751
  import "commander";
5029
5752
  import {
5030
- findProjectRoot as findProjectRoot35,
5753
+ findProjectRoot as findProjectRoot36,
5031
5754
  getUsage as getUsage9,
5032
5755
  loadMemoriesFromDir as loadMemoriesFromDir9,
5033
- loadUsageIndex as loadUsageIndex11,
5034
- resolveHaivePaths as resolveHaivePaths32,
5756
+ loadUsageIndex as loadUsageIndex13,
5757
+ resolveHaivePaths as resolveHaivePaths33,
5035
5758
  serializeMemory as serializeMemory15
5036
5759
  } from "@hiveai/core";
5037
5760
  var MS_PER_DAY = 24 * 60 * 60 * 1e3;
@@ -5039,9 +5762,9 @@ function registerMemoryArchive(memory2) {
5039
5762
  memory2.command("archive").description(
5040
5763
  "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."
5041
5764
  ).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) => {
5042
- const root = findProjectRoot35(opts.dir);
5043
- const paths = resolveHaivePaths32(root);
5044
- if (!existsSync34(paths.memoriesDir)) {
5765
+ const root = findProjectRoot36(opts.dir);
5766
+ const paths = resolveHaivePaths33(root);
5767
+ if (!existsSync36(paths.memoriesDir)) {
5045
5768
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
5046
5769
  process.exitCode = 1;
5047
5770
  return;
@@ -5054,7 +5777,7 @@ function registerMemoryArchive(memory2) {
5054
5777
  }
5055
5778
  const cutoff = Date.now() - minDays * MS_PER_DAY;
5056
5779
  const all = await loadMemoriesFromDir9(paths.memoriesDir);
5057
- const usage = await loadUsageIndex11(paths);
5780
+ const usage = await loadUsageIndex13(paths);
5058
5781
  const typeFilter = opts.type === "all" ? null : opts.type ?? "attempt";
5059
5782
  const candidates = [];
5060
5783
  for (const { memory: mem, filePath } of all) {
@@ -5062,7 +5785,7 @@ function registerMemoryArchive(memory2) {
5062
5785
  if (typeFilter && fm.type !== typeFilter) continue;
5063
5786
  if (fm.status === "deprecated" || fm.status === "rejected") continue;
5064
5787
  const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
5065
- const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync34(path33.join(paths.root, p)));
5788
+ const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync36(path35.join(paths.root, p)));
5066
5789
  const isAnchorless = !hasAnyAnchor;
5067
5790
  if (!isAnchorless && !allPathsGone) continue;
5068
5791
  const u = getUsage9(usage, fm.id);
@@ -5110,7 +5833,7 @@ function registerMemoryArchive(memory2) {
5110
5833
  if (!found) continue;
5111
5834
  const fm = { ...found.memory.frontmatter, status: "deprecated" };
5112
5835
  try {
5113
- await writeFile19(c.filePath, serializeMemory15({ frontmatter: fm, body: found.memory.body }), "utf8");
5836
+ await writeFile20(c.filePath, serializeMemory15({ frontmatter: fm, body: found.memory.body }), "utf8");
5114
5837
  archived++;
5115
5838
  } catch (err) {
5116
5839
  if (!opts.json) {
@@ -5136,30 +5859,30 @@ function parseDays(input) {
5136
5859
  }
5137
5860
 
5138
5861
  // src/commands/doctor.ts
5139
- import { existsSync as existsSync35 } from "fs";
5862
+ import { existsSync as existsSync37 } from "fs";
5140
5863
  import { stat } from "fs/promises";
5141
5864
  import "path";
5142
5865
  import "commander";
5143
5866
  import {
5144
5867
  codeMapPath as codeMapPath2,
5145
- findProjectRoot as findProjectRoot36,
5868
+ findProjectRoot as findProjectRoot37,
5146
5869
  getUsage as getUsage10,
5147
5870
  loadCodeMap as loadCodeMap3,
5148
5871
  loadConfig as loadConfig4,
5149
5872
  loadMemoriesFromDir as loadMemoriesFromDir10,
5150
- loadUsageIndex as loadUsageIndex12,
5873
+ loadUsageIndex as loadUsageIndex14,
5151
5874
  readUsageEvents as readUsageEvents3,
5152
- resolveHaivePaths as resolveHaivePaths33
5875
+ resolveHaivePaths as resolveHaivePaths34
5153
5876
  } from "@hiveai/core";
5154
5877
  var MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
5155
5878
  function registerDoctor(program2) {
5156
5879
  program2.command("doctor").description(
5157
5880
  "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."
5158
5881
  ).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) => {
5159
- const root = findProjectRoot36(opts.dir);
5160
- const paths = resolveHaivePaths33(root);
5882
+ const root = findProjectRoot37(opts.dir);
5883
+ const paths = resolveHaivePaths34(root);
5161
5884
  const findings = [];
5162
- if (!existsSync35(paths.haiveDir)) {
5885
+ if (!existsSync37(paths.haiveDir)) {
5163
5886
  findings.push({
5164
5887
  severity: "error",
5165
5888
  code: "not-initialized",
@@ -5168,7 +5891,7 @@ function registerDoctor(program2) {
5168
5891
  });
5169
5892
  return emit(findings, opts);
5170
5893
  }
5171
- if (!existsSync35(paths.projectContext)) {
5894
+ if (!existsSync37(paths.projectContext)) {
5172
5895
  findings.push({
5173
5896
  severity: "warn",
5174
5897
  code: "no-project-context",
@@ -5176,8 +5899,8 @@ function registerDoctor(program2) {
5176
5899
  fix: "haive init"
5177
5900
  });
5178
5901
  } else {
5179
- const { readFile: readFile13 } = await import("fs/promises");
5180
- const content = await readFile13(paths.projectContext, "utf8");
5902
+ const { readFile: readFile15 } = await import("fs/promises");
5903
+ const content = await readFile15(paths.projectContext, "utf8");
5181
5904
  const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
5182
5905
  if (isTemplate) {
5183
5906
  findings.push({
@@ -5188,7 +5911,7 @@ function registerDoctor(program2) {
5188
5911
  });
5189
5912
  }
5190
5913
  }
5191
- const memories = existsSync35(paths.memoriesDir) ? await loadMemoriesFromDir10(paths.memoriesDir) : [];
5914
+ const memories = existsSync37(paths.memoriesDir) ? await loadMemoriesFromDir10(paths.memoriesDir) : [];
5192
5915
  const now = Date.now();
5193
5916
  if (memories.length === 0) {
5194
5917
  findings.push({
@@ -5197,7 +5920,7 @@ function registerDoctor(program2) {
5197
5920
  message: "No memories yet. Capture knowledge as agents work via mem_save / mem_observe / mem_tried."
5198
5921
  });
5199
5922
  } else {
5200
- const usage = await loadUsageIndex12(paths);
5923
+ const usage = await loadUsageIndex14(paths);
5201
5924
  const stale = memories.filter((m) => m.memory.frontmatter.status === "stale");
5202
5925
  if (stale.length > 0) {
5203
5926
  findings.push({
@@ -5342,22 +6065,22 @@ function isSearchTool(name) {
5342
6065
  }
5343
6066
 
5344
6067
  // src/commands/playback.ts
5345
- import { existsSync as existsSync36 } from "fs";
6068
+ import { existsSync as existsSync38 } from "fs";
5346
6069
  import "commander";
5347
6070
  import {
5348
- findProjectRoot as findProjectRoot37,
6071
+ findProjectRoot as findProjectRoot38,
5349
6072
  loadMemoriesFromDir as loadMemoriesFromDir11,
5350
6073
  parseSince as parseSince3,
5351
6074
  readUsageEvents as readUsageEvents4,
5352
- resolveHaivePaths as resolveHaivePaths34
6075
+ resolveHaivePaths as resolveHaivePaths35
5353
6076
  } from "@hiveai/core";
5354
6077
  var MS_PER_MINUTE = 6e4;
5355
6078
  function registerPlayback(program2) {
5356
6079
  program2.command("playback").description(
5357
6080
  "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?'"
5358
6081
  ).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) => {
5359
- const root = findProjectRoot37(opts.dir);
5360
- const paths = resolveHaivePaths34(root);
6082
+ const root = findProjectRoot38(opts.dir);
6083
+ const paths = resolveHaivePaths35(root);
5361
6084
  const events = await readUsageEvents4(paths);
5362
6085
  if (events.length === 0) {
5363
6086
  if (opts.json) {
@@ -5372,7 +6095,7 @@ function registerPlayback(program2) {
5372
6095
  const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
5373
6096
  const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
5374
6097
  const sessions = bucketSessions(filtered, gapMs);
5375
- const all = existsSync36(paths.memoriesDir) ? await loadMemoriesFromDir11(paths.memoriesDir) : [];
6098
+ const all = existsSync38(paths.memoriesDir) ? await loadMemoriesFromDir11(paths.memoriesDir) : [];
5376
6099
  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);
5377
6100
  const enriched = sessions.map((s, i) => {
5378
6101
  const startMs = Date.parse(s.start);
@@ -5412,7 +6135,7 @@ function registerPlayback(program2) {
5412
6135
  if (s.briefing_tasks.length > 0) {
5413
6136
  console.log(` ${ui.dim("briefings asked:")}`);
5414
6137
  for (const t of s.briefing_tasks) {
5415
- console.log(` \u2022 ${truncate2(t, 80)}`);
6138
+ console.log(` \u2022 ${truncate3(t, 80)}`);
5416
6139
  }
5417
6140
  }
5418
6141
  if (s.memories_created_since > 0) {
@@ -5453,7 +6176,7 @@ function countTools(events) {
5453
6176
  for (const e of events) out[e.tool] = (out[e.tool] ?? 0) + 1;
5454
6177
  return out;
5455
6178
  }
5456
- function truncate2(text, max) {
6179
+ function truncate3(text, max) {
5457
6180
  if (text.length <= max) return text;
5458
6181
  return text.slice(0, max - 1) + "\u2026";
5459
6182
  }
@@ -5462,8 +6185,8 @@ function truncate2(text, max) {
5462
6185
  import { spawn as spawn3 } from "child_process";
5463
6186
  import "commander";
5464
6187
  import {
5465
- findProjectRoot as findProjectRoot38,
5466
- resolveHaivePaths as resolveHaivePaths35
6188
+ findProjectRoot as findProjectRoot39,
6189
+ resolveHaivePaths as resolveHaivePaths36
5467
6190
  } from "@hiveai/core";
5468
6191
  import { preCommitCheck } from "@hiveai/mcp";
5469
6192
  function registerPrecommit(program2) {
@@ -5474,8 +6197,8 @@ function registerPrecommit(program2) {
5474
6197
  "'any' | 'high-confidence' (default) | 'never' (report only)",
5475
6198
  "high-confidence"
5476
6199
  ).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) => {
5477
- const root = findProjectRoot38(opts.dir);
5478
- const paths = resolveHaivePaths35(root);
6200
+ const root = findProjectRoot39(opts.dir);
6201
+ const paths = resolveHaivePaths36(root);
5479
6202
  const ctx = { paths };
5480
6203
  let diff = "";
5481
6204
  let touchedPaths = opts.paths ?? [];
@@ -5567,8 +6290,8 @@ function runCommand(cmd, args, cwd) {
5567
6290
  }
5568
6291
 
5569
6292
  // src/index.ts
5570
- var program = new Command39();
5571
- program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.7.2");
6293
+ var program = new Command40();
6294
+ program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.9.0");
5572
6295
  registerInit(program);
5573
6296
  registerMcp(program);
5574
6297
  registerBriefing(program);
@@ -5576,6 +6299,7 @@ registerTui(program);
5576
6299
  registerEmbeddings(program);
5577
6300
  registerSync(program);
5578
6301
  registerInstallHooks(program);
6302
+ registerObserve(program);
5579
6303
  registerIndexCode(program);
5580
6304
  var memory = program.command("memory").description("Manage memory entries");
5581
6305
  registerMemoryAdd(memory);