@hiveai/cli 0.4.5 → 0.6.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,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command32 } from "commander";
4
+ import { Command as Command39 } from "commander";
5
5
 
6
6
  // src/commands/briefing.ts
7
7
  import { existsSync } from "fs";
8
8
  import { readFile } from "fs/promises";
9
+ import path from "path";
9
10
  import "commander";
10
11
  import {
11
12
  findProjectRoot,
@@ -58,7 +59,12 @@ function registerBriefing(program2) {
58
59
  "--scope <scope>",
59
60
  "personal | team | shared | all (default: all \u2014 includes team + shared cross-repo memories)",
60
61
  "all"
61
- ).option("--include-draft", "include draft memories (excluded by default)").option("--include-stale", "include stale memories (excluded by default \u2014 may be outdated)").option("-d, --dir <dir>", "project root").action(async (opts) => {
62
+ ).option("--include-draft", "include draft memories (excluded by default)").option("--include-stale", "include stale memories (excluded by default \u2014 may be outdated)").option(
63
+ "--include <path>",
64
+ "merge memories from another haive-initialized project (repeatable). Useful for teams with multiple coordinated repos (e.g. backend + frontend).",
65
+ collectInclude,
66
+ []
67
+ ).option("-d, --dir <dir>", "project root").action(async (opts) => {
62
68
  const root = findProjectRoot(opts.dir);
63
69
  const paths = resolveHaivePaths(root);
64
70
  if (!existsSync(paths.memoriesDir)) {
@@ -71,7 +77,34 @@ function registerBriefing(program2) {
71
77
  }
72
78
  return;
73
79
  }
74
- const all = await loadMemoriesFromDir(paths.memoriesDir);
80
+ const ownMemories = await loadMemoriesFromDir(paths.memoriesDir);
81
+ const externalRoots = [];
82
+ if (opts.include && opts.include.length > 0) {
83
+ for (const includePath of opts.include) {
84
+ try {
85
+ const otherRoot = findProjectRoot(includePath);
86
+ if (otherRoot === root) continue;
87
+ const otherPaths = resolveHaivePaths(otherRoot);
88
+ if (!existsSync(otherPaths.memoriesDir)) {
89
+ ui.warn(`--include ${includePath}: no .ai/memories at ${otherRoot} \u2014 skipping`);
90
+ continue;
91
+ }
92
+ const otherMemories = await loadMemoriesFromDir(otherPaths.memoriesDir);
93
+ const tag = path.basename(otherRoot);
94
+ for (const m of otherMemories) {
95
+ ownMemories.push({ ...m, origin: tag });
96
+ }
97
+ externalRoots.push(`${tag} (${otherMemories.length})`);
98
+ } catch (err) {
99
+ ui.warn(`--include ${includePath}: ${err instanceof Error ? err.message : String(err)}`);
100
+ }
101
+ }
102
+ if (externalRoots.length > 0) {
103
+ ui.info(`merged from: ${externalRoots.join(", ")}`);
104
+ console.log();
105
+ }
106
+ }
107
+ const all = ownMemories;
75
108
  const filePaths = parseCsv(opts.files);
76
109
  const tokens = opts.task ? tokenizeQuery(opts.task) : null;
77
110
  const maxMemories = Math.max(1, Number(opts.maxMemories ?? 10));
@@ -148,15 +181,16 @@ function registerBriefing(program2) {
148
181
  }
149
182
  console.log(`${ui.bold("=== Relevant Memories ===")}
150
183
  `);
151
- for (const { memory: mem } of top) {
152
- const fm = mem.frontmatter;
184
+ for (const item of top) {
185
+ const fm = item.memory.frontmatter;
153
186
  const badge = ui.statusBadge(fm.status);
154
187
  const draftMarker = fm.status === "draft" ? ui.yellow(" [DRAFT]") : "";
155
188
  const unverifiedMarker = fm.status === "proposed" ? ui.yellow(" [UNVERIFIED]") : "";
189
+ const originMarker = item.origin ? ` ${ui.yellow("[from " + item.origin + "]")}` : "";
156
190
  console.log(
157
- `${ui.bold(fm.id)} ${ui.dim(fm.scope + "/" + fm.type)} ${badge}${draftMarker}${unverifiedMarker}`
191
+ `${ui.bold(fm.id)} ${ui.dim(fm.scope + "/" + fm.type)} ${badge}${draftMarker}${unverifiedMarker}${originMarker}`
158
192
  );
159
- console.log(mem.body.trim());
193
+ console.log(item.memory.body.trim());
160
194
  console.log();
161
195
  }
162
196
  console.log(ui.dim(`${top.length} memor${top.length === 1 ? "y" : "ies"} surfaced`));
@@ -199,6 +233,9 @@ function parseCsv(value) {
199
233
  if (!value) return [];
200
234
  return value.split(",").map((s) => s.trim()).filter(Boolean);
201
235
  }
236
+ function collectInclude(value, previous) {
237
+ return [...previous, value];
238
+ }
202
239
 
203
240
  // src/commands/tui.ts
204
241
  import "commander";
@@ -223,7 +260,7 @@ function registerTui(program2) {
223
260
 
224
261
  // src/commands/embeddings.ts
225
262
  import { existsSync as existsSync2 } from "fs";
226
- import path from "path";
263
+ import path2 from "path";
227
264
  import "commander";
228
265
  import { findProjectRoot as findProjectRoot3, resolveHaivePaths as resolveHaivePaths2 } from "@hiveai/core";
229
266
  function registerEmbeddings(program2) {
@@ -265,22 +302,22 @@ function registerEmbeddings(program2) {
265
302
  for (const hit of result.hits) {
266
303
  const score = hit.score.toFixed(3);
267
304
  console.log(`${ui.bold(score)} ${hit.id}`);
268
- console.log(` ${ui.dim(path.relative(root, hit.file_path))}`);
305
+ console.log(` ${ui.dim(path2.relative(root, hit.file_path))}`);
269
306
  }
270
307
  });
271
308
  embeddings.command("status").description("Show the embeddings index status").option("-d, --dir <dir>", "project root").action(async (opts) => {
272
309
  const root = findProjectRoot3(opts.dir);
273
310
  const paths = resolveHaivePaths2(root);
274
311
  const { indexStat } = await loadEmbeddings();
275
- const stat = await indexStat(paths);
276
- if (!stat.exists) {
312
+ const stat2 = await indexStat(paths);
313
+ if (!stat2.exists) {
277
314
  ui.warn("No embeddings index. Run `haive embeddings index` to create one.");
278
315
  return;
279
316
  }
280
- console.log(`${ui.bold("entries:")} ${stat.count}`);
281
- console.log(`${ui.bold("model:")} ${stat.model}`);
282
- console.log(`${ui.bold("updated_at:")} ${stat.updatedAt}`);
283
- console.log(`${ui.bold("size:")} ${(stat.sizeBytes / 1024).toFixed(1)} KB`);
317
+ console.log(`${ui.bold("entries:")} ${stat2.count}`);
318
+ console.log(`${ui.bold("model:")} ${stat2.model}`);
319
+ console.log(`${ui.bold("updated_at:")} ${stat2.updatedAt}`);
320
+ console.log(`${ui.bold("size:")} ${(stat2.sizeBytes / 1024).toFixed(1)} KB`);
284
321
  });
285
322
  }
286
323
  async function loadEmbeddings() {
@@ -295,7 +332,7 @@ async function loadEmbeddings() {
295
332
  }
296
333
 
297
334
  // src/commands/index-code.ts
298
- import path2 from "path";
335
+ import path3 from "path";
299
336
  import "commander";
300
337
  import {
301
338
  buildCodeMap,
@@ -338,15 +375,42 @@ function registerIndexCode(program2) {
338
375
  const fileCount = Object.keys(map.files).length;
339
376
  const exportCount = Object.values(map.files).reduce((s, f) => s + f.exports.length, 0);
340
377
  ui.success(
341
- `Indexed ${fileCount} file(s) with ${exportCount} export(s) \u2192 ${path2.relative(root, codeMapPath(paths))}`
378
+ `Indexed ${fileCount} file(s) with ${exportCount} export(s) \u2192 ${path3.relative(root, codeMapPath(paths))}`
342
379
  );
343
380
  });
381
+ idx.command("code-search").description(
382
+ "Build the semantic-search embeddings index for code (powers the code_search MCP tool).\n\n Reads .ai/code-map.json (run `haive index code` first) and embeds each exported\n symbol's metadata (filename + name + kind + description).\n\n Re-runs are incremental: unchanged entries keep their cached vectors, only the\n diff is re-embedded. First run downloads the bge-small-en-v1.5 model (~110MB).\n"
383
+ ).option("-d, --dir <dir>", "project root").action(async (opts) => {
384
+ const root = findProjectRoot4(opts.dir);
385
+ const paths = resolveHaivePaths3(root);
386
+ let mod;
387
+ try {
388
+ mod = await import("@hiveai/embeddings");
389
+ } catch {
390
+ ui.error(
391
+ "@hiveai/embeddings is not installed. Install it (`pnpm add @hiveai/embeddings`) or run `haive embeddings install`."
392
+ );
393
+ process.exit(1);
394
+ }
395
+ ui.info("Loading embedder (first run downloads ~110MB)\u2026");
396
+ const embedder = await mod.Embedder.create();
397
+ ui.info(`Embedding code-map symbols\u2026`);
398
+ try {
399
+ const { report } = await mod.rebuildCodeIndex(paths, embedder);
400
+ ui.success(
401
+ `Code-search index ready: ${report.total} symbols (+${report.added} new, ~${report.updated} updated, =${report.unchanged} cached, -${report.removed} removed)`
402
+ );
403
+ } catch (err) {
404
+ ui.error(err instanceof Error ? err.message : String(err));
405
+ process.exit(1);
406
+ }
407
+ });
344
408
  }
345
409
 
346
410
  // src/commands/init.ts
347
411
  import { mkdir, writeFile } from "fs/promises";
348
412
  import { existsSync as existsSync3 } from "fs";
349
- import path3 from "path";
413
+ import path4 from "path";
350
414
  import { spawnSync } from "child_process";
351
415
  import "commander";
352
416
  import {
@@ -517,7 +581,7 @@ function registerInit(program2) {
517
581
  "--manual",
518
582
  "opt out of autopilot: memories require manual approval, no auto-session recap, no auto-context"
519
583
  ).action(async (opts) => {
520
- const root = path3.resolve(opts.dir);
584
+ const root = path4.resolve(opts.dir);
521
585
  const paths = resolveHaivePaths4(root);
522
586
  const autopilot = opts.manual !== true;
523
587
  if (existsSync3(paths.haiveDir)) {
@@ -529,10 +593,10 @@ function registerInit(program2) {
529
593
  await mkdir(paths.modulesContextDir, { recursive: true });
530
594
  if (!existsSync3(paths.projectContext)) {
531
595
  await writeFile(paths.projectContext, PROJECT_CONTEXT_TEMPLATE, "utf8");
532
- ui.success(`Created ${path3.relative(root, paths.projectContext)}`);
596
+ ui.success(`Created ${path4.relative(root, paths.projectContext)}`);
533
597
  }
534
598
  const configExists = existsSync3(
535
- path3.join(paths.haiveDir, "haive.config.json")
599
+ path4.join(paths.haiveDir, "haive.config.json")
536
600
  );
537
601
  if (!configExists) {
538
602
  await saveConfig(paths, autopilot ? AUTOPILOT_DEFAULTS : { autopilot: false });
@@ -543,17 +607,17 @@ function registerInit(program2) {
543
607
  if (opts.bridges) {
544
608
  await writeBridge(root, "CLAUDE.md");
545
609
  await writeBridge(root, ".cursorrules");
546
- await writeBridge(root, path3.join(".github", "copilot-instructions.md"));
610
+ await writeBridge(root, path4.join(".github", "copilot-instructions.md"));
547
611
  }
548
612
  const wantCi = opts.withCi || autopilot;
549
613
  if (wantCi) {
550
- const ciPath = path3.join(root, ".github", "workflows", "haive-sync.yml");
614
+ const ciPath = path4.join(root, ".github", "workflows", "haive-sync.yml");
551
615
  if (existsSync3(ciPath)) {
552
616
  ui.info("CI workflow already exists \u2014 skipped");
553
617
  } else {
554
- await mkdir(path3.dirname(ciPath), { recursive: true });
618
+ await mkdir(path4.dirname(ciPath), { recursive: true });
555
619
  await writeFile(ciPath, CI_WORKFLOW, "utf8");
556
- ui.success(`Created ${path3.relative(root, ciPath)}`);
620
+ ui.success(`Created ${path4.relative(root, ciPath)}`);
557
621
  }
558
622
  }
559
623
  if (autopilot) {
@@ -608,12 +672,12 @@ function registerInit(program2) {
608
672
  });
609
673
  }
610
674
  async function writeBridge(root, relPath) {
611
- const target = path3.join(root, relPath);
675
+ const target = path4.join(root, relPath);
612
676
  if (existsSync3(target)) {
613
677
  ui.info(`Bridge ${relPath} already exists \u2014 skipped`);
614
678
  return;
615
679
  }
616
- await mkdir(path3.dirname(target), { recursive: true });
680
+ await mkdir(path4.dirname(target), { recursive: true });
617
681
  await writeFile(target, BRIDGE_BODY, "utf8");
618
682
  ui.success(`Created bridge ${relPath}`);
619
683
  }
@@ -621,7 +685,7 @@ async function writeBridge(root, relPath) {
621
685
  // src/commands/install-hooks.ts
622
686
  import { mkdir as mkdir2, writeFile as writeFile2, chmod, readFile as readFile2 } from "fs/promises";
623
687
  import { existsSync as existsSync4 } from "fs";
624
- import path4 from "path";
688
+ import path5 from "path";
625
689
  import "commander";
626
690
  import { findProjectRoot as findProjectRoot6 } from "@hiveai/core";
627
691
  var HOOK_MARKER = "# hAIve auto-generated";
@@ -642,18 +706,18 @@ function registerInstallHooks(program2) {
642
706
  "Install git hooks so haive sync runs automatically after every pull or merge.\n\n Installs a post-merge hook at .git/hooks/post-merge that runs:\n haive sync --quiet --since ORIG_HEAD --embed\n\n This ensures memory anchors are always verified and the embeddings index\n is kept fresh without requiring any manual steps.\n\n Installed automatically by haive init (autopilot mode).\n Use --force to overwrite an existing post-merge hook.\n"
643
707
  ).option("-d, --dir <dir>", "project root").option("--force", "overwrite existing hooks").action(async (opts) => {
644
708
  const root = findProjectRoot6(opts.dir);
645
- const gitDir = path4.join(root, ".git");
709
+ const gitDir = path5.join(root, ".git");
646
710
  if (!existsSync4(gitDir)) {
647
711
  ui.error(`No .git directory at ${root}.`);
648
712
  process.exitCode = 1;
649
713
  return;
650
714
  }
651
- const hooksDir = path4.join(gitDir, "hooks");
715
+ const hooksDir = path5.join(gitDir, "hooks");
652
716
  await mkdir2(hooksDir, { recursive: true });
653
717
  let installed = 0;
654
718
  let skipped = 0;
655
719
  for (const name of HOOKS) {
656
- const file = path4.join(hooksDir, name);
720
+ const file = path5.join(hooksDir, name);
657
721
  if (existsSync4(file) && !opts.force) {
658
722
  const existing = await readFile2(file, "utf8");
659
723
  if (!existing.includes(HOOK_MARKER)) {
@@ -675,7 +739,7 @@ function registerInstallHooks(program2) {
675
739
  import { spawn } from "child_process";
676
740
  import { existsSync as existsSync5 } from "fs";
677
741
  import { createRequire } from "module";
678
- import path5 from "path";
742
+ import path6 from "path";
679
743
  import { fileURLToPath } from "url";
680
744
  import "commander";
681
745
  import { findProjectRoot as findProjectRoot7 } from "@hiveai/core";
@@ -713,13 +777,13 @@ function registerMcp(program2) {
713
777
  function locateMcpBin() {
714
778
  try {
715
779
  const pkgPath = require2.resolve("@hiveai/mcp/package.json");
716
- const pkgDir = path5.dirname(pkgPath);
717
- const candidate = path5.join(pkgDir, "dist", "index.js");
780
+ const pkgDir = path6.dirname(pkgPath);
781
+ const candidate = path6.join(pkgDir, "dist", "index.js");
718
782
  if (existsSync5(candidate)) return candidate;
719
783
  } catch {
720
784
  }
721
- const here = path5.dirname(fileURLToPath(import.meta.url));
722
- const sibling = path5.resolve(here, "..", "..", "..", "mcp", "dist", "index.js");
785
+ const here = path6.dirname(fileURLToPath(import.meta.url));
786
+ const sibling = path6.resolve(here, "..", "..", "..", "mcp", "dist", "index.js");
723
787
  if (existsSync5(sibling)) return sibling;
724
788
  return null;
725
789
  }
@@ -728,7 +792,7 @@ function locateMcpBin() {
728
792
  import { spawnSync as spawnSync2 } from "child_process";
729
793
  import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
730
794
  import { existsSync as existsSync6 } from "fs";
731
- import path6 from "path";
795
+ import path7 from "path";
732
796
  import "commander";
733
797
  import {
734
798
  DEFAULT_AUTO_PROMOTE_RULE,
@@ -895,7 +959,7 @@ function registerSync(program2) {
895
959
  );
896
960
  }
897
961
  if (opts.injectBridge) {
898
- const bridgeFile = opts.bridgeFile ? path6.resolve(opts.bridgeFile) : path6.join(root, "CLAUDE.md");
962
+ const bridgeFile = opts.bridgeFile ? path7.resolve(opts.bridgeFile) : path7.join(root, "CLAUDE.md");
899
963
  const maxInject = Math.max(1, Number(opts.bridgeMaxMemories ?? 5));
900
964
  await injectBridge(bridgeFile, paths.memoriesDir, maxInject, root, opts.quiet);
901
965
  }
@@ -1001,10 +1065,10 @@ Attends une **confirmation explicite** avant d'agir.
1001
1065
  paths: [result.file],
1002
1066
  topic: `dep-bump-${slugParts}`
1003
1067
  });
1004
- const teamDir = path6.join(paths.memoriesDir, "team");
1068
+ const teamDir = path7.join(paths.memoriesDir, "team");
1005
1069
  await mkdir3(teamDir, { recursive: true });
1006
1070
  await writeFile3(
1007
- path6.join(teamDir, `${fm.id}.md`),
1071
+ path7.join(teamDir, `${fm.id}.md`),
1008
1072
  serializeMemory({ frontmatter: { ...fm, requires_human_approval: true }, body }),
1009
1073
  "utf8"
1010
1074
  );
@@ -1068,10 +1132,10 @@ Attends une **confirmation explicite** avant d'agir.
1068
1132
  paths: [diff.file],
1069
1133
  topic: `contract-breaking-${diff.contract}`
1070
1134
  });
1071
- const teamDir = path6.join(paths.memoriesDir, "team");
1135
+ const teamDir = path7.join(paths.memoriesDir, "team");
1072
1136
  await mkdir3(teamDir, { recursive: true });
1073
1137
  await writeFile3(
1074
- path6.join(teamDir, `${fm.id}.md`),
1138
+ path7.join(teamDir, `${fm.id}.md`),
1075
1139
  serializeMemory({ frontmatter: { ...fm, requires_human_approval: true }, body }),
1076
1140
  "utf8"
1077
1141
  );
@@ -1163,11 +1227,11 @@ ${BRIDGE_END}`;
1163
1227
  const startIdx = existing.indexOf(BRIDGE_START);
1164
1228
  const endIdx = existing.indexOf(BRIDGE_END);
1165
1229
  if (startIdx !== -1 && endIdx === -1) {
1166
- ui.warn(`${path6.relative(root, bridgeFile)}: found ${BRIDGE_START} without ${BRIDGE_END}. Fix the file manually before running --inject-bridge.`);
1230
+ ui.warn(`${path7.relative(root, bridgeFile)}: found ${BRIDGE_START} without ${BRIDGE_END}. Fix the file manually before running --inject-bridge.`);
1167
1231
  return;
1168
1232
  }
1169
1233
  if (startIdx === -1 && endIdx !== -1) {
1170
- ui.warn(`${path6.relative(root, bridgeFile)}: found ${BRIDGE_END} without ${BRIDGE_START}. Fix the file manually before running --inject-bridge.`);
1234
+ ui.warn(`${path7.relative(root, bridgeFile)}: found ${BRIDGE_END} without ${BRIDGE_START}. Fix the file manually before running --inject-bridge.`);
1171
1235
  return;
1172
1236
  }
1173
1237
  let updated;
@@ -1175,14 +1239,14 @@ ${BRIDGE_END}`;
1175
1239
  updated = existing.slice(0, startIdx) + injected + existing.slice(endIdx + BRIDGE_END.length);
1176
1240
  } else {
1177
1241
  if (!fileExists && !quiet) {
1178
- ui.info(`Creating ${path6.relative(root, bridgeFile)} with haive memory block.`);
1242
+ ui.info(`Creating ${path7.relative(root, bridgeFile)} with haive memory block.`);
1179
1243
  }
1180
1244
  updated = existing + (existing.endsWith("\n") ? "" : "\n") + "\n" + injected + "\n";
1181
1245
  }
1182
1246
  await writeFile3(bridgeFile, updated, "utf8");
1183
1247
  if (!quiet) {
1184
1248
  console.log(
1185
- ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${path6.relative(root, bridgeFile)}`)
1249
+ ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${path7.relative(root, bridgeFile)}`)
1186
1250
  );
1187
1251
  }
1188
1252
  }
@@ -1209,7 +1273,7 @@ function collectSinceChanges(root, ref) {
1209
1273
  import { createHash } from "crypto";
1210
1274
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
1211
1275
  import { existsSync as existsSync7 } from "fs";
1212
- import path7 from "path";
1276
+ import path8 from "path";
1213
1277
  import "commander";
1214
1278
  import {
1215
1279
  buildFrontmatter as buildFrontmatter2,
@@ -1258,7 +1322,7 @@ function registerMemoryAdd(memory2) {
1258
1322
  const inferredTags = autoTagsEnabled ? inferModulesFromPaths(anchorPaths) : [];
1259
1323
  const mergedTags = Array.from(/* @__PURE__ */ new Set([...userTags, ...inferredTags]));
1260
1324
  if (anchorPaths.length > 0) {
1261
- const missing = anchorPaths.filter((p) => !existsSync7(path7.resolve(root, p)));
1325
+ const missing = anchorPaths.filter((p) => !existsSync7(path8.resolve(root, p)));
1262
1326
  if (missing.length > 0) {
1263
1327
  ui.warn(`Anchor path${missing.length > 1 ? "s" : ""} not found in project:`);
1264
1328
  for (const p of missing) ui.warn(` \u2717 ${p}`);
@@ -1323,7 +1387,7 @@ TODO \u2014 write the memory body.
1323
1387
  }
1324
1388
  };
1325
1389
  await writeFile4(topicMatch.filePath, serializeMemory2({ frontmatter: newFrontmatter, body }), "utf8");
1326
- ui.success(`Updated (topic upsert) ${path7.relative(root, topicMatch.filePath)}`);
1390
+ ui.success(`Updated (topic upsert) ${path8.relative(root, topicMatch.filePath)}`);
1327
1391
  ui.info(`id=${fm.id} revision=${revisionCount}`);
1328
1392
  return;
1329
1393
  }
@@ -1342,7 +1406,7 @@ TODO \u2014 write the memory body.
1342
1406
  topic: opts.topic
1343
1407
  });
1344
1408
  const file = memoryFilePath(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
1345
- await mkdir4(path7.dirname(file), { recursive: true });
1409
+ await mkdir4(path8.dirname(file), { recursive: true });
1346
1410
  if (existsSync7(file)) {
1347
1411
  ui.error(`Memory already exists at ${file}`);
1348
1412
  process.exitCode = 1;
@@ -1361,7 +1425,7 @@ TODO \u2014 write the memory body.
1361
1425
  }
1362
1426
  }
1363
1427
  await writeFile4(file, serializeMemory2({ frontmatter, body }), "utf8");
1364
- ui.success(`Created ${path7.relative(root, file)}`);
1428
+ ui.success(`Created ${path8.relative(root, file)}`);
1365
1429
  ui.info(`id=${frontmatter.id} scope=${frontmatter.scope} status=${frontmatter.status}`);
1366
1430
  if (inferredTags.length > 0) {
1367
1431
  ui.info(`auto-tagged: ${inferredTags.join(", ")} (use --no-auto-tag to disable)`);
@@ -1392,7 +1456,7 @@ function parseCsv2(value) {
1392
1456
 
1393
1457
  // src/commands/memory-list.ts
1394
1458
  import { existsSync as existsSync8 } from "fs";
1395
- import path8 from "path";
1459
+ import path9 from "path";
1396
1460
  import "commander";
1397
1461
  import { findProjectRoot as findProjectRoot10, resolveHaivePaths as resolveHaivePaths7 } from "@hiveai/core";
1398
1462
 
@@ -1440,7 +1504,7 @@ function registerMemoryList(memory2) {
1440
1504
  console.log(
1441
1505
  `${ui.bold(fm.id)} ${ui.dim(fm.scope)}/${ui.dim(fm.type)} ${statusBadge}${moduleStr}${tagStr}`
1442
1506
  );
1443
- console.log(` ${ui.dim(path8.relative(root, filePath))}`);
1507
+ console.log(` ${ui.dim(path9.relative(root, filePath))}`);
1444
1508
  }
1445
1509
  console.log(ui.dim(`
1446
1510
  ${filtered.length} memor${filtered.length === 1 ? "y" : "ies"}`));
@@ -1477,7 +1541,7 @@ function matchesFilters(loaded, opts) {
1477
1541
  // src/commands/memory-promote.ts
1478
1542
  import { mkdir as mkdir5, unlink, writeFile as writeFile5 } from "fs/promises";
1479
1543
  import { existsSync as existsSync9 } from "fs";
1480
- import path9 from "path";
1544
+ import path10 from "path";
1481
1545
  import "commander";
1482
1546
  import {
1483
1547
  findProjectRoot as findProjectRoot11,
@@ -1524,11 +1588,11 @@ function registerMemoryPromote(memory2) {
1524
1588
  body: found.memory.body
1525
1589
  };
1526
1590
  const newPath = memoryFilePath2(paths, "team", updated.frontmatter.id);
1527
- await mkdir5(path9.dirname(newPath), { recursive: true });
1591
+ await mkdir5(path10.dirname(newPath), { recursive: true });
1528
1592
  await writeFile5(newPath, serializeMemory3(updated), "utf8");
1529
1593
  await unlink(found.filePath);
1530
1594
  ui.success(`Promoted ${id} to team scope (status=proposed)`);
1531
- ui.info(`Now at ${path9.relative(root, newPath)}`);
1595
+ ui.info(`Now at ${path10.relative(root, newPath)}`);
1532
1596
  console.log(ui.dim(`\u2192 next: haive memory approve ${id} (validate for team use)`));
1533
1597
  });
1534
1598
  }
@@ -1536,7 +1600,7 @@ function registerMemoryPromote(memory2) {
1536
1600
  // src/commands/memory-approve.ts
1537
1601
  import { existsSync as existsSync10 } from "fs";
1538
1602
  import { writeFile as writeFile6 } from "fs/promises";
1539
- import path10 from "path";
1603
+ import path11 from "path";
1540
1604
  import "commander";
1541
1605
  import {
1542
1606
  findProjectRoot as findProjectRoot12,
@@ -1600,14 +1664,14 @@ function registerMemoryApprove(memory2) {
1600
1664
  };
1601
1665
  await writeFile6(found.filePath, serializeMemory4(next), "utf8");
1602
1666
  ui.success(`Approved ${id} (status=validated)`);
1603
- ui.info(path10.relative(root, found.filePath));
1667
+ ui.info(path11.relative(root, found.filePath));
1604
1668
  });
1605
1669
  }
1606
1670
 
1607
1671
  // src/commands/memory-update.ts
1608
1672
  import { writeFile as writeFile7 } from "fs/promises";
1609
1673
  import { existsSync as existsSync11 } from "fs";
1610
- import path11 from "path";
1674
+ import path12 from "path";
1611
1675
  import "commander";
1612
1676
  import {
1613
1677
  findProjectRoot as findProjectRoot13,
@@ -1670,7 +1734,7 @@ function registerMemoryUpdate(memory2) {
1670
1734
  serializeMemory5({ frontmatter: newFrontmatter, body: newBody }),
1671
1735
  "utf8"
1672
1736
  );
1673
- ui.success(`Updated ${path11.relative(root, loaded.filePath)}`);
1737
+ ui.success(`Updated ${path12.relative(root, loaded.filePath)}`);
1674
1738
  ui.info(`fields: ${updated.join(", ")}`);
1675
1739
  });
1676
1740
  }
@@ -1691,7 +1755,7 @@ function parseCsv3(value) {
1691
1755
  // src/commands/memory-auto-promote.ts
1692
1756
  import { writeFile as writeFile8 } from "fs/promises";
1693
1757
  import { existsSync as existsSync12 } from "fs";
1694
- import path12 from "path";
1758
+ import path13 from "path";
1695
1759
  import "commander";
1696
1760
  import {
1697
1761
  DEFAULT_AUTO_PROMOTE_RULE as DEFAULT_AUTO_PROMOTE_RULE2,
@@ -1736,7 +1800,7 @@ function registerMemoryAutoPromote(memory2) {
1736
1800
  console.log(
1737
1801
  `${ui.bold(opts.apply ? "PROMOTE" : "would promote")} ${mem.frontmatter.id} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
1738
1802
  );
1739
- console.log(` ${ui.dim(path12.relative(root, filePath))}`);
1803
+ console.log(` ${ui.dim(path13.relative(root, filePath))}`);
1740
1804
  if (opts.apply) {
1741
1805
  const next = {
1742
1806
  frontmatter: { ...mem.frontmatter, status: "validated" },
@@ -1755,7 +1819,7 @@ function registerMemoryAutoPromote(memory2) {
1755
1819
  import { spawn as spawn2 } from "child_process";
1756
1820
  import { existsSync as existsSync13 } from "fs";
1757
1821
  import { readFile as readFile5 } from "fs/promises";
1758
- import path13 from "path";
1822
+ import path14 from "path";
1759
1823
  import "commander";
1760
1824
  import {
1761
1825
  findProjectRoot as findProjectRoot15,
@@ -1779,7 +1843,7 @@ function registerMemoryEdit(memory2) {
1779
1843
  return;
1780
1844
  }
1781
1845
  const editor = opts.editor ?? process.env.EDITOR ?? process.env.VISUAL ?? "vi";
1782
- ui.info(`Opening ${path13.relative(root, found.filePath)} with ${editor}\u2026`);
1846
+ ui.info(`Opening ${path14.relative(root, found.filePath)} with ${editor}\u2026`);
1783
1847
  const code = await runEditor(editor, found.filePath);
1784
1848
  if (code !== 0) {
1785
1849
  ui.warn(`Editor exited with status ${code}.`);
@@ -1807,7 +1871,7 @@ function runEditor(editor, file) {
1807
1871
 
1808
1872
  // src/commands/memory-for-files.ts
1809
1873
  import { existsSync as existsSync14 } from "fs";
1810
- import path14 from "path";
1874
+ import path15 from "path";
1811
1875
  import "commander";
1812
1876
  import {
1813
1877
  deriveConfidence,
@@ -1929,13 +1993,13 @@ function printGroup(root, label, loaded, usage) {
1929
1993
  const u = getUsage3(usage, fm.id);
1930
1994
  const conf = deriveConfidence(fm, u);
1931
1995
  console.log(`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(conf)}`);
1932
- console.log(` ${ui.dim(path14.relative(root, filePath))}`);
1996
+ console.log(` ${ui.dim(path15.relative(root, filePath))}`);
1933
1997
  }
1934
1998
  }
1935
1999
 
1936
2000
  // src/commands/memory-hot.ts
1937
2001
  import { existsSync as existsSync15 } from "fs";
1938
- import path15 from "path";
2002
+ import path16 from "path";
1939
2003
  import "commander";
1940
2004
  import {
1941
2005
  findProjectRoot as findProjectRoot17,
@@ -1975,7 +2039,7 @@ function registerMemoryHot(memory2) {
1975
2039
  console.log(
1976
2040
  `${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(fm.status)} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
1977
2041
  );
1978
- console.log(` ${ui.dim(path15.relative(root, filePath))}`);
2042
+ console.log(` ${ui.dim(path16.relative(root, filePath))}`);
1979
2043
  }
1980
2044
  ui.info(
1981
2045
  `${candidates.length} hot \u2014 promote drafts with \`haive memory promote <id>\`, then \`haive memory auto-promote --apply\`.`
@@ -1986,7 +2050,7 @@ function registerMemoryHot(memory2) {
1986
2050
  // src/commands/memory-tried.ts
1987
2051
  import { mkdir as mkdir6, writeFile as writeFile9 } from "fs/promises";
1988
2052
  import { existsSync as existsSync16 } from "fs";
1989
- import path16 from "path";
2053
+ import path17 from "path";
1990
2054
  import "commander";
1991
2055
  import {
1992
2056
  buildFrontmatter as buildFrontmatter3,
@@ -2037,14 +2101,14 @@ function registerMemoryTried(memory2) {
2037
2101
  }
2038
2102
  const body = lines.join("\n") + "\n";
2039
2103
  const file = memoryFilePath3(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
2040
- await mkdir6(path16.dirname(file), { recursive: true });
2104
+ await mkdir6(path17.dirname(file), { recursive: true });
2041
2105
  if (existsSync16(file)) {
2042
2106
  ui.error(`Memory already exists at ${file}`);
2043
2107
  process.exitCode = 1;
2044
2108
  return;
2045
2109
  }
2046
2110
  await writeFile9(file, serializeMemory7({ frontmatter, body }), "utf8");
2047
- ui.success(`Recorded: ${path16.relative(root, file)}`);
2111
+ ui.success(`Recorded: ${path17.relative(root, file)}`);
2048
2112
  ui.info(`id=${frontmatter.id} type=attempt status=validated (auto-approved)`);
2049
2113
  });
2050
2114
  }
@@ -2055,7 +2119,7 @@ function parseCsv4(value) {
2055
2119
 
2056
2120
  // src/commands/memory-pending.ts
2057
2121
  import { existsSync as existsSync17 } from "fs";
2058
- import path17 from "path";
2122
+ import path18 from "path";
2059
2123
  import "commander";
2060
2124
  import {
2061
2125
  findProjectRoot as findProjectRoot19,
@@ -2095,7 +2159,7 @@ function registerMemoryPending(memory2) {
2095
2159
  console.log(
2096
2160
  `${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.dim(`age=${ageStr} reads=${u.read_count} rejections=${u.rejected_count}`)}`
2097
2161
  );
2098
- console.log(` ${ui.dim(path17.relative(root, filePath))}`);
2162
+ console.log(` ${ui.dim(path18.relative(root, filePath))}`);
2099
2163
  }
2100
2164
  ui.info(`${proposed.length} pending`);
2101
2165
  });
@@ -2103,7 +2167,7 @@ function registerMemoryPending(memory2) {
2103
2167
 
2104
2168
  // src/commands/memory-query.ts
2105
2169
  import { existsSync as existsSync18 } from "fs";
2106
- import path18 from "path";
2170
+ import path19 from "path";
2107
2171
  import "commander";
2108
2172
  import {
2109
2173
  extractSnippet,
@@ -2160,7 +2224,7 @@ function registerMemoryQuery(memory2) {
2160
2224
  const fm = mem.frontmatter;
2161
2225
  const statusBadge = ui.statusBadge(fm.status);
2162
2226
  console.log(`${ui.bold(fm.id)} ${ui.dim(fm.scope)} ${statusBadge}`);
2163
- console.log(` ${ui.dim(path18.relative(root, filePath))}`);
2227
+ console.log(` ${ui.dim(path19.relative(root, filePath))}`);
2164
2228
  const snippet = extractSnippet(mem.body, snippetNeedle);
2165
2229
  if (snippet) console.log(` ${snippet}`);
2166
2230
  }
@@ -2230,7 +2294,7 @@ function registerMemoryReject(memory2) {
2230
2294
  // src/commands/memory-rm.ts
2231
2295
  import { existsSync as existsSync20 } from "fs";
2232
2296
  import { unlink as unlink2 } from "fs/promises";
2233
- import path19 from "path";
2297
+ import path20 from "path";
2234
2298
  import { createInterface } from "readline/promises";
2235
2299
  import "commander";
2236
2300
  import {
@@ -2255,7 +2319,7 @@ function registerMemoryRm(memory2) {
2255
2319
  process.exitCode = 1;
2256
2320
  return;
2257
2321
  }
2258
- const rel = path19.relative(root, found.filePath);
2322
+ const rel = path20.relative(root, found.filePath);
2259
2323
  if (!opts.yes) {
2260
2324
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2261
2325
  const answer = (await rl.question(`Delete ${rel}? [y/N] `)).trim().toLowerCase();
@@ -2281,7 +2345,7 @@ function registerMemoryRm(memory2) {
2281
2345
  // src/commands/memory-show.ts
2282
2346
  import { existsSync as existsSync21 } from "fs";
2283
2347
  import { readFile as readFile6 } from "fs/promises";
2284
- import path20 from "path";
2348
+ import path21 from "path";
2285
2349
  import "commander";
2286
2350
  import {
2287
2351
  deriveConfidence as deriveConfidence2,
@@ -2323,7 +2387,7 @@ function registerMemoryShow(memory2) {
2323
2387
  if (fm.verified_at) console.log(`${ui.dim("verified:")} ${fm.verified_at}`);
2324
2388
  if (fm.stale_reason) console.log(`${ui.dim("stale:")} ${fm.stale_reason}`);
2325
2389
  console.log(`${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`);
2326
- console.log(`${ui.dim("file:")} ${path20.relative(root, found.filePath)}`);
2390
+ console.log(`${ui.dim("file:")} ${path21.relative(root, found.filePath)}`);
2327
2391
  if (fm.anchor.paths.length || fm.anchor.symbols.length) {
2328
2392
  console.log(ui.dim("anchor:"));
2329
2393
  if (fm.anchor.commit) console.log(` ${ui.dim("commit:")} ${fm.anchor.commit}`);
@@ -2339,7 +2403,7 @@ function registerMemoryShow(memory2) {
2339
2403
 
2340
2404
  // src/commands/memory-stats.ts
2341
2405
  import { existsSync as existsSync22 } from "fs";
2342
- import path21 from "path";
2406
+ import path22 from "path";
2343
2407
  import "commander";
2344
2408
  import {
2345
2409
  deriveConfidence as deriveConfidence3,
@@ -2377,7 +2441,7 @@ function registerMemoryStats(memory2) {
2377
2441
  console.log(
2378
2442
  ` ${ui.dim("status:")} ${fm.status} ${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`
2379
2443
  );
2380
- console.log(` ${ui.dim(path21.relative(root, filePath))}`);
2444
+ console.log(` ${ui.dim(path22.relative(root, filePath))}`);
2381
2445
  }
2382
2446
  });
2383
2447
  }
@@ -2385,7 +2449,7 @@ function registerMemoryStats(memory2) {
2385
2449
  // src/commands/memory-verify.ts
2386
2450
  import { writeFile as writeFile11 } from "fs/promises";
2387
2451
  import { existsSync as existsSync23 } from "fs";
2388
- import path22 from "path";
2452
+ import path23 from "path";
2389
2453
  import "commander";
2390
2454
  import {
2391
2455
  findProjectRoot as findProjectRoot25,
@@ -2422,7 +2486,7 @@ function registerMemoryVerify(memory2) {
2422
2486
  anchorlessIds.push(mem.frontmatter.id);
2423
2487
  continue;
2424
2488
  }
2425
- const rel = path22.relative(root, filePath);
2489
+ const rel = path23.relative(root, filePath);
2426
2490
  if (result.stale) {
2427
2491
  staleCount++;
2428
2492
  console.log(`${ui.bold("STALE")} ${mem.frontmatter.id}`);
@@ -2538,7 +2602,7 @@ function registerMemoryImport(memory2) {
2538
2602
  // src/commands/memory-import-changelog.ts
2539
2603
  import { existsSync as existsSync25 } from "fs";
2540
2604
  import { readFile as readFile8, mkdir as mkdir7, writeFile as writeFile12 } from "fs/promises";
2541
- import path23 from "path";
2605
+ import path24 from "path";
2542
2606
  import "commander";
2543
2607
  import {
2544
2608
  buildFrontmatter as buildFrontmatter4,
@@ -2609,7 +2673,7 @@ function registerMemoryImportChangelog(memory2) {
2609
2673
  ).option("-d, --dir <dir>", "project root").action(async (opts) => {
2610
2674
  const root = findProjectRoot27(opts.dir);
2611
2675
  const paths = resolveHaivePaths24(root);
2612
- const changelogPath = path23.resolve(root, opts.fromChangelog);
2676
+ const changelogPath = path24.resolve(root, opts.fromChangelog);
2613
2677
  if (!existsSync25(changelogPath)) {
2614
2678
  ui.error(`CHANGELOG not found: ${changelogPath}`);
2615
2679
  process.exitCode = 1;
@@ -2629,9 +2693,9 @@ function registerMemoryImportChangelog(memory2) {
2629
2693
  entries = entries.filter((e) => requested.includes(e.version));
2630
2694
  }
2631
2695
  }
2632
- const pkgName = opts.package ?? path23.basename(path23.dirname(changelogPath));
2696
+ const pkgName = opts.package ?? path24.basename(path24.dirname(changelogPath));
2633
2697
  const scope = opts.scope ?? "team";
2634
- const teamDir = path23.join(paths.memoriesDir, scope);
2698
+ const teamDir = path24.join(paths.memoriesDir, scope);
2635
2699
  await mkdir7(teamDir, { recursive: true });
2636
2700
  let saved = 0;
2637
2701
  for (const entry of entries) {
@@ -2654,7 +2718,7 @@ function registerMemoryImportChangelog(memory2) {
2654
2718
  lines.push("");
2655
2719
  }
2656
2720
  lines.push(
2657
- `**Source:** \`${path23.relative(root, changelogPath)}\`
2721
+ `**Source:** \`${path24.relative(root, changelogPath)}\`
2658
2722
  **Action:** Update all usages of ${pkgName} if they rely on any of the above.`
2659
2723
  );
2660
2724
  const slug = `changelog-${pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase()}-v${entry.version.replace(/\./g, "-")}`;
@@ -2669,11 +2733,11 @@ function registerMemoryImportChangelog(memory2) {
2669
2733
  pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase(),
2670
2734
  `v${entry.version}`
2671
2735
  ],
2672
- paths: [path23.relative(root, changelogPath)],
2736
+ paths: [path24.relative(root, changelogPath)],
2673
2737
  topic: `changelog-${pkgName}-${entry.version}`
2674
2738
  });
2675
2739
  await writeFile12(
2676
- path23.join(teamDir, `${fm.id}.md`),
2740
+ path24.join(teamDir, `${fm.id}.md`),
2677
2741
  serializeMemory10({ frontmatter: fm, body: lines.join("\n") }),
2678
2742
  "utf8"
2679
2743
  );
@@ -2698,7 +2762,7 @@ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${p
2698
2762
  // src/commands/memory-digest.ts
2699
2763
  import { existsSync as existsSync26 } from "fs";
2700
2764
  import { writeFile as writeFile13 } from "fs/promises";
2701
- import path24 from "path";
2765
+ import path25 from "path";
2702
2766
  import "commander";
2703
2767
  import {
2704
2768
  deriveConfidence as deriveConfidence4,
@@ -2793,7 +2857,7 @@ function registerMemoryDigest(program2) {
2793
2857
  );
2794
2858
  const digest = lines.join("\n");
2795
2859
  if (opts.out) {
2796
- const outPath = path24.resolve(process.cwd(), opts.out);
2860
+ const outPath = path25.resolve(process.cwd(), opts.out);
2797
2861
  await writeFile13(outPath, digest, "utf8");
2798
2862
  ui.success(`Digest written to ${opts.out} (${recent.length} memor${recent.length === 1 ? "y" : "ies"})`);
2799
2863
  } else {
@@ -2805,7 +2869,7 @@ function registerMemoryDigest(program2) {
2805
2869
  // src/commands/session-end.ts
2806
2870
  import { writeFile as writeFile14, mkdir as mkdir8 } from "fs/promises";
2807
2871
  import { existsSync as existsSync27 } from "fs";
2808
- import path25 from "path";
2872
+ import path26 from "path";
2809
2873
  import "commander";
2810
2874
  import {
2811
2875
  buildFrontmatter as buildFrontmatter5,
@@ -2873,7 +2937,7 @@ function registerSessionEnd(session2) {
2873
2937
  const body = buildRecapBody(opts);
2874
2938
  const topic = recapTopic(scope, opts.module);
2875
2939
  const filesTouched = parseCsv5(opts.files);
2876
- const missingPaths = filesTouched.filter((p) => !existsSync27(path25.resolve(root, p)));
2940
+ const missingPaths = filesTouched.filter((p) => !existsSync27(path26.resolve(root, p)));
2877
2941
  if (missingPaths.length > 0) {
2878
2942
  ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
2879
2943
  for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
@@ -2896,7 +2960,7 @@ function registerSessionEnd(session2) {
2896
2960
  };
2897
2961
  await writeFile14(topicMatch.filePath, serializeMemory11({ frontmatter: newFrontmatter, body }), "utf8");
2898
2962
  ui.success(`Session recap updated (revision #${revisionCount})`);
2899
- ui.info(`id=${fm.id} file=${path25.relative(root, topicMatch.filePath)}`);
2963
+ ui.info(`id=${fm.id} file=${path26.relative(root, topicMatch.filePath)}`);
2900
2964
  return;
2901
2965
  }
2902
2966
  }
@@ -2911,10 +2975,10 @@ function registerSessionEnd(session2) {
2911
2975
  status: "validated"
2912
2976
  });
2913
2977
  const file = memoryFilePath4(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
2914
- await mkdir8(path25.dirname(file), { recursive: true });
2978
+ await mkdir8(path26.dirname(file), { recursive: true });
2915
2979
  await writeFile14(file, serializeMemory11({ frontmatter, body }), "utf8");
2916
2980
  ui.success(`Session recap created`);
2917
- ui.info(`id=${frontmatter.id} scope=${scope} file=${path25.relative(root, file)}`);
2981
+ ui.info(`id=${frontmatter.id} scope=${scope} file=${path26.relative(root, file)}`);
2918
2982
  ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
2919
2983
  });
2920
2984
  }
@@ -2926,7 +2990,7 @@ function parseCsv5(value) {
2926
2990
  // src/commands/snapshot.ts
2927
2991
  import { existsSync as existsSync28 } from "fs";
2928
2992
  import { readdir } from "fs/promises";
2929
- import path26 from "path";
2993
+ import path27 from "path";
2930
2994
  import "commander";
2931
2995
  import {
2932
2996
  diffContract,
@@ -2965,7 +3029,7 @@ function registerSnapshot(program2) {
2965
3029
  return;
2966
3030
  }
2967
3031
  if (opts.list) {
2968
- const contractsDir = path26.join(paths.haiveDir, "contracts");
3032
+ const contractsDir = path27.join(paths.haiveDir, "contracts");
2969
3033
  if (!existsSync28(contractsDir)) {
2970
3034
  console.log(ui.dim("No contract snapshots found."));
2971
3035
  return;
@@ -3021,7 +3085,7 @@ function registerSnapshot(program2) {
3021
3085
  return;
3022
3086
  }
3023
3087
  const contractPath = opts.contract;
3024
- const name = opts.name ?? path26.basename(contractPath, path26.extname(contractPath));
3088
+ const name = opts.name ?? path27.basename(contractPath, path27.extname(contractPath));
3025
3089
  const format = opts.format ?? detectFormat(contractPath) ?? "openapi";
3026
3090
  const contract = { name, path: contractPath, format };
3027
3091
  try {
@@ -3076,8 +3140,8 @@ async function runDiff(root, haiveDir, contract) {
3076
3140
  }
3077
3141
  }
3078
3142
  function detectFormat(filePath) {
3079
- const ext = path26.extname(filePath).toLowerCase();
3080
- const base = path26.basename(filePath).toLowerCase();
3143
+ const ext = path27.extname(filePath).toLowerCase();
3144
+ const base = path27.basename(filePath).toLowerCase();
3081
3145
  if (ext === ".yaml" || ext === ".yml" || ext === ".json") {
3082
3146
  if (base.includes("openapi") || base.includes("swagger")) return "openapi";
3083
3147
  if (base.includes("schema") || base.includes("graphql")) return "graphql";
@@ -3092,7 +3156,7 @@ function detectFormat(filePath) {
3092
3156
  // src/commands/hub.ts
3093
3157
  import { existsSync as existsSync29 } from "fs";
3094
3158
  import { mkdir as mkdir9, readFile as readFile9, writeFile as writeFile15, copyFile } from "fs/promises";
3095
- import path27 from "path";
3159
+ import path28 from "path";
3096
3160
  import { spawnSync as spawnSync3 } from "child_process";
3097
3161
  import "commander";
3098
3162
  import {
@@ -3111,7 +3175,7 @@ function registerHub(program2) {
3111
3175
  hub.command("init <hubPath>").description(
3112
3176
  "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"
3113
3177
  ).action(async (hubPath) => {
3114
- const absPath = path27.resolve(hubPath);
3178
+ const absPath = path28.resolve(hubPath);
3115
3179
  await mkdir9(absPath, { recursive: true });
3116
3180
  const gitCheck = spawnSync3("git", ["rev-parse", "--git-dir"], { cwd: absPath });
3117
3181
  if (gitCheck.status !== 0) {
@@ -3122,10 +3186,10 @@ function registerHub(program2) {
3122
3186
  return;
3123
3187
  }
3124
3188
  }
3125
- const sharedDir = path27.join(absPath, ".ai", "memories", "shared");
3189
+ const sharedDir = path28.join(absPath, ".ai", "memories", "shared");
3126
3190
  await mkdir9(sharedDir, { recursive: true });
3127
3191
  await writeFile15(
3128
- path27.join(absPath, ".ai", "README.md"),
3192
+ path28.join(absPath, ".ai", "README.md"),
3129
3193
  `# hAIve Team Knowledge Hub
3130
3194
 
3131
3195
  This repo is a shared knowledge hub for hAIve.
@@ -3147,7 +3211,7 @@ haive hub pull # import into a project
3147
3211
  "utf8"
3148
3212
  );
3149
3213
  await writeFile15(
3150
- path27.join(absPath, ".gitignore"),
3214
+ path28.join(absPath, ".gitignore"),
3151
3215
  ".ai/.cache/\n.ai/memories/personal/\n",
3152
3216
  "utf8"
3153
3217
  );
@@ -3162,7 +3226,7 @@ haive hub pull # import into a project
3162
3226
  `
3163
3227
  Next steps:
3164
3228
  1. Add hubPath to your project's .ai/haive.config.json:
3165
- { "hubPath": "${path27.relative(process.cwd(), absPath)}" }
3229
+ { "hubPath": "${path28.relative(process.cwd(), absPath)}" }
3166
3230
  2. Run \`haive hub push\` to publish your shared memories
3167
3231
  3. Share ${absPath} with teammates (git remote, NFS, etc.)
3168
3232
  `
@@ -3191,14 +3255,14 @@ Next steps:
3191
3255
  process.exitCode = 1;
3192
3256
  return;
3193
3257
  }
3194
- const hubRoot = path27.resolve(root, config.hubPath);
3258
+ const hubRoot = path28.resolve(root, config.hubPath);
3195
3259
  if (!existsSync29(hubRoot)) {
3196
3260
  ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
3197
3261
  process.exitCode = 1;
3198
3262
  return;
3199
3263
  }
3200
- const projectName = path27.basename(root);
3201
- const destDir = path27.join(hubRoot, ".ai", "memories", "shared", projectName);
3264
+ const projectName = path28.basename(root);
3265
+ const destDir = path28.join(hubRoot, ".ai", "memories", "shared", projectName);
3202
3266
  await mkdir9(destDir, { recursive: true });
3203
3267
  const all = await loadMemoriesFromDir7(paths.memoriesDir);
3204
3268
  const shared = all.filter(
@@ -3217,7 +3281,7 @@ Next steps:
3217
3281
  for (const { memory: memory2 } of shared) {
3218
3282
  const fm = memory2.frontmatter;
3219
3283
  const fileName = `${fm.id}.md`;
3220
- const destPath = path27.join(destDir, fileName);
3284
+ const destPath = path28.join(destDir, fileName);
3221
3285
  await writeFile15(destPath, serializeMemory12(memory2), "utf8");
3222
3286
  pushed++;
3223
3287
  }
@@ -3225,7 +3289,7 @@ Next steps:
3225
3289
  console.log(ui.dim(` Location: ${destDir}`));
3226
3290
  if (opts.commit) {
3227
3291
  const message = opts.message ?? `haive: sync shared memories from ${projectName} (${pushed} memories)`;
3228
- spawnSync3("git", ["add", path27.join(".ai", "memories", "shared", projectName)], {
3292
+ spawnSync3("git", ["add", path28.join(".ai", "memories", "shared", projectName)], {
3229
3293
  cwd: hubRoot
3230
3294
  });
3231
3295
  const commit = spawnSync3("git", ["commit", "-m", message], {
@@ -3260,13 +3324,13 @@ Next steps:
3260
3324
  process.exitCode = 1;
3261
3325
  return;
3262
3326
  }
3263
- const hubRoot = path27.resolve(root, config.hubPath);
3264
- const hubSharedDir = path27.join(hubRoot, ".ai", "memories", "shared");
3327
+ const hubRoot = path28.resolve(root, config.hubPath);
3328
+ const hubSharedDir = path28.join(hubRoot, ".ai", "memories", "shared");
3265
3329
  if (!existsSync29(hubSharedDir)) {
3266
3330
  ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
3267
3331
  return;
3268
3332
  }
3269
- const projectName = path27.basename(root);
3333
+ const projectName = path28.basename(root);
3270
3334
  const { readdir: readdir2 } = await import("fs/promises");
3271
3335
  const projectDirs = (await readdir2(hubSharedDir, { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== projectName).map((d) => d.name);
3272
3336
  if (projectDirs.length === 0) {
@@ -3276,16 +3340,16 @@ Next steps:
3276
3340
  let totalImported = 0;
3277
3341
  let totalUpdated = 0;
3278
3342
  for (const sourceName of projectDirs) {
3279
- const sourceDir = path27.join(hubSharedDir, sourceName);
3280
- const destDir = path27.join(paths.memoriesDir, "shared", sourceName);
3343
+ const sourceDir = path28.join(hubSharedDir, sourceName);
3344
+ const destDir = path28.join(paths.memoriesDir, "shared", sourceName);
3281
3345
  await mkdir9(destDir, { recursive: true });
3282
3346
  const sourceFiles = (await readdir2(sourceDir)).filter((f) => f.endsWith(".md"));
3283
3347
  const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
3284
3348
  const existingInDest = await loadDir(destDir);
3285
3349
  const existingIds = new Set(existingInDest.map(({ memory: memory2 }) => memory2.frontmatter.id));
3286
3350
  for (const file of sourceFiles) {
3287
- const srcPath = path27.join(sourceDir, file);
3288
- const destPath = path27.join(destDir, file);
3351
+ const srcPath = path28.join(sourceDir, file);
3352
+ const destPath = path28.join(destDir, file);
3289
3353
  const fileContent = await readFile9(srcPath, "utf8");
3290
3354
  const alreadyTagged = fileContent.includes(`cross-repo:${sourceName}`);
3291
3355
  if (!alreadyTagged) {
@@ -3316,14 +3380,14 @@ Next steps:
3316
3380
  console.log(
3317
3381
  ` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
3318
3382
  );
3319
- const sharedDir = path27.join(paths.memoriesDir, "shared");
3383
+ const sharedDir = path28.join(paths.memoriesDir, "shared");
3320
3384
  if (existsSync29(sharedDir)) {
3321
3385
  const { readdir: readdir2 } = await import("fs/promises");
3322
3386
  const sources = (await readdir2(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
3323
3387
  console.log(`
3324
3388
  Imported from ${sources.length} source(s):`);
3325
3389
  for (const src of sources) {
3326
- const files = (await readdir2(path27.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
3390
+ const files = (await readdir2(path28.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
3327
3391
  console.log(` ${src}: ${files.length} memor${files.length === 1 ? "y" : "ies"}`);
3328
3392
  }
3329
3393
  } else {
@@ -3344,9 +3408,944 @@ Next steps:
3344
3408
  });
3345
3409
  }
3346
3410
 
3411
+ // src/commands/stats.ts
3412
+ import "commander";
3413
+ import {
3414
+ aggregateUsage,
3415
+ findProjectRoot as findProjectRoot32,
3416
+ parseSince,
3417
+ readUsageEvents,
3418
+ resolveHaivePaths as resolveHaivePaths29,
3419
+ usageLogSize
3420
+ } from "@hiveai/core";
3421
+ function registerStats(program2) {
3422
+ 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) => {
3423
+ const root = findProjectRoot32(opts.dir);
3424
+ const paths = resolveHaivePaths29(root);
3425
+ const size = await usageLogSize(paths);
3426
+ if (!size.exists) {
3427
+ if (opts.json) {
3428
+ console.log(JSON.stringify({ error: "no_usage_log" }));
3429
+ return;
3430
+ }
3431
+ ui.warn(
3432
+ `No usage log found at ${root}. Stats are populated as the MCP server logs each tool call. Run a session first, then re-check.`
3433
+ );
3434
+ return;
3435
+ }
3436
+ const events = await readUsageEvents(paths);
3437
+ const since = parseSince(opts.since);
3438
+ const aggregate = aggregateUsage(events, since ?? void 0);
3439
+ if (opts.json) {
3440
+ console.log(JSON.stringify(aggregate, null, 2));
3441
+ return;
3442
+ }
3443
+ const window = opts.since ?? "all time";
3444
+ console.log(ui.bold(`hAIve usage stats (${window})`));
3445
+ console.log(
3446
+ ` ${ui.dim("total calls:")} ${aggregate.total} ${ui.dim("unique tools:")} ${aggregate.by_tool.length} ${ui.dim("log lines:")} ${size.lines}`
3447
+ );
3448
+ if (aggregate.window_start) {
3449
+ console.log(
3450
+ ` ${ui.dim("window:")} ${aggregate.window_start.slice(0, 19)} \u2192 ${aggregate.window_end?.slice(0, 19)}`
3451
+ );
3452
+ }
3453
+ if (aggregate.by_tool.length === 0) {
3454
+ ui.info(`No events in window. Try a wider --since (current: ${window}).`);
3455
+ return;
3456
+ }
3457
+ console.log();
3458
+ console.log(ui.bold("Top tools:"));
3459
+ const maxCount = aggregate.by_tool[0]?.count ?? 1;
3460
+ for (const t of aggregate.by_tool.slice(0, 20)) {
3461
+ const bar = "\u2588".repeat(Math.max(1, Math.round(t.count / maxCount * 30)));
3462
+ const pct = (t.count / aggregate.total * 100).toFixed(1);
3463
+ console.log(
3464
+ ` ${t.tool.padEnd(28)} ${ui.green(bar)} ${ui.bold(String(t.count))} ${ui.dim(`(${pct}%, last ${t.last_used.slice(0, 19)})`)}`
3465
+ );
3466
+ }
3467
+ });
3468
+ }
3469
+
3470
+ // src/commands/bench.ts
3471
+ import { performance } from "perf_hooks";
3472
+ import "commander";
3473
+ import {
3474
+ estimateTokens,
3475
+ findProjectRoot as findProjectRoot33,
3476
+ resolveHaivePaths as resolveHaivePaths30
3477
+ } from "@hiveai/core";
3478
+ import {
3479
+ antiPatternsCheck,
3480
+ codeMapTool,
3481
+ codeSearch,
3482
+ getBriefing,
3483
+ getRecap,
3484
+ memRelevantTo
3485
+ } from "@hiveai/mcp";
3486
+ function registerBench(program2) {
3487
+ 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) => {
3488
+ const root = findProjectRoot33(opts.dir);
3489
+ const paths = resolveHaivePaths30(root);
3490
+ const ctx = { paths };
3491
+ const task = opts.task ?? "audit dependencies for security risks";
3492
+ const scenarios = [
3493
+ async () => {
3494
+ const t0 = performance.now();
3495
+ const out = await getBriefing(
3496
+ {
3497
+ task,
3498
+ files: [],
3499
+ max_tokens: 4e3,
3500
+ max_memories: 8,
3501
+ include_project_context: true,
3502
+ include_module_contexts: true,
3503
+ semantic: true,
3504
+ include_stale: false,
3505
+ track: false,
3506
+ format: "compact",
3507
+ symbols: [],
3508
+ min_semantic_score: 0
3509
+ },
3510
+ ctx
3511
+ );
3512
+ return summarize("get_briefing(compact)", t0, out, [
3513
+ out.low_value ? "low_value (cold-start)" : `${out.memories.length} memories`,
3514
+ `search=${out.search_mode}`
3515
+ ]);
3516
+ },
3517
+ async () => {
3518
+ const t0 = performance.now();
3519
+ const out = await codeMapTool({ paths: [], max_files: 40, max_tokens: 2e3 }, ctx);
3520
+ return summarize("code_map(budget=2k)", t0, out, [
3521
+ out.available ? `${out.files.length}/${out.total_files} files` : "unavailable",
3522
+ out.budget_clipped ? "clipped" : "fits"
3523
+ ]);
3524
+ },
3525
+ async () => {
3526
+ const t0 = performance.now();
3527
+ const out = await getRecap({ scope: "any" }, ctx);
3528
+ return summarize("get_recap", t0, out, [
3529
+ out.recap ? `${out.recap.id.slice(0, 30)}\u2026` : "no recap"
3530
+ ]);
3531
+ },
3532
+ async () => {
3533
+ const t0 = performance.now();
3534
+ const out = await memRelevantTo(
3535
+ { task, files: [], limit: 8, min_semantic_score: 0.25, format: "compact" },
3536
+ ctx
3537
+ );
3538
+ return summarize("mem_relevant_to", t0, out, [
3539
+ `${out.memories.length} memories`,
3540
+ `search=${out.search_mode}`
3541
+ ]);
3542
+ },
3543
+ async () => {
3544
+ const t0 = performance.now();
3545
+ const out = await codeSearch({ query: task, k: 5, min_score: 0.2 }, ctx);
3546
+ return summarize("code_search", t0, out, [
3547
+ out.available ? `${out.hits.length} hits` : "needs index (haive index code-search)"
3548
+ ]);
3549
+ },
3550
+ async () => {
3551
+ const t0 = performance.now();
3552
+ const out = await antiPatternsCheck({ diff: task, paths: [], limit: 5, semantic: true }, ctx);
3553
+ return summarize("anti_patterns_check", t0, out, [
3554
+ `${out.warnings.length}/${out.scanned} warn`
3555
+ ]);
3556
+ }
3557
+ ];
3558
+ const results = [];
3559
+ for (const run of scenarios) {
3560
+ try {
3561
+ results.push(await run());
3562
+ } catch (err) {
3563
+ results.push({
3564
+ name: "(error)",
3565
+ ok: false,
3566
+ latency_ms: 0,
3567
+ payload_tokens: 0,
3568
+ notes: [err instanceof Error ? err.message : String(err)]
3569
+ });
3570
+ }
3571
+ }
3572
+ if (opts.json) {
3573
+ console.log(JSON.stringify({ root, task, scenarios: results }, null, 2));
3574
+ return;
3575
+ }
3576
+ console.log(ui.bold(`hAIve bench \u2014 ${root}`));
3577
+ console.log(ui.dim(`task: ${task}`));
3578
+ console.log();
3579
+ console.log(
3580
+ `${"scenario".padEnd(28)} ${"latency".padStart(8)} ${"tokens".padStart(7)} notes`
3581
+ );
3582
+ console.log("\u2500".repeat(88));
3583
+ for (const r of results) {
3584
+ const status = r.ok ? ui.green("\u2713") : ui.red("\u2717");
3585
+ console.log(
3586
+ `${status} ${r.name.padEnd(26)} ${`${r.latency_ms.toFixed(0)} ms`.padStart(8)} ${String(r.payload_tokens).padStart(7)} ${r.notes.join("; ")}`
3587
+ );
3588
+ }
3589
+ const totalTokens = results.reduce((s, r) => s + r.payload_tokens, 0);
3590
+ const totalMs = results.reduce((s, r) => s + r.latency_ms, 0);
3591
+ console.log("\u2500".repeat(88));
3592
+ console.log(
3593
+ `${ui.dim("totals:")} ${`${totalMs.toFixed(0)} ms`.padStart(8)} ${String(totalTokens).padStart(7)}`
3594
+ );
3595
+ });
3596
+ }
3597
+ function summarize(name, t0, payload, notes) {
3598
+ return {
3599
+ name,
3600
+ ok: true,
3601
+ latency_ms: performance.now() - t0,
3602
+ payload_tokens: estimateTokens(JSON.stringify(payload)),
3603
+ notes
3604
+ };
3605
+ }
3606
+
3607
+ // src/commands/memory-suggest.ts
3608
+ import { mkdir as mkdir10, writeFile as writeFile16 } from "fs/promises";
3609
+ import { existsSync as existsSync30 } from "fs";
3610
+ import path29 from "path";
3611
+ import "commander";
3612
+ import {
3613
+ aggregateUsage as aggregateUsage2,
3614
+ buildFrontmatter as buildFrontmatter6,
3615
+ findProjectRoot as findProjectRoot34,
3616
+ loadMemoriesFromDir as loadMemoriesFromDir8,
3617
+ memoryFilePath as memoryFilePath5,
3618
+ parseSince as parseSince2,
3619
+ readUsageEvents as readUsageEvents2,
3620
+ resolveHaivePaths as resolveHaivePaths31,
3621
+ serializeMemory as serializeMemory13
3622
+ } from "@hiveai/core";
3623
+ var SEARCH_TOOLS = /* @__PURE__ */ new Set([
3624
+ "mem_search",
3625
+ "code_search",
3626
+ "mem_relevant_to",
3627
+ "get_briefing"
3628
+ ]);
3629
+ function registerMemorySuggest(memory2) {
3630
+ memory2.command("suggest").description(
3631
+ "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."
3632
+ ).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) => {
3633
+ const root = findProjectRoot34(opts.dir);
3634
+ const paths = resolveHaivePaths31(root);
3635
+ const events = await readUsageEvents2(paths);
3636
+ if (events.length === 0) {
3637
+ if (opts.json) {
3638
+ console.log(JSON.stringify({ suggestions: [] }));
3639
+ return;
3640
+ }
3641
+ ui.warn("No usage log entries yet. Suggestions appear after the MCP server records some calls.");
3642
+ return;
3643
+ }
3644
+ const since = parseSince2(opts.since);
3645
+ const minCount = Math.max(1, parseInt(opts.min ?? "2", 10));
3646
+ const cutoff = since ? since.getTime() : 0;
3647
+ const queries = /* @__PURE__ */ new Map();
3648
+ for (const e of events) {
3649
+ if (cutoff && Date.parse(e.at) < cutoff) continue;
3650
+ if (!SEARCH_TOOLS.has(e.tool)) continue;
3651
+ const key = (e.summary ?? "").toLowerCase().trim();
3652
+ if (!key) continue;
3653
+ const prior = queries.get(key);
3654
+ if (prior) {
3655
+ prior.count++;
3656
+ prior.tools.add(e.tool);
3657
+ if (e.at > prior.last) prior.last = e.at;
3658
+ } else {
3659
+ queries.set(key, { count: 1, tools: /* @__PURE__ */ new Set([e.tool]), last: e.at });
3660
+ }
3661
+ }
3662
+ const suggestions = [...queries.entries()].filter(([, v]) => v.count >= minCount).map(([query, v]) => ({
3663
+ query,
3664
+ count: v.count,
3665
+ tools: [...v.tools].sort(),
3666
+ last_used: v.last,
3667
+ reason: chooseReason(v.tools, v.count),
3668
+ inferred_type: inferType(v.tools, query)
3669
+ })).sort((a, b) => b.count - a.count);
3670
+ if (opts.autoSave) {
3671
+ const topN = Math.max(1, parseInt(opts.topN ?? "3", 10));
3672
+ const scope = opts.scope === "team" ? "team" : "personal";
3673
+ const top = suggestions.slice(0, topN);
3674
+ if (top.length === 0) {
3675
+ ui.warn(`No suggestions met --min=${minCount} \u2014 nothing to draft.`);
3676
+ return;
3677
+ }
3678
+ const created = [];
3679
+ const skipped = [];
3680
+ const existing = existsSync30(paths.memoriesDir) ? await loadMemoriesFromDir8(paths.memoriesDir) : [];
3681
+ for (const s of top) {
3682
+ const slug = slugify(s.query);
3683
+ if (!slug) {
3684
+ skipped.push({ query: s.query, reason: "could not derive a slug" });
3685
+ continue;
3686
+ }
3687
+ const dup = existing.find(({ memory: memory3 }) => memory3.frontmatter.id.endsWith(`-${slug}`));
3688
+ if (dup) {
3689
+ skipped.push({ query: s.query, reason: `similar memory already exists (${dup.memory.frontmatter.id})` });
3690
+ continue;
3691
+ }
3692
+ const fm = buildFrontmatter6({
3693
+ type: s.inferred_type,
3694
+ slug,
3695
+ scope,
3696
+ tags: ["auto-suggested", ...s.tools],
3697
+ paths: [],
3698
+ symbols: []
3699
+ });
3700
+ fm.status = "draft";
3701
+ const body = renderTemplate(s);
3702
+ const file = memoryFilePath5(paths, fm.scope, fm.id, fm.module);
3703
+ await mkdir10(path29.dirname(file), { recursive: true });
3704
+ if (existsSync30(file)) {
3705
+ skipped.push({ query: s.query, reason: `file already exists at ${path29.relative(root, file)}` });
3706
+ continue;
3707
+ }
3708
+ await writeFile16(file, serializeMemory13({ frontmatter: fm, body }), "utf8");
3709
+ created.push({ id: fm.id, file: path29.relative(root, file), query: s.query });
3710
+ }
3711
+ if (opts.json) {
3712
+ console.log(JSON.stringify({ created, skipped }, null, 2));
3713
+ return;
3714
+ }
3715
+ for (const c of created) {
3716
+ ui.success(`Drafted ${c.id} \u2192 ${c.file}`);
3717
+ console.log(` ${ui.dim("from query:")} ${truncate(c.query, 60)}`);
3718
+ }
3719
+ for (const s of skipped) {
3720
+ ui.warn(`Skipped: ${truncate(s.query, 50)} \u2014 ${s.reason}`);
3721
+ }
3722
+ if (created.length > 0) {
3723
+ console.log();
3724
+ ui.info("Drafts are status=draft \u2014 edit them, then `haive memory promote` to validate.");
3725
+ }
3726
+ return;
3727
+ }
3728
+ if (opts.json) {
3729
+ console.log(JSON.stringify({ window: opts.since, suggestions }, null, 2));
3730
+ return;
3731
+ }
3732
+ const totals = aggregateUsage2(events, since ?? void 0);
3733
+ console.log(ui.bold(`hAIve memory suggestions (${opts.since ?? "all time"})`));
3734
+ console.log(
3735
+ ui.dim(`scanned ${totals.total} events, ${suggestions.length} repeated queries (\u2265${minCount})`)
3736
+ );
3737
+ console.log();
3738
+ if (suggestions.length === 0) {
3739
+ ui.info("No recurring searches yet \u2014 nothing to suggest.");
3740
+ return;
3741
+ }
3742
+ for (const s of suggestions.slice(0, 30)) {
3743
+ console.log(
3744
+ ` ${ui.bold(`\xD7${s.count}`)} ${ui.dim(`[${s.tools.join(",")}]`)} ${truncate(s.query, 70)}`
3745
+ );
3746
+ console.log(` ${ui.dim("\u2192")} ${s.reason}`);
3747
+ }
3748
+ console.log();
3749
+ ui.info("Run with --auto-save to draft the top-3 as draft memories.");
3750
+ });
3751
+ }
3752
+ function chooseReason(tools, count) {
3753
+ if (tools.has("code_search")) {
3754
+ return `${count} agents searched the code for this \u2014 consider mem_save (architecture/decision) capturing where it lives.`;
3755
+ }
3756
+ if (tools.has("mem_search") || tools.has("mem_relevant_to")) {
3757
+ return `${count} agents asked but the memory layer had no clear answer \u2014 consider mem_save (convention/decision/gotcha).`;
3758
+ }
3759
+ return `${count} agents asked the briefing for this \u2014 consider promoting the answer to a team memory.`;
3760
+ }
3761
+ function inferType(tools, query) {
3762
+ const q = query.toLowerCase();
3763
+ if (q.includes("bug") || q.includes("error") || q.includes("crash") || q.includes("trap")) return "gotcha";
3764
+ if (q.includes("decid") || q.includes("why") || q.includes("choose") || q.includes("vs ")) return "decision";
3765
+ if (tools.has("code_search") && (q.includes("where") || q.includes("location") || q.includes("structure"))) {
3766
+ return "architecture";
3767
+ }
3768
+ return "convention";
3769
+ }
3770
+ function renderTemplate(s) {
3771
+ return [
3772
+ `# Auto-drafted from recurring searches`,
3773
+ ``,
3774
+ `> This memory was drafted by \`haive memory suggest --auto-save\` because`,
3775
+ `> agents searched for this **${s.count} times** in the recent window`,
3776
+ `> via ${s.tools.join(", ")}.`,
3777
+ ``,
3778
+ `## Query`,
3779
+ ``,
3780
+ `> ${s.query}`,
3781
+ ``,
3782
+ `## What to fill in`,
3783
+ ``,
3784
+ `Replace this section with the actual answer the team keeps re-discovering:`,
3785
+ ``,
3786
+ `- **What** \u2014 the convention / decision / gotcha (1-3 sentences)`,
3787
+ `- **Why** \u2014 the rationale or root cause`,
3788
+ `- **How to apply** \u2014 what an agent should do when this comes up again`,
3789
+ ``,
3790
+ `Then run \`haive memory promote ${truncate(s.query, 30)}\` to mark it validated.`
3791
+ ].join("\n");
3792
+ }
3793
+ function slugify(s) {
3794
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
3795
+ }
3796
+ function truncate(text, max) {
3797
+ if (text.length <= max) return text;
3798
+ return text.slice(0, max - 1) + "\u2026";
3799
+ }
3800
+
3801
+ // src/commands/memory-archive.ts
3802
+ import { existsSync as existsSync31 } from "fs";
3803
+ import { writeFile as writeFile17 } from "fs/promises";
3804
+ import path30 from "path";
3805
+ import "commander";
3806
+ import {
3807
+ findProjectRoot as findProjectRoot35,
3808
+ getUsage as getUsage9,
3809
+ loadMemoriesFromDir as loadMemoriesFromDir9,
3810
+ loadUsageIndex as loadUsageIndex11,
3811
+ resolveHaivePaths as resolveHaivePaths32,
3812
+ serializeMemory as serializeMemory14
3813
+ } from "@hiveai/core";
3814
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
3815
+ function registerMemoryArchive(memory2) {
3816
+ memory2.command("archive").description(
3817
+ "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."
3818
+ ).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) => {
3819
+ const root = findProjectRoot35(opts.dir);
3820
+ const paths = resolveHaivePaths32(root);
3821
+ if (!existsSync31(paths.memoriesDir)) {
3822
+ ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
3823
+ process.exitCode = 1;
3824
+ return;
3825
+ }
3826
+ const minDays = parseDays(opts.since ?? "180d");
3827
+ if (minDays === null) {
3828
+ ui.error(`Invalid --since value: ${opts.since}. Use formats like '180d', '6m', '1y'.`);
3829
+ process.exitCode = 1;
3830
+ return;
3831
+ }
3832
+ const cutoff = Date.now() - minDays * MS_PER_DAY;
3833
+ const all = await loadMemoriesFromDir9(paths.memoriesDir);
3834
+ const usage = await loadUsageIndex11(paths);
3835
+ const typeFilter = opts.type === "all" ? null : opts.type ?? "attempt";
3836
+ const candidates = [];
3837
+ for (const { memory: mem, filePath } of all) {
3838
+ const fm = mem.frontmatter;
3839
+ if (typeFilter && fm.type !== typeFilter) continue;
3840
+ if (fm.status === "deprecated" || fm.status === "rejected") continue;
3841
+ const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
3842
+ const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync31(path30.join(paths.root, p)));
3843
+ const isAnchorless = !hasAnyAnchor;
3844
+ if (!isAnchorless && !allPathsGone) continue;
3845
+ const u = getUsage9(usage, fm.id);
3846
+ const lastSeen = u.last_read_at ?? fm.created_at;
3847
+ if (Date.parse(lastSeen) >= cutoff) continue;
3848
+ candidates.push({
3849
+ id: fm.id,
3850
+ type: fm.type,
3851
+ status: fm.status,
3852
+ last_seen: lastSeen,
3853
+ reason: isAnchorless ? `anchorless and not read since ${lastSeen.slice(0, 10)}` : `all ${fm.anchor.paths.length} anchored path(s) missing and not read since ${lastSeen.slice(0, 10)}`,
3854
+ filePath
3855
+ });
3856
+ }
3857
+ if (opts.json) {
3858
+ console.log(JSON.stringify({
3859
+ dry_run: !opts.apply,
3860
+ window_days: minDays,
3861
+ candidates: candidates.length,
3862
+ archived: opts.apply ? candidates.length : 0,
3863
+ items: candidates
3864
+ }, null, 2));
3865
+ } else {
3866
+ const header = opts.apply ? "Archiving" : "Would archive";
3867
+ console.log(ui.bold(`${header} ${candidates.length} memor${candidates.length === 1 ? "y" : "ies"} (older than ${minDays}d, type=${typeFilter ?? "all"})`));
3868
+ if (candidates.length === 0) {
3869
+ ui.info("Nothing to archive \u2014 all memories are anchored or read recently.");
3870
+ return;
3871
+ }
3872
+ for (const c of candidates) {
3873
+ console.log(` ${ui.dim(c.last_seen.slice(0, 10))} ${c.id} ${ui.dim(`(${c.type})`)} \u2014 ${c.reason}`);
3874
+ }
3875
+ }
3876
+ if (!opts.apply) {
3877
+ if (!opts.json) {
3878
+ console.log();
3879
+ ui.info("Dry run \u2014 pass --apply to mark these as deprecated on disk.");
3880
+ }
3881
+ return;
3882
+ }
3883
+ let archived = 0;
3884
+ let failed = 0;
3885
+ for (const c of candidates) {
3886
+ const found = all.find(({ filePath }) => filePath === c.filePath);
3887
+ if (!found) continue;
3888
+ const fm = { ...found.memory.frontmatter, status: "deprecated" };
3889
+ try {
3890
+ await writeFile17(c.filePath, serializeMemory14({ frontmatter: fm, body: found.memory.body }), "utf8");
3891
+ archived++;
3892
+ } catch (err) {
3893
+ if (!opts.json) {
3894
+ ui.error(`Failed to archive ${c.id}: ${err instanceof Error ? err.message : String(err)}`);
3895
+ }
3896
+ failed++;
3897
+ }
3898
+ }
3899
+ if (!opts.json) {
3900
+ ui.success(`Archived ${archived} memor${archived === 1 ? "y" : "ies"}${failed > 0 ? ` (${failed} failed)` : ""}`);
3901
+ }
3902
+ });
3903
+ }
3904
+ function parseDays(input) {
3905
+ const m = input.match(/^(\d+)([dmy])$/);
3906
+ if (!m) return null;
3907
+ const n = parseInt(m[1] ?? "0", 10);
3908
+ const unit = m[2] ?? "d";
3909
+ if (unit === "d") return n;
3910
+ if (unit === "m") return n * 30;
3911
+ if (unit === "y") return n * 365;
3912
+ return null;
3913
+ }
3914
+
3915
+ // src/commands/doctor.ts
3916
+ import { existsSync as existsSync32 } from "fs";
3917
+ import { stat } from "fs/promises";
3918
+ import "path";
3919
+ import "commander";
3920
+ import {
3921
+ codeMapPath as codeMapPath2,
3922
+ findProjectRoot as findProjectRoot36,
3923
+ getUsage as getUsage10,
3924
+ loadCodeMap as loadCodeMap3,
3925
+ loadConfig as loadConfig4,
3926
+ loadMemoriesFromDir as loadMemoriesFromDir10,
3927
+ loadUsageIndex as loadUsageIndex12,
3928
+ readUsageEvents as readUsageEvents3,
3929
+ resolveHaivePaths as resolveHaivePaths33
3930
+ } from "@hiveai/core";
3931
+ var MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
3932
+ function registerDoctor(program2) {
3933
+ program2.command("doctor").description(
3934
+ "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."
3935
+ ).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) => {
3936
+ const root = findProjectRoot36(opts.dir);
3937
+ const paths = resolveHaivePaths33(root);
3938
+ const findings = [];
3939
+ if (!existsSync32(paths.haiveDir)) {
3940
+ findings.push({
3941
+ severity: "error",
3942
+ code: "not-initialized",
3943
+ message: ".ai/ directory missing \u2014 haive is not initialized in this project.",
3944
+ fix: "haive init"
3945
+ });
3946
+ return emit(findings, opts);
3947
+ }
3948
+ if (!existsSync32(paths.projectContext)) {
3949
+ findings.push({
3950
+ severity: "warn",
3951
+ code: "no-project-context",
3952
+ message: ".ai/project-context.md is missing.",
3953
+ fix: "haive init"
3954
+ });
3955
+ } else {
3956
+ const { readFile: readFile10 } = await import("fs/promises");
3957
+ const content = await readFile10(paths.projectContext, "utf8");
3958
+ const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
3959
+ if (isTemplate) {
3960
+ findings.push({
3961
+ severity: "warn",
3962
+ code: "template-context",
3963
+ message: "project-context.md still contains the default template \u2014 get_briefing returns little value until filled.",
3964
+ fix: "Invoke the bootstrap_project MCP prompt from your AI client."
3965
+ });
3966
+ }
3967
+ }
3968
+ const memories = existsSync32(paths.memoriesDir) ? await loadMemoriesFromDir10(paths.memoriesDir) : [];
3969
+ const now = Date.now();
3970
+ if (memories.length === 0) {
3971
+ findings.push({
3972
+ severity: "info",
3973
+ code: "no-memories",
3974
+ message: "No memories yet. Capture knowledge as agents work via mem_save / mem_observe / mem_tried."
3975
+ });
3976
+ } else {
3977
+ const usage = await loadUsageIndex12(paths);
3978
+ const stale = memories.filter((m) => m.memory.frontmatter.status === "stale");
3979
+ if (stale.length > 0) {
3980
+ findings.push({
3981
+ severity: "warn",
3982
+ code: "stale-memories",
3983
+ message: `${stale.length} memor${stale.length === 1 ? "y" : "ies"} marked stale (anchored code drifted).`,
3984
+ fix: "haive memory verify --update # re-check anchors\nhaive memory edit <id> # manually refresh body"
3985
+ });
3986
+ }
3987
+ const proposed = memories.filter((m) => m.memory.frontmatter.status === "proposed");
3988
+ if (proposed.length > 0) {
3989
+ findings.push({
3990
+ severity: "info",
3991
+ code: "pending-review",
3992
+ message: `${proposed.length} memor${proposed.length === 1 ? "y is" : "ies are"} proposed and awaiting validation.`,
3993
+ fix: "haive memory pending # list them\nhaive memory auto-promote # promote those with high read_count"
3994
+ });
3995
+ }
3996
+ const anchorless = memories.filter(
3997
+ (m) => m.memory.frontmatter.anchor.paths.length === 0 && m.memory.frontmatter.anchor.symbols.length === 0 && m.memory.frontmatter.type !== "session_recap" && m.memory.frontmatter.type !== "glossary"
3998
+ );
3999
+ if (anchorless.length / Math.max(memories.length, 1) > 0.3) {
4000
+ findings.push({
4001
+ severity: "warn",
4002
+ code: "anchorless-majority",
4003
+ message: `${anchorless.length}/${memories.length} memories have no anchor path/symbol \u2014 staleness undetectable.`,
4004
+ fix: "Add `paths:` + `symbols:` to mem_save calls to enable haive memory verify."
4005
+ });
4006
+ }
4007
+ const decayCandidates = memories.filter((m) => {
4008
+ if (m.memory.frontmatter.status !== "validated") return false;
4009
+ const u = getUsage10(usage, m.memory.frontmatter.id);
4010
+ const last = u.last_read_at ?? m.memory.frontmatter.created_at;
4011
+ return (now - Date.parse(last)) / MS_PER_DAY2 > 180;
4012
+ });
4013
+ if (decayCandidates.length > 0) {
4014
+ findings.push({
4015
+ severity: "info",
4016
+ code: "decay-candidates",
4017
+ message: `${decayCandidates.length} validated memor${decayCandidates.length === 1 ? "y has" : "ies have"} not been read in 180+ days \u2014 confidence is decaying.`,
4018
+ fix: "haive memory archive --type all --since 365d # dry run"
4019
+ });
4020
+ }
4021
+ }
4022
+ const codeMap = await loadCodeMap3(paths);
4023
+ if (!codeMap) {
4024
+ findings.push({
4025
+ severity: "warn",
4026
+ code: "no-code-map",
4027
+ message: "No code-map found \u2014 code_map MCP tool and symbol_locations are unavailable.",
4028
+ fix: "haive index code"
4029
+ });
4030
+ } else {
4031
+ const cmFile = codeMapPath2(paths);
4032
+ const cmStat = await stat(cmFile);
4033
+ const ageDays = (now - cmStat.mtimeMs) / MS_PER_DAY2;
4034
+ if (ageDays > 14) {
4035
+ findings.push({
4036
+ severity: "warn",
4037
+ code: "stale-code-map",
4038
+ message: `code-map is ${Math.round(ageDays)} days old (${Object.keys(codeMap.files).length} files indexed).`,
4039
+ fix: "haive index code # or rely on the post-merge git hook"
4040
+ });
4041
+ }
4042
+ }
4043
+ const events = await readUsageEvents3(paths);
4044
+ if (events.length === 0) {
4045
+ findings.push({
4046
+ severity: "info",
4047
+ code: "no-usage-log",
4048
+ message: "No usage log entries \u2014 MCP server hasn't recorded any calls yet, or this project hasn't been used by an agent."
4049
+ });
4050
+ } else {
4051
+ const queryRepeats = /* @__PURE__ */ new Map();
4052
+ for (const e of events) {
4053
+ if (!isSearchTool(e.tool)) continue;
4054
+ const key = (e.summary ?? "").toLowerCase().trim();
4055
+ if (!key) continue;
4056
+ queryRepeats.set(key, (queryRepeats.get(key) ?? 0) + 1);
4057
+ }
4058
+ const repeated = [...queryRepeats.entries()].filter(([, n]) => n >= 3);
4059
+ if (repeated.length > 0) {
4060
+ findings.push({
4061
+ severity: "info",
4062
+ code: "recurring-searches",
4063
+ message: `${repeated.length} query${repeated.length === 1 ? "" : "ies"} repeated 3+ times \u2014 agents keep asking the same things.`,
4064
+ fix: `haive memory suggest --auto-save --top-n ${Math.min(5, repeated.length)}`
4065
+ });
4066
+ }
4067
+ const codeMapCalls = events.filter((e) => e.tool === "code_map").length;
4068
+ const briefingCalls = events.filter((e) => e.tool === "get_briefing").length;
4069
+ if (codeMapCalls > 0 && memories.length > 0) {
4070
+ findings.push({
4071
+ severity: "info",
4072
+ code: "tool-mix",
4073
+ message: `${briefingCalls} get_briefing call${briefingCalls === 1 ? "" : "s"}, ${codeMapCalls} code_map call${codeMapCalls === 1 ? "" : "s"} recorded.`
4074
+ });
4075
+ }
4076
+ }
4077
+ const config = await loadConfig4(paths);
4078
+ if (!config.autoSessionEnd) {
4079
+ findings.push({
4080
+ severity: "info",
4081
+ code: "no-autopilot",
4082
+ message: "Autopilot is OFF \u2014 session recaps are not auto-saved on shutdown.",
4083
+ fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
4084
+ });
4085
+ }
4086
+ emit(findings, opts);
4087
+ });
4088
+ }
4089
+ function emit(findings, opts) {
4090
+ if (opts.json) {
4091
+ console.log(JSON.stringify({ findings }, null, 2));
4092
+ return;
4093
+ }
4094
+ if (findings.length === 0) {
4095
+ ui.success("hAIve doctor \u2014 no issues found.");
4096
+ return;
4097
+ }
4098
+ console.log(ui.bold(`hAIve doctor \u2014 ${findings.length} finding${findings.length === 1 ? "" : "s"}`));
4099
+ console.log();
4100
+ const order = ["error", "warn", "info"];
4101
+ for (const sev of order) {
4102
+ for (const f of findings.filter((x) => x.severity === sev)) {
4103
+ const icon = sev === "error" ? ui.red("\u2717") : sev === "warn" ? ui.yellow("\u26A0") : ui.dim("\u2139");
4104
+ console.log(`${icon} ${ui.bold(f.code)} ${f.message}`);
4105
+ if (opts.fix && f.fix) {
4106
+ for (const line of f.fix.split("\n")) {
4107
+ console.log(` ${ui.dim("$")} ${line}`);
4108
+ }
4109
+ }
4110
+ }
4111
+ }
4112
+ if (!opts.fix && findings.some((f) => f.fix)) {
4113
+ console.log();
4114
+ ui.info("Re-run with --fix to see suggested commands.");
4115
+ }
4116
+ }
4117
+ function isSearchTool(name) {
4118
+ return ["mem_search", "code_search", "mem_relevant_to", "get_briefing"].includes(name);
4119
+ }
4120
+
4121
+ // src/commands/playback.ts
4122
+ import { existsSync as existsSync33 } from "fs";
4123
+ import "commander";
4124
+ import {
4125
+ findProjectRoot as findProjectRoot37,
4126
+ loadMemoriesFromDir as loadMemoriesFromDir11,
4127
+ parseSince as parseSince3,
4128
+ readUsageEvents as readUsageEvents4,
4129
+ resolveHaivePaths as resolveHaivePaths34
4130
+ } from "@hiveai/core";
4131
+ var MS_PER_MINUTE = 6e4;
4132
+ function registerPlayback(program2) {
4133
+ program2.command("playback").description(
4134
+ "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?'"
4135
+ ).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) => {
4136
+ const root = findProjectRoot37(opts.dir);
4137
+ const paths = resolveHaivePaths34(root);
4138
+ const events = await readUsageEvents4(paths);
4139
+ if (events.length === 0) {
4140
+ if (opts.json) {
4141
+ console.log(JSON.stringify({ sessions: [] }));
4142
+ return;
4143
+ }
4144
+ ui.warn("No usage log entries yet.");
4145
+ return;
4146
+ }
4147
+ const since = parseSince3(opts.since);
4148
+ const cutoff = since ? since.getTime() : 0;
4149
+ const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
4150
+ const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
4151
+ const sessions = bucketSessions(filtered, gapMs);
4152
+ const all = existsSync33(paths.memoriesDir) ? await loadMemoriesFromDir11(paths.memoriesDir) : [];
4153
+ 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);
4154
+ const enriched = sessions.map((s, i) => {
4155
+ const startMs = Date.parse(s.start);
4156
+ const newer = memByCreatedAt.filter((m) => m.at > startMs);
4157
+ return {
4158
+ index: i,
4159
+ start: s.start,
4160
+ end: s.end,
4161
+ duration_minutes: (Date.parse(s.end) - startMs) / MS_PER_MINUTE,
4162
+ events: s.events.length,
4163
+ tools_count: countTools(s.events),
4164
+ briefing_tasks: s.events.filter((e) => e.tool === "get_briefing" && e.summary).map((e) => e.summary).slice(0, 5),
4165
+ memories_created_since: newer.length,
4166
+ new_memories: newer.slice(0, 5).map((m) => m.id)
4167
+ };
4168
+ });
4169
+ enriched.sort((a, b) => Date.parse(b.start) - Date.parse(a.start));
4170
+ const limit = Math.max(1, parseInt(opts.limit ?? "10", 10));
4171
+ const shown = enriched.slice(0, limit);
4172
+ if (opts.json) {
4173
+ console.log(JSON.stringify({
4174
+ window: opts.since,
4175
+ session_gap_minutes: gapMs / MS_PER_MINUTE,
4176
+ total_sessions: enriched.length,
4177
+ sessions: shown
4178
+ }, null, 2));
4179
+ return;
4180
+ }
4181
+ console.log(ui.bold(`hAIve playback \u2014 ${enriched.length} session(s) over ${opts.since ?? "all time"}`));
4182
+ console.log();
4183
+ for (const s of shown) {
4184
+ console.log(
4185
+ `${ui.bold(`Session ${s.index + 1}`)} ${ui.dim(s.start.slice(0, 19) + " \u2192 " + s.end.slice(11, 19))} ${ui.dim(`(${Math.round(s.duration_minutes)}m, ${s.events} call${s.events === 1 ? "" : "s"})`)}`
4186
+ );
4187
+ const toolList = Object.entries(s.tools_count).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([t, n]) => `${t}\xD7${n}`).join(", ");
4188
+ if (toolList) console.log(` ${ui.dim("tools:")} ${toolList}`);
4189
+ if (s.briefing_tasks.length > 0) {
4190
+ console.log(` ${ui.dim("briefings asked:")}`);
4191
+ for (const t of s.briefing_tasks) {
4192
+ console.log(` \u2022 ${truncate2(t, 80)}`);
4193
+ }
4194
+ }
4195
+ if (s.memories_created_since > 0) {
4196
+ console.log(
4197
+ ` ${ui.green("\u2934")} ${s.memories_created_since} memor${s.memories_created_since === 1 ? "y has" : "ies have"} been created since this session ` + ui.dim(`\u2014 newer haive could have answered better`)
4198
+ );
4199
+ for (const id of s.new_memories) {
4200
+ console.log(` + ${ui.dim(id)}`);
4201
+ }
4202
+ }
4203
+ console.log();
4204
+ }
4205
+ });
4206
+ }
4207
+ function bucketSessions(events, gapMs) {
4208
+ if (events.length === 0) return [];
4209
+ const sorted = [...events].sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
4210
+ const buckets = [];
4211
+ let current = null;
4212
+ for (const e of sorted) {
4213
+ if (!current) {
4214
+ current = { start: e.at, end: e.at, events: [e] };
4215
+ continue;
4216
+ }
4217
+ if (Date.parse(e.at) - Date.parse(current.end) > gapMs) {
4218
+ buckets.push(current);
4219
+ current = { start: e.at, end: e.at, events: [e] };
4220
+ } else {
4221
+ current.events.push(e);
4222
+ current.end = e.at;
4223
+ }
4224
+ }
4225
+ if (current) buckets.push(current);
4226
+ return buckets;
4227
+ }
4228
+ function countTools(events) {
4229
+ const out = {};
4230
+ for (const e of events) out[e.tool] = (out[e.tool] ?? 0) + 1;
4231
+ return out;
4232
+ }
4233
+ function truncate2(text, max) {
4234
+ if (text.length <= max) return text;
4235
+ return text.slice(0, max - 1) + "\u2026";
4236
+ }
4237
+
4238
+ // src/commands/precommit.ts
4239
+ import { spawn as spawn3 } from "child_process";
4240
+ import "commander";
4241
+ import {
4242
+ findProjectRoot as findProjectRoot38,
4243
+ resolveHaivePaths as resolveHaivePaths35
4244
+ } from "@hiveai/core";
4245
+ import { preCommitCheck } from "@hiveai/mcp";
4246
+ function registerPrecommit(program2) {
4247
+ program2.command("precommit").description(
4248
+ "Run a pre-commit safety check: scans `git diff --cached` against known anti-patterns,\n surfaces conventions/decisions anchored to touched files, and warns about stale anchored memories.\n\n Wire it into git as: `.git/hooks/pre-commit` running `haive precommit` (exit 1 = block).\n\n Examples:\n haive precommit # auto-detects staged diff\n haive precommit --block-on any # block on any warning, not just high-confidence\n haive precommit --paths src/auth.ts src/db.ts # explicit paths instead of git diff"
4249
+ ).option(
4250
+ "--block-on <mode>",
4251
+ "'any' | 'high-confidence' (default) | 'never' (report only)",
4252
+ "high-confidence"
4253
+ ).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) => {
4254
+ const root = findProjectRoot38(opts.dir);
4255
+ const paths = resolveHaivePaths35(root);
4256
+ const ctx = { paths };
4257
+ let diff = "";
4258
+ let touchedPaths = opts.paths ?? [];
4259
+ if (touchedPaths.length === 0) {
4260
+ try {
4261
+ diff = await runCommand("git", ["diff", "--cached"], root);
4262
+ if (!diff.trim()) {
4263
+ ui.warn("No staged changes \u2014 nothing to check. Stage with `git add` first.");
4264
+ return;
4265
+ }
4266
+ const nameOnly = await runCommand("git", ["diff", "--cached", "--name-only"], root);
4267
+ touchedPaths = nameOnly.split("\n").map((s) => s.trim()).filter(Boolean);
4268
+ } catch (err) {
4269
+ ui.error(`git diff failed: ${err instanceof Error ? err.message : String(err)}`);
4270
+ process.exit(1);
4271
+ }
4272
+ }
4273
+ const result = await preCommitCheck({
4274
+ diff: diff || void 0,
4275
+ paths: touchedPaths,
4276
+ block_on: opts.blockOn ?? "high-confidence",
4277
+ semantic: opts.noSemantic ? false : true
4278
+ }, ctx);
4279
+ if (opts.json) {
4280
+ console.log(JSON.stringify(result, null, 2));
4281
+ process.exit(result.should_block ? 1 : 0);
4282
+ }
4283
+ console.log(ui.bold(`hAIve precommit \u2014 ${touchedPaths.length} file(s)`));
4284
+ console.log(
4285
+ ui.dim(
4286
+ ` anti-patterns: ${result.summary.anti_patterns} relevant memories: ${result.summary.relevant_memories} stale anchors: ${result.summary.stale_anchors}`
4287
+ )
4288
+ );
4289
+ console.log();
4290
+ if (result.warnings.length > 0) {
4291
+ console.log(ui.bold("\u26A0 Anti-patterns matched:"));
4292
+ for (const w of result.warnings.slice(0, 10)) {
4293
+ console.log(` ${ui.yellow("\u26A0")} ${w.id} ${ui.dim(`(${w.type}, ${w.confidence})`)}`);
4294
+ for (const line of w.body_preview.split("\n").slice(0, 3)) {
4295
+ console.log(` ${ui.dim(line)}`);
4296
+ }
4297
+ console.log(` ${ui.dim("reasons:")} ${w.reasons.join(", ")}`);
4298
+ }
4299
+ console.log();
4300
+ }
4301
+ if (result.relevant_memories.length > 0) {
4302
+ console.log(ui.bold("\u{1F4CC} Relevant conventions/decisions:"));
4303
+ for (const m of result.relevant_memories) {
4304
+ console.log(` \u2022 ${m.id} ${ui.dim(`(${m.type}, ${m.confidence})`)}`);
4305
+ }
4306
+ console.log();
4307
+ }
4308
+ if (result.stale_anchors.length > 0) {
4309
+ console.log(ui.bold("\u{1F552} Stale anchored memories:"));
4310
+ for (const s of result.stale_anchors) {
4311
+ console.log(` \u2022 ${s.id}`);
4312
+ if (s.body_preview) console.log(` ${ui.dim(s.body_preview)}`);
4313
+ }
4314
+ console.log();
4315
+ }
4316
+ if (result.should_block) {
4317
+ ui.error(`Blocking commit (block_on=${opts.blockOn ?? "high-confidence"}). Address the warnings above or pass --block-on never to bypass.`);
4318
+ process.exit(1);
4319
+ }
4320
+ if (result.warnings.length === 0 && result.stale_anchors.length === 0) {
4321
+ ui.success("No anti-patterns or stale anchors found.");
4322
+ } else {
4323
+ ui.success("Check passed (block_on threshold not met).");
4324
+ }
4325
+ });
4326
+ }
4327
+ function runCommand(cmd, args, cwd) {
4328
+ return new Promise((resolve, reject) => {
4329
+ const proc = spawn3(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
4330
+ let stdout = "";
4331
+ let stderr = "";
4332
+ proc.stdout.on("data", (chunk) => {
4333
+ stdout += chunk.toString();
4334
+ });
4335
+ proc.stderr.on("data", (chunk) => {
4336
+ stderr += chunk.toString();
4337
+ });
4338
+ proc.on("error", reject);
4339
+ proc.on("close", (code) => {
4340
+ if (code === 0) resolve(stdout);
4341
+ else reject(new Error(stderr || `${cmd} exited with code ${code}`));
4342
+ });
4343
+ });
4344
+ }
4345
+
3347
4346
  // src/index.ts
3348
- var program = new Command32();
3349
- program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.4.5");
4347
+ var program = new Command39();
4348
+ program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.6.0");
3350
4349
  registerInit(program);
3351
4350
  registerMcp(program);
3352
4351
  registerBriefing(program);
@@ -3376,10 +4375,17 @@ registerMemoryTried(memory);
3376
4375
  registerMemoryImport(memory);
3377
4376
  registerMemoryImportChangelog(memory);
3378
4377
  registerMemoryDigest(memory);
4378
+ registerMemorySuggest(memory);
4379
+ registerMemoryArchive(memory);
3379
4380
  var session = program.command("session").description("Manage session lifecycle");
3380
4381
  registerSessionEnd(session);
3381
4382
  registerSnapshot(program);
3382
4383
  registerHub(program);
4384
+ registerStats(program);
4385
+ registerBench(program);
4386
+ registerDoctor(program);
4387
+ registerPlayback(program);
4388
+ registerPrecommit(program);
3383
4389
  program.parseAsync(process.argv).catch((err) => {
3384
4390
  if (isZodError(err)) {
3385
4391
  for (const issue of err.issues) {