@hiveai/cli 0.9.2 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command40 } from "commander";
4
+ import { Command as Command44 } from "commander";
5
5
 
6
6
  // src/commands/briefing.ts
7
7
  import { existsSync } from "fs";
@@ -9,6 +9,7 @@ import { readFile } from "fs/promises";
9
9
  import path from "path";
10
10
  import "commander";
11
11
  import {
12
+ extractActionsBriefBody,
12
13
  findProjectRoot,
13
14
  literalMatchesAllTokens,
14
15
  literalMatchesAnyToken,
@@ -17,6 +18,7 @@ import {
17
18
  loadUsageIndex,
18
19
  memoryMatchesAnchorPaths,
19
20
  queryCodeMap,
21
+ resolveBriefingBudget,
20
22
  resolveHaivePaths,
21
23
  tokenizeQuery,
22
24
  trackReads
@@ -195,7 +197,7 @@ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
195
197
  if (!f) continue;
196
198
  counts.set(f, (counts.get(f) ?? 0) + 1);
197
199
  }
198
- let entries = [...counts.entries()].map(([path39, changes]) => ({ path: path39, changes }));
200
+ let entries = [...counts.entries()].map(([path41, changes]) => ({ path: path41, changes }));
199
201
  const lowerPaths = filePaths.map((p) => p.toLowerCase());
200
202
  if (lowerPaths.length > 0) {
201
203
  entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
@@ -284,8 +286,16 @@ var TokenBudgetWriter = class {
284
286
  };
285
287
  function registerBriefing(program2) {
286
288
  program2.command("briefing").description(
287
- 'Print the full project briefing: last session recap + project context + relevant memories.\n Equivalent to calling get_briefing via MCP. Run before starting any task.\n\n Examples:\n haive briefing\n haive briefing --task "add Stripe payment" --files src/payments/PaymentService.ts\n haive briefing --symbols PaymentService,TenantFilter # look up where symbols live\n'
289
+ 'Print the full project briefing: last session recap + project context + relevant memories.\n Equivalent to calling get_briefing via MCP. Run before starting any task.\n\n Examples:\n haive briefing\n haive briefing --task "add Stripe payment" --files src/payments/PaymentService.ts\n haive briefing --budget quick --task "tiny fix"\n'
288
290
  ).option("--task <text>", "what you are about to do \u2014 filters memories by relevance").option("--files <csv>", "comma-separated file paths being worked on (surfaces anchored memories)").option("--symbols <csv>", "symbol names to look up in the code-map (e.g. PaymentService,TenantFilter) \u2014 requires haive index code").option("--max-memories <n>", "cap on memories surfaced", "10").option("--max-tokens <n>", "approximate token budget for the entire briefing (truncates if exceeded)").option("--explain-source", "annotate each memory with [source: <relative-path> \xB7 anchors: <files>] for traceable citations").option("--radar", "force project radar (recent commits, open TODOs, hot files) even when memories are plentiful").option("--no-radar", "disable the project radar even when memories are scarce").option(
291
+ "--budget <preset>",
292
+ "align with MCP get_briefing budget_preset: quick | balanced | deep \u2014 sets cap + truncation budget (overrides --max-memories / replaces default open-ended output)",
293
+ void 0
294
+ ).option(
295
+ "--memory-format <mode>",
296
+ "printed memory bodies: full (default) | actions (cheap bullet-focused excerpt)",
297
+ "full"
298
+ ).option(
289
299
  "--scope <scope>",
290
300
  "personal | team | shared | all (default: all \u2014 includes team + shared cross-repo memories)",
291
301
  "all"
@@ -297,8 +307,24 @@ function registerBriefing(program2) {
297
307
  ).option("-d, --dir <dir>", "project root").action(async (opts) => {
298
308
  const root = findProjectRoot(opts.dir);
299
309
  const paths = resolveHaivePaths(root);
300
- const budgetTokens = opts.maxTokens ? Math.max(100, Number(opts.maxTokens)) : null;
301
- const writer = budgetTokens ? new TokenBudgetWriter(budgetTokens * CHARS_PER_TOKEN) : null;
310
+ let budgetPreset = null;
311
+ if (opts.budget) {
312
+ const b = opts.budget.trim().toLowerCase();
313
+ if (b === "quick" || b === "balanced" || b === "deep") budgetPreset = b;
314
+ else ui.warn(`Unknown --budget '${opts.budget}' \u2014 ignoring (use quick|balanced|deep).`);
315
+ }
316
+ let maxMemories = Math.max(1, Number(opts.maxMemories ?? 10));
317
+ let budgetTokensCap = opts.maxTokens ? Math.max(100, Number(opts.maxTokens)) : null;
318
+ if (budgetPreset !== null) {
319
+ const presetNums = resolveBriefingBudget(budgetPreset, {
320
+ max_tokens: 8e3,
321
+ max_memories: 8,
322
+ include_module_contexts: true
323
+ });
324
+ budgetTokensCap = presetNums.max_tokens;
325
+ maxMemories = presetNums.max_memories;
326
+ }
327
+ const writer = budgetTokensCap !== null ? new TokenBudgetWriter(budgetTokensCap * CHARS_PER_TOKEN) : null;
302
328
  const out = (text) => {
303
329
  if (writer) return writer.write(text);
304
330
  console.log(text);
@@ -352,7 +378,6 @@ function registerBriefing(program2) {
352
378
  const all = ownMemories;
353
379
  const filePaths = parseCsv(opts.files);
354
380
  const tokens = opts.task ? tokenizeQuery(opts.task) : null;
355
- const maxMemories = Math.max(1, Number(opts.maxMemories ?? 10));
356
381
  const scopeFilter = opts.scope ?? "all";
357
382
  const recaps = all.filter(({ memory: mem }) => mem.frontmatter.type === "session_recap").sort(
358
383
  (a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
@@ -454,7 +479,8 @@ function registerBriefing(program2) {
454
479
  if (anchorSymbols.length > 0) parts.push(`symbols: ${anchorSymbols.join(", ")}`);
455
480
  out(ui.dim(` [${parts.join(" \xB7 ")}]`));
456
481
  }
457
- out(item.memory.body.trim());
482
+ const memBody = opts.memoryFormat?.toLowerCase() === "actions" ? extractActionsBriefBody(item.memory.body) : item.memory.body.trim();
483
+ out(memBody);
458
484
  out("");
459
485
  }
460
486
  if (!stopped()) out(ui.dim(`${top.length} memor${top.length === 1 ? "y" : "ies"} surfaced`));
@@ -1856,6 +1882,7 @@ var PROJECT_CONTEXT_TEMPLATE = `# Project context
1856
1882
 
1857
1883
  > Generated by \`haive init\`. Run \`haive init --bootstrap\` to auto-fill from your codebase,
1858
1884
  > or invoke the MCP prompt \`bootstrap_project\` in your AI client for a richer AI-generated version.
1885
+ > **AI agents**: use the **haive** MCP (\`get_briefing\` / \`mem_relevant_to\`) before substantive edits \u2014 see bridge files and \`.cursor/rules/haive-mcp-required.mdc\`.
1859
1886
 
1860
1887
  ## Architecture
1861
1888
  TODO \u2014 high-level overview of the codebase.
@@ -1880,16 +1907,47 @@ Memories live under \`.ai/memories/\` (personal/team/module).
1880
1907
 
1881
1908
  ## hAIve \u2014 mandatory rules for AI agents
1882
1909
 
1910
+ Installing or initializing hAIve in this repository means **the team expects AI agents to drive work through hAIve**, not bypass it.
1911
+
1912
+ ### MCP first (default \u2014 never optional unless the developer says so)
1913
+ The **haive** MCP server must be enabled in your client. Until it is unavailable (misconfiguration report to the developer), you **must** use it for substantive work:
1914
+
1915
+ - **Before** you edit project source files, configs (\`package.json\`, tooling), or workflows for the current goal, invoke \`get_briefing\` with a short **task** string (and **files** / **symbols** when you know them). If project context was already refreshed in this session and you only need ranked memories, you may call \`mem_relevant_to\` instead\u2014but **still** consult hAIve before trusting ungrounded guesses.
1916
+ - **Before** declaring a coding task finished, invoke the MCP prompt \`post_task\` when the session produced learnings or substantive changes worth capturing.
1917
+
1918
+ Do **not** treat these as nice-to-have: they are **default-required** for coding agents here.
1919
+
1883
1920
  ### Session rules
1884
- - **Before** marking a task as done ("Done", "Finished", "C'est fait"), call the MCP prompt \`post_task\`.
1885
1921
  - **Immediately** when an approach fails (wrong API, wrong pattern, wrong assumption), call \`mem_tried\` \u2014 do not wait until the end of the session.
1886
- - When starting a new task, call \`get_briefing\` with the task description to load relevant memories and avoid repeating past mistakes.
1887
1922
 
1888
1923
  ### Safety rules \u2014 NEVER violate these
1889
1924
  - If \`get_briefing\` returns an \`action_required\` list, **stop and show each item to the developer** before doing anything. Use the exact \`developer_message\` provided. Wait for explicit confirmation.
1890
1925
  - **Never modify code autonomously** because of a breaking change detected in another project (dependency version bump, API contract change, removed field). Always ask first.
1891
1926
  - When in doubt about a cross-repo change: ask, don't act.
1892
1927
  `;
1928
+ var CURSOR_HAIVE_RULE_MDC = `---
1929
+ description: Require hAIve MCP (get_briefing / mem_relevant_to) before substantive repo edits
1930
+ alwaysApply: true
1931
+ ---
1932
+
1933
+ # hAIve \u2014 required for agents
1934
+
1935
+ This repository uses **hAIve**. Running \`haive init\` means the team expects agents to **use the haive MCP server by default**, not skip it.
1936
+
1937
+ ## Before you change the repo
1938
+
1939
+ 1. Call **\`get_briefing\`** with \`task\` (and \`files\` / \`symbols\` when known), **or** \`mem_relevant_to\` if full project context is already loaded and you only need ranked memories.
1940
+ 2. Then read/search the codebase as needed. Do not invert this order for non-trivial work.
1941
+
1942
+ ## During / after the task
1943
+
1944
+ - On failure: **\`mem_tried\`** immediately.
1945
+ - Before closing a substantive session: MCP prompt **\`post_task\`** when there is something worth capturing.
1946
+
1947
+ ## If haive MCP is missing
1948
+
1949
+ Tell the developer to enable the **haive** server (e.g. \`haive mcp --stdio\` in client config) and restart the client. Do not silently ignore hAIve.
1950
+ `;
1893
1951
  var CI_WORKFLOW = `name: haive-sync
1894
1952
 
1895
1953
  on:
@@ -2010,7 +2068,7 @@ jobs:
2010
2068
  function registerInit(program2) {
2011
2069
  program2.command("init").description(
2012
2070
  "Initialize a hAIve project \u2014 autopilot mode ON by default (zero human intervention).\n Auto-bootstraps project-context.md from local files and seeds detected stack packs.\n Add --manual to control memory approval and session recaps yourself.\n Add --no-bootstrap and --stack none to disable the auto-features."
2013
- ).option("-d, --dir <dir>", "project root", process.cwd()).option("--no-bridges", "do not generate CLAUDE.md / .cursorrules / copilot-instructions.md").option("--with-ci", "write a GitHub Actions workflow (.github/workflows/haive-sync.yml) \u2014 included automatically in autopilot mode").option(
2071
+ ).option("-d, --dir <dir>", "project root", process.cwd()).option("--no-bridges", "do not generate CLAUDE.md / .cursorrules / copilot-instructions.md / .cursor/rules/haive-mcp-required.mdc").option("--with-ci", "write a GitHub Actions workflow (.github/workflows/haive-sync.yml) \u2014 included automatically in autopilot mode").option(
2014
2072
  "--manual",
2015
2073
  "opt out of autopilot: memories require manual approval, no auto-session recap, no auto-context"
2016
2074
  ).option(
@@ -2040,6 +2098,7 @@ function registerInit(program2) {
2040
2098
  await mkdir3(paths.teamDir, { recursive: true });
2041
2099
  await mkdir3(paths.moduleDir, { recursive: true });
2042
2100
  await mkdir3(paths.modulesContextDir, { recursive: true });
2101
+ await ensureAiRuntimeLayout(paths.runtimeDir);
2043
2102
  if (!existsSync6(paths.projectContext)) {
2044
2103
  if (wantBootstrap) {
2045
2104
  ui.info("Bootstrapping project context from local files\u2026");
@@ -2069,6 +2128,7 @@ function registerInit(program2) {
2069
2128
  await writeBridge(root, "CLAUDE.md");
2070
2129
  await writeBridge(root, ".cursorrules");
2071
2130
  await writeBridge(root, path7.join(".github", "copilot-instructions.md"));
2131
+ await writeCursorHaiveRule(root);
2072
2132
  }
2073
2133
  const stacksToSeed = await resolveStacksToSeed(root, wantStack);
2074
2134
  if (stacksToSeed.length > 0) {
@@ -2211,6 +2271,17 @@ async function resolveStacksToSeed(root, stackOpt) {
2211
2271
  }
2212
2272
  return stackOpt.split(",").map((s) => s.trim().toLowerCase()).filter(isValidStack);
2213
2273
  }
2274
+ async function writeCursorHaiveRule(root) {
2275
+ const relPath = ".cursor/rules/haive-mcp-required.mdc";
2276
+ const target = path7.join(root, relPath);
2277
+ if (existsSync6(target)) {
2278
+ ui.info(`Cursor rule ${relPath} already exists \u2014 skipped`);
2279
+ return;
2280
+ }
2281
+ await mkdir3(path7.dirname(target), { recursive: true });
2282
+ await writeFile3(target, CURSOR_HAIVE_RULE_MDC, "utf8");
2283
+ ui.success(`Created Cursor rule ${relPath}`);
2284
+ }
2214
2285
  async function writeBridge(root, relPath) {
2215
2286
  const target = path7.join(root, relPath);
2216
2287
  if (existsSync6(target)) {
@@ -2221,6 +2292,27 @@ async function writeBridge(root, relPath) {
2221
2292
  await writeFile3(target, BRIDGE_BODY, "utf8");
2222
2293
  ui.success(`Created bridge ${relPath}`);
2223
2294
  }
2295
+ var RUNTIME_README_BODY = `# .ai/.runtime \u2014 disposable local layer
2296
+
2297
+ Not team truth. Use for machine-local session notes or tooling scratch files.
2298
+ Official memories belong in .ai/memories/ (versioned in Git).
2299
+ Only .gitignore and this README are meant to commit; everything else stays untracked.
2300
+ `;
2301
+ var RUNTIME_GITIGNORE_BODY = `*
2302
+ !.gitignore
2303
+ !README.md
2304
+ `;
2305
+ async function ensureAiRuntimeLayout(runtimeDir) {
2306
+ await mkdir3(runtimeDir, { recursive: true });
2307
+ const gi = path7.join(runtimeDir, ".gitignore");
2308
+ if (!existsSync6(gi)) {
2309
+ await writeFile3(gi, RUNTIME_GITIGNORE_BODY, "utf8");
2310
+ }
2311
+ const readme = path7.join(runtimeDir, "README.md");
2312
+ if (!existsSync6(readme)) {
2313
+ await writeFile3(readme, RUNTIME_README_BODY, "utf8");
2314
+ }
2315
+ }
2224
2316
  async function ensureGitignoreEntries(root, patterns) {
2225
2317
  try {
2226
2318
  const gitignorePath = path7.join(root, ".gitignore");
@@ -2605,6 +2697,7 @@ import {
2605
2697
  loadMemoriesFromDir as loadMemoriesFromDir3,
2606
2698
  loadUsageIndex as loadUsageIndex2,
2607
2699
  pickSnippetNeedle,
2700
+ rankMemoriesLexical,
2608
2701
  tokenizeQuery as tokenizeQuery2,
2609
2702
  trackReads as trackReads2
2610
2703
  } from "@hiveai/core";
@@ -2715,6 +2808,7 @@ import {
2715
2808
  DEFAULT_AUTO_PROMOTE_RULE,
2716
2809
  deriveConfidence as deriveConfidence4,
2717
2810
  estimateTokens,
2811
+ extractActionsBriefBody as extractActionsBriefBody2,
2718
2812
  getUsage as getUsage5,
2719
2813
  inferModulesFromPaths as inferModulesFromPaths2,
2720
2814
  isAutoPromoteEligible,
@@ -2727,6 +2821,7 @@ import {
2727
2821
  loadUsageIndex as loadUsageIndex7,
2728
2822
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths22,
2729
2823
  queryCodeMap as queryCodeMap2,
2824
+ resolveBriefingBudget as resolveBriefingBudget2,
2730
2825
  serializeMemory as serializeMemory9,
2731
2826
  tokenizeQuery as tokenizeQuery22,
2732
2827
  trackReads as trackReads3,
@@ -2804,9 +2899,19 @@ import {
2804
2899
  serializeMemory as serializeMemory10
2805
2900
  } from "@hiveai/core";
2806
2901
  import { z as z29 } from "zod";
2902
+ import { existsSync as existsSync27 } from "fs";
2903
+ import { findLexicalConflictPairs, loadMemoriesFromDir as loadMemoriesFromDir21 } from "@hiveai/core";
2807
2904
  import { z as z30 } from "zod";
2905
+ import { resolveProjectInfo } from "@hiveai/core";
2808
2906
  import { z as z31 } from "zod";
2907
+ import { MemoryTypeSchema, suggestTopicKey } from "@hiveai/core";
2809
2908
  import { z as z32 } from "zod";
2909
+ import { existsSync as existsSync28 } from "fs";
2910
+ import { collectTimelineEntries, loadMemoriesFromDir as loadMemoriesFromDir222 } from "@hiveai/core";
2911
+ import { z as z33 } from "zod";
2912
+ import { z as z34 } from "zod";
2913
+ import { z as z35 } from "zod";
2914
+ import { z as z36 } from "zod";
2810
2915
  function createContext(options = {}) {
2811
2916
  const env = options.env ?? process.env;
2812
2917
  const cwd = options.cwd ?? process.cwd();
@@ -2927,6 +3032,30 @@ var MemSaveInputSchema = {
2927
3032
  function bodyHash(body) {
2928
3033
  return createHash("sha256").update(body.trim()).digest("hex").slice(0, 12);
2929
3034
  }
3035
+ var WORD_RE = /\b[a-z0-9]{3,}\b/gi;
3036
+ function bodyTokenSet(body) {
3037
+ const raw = body.toLowerCase().match(WORD_RE) ?? [];
3038
+ return new Set(raw);
3039
+ }
3040
+ function maxBodySimilarity(incomingTokens, memories, scope, type, excludeIds) {
3041
+ if (incomingTokens.size < 6) return null;
3042
+ let best = null;
3043
+ const skip = excludeIds ?? /* @__PURE__ */ new Set();
3044
+ for (const { memory: memory2 } of memories) {
3045
+ const fm = memory2.frontmatter;
3046
+ if (skip.has(fm.id)) continue;
3047
+ if (fm.scope !== scope || fm.type !== type) continue;
3048
+ if (fm.status === "rejected" || fm.status === "deprecated") continue;
3049
+ const other = bodyTokenSet(memory2.body);
3050
+ if (other.size === 0) continue;
3051
+ let inter = 0;
3052
+ for (const t of incomingTokens) if (other.has(t)) inter++;
3053
+ const uni = incomingTokens.size + other.size - inter;
3054
+ const j = uni === 0 ? 0 : inter / uni;
3055
+ if (j >= 0.72 && (!best || j > best.score)) best = { score: j, id: fm.id };
3056
+ }
3057
+ return best;
3058
+ }
2930
3059
  async function memSave(input, ctx) {
2931
3060
  if (!existsSync42(ctx.paths.haiveDir)) {
2932
3061
  throw new Error(
@@ -2948,12 +3077,26 @@ async function memSave(input, ctx) {
2948
3077
  `Duplicate content detected \u2014 identical body already saved as "${hashDuplicate.memory.frontmatter.id}". Use mem_update to modify it, or change the body to add new information.`
2949
3078
  );
2950
3079
  }
3080
+ const incomingTokens = bodyTokenSet(input.body);
3081
+ function bodySimilarWarnings(excludeIds) {
3082
+ const dup = maxBodySimilarity(incomingTokens, existing, resolvedScope, input.type, excludeIds);
3083
+ if (!dup?.id) return {};
3084
+ const body_similar = {
3085
+ id: dup.id,
3086
+ score: Math.round(dup.score * 100) / 100
3087
+ };
3088
+ return {
3089
+ similarityWarning: `Body is ~${Math.round(dup.score * 100)}% similar (token overlap) to existing "${dup.id}" \u2014 consolidate if redundant.`,
3090
+ body_similar
3091
+ };
3092
+ }
2951
3093
  if (input.topic) {
2952
3094
  const topicMatch = existing.find(
2953
3095
  ({ memory: memory2 }) => memory2.frontmatter.topic === input.topic && memory2.frontmatter.scope === resolvedScope && (!input.module || memory2.frontmatter.module === input.module)
2954
3096
  );
2955
3097
  if (topicMatch) {
2956
3098
  const fm = topicMatch.memory.frontmatter;
3099
+ const { similarityWarning: simW, body_similar: bs } = bodySimilarWarnings(/* @__PURE__ */ new Set([fm.id]));
2957
3100
  const newFrontmatter = {
2958
3101
  ...fm,
2959
3102
  body: input.body,
@@ -2970,13 +3113,19 @@ async function memSave(input, ctx) {
2970
3113
  serializeMemory2({ frontmatter: newFrontmatter, body: input.body }),
2971
3114
  "utf8"
2972
3115
  );
3116
+ const mergedTw = [
3117
+ invalidPaths.length > 0 ? `Anchor path(s) not found in project: ${invalidPaths.join(", ")}. They will be marked stale by haive sync.` : null,
3118
+ simW ?? null
3119
+ ].filter(Boolean).join(" \u2014 ") || void 0;
2973
3120
  return {
2974
3121
  id: fm.id,
2975
3122
  scope: fm.scope,
2976
3123
  file_path: topicMatch.filePath,
2977
3124
  action: "updated",
2978
3125
  revision_count: newFrontmatter.revision_count,
2979
- ...invalidPaths.length > 0 ? { invalid_paths: invalidPaths, warning: `Anchor path(s) not found in project: ${invalidPaths.join(", ")}. They will be marked stale by haive sync.` } : {}
3126
+ ...mergedTw ? { warning: mergedTw } : {},
3127
+ ...bs ? { body_similar: bs } : {},
3128
+ ...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {}
2980
3129
  };
2981
3130
  }
2982
3131
  }
@@ -3018,9 +3167,11 @@ async function memSave(input, ctx) {
3018
3167
  }
3019
3168
  }
3020
3169
  await writeFile22(file, serializeMemory2({ frontmatter, body: input.body }), "utf8");
3170
+ const { similarityWarning: simWarnNew, body_similar: bsNew } = bodySimilarWarnings();
3021
3171
  const finalWarning = [
3022
3172
  invalidPaths.length > 0 ? `Anchor path(s) not found in project: ${invalidPaths.join(", ")}. They will be marked stale by \`haive sync\`.` : null,
3023
- warning ?? null
3173
+ warning ?? null,
3174
+ simWarnNew ?? null
3024
3175
  ].filter(Boolean).join(" \u2014 ") || void 0;
3025
3176
  return {
3026
3177
  id: frontmatter.id,
@@ -3029,6 +3180,7 @@ async function memSave(input, ctx) {
3029
3180
  action: "created",
3030
3181
  ...finalWarning ? { warning: finalWarning } : {},
3031
3182
  ...similar_found ? { similar_found } : {},
3183
+ ...bsNew ? { body_similar: bsNew } : {},
3032
3184
  ...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {}
3033
3185
  };
3034
3186
  }
@@ -3044,6 +3196,9 @@ var MemSearchInputSchema = {
3044
3196
  semantic: z5.boolean().default(false).describe(
3045
3197
  "Use semantic similarity from the embeddings index (requires `haive embeddings index`)."
3046
3198
  ),
3199
+ lexical_rank: z5.boolean().default(false).describe(
3200
+ "When true (and semantic is false), rank the filtered corpus with Okapi-BM25-style lexical scoring instead of literal AND/OR. Helps phrase-like queries without embeddings."
3201
+ ),
3047
3202
  min_score: z5.number().min(0).max(1).default(0).describe("Minimum cosine similarity (semantic mode only)"),
3048
3203
  track: z5.boolean().default(true).describe("Increment read_count on returned memories (used for passive validation)")
3049
3204
  };
@@ -3069,6 +3224,27 @@ async function memSearch(input, ctx) {
3069
3224
  notice: "Semantic search unavailable (embeddings index missing or @hiveai/embeddings not installed). Falling back to literal search."
3070
3225
  };
3071
3226
  }
3227
+ } else if (input.lexical_rank && input.query.trim()) {
3228
+ const { ranked, scores } = rankMemoriesLexical(
3229
+ filtered,
3230
+ input.query,
3231
+ input.limit
3232
+ );
3233
+ if (ranked.length > 0) {
3234
+ const snippetNeedle = pickSnippetNeedle(input.query);
3235
+ result = {
3236
+ matches: ranked.map(
3237
+ (loaded, i) => lexicalHit(loaded, snippetNeedle, usage, scores[i])
3238
+ ),
3239
+ total: ranked.length,
3240
+ mode: "lexical_ranked"
3241
+ };
3242
+ } else {
3243
+ result = {
3244
+ ...buildLiteralResult(input, filtered, usage),
3245
+ notice: "Lexical ranking found no BM25-positive hits \u2014 showing literal matches instead."
3246
+ };
3247
+ }
3072
3248
  } else {
3073
3249
  result = buildLiteralResult(input, filtered, usage);
3074
3250
  }
@@ -3166,6 +3342,9 @@ function toHit(loaded, needle, usage) {
3166
3342
  file_path: loaded.filePath
3167
3343
  };
3168
3344
  }
3345
+ function lexicalHit(loaded, needle, usage, lexicalScore) {
3346
+ return { ...toHit(loaded, needle, usage), score: lexicalScore };
3347
+ }
3169
3348
  var MemVerifyInputSchema = {
3170
3349
  id: z6.string().optional().describe("If set, verify only this memory id"),
3171
3350
  update: z6.boolean().default(false).describe("Write the resulting status back to disk (status=stale or validated)")
@@ -3930,17 +4109,29 @@ var GetBriefingInputSchema = {
3930
4109
  ),
3931
4110
  include_stale: z17.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
3932
4111
  track: z17.boolean().default(true).describe("Increment read_count on returned memories"),
3933
- format: z17.enum(["full", "compact"]).default("full").describe(
3934
- "Output format: 'full' returns complete memory bodies; 'compact' returns id + 1-line summary only (call mem_get for details)."
4112
+ format: z17.enum(["full", "compact", "actions"]).default("full").describe(
4113
+ "Output format: 'full' returns memory bodies (honors token budget via truncation); 'compact' returns a 1-line summary per memory (call mem_get for detail); 'actions' squeezes bodies to actionable bullet lines \u2014 fewer tokens vs full."
3935
4114
  ),
3936
4115
  symbols: z17.array(z17.string()).default([]).describe(
3937
4116
  "Symbol names to look up in the code-map (e.g. ['PaymentService', 'TenantFilter']). Returns the file(s) exporting each symbol so agents don't need to grep. Requires `haive index code` to have been run."
3938
4117
  ),
3939
4118
  min_semantic_score: z17.number().min(0).max(1).default(0).describe(
3940
4119
  "Drop semantic-only memory hits whose cosine score is below this threshold. Useful to avoid weakly-related noise when the task is short or the corpus is broad. Has no effect on memories matched via anchor/module/literal \u2014 those are always kept. Try 0.25\u20130.4 for stricter matching."
4120
+ ),
4121
+ budget_preset: z17.enum(["quick", "balanced", "deep"]).optional().describe(
4122
+ "Shortcut token budget: 'quick' minimizes tokens/skip module CONTEXT slices; 'balanced' mirrors historical defaults; 'deep' uses a larger briefing. When set, overrides max_tokens, max_memories, and include_module_contexts."
3941
4123
  )
3942
4124
  };
4125
+ var GetBriefingZod = z17.object(GetBriefingInputSchema);
3943
4126
  async function getBriefing(input, ctx) {
4127
+ const resolvedBudget = resolveBriefingBudget2(input.budget_preset, {
4128
+ max_tokens: input.max_tokens,
4129
+ max_memories: input.max_memories,
4130
+ include_module_contexts: input.include_module_contexts
4131
+ });
4132
+ const briefingMaxTokens = resolvedBudget.max_tokens;
4133
+ const briefingMaxMemories = resolvedBudget.max_memories;
4134
+ const briefingIncludeModules = resolvedBudget.include_module_contexts;
3944
4135
  const inferred = inferModulesFromPaths2(input.files);
3945
4136
  const memories = [];
3946
4137
  let searchMode = "literal";
@@ -4052,8 +4243,8 @@ async function getBriefing(input, ctx) {
4052
4243
  const sb = reasonScore(b) + confidenceScore(b) + (b.semantic_score ?? 0);
4053
4244
  return sb - sa;
4054
4245
  });
4055
- for (const mem of ranked.slice(0, input.max_memories)) {
4056
- if (seen.size >= input.max_memories * 2) break;
4246
+ for (const mem of ranked.slice(0, briefingMaxMemories)) {
4247
+ if (seen.size >= briefingMaxMemories * 2) break;
4057
4248
  const loaded = byId.get(mem.id);
4058
4249
  if (!loaded) continue;
4059
4250
  for (const relId of loaded.memory.frontmatter.related_ids ?? []) {
@@ -4062,7 +4253,7 @@ async function getBriefing(input, ctx) {
4062
4253
  if (related) addOrUpdate(related, "anchor", void 0, "partial");
4063
4254
  }
4064
4255
  }
4065
- memories.push(...ranked.slice(0, input.max_memories));
4256
+ memories.push(...ranked.slice(0, briefingMaxMemories));
4066
4257
  if (input.track && memories.length > 0) {
4067
4258
  await trackReads3(ctx.paths, memories.map((m) => m.id));
4068
4259
  const freshUsage = await loadUsageIndex7(ctx.paths);
@@ -4142,7 +4333,7 @@ async function getBriefing(input, ctx) {
4142
4333
  }
4143
4334
  }
4144
4335
  }
4145
- const moduleContents = input.include_module_contexts ? await loadModuleContexts2(ctx, inferred) : [];
4336
+ const moduleContents = briefingIncludeModules ? await loadModuleContexts2(ctx, inferred) : [];
4146
4337
  const memoriesText = memories.map((m) => {
4147
4338
  const unverified = m.status === "proposed" ? " [UNVERIFIED \u2014 not yet validated]" : "";
4148
4339
  return `### ${m.id} (${m.scope}/${m.type}, ${m.confidence})${unverified}
@@ -4160,7 +4351,7 @@ ${m.content}`).join("\n\n---\n\n"),
4160
4351
  },
4161
4352
  { key: "memories", text: memoriesText, weight: 4, mode: "head" }
4162
4353
  ],
4163
- input.max_tokens
4354
+ briefingMaxTokens
4164
4355
  );
4165
4356
  const projectSlice = slices.find((s) => s.key === "project");
4166
4357
  const modulesSlice = slices.find((s) => s.key === "modules");
@@ -4202,7 +4393,10 @@ ${m.content}`).join("\n\n---\n\n"),
4202
4393
  const createdAt = loaded?.memory.frontmatter.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
4203
4394
  if (isDecaying(u, createdAt)) decayWarnings.push(m.id);
4204
4395
  }
4205
- const outputMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : trimmedMemories;
4396
+ const outputMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : input.format === "actions" ? trimmedMemories.map((m) => ({
4397
+ ...m,
4398
+ body: extractActionsBriefBody2(m.body)
4399
+ })) : trimmedMemories;
4206
4400
  let symbolLocations;
4207
4401
  const symbolsToLookup = new Set(input.symbols);
4208
4402
  for (const m of outputMemories) {
@@ -4324,6 +4518,11 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
4324
4518
  "After completing the task: capture new gotchas with mem_observe, failed approaches with mem_tried, validated patterns with mem_save."
4325
4519
  );
4326
4520
  }
4521
+ if (outputMemories.length > 2 && !input.budget_preset && input.task && !hints.some((h) => h.includes("budget_preset"))) {
4522
+ hints.push(
4523
+ "For tighter token budgets on small tasks pass budget_preset:'quick'; for refactor-sized work use budget_preset:'deep'."
4524
+ );
4525
+ }
4327
4526
  }
4328
4527
  return {
4329
4528
  ...input.task ? { task: input.task } : {},
@@ -4346,7 +4545,8 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
4346
4545
  ...hints.length > 0 ? { hints } : {},
4347
4546
  estimated_tokens: totalTokens,
4348
4547
  budget: {
4349
- max_tokens: input.max_tokens,
4548
+ max_tokens: briefingMaxTokens,
4549
+ ...input.budget_preset ? { preset_applied: input.budget_preset } : {},
4350
4550
  spent: {
4351
4551
  project: projectSlice.estimatedTokens,
4352
4552
  modules: modulesSlice.estimatedTokens,
@@ -4542,7 +4742,7 @@ var MemRelevantToInputSchema = {
4542
4742
  files: z21.array(z21.string()).default([]).describe("Optional: files you are about to edit \u2014 surfaces anchored memories."),
4543
4743
  limit: z21.number().int().positive().max(30).default(8).describe("Cap on returned memories."),
4544
4744
  min_semantic_score: z21.number().min(0).max(1).default(0.25).describe("Drop weakly-related semantic hits below this cosine threshold."),
4545
- format: z21.enum(["full", "compact"]).default("full").describe("'compact' = id + 1-line summary; 'full' = complete bodies.")
4745
+ format: z21.enum(["full", "compact", "actions"]).default("full").describe("'compact' = id + 1-line summary; 'full' = complete bodies; 'actions' = bullet-first excerpts.")
4546
4746
  };
4547
4747
  async function memRelevantTo(input, ctx) {
4548
4748
  const briefingInput = {
@@ -5476,11 +5676,76 @@ function gitFileDiff(root, file, sinceDays) {
5476
5676
  return null;
5477
5677
  }
5478
5678
  }
5679
+ var MemConflictCandidatesInputSchema = {
5680
+ since_days: z30.number().int().positive().max(3650).default(365).describe("Only memories created since N days ago"),
5681
+ types: z30.array(z30.enum(["decision", "architecture", "convention", "gotcha"])).default(["decision", "architecture"]).describe("Memory types scanned for pairwise lexical overlap"),
5682
+ min_jaccard: z30.number().min(0).max(1).default(0.45).describe("Minimum Jaccard token similarity to surface as a candidate pair"),
5683
+ max_pairs: z30.number().int().positive().max(100).default(20).describe("Cap pairs returned"),
5684
+ max_scan: z30.number().int().positive().max(2e3).default(500).describe("Maximum memories sampled for O(n\xB2) scan \u2014 excess dropped after chronological sort.")
5685
+ };
5686
+ async function memConflictCandidates(input, ctx) {
5687
+ if (!existsSync27(ctx.paths.memoriesDir)) {
5688
+ return {
5689
+ pairs: [],
5690
+ scanned: 0,
5691
+ truncated: false,
5692
+ notice: "No .ai/memories directory."
5693
+ };
5694
+ }
5695
+ const all = await loadMemoriesFromDir21(ctx.paths.memoriesDir);
5696
+ const { pairs, scanned, truncated } = findLexicalConflictPairs(all, {
5697
+ sinceDays: input.since_days,
5698
+ types: input.types,
5699
+ minJaccard: input.min_jaccard,
5700
+ maxPairs: input.max_pairs,
5701
+ maxScan: input.max_scan
5702
+ });
5703
+ const notice = pairs.length === 0 ? "No lexical candidate pairs \u2265 threshold \u2014 try lowering min_jaccard or widen since_days/types." : void 0;
5704
+ return { pairs, scanned, truncated, notice };
5705
+ }
5706
+ var MemResolveProjectInputSchema = {
5707
+ cwd: z31.string().optional().describe("Directory used for root discovery when HAIVE_PROJECT_ROOT is unset.")
5708
+ };
5709
+ async function memResolveProject(input, _ctx) {
5710
+ void _ctx;
5711
+ return {
5712
+ ok: true,
5713
+ info: resolveProjectInfo({
5714
+ cwd: input.cwd
5715
+ })
5716
+ };
5717
+ }
5718
+ var MemSuggestTopicInputSchema = {
5719
+ type: MemoryTypeSchema.describe("Memory kind \u2014 drives the suggested topic family."),
5720
+ title: z32.string().min(1).describe("Short title or phrase (headers, headings) \u2014 turned into slug")
5721
+ };
5722
+ async function memSuggestTopic(input, _ctx) {
5723
+ void _ctx;
5724
+ const suggestion = suggestTopicKey(input.type, input.title);
5725
+ return { topic_key: suggestion.topic_key, family: suggestion.family, type: input.type };
5726
+ }
5727
+ var MemTimelineInputSchema = {
5728
+ memory_id: z33.string().optional().describe("Seed id \u2014 expands via related_ids, topic, anchors"),
5729
+ topic: z33.string().optional().describe("Frontmatter.topic value \u2014 chronological list when memory_id omitted"),
5730
+ limit: z33.number().int().positive().max(100).default(30).describe("Max timeline entries returned")
5731
+ };
5732
+ async function memTimeline(input, ctx) {
5733
+ if (!existsSync28(ctx.paths.memoriesDir)) {
5734
+ return { entries: [], total: 0, notice: "No .ai/memories directory." };
5735
+ }
5736
+ const all = await loadMemoriesFromDir222(ctx.paths.memoriesDir);
5737
+ const { entries, notice } = collectTimelineEntries(all, {
5738
+ memoryId: input.memory_id,
5739
+ topic: input.topic,
5740
+ limit: input.limit
5741
+ });
5742
+ return { entries, total: entries.length, notice };
5743
+ }
5479
5744
  var BootstrapProjectArgsSchema = {
5480
- module: z30.string().optional().describe(
5745
+ module: z34.string().optional().describe(
5481
5746
  "Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
5482
5747
  ),
5483
- focus: z30.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
5748
+ focus: z34.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
5484
5749
  };
5485
5750
  var ROOT_TEMPLATE = `# Project context
5486
5751
 
@@ -5561,8 +5826,8 @@ ${template}\`\`\`
5561
5826
  };
5562
5827
  }
5563
5828
  var PostTaskArgsSchema = {
5564
- task_summary: z31.string().optional().describe("One sentence describing what you just did"),
5565
- files_touched: z31.array(z31.string()).optional().describe("Files you created or modified during the task")
5829
+ task_summary: z35.string().optional().describe("One sentence describing what you just did"),
5830
+ files_touched: z35.array(z35.string()).optional().describe("Files you created or modified during the task")
5566
5831
  };
5567
5832
  function postTaskPrompt(args, ctx) {
5568
5833
  const taskLine = args.task_summary ? `
@@ -5645,10 +5910,10 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
5645
5910
  };
5646
5911
  }
5647
5912
  var ImportDocsArgsSchema = {
5648
- content: z32.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
5649
- source: z32.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
5650
- scope: z32.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
5651
- dry_run: z32.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
5913
+ content: z36.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
5914
+ source: z36.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
5915
+ scope: z36.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
5916
+ dry_run: z36.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
5652
5917
  };
5653
5918
  function importDocsPrompt(args, ctx) {
5654
5919
  const sourceLine = args.source ? `
@@ -5711,7 +5976,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
5711
5976
  };
5712
5977
  }
5713
5978
  var SERVER_NAME = "haive";
5714
- var SERVER_VERSION = "0.9.2";
5979
+ var SERVER_VERSION = "0.9.5";
5715
5980
  function jsonResult(data) {
5716
5981
  return {
5717
5982
  content: [
@@ -5762,6 +6027,23 @@ function createHaiveServer(options = {}) {
5762
6027
  return jsonResult(await memSave(input, context));
5763
6028
  }
5764
6029
  );
6030
+ server.tool(
6031
+ "mem_suggest_topic",
6032
+ [
6033
+ "Propose a stable `topic` key (topic-upsert) from type + short title.",
6034
+ "",
6035
+ "USE BEFORE mem_save when you want deterministic updates to the same memory over time;",
6036
+ "families mimic Engram-style grouping (architecture/*, bug/*, decision/*, \u2026).",
6037
+ "",
6038
+ "PARAMETERS:",
6039
+ " type \u2014 convention | decision | gotcha | architecture | glossary | attempt | session_recap",
6040
+ " title \u2014 phrase to slugify under the suggested family prefix",
6041
+ "",
6042
+ "RETURNS: { topic_key, family, type }"
6043
+ ].join("\n"),
6044
+ MemSuggestTopicInputSchema,
6045
+ async (input) => jsonResult(await memSuggestTopic(input, context))
6046
+ );
5765
6047
  server.tool(
5766
6048
  "mem_tried",
5767
6049
  [
@@ -5853,8 +6135,12 @@ function createHaiveServer(options = {}) {
5853
6135
  server.tool(
5854
6136
  "get_briefing",
5855
6137
  [
5856
- "\u2B50 CALL THIS FIRST at the start of every task. One-shot onboarding that returns",
5857
- "everything relevant in a single call under a token budget.",
6138
+ "\u2B50 DEFAULT-FIRST for coding agents on any repo where `haive init` ran: call this BEFORE",
6139
+ "changing source or project config for the current goal (unless the developer explicitly opts out).",
6140
+ "One-shot onboarding: everything relevant in a single call under a token budget.",
6141
+ "",
6142
+ "PROGRESSIVE DISCLOSURE \u2014 after this, drill down only if needed:",
6143
+ " mem_relevant_to / mem_search (compact lists) \u2192 mem_get (full body + anchors).",
5858
6144
  "",
5859
6145
  "RETURNS (in order of priority):",
5860
6146
  " 0. action_required \u2014 \u26A0\uFE0F HANDLE THIS FIRST if non-empty (see protocol below)",
@@ -5877,7 +6163,8 @@ function createHaiveServer(options = {}) {
5877
6163
  " task \u2014 what you are about to do (1\u20132 sentences) \u2014 ALWAYS provide this",
5878
6164
  " files \u2014 files you are about to edit \u2014 surfaces anchored memories",
5879
6165
  " symbols \u2014 symbol names to look up in the code-map (e.g. ['PaymentService'])",
5880
- " format \u2014 'full' (default) | 'compact' (1-line summaries, use when token budget is tight)",
6166
+ " format \u2014 'full' (default) | 'compact' (1-line) | 'actions' (bullet-first excerpts)",
6167
+ " budget_preset \u2014 'quick' | 'balanced' | 'deep' \u2014 scales max_tokens/memories/module contexts",
5881
6168
  "",
5882
6169
  "EXAMPLE USAGE:",
5883
6170
  " get_briefing({ task: 'add a Stripe payment integration', files: ['src/payments/'], symbols: ['PaymentService'] })",
@@ -5888,7 +6175,7 @@ function createHaiveServer(options = {}) {
5888
6175
  " low \u2014 proposed, few reads (take with caution)",
5889
6176
  " unverified \u2014 draft (unverified: true flag set)",
5890
6177
  "",
5891
- "Replaces 4\u20135 separate tool calls. Always call this before any other tool."
6178
+ "Replaces 4\u20135 separate tool calls. Prefer this first; use mem_search / mem_get only for follow-up."
5892
6179
  ].join("\n"),
5893
6180
  GetBriefingInputSchema,
5894
6181
  async (input) => {
@@ -5907,6 +6194,8 @@ function createHaiveServer(options = {}) {
5907
6194
  "SEARCH MODES:",
5908
6195
  " Literal (default): AND search across id, tags, and body \u2014 all tokens must match.",
5909
6196
  " Falls back to OR automatically if no AND results (partial match).",
6197
+ " Lexical rank (lexical_rank: true, semantic: false): Okapi-BM25-style scoring on the",
6198
+ " filtered corpus \u2014 good for phrase-like queries without embeddings.",
5910
6199
  " Semantic (semantic: true): embedding-based similarity \u2014 finds related memories",
5911
6200
  " even with different wording. Requires haive embeddings index to be built.",
5912
6201
  "",
@@ -5915,6 +6204,7 @@ function createHaiveServer(options = {}) {
5915
6204
  " scope \u2014 filter by personal | team | module",
5916
6205
  " type \u2014 filter by convention | decision | gotcha | architecture | glossary",
5917
6206
  " semantic \u2014 true for embedding-based search (requires @hiveai/embeddings)",
6207
+ " lexical_rank \u2014 BM25-style ranking (ignored when semantic is true)",
5918
6208
  " limit \u2014 max results (default 10)",
5919
6209
  "",
5920
6210
  "RETURNS: array of { id, type, scope, status, confidence, body, match_quality }"
@@ -5925,6 +6215,22 @@ function createHaiveServer(options = {}) {
5925
6215
  return jsonResult(await memSearch(input, context));
5926
6216
  }
5927
6217
  );
6218
+ server.tool(
6219
+ "mem_timeline",
6220
+ [
6221
+ "Chronological view of related memories: by shared frontmatter.topic OR expanded from a seed id",
6222
+ "(related_ids, same topic, overlapping anchor paths \u2014 one extra hop on related_ids).",
6223
+ "",
6224
+ "PARAMETERS:",
6225
+ " memory_id \u2014 optional seed memory id",
6226
+ " topic \u2014 optional topic key (required if memory_id omitted)",
6227
+ " limit \u2014 max entries (default 30)",
6228
+ "",
6229
+ "RETURNS: { entries: [{ id, type, scope, created_at, one_line, topic? }], total, notice? }"
6230
+ ].join("\n"),
6231
+ MemTimelineInputSchema,
6232
+ async (input) => jsonResult(await memTimeline(input, context))
6233
+ );
5928
6234
  server.tool(
5929
6235
  "mem_for_files",
5930
6236
  [
@@ -5954,7 +6260,7 @@ function createHaiveServer(options = {}) {
5954
6260
  [
5955
6261
  "Fetch a single memory by its full id with all details.",
5956
6262
  "",
5957
- "USE WHEN get_briefing returned a memory in 'compact' format and you need",
6263
+ "USE WHEN get_briefing / mem_relevant_to / mem_search returned a compact hit and you need",
5958
6264
  "the full body, or when you know the exact id of a memory.",
5959
6265
  "",
5960
6266
  "PARAMETERS:",
@@ -6042,6 +6348,22 @@ function createHaiveServer(options = {}) {
6042
6348
  CodeMapInputSchema,
6043
6349
  async (input) => jsonResult(await codeMapTool(input, context))
6044
6350
  );
6351
+ server.tool(
6352
+ "mem_resolve_project",
6353
+ [
6354
+ "Diagnostics: resolve which project root hAIve is using (never throws).",
6355
+ "",
6356
+ "USE IN multi-root workspaces or when the agent CWD may not be the repo root \u2014",
6357
+ "mirrors HAIVE_PROJECT_ROOT, findProjectRoot markers, and presence of .ai/memories.",
6358
+ "",
6359
+ "PARAMETERS:",
6360
+ " cwd \u2014 optional directory used for discovery when HAIVE_PROJECT_ROOT is unset",
6361
+ "",
6362
+ "RETURNS: { ok: true, info: { cwd, resolved_root, haive_project_root_env, \u2026 } }"
6363
+ ].join("\n"),
6364
+ MemResolveProjectInputSchema,
6365
+ async (input) => jsonResult(await memResolveProject(input, context))
6366
+ );
6045
6367
  server.tool(
6046
6368
  "mem_update",
6047
6369
  [
@@ -6175,6 +6497,8 @@ function createHaiveServer(options = {}) {
6175
6497
  "One-shot ranked memories for a task \u2014 use instead of get_briefing when",
6176
6498
  "project context is already loaded and you only want the relevant memory layer.",
6177
6499
  "",
6500
+ "Second step in progressive disclosure (after get_briefing): narrow here, then mem_get for full text.",
6501
+ "",
6178
6502
  "Reuses the same ranking pipeline (anchor / module / literal / semantic) but",
6179
6503
  "skips project_context, modules, action_required, etc.",
6180
6504
  "",
@@ -6183,6 +6507,7 @@ function createHaiveServer(options = {}) {
6183
6507
  " files \u2014 files you'll edit (surfaces anchored memories)",
6184
6508
  " limit \u2014 cap on returned memories (default 8)",
6185
6509
  " min_semantic_score \u2014 drop weak semantic hits below this cosine (default 0.25)",
6510
+ " format \u2014 'full' | 'compact' | 'actions' (inherits get_briefing memory framing)",
6186
6511
  "",
6187
6512
  "RETURNS: { task, search_mode, memories: [...], hints?: [...], empty?: true }"
6188
6513
  ].join("\n"),
@@ -6331,6 +6656,24 @@ function createHaiveServer(options = {}) {
6331
6656
  return jsonResult(await memConflicts(input, context));
6332
6657
  }
6333
6658
  );
6659
+ server.tool(
6660
+ "mem_conflict_candidates",
6661
+ [
6662
+ "Bulk lexical scan for decision/architecture-like pairs that look similar (Jaccard on tokens).",
6663
+ "",
6664
+ "Advisory only \u2014 follow with mem_conflicts_with on specific ids for real contradiction checks.",
6665
+ "",
6666
+ "PARAMETERS:",
6667
+ " since_days, types, min_jaccard, max_pairs, max_scan",
6668
+ "",
6669
+ "RETURNS: { pairs: [{ id_a, id_b, jaccard }], scanned, truncated, notice? }"
6670
+ ].join("\n"),
6671
+ MemConflictCandidatesInputSchema,
6672
+ async (input) => {
6673
+ tracker.record("mem_conflict_candidates", `${input.since_days}d`);
6674
+ return jsonResult(await memConflictCandidates(input, context));
6675
+ }
6676
+ );
6334
6677
  server.tool(
6335
6678
  "pre_commit_check",
6336
6679
  [
@@ -6468,7 +6811,7 @@ function registerMcp(program2) {
6468
6811
  // src/commands/sync.ts
6469
6812
  import { spawnSync as spawnSync2 } from "child_process";
6470
6813
  import { readFile as readFile8, writeFile as writeFile13, mkdir as mkdir8 } from "fs/promises";
6471
- import { existsSync as existsSync27 } from "fs";
6814
+ import { existsSync as existsSync29 } from "fs";
6472
6815
  import path12 from "path";
6473
6816
  import "commander";
6474
6817
  import {
@@ -6480,7 +6823,7 @@ import {
6480
6823
  isDecaying as isDecaying2,
6481
6824
  loadCodeMap as loadCodeMap4,
6482
6825
  loadConfig as loadConfig4,
6483
- loadMemoriesFromDir as loadMemoriesFromDir21,
6826
+ loadMemoriesFromDir as loadMemoriesFromDir23,
6484
6827
  loadUsageIndex as loadUsageIndex12,
6485
6828
  pullCrossRepoSources,
6486
6829
  resolveHaivePaths as resolveHaivePaths7,
@@ -6504,7 +6847,7 @@ function registerSync(program2) {
6504
6847
  ).option("--bridge-file <path>", "bridge file to inject into (default: CLAUDE.md)").option("--bridge-max-memories <n>", "max memories to inject into bridge file", "5").option("--embed", "rebuild embeddings index after sync (requires @haive/embeddings)").option("--no-cross-repo", "skip cross-repo memory pull even if crossRepoSources is configured").option("--no-deps", "skip dependency version tracking").option("--no-contracts", "skip contract file diff checking").action(async (opts) => {
6505
6848
  const root = findProjectRoot10(opts.dir);
6506
6849
  const paths = resolveHaivePaths7(root);
6507
- if (!existsSync27(paths.memoriesDir)) {
6850
+ if (!existsSync29(paths.memoriesDir)) {
6508
6851
  if (!opts.quiet) ui.warn(`No .ai/memories at ${root}. Run \`haive init\` first.`);
6509
6852
  process.exitCode = 1;
6510
6853
  return;
@@ -6520,7 +6863,7 @@ function registerSync(program2) {
6520
6863
  let promoted = 0;
6521
6864
  let autoApproved = 0;
6522
6865
  if (opts.verify !== false) {
6523
- const memories = await loadMemoriesFromDir21(paths.memoriesDir);
6866
+ const memories = await loadMemoriesFromDir23(paths.memoriesDir);
6524
6867
  for (const { memory: memory2, filePath } of memories) {
6525
6868
  if (memory2.frontmatter.type === "session_recap") {
6526
6869
  if (memory2.frontmatter.status === "stale") {
@@ -6581,7 +6924,7 @@ function registerSync(program2) {
6581
6924
  }
6582
6925
  }
6583
6926
  if (opts.promote !== false) {
6584
- const memories = await loadMemoriesFromDir21(paths.memoriesDir);
6927
+ const memories = await loadMemoriesFromDir23(paths.memoriesDir);
6585
6928
  const usage = await loadUsageIndex12(paths);
6586
6929
  const nowMs = Date.now();
6587
6930
  for (const { memory: memory2, filePath } of memories) {
@@ -6620,7 +6963,7 @@ function registerSync(program2) {
6620
6963
  }
6621
6964
  }
6622
6965
  const sinceReport = opts.since ? collectSinceChanges(root, opts.since) : null;
6623
- const draftMemories = (await loadMemoriesFromDir21(paths.memoriesDir)).filter(
6966
+ const draftMemories = (await loadMemoriesFromDir23(paths.memoriesDir)).filter(
6624
6967
  (m) => m.memory.frontmatter.status === "draft"
6625
6968
  );
6626
6969
  const draftCount = draftMemories.length;
@@ -6655,7 +6998,7 @@ function registerSync(program2) {
6655
6998
  }
6656
6999
  }
6657
7000
  if (!opts.quiet) {
6658
- const allForDecay = await loadMemoriesFromDir21(paths.memoriesDir);
7001
+ const allForDecay = await loadMemoriesFromDir23(paths.memoriesDir);
6659
7002
  const usageForDecay = await loadUsageIndex12(paths);
6660
7003
  const decaying = allForDecay.filter(({ memory: memory2 }) => {
6661
7004
  const fm = memory2.frontmatter;
@@ -6873,8 +7216,8 @@ Attends une **confirmation explicite** avant d'agir.
6873
7216
  });
6874
7217
  }
6875
7218
  async function injectBridge(bridgeFile, memoriesDir, maxMemories, root, quiet) {
6876
- if (!existsSync27(memoriesDir)) return;
6877
- const all = await loadMemoriesFromDir21(memoriesDir);
7219
+ if (!existsSync29(memoriesDir)) return;
7220
+ const all = await loadMemoriesFromDir23(memoriesDir);
6878
7221
  const top = all.filter(({ memory: memory2 }) => {
6879
7222
  const s = memory2.frontmatter.status;
6880
7223
  if (memory2.frontmatter.type === "session_recap") return false;
@@ -6898,7 +7241,7 @@ ${m.memory.body.trim()}`;
6898
7241
  ` + block + `
6899
7242
 
6900
7243
  ${BRIDGE_END}`;
6901
- const fileExists = existsSync27(bridgeFile);
7244
+ const fileExists = existsSync29(bridgeFile);
6902
7245
  let existing = fileExists ? await readFile8(bridgeFile, "utf8") : "";
6903
7246
  existing = existing.replace(/\r\n/g, "\n");
6904
7247
  const startIdx = existing.indexOf(BRIDGE_START);
@@ -6949,14 +7292,14 @@ function collectSinceChanges(root, ref) {
6949
7292
  // src/commands/memory-add.ts
6950
7293
  import { createHash as createHash2 } from "crypto";
6951
7294
  import { mkdir as mkdir9, readFile as readFile9, writeFile as writeFile14 } from "fs/promises";
6952
- import { existsSync as existsSync28 } from "fs";
7295
+ import { existsSync as existsSync30 } from "fs";
6953
7296
  import path13 from "path";
6954
7297
  import "commander";
6955
7298
  import {
6956
7299
  buildFrontmatter as buildFrontmatter7,
6957
7300
  findProjectRoot as findProjectRoot11,
6958
7301
  inferModulesFromPaths as inferModulesFromPaths3,
6959
- loadMemoriesFromDir as loadMemoriesFromDir23,
7302
+ loadMemoriesFromDir as loadMemoriesFromDir24,
6960
7303
  memoryFilePath as memoryFilePath6,
6961
7304
  resolveHaivePaths as resolveHaivePaths8,
6962
7305
  serializeMemory as serializeMemory12
@@ -6988,7 +7331,7 @@ function registerMemoryAdd(memory2) {
6988
7331
  ).requiredOption("--type <type>", "convention | decision | gotcha | architecture | glossary | attempt").requiredOption("--slug <slug>", "short kebab-case identifier used in the file name").option("--title <text>", "memory title \u2014 becomes the first heading of the body").option("--scope <scope>", "personal | team | module (default: personal, or team in autopilot)", "personal").option("--module <name>", "module name (required when scope=module)").option("--tags <csv>", "comma-separated tags for easier retrieval").option("--domain <domain>", "domain (e.g. transactions)").option("--author <author>", "author email or handle").option("--paths <csv>", "anchor to source files \u2014 used for staleness detection by haive sync").option("--symbols <csv>", "anchor to specific symbols (class/function names)").option("--commit <sha>", "anchor to a specific commit SHA").option("--body <text>", "memory body content (Markdown) \u2014 overrides --title default body").option("--body-file <path>", "read memory body from a Markdown file \u2014 for long content").option("--no-auto-tag", "disable automatic tag suggestions inferred from anchor paths").option("--topic <key>", "stable key for upsert: if a memory with this topic+scope already exists, update it in-place (revision_count++)").option("-d, --dir <dir>", "project root").action(async (opts) => {
6989
7332
  const root = findProjectRoot11(opts.dir);
6990
7333
  const paths = resolveHaivePaths8(root);
6991
- if (!existsSync28(paths.haiveDir)) {
7334
+ if (!existsSync30(paths.haiveDir)) {
6992
7335
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
6993
7336
  process.exitCode = 1;
6994
7337
  return;
@@ -6999,7 +7342,7 @@ function registerMemoryAdd(memory2) {
6999
7342
  const inferredTags = autoTagsEnabled ? inferModulesFromPaths3(anchorPaths) : [];
7000
7343
  const mergedTags = Array.from(/* @__PURE__ */ new Set([...userTags, ...inferredTags]));
7001
7344
  if (anchorPaths.length > 0) {
7002
- const missing = anchorPaths.filter((p) => !existsSync28(path13.resolve(root, p)));
7345
+ const missing = anchorPaths.filter((p) => !existsSync30(path13.resolve(root, p)));
7003
7346
  if (missing.length > 0) {
7004
7347
  ui.warn(`Anchor path${missing.length > 1 ? "s" : ""} not found in project:`);
7005
7348
  for (const p of missing) ui.warn(` \u2717 ${p}`);
@@ -7011,7 +7354,7 @@ function registerMemoryAdd(memory2) {
7011
7354
  const title = opts.title ?? opts.slug;
7012
7355
  let body;
7013
7356
  if (opts.bodyFile !== void 0) {
7014
- if (!existsSync28(opts.bodyFile)) {
7357
+ if (!existsSync30(opts.bodyFile)) {
7015
7358
  ui.error(`--body-file not found: ${opts.bodyFile}`);
7016
7359
  process.exitCode = 1;
7017
7360
  return;
@@ -7032,9 +7375,9 @@ TODO \u2014 write the memory body.
7032
7375
  `;
7033
7376
  }
7034
7377
  const scope = opts.scope ?? "personal";
7035
- if (existsSync28(paths.memoriesDir)) {
7378
+ if (existsSync30(paths.memoriesDir)) {
7036
7379
  const incomingHash = createHash2("sha256").update(body.trim()).digest("hex").slice(0, 12);
7037
- const allForHash = await loadMemoriesFromDir23(paths.memoriesDir);
7380
+ const allForHash = await loadMemoriesFromDir24(paths.memoriesDir);
7038
7381
  const hashDup = allForHash.find(
7039
7382
  ({ memory: memory3 }) => createHash2("sha256").update(memory3.body.trim()).digest("hex").slice(0, 12) === incomingHash && memory3.frontmatter.scope === scope
7040
7383
  );
@@ -7045,8 +7388,8 @@ TODO \u2014 write the memory body.
7045
7388
  return;
7046
7389
  }
7047
7390
  }
7048
- if (opts.topic && existsSync28(paths.memoriesDir)) {
7049
- const existing = await loadMemoriesFromDir23(paths.memoriesDir);
7391
+ if (opts.topic && existsSync30(paths.memoriesDir)) {
7392
+ const existing = await loadMemoriesFromDir24(paths.memoriesDir);
7050
7393
  const topicMatch = existing.find(
7051
7394
  ({ memory: memory3 }) => memory3.frontmatter.topic === opts.topic && memory3.frontmatter.scope === scope && (!opts.module || memory3.frontmatter.module === opts.module)
7052
7395
  );
@@ -7084,13 +7427,13 @@ TODO \u2014 write the memory body.
7084
7427
  });
7085
7428
  const file = memoryFilePath6(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
7086
7429
  await mkdir9(path13.dirname(file), { recursive: true });
7087
- if (existsSync28(file)) {
7430
+ if (existsSync30(file)) {
7088
7431
  ui.error(`Memory already exists at ${file}`);
7089
7432
  process.exitCode = 1;
7090
7433
  return;
7091
7434
  }
7092
- if (existsSync28(paths.memoriesDir)) {
7093
- const existing = await loadMemoriesFromDir23(paths.memoriesDir);
7435
+ if (existsSync30(paths.memoriesDir)) {
7436
+ const existing = await loadMemoriesFromDir24(paths.memoriesDir);
7094
7437
  const slugTokens = opts.slug.toLowerCase().split(/[-_\s]+/).filter(Boolean);
7095
7438
  const similar = existing.filter(({ memory: memory3 }) => {
7096
7439
  const id = memory3.frontmatter.id.toLowerCase();
@@ -7132,14 +7475,14 @@ function parseCsv2(value) {
7132
7475
  }
7133
7476
 
7134
7477
  // src/commands/memory-list.ts
7135
- import { existsSync as existsSync29 } from "fs";
7478
+ import { existsSync as existsSync31 } from "fs";
7136
7479
  import path14 from "path";
7137
7480
  import "commander";
7138
7481
  import { findProjectRoot as findProjectRoot12, resolveHaivePaths as resolveHaivePaths9 } from "@hiveai/core";
7139
7482
 
7140
7483
  // src/utils/fs.ts
7141
7484
  import {
7142
- loadMemoriesFromDir as loadMemoriesFromDir24,
7485
+ loadMemoriesFromDir as loadMemoriesFromDir25,
7143
7486
  loadMemory,
7144
7487
  listMarkdownFilesRecursive
7145
7488
  } from "@hiveai/core";
@@ -7149,12 +7492,12 @@ function registerMemoryList(memory2) {
7149
7492
  memory2.command("list").description("List memories with optional filters").option("--scope <scope>", "personal | team | module").option("--type <type>", "filter by type").option("--tag <tag>", "filter by tag").option("--module <name>", "filter by module name").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected,deprecated)").option("--show-rejected", "include rejected memories (hidden by default)").option("-d, --dir <dir>", "project root").action(async (opts) => {
7150
7493
  const root = findProjectRoot12(opts.dir);
7151
7494
  const paths = resolveHaivePaths9(root);
7152
- if (!existsSync29(paths.memoriesDir)) {
7495
+ if (!existsSync31(paths.memoriesDir)) {
7153
7496
  ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
7154
7497
  process.exitCode = 1;
7155
7498
  return;
7156
7499
  }
7157
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
7500
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
7158
7501
  const statusFilter = opts.status ? opts.status.split(",").map((s) => s.trim()) : null;
7159
7502
  const filtered = all.filter((m) => {
7160
7503
  if (!matchesFilters(m, opts)) return false;
@@ -7217,7 +7560,7 @@ function matchesFilters(loaded, opts) {
7217
7560
 
7218
7561
  // src/commands/memory-promote.ts
7219
7562
  import { mkdir as mkdir10, unlink as unlink2, writeFile as writeFile15 } from "fs/promises";
7220
- import { existsSync as existsSync30 } from "fs";
7563
+ import { existsSync as existsSync33 } from "fs";
7221
7564
  import path15 from "path";
7222
7565
  import "commander";
7223
7566
  import {
@@ -7230,12 +7573,12 @@ function registerMemoryPromote(memory2) {
7230
7573
  memory2.command("promote <id>").description("Promote a personal memory to team scope (status -> proposed)").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
7231
7574
  const root = findProjectRoot13(opts.dir);
7232
7575
  const paths = resolveHaivePaths10(root);
7233
- if (!existsSync30(paths.memoriesDir)) {
7576
+ if (!existsSync33(paths.memoriesDir)) {
7234
7577
  ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
7235
7578
  process.exitCode = 1;
7236
7579
  return;
7237
7580
  }
7238
- const teamAndModule = await loadMemoriesFromDir24(paths.memoriesDir);
7581
+ const teamAndModule = await loadMemoriesFromDir25(paths.memoriesDir);
7239
7582
  const alreadyShared = teamAndModule.find(
7240
7583
  (m) => m.memory.frontmatter.id === id && (m.memory.frontmatter.scope === "team" || m.memory.frontmatter.scope === "module")
7241
7584
  );
@@ -7249,7 +7592,7 @@ function registerMemoryPromote(memory2) {
7249
7592
  }
7250
7593
  return;
7251
7594
  }
7252
- const all = await loadMemoriesFromDir24(paths.personalDir);
7595
+ const all = await loadMemoriesFromDir25(paths.personalDir);
7253
7596
  const found = all.find((m) => m.memory.frontmatter.id === id);
7254
7597
  if (!found) {
7255
7598
  ui.error(`No personal memory with id "${id}". (Promotion only applies to personal scope.)`);
@@ -7275,7 +7618,7 @@ function registerMemoryPromote(memory2) {
7275
7618
  }
7276
7619
 
7277
7620
  // src/commands/memory-approve.ts
7278
- import { existsSync as existsSync31 } from "fs";
7621
+ import { existsSync as existsSync34 } from "fs";
7279
7622
  import { writeFile as writeFile16 } from "fs/promises";
7280
7623
  import path16 from "path";
7281
7624
  import "commander";
@@ -7288,12 +7631,12 @@ function registerMemoryApprove(memory2) {
7288
7631
  memory2.command("approve [id]").description("Mark a memory as 'validated'. Use --all to bulk-approve all proposed/draft memories.").option("--all", "approve all proposed and draft memories at once").option("--pending", "approve all memories with status 'proposed'").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
7289
7632
  const root = findProjectRoot14(opts.dir);
7290
7633
  const paths = resolveHaivePaths11(root);
7291
- if (!existsSync31(paths.memoriesDir)) {
7634
+ if (!existsSync34(paths.memoriesDir)) {
7292
7635
  ui.error(`No .ai/memories at ${root}.`);
7293
7636
  process.exitCode = 1;
7294
7637
  return;
7295
7638
  }
7296
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
7639
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
7297
7640
  if (opts.all || opts.pending) {
7298
7641
  const candidates = all.filter((m) => {
7299
7642
  const s = m.memory.frontmatter.status;
@@ -7347,7 +7690,7 @@ function registerMemoryApprove(memory2) {
7347
7690
 
7348
7691
  // src/commands/memory-update.ts
7349
7692
  import { writeFile as writeFile17 } from "fs/promises";
7350
- import { existsSync as existsSync33 } from "fs";
7693
+ import { existsSync as existsSync35 } from "fs";
7351
7694
  import path17 from "path";
7352
7695
  import "commander";
7353
7696
  import {
@@ -7359,12 +7702,12 @@ function registerMemoryUpdate(memory2) {
7359
7702
  memory2.command("update <id>").description("Update body, tags, or anchor of an existing memory (preserves id and usage history)").option("--title <text>", "new title \u2014 replaces the first heading of the body").option("--body <text>", "new Markdown body \u2014 replaces the existing body").option("--tags <csv>", "new tags, comma-separated \u2014 fully replaces existing tags").option("--paths <csv>", "new anchor paths, comma-separated").option("--symbols <csv>", "new anchor symbols, comma-separated").option("--commit <sha>", "new anchor commit SHA").option("--domain <domain>", "new domain label").option("--author <author>", "new author handle or email").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
7360
7703
  const root = findProjectRoot15(opts.dir);
7361
7704
  const paths = resolveHaivePaths12(root);
7362
- if (!existsSync33(paths.memoriesDir)) {
7705
+ if (!existsSync35(paths.memoriesDir)) {
7363
7706
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
7364
7707
  process.exitCode = 1;
7365
7708
  return;
7366
7709
  }
7367
- const memories = await loadMemoriesFromDir24(paths.memoriesDir);
7710
+ const memories = await loadMemoriesFromDir25(paths.memoriesDir);
7368
7711
  const loaded = memories.find((m) => m.memory.frontmatter.id === id);
7369
7712
  if (!loaded) {
7370
7713
  ui.error(`No memory with id "${id}".`);
@@ -7431,7 +7774,7 @@ function parseCsv3(value) {
7431
7774
 
7432
7775
  // src/commands/memory-auto-promote.ts
7433
7776
  import { writeFile as writeFile18 } from "fs/promises";
7434
- import { existsSync as existsSync34 } from "fs";
7777
+ import { existsSync as existsSync36 } from "fs";
7435
7778
  import path18 from "path";
7436
7779
  import "commander";
7437
7780
  import {
@@ -7451,7 +7794,7 @@ function registerMemoryAutoPromote(memory2) {
7451
7794
  ).option("--apply", "actually write status=validated to disk (default: dry-run)").option("-d, --dir <dir>", "project root").action(async (opts) => {
7452
7795
  const root = findProjectRoot16(opts.dir);
7453
7796
  const paths = resolveHaivePaths13(root);
7454
- if (!existsSync34(paths.memoriesDir)) {
7797
+ if (!existsSync36(paths.memoriesDir)) {
7455
7798
  ui.error(`No .ai/memories at ${root}.`);
7456
7799
  process.exitCode = 1;
7457
7800
  return;
@@ -7460,7 +7803,7 @@ function registerMemoryAutoPromote(memory2) {
7460
7803
  minReads: Number(opts.minReads ?? DEFAULT_AUTO_PROMOTE_RULE3.minReads),
7461
7804
  maxRejections: Number(opts.maxRejections ?? DEFAULT_AUTO_PROMOTE_RULE3.maxRejections)
7462
7805
  };
7463
- const memories = await loadMemoriesFromDir24(paths.memoriesDir);
7806
+ const memories = await loadMemoriesFromDir25(paths.memoriesDir);
7464
7807
  const usage = await loadUsageIndex13(paths);
7465
7808
  const eligible = memories.filter(
7466
7809
  ({ memory: memory3 }) => isAutoPromoteEligible3(memory3.frontmatter, getUsage11(usage, memory3.frontmatter.id), rule)
@@ -7494,7 +7837,7 @@ function registerMemoryAutoPromote(memory2) {
7494
7837
 
7495
7838
  // src/commands/memory-edit.ts
7496
7839
  import { spawn as spawn3 } from "child_process";
7497
- import { existsSync as existsSync35 } from "fs";
7840
+ import { existsSync as existsSync37 } from "fs";
7498
7841
  import { readFile as readFile10 } from "fs/promises";
7499
7842
  import path19 from "path";
7500
7843
  import "commander";
@@ -7507,12 +7850,12 @@ function registerMemoryEdit(memory2) {
7507
7850
  memory2.command("edit <id>").description("Open a memory in $EDITOR and re-validate when you save").option("-e, --editor <cmd>", "editor command (defaults to $EDITOR or 'vi')").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
7508
7851
  const root = findProjectRoot17(opts.dir);
7509
7852
  const paths = resolveHaivePaths14(root);
7510
- if (!existsSync35(paths.memoriesDir)) {
7853
+ if (!existsSync37(paths.memoriesDir)) {
7511
7854
  ui.error(`No .ai/memories at ${root}.`);
7512
7855
  process.exitCode = 1;
7513
7856
  return;
7514
7857
  }
7515
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
7858
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
7516
7859
  const found = all.find((m) => m.memory.frontmatter.id === id);
7517
7860
  if (!found) {
7518
7861
  ui.error(`No memory with id "${id}".`);
@@ -7547,7 +7890,7 @@ function runEditor(editor, file) {
7547
7890
  }
7548
7891
 
7549
7892
  // src/commands/memory-for-files.ts
7550
- import { existsSync as existsSync36 } from "fs";
7893
+ import { existsSync as existsSync38 } from "fs";
7551
7894
  import path20 from "path";
7552
7895
  import "commander";
7553
7896
  import {
@@ -7563,12 +7906,12 @@ function registerMemoryForFiles(memory2) {
7563
7906
  memory2.command("for-files <files...>").description("Show memories relevant to the given files (anchor overlap, module, domain)").option("-d, --dir <dir>", "project root").action(async (files, opts) => {
7564
7907
  const root = findProjectRoot18(opts.dir);
7565
7908
  const paths = resolveHaivePaths15(root);
7566
- if (!existsSync36(paths.memoriesDir)) {
7909
+ if (!existsSync38(paths.memoriesDir)) {
7567
7910
  ui.error(`No .ai/memories at ${root}.`);
7568
7911
  process.exitCode = 1;
7569
7912
  return;
7570
7913
  }
7571
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
7914
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
7572
7915
  const usage = await loadUsageIndex14(paths);
7573
7916
  const inferred = inferModulesFromPaths4(files);
7574
7917
  const byAnchor = [];
@@ -7675,7 +8018,7 @@ function printGroup(root, label, loaded, usage) {
7675
8018
  }
7676
8019
 
7677
8020
  // src/commands/memory-hot.ts
7678
- import { existsSync as existsSync37 } from "fs";
8021
+ import { existsSync as existsSync39 } from "fs";
7679
8022
  import path21 from "path";
7680
8023
  import "commander";
7681
8024
  import {
@@ -7688,13 +8031,13 @@ function registerMemoryHot(memory2) {
7688
8031
  memory2.command("hot").description("List memories actively used but not yet validated (good promotion candidates)").option("--threshold <n>", "minimum read_count to qualify", "3").option("--status <status>", "limit to one status (default: draft + proposed)").option("-d, --dir <dir>", "project root").action(async (opts) => {
7689
8032
  const root = findProjectRoot19(opts.dir);
7690
8033
  const paths = resolveHaivePaths16(root);
7691
- if (!existsSync37(paths.memoriesDir)) {
8034
+ if (!existsSync39(paths.memoriesDir)) {
7692
8035
  ui.error(`No .ai/memories at ${root}.`);
7693
8036
  process.exitCode = 1;
7694
8037
  return;
7695
8038
  }
7696
8039
  const threshold = Math.max(1, Number(opts.threshold ?? 3));
7697
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
8040
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
7698
8041
  const usage = await loadUsageIndex15(paths);
7699
8042
  const candidates = all.filter(({ memory: mem }) => {
7700
8043
  const fm = mem.frontmatter;
@@ -7726,7 +8069,7 @@ function registerMemoryHot(memory2) {
7726
8069
 
7727
8070
  // src/commands/memory-tried.ts
7728
8071
  import { mkdir as mkdir11, writeFile as writeFile19 } from "fs/promises";
7729
- import { existsSync as existsSync38 } from "fs";
8072
+ import { existsSync as existsSync40 } from "fs";
7730
8073
  import path23 from "path";
7731
8074
  import "commander";
7732
8075
  import {
@@ -7755,7 +8098,7 @@ function registerMemoryTried(memory2) {
7755
8098
  ).requiredOption("--what <text>", "what approach was tried (short, descriptive title)").requiredOption("--why-failed <text>", "why it failed or should NOT be used (include the exact error if possible)").option("--instead <text>", "the correct approach to use instead").option("--scope <scope>", "personal | team | module (default: personal)", "personal").option("--module <name>", "module name (required when scope=module)").option("--tags <csv>", "comma-separated tags").option("--paths <csv>", "anchor paths, comma-separated").option("--author <author>", "author email or handle").option("-d, --dir <dir>", "project root").action(async (opts) => {
7756
8099
  const root = findProjectRoot20(opts.dir);
7757
8100
  const paths = resolveHaivePaths17(root);
7758
- if (!existsSync38(paths.haiveDir)) {
8101
+ if (!existsSync40(paths.haiveDir)) {
7759
8102
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
7760
8103
  process.exitCode = 1;
7761
8104
  return;
@@ -7779,7 +8122,7 @@ function registerMemoryTried(memory2) {
7779
8122
  const body = lines.join("\n") + "\n";
7780
8123
  const file = memoryFilePath8(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
7781
8124
  await mkdir11(path23.dirname(file), { recursive: true });
7782
- if (existsSync38(file)) {
8125
+ if (existsSync40(file)) {
7783
8126
  ui.error(`Memory already exists at ${file}`);
7784
8127
  process.exitCode = 1;
7785
8128
  return;
@@ -7795,7 +8138,7 @@ function parseCsv4(value) {
7795
8138
  }
7796
8139
 
7797
8140
  // src/commands/memory-pending.ts
7798
- import { existsSync as existsSync39 } from "fs";
8141
+ import { existsSync as existsSync41 } from "fs";
7799
8142
  import path24 from "path";
7800
8143
  import "commander";
7801
8144
  import {
@@ -7808,12 +8151,12 @@ function registerMemoryPending(memory2) {
7808
8151
  memory2.command("pending").description("List 'proposed' memories awaiting review (sorted by reads desc)").option("--scope <scope>", "filter by scope (personal | team | module)").option("-d, --dir <dir>", "project root").action(async (opts) => {
7809
8152
  const root = findProjectRoot21(opts.dir);
7810
8153
  const paths = resolveHaivePaths18(root);
7811
- if (!existsSync39(paths.memoriesDir)) {
8154
+ if (!existsSync41(paths.memoriesDir)) {
7812
8155
  ui.error(`No .ai/memories at ${root}.`);
7813
8156
  process.exitCode = 1;
7814
8157
  return;
7815
8158
  }
7816
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
8159
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
7817
8160
  const usage = await loadUsageIndex16(paths);
7818
8161
  const proposed = all.filter(({ memory: mem }) => {
7819
8162
  if (mem.frontmatter.status !== "proposed") return false;
@@ -7843,7 +8186,7 @@ function registerMemoryPending(memory2) {
7843
8186
  }
7844
8187
 
7845
8188
  // src/commands/memory-query.ts
7846
- import { existsSync as existsSync40 } from "fs";
8189
+ import { existsSync as existsSync43 } from "fs";
7847
8190
  import path25 from "path";
7848
8191
  import "commander";
7849
8192
  import {
@@ -7860,7 +8203,7 @@ function registerMemoryQuery(memory2) {
7860
8203
  memory2.command("query <text>").alias("search").description("Search memories by id, tag, or substring (AND, OR fallback). Alias: search").option("-d, --dir <dir>", "project root").option("--limit <n>", "max results", "20").option("--scope <scope>", "personal | team | module").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected)").option("--show-rejected", "include rejected memories (hidden by default)").action(async (text, opts) => {
7861
8204
  const root = findProjectRoot22(opts.dir);
7862
8205
  const paths = resolveHaivePaths19(root);
7863
- if (!existsSync40(paths.memoriesDir)) {
8206
+ if (!existsSync43(paths.memoriesDir)) {
7864
8207
  ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
7865
8208
  process.exitCode = 1;
7866
8209
  return;
@@ -7871,7 +8214,7 @@ function registerMemoryQuery(memory2) {
7871
8214
  return;
7872
8215
  }
7873
8216
  const statusFilter = opts.status ? opts.status.split(",").map((s) => s.trim()) : null;
7874
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
8217
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
7875
8218
  const passesFilters2 = (mem) => {
7876
8219
  const fm = mem.frontmatter;
7877
8220
  if (opts.scope && fm.scope !== opts.scope) return false;
@@ -7919,7 +8262,7 @@ ${top.length} of ${matches.length} match${matches.length === 1 ? "" : "es"}`)
7919
8262
 
7920
8263
  // src/commands/memory-reject.ts
7921
8264
  import { writeFile as writeFile20 } from "fs/promises";
7922
- import { existsSync as existsSync41 } from "fs";
8265
+ import { existsSync as existsSync44 } from "fs";
7923
8266
  import "commander";
7924
8267
  import {
7925
8268
  findProjectRoot as findProjectRoot23,
@@ -7933,12 +8276,12 @@ function registerMemoryReject(memory2) {
7933
8276
  memory2.command("reject <id>").description("Record a rejection (blocks auto-promotion and lowers confidence)").option("-r, --reason <reason>", "why this memory is being rejected").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
7934
8277
  const root = findProjectRoot23(opts.dir);
7935
8278
  const paths = resolveHaivePaths20(root);
7936
- if (!existsSync41(paths.memoriesDir)) {
8279
+ if (!existsSync44(paths.memoriesDir)) {
7937
8280
  ui.error(`No .ai/memories at ${root}.`);
7938
8281
  process.exitCode = 1;
7939
8282
  return;
7940
8283
  }
7941
- const memories = await loadMemoriesFromDir24(paths.memoriesDir);
8284
+ const memories = await loadMemoriesFromDir25(paths.memoriesDir);
7942
8285
  const loaded = memories.find((m) => m.memory.frontmatter.id === id);
7943
8286
  if (!loaded) {
7944
8287
  ui.error(`No memory with id "${id}".`);
@@ -7969,7 +8312,7 @@ function registerMemoryReject(memory2) {
7969
8312
  }
7970
8313
 
7971
8314
  // src/commands/memory-rm.ts
7972
- import { existsSync as existsSync43 } from "fs";
8315
+ import { existsSync as existsSync45 } from "fs";
7973
8316
  import { unlink as unlink3 } from "fs/promises";
7974
8317
  import path26 from "path";
7975
8318
  import { createInterface } from "readline/promises";
@@ -7984,12 +8327,12 @@ function registerMemoryRm(memory2) {
7984
8327
  memory2.command("rm <id>").description("Delete a memory file (and its usage entry by default)").option("-y, --yes", "skip the confirmation prompt").option("--keep-usage", "do not remove the usage.json entry").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
7985
8328
  const root = findProjectRoot24(opts.dir);
7986
8329
  const paths = resolveHaivePaths21(root);
7987
- if (!existsSync43(paths.memoriesDir)) {
8330
+ if (!existsSync45(paths.memoriesDir)) {
7988
8331
  ui.error(`No .ai/memories at ${root}.`);
7989
8332
  process.exitCode = 1;
7990
8333
  return;
7991
8334
  }
7992
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
8335
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
7993
8336
  const found = all.find((m) => m.memory.frontmatter.id === id);
7994
8337
  if (!found) {
7995
8338
  ui.error(`No memory with id "${id}".`);
@@ -8020,7 +8363,7 @@ function registerMemoryRm(memory2) {
8020
8363
  }
8021
8364
 
8022
8365
  // src/commands/memory-show.ts
8023
- import { existsSync as existsSync44 } from "fs";
8366
+ import { existsSync as existsSync46 } from "fs";
8024
8367
  import { readFile as readFile11 } from "fs/promises";
8025
8368
  import path27 from "path";
8026
8369
  import "commander";
@@ -8035,12 +8378,12 @@ function registerMemoryShow(memory2) {
8035
8378
  memory2.command("show <id>").description("Print a memory's frontmatter, body, and confidence/usage").option("--raw", "print the raw file contents instead of a summary").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
8036
8379
  const root = findProjectRoot25(opts.dir);
8037
8380
  const paths = resolveHaivePaths22(root);
8038
- if (!existsSync44(paths.memoriesDir)) {
8381
+ if (!existsSync46(paths.memoriesDir)) {
8039
8382
  ui.error(`No .ai/memories at ${root}.`);
8040
8383
  process.exitCode = 1;
8041
8384
  return;
8042
8385
  }
8043
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
8386
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
8044
8387
  const found = all.find((m) => m.memory.frontmatter.id === id);
8045
8388
  if (!found) {
8046
8389
  ui.error(`No memory with id "${id}".`);
@@ -8079,7 +8422,7 @@ function registerMemoryShow(memory2) {
8079
8422
  }
8080
8423
 
8081
8424
  // src/commands/memory-stats.ts
8082
- import { existsSync as existsSync45 } from "fs";
8425
+ import { existsSync as existsSync47 } from "fs";
8083
8426
  import path28 from "path";
8084
8427
  import "commander";
8085
8428
  import {
@@ -8093,12 +8436,12 @@ function registerMemoryStats(memory2) {
8093
8436
  memory2.command("stats").description("Show usage stats and confidence levels per memory").option("--id <id>", "show stats for a single memory id").option("-d, --dir <dir>", "project root").action(async (opts) => {
8094
8437
  const root = findProjectRoot26(opts.dir);
8095
8438
  const paths = resolveHaivePaths23(root);
8096
- if (!existsSync45(paths.memoriesDir)) {
8439
+ if (!existsSync47(paths.memoriesDir)) {
8097
8440
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
8098
8441
  process.exitCode = 1;
8099
8442
  return;
8100
8443
  }
8101
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
8444
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
8102
8445
  const usage = await loadUsageIndex20(paths);
8103
8446
  const target = opts.id ? all.filter((m) => m.memory.frontmatter.id === opts.id) : all;
8104
8447
  if (target.length === 0) {
@@ -8125,7 +8468,7 @@ function registerMemoryStats(memory2) {
8125
8468
 
8126
8469
  // src/commands/memory-verify.ts
8127
8470
  import { writeFile as writeFile21 } from "fs/promises";
8128
- import { existsSync as existsSync46 } from "fs";
8471
+ import { existsSync as existsSync48 } from "fs";
8129
8472
  import path29 from "path";
8130
8473
  import "commander";
8131
8474
  import {
@@ -8140,12 +8483,12 @@ function registerMemoryVerify(memory2) {
8140
8483
  ).option("--id <id>", "verify a single memory by id").option("--all", "verify every memory (default if --id is omitted)").option("--update", "write status=stale or status=validated back to disk").option("-d, --dir <dir>", "project root").action(async (opts) => {
8141
8484
  const root = findProjectRoot27(opts.dir);
8142
8485
  const paths = resolveHaivePaths24(root);
8143
- if (!existsSync46(paths.memoriesDir)) {
8486
+ if (!existsSync48(paths.memoriesDir)) {
8144
8487
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
8145
8488
  process.exitCode = 1;
8146
8489
  return;
8147
8490
  }
8148
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
8491
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
8149
8492
  const targets = opts.id ? all.filter((m) => m.memory.frontmatter.id === opts.id) : all;
8150
8493
  if (opts.id && targets.length === 0) {
8151
8494
  ui.error(`No memory with id "${opts.id}".`);
@@ -8227,7 +8570,7 @@ function applyVerification2(mem, result) {
8227
8570
 
8228
8571
  // src/commands/memory-import.ts
8229
8572
  import { readFile as readFile12 } from "fs/promises";
8230
- import { existsSync as existsSync47 } from "fs";
8573
+ import { existsSync as existsSync49 } from "fs";
8231
8574
  import "commander";
8232
8575
  import {
8233
8576
  findProjectRoot as findProjectRoot28,
@@ -8239,12 +8582,12 @@ function registerMemoryImport(memory2) {
8239
8582
  ).requiredOption("--from <file>", "Markdown/text file to import from").option("--scope <scope>", "personal | team (default: team)", "team").option("-d, --dir <dir>", "project root").action(async (opts) => {
8240
8583
  const root = findProjectRoot28(opts.dir);
8241
8584
  const paths = resolveHaivePaths25(root);
8242
- if (!existsSync47(paths.haiveDir)) {
8585
+ if (!existsSync49(paths.haiveDir)) {
8243
8586
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
8244
8587
  process.exitCode = 1;
8245
8588
  return;
8246
8589
  }
8247
- if (!existsSync47(opts.from)) {
8590
+ if (!existsSync49(opts.from)) {
8248
8591
  ui.error(`File not found: ${opts.from}`);
8249
8592
  process.exitCode = 1;
8250
8593
  return;
@@ -8277,7 +8620,7 @@ function registerMemoryImport(memory2) {
8277
8620
  }
8278
8621
 
8279
8622
  // src/commands/memory-import-changelog.ts
8280
- import { existsSync as existsSync48 } from "fs";
8623
+ import { existsSync as existsSync50 } from "fs";
8281
8624
  import { readFile as readFile13, mkdir as mkdir12, writeFile as writeFile23 } from "fs/promises";
8282
8625
  import path30 from "path";
8283
8626
  import "commander";
@@ -8351,7 +8694,7 @@ function registerMemoryImportChangelog(memory2) {
8351
8694
  const root = findProjectRoot29(opts.dir);
8352
8695
  const paths = resolveHaivePaths26(root);
8353
8696
  const changelogPath = path30.resolve(root, opts.fromChangelog);
8354
- if (!existsSync48(changelogPath)) {
8697
+ if (!existsSync50(changelogPath)) {
8355
8698
  ui.error(`CHANGELOG not found: ${changelogPath}`);
8356
8699
  process.exitCode = 1;
8357
8700
  return;
@@ -8437,7 +8780,7 @@ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${p
8437
8780
  }
8438
8781
 
8439
8782
  // src/commands/memory-digest.ts
8440
- import { existsSync as existsSync49 } from "fs";
8783
+ import { existsSync as existsSync51 } from "fs";
8441
8784
  import { writeFile as writeFile24 } from "fs/promises";
8442
8785
  import path31 from "path";
8443
8786
  import "commander";
@@ -8445,7 +8788,7 @@ import {
8445
8788
  deriveConfidence as deriveConfidence12,
8446
8789
  findProjectRoot as findProjectRoot30,
8447
8790
  getUsage as getUsage17,
8448
- loadMemoriesFromDir as loadMemoriesFromDir25,
8791
+ loadMemoriesFromDir as loadMemoriesFromDir26,
8449
8792
  loadUsageIndex as loadUsageIndex21,
8450
8793
  resolveHaivePaths as resolveHaivePaths27
8451
8794
  } from "@hiveai/core";
@@ -8462,7 +8805,7 @@ function registerMemoryDigest(program2) {
8462
8805
  ).option("--days <n>", "look-back window in days (default: 7)", "7").option("--scope <scope>", "personal | team | module | all (default: team)", "team").option("--out <file>", "write digest to a file instead of stdout").option("-d, --dir <dir>", "project root").action(async (opts) => {
8463
8806
  const root = findProjectRoot30(opts.dir);
8464
8807
  const paths = resolveHaivePaths27(root);
8465
- if (!existsSync49(paths.memoriesDir)) {
8808
+ if (!existsSync51(paths.memoriesDir)) {
8466
8809
  ui.error("No .ai/memories found. Run `haive init` first.");
8467
8810
  process.exitCode = 1;
8468
8811
  return;
@@ -8470,7 +8813,7 @@ function registerMemoryDigest(program2) {
8470
8813
  const days = Math.max(1, Number(opts.days ?? 7));
8471
8814
  const scopeFilter = opts.scope ?? "team";
8472
8815
  const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
8473
- const all = await loadMemoriesFromDir25(paths.memoriesDir);
8816
+ const all = await loadMemoriesFromDir26(paths.memoriesDir);
8474
8817
  const usage = await loadUsageIndex21(paths);
8475
8818
  const recent = all.filter(({ memory: mem }) => {
8476
8819
  const fm = mem.frontmatter;
@@ -8545,20 +8888,20 @@ function registerMemoryDigest(program2) {
8545
8888
 
8546
8889
  // src/commands/session-end.ts
8547
8890
  import { writeFile as writeFile25, mkdir as mkdir13, readFile as readFile14, rm as rm2 } from "fs/promises";
8548
- import { existsSync as existsSync50 } from "fs";
8891
+ import { existsSync as existsSync53 } from "fs";
8549
8892
  import path33 from "path";
8550
8893
  import "commander";
8551
8894
  import {
8552
8895
  buildFrontmatter as buildFrontmatter10,
8553
8896
  findProjectRoot as findProjectRoot31,
8554
- loadMemoriesFromDir as loadMemoriesFromDir26,
8897
+ loadMemoriesFromDir as loadMemoriesFromDir27,
8555
8898
  memoryFilePath as memoryFilePath9,
8556
8899
  resolveHaivePaths as resolveHaivePaths28,
8557
8900
  serializeMemory as serializeMemory21
8558
8901
  } from "@hiveai/core";
8559
8902
  async function buildAutoRecap(paths) {
8560
8903
  const obsFile = path33.join(paths.haiveDir, ".cache", "observations.jsonl");
8561
- if (!existsSync50(obsFile)) return null;
8904
+ if (!existsSync53(obsFile)) return null;
8562
8905
  const raw = await readFile14(obsFile, "utf8").catch(() => "");
8563
8906
  if (!raw.trim()) return null;
8564
8907
  const lines = raw.split("\n").filter(Boolean);
@@ -8639,7 +8982,7 @@ function registerSessionEnd(session2) {
8639
8982
  ).option("--goal <text>", "what you were trying to accomplish (1\u20132 sentences)").option("--accomplished <text>", "what was actually done (bullet list recommended)").option("--discoveries <text>", "bugs, surprises, or inconsistencies found during this session").option("--files <csv>", "key files touched, comma-separated (used as anchor for staleness detection)").option("--next <text>", "what should happen next (for the next session or a teammate)").option("--scope <scope>", "personal | team | module (default: personal)", "personal").option("--module <name>", "module name (required when scope=module)").option("--auto", "synthesize the recap from .ai/.cache/observations.jsonl (used by Claude Code SessionEnd hook)").option("--quiet", "suppress non-error output (for hook use)").option("-d, --dir <dir>", "project root").action(async (opts) => {
8640
8983
  const root = findProjectRoot31(opts.dir);
8641
8984
  const paths = resolveHaivePaths28(root);
8642
- if (!existsSync50(paths.haiveDir)) {
8985
+ if (!existsSync53(paths.haiveDir)) {
8643
8986
  if (opts.auto || opts.quiet) return;
8644
8987
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
8645
8988
  process.exitCode = 1;
@@ -8671,7 +9014,7 @@ function registerSessionEnd(session2) {
8671
9014
  });
8672
9015
  const topic = recapTopic2(scope, opts.module);
8673
9016
  const filesTouched = parseCsv5(resolvedFiles);
8674
- const missingPaths = filesTouched.filter((p) => !existsSync50(path33.resolve(root, p)));
9017
+ const missingPaths = filesTouched.filter((p) => !existsSync53(path33.resolve(root, p)));
8675
9018
  if (missingPaths.length > 0 && !opts.quiet) {
8676
9019
  ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
8677
9020
  for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
@@ -8679,11 +9022,11 @@ function registerSessionEnd(session2) {
8679
9022
  const cleanupObservations = async () => {
8680
9023
  if (!opts.auto) return;
8681
9024
  const obsFile = path33.join(paths.haiveDir, ".cache", "observations.jsonl");
8682
- if (existsSync50(obsFile)) await rm2(obsFile).catch(() => {
9025
+ if (existsSync53(obsFile)) await rm2(obsFile).catch(() => {
8683
9026
  });
8684
9027
  };
8685
- if (existsSync50(paths.memoriesDir)) {
8686
- const existing = await loadMemoriesFromDir26(paths.memoriesDir);
9028
+ if (existsSync53(paths.memoriesDir)) {
9029
+ const existing = await loadMemoriesFromDir27(paths.memoriesDir);
8687
9030
  const topicMatch = existing.find(
8688
9031
  ({ memory: memory2 }) => memory2.frontmatter.topic === topic && memory2.frontmatter.scope === scope && (!opts.module || memory2.frontmatter.module === opts.module)
8689
9032
  );
@@ -8703,6 +9046,7 @@ function registerSessionEnd(session2) {
8703
9046
  if (!opts.quiet) {
8704
9047
  ui.success(`Session recap updated (revision #${revisionCount})`);
8705
9048
  ui.info(`id=${fm.id} file=${path33.relative(root, topicMatch.filePath)}`);
9049
+ ui.info("Tip: `haive stats --export-report` generates a usage JSON suitable for dashboards.");
8706
9050
  }
8707
9051
  return;
8708
9052
  }
@@ -8725,6 +9069,7 @@ function registerSessionEnd(session2) {
8725
9069
  ui.success(`Session recap created`);
8726
9070
  ui.info(`id=${frontmatter.id} scope=${scope} file=${path33.relative(root, file)}`);
8727
9071
  ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
9072
+ ui.info("Tip: export a local MCP usage rollup with `haive stats --export-report .ai/tool-usage-roi-report.json`.");
8728
9073
  }
8729
9074
  });
8730
9075
  }
@@ -8734,7 +9079,7 @@ function parseCsv5(value) {
8734
9079
  }
8735
9080
 
8736
9081
  // src/commands/snapshot.ts
8737
- import { existsSync as existsSync51 } from "fs";
9082
+ import { existsSync as existsSync54 } from "fs";
8738
9083
  import { readdir as readdir4 } from "fs/promises";
8739
9084
  import path34 from "path";
8740
9085
  import "commander";
@@ -8769,14 +9114,14 @@ function registerSnapshot(program2) {
8769
9114
  ).option("--diff", "compare the contract against its stored snapshot").option("--list", "list all stored contract snapshots").option("-d, --dir <dir>", "project root").action(async (opts) => {
8770
9115
  const root = findProjectRoot32(opts.dir);
8771
9116
  const paths = resolveHaivePaths29(root);
8772
- if (!existsSync51(paths.haiveDir)) {
9117
+ if (!existsSync54(paths.haiveDir)) {
8773
9118
  ui.error("No .ai/ found. Run `haive init` first.");
8774
9119
  process.exitCode = 1;
8775
9120
  return;
8776
9121
  }
8777
9122
  if (opts.list) {
8778
9123
  const contractsDir = path34.join(paths.haiveDir, "contracts");
8779
- if (!existsSync51(contractsDir)) {
9124
+ if (!existsSync54(contractsDir)) {
8780
9125
  console.log(ui.dim("No contract snapshots found."));
8781
9126
  return;
8782
9127
  }
@@ -8900,7 +9245,7 @@ function detectFormat(filePath) {
8900
9245
  }
8901
9246
 
8902
9247
  // src/commands/hub.ts
8903
- import { existsSync as existsSync53 } from "fs";
9248
+ import { existsSync as existsSync55 } from "fs";
8904
9249
  import { mkdir as mkdir14, readFile as readFile15, writeFile as writeFile26, copyFile } from "fs/promises";
8905
9250
  import path35 from "path";
8906
9251
  import { spawnSync as spawnSync3 } from "child_process";
@@ -8908,7 +9253,7 @@ import "commander";
8908
9253
  import {
8909
9254
  findProjectRoot as findProjectRoot33,
8910
9255
  loadConfig as loadConfig6,
8911
- loadMemoriesFromDir as loadMemoriesFromDir27,
9256
+ loadMemoriesFromDir as loadMemoriesFromDir28,
8912
9257
  resolveHaivePaths as resolveHaivePaths30,
8913
9258
  saveConfig as saveConfig2,
8914
9259
  serializeMemory as serializeMemory23
@@ -9002,7 +9347,7 @@ Next steps:
9002
9347
  return;
9003
9348
  }
9004
9349
  const hubRoot = path35.resolve(root, config.hubPath);
9005
- if (!existsSync53(hubRoot)) {
9350
+ if (!existsSync55(hubRoot)) {
9006
9351
  ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
9007
9352
  process.exitCode = 1;
9008
9353
  return;
@@ -9010,7 +9355,7 @@ Next steps:
9010
9355
  const projectName = path35.basename(root);
9011
9356
  const destDir = path35.join(hubRoot, ".ai", "memories", "shared", projectName);
9012
9357
  await mkdir14(destDir, { recursive: true });
9013
- const all = await loadMemoriesFromDir27(paths.memoriesDir);
9358
+ const all = await loadMemoriesFromDir28(paths.memoriesDir);
9014
9359
  const shared = all.filter(
9015
9360
  ({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && // Don't push imported memories (avoid echo loops)
9016
9361
  !memory2.frontmatter.tags.some((t) => t.startsWith("cross-repo:"))
@@ -9072,7 +9417,7 @@ Next steps:
9072
9417
  }
9073
9418
  const hubRoot = path35.resolve(root, config.hubPath);
9074
9419
  const hubSharedDir = path35.join(hubRoot, ".ai", "memories", "shared");
9075
- if (!existsSync53(hubSharedDir)) {
9420
+ if (!existsSync55(hubSharedDir)) {
9076
9421
  ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
9077
9422
  return;
9078
9423
  }
@@ -9127,7 +9472,7 @@ Next steps:
9127
9472
  ` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
9128
9473
  );
9129
9474
  const sharedDir = path35.join(paths.memoriesDir, "shared");
9130
- if (existsSync53(sharedDir)) {
9475
+ if (existsSync55(sharedDir)) {
9131
9476
  const { readdir: readdir5 } = await import("fs/promises");
9132
9477
  const sources = (await readdir5(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
9133
9478
  console.log(`
@@ -9139,7 +9484,7 @@ Next steps:
9139
9484
  } else {
9140
9485
  console.log(ui.dim(" No imported shared memories yet."));
9141
9486
  }
9142
- const all = await loadMemoriesFromDir27(paths.memoriesDir);
9487
+ const all = await loadMemoriesFromDir28(paths.memoriesDir);
9143
9488
  const outgoing = all.filter(
9144
9489
  ({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && !memory2.frontmatter.tags.some((t) => t.startsWith("cross-repo:"))
9145
9490
  );
@@ -9156,9 +9501,13 @@ Next steps:
9156
9501
 
9157
9502
  // src/commands/stats.ts
9158
9503
  import "commander";
9504
+ import { existsSync as existsSync56 } from "fs";
9505
+ import { mkdir as mkdir15, writeFile as writeFile27 } from "fs/promises";
9506
+ import path36 from "path";
9159
9507
  import {
9160
9508
  aggregateUsage,
9161
9509
  findProjectRoot as findProjectRoot34,
9510
+ loadMemoriesFromDir as loadMemoriesFromDir29,
9162
9511
  loadUsageIndex as loadUsageIndex23,
9163
9512
  parseSince,
9164
9513
  readUsageEvents as readUsageEvents2,
@@ -9166,9 +9515,17 @@ import {
9166
9515
  usageLogSize
9167
9516
  } from "@hiveai/core";
9168
9517
  function registerStats(program2) {
9169
- program2.command("stats").description("Show MCP tool-usage stats over a window (e.g. --since 7d).").option("--since <window>", "ISO date or relative (e.g. '7d', '24h', '30m')", "30d").option("--json", "emit JSON instead of human-readable output", false).option("--memory-hits", "show top-read memories (which mems are actually being used)", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
9518
+ program2.command("stats").description("Show MCP tool-usage stats over a window (e.g. --since 7d).").option("--since <window>", "ISO date or relative (e.g. '7d', '24h', '30m')", "30d").option("--json", "emit JSON instead of human-readable output", false).option("--memory-hits", "show top-read memories (which mems are actually being used)", false).option(
9519
+ "--export-report <path>",
9520
+ "write a JSON rollup (tools + briefing counts + heuristic ROI hints). Parent dirs are created if needed.",
9521
+ void 0
9522
+ ).option("-d, --dir <dir>", "project root").action(async (opts) => {
9170
9523
  const root = findProjectRoot34(opts.dir);
9171
9524
  const paths = resolveHaivePaths31(root);
9525
+ if (opts.exportReport) {
9526
+ await writeRoiReport(paths, root, opts.since ?? "30d", opts.exportReport);
9527
+ return;
9528
+ }
9172
9529
  if (opts.memoryHits) {
9173
9530
  await renderMemoryHits(paths, opts);
9174
9531
  return;
@@ -9217,6 +9574,57 @@ function registerStats(program2) {
9217
9574
  }
9218
9575
  });
9219
9576
  }
9577
+ async function writeRoiReport(paths, root, sinceRaw, outRelative) {
9578
+ const outAbs = path36.isAbsolute(outRelative) ? path36.resolve(outRelative) : path36.resolve(root, outRelative);
9579
+ const size = await usageLogSize(paths);
9580
+ let events = await readUsageEvents2(paths);
9581
+ let memoryCount = { team: 0, personal: 0, total_skipped_session: 0 };
9582
+ if (existsSync56(paths.memoriesDir)) {
9583
+ const mems = await loadMemoriesFromDir29(paths.memoriesDir);
9584
+ for (const { memory: memory2 } of mems) {
9585
+ const fm = memory2.frontmatter;
9586
+ if (fm.type === "session_recap") memoryCount.total_skipped_session++;
9587
+ else if (fm.scope === "team") memoryCount.team++;
9588
+ else if (fm.scope === "personal") memoryCount.personal++;
9589
+ }
9590
+ }
9591
+ const sinceDt = parseSince(sinceRaw) ?? void 0;
9592
+ const aggregate = aggregateUsage(events, sinceDt);
9593
+ const inWindow = (at) => sinceDt === void 0 || Date.parse(at) >= sinceDt.getTime();
9594
+ const briefingCalls = events.filter((e) => inWindow(e.at) && e.tool === "get_briefing").length;
9595
+ let memoryHitsLeader = null;
9596
+ try {
9597
+ const usageIdx = await loadUsageIndex23(paths);
9598
+ const tops = Object.entries(usageIdx.by_id).map(([id, v]) => ({ id, read_count: v.read_count })).filter((x) => x.read_count > 0).sort((a, b) => b.read_count - a.read_count);
9599
+ memoryHitsLeader = tops[0] ?? null;
9600
+ } catch {
9601
+ memoryHitsLeader = null;
9602
+ }
9603
+ const roiHints = [
9604
+ "Correlate get_briefing calls with skipped multi-file exploration \u2014 proxies available via `pnpm benchmark:roi` at repo root.",
9605
+ "Prefer get_briefing(format:'actions') or budget_preset:'quick' for low-risk edits to reduce token pressure.",
9606
+ "Run `haive memory lint` in CI to keep the corpus actionable.",
9607
+ "Install the haive VS Code extension (packages/vscode) for always-on memory surfacing beside the editor."
9608
+ ];
9609
+ if (!size.exists || events.length === 0) {
9610
+ ui.warn("Usage log missing or empty \u2014 report still written with partial data.");
9611
+ events = [];
9612
+ }
9613
+ await mkdir15(path36.dirname(outAbs), { recursive: true });
9614
+ const payload = {
9615
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
9616
+ project_root: root,
9617
+ window_since: sinceRaw,
9618
+ usage_log_meta: size,
9619
+ memory_inventory: memoryCount,
9620
+ aggregate,
9621
+ get_briefing_calls_in_window: briefingCalls,
9622
+ top_memory_reads: memoryHitsLeader,
9623
+ roi_hints: roiHints
9624
+ };
9625
+ await writeFile27(outAbs, JSON.stringify(payload, null, 2), "utf8");
9626
+ ui.success(`Wrote ROI / usage rollup \u2192 ${outAbs}`);
9627
+ }
9220
9628
  async function renderMemoryHits(paths, opts) {
9221
9629
  const index = await loadUsageIndex23(paths);
9222
9630
  const since = parseSince(opts.since ?? "30d");
@@ -9391,15 +9799,15 @@ function summarize(name, t0, payload, notes) {
9391
9799
  }
9392
9800
 
9393
9801
  // src/commands/memory-suggest.ts
9394
- import { mkdir as mkdir15, writeFile as writeFile27 } from "fs/promises";
9395
- import { existsSync as existsSync54 } from "fs";
9396
- import path36 from "path";
9802
+ import { mkdir as mkdir16, writeFile as writeFile28 } from "fs/promises";
9803
+ import { existsSync as existsSync57 } from "fs";
9804
+ import path37 from "path";
9397
9805
  import "commander";
9398
9806
  import {
9399
9807
  aggregateUsage as aggregateUsage2,
9400
9808
  buildFrontmatter as buildFrontmatter11,
9401
9809
  findProjectRoot as findProjectRoot36,
9402
- loadMemoriesFromDir as loadMemoriesFromDir28,
9810
+ loadMemoriesFromDir as loadMemoriesFromDir30,
9403
9811
  memoryFilePath as memoryFilePath10,
9404
9812
  parseSince as parseSince2,
9405
9813
  readUsageEvents as readUsageEvents3,
@@ -9463,7 +9871,7 @@ function registerMemorySuggest(memory2) {
9463
9871
  }
9464
9872
  const created = [];
9465
9873
  const skipped = [];
9466
- const existing = existsSync54(paths.memoriesDir) ? await loadMemoriesFromDir28(paths.memoriesDir) : [];
9874
+ const existing = existsSync57(paths.memoriesDir) ? await loadMemoriesFromDir30(paths.memoriesDir) : [];
9467
9875
  for (const s of top) {
9468
9876
  const slug = slugify(s.query);
9469
9877
  if (!slug) {
@@ -9486,13 +9894,13 @@ function registerMemorySuggest(memory2) {
9486
9894
  fm.status = "draft";
9487
9895
  const body = renderTemplate(s);
9488
9896
  const file = memoryFilePath10(paths, fm.scope, fm.id, fm.module);
9489
- await mkdir15(path36.dirname(file), { recursive: true });
9490
- if (existsSync54(file)) {
9491
- skipped.push({ query: s.query, reason: `file already exists at ${path36.relative(root, file)}` });
9897
+ await mkdir16(path37.dirname(file), { recursive: true });
9898
+ if (existsSync57(file)) {
9899
+ skipped.push({ query: s.query, reason: `file already exists at ${path37.relative(root, file)}` });
9492
9900
  continue;
9493
9901
  }
9494
- await writeFile27(file, serializeMemory24({ frontmatter: fm, body }), "utf8");
9495
- created.push({ id: fm.id, file: path36.relative(root, file), query: s.query });
9902
+ await writeFile28(file, serializeMemory24({ frontmatter: fm, body }), "utf8");
9903
+ created.push({ id: fm.id, file: path37.relative(root, file), query: s.query });
9496
9904
  }
9497
9905
  if (opts.json) {
9498
9906
  console.log(JSON.stringify({ created, skipped }, null, 2));
@@ -9585,14 +9993,14 @@ function truncate2(text, max) {
9585
9993
  }
9586
9994
 
9587
9995
  // src/commands/memory-archive.ts
9588
- import { existsSync as existsSync55 } from "fs";
9589
- import { writeFile as writeFile28 } from "fs/promises";
9590
- import path37 from "path";
9996
+ import { existsSync as existsSync58 } from "fs";
9997
+ import { writeFile as writeFile29 } from "fs/promises";
9998
+ import path38 from "path";
9591
9999
  import "commander";
9592
10000
  import {
9593
10001
  findProjectRoot as findProjectRoot37,
9594
10002
  getUsage as getUsage18,
9595
- loadMemoriesFromDir as loadMemoriesFromDir29,
10003
+ loadMemoriesFromDir as loadMemoriesFromDir31,
9596
10004
  loadUsageIndex as loadUsageIndex24,
9597
10005
  resolveHaivePaths as resolveHaivePaths34,
9598
10006
  serializeMemory as serializeMemory25
@@ -9604,7 +10012,7 @@ function registerMemoryArchive(memory2) {
9604
10012
  ).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) => {
9605
10013
  const root = findProjectRoot37(opts.dir);
9606
10014
  const paths = resolveHaivePaths34(root);
9607
- if (!existsSync55(paths.memoriesDir)) {
10015
+ if (!existsSync58(paths.memoriesDir)) {
9608
10016
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
9609
10017
  process.exitCode = 1;
9610
10018
  return;
@@ -9616,7 +10024,7 @@ function registerMemoryArchive(memory2) {
9616
10024
  return;
9617
10025
  }
9618
10026
  const cutoff = Date.now() - minDays * MS_PER_DAY2;
9619
- const all = await loadMemoriesFromDir29(paths.memoriesDir);
10027
+ const all = await loadMemoriesFromDir31(paths.memoriesDir);
9620
10028
  const usage = await loadUsageIndex24(paths);
9621
10029
  const typeFilter = opts.type === "all" ? null : opts.type ?? "attempt";
9622
10030
  const candidates = [];
@@ -9625,7 +10033,7 @@ function registerMemoryArchive(memory2) {
9625
10033
  if (typeFilter && fm.type !== typeFilter) continue;
9626
10034
  if (fm.status === "deprecated" || fm.status === "rejected") continue;
9627
10035
  const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
9628
- const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync55(path37.join(paths.root, p)));
10036
+ const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync58(path38.join(paths.root, p)));
9629
10037
  const isAnchorless = !hasAnyAnchor;
9630
10038
  if (!isAnchorless && !allPathsGone) continue;
9631
10039
  const u = getUsage18(usage, fm.id);
@@ -9673,7 +10081,7 @@ function registerMemoryArchive(memory2) {
9673
10081
  if (!found) continue;
9674
10082
  const fm = { ...found.memory.frontmatter, status: "deprecated" };
9675
10083
  try {
9676
- await writeFile28(c.filePath, serializeMemory25({ frontmatter: fm, body: found.memory.body }), "utf8");
10084
+ await writeFile29(c.filePath, serializeMemory25({ frontmatter: fm, body: found.memory.body }), "utf8");
9677
10085
  archived++;
9678
10086
  } catch (err) {
9679
10087
  if (!opts.json) {
@@ -9699,7 +10107,7 @@ function parseDays(input) {
9699
10107
  }
9700
10108
 
9701
10109
  // src/commands/doctor.ts
9702
- import { existsSync as existsSync56 } from "fs";
10110
+ import { existsSync as existsSync59 } from "fs";
9703
10111
  import { stat } from "fs/promises";
9704
10112
  import "path";
9705
10113
  import { execSync as execSync3 } from "child_process";
@@ -9710,7 +10118,7 @@ import {
9710
10118
  getUsage as getUsage19,
9711
10119
  loadCodeMap as loadCodeMap5,
9712
10120
  loadConfig as loadConfig7,
9713
- loadMemoriesFromDir as loadMemoriesFromDir30,
10121
+ loadMemoriesFromDir as loadMemoriesFromDir32,
9714
10122
  loadUsageIndex as loadUsageIndex25,
9715
10123
  readUsageEvents as readUsageEvents4,
9716
10124
  resolveHaivePaths as resolveHaivePaths35
@@ -9723,7 +10131,7 @@ function registerDoctor(program2) {
9723
10131
  const root = findProjectRoot38(opts.dir);
9724
10132
  const paths = resolveHaivePaths35(root);
9725
10133
  const findings = [];
9726
- if (!existsSync56(paths.haiveDir)) {
10134
+ if (!existsSync59(paths.haiveDir)) {
9727
10135
  findings.push({
9728
10136
  severity: "error",
9729
10137
  code: "not-initialized",
@@ -9732,7 +10140,7 @@ function registerDoctor(program2) {
9732
10140
  });
9733
10141
  return emit(findings, opts);
9734
10142
  }
9735
- if (!existsSync56(paths.projectContext)) {
10143
+ if (!existsSync59(paths.projectContext)) {
9736
10144
  findings.push({
9737
10145
  severity: "warn",
9738
10146
  code: "no-project-context",
@@ -9752,7 +10160,7 @@ function registerDoctor(program2) {
9752
10160
  });
9753
10161
  }
9754
10162
  }
9755
- const memories = existsSync56(paths.memoriesDir) ? await loadMemoriesFromDir30(paths.memoriesDir) : [];
10163
+ const memories = existsSync59(paths.memoriesDir) ? await loadMemoriesFromDir32(paths.memoriesDir) : [];
9756
10164
  const now = Date.now();
9757
10165
  if (memories.length === 0) {
9758
10166
  findings.push({
@@ -9876,7 +10284,7 @@ function registerDoctor(program2) {
9876
10284
  timeout: 3e3,
9877
10285
  stdio: ["ignore", "pipe", "ignore"]
9878
10286
  }).trim();
9879
- const cliVersion = "0.9.2";
10287
+ const cliVersion = "0.9.5";
9880
10288
  if (legacyRaw && legacyRaw !== cliVersion) {
9881
10289
  findings.push({
9882
10290
  severity: "warn",
@@ -9925,11 +10333,11 @@ function isSearchTool(name) {
9925
10333
  }
9926
10334
 
9927
10335
  // src/commands/playback.ts
9928
- import { existsSync as existsSync57 } from "fs";
10336
+ import { existsSync as existsSync60 } from "fs";
9929
10337
  import "commander";
9930
10338
  import {
9931
10339
  findProjectRoot as findProjectRoot39,
9932
- loadMemoriesFromDir as loadMemoriesFromDir31,
10340
+ loadMemoriesFromDir as loadMemoriesFromDir33,
9933
10341
  parseSince as parseSince3,
9934
10342
  readUsageEvents as readUsageEvents5,
9935
10343
  resolveHaivePaths as resolveHaivePaths36
@@ -9955,7 +10363,7 @@ function registerPlayback(program2) {
9955
10363
  const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
9956
10364
  const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
9957
10365
  const sessions = bucketSessions(filtered, gapMs);
9958
- const all = existsSync57(paths.memoriesDir) ? await loadMemoriesFromDir31(paths.memoriesDir) : [];
10366
+ const all = existsSync60(paths.memoriesDir) ? await loadMemoriesFromDir33(paths.memoriesDir) : [];
9959
10367
  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);
9960
10368
  const enriched = sessions.map((s, i) => {
9961
10369
  const startMs = Date.parse(s.start);
@@ -10148,10 +10556,204 @@ function runCommand3(cmd, args, cwd) {
10148
10556
  });
10149
10557
  }
10150
10558
 
10559
+ // src/commands/welcome.ts
10560
+ import { existsSync as existsSync61 } from "fs";
10561
+ import "commander";
10562
+ import {
10563
+ findProjectRoot as findProjectRoot41,
10564
+ loadMemoriesFromDir as loadMemoriesFromDir34,
10565
+ resolveHaivePaths as resolveHaivePaths38
10566
+ } from "@hiveai/core";
10567
+ var TYPE_RANK = {
10568
+ decision: 0,
10569
+ architecture: 1,
10570
+ convention: 2,
10571
+ glossary: 3,
10572
+ gotcha: 4,
10573
+ attempt: 5
10574
+ };
10575
+ function registerWelcome(program2) {
10576
+ program2.command("welcome").description(
10577
+ "Onboarding checklist: ranks validated/proposed **team** memories by type.\nUse after `haive init` so new devs skim institutional knowledge quickly.\n\n haive welcome\n haive welcome --limit 15\n"
10578
+ ).option("--limit <n>", "maximum memories listed", "20").option("-d, --dir <dir>", "project root").action(async (opts) => {
10579
+ const root = findProjectRoot41(opts.dir);
10580
+ const paths = resolveHaivePaths38(root);
10581
+ if (!existsSync61(paths.memoriesDir)) {
10582
+ ui.error(`No memories at ${paths.memoriesDir}. Run 'haive init' first.`);
10583
+ process.exitCode = 1;
10584
+ return;
10585
+ }
10586
+ const all = await loadMemoriesFromDir34(paths.memoriesDir);
10587
+ const team = all.filter(
10588
+ ({ memory: memory2 }) => memory2.frontmatter.scope === "team" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && memory2.frontmatter.status !== "stale" && memory2.frontmatter.type !== "session_recap"
10589
+ );
10590
+ team.sort((a, b) => {
10591
+ const ta = TYPE_RANK[a.memory.frontmatter.type] ?? 99;
10592
+ const tb = TYPE_RANK[b.memory.frontmatter.type] ?? 99;
10593
+ if (ta !== tb) return ta - tb;
10594
+ const sta = a.memory.frontmatter.status === "validated" ? 0 : 1;
10595
+ const stb = b.memory.frontmatter.status === "validated" ? 0 : 1;
10596
+ if (sta !== stb) return sta - stb;
10597
+ return b.memory.frontmatter.created_at.localeCompare(a.memory.frontmatter.created_at);
10598
+ });
10599
+ const cap = Math.max(1, Math.min(500, Number(opts.limit) || 20));
10600
+ const pick = team.slice(0, cap);
10601
+ console.log(ui.bold(`hAIve welcome \u2014 ${pick.length} team memories (${root})`));
10602
+ console.log(ui.dim(`Next: invoke get_briefing with your task or run 'haive briefing --task "\u2026"'`));
10603
+ if (pick.length === 0) {
10604
+ ui.warn("No team memories yet \u2014 add some with 'haive memory add' or promote personal ones.");
10605
+ return;
10606
+ }
10607
+ let i = 1;
10608
+ for (const { memory: memory2 } of pick) {
10609
+ const fm = memory2.frontmatter;
10610
+ const head = memory2.body.match(/^#\s+(.+)/m)?.[1]?.trim();
10611
+ const line = head ?? fm.id;
10612
+ console.log(
10613
+ `${String(i).padStart(2, " ")} ${fm.type.padEnd(12)} ${fm.status.padEnd(10)} ${ui.dim(fm.id)}
10614
+ ${line}`
10615
+ );
10616
+ i++;
10617
+ }
10618
+ });
10619
+ }
10620
+
10621
+ // src/commands/memory-lint.ts
10622
+ import { existsSync as existsSync63 } from "fs";
10623
+ import "commander";
10624
+ import {
10625
+ findProjectRoot as findProjectRoot42,
10626
+ loadMemoriesFromDir as loadMemoriesFromDir35,
10627
+ resolveHaivePaths as resolveHaivePaths39
10628
+ } from "@hiveai/core";
10629
+ async function lintMemoriesAsync(root) {
10630
+ const paths = resolveHaivePaths39(root);
10631
+ const out = [];
10632
+ if (!existsSync63(paths.memoriesDir)) return out;
10633
+ const loaded = await loadMemoriesFromDir35(paths.memoriesDir);
10634
+ const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
10635
+ for (const { filePath, memory: memory2 } of loaded) {
10636
+ const fm = memory2.frontmatter;
10637
+ if (fm.type === "session_recap") continue;
10638
+ const body = memory2.body.trim();
10639
+ const naked = body.replace(/^#.*$/gm, "").replace(/```[\s\S]*?```/g, "").trim();
10640
+ if (naked.length < 40 && fm.status !== "rejected") {
10641
+ out.push({
10642
+ file: filePath,
10643
+ id: fm.id,
10644
+ severity: "warn",
10645
+ code: "SHORT_BODY",
10646
+ message: "Body looks very short (< ~40 chars of prose after headings). Prefer actionable detail."
10647
+ });
10648
+ }
10649
+ if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated") {
10650
+ out.push({
10651
+ file: filePath,
10652
+ id: fm.id,
10653
+ severity: "warn",
10654
+ code: "MISSING_ANCHOR",
10655
+ message: `${fm.type} is validated without anchor paths \u2014 add anchor.paths so haive sync can flag staleness.`
10656
+ });
10657
+ }
10658
+ if (fm.status === "stale" && !fm.stale_reason) {
10659
+ out.push({
10660
+ file: filePath,
10661
+ id: fm.id,
10662
+ severity: "info",
10663
+ code: "STALE_NO_REASON",
10664
+ message: "Status is stale but stale_reason is empty \u2014 document why when possible."
10665
+ });
10666
+ }
10667
+ if (fm.type === "glossary" && naked.length > 6e3) {
10668
+ out.push({
10669
+ file: filePath,
10670
+ id: fm.id,
10671
+ severity: "info",
10672
+ code: "LONG_GLOSSARY",
10673
+ message: "Very long glossary \u2014 consider splitting concepts for tighter briefings."
10674
+ });
10675
+ }
10676
+ const hasMarkdownHeading = /^#{1,3}\s+\S/m.test(memory2.body.trim());
10677
+ if (!hasMarkdownHeading) {
10678
+ out.push({
10679
+ file: filePath,
10680
+ id: fm.id,
10681
+ severity: "warn",
10682
+ code: "NO_MD_HEADING",
10683
+ message: "No Markdown heading (#/##/###) \u2014 add one so humans and auditors can skim the memo quickly."
10684
+ });
10685
+ }
10686
+ }
10687
+ return out;
10688
+ }
10689
+ function registerMemoryLint(parent) {
10690
+ parent.command("lint").description(
10691
+ "Heuristic corpus checks (anchors on key types, headings, verbosity). Static analysis only."
10692
+ ).option("--json", "emit findings as JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
10693
+ const root = findProjectRoot42(opts.dir);
10694
+ const findings = await lintMemoriesAsync(root);
10695
+ if (opts.json) {
10696
+ console.log(JSON.stringify({ findings_count: findings.length, findings }, null, 2));
10697
+ process.exitCode = findings.some((f) => f.severity === "error") ? 1 : 0;
10698
+ return;
10699
+ }
10700
+ if (findings.length === 0) {
10701
+ ui.success(`memory lint OK \u2014 ${root}`);
10702
+ return;
10703
+ }
10704
+ console.log(ui.bold(`memory lint (${findings.length} finding${findings.length === 1 ? "" : "s"})`) + `
10705
+ `);
10706
+ const order = { error: 0, warn: 1, info: 2 };
10707
+ findings.sort((a, b) => order[a.severity] - order[b.severity] || a.id.localeCompare(b.id));
10708
+ for (const f of findings) {
10709
+ const color = f.severity === "error" ? ui.red : f.severity === "warn" ? ui.yellow : ui.dim;
10710
+ console.log(
10711
+ `${color(f.severity.padEnd(5))} ${ui.dim(f.code)} ${f.id}`
10712
+ );
10713
+ console.log(` ${f.message}`);
10714
+ console.log(ui.dim(` \u2192 ${f.file}`));
10715
+ }
10716
+ process.exitCode = findings.some((x) => x.severity === "error") ? 1 : 0;
10717
+ });
10718
+ }
10719
+
10720
+ // src/commands/memory-suggest-topic.ts
10721
+ import "commander";
10722
+ import { MemoryTypeSchema as MemoryTypeSchema2, suggestTopicKey as suggestTopicKey2 } from "@hiveai/core";
10723
+ function registerMemorySuggestTopic(memory2) {
10724
+ memory2.command("suggest-topic").description("Suggest a stable topic key (topic-upsert) from type + title phrase").requiredOption(
10725
+ "--type <type>",
10726
+ "convention | decision | gotcha | architecture | glossary | attempt | session_recap"
10727
+ ).argument("<title>", "Short title or phrase to slugify").action((title, opts) => {
10728
+ const parsed = MemoryTypeSchema2.safeParse(opts.type);
10729
+ if (!parsed.success) {
10730
+ ui.error(`Invalid type: ${opts.type}`);
10731
+ process.exit(1);
10732
+ }
10733
+ const suggestion = suggestTopicKey2(parsed.data, title);
10734
+ console.log(JSON.stringify({ type: parsed.data, ...suggestion }, null, 2));
10735
+ });
10736
+ }
10737
+
10738
+ // src/commands/resolve-project.ts
10739
+ import path40 from "path";
10740
+ import "commander";
10741
+ import { resolveProjectInfo as resolveProjectInfo2 } from "@hiveai/core";
10742
+ function registerResolveProject(program2) {
10743
+ program2.command("resolve-project").description(
10744
+ "Print JSON for hAIve project root resolution (HAIVE_PROJECT_ROOT, markers, .ai layout)."
10745
+ ).option("-d, --dir <dir>", "working directory", process.cwd()).action((opts) => {
10746
+ const info = resolveProjectInfo2({ cwd: path40.resolve(opts.dir) });
10747
+ console.log(JSON.stringify({ ok: true, info }, null, 2));
10748
+ });
10749
+ }
10750
+
10151
10751
  // src/index.ts
10152
- var program = new Command40();
10153
- program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.9.2");
10752
+ var program = new Command44();
10753
+ program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.9.5");
10154
10754
  registerInit(program);
10755
+ registerWelcome(program);
10756
+ registerResolveProject(program);
10155
10757
  registerMcp(program);
10156
10758
  registerBriefing(program);
10157
10759
  registerTui(program);
@@ -10182,7 +10784,9 @@ registerMemoryImport(memory);
10182
10784
  registerMemoryImportChangelog(memory);
10183
10785
  registerMemoryDigest(memory);
10184
10786
  registerMemorySuggest(memory);
10787
+ registerMemorySuggestTopic(memory);
10185
10788
  registerMemoryArchive(memory);
10789
+ registerMemoryLint(memory);
10186
10790
  var session = program.command("session").description("Manage session lifecycle");
10187
10791
  registerSessionEnd(session);
10188
10792
  registerSnapshot(program);