@hiveai/cli 0.9.3 → 0.9.6

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 Command42 } from "commander";
4
+ import { Command as Command47 } from "commander";
5
5
 
6
6
  // src/commands/briefing.ts
7
7
  import { existsSync } from "fs";
@@ -197,7 +197,7 @@ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
197
197
  if (!f) continue;
198
198
  counts.set(f, (counts.get(f) ?? 0) + 1);
199
199
  }
200
- let entries = [...counts.entries()].map(([path40, changes]) => ({ path: path40, changes }));
200
+ let entries = [...counts.entries()].map(([path45, changes]) => ({ path: path45, changes }));
201
201
  const lowerPaths = filePaths.map((p) => p.toLowerCase());
202
202
  if (lowerPaths.length > 0) {
203
203
  entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
@@ -1882,6 +1882,7 @@ var PROJECT_CONTEXT_TEMPLATE = `# Project context
1882
1882
 
1883
1883
  > Generated by \`haive init\`. Run \`haive init --bootstrap\` to auto-fill from your codebase,
1884
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\`.
1885
1886
 
1886
1887
  ## Architecture
1887
1888
  TODO \u2014 high-level overview of the codebase.
@@ -1906,16 +1907,47 @@ Memories live under \`.ai/memories/\` (personal/team/module).
1906
1907
 
1907
1908
  ## hAIve \u2014 mandatory rules for AI agents
1908
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
+
1909
1920
  ### Session rules
1910
- - **Before** marking a task as done ("Done", "Finished", "C'est fait"), call the MCP prompt \`post_task\`.
1911
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.
1912
- - When starting a new task, call \`get_briefing\` with the task description to load relevant memories and avoid repeating past mistakes.
1913
1922
 
1914
1923
  ### Safety rules \u2014 NEVER violate these
1915
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.
1916
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.
1917
1926
  - When in doubt about a cross-repo change: ask, don't act.
1918
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
+ `;
1919
1951
  var CI_WORKFLOW = `name: haive-sync
1920
1952
 
1921
1953
  on:
@@ -2036,7 +2068,7 @@ jobs:
2036
2068
  function registerInit(program2) {
2037
2069
  program2.command("init").description(
2038
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."
2039
- ).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(
2040
2072
  "--manual",
2041
2073
  "opt out of autopilot: memories require manual approval, no auto-session recap, no auto-context"
2042
2074
  ).option(
@@ -2066,6 +2098,7 @@ function registerInit(program2) {
2066
2098
  await mkdir3(paths.teamDir, { recursive: true });
2067
2099
  await mkdir3(paths.moduleDir, { recursive: true });
2068
2100
  await mkdir3(paths.modulesContextDir, { recursive: true });
2101
+ await ensureAiRuntimeLayout(paths.runtimeDir);
2069
2102
  if (!existsSync6(paths.projectContext)) {
2070
2103
  if (wantBootstrap) {
2071
2104
  ui.info("Bootstrapping project context from local files\u2026");
@@ -2095,6 +2128,7 @@ function registerInit(program2) {
2095
2128
  await writeBridge(root, "CLAUDE.md");
2096
2129
  await writeBridge(root, ".cursorrules");
2097
2130
  await writeBridge(root, path7.join(".github", "copilot-instructions.md"));
2131
+ await writeCursorHaiveRule(root);
2098
2132
  }
2099
2133
  const stacksToSeed = await resolveStacksToSeed(root, wantStack);
2100
2134
  if (stacksToSeed.length > 0) {
@@ -2237,6 +2271,17 @@ async function resolveStacksToSeed(root, stackOpt) {
2237
2271
  }
2238
2272
  return stackOpt.split(",").map((s) => s.trim().toLowerCase()).filter(isValidStack);
2239
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
+ }
2240
2285
  async function writeBridge(root, relPath) {
2241
2286
  const target = path7.join(root, relPath);
2242
2287
  if (existsSync6(target)) {
@@ -2247,6 +2292,29 @@ async function writeBridge(root, relPath) {
2247
2292
  await writeFile3(target, BRIDGE_BODY, "utf8");
2248
2293
  ui.success(`Created bridge ${relPath}`);
2249
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
+ Session continuity (local): agents may append \`session-journal.ndjson\` via MCP \`runtime_journal_append\` or \`haive runtime journal append\`.
2302
+ `;
2303
+ var RUNTIME_GITIGNORE_BODY = `*
2304
+ !.gitignore
2305
+ !README.md
2306
+ `;
2307
+ async function ensureAiRuntimeLayout(runtimeDir) {
2308
+ await mkdir3(runtimeDir, { recursive: true });
2309
+ const gi = path7.join(runtimeDir, ".gitignore");
2310
+ if (!existsSync6(gi)) {
2311
+ await writeFile3(gi, RUNTIME_GITIGNORE_BODY, "utf8");
2312
+ }
2313
+ const readme = path7.join(runtimeDir, "README.md");
2314
+ if (!existsSync6(readme)) {
2315
+ await writeFile3(readme, RUNTIME_README_BODY, "utf8");
2316
+ }
2317
+ }
2250
2318
  async function ensureGitignoreEntries(root, patterns) {
2251
2319
  try {
2252
2320
  const gitignorePath = path7.join(root, ".gitignore");
@@ -2631,6 +2699,7 @@ import {
2631
2699
  loadMemoriesFromDir as loadMemoriesFromDir3,
2632
2700
  loadUsageIndex as loadUsageIndex2,
2633
2701
  pickSnippetNeedle,
2702
+ rankMemoriesLexical,
2634
2703
  tokenizeQuery as tokenizeQuery2,
2635
2704
  trackReads as trackReads2
2636
2705
  } from "@hiveai/core";
@@ -2728,7 +2797,11 @@ import {
2728
2797
  serializeMemory as serializeMemory8
2729
2798
  } from "@hiveai/core";
2730
2799
  import { z as z16 } from "zod";
2731
- import { appendUsageEvent, loadConfig as loadConfig2 } from "@hiveai/core";
2800
+ import {
2801
+ appendUsageEvent,
2802
+ appendRuntimeJournalEntry,
2803
+ loadConfig as loadConfig2
2804
+ } from "@hiveai/core";
2732
2805
  import { mkdir as mkdir52, writeFile as writeFile9, rm } from "fs/promises";
2733
2806
  import { existsSync as existsSync16 } from "fs";
2734
2807
  import path72 from "path";
@@ -2832,9 +2905,27 @@ import {
2832
2905
  serializeMemory as serializeMemory10
2833
2906
  } from "@hiveai/core";
2834
2907
  import { z as z29 } from "zod";
2908
+ import { existsSync as existsSync27 } from "fs";
2909
+ import {
2910
+ findLexicalConflictPairs,
2911
+ findTopicStatusConflictPairs,
2912
+ loadMemoriesFromDir as loadMemoriesFromDir21
2913
+ } from "@hiveai/core";
2835
2914
  import { z as z30 } from "zod";
2915
+ import { resolveProjectInfo } from "@hiveai/core";
2836
2916
  import { z as z31 } from "zod";
2917
+ import { MemoryTypeSchema, suggestTopicKey } from "@hiveai/core";
2837
2918
  import { z as z32 } from "zod";
2919
+ import { existsSync as existsSync28 } from "fs";
2920
+ import { collectTimelineEntries, loadMemoriesFromDir as loadMemoriesFromDir222 } from "@hiveai/core";
2921
+ import { z as z33 } from "zod";
2922
+ import { appendRuntimeJournalEntry as appendRuntimeJournalEntry2 } from "@hiveai/core";
2923
+ import { z as z34 } from "zod";
2924
+ import { readRuntimeJournalTail } from "@hiveai/core";
2925
+ import { z as z35 } from "zod";
2926
+ import { z as z36 } from "zod";
2927
+ import { z as z37 } from "zod";
2928
+ import { z as z38 } from "zod";
2838
2929
  function createContext(options = {}) {
2839
2930
  const env = options.env ?? process.env;
2840
2931
  const cwd = options.cwd ?? process.cwd();
@@ -3119,6 +3210,9 @@ var MemSearchInputSchema = {
3119
3210
  semantic: z5.boolean().default(false).describe(
3120
3211
  "Use semantic similarity from the embeddings index (requires `haive embeddings index`)."
3121
3212
  ),
3213
+ lexical_rank: z5.boolean().default(false).describe(
3214
+ "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."
3215
+ ),
3122
3216
  min_score: z5.number().min(0).max(1).default(0).describe("Minimum cosine similarity (semantic mode only)"),
3123
3217
  track: z5.boolean().default(true).describe("Increment read_count on returned memories (used for passive validation)")
3124
3218
  };
@@ -3144,6 +3238,27 @@ async function memSearch(input, ctx) {
3144
3238
  notice: "Semantic search unavailable (embeddings index missing or @hiveai/embeddings not installed). Falling back to literal search."
3145
3239
  };
3146
3240
  }
3241
+ } else if (input.lexical_rank && input.query.trim()) {
3242
+ const { ranked, scores } = rankMemoriesLexical(
3243
+ filtered,
3244
+ input.query,
3245
+ input.limit
3246
+ );
3247
+ if (ranked.length > 0) {
3248
+ const snippetNeedle = pickSnippetNeedle(input.query);
3249
+ result = {
3250
+ matches: ranked.map(
3251
+ (loaded, i) => lexicalHit(loaded, snippetNeedle, usage, scores[i])
3252
+ ),
3253
+ total: ranked.length,
3254
+ mode: "lexical_ranked"
3255
+ };
3256
+ } else {
3257
+ result = {
3258
+ ...buildLiteralResult(input, filtered, usage),
3259
+ notice: "Lexical ranking found no BM25-positive hits \u2014 showing literal matches instead."
3260
+ };
3261
+ }
3147
3262
  } else {
3148
3263
  result = buildLiteralResult(input, filtered, usage);
3149
3264
  }
@@ -3241,6 +3356,9 @@ function toHit(loaded, needle, usage) {
3241
3356
  file_path: loaded.filePath
3242
3357
  };
3243
3358
  }
3359
+ function lexicalHit(loaded, needle, usage, lexicalScore) {
3360
+ return { ...toHit(loaded, needle, usage), score: lexicalScore };
3361
+ }
3244
3362
  var MemVerifyInputSchema = {
3245
3363
  id: z6.string().optional().describe("If set, verify only this memory id"),
3246
3364
  update: z6.boolean().default(false).describe("Write the resulting status back to disk (status=stale or validated)")
@@ -3832,6 +3950,14 @@ var SessionTracker = class {
3832
3950
  recapId = result.id;
3833
3951
  } catch {
3834
3952
  }
3953
+ void appendRuntimeJournalEntry(this.ctx.paths, {
3954
+ kind: "session_end",
3955
+ message: recapId ? `auto session close | ${toolSummary} | recap:${recapId}` : `auto session close | ${toolSummary}`,
3956
+ meta: {
3957
+ recap_id: recapId ?? null,
3958
+ total_tool_calls: totalCalls
3959
+ }
3960
+ });
3835
3961
  const ranPostTask = this.events.some(
3836
3962
  (e) => e.tool === "mem_session_end" && !e.summary?.startsWith("Auto-captured")
3837
3963
  );
@@ -5572,11 +5698,107 @@ function gitFileDiff(root, file, sinceDays) {
5572
5698
  return null;
5573
5699
  }
5574
5700
  }
5701
+ var MemConflictCandidatesInputSchema = {
5702
+ since_days: z30.number().int().positive().max(3650).default(365).describe("Only memories created since N days ago"),
5703
+ types: z30.array(z30.enum(["decision", "architecture", "convention", "gotcha"])).default(["decision", "architecture"]).describe("Memory types scanned for pairwise lexical overlap"),
5704
+ min_jaccard: z30.number().min(0).max(1).default(0.45).describe("Minimum Jaccard token similarity to surface as a candidate pair"),
5705
+ max_pairs: z30.number().int().positive().max(100).default(20).describe("Cap pairs returned"),
5706
+ 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."),
5707
+ max_topic_pairs: z30.number().int().positive().max(100).default(20).describe(
5708
+ "Cap for extra signal: memories sharing the same topic with validated vs rejected status."
5709
+ )
5710
+ };
5711
+ async function memConflictCandidates(input, ctx) {
5712
+ if (!existsSync27(ctx.paths.memoriesDir)) {
5713
+ return {
5714
+ pairs: [],
5715
+ topic_status_pairs: [],
5716
+ scanned: 0,
5717
+ truncated: false,
5718
+ notice: "No .ai/memories directory."
5719
+ };
5720
+ }
5721
+ const all = await loadMemoriesFromDir21(ctx.paths.memoriesDir);
5722
+ const { pairs, scanned, truncated } = findLexicalConflictPairs(all, {
5723
+ sinceDays: input.since_days,
5724
+ types: input.types,
5725
+ minJaccard: input.min_jaccard,
5726
+ maxPairs: input.max_pairs,
5727
+ maxScan: input.max_scan
5728
+ });
5729
+ const topicStatusPairs = findTopicStatusConflictPairs(all, input.max_topic_pairs);
5730
+ const notice = pairs.length === 0 && topicStatusPairs.length === 0 ? "No lexical or topic-status candidates \u2014 widen since_days/types or lower min_jaccard." : void 0;
5731
+ return { pairs, topic_status_pairs: topicStatusPairs, scanned, truncated, notice };
5732
+ }
5733
+ var MemResolveProjectInputSchema = {
5734
+ cwd: z31.string().optional().describe("Directory used for root discovery when HAIVE_PROJECT_ROOT is unset.")
5735
+ };
5736
+ async function memResolveProject(input, _ctx) {
5737
+ void _ctx;
5738
+ return {
5739
+ ok: true,
5740
+ info: resolveProjectInfo({
5741
+ cwd: input.cwd
5742
+ })
5743
+ };
5744
+ }
5745
+ var MemSuggestTopicInputSchema = {
5746
+ type: MemoryTypeSchema.describe("Memory kind \u2014 drives the suggested topic family."),
5747
+ title: z32.string().min(1).describe("Short title or phrase (headers, headings) \u2014 turned into slug")
5748
+ };
5749
+ async function memSuggestTopic(input, _ctx) {
5750
+ void _ctx;
5751
+ const suggestion = suggestTopicKey(input.type, input.title);
5752
+ return { topic_key: suggestion.topic_key, family: suggestion.family, type: input.type };
5753
+ }
5754
+ var MemTimelineInputSchema = {
5755
+ memory_id: z33.string().optional().describe("Seed id \u2014 expands via related_ids, topic, anchors"),
5756
+ topic: z33.string().optional().describe("Frontmatter.topic value \u2014 chronological list when memory_id omitted"),
5757
+ limit: z33.number().int().positive().max(100).default(30).describe("Max timeline entries returned")
5758
+ };
5759
+ async function memTimeline(input, ctx) {
5760
+ if (!existsSync28(ctx.paths.memoriesDir)) {
5761
+ return { entries: [], total: 0, notice: "No .ai/memories directory." };
5762
+ }
5763
+ const all = await loadMemoriesFromDir222(ctx.paths.memoriesDir);
5764
+ const { entries, notice } = collectTimelineEntries(all, {
5765
+ memoryId: input.memory_id,
5766
+ topic: input.topic,
5767
+ limit: input.limit
5768
+ });
5769
+ return { entries, total: entries.length, notice };
5770
+ }
5771
+ var RuntimeJournalAppendInputSchema = {
5772
+ message: z34.string().min(1).describe("Short line to append to the runtime session journal"),
5773
+ kind: z34.enum(["note", "session_end", "mcp"]).default("note"),
5774
+ tool: z34.string().optional().describe("When kind=mcp, which tool name (optional)")
5775
+ };
5776
+ async function runtimeJournalAppend(input, ctx) {
5777
+ await appendRuntimeJournalEntry2(ctx.paths, {
5778
+ kind: input.kind,
5779
+ message: input.message,
5780
+ ...input.tool ? { tool: input.tool } : {}
5781
+ });
5782
+ return {
5783
+ ok: true,
5784
+ path_hint: `${ctx.paths.runtimeDir}/session-journal.ndjson`
5785
+ };
5786
+ }
5787
+ var RuntimeJournalTailInputSchema = {
5788
+ limit: z35.number().int().positive().max(500).default(30).describe("Last N journal entries to return")
5789
+ };
5790
+ async function runtimeJournalTail(input, ctx) {
5791
+ const entries = await readRuntimeJournalTail(ctx.paths, input.limit);
5792
+ if (entries.length === 0) {
5793
+ return { entries: [], empty: true };
5794
+ }
5795
+ return { entries };
5796
+ }
5575
5797
  var BootstrapProjectArgsSchema = {
5576
- module: z30.string().optional().describe(
5798
+ module: z36.string().optional().describe(
5577
5799
  "Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
5578
5800
  ),
5579
- focus: z30.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
5801
+ focus: z36.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
5580
5802
  };
5581
5803
  var ROOT_TEMPLATE = `# Project context
5582
5804
 
@@ -5657,8 +5879,8 @@ ${template}\`\`\`
5657
5879
  };
5658
5880
  }
5659
5881
  var PostTaskArgsSchema = {
5660
- task_summary: z31.string().optional().describe("One sentence describing what you just did"),
5661
- files_touched: z31.array(z31.string()).optional().describe("Files you created or modified during the task")
5882
+ task_summary: z37.string().optional().describe("One sentence describing what you just did"),
5883
+ files_touched: z37.array(z37.string()).optional().describe("Files you created or modified during the task")
5662
5884
  };
5663
5885
  function postTaskPrompt(args, ctx) {
5664
5886
  const taskLine = args.task_summary ? `
@@ -5741,10 +5963,10 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
5741
5963
  };
5742
5964
  }
5743
5965
  var ImportDocsArgsSchema = {
5744
- content: z32.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
5745
- source: z32.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
5746
- scope: z32.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
5747
- dry_run: z32.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
5966
+ content: z38.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
5967
+ source: z38.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
5968
+ scope: z38.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
5969
+ dry_run: z38.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
5748
5970
  };
5749
5971
  function importDocsPrompt(args, ctx) {
5750
5972
  const sourceLine = args.source ? `
@@ -5807,7 +6029,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
5807
6029
  };
5808
6030
  }
5809
6031
  var SERVER_NAME = "haive";
5810
- var SERVER_VERSION = "0.9.3";
6032
+ var SERVER_VERSION = "0.9.6";
5811
6033
  function jsonResult(data) {
5812
6034
  return {
5813
6035
  content: [
@@ -5858,6 +6080,23 @@ function createHaiveServer(options = {}) {
5858
6080
  return jsonResult(await memSave(input, context));
5859
6081
  }
5860
6082
  );
6083
+ server.tool(
6084
+ "mem_suggest_topic",
6085
+ [
6086
+ "Propose a stable `topic` key (topic-upsert) from type + short title.",
6087
+ "",
6088
+ "USE BEFORE mem_save when you want deterministic updates to the same memory over time;",
6089
+ "families mimic Engram-style grouping (architecture/*, bug/*, decision/*, \u2026).",
6090
+ "",
6091
+ "PARAMETERS:",
6092
+ " type \u2014 convention | decision | gotcha | architecture | glossary | attempt | session_recap",
6093
+ " title \u2014 phrase to slugify under the suggested family prefix",
6094
+ "",
6095
+ "RETURNS: { topic_key, family, type }"
6096
+ ].join("\n"),
6097
+ MemSuggestTopicInputSchema,
6098
+ async (input) => jsonResult(await memSuggestTopic(input, context))
6099
+ );
5861
6100
  server.tool(
5862
6101
  "mem_tried",
5863
6102
  [
@@ -5949,8 +6188,12 @@ function createHaiveServer(options = {}) {
5949
6188
  server.tool(
5950
6189
  "get_briefing",
5951
6190
  [
5952
- "\u2B50 CALL THIS FIRST at the start of every task. One-shot onboarding that returns",
5953
- "everything relevant in a single call under a token budget.",
6191
+ "\u2B50 DEFAULT-FIRST for coding agents on any repo where `haive init` ran: call this BEFORE",
6192
+ "changing source or project config for the current goal (unless the developer explicitly opts out).",
6193
+ "One-shot onboarding: everything relevant in a single call under a token budget.",
6194
+ "",
6195
+ "PROGRESSIVE DISCLOSURE \u2014 after this, drill down only if needed:",
6196
+ " mem_relevant_to / mem_search (compact lists) \u2192 mem_get (full body + anchors).",
5954
6197
  "",
5955
6198
  "RETURNS (in order of priority):",
5956
6199
  " 0. action_required \u2014 \u26A0\uFE0F HANDLE THIS FIRST if non-empty (see protocol below)",
@@ -5985,7 +6228,7 @@ function createHaiveServer(options = {}) {
5985
6228
  " low \u2014 proposed, few reads (take with caution)",
5986
6229
  " unverified \u2014 draft (unverified: true flag set)",
5987
6230
  "",
5988
- "Replaces 4\u20135 separate tool calls. Always call this before any other tool."
6231
+ "Replaces 4\u20135 separate tool calls. Prefer this first; use mem_search / mem_get only for follow-up."
5989
6232
  ].join("\n"),
5990
6233
  GetBriefingInputSchema,
5991
6234
  async (input) => {
@@ -6004,6 +6247,8 @@ function createHaiveServer(options = {}) {
6004
6247
  "SEARCH MODES:",
6005
6248
  " Literal (default): AND search across id, tags, and body \u2014 all tokens must match.",
6006
6249
  " Falls back to OR automatically if no AND results (partial match).",
6250
+ " Lexical rank (lexical_rank: true, semantic: false): Okapi-BM25-style scoring on the",
6251
+ " filtered corpus \u2014 good for phrase-like queries without embeddings.",
6007
6252
  " Semantic (semantic: true): embedding-based similarity \u2014 finds related memories",
6008
6253
  " even with different wording. Requires haive embeddings index to be built.",
6009
6254
  "",
@@ -6012,6 +6257,7 @@ function createHaiveServer(options = {}) {
6012
6257
  " scope \u2014 filter by personal | team | module",
6013
6258
  " type \u2014 filter by convention | decision | gotcha | architecture | glossary",
6014
6259
  " semantic \u2014 true for embedding-based search (requires @hiveai/embeddings)",
6260
+ " lexical_rank \u2014 BM25-style ranking (ignored when semantic is true)",
6015
6261
  " limit \u2014 max results (default 10)",
6016
6262
  "",
6017
6263
  "RETURNS: array of { id, type, scope, status, confidence, body, match_quality }"
@@ -6022,6 +6268,22 @@ function createHaiveServer(options = {}) {
6022
6268
  return jsonResult(await memSearch(input, context));
6023
6269
  }
6024
6270
  );
6271
+ server.tool(
6272
+ "mem_timeline",
6273
+ [
6274
+ "Chronological view of related memories: by shared frontmatter.topic OR expanded from a seed id",
6275
+ "(related_ids, same topic, overlapping anchor paths \u2014 one extra hop on related_ids).",
6276
+ "",
6277
+ "PARAMETERS:",
6278
+ " memory_id \u2014 optional seed memory id",
6279
+ " topic \u2014 optional topic key (required if memory_id omitted)",
6280
+ " limit \u2014 max entries (default 30)",
6281
+ "",
6282
+ "RETURNS: { entries: [{ id, type, scope, created_at, one_line, topic? }], total, notice? }"
6283
+ ].join("\n"),
6284
+ MemTimelineInputSchema,
6285
+ async (input) => jsonResult(await memTimeline(input, context))
6286
+ );
6025
6287
  server.tool(
6026
6288
  "mem_for_files",
6027
6289
  [
@@ -6051,7 +6313,7 @@ function createHaiveServer(options = {}) {
6051
6313
  [
6052
6314
  "Fetch a single memory by its full id with all details.",
6053
6315
  "",
6054
- "USE WHEN get_briefing returned a memory in 'compact' format and you need",
6316
+ "USE WHEN get_briefing / mem_relevant_to / mem_search returned a compact hit and you need",
6055
6317
  "the full body, or when you know the exact id of a memory.",
6056
6318
  "",
6057
6319
  "PARAMETERS:",
@@ -6139,6 +6401,22 @@ function createHaiveServer(options = {}) {
6139
6401
  CodeMapInputSchema,
6140
6402
  async (input) => jsonResult(await codeMapTool(input, context))
6141
6403
  );
6404
+ server.tool(
6405
+ "mem_resolve_project",
6406
+ [
6407
+ "Diagnostics: resolve which project root hAIve is using (never throws).",
6408
+ "",
6409
+ "USE IN multi-root workspaces or when the agent CWD may not be the repo root \u2014",
6410
+ "mirrors HAIVE_PROJECT_ROOT, findProjectRoot markers, and presence of .ai/memories.",
6411
+ "",
6412
+ "PARAMETERS:",
6413
+ " cwd \u2014 optional directory used for discovery when HAIVE_PROJECT_ROOT is unset",
6414
+ "",
6415
+ "RETURNS: { ok: true, info: { cwd, resolved_root, haive_project_root_env, \u2026 } }"
6416
+ ].join("\n"),
6417
+ MemResolveProjectInputSchema,
6418
+ async (input) => jsonResult(await memResolveProject(input, context))
6419
+ );
6142
6420
  server.tool(
6143
6421
  "mem_update",
6144
6422
  [
@@ -6272,6 +6550,8 @@ function createHaiveServer(options = {}) {
6272
6550
  "One-shot ranked memories for a task \u2014 use instead of get_briefing when",
6273
6551
  "project context is already loaded and you only want the relevant memory layer.",
6274
6552
  "",
6553
+ "Second step in progressive disclosure (after get_briefing): narrow here, then mem_get for full text.",
6554
+ "",
6275
6555
  "Reuses the same ranking pipeline (anchor / module / literal / semantic) but",
6276
6556
  "skips project_context, modules, action_required, etc.",
6277
6557
  "",
@@ -6429,6 +6709,53 @@ function createHaiveServer(options = {}) {
6429
6709
  return jsonResult(await memConflicts(input, context));
6430
6710
  }
6431
6711
  );
6712
+ server.tool(
6713
+ "mem_conflict_candidates",
6714
+ [
6715
+ "Bulk scan for conflict CANDIDATES (not proof):",
6716
+ "",
6717
+ " 1. Lexical similarity (Jaccard) on decision/architecture-like pairs",
6718
+ " 2. Same frontmatter.topic with validated vs rejected \u2014 quick human-review signal",
6719
+ "",
6720
+ "Advisory only \u2014 follow with mem_conflicts_with on specific ids.",
6721
+ "",
6722
+ "PARAMETERS:",
6723
+ " since_days, types, min_jaccard, max_pairs, max_scan, max_topic_pairs",
6724
+ "",
6725
+ "RETURNS: { pairs, topic_status_pairs, scanned, truncated, notice? }"
6726
+ ].join("\n"),
6727
+ MemConflictCandidatesInputSchema,
6728
+ async (input) => {
6729
+ tracker.record("mem_conflict_candidates", `${input.since_days}d`);
6730
+ return jsonResult(await memConflictCandidates(input, context));
6731
+ }
6732
+ );
6733
+ server.tool(
6734
+ "runtime_journal_append",
6735
+ [
6736
+ "Append one line to `.ai/.runtime/session-journal.ndjson` \u2014 machine-local session continuity.",
6737
+ "",
6738
+ "Does NOT replace team memories; complements mem_session_end recaps for local traces.",
6739
+ "",
6740
+ "PARAMETERS: message, kind (note|session_end|mcp), optional tool",
6741
+ "",
6742
+ "RETURNS: { ok, path_hint }"
6743
+ ].join("\n"),
6744
+ RuntimeJournalAppendInputSchema,
6745
+ async (input) => jsonResult(await runtimeJournalAppend(input, context))
6746
+ );
6747
+ server.tool(
6748
+ "runtime_journal_tail",
6749
+ [
6750
+ "Read the last N entries from the runtime session journal (parsed JSON lines).",
6751
+ "",
6752
+ "PARAMETERS: limit (default 30, max 500)",
6753
+ "",
6754
+ "RETURNS: { entries: [...], empty?: true }"
6755
+ ].join("\n"),
6756
+ RuntimeJournalTailInputSchema,
6757
+ async (input) => jsonResult(await runtimeJournalTail(input, context))
6758
+ );
6432
6759
  server.tool(
6433
6760
  "pre_commit_check",
6434
6761
  [
@@ -6566,7 +6893,7 @@ function registerMcp(program2) {
6566
6893
  // src/commands/sync.ts
6567
6894
  import { spawnSync as spawnSync2 } from "child_process";
6568
6895
  import { readFile as readFile8, writeFile as writeFile13, mkdir as mkdir8 } from "fs/promises";
6569
- import { existsSync as existsSync27 } from "fs";
6896
+ import { existsSync as existsSync29 } from "fs";
6570
6897
  import path12 from "path";
6571
6898
  import "commander";
6572
6899
  import {
@@ -6578,7 +6905,7 @@ import {
6578
6905
  isDecaying as isDecaying2,
6579
6906
  loadCodeMap as loadCodeMap4,
6580
6907
  loadConfig as loadConfig4,
6581
- loadMemoriesFromDir as loadMemoriesFromDir21,
6908
+ loadMemoriesFromDir as loadMemoriesFromDir23,
6582
6909
  loadUsageIndex as loadUsageIndex12,
6583
6910
  pullCrossRepoSources,
6584
6911
  resolveHaivePaths as resolveHaivePaths7,
@@ -6602,7 +6929,7 @@ function registerSync(program2) {
6602
6929
  ).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) => {
6603
6930
  const root = findProjectRoot10(opts.dir);
6604
6931
  const paths = resolveHaivePaths7(root);
6605
- if (!existsSync27(paths.memoriesDir)) {
6932
+ if (!existsSync29(paths.memoriesDir)) {
6606
6933
  if (!opts.quiet) ui.warn(`No .ai/memories at ${root}. Run \`haive init\` first.`);
6607
6934
  process.exitCode = 1;
6608
6935
  return;
@@ -6618,7 +6945,7 @@ function registerSync(program2) {
6618
6945
  let promoted = 0;
6619
6946
  let autoApproved = 0;
6620
6947
  if (opts.verify !== false) {
6621
- const memories = await loadMemoriesFromDir21(paths.memoriesDir);
6948
+ const memories = await loadMemoriesFromDir23(paths.memoriesDir);
6622
6949
  for (const { memory: memory2, filePath } of memories) {
6623
6950
  if (memory2.frontmatter.type === "session_recap") {
6624
6951
  if (memory2.frontmatter.status === "stale") {
@@ -6679,7 +7006,7 @@ function registerSync(program2) {
6679
7006
  }
6680
7007
  }
6681
7008
  if (opts.promote !== false) {
6682
- const memories = await loadMemoriesFromDir21(paths.memoriesDir);
7009
+ const memories = await loadMemoriesFromDir23(paths.memoriesDir);
6683
7010
  const usage = await loadUsageIndex12(paths);
6684
7011
  const nowMs = Date.now();
6685
7012
  for (const { memory: memory2, filePath } of memories) {
@@ -6718,7 +7045,7 @@ function registerSync(program2) {
6718
7045
  }
6719
7046
  }
6720
7047
  const sinceReport = opts.since ? collectSinceChanges(root, opts.since) : null;
6721
- const draftMemories = (await loadMemoriesFromDir21(paths.memoriesDir)).filter(
7048
+ const draftMemories = (await loadMemoriesFromDir23(paths.memoriesDir)).filter(
6722
7049
  (m) => m.memory.frontmatter.status === "draft"
6723
7050
  );
6724
7051
  const draftCount = draftMemories.length;
@@ -6753,7 +7080,7 @@ function registerSync(program2) {
6753
7080
  }
6754
7081
  }
6755
7082
  if (!opts.quiet) {
6756
- const allForDecay = await loadMemoriesFromDir21(paths.memoriesDir);
7083
+ const allForDecay = await loadMemoriesFromDir23(paths.memoriesDir);
6757
7084
  const usageForDecay = await loadUsageIndex12(paths);
6758
7085
  const decaying = allForDecay.filter(({ memory: memory2 }) => {
6759
7086
  const fm = memory2.frontmatter;
@@ -6971,8 +7298,8 @@ Attends une **confirmation explicite** avant d'agir.
6971
7298
  });
6972
7299
  }
6973
7300
  async function injectBridge(bridgeFile, memoriesDir, maxMemories, root, quiet) {
6974
- if (!existsSync27(memoriesDir)) return;
6975
- const all = await loadMemoriesFromDir21(memoriesDir);
7301
+ if (!existsSync29(memoriesDir)) return;
7302
+ const all = await loadMemoriesFromDir23(memoriesDir);
6976
7303
  const top = all.filter(({ memory: memory2 }) => {
6977
7304
  const s = memory2.frontmatter.status;
6978
7305
  if (memory2.frontmatter.type === "session_recap") return false;
@@ -6996,7 +7323,7 @@ ${m.memory.body.trim()}`;
6996
7323
  ` + block + `
6997
7324
 
6998
7325
  ${BRIDGE_END}`;
6999
- const fileExists = existsSync27(bridgeFile);
7326
+ const fileExists = existsSync29(bridgeFile);
7000
7327
  let existing = fileExists ? await readFile8(bridgeFile, "utf8") : "";
7001
7328
  existing = existing.replace(/\r\n/g, "\n");
7002
7329
  const startIdx = existing.indexOf(BRIDGE_START);
@@ -7047,14 +7374,14 @@ function collectSinceChanges(root, ref) {
7047
7374
  // src/commands/memory-add.ts
7048
7375
  import { createHash as createHash2 } from "crypto";
7049
7376
  import { mkdir as mkdir9, readFile as readFile9, writeFile as writeFile14 } from "fs/promises";
7050
- import { existsSync as existsSync28 } from "fs";
7377
+ import { existsSync as existsSync30 } from "fs";
7051
7378
  import path13 from "path";
7052
7379
  import "commander";
7053
7380
  import {
7054
7381
  buildFrontmatter as buildFrontmatter7,
7055
7382
  findProjectRoot as findProjectRoot11,
7056
7383
  inferModulesFromPaths as inferModulesFromPaths3,
7057
- loadMemoriesFromDir as loadMemoriesFromDir23,
7384
+ loadMemoriesFromDir as loadMemoriesFromDir24,
7058
7385
  memoryFilePath as memoryFilePath6,
7059
7386
  resolveHaivePaths as resolveHaivePaths8,
7060
7387
  serializeMemory as serializeMemory12
@@ -7086,7 +7413,7 @@ function registerMemoryAdd(memory2) {
7086
7413
  ).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) => {
7087
7414
  const root = findProjectRoot11(opts.dir);
7088
7415
  const paths = resolveHaivePaths8(root);
7089
- if (!existsSync28(paths.haiveDir)) {
7416
+ if (!existsSync30(paths.haiveDir)) {
7090
7417
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
7091
7418
  process.exitCode = 1;
7092
7419
  return;
@@ -7097,7 +7424,7 @@ function registerMemoryAdd(memory2) {
7097
7424
  const inferredTags = autoTagsEnabled ? inferModulesFromPaths3(anchorPaths) : [];
7098
7425
  const mergedTags = Array.from(/* @__PURE__ */ new Set([...userTags, ...inferredTags]));
7099
7426
  if (anchorPaths.length > 0) {
7100
- const missing = anchorPaths.filter((p) => !existsSync28(path13.resolve(root, p)));
7427
+ const missing = anchorPaths.filter((p) => !existsSync30(path13.resolve(root, p)));
7101
7428
  if (missing.length > 0) {
7102
7429
  ui.warn(`Anchor path${missing.length > 1 ? "s" : ""} not found in project:`);
7103
7430
  for (const p of missing) ui.warn(` \u2717 ${p}`);
@@ -7109,7 +7436,7 @@ function registerMemoryAdd(memory2) {
7109
7436
  const title = opts.title ?? opts.slug;
7110
7437
  let body;
7111
7438
  if (opts.bodyFile !== void 0) {
7112
- if (!existsSync28(opts.bodyFile)) {
7439
+ if (!existsSync30(opts.bodyFile)) {
7113
7440
  ui.error(`--body-file not found: ${opts.bodyFile}`);
7114
7441
  process.exitCode = 1;
7115
7442
  return;
@@ -7130,9 +7457,9 @@ TODO \u2014 write the memory body.
7130
7457
  `;
7131
7458
  }
7132
7459
  const scope = opts.scope ?? "personal";
7133
- if (existsSync28(paths.memoriesDir)) {
7460
+ if (existsSync30(paths.memoriesDir)) {
7134
7461
  const incomingHash = createHash2("sha256").update(body.trim()).digest("hex").slice(0, 12);
7135
- const allForHash = await loadMemoriesFromDir23(paths.memoriesDir);
7462
+ const allForHash = await loadMemoriesFromDir24(paths.memoriesDir);
7136
7463
  const hashDup = allForHash.find(
7137
7464
  ({ memory: memory3 }) => createHash2("sha256").update(memory3.body.trim()).digest("hex").slice(0, 12) === incomingHash && memory3.frontmatter.scope === scope
7138
7465
  );
@@ -7143,8 +7470,8 @@ TODO \u2014 write the memory body.
7143
7470
  return;
7144
7471
  }
7145
7472
  }
7146
- if (opts.topic && existsSync28(paths.memoriesDir)) {
7147
- const existing = await loadMemoriesFromDir23(paths.memoriesDir);
7473
+ if (opts.topic && existsSync30(paths.memoriesDir)) {
7474
+ const existing = await loadMemoriesFromDir24(paths.memoriesDir);
7148
7475
  const topicMatch = existing.find(
7149
7476
  ({ memory: memory3 }) => memory3.frontmatter.topic === opts.topic && memory3.frontmatter.scope === scope && (!opts.module || memory3.frontmatter.module === opts.module)
7150
7477
  );
@@ -7182,13 +7509,13 @@ TODO \u2014 write the memory body.
7182
7509
  });
7183
7510
  const file = memoryFilePath6(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
7184
7511
  await mkdir9(path13.dirname(file), { recursive: true });
7185
- if (existsSync28(file)) {
7512
+ if (existsSync30(file)) {
7186
7513
  ui.error(`Memory already exists at ${file}`);
7187
7514
  process.exitCode = 1;
7188
7515
  return;
7189
7516
  }
7190
- if (existsSync28(paths.memoriesDir)) {
7191
- const existing = await loadMemoriesFromDir23(paths.memoriesDir);
7517
+ if (existsSync30(paths.memoriesDir)) {
7518
+ const existing = await loadMemoriesFromDir24(paths.memoriesDir);
7192
7519
  const slugTokens = opts.slug.toLowerCase().split(/[-_\s]+/).filter(Boolean);
7193
7520
  const similar = existing.filter(({ memory: memory3 }) => {
7194
7521
  const id = memory3.frontmatter.id.toLowerCase();
@@ -7230,14 +7557,14 @@ function parseCsv2(value) {
7230
7557
  }
7231
7558
 
7232
7559
  // src/commands/memory-list.ts
7233
- import { existsSync as existsSync29 } from "fs";
7560
+ import { existsSync as existsSync31 } from "fs";
7234
7561
  import path14 from "path";
7235
7562
  import "commander";
7236
7563
  import { findProjectRoot as findProjectRoot12, resolveHaivePaths as resolveHaivePaths9 } from "@hiveai/core";
7237
7564
 
7238
7565
  // src/utils/fs.ts
7239
7566
  import {
7240
- loadMemoriesFromDir as loadMemoriesFromDir24,
7567
+ loadMemoriesFromDir as loadMemoriesFromDir25,
7241
7568
  loadMemory,
7242
7569
  listMarkdownFilesRecursive
7243
7570
  } from "@hiveai/core";
@@ -7247,12 +7574,12 @@ function registerMemoryList(memory2) {
7247
7574
  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) => {
7248
7575
  const root = findProjectRoot12(opts.dir);
7249
7576
  const paths = resolveHaivePaths9(root);
7250
- if (!existsSync29(paths.memoriesDir)) {
7577
+ if (!existsSync31(paths.memoriesDir)) {
7251
7578
  ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
7252
7579
  process.exitCode = 1;
7253
7580
  return;
7254
7581
  }
7255
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
7582
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
7256
7583
  const statusFilter = opts.status ? opts.status.split(",").map((s) => s.trim()) : null;
7257
7584
  const filtered = all.filter((m) => {
7258
7585
  if (!matchesFilters(m, opts)) return false;
@@ -7315,7 +7642,7 @@ function matchesFilters(loaded, opts) {
7315
7642
 
7316
7643
  // src/commands/memory-promote.ts
7317
7644
  import { mkdir as mkdir10, unlink as unlink2, writeFile as writeFile15 } from "fs/promises";
7318
- import { existsSync as existsSync30 } from "fs";
7645
+ import { existsSync as existsSync33 } from "fs";
7319
7646
  import path15 from "path";
7320
7647
  import "commander";
7321
7648
  import {
@@ -7328,12 +7655,12 @@ function registerMemoryPromote(memory2) {
7328
7655
  memory2.command("promote <id>").description("Promote a personal memory to team scope (status -> proposed)").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
7329
7656
  const root = findProjectRoot13(opts.dir);
7330
7657
  const paths = resolveHaivePaths10(root);
7331
- if (!existsSync30(paths.memoriesDir)) {
7658
+ if (!existsSync33(paths.memoriesDir)) {
7332
7659
  ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
7333
7660
  process.exitCode = 1;
7334
7661
  return;
7335
7662
  }
7336
- const teamAndModule = await loadMemoriesFromDir24(paths.memoriesDir);
7663
+ const teamAndModule = await loadMemoriesFromDir25(paths.memoriesDir);
7337
7664
  const alreadyShared = teamAndModule.find(
7338
7665
  (m) => m.memory.frontmatter.id === id && (m.memory.frontmatter.scope === "team" || m.memory.frontmatter.scope === "module")
7339
7666
  );
@@ -7347,7 +7674,7 @@ function registerMemoryPromote(memory2) {
7347
7674
  }
7348
7675
  return;
7349
7676
  }
7350
- const all = await loadMemoriesFromDir24(paths.personalDir);
7677
+ const all = await loadMemoriesFromDir25(paths.personalDir);
7351
7678
  const found = all.find((m) => m.memory.frontmatter.id === id);
7352
7679
  if (!found) {
7353
7680
  ui.error(`No personal memory with id "${id}". (Promotion only applies to personal scope.)`);
@@ -7373,7 +7700,7 @@ function registerMemoryPromote(memory2) {
7373
7700
  }
7374
7701
 
7375
7702
  // src/commands/memory-approve.ts
7376
- import { existsSync as existsSync31 } from "fs";
7703
+ import { existsSync as existsSync34 } from "fs";
7377
7704
  import { writeFile as writeFile16 } from "fs/promises";
7378
7705
  import path16 from "path";
7379
7706
  import "commander";
@@ -7386,12 +7713,12 @@ function registerMemoryApprove(memory2) {
7386
7713
  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) => {
7387
7714
  const root = findProjectRoot14(opts.dir);
7388
7715
  const paths = resolveHaivePaths11(root);
7389
- if (!existsSync31(paths.memoriesDir)) {
7716
+ if (!existsSync34(paths.memoriesDir)) {
7390
7717
  ui.error(`No .ai/memories at ${root}.`);
7391
7718
  process.exitCode = 1;
7392
7719
  return;
7393
7720
  }
7394
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
7721
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
7395
7722
  if (opts.all || opts.pending) {
7396
7723
  const candidates = all.filter((m) => {
7397
7724
  const s = m.memory.frontmatter.status;
@@ -7445,7 +7772,7 @@ function registerMemoryApprove(memory2) {
7445
7772
 
7446
7773
  // src/commands/memory-update.ts
7447
7774
  import { writeFile as writeFile17 } from "fs/promises";
7448
- import { existsSync as existsSync33 } from "fs";
7775
+ import { existsSync as existsSync35 } from "fs";
7449
7776
  import path17 from "path";
7450
7777
  import "commander";
7451
7778
  import {
@@ -7457,12 +7784,12 @@ function registerMemoryUpdate(memory2) {
7457
7784
  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) => {
7458
7785
  const root = findProjectRoot15(opts.dir);
7459
7786
  const paths = resolveHaivePaths12(root);
7460
- if (!existsSync33(paths.memoriesDir)) {
7787
+ if (!existsSync35(paths.memoriesDir)) {
7461
7788
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
7462
7789
  process.exitCode = 1;
7463
7790
  return;
7464
7791
  }
7465
- const memories = await loadMemoriesFromDir24(paths.memoriesDir);
7792
+ const memories = await loadMemoriesFromDir25(paths.memoriesDir);
7466
7793
  const loaded = memories.find((m) => m.memory.frontmatter.id === id);
7467
7794
  if (!loaded) {
7468
7795
  ui.error(`No memory with id "${id}".`);
@@ -7529,7 +7856,7 @@ function parseCsv3(value) {
7529
7856
 
7530
7857
  // src/commands/memory-auto-promote.ts
7531
7858
  import { writeFile as writeFile18 } from "fs/promises";
7532
- import { existsSync as existsSync34 } from "fs";
7859
+ import { existsSync as existsSync36 } from "fs";
7533
7860
  import path18 from "path";
7534
7861
  import "commander";
7535
7862
  import {
@@ -7549,7 +7876,7 @@ function registerMemoryAutoPromote(memory2) {
7549
7876
  ).option("--apply", "actually write status=validated to disk (default: dry-run)").option("-d, --dir <dir>", "project root").action(async (opts) => {
7550
7877
  const root = findProjectRoot16(opts.dir);
7551
7878
  const paths = resolveHaivePaths13(root);
7552
- if (!existsSync34(paths.memoriesDir)) {
7879
+ if (!existsSync36(paths.memoriesDir)) {
7553
7880
  ui.error(`No .ai/memories at ${root}.`);
7554
7881
  process.exitCode = 1;
7555
7882
  return;
@@ -7558,7 +7885,7 @@ function registerMemoryAutoPromote(memory2) {
7558
7885
  minReads: Number(opts.minReads ?? DEFAULT_AUTO_PROMOTE_RULE3.minReads),
7559
7886
  maxRejections: Number(opts.maxRejections ?? DEFAULT_AUTO_PROMOTE_RULE3.maxRejections)
7560
7887
  };
7561
- const memories = await loadMemoriesFromDir24(paths.memoriesDir);
7888
+ const memories = await loadMemoriesFromDir25(paths.memoriesDir);
7562
7889
  const usage = await loadUsageIndex13(paths);
7563
7890
  const eligible = memories.filter(
7564
7891
  ({ memory: memory3 }) => isAutoPromoteEligible3(memory3.frontmatter, getUsage11(usage, memory3.frontmatter.id), rule)
@@ -7592,7 +7919,7 @@ function registerMemoryAutoPromote(memory2) {
7592
7919
 
7593
7920
  // src/commands/memory-edit.ts
7594
7921
  import { spawn as spawn3 } from "child_process";
7595
- import { existsSync as existsSync35 } from "fs";
7922
+ import { existsSync as existsSync37 } from "fs";
7596
7923
  import { readFile as readFile10 } from "fs/promises";
7597
7924
  import path19 from "path";
7598
7925
  import "commander";
@@ -7605,12 +7932,12 @@ function registerMemoryEdit(memory2) {
7605
7932
  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) => {
7606
7933
  const root = findProjectRoot17(opts.dir);
7607
7934
  const paths = resolveHaivePaths14(root);
7608
- if (!existsSync35(paths.memoriesDir)) {
7935
+ if (!existsSync37(paths.memoriesDir)) {
7609
7936
  ui.error(`No .ai/memories at ${root}.`);
7610
7937
  process.exitCode = 1;
7611
7938
  return;
7612
7939
  }
7613
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
7940
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
7614
7941
  const found = all.find((m) => m.memory.frontmatter.id === id);
7615
7942
  if (!found) {
7616
7943
  ui.error(`No memory with id "${id}".`);
@@ -7645,7 +7972,7 @@ function runEditor(editor, file) {
7645
7972
  }
7646
7973
 
7647
7974
  // src/commands/memory-for-files.ts
7648
- import { existsSync as existsSync36 } from "fs";
7975
+ import { existsSync as existsSync38 } from "fs";
7649
7976
  import path20 from "path";
7650
7977
  import "commander";
7651
7978
  import {
@@ -7661,12 +7988,12 @@ function registerMemoryForFiles(memory2) {
7661
7988
  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) => {
7662
7989
  const root = findProjectRoot18(opts.dir);
7663
7990
  const paths = resolveHaivePaths15(root);
7664
- if (!existsSync36(paths.memoriesDir)) {
7991
+ if (!existsSync38(paths.memoriesDir)) {
7665
7992
  ui.error(`No .ai/memories at ${root}.`);
7666
7993
  process.exitCode = 1;
7667
7994
  return;
7668
7995
  }
7669
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
7996
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
7670
7997
  const usage = await loadUsageIndex14(paths);
7671
7998
  const inferred = inferModulesFromPaths4(files);
7672
7999
  const byAnchor = [];
@@ -7773,7 +8100,7 @@ function printGroup(root, label, loaded, usage) {
7773
8100
  }
7774
8101
 
7775
8102
  // src/commands/memory-hot.ts
7776
- import { existsSync as existsSync37 } from "fs";
8103
+ import { existsSync as existsSync39 } from "fs";
7777
8104
  import path21 from "path";
7778
8105
  import "commander";
7779
8106
  import {
@@ -7786,13 +8113,13 @@ function registerMemoryHot(memory2) {
7786
8113
  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) => {
7787
8114
  const root = findProjectRoot19(opts.dir);
7788
8115
  const paths = resolveHaivePaths16(root);
7789
- if (!existsSync37(paths.memoriesDir)) {
8116
+ if (!existsSync39(paths.memoriesDir)) {
7790
8117
  ui.error(`No .ai/memories at ${root}.`);
7791
8118
  process.exitCode = 1;
7792
8119
  return;
7793
8120
  }
7794
8121
  const threshold = Math.max(1, Number(opts.threshold ?? 3));
7795
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
8122
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
7796
8123
  const usage = await loadUsageIndex15(paths);
7797
8124
  const candidates = all.filter(({ memory: mem }) => {
7798
8125
  const fm = mem.frontmatter;
@@ -7824,7 +8151,7 @@ function registerMemoryHot(memory2) {
7824
8151
 
7825
8152
  // src/commands/memory-tried.ts
7826
8153
  import { mkdir as mkdir11, writeFile as writeFile19 } from "fs/promises";
7827
- import { existsSync as existsSync38 } from "fs";
8154
+ import { existsSync as existsSync40 } from "fs";
7828
8155
  import path23 from "path";
7829
8156
  import "commander";
7830
8157
  import {
@@ -7853,7 +8180,7 @@ function registerMemoryTried(memory2) {
7853
8180
  ).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) => {
7854
8181
  const root = findProjectRoot20(opts.dir);
7855
8182
  const paths = resolveHaivePaths17(root);
7856
- if (!existsSync38(paths.haiveDir)) {
8183
+ if (!existsSync40(paths.haiveDir)) {
7857
8184
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
7858
8185
  process.exitCode = 1;
7859
8186
  return;
@@ -7877,7 +8204,7 @@ function registerMemoryTried(memory2) {
7877
8204
  const body = lines.join("\n") + "\n";
7878
8205
  const file = memoryFilePath8(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
7879
8206
  await mkdir11(path23.dirname(file), { recursive: true });
7880
- if (existsSync38(file)) {
8207
+ if (existsSync40(file)) {
7881
8208
  ui.error(`Memory already exists at ${file}`);
7882
8209
  process.exitCode = 1;
7883
8210
  return;
@@ -7893,7 +8220,7 @@ function parseCsv4(value) {
7893
8220
  }
7894
8221
 
7895
8222
  // src/commands/memory-pending.ts
7896
- import { existsSync as existsSync39 } from "fs";
8223
+ import { existsSync as existsSync41 } from "fs";
7897
8224
  import path24 from "path";
7898
8225
  import "commander";
7899
8226
  import {
@@ -7906,12 +8233,12 @@ function registerMemoryPending(memory2) {
7906
8233
  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) => {
7907
8234
  const root = findProjectRoot21(opts.dir);
7908
8235
  const paths = resolveHaivePaths18(root);
7909
- if (!existsSync39(paths.memoriesDir)) {
8236
+ if (!existsSync41(paths.memoriesDir)) {
7910
8237
  ui.error(`No .ai/memories at ${root}.`);
7911
8238
  process.exitCode = 1;
7912
8239
  return;
7913
8240
  }
7914
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
8241
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
7915
8242
  const usage = await loadUsageIndex16(paths);
7916
8243
  const proposed = all.filter(({ memory: mem }) => {
7917
8244
  if (mem.frontmatter.status !== "proposed") return false;
@@ -7941,7 +8268,7 @@ function registerMemoryPending(memory2) {
7941
8268
  }
7942
8269
 
7943
8270
  // src/commands/memory-query.ts
7944
- import { existsSync as existsSync40 } from "fs";
8271
+ import { existsSync as existsSync43 } from "fs";
7945
8272
  import path25 from "path";
7946
8273
  import "commander";
7947
8274
  import {
@@ -7958,7 +8285,7 @@ function registerMemoryQuery(memory2) {
7958
8285
  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) => {
7959
8286
  const root = findProjectRoot22(opts.dir);
7960
8287
  const paths = resolveHaivePaths19(root);
7961
- if (!existsSync40(paths.memoriesDir)) {
8288
+ if (!existsSync43(paths.memoriesDir)) {
7962
8289
  ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
7963
8290
  process.exitCode = 1;
7964
8291
  return;
@@ -7969,7 +8296,7 @@ function registerMemoryQuery(memory2) {
7969
8296
  return;
7970
8297
  }
7971
8298
  const statusFilter = opts.status ? opts.status.split(",").map((s) => s.trim()) : null;
7972
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
8299
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
7973
8300
  const passesFilters2 = (mem) => {
7974
8301
  const fm = mem.frontmatter;
7975
8302
  if (opts.scope && fm.scope !== opts.scope) return false;
@@ -8017,7 +8344,7 @@ ${top.length} of ${matches.length} match${matches.length === 1 ? "" : "es"}`)
8017
8344
 
8018
8345
  // src/commands/memory-reject.ts
8019
8346
  import { writeFile as writeFile20 } from "fs/promises";
8020
- import { existsSync as existsSync41 } from "fs";
8347
+ import { existsSync as existsSync44 } from "fs";
8021
8348
  import "commander";
8022
8349
  import {
8023
8350
  findProjectRoot as findProjectRoot23,
@@ -8031,12 +8358,12 @@ function registerMemoryReject(memory2) {
8031
8358
  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) => {
8032
8359
  const root = findProjectRoot23(opts.dir);
8033
8360
  const paths = resolveHaivePaths20(root);
8034
- if (!existsSync41(paths.memoriesDir)) {
8361
+ if (!existsSync44(paths.memoriesDir)) {
8035
8362
  ui.error(`No .ai/memories at ${root}.`);
8036
8363
  process.exitCode = 1;
8037
8364
  return;
8038
8365
  }
8039
- const memories = await loadMemoriesFromDir24(paths.memoriesDir);
8366
+ const memories = await loadMemoriesFromDir25(paths.memoriesDir);
8040
8367
  const loaded = memories.find((m) => m.memory.frontmatter.id === id);
8041
8368
  if (!loaded) {
8042
8369
  ui.error(`No memory with id "${id}".`);
@@ -8067,7 +8394,7 @@ function registerMemoryReject(memory2) {
8067
8394
  }
8068
8395
 
8069
8396
  // src/commands/memory-rm.ts
8070
- import { existsSync as existsSync43 } from "fs";
8397
+ import { existsSync as existsSync45 } from "fs";
8071
8398
  import { unlink as unlink3 } from "fs/promises";
8072
8399
  import path26 from "path";
8073
8400
  import { createInterface } from "readline/promises";
@@ -8082,12 +8409,12 @@ function registerMemoryRm(memory2) {
8082
8409
  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) => {
8083
8410
  const root = findProjectRoot24(opts.dir);
8084
8411
  const paths = resolveHaivePaths21(root);
8085
- if (!existsSync43(paths.memoriesDir)) {
8412
+ if (!existsSync45(paths.memoriesDir)) {
8086
8413
  ui.error(`No .ai/memories at ${root}.`);
8087
8414
  process.exitCode = 1;
8088
8415
  return;
8089
8416
  }
8090
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
8417
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
8091
8418
  const found = all.find((m) => m.memory.frontmatter.id === id);
8092
8419
  if (!found) {
8093
8420
  ui.error(`No memory with id "${id}".`);
@@ -8118,7 +8445,7 @@ function registerMemoryRm(memory2) {
8118
8445
  }
8119
8446
 
8120
8447
  // src/commands/memory-show.ts
8121
- import { existsSync as existsSync44 } from "fs";
8448
+ import { existsSync as existsSync46 } from "fs";
8122
8449
  import { readFile as readFile11 } from "fs/promises";
8123
8450
  import path27 from "path";
8124
8451
  import "commander";
@@ -8133,12 +8460,12 @@ function registerMemoryShow(memory2) {
8133
8460
  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) => {
8134
8461
  const root = findProjectRoot25(opts.dir);
8135
8462
  const paths = resolveHaivePaths22(root);
8136
- if (!existsSync44(paths.memoriesDir)) {
8463
+ if (!existsSync46(paths.memoriesDir)) {
8137
8464
  ui.error(`No .ai/memories at ${root}.`);
8138
8465
  process.exitCode = 1;
8139
8466
  return;
8140
8467
  }
8141
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
8468
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
8142
8469
  const found = all.find((m) => m.memory.frontmatter.id === id);
8143
8470
  if (!found) {
8144
8471
  ui.error(`No memory with id "${id}".`);
@@ -8177,7 +8504,7 @@ function registerMemoryShow(memory2) {
8177
8504
  }
8178
8505
 
8179
8506
  // src/commands/memory-stats.ts
8180
- import { existsSync as existsSync45 } from "fs";
8507
+ import { existsSync as existsSync47 } from "fs";
8181
8508
  import path28 from "path";
8182
8509
  import "commander";
8183
8510
  import {
@@ -8191,12 +8518,12 @@ function registerMemoryStats(memory2) {
8191
8518
  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) => {
8192
8519
  const root = findProjectRoot26(opts.dir);
8193
8520
  const paths = resolveHaivePaths23(root);
8194
- if (!existsSync45(paths.memoriesDir)) {
8521
+ if (!existsSync47(paths.memoriesDir)) {
8195
8522
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
8196
8523
  process.exitCode = 1;
8197
8524
  return;
8198
8525
  }
8199
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
8526
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
8200
8527
  const usage = await loadUsageIndex20(paths);
8201
8528
  const target = opts.id ? all.filter((m) => m.memory.frontmatter.id === opts.id) : all;
8202
8529
  if (target.length === 0) {
@@ -8223,7 +8550,7 @@ function registerMemoryStats(memory2) {
8223
8550
 
8224
8551
  // src/commands/memory-verify.ts
8225
8552
  import { writeFile as writeFile21 } from "fs/promises";
8226
- import { existsSync as existsSync46 } from "fs";
8553
+ import { existsSync as existsSync48 } from "fs";
8227
8554
  import path29 from "path";
8228
8555
  import "commander";
8229
8556
  import {
@@ -8238,12 +8565,12 @@ function registerMemoryVerify(memory2) {
8238
8565
  ).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) => {
8239
8566
  const root = findProjectRoot27(opts.dir);
8240
8567
  const paths = resolveHaivePaths24(root);
8241
- if (!existsSync46(paths.memoriesDir)) {
8568
+ if (!existsSync48(paths.memoriesDir)) {
8242
8569
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
8243
8570
  process.exitCode = 1;
8244
8571
  return;
8245
8572
  }
8246
- const all = await loadMemoriesFromDir24(paths.memoriesDir);
8573
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
8247
8574
  const targets = opts.id ? all.filter((m) => m.memory.frontmatter.id === opts.id) : all;
8248
8575
  if (opts.id && targets.length === 0) {
8249
8576
  ui.error(`No memory with id "${opts.id}".`);
@@ -8325,7 +8652,7 @@ function applyVerification2(mem, result) {
8325
8652
 
8326
8653
  // src/commands/memory-import.ts
8327
8654
  import { readFile as readFile12 } from "fs/promises";
8328
- import { existsSync as existsSync47 } from "fs";
8655
+ import { existsSync as existsSync49 } from "fs";
8329
8656
  import "commander";
8330
8657
  import {
8331
8658
  findProjectRoot as findProjectRoot28,
@@ -8337,12 +8664,12 @@ function registerMemoryImport(memory2) {
8337
8664
  ).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) => {
8338
8665
  const root = findProjectRoot28(opts.dir);
8339
8666
  const paths = resolveHaivePaths25(root);
8340
- if (!existsSync47(paths.haiveDir)) {
8667
+ if (!existsSync49(paths.haiveDir)) {
8341
8668
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
8342
8669
  process.exitCode = 1;
8343
8670
  return;
8344
8671
  }
8345
- if (!existsSync47(opts.from)) {
8672
+ if (!existsSync49(opts.from)) {
8346
8673
  ui.error(`File not found: ${opts.from}`);
8347
8674
  process.exitCode = 1;
8348
8675
  return;
@@ -8375,7 +8702,7 @@ function registerMemoryImport(memory2) {
8375
8702
  }
8376
8703
 
8377
8704
  // src/commands/memory-import-changelog.ts
8378
- import { existsSync as existsSync48 } from "fs";
8705
+ import { existsSync as existsSync50 } from "fs";
8379
8706
  import { readFile as readFile13, mkdir as mkdir12, writeFile as writeFile23 } from "fs/promises";
8380
8707
  import path30 from "path";
8381
8708
  import "commander";
@@ -8449,7 +8776,7 @@ function registerMemoryImportChangelog(memory2) {
8449
8776
  const root = findProjectRoot29(opts.dir);
8450
8777
  const paths = resolveHaivePaths26(root);
8451
8778
  const changelogPath = path30.resolve(root, opts.fromChangelog);
8452
- if (!existsSync48(changelogPath)) {
8779
+ if (!existsSync50(changelogPath)) {
8453
8780
  ui.error(`CHANGELOG not found: ${changelogPath}`);
8454
8781
  process.exitCode = 1;
8455
8782
  return;
@@ -8535,7 +8862,7 @@ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${p
8535
8862
  }
8536
8863
 
8537
8864
  // src/commands/memory-digest.ts
8538
- import { existsSync as existsSync49 } from "fs";
8865
+ import { existsSync as existsSync51 } from "fs";
8539
8866
  import { writeFile as writeFile24 } from "fs/promises";
8540
8867
  import path31 from "path";
8541
8868
  import "commander";
@@ -8543,7 +8870,7 @@ import {
8543
8870
  deriveConfidence as deriveConfidence12,
8544
8871
  findProjectRoot as findProjectRoot30,
8545
8872
  getUsage as getUsage17,
8546
- loadMemoriesFromDir as loadMemoriesFromDir25,
8873
+ loadMemoriesFromDir as loadMemoriesFromDir26,
8547
8874
  loadUsageIndex as loadUsageIndex21,
8548
8875
  resolveHaivePaths as resolveHaivePaths27
8549
8876
  } from "@hiveai/core";
@@ -8560,7 +8887,7 @@ function registerMemoryDigest(program2) {
8560
8887
  ).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) => {
8561
8888
  const root = findProjectRoot30(opts.dir);
8562
8889
  const paths = resolveHaivePaths27(root);
8563
- if (!existsSync49(paths.memoriesDir)) {
8890
+ if (!existsSync51(paths.memoriesDir)) {
8564
8891
  ui.error("No .ai/memories found. Run `haive init` first.");
8565
8892
  process.exitCode = 1;
8566
8893
  return;
@@ -8568,7 +8895,7 @@ function registerMemoryDigest(program2) {
8568
8895
  const days = Math.max(1, Number(opts.days ?? 7));
8569
8896
  const scopeFilter = opts.scope ?? "team";
8570
8897
  const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
8571
- const all = await loadMemoriesFromDir25(paths.memoriesDir);
8898
+ const all = await loadMemoriesFromDir26(paths.memoriesDir);
8572
8899
  const usage = await loadUsageIndex21(paths);
8573
8900
  const recent = all.filter(({ memory: mem }) => {
8574
8901
  const fm = mem.frontmatter;
@@ -8643,20 +8970,20 @@ function registerMemoryDigest(program2) {
8643
8970
 
8644
8971
  // src/commands/session-end.ts
8645
8972
  import { writeFile as writeFile25, mkdir as mkdir13, readFile as readFile14, rm as rm2 } from "fs/promises";
8646
- import { existsSync as existsSync50 } from "fs";
8973
+ import { existsSync as existsSync53 } from "fs";
8647
8974
  import path33 from "path";
8648
8975
  import "commander";
8649
8976
  import {
8650
8977
  buildFrontmatter as buildFrontmatter10,
8651
8978
  findProjectRoot as findProjectRoot31,
8652
- loadMemoriesFromDir as loadMemoriesFromDir26,
8979
+ loadMemoriesFromDir as loadMemoriesFromDir27,
8653
8980
  memoryFilePath as memoryFilePath9,
8654
8981
  resolveHaivePaths as resolveHaivePaths28,
8655
8982
  serializeMemory as serializeMemory21
8656
8983
  } from "@hiveai/core";
8657
8984
  async function buildAutoRecap(paths) {
8658
8985
  const obsFile = path33.join(paths.haiveDir, ".cache", "observations.jsonl");
8659
- if (!existsSync50(obsFile)) return null;
8986
+ if (!existsSync53(obsFile)) return null;
8660
8987
  const raw = await readFile14(obsFile, "utf8").catch(() => "");
8661
8988
  if (!raw.trim()) return null;
8662
8989
  const lines = raw.split("\n").filter(Boolean);
@@ -8737,7 +9064,7 @@ function registerSessionEnd(session2) {
8737
9064
  ).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) => {
8738
9065
  const root = findProjectRoot31(opts.dir);
8739
9066
  const paths = resolveHaivePaths28(root);
8740
- if (!existsSync50(paths.haiveDir)) {
9067
+ if (!existsSync53(paths.haiveDir)) {
8741
9068
  if (opts.auto || opts.quiet) return;
8742
9069
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
8743
9070
  process.exitCode = 1;
@@ -8769,7 +9096,7 @@ function registerSessionEnd(session2) {
8769
9096
  });
8770
9097
  const topic = recapTopic2(scope, opts.module);
8771
9098
  const filesTouched = parseCsv5(resolvedFiles);
8772
- const missingPaths = filesTouched.filter((p) => !existsSync50(path33.resolve(root, p)));
9099
+ const missingPaths = filesTouched.filter((p) => !existsSync53(path33.resolve(root, p)));
8773
9100
  if (missingPaths.length > 0 && !opts.quiet) {
8774
9101
  ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
8775
9102
  for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
@@ -8777,11 +9104,11 @@ function registerSessionEnd(session2) {
8777
9104
  const cleanupObservations = async () => {
8778
9105
  if (!opts.auto) return;
8779
9106
  const obsFile = path33.join(paths.haiveDir, ".cache", "observations.jsonl");
8780
- if (existsSync50(obsFile)) await rm2(obsFile).catch(() => {
9107
+ if (existsSync53(obsFile)) await rm2(obsFile).catch(() => {
8781
9108
  });
8782
9109
  };
8783
- if (existsSync50(paths.memoriesDir)) {
8784
- const existing = await loadMemoriesFromDir26(paths.memoriesDir);
9110
+ if (existsSync53(paths.memoriesDir)) {
9111
+ const existing = await loadMemoriesFromDir27(paths.memoriesDir);
8785
9112
  const topicMatch = existing.find(
8786
9113
  ({ memory: memory2 }) => memory2.frontmatter.topic === topic && memory2.frontmatter.scope === scope && (!opts.module || memory2.frontmatter.module === opts.module)
8787
9114
  );
@@ -8834,7 +9161,7 @@ function parseCsv5(value) {
8834
9161
  }
8835
9162
 
8836
9163
  // src/commands/snapshot.ts
8837
- import { existsSync as existsSync51 } from "fs";
9164
+ import { existsSync as existsSync54 } from "fs";
8838
9165
  import { readdir as readdir4 } from "fs/promises";
8839
9166
  import path34 from "path";
8840
9167
  import "commander";
@@ -8869,14 +9196,14 @@ function registerSnapshot(program2) {
8869
9196
  ).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) => {
8870
9197
  const root = findProjectRoot32(opts.dir);
8871
9198
  const paths = resolveHaivePaths29(root);
8872
- if (!existsSync51(paths.haiveDir)) {
9199
+ if (!existsSync54(paths.haiveDir)) {
8873
9200
  ui.error("No .ai/ found. Run `haive init` first.");
8874
9201
  process.exitCode = 1;
8875
9202
  return;
8876
9203
  }
8877
9204
  if (opts.list) {
8878
9205
  const contractsDir = path34.join(paths.haiveDir, "contracts");
8879
- if (!existsSync51(contractsDir)) {
9206
+ if (!existsSync54(contractsDir)) {
8880
9207
  console.log(ui.dim("No contract snapshots found."));
8881
9208
  return;
8882
9209
  }
@@ -9000,7 +9327,7 @@ function detectFormat(filePath) {
9000
9327
  }
9001
9328
 
9002
9329
  // src/commands/hub.ts
9003
- import { existsSync as existsSync53 } from "fs";
9330
+ import { existsSync as existsSync55 } from "fs";
9004
9331
  import { mkdir as mkdir14, readFile as readFile15, writeFile as writeFile26, copyFile } from "fs/promises";
9005
9332
  import path35 from "path";
9006
9333
  import { spawnSync as spawnSync3 } from "child_process";
@@ -9008,7 +9335,7 @@ import "commander";
9008
9335
  import {
9009
9336
  findProjectRoot as findProjectRoot33,
9010
9337
  loadConfig as loadConfig6,
9011
- loadMemoriesFromDir as loadMemoriesFromDir27,
9338
+ loadMemoriesFromDir as loadMemoriesFromDir28,
9012
9339
  resolveHaivePaths as resolveHaivePaths30,
9013
9340
  saveConfig as saveConfig2,
9014
9341
  serializeMemory as serializeMemory23
@@ -9102,7 +9429,7 @@ Next steps:
9102
9429
  return;
9103
9430
  }
9104
9431
  const hubRoot = path35.resolve(root, config.hubPath);
9105
- if (!existsSync53(hubRoot)) {
9432
+ if (!existsSync55(hubRoot)) {
9106
9433
  ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
9107
9434
  process.exitCode = 1;
9108
9435
  return;
@@ -9110,7 +9437,7 @@ Next steps:
9110
9437
  const projectName = path35.basename(root);
9111
9438
  const destDir = path35.join(hubRoot, ".ai", "memories", "shared", projectName);
9112
9439
  await mkdir14(destDir, { recursive: true });
9113
- const all = await loadMemoriesFromDir27(paths.memoriesDir);
9440
+ const all = await loadMemoriesFromDir28(paths.memoriesDir);
9114
9441
  const shared = all.filter(
9115
9442
  ({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && // Don't push imported memories (avoid echo loops)
9116
9443
  !memory2.frontmatter.tags.some((t) => t.startsWith("cross-repo:"))
@@ -9172,7 +9499,7 @@ Next steps:
9172
9499
  }
9173
9500
  const hubRoot = path35.resolve(root, config.hubPath);
9174
9501
  const hubSharedDir = path35.join(hubRoot, ".ai", "memories", "shared");
9175
- if (!existsSync53(hubSharedDir)) {
9502
+ if (!existsSync55(hubSharedDir)) {
9176
9503
  ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
9177
9504
  return;
9178
9505
  }
@@ -9227,7 +9554,7 @@ Next steps:
9227
9554
  ` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
9228
9555
  );
9229
9556
  const sharedDir = path35.join(paths.memoriesDir, "shared");
9230
- if (existsSync53(sharedDir)) {
9557
+ if (existsSync55(sharedDir)) {
9231
9558
  const { readdir: readdir5 } = await import("fs/promises");
9232
9559
  const sources = (await readdir5(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
9233
9560
  console.log(`
@@ -9239,7 +9566,7 @@ Next steps:
9239
9566
  } else {
9240
9567
  console.log(ui.dim(" No imported shared memories yet."));
9241
9568
  }
9242
- const all = await loadMemoriesFromDir27(paths.memoriesDir);
9569
+ const all = await loadMemoriesFromDir28(paths.memoriesDir);
9243
9570
  const outgoing = all.filter(
9244
9571
  ({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && !memory2.frontmatter.tags.some((t) => t.startsWith("cross-repo:"))
9245
9572
  );
@@ -9256,13 +9583,13 @@ Next steps:
9256
9583
 
9257
9584
  // src/commands/stats.ts
9258
9585
  import "commander";
9259
- import { existsSync as existsSync54 } from "fs";
9586
+ import { existsSync as existsSync56 } from "fs";
9260
9587
  import { mkdir as mkdir15, writeFile as writeFile27 } from "fs/promises";
9261
9588
  import path36 from "path";
9262
9589
  import {
9263
9590
  aggregateUsage,
9264
9591
  findProjectRoot as findProjectRoot34,
9265
- loadMemoriesFromDir as loadMemoriesFromDir28,
9592
+ loadMemoriesFromDir as loadMemoriesFromDir29,
9266
9593
  loadUsageIndex as loadUsageIndex23,
9267
9594
  parseSince,
9268
9595
  readUsageEvents as readUsageEvents2,
@@ -9334,8 +9661,8 @@ async function writeRoiReport(paths, root, sinceRaw, outRelative) {
9334
9661
  const size = await usageLogSize(paths);
9335
9662
  let events = await readUsageEvents2(paths);
9336
9663
  let memoryCount = { team: 0, personal: 0, total_skipped_session: 0 };
9337
- if (existsSync54(paths.memoriesDir)) {
9338
- const mems = await loadMemoriesFromDir28(paths.memoriesDir);
9664
+ if (existsSync56(paths.memoriesDir)) {
9665
+ const mems = await loadMemoriesFromDir29(paths.memoriesDir);
9339
9666
  for (const { memory: memory2 } of mems) {
9340
9667
  const fm = memory2.frontmatter;
9341
9668
  if (fm.type === "session_recap") memoryCount.total_skipped_session++;
@@ -9555,14 +9882,14 @@ function summarize(name, t0, payload, notes) {
9555
9882
 
9556
9883
  // src/commands/memory-suggest.ts
9557
9884
  import { mkdir as mkdir16, writeFile as writeFile28 } from "fs/promises";
9558
- import { existsSync as existsSync55 } from "fs";
9885
+ import { existsSync as existsSync57 } from "fs";
9559
9886
  import path37 from "path";
9560
9887
  import "commander";
9561
9888
  import {
9562
9889
  aggregateUsage as aggregateUsage2,
9563
9890
  buildFrontmatter as buildFrontmatter11,
9564
9891
  findProjectRoot as findProjectRoot36,
9565
- loadMemoriesFromDir as loadMemoriesFromDir29,
9892
+ loadMemoriesFromDir as loadMemoriesFromDir30,
9566
9893
  memoryFilePath as memoryFilePath10,
9567
9894
  parseSince as parseSince2,
9568
9895
  readUsageEvents as readUsageEvents3,
@@ -9626,7 +9953,7 @@ function registerMemorySuggest(memory2) {
9626
9953
  }
9627
9954
  const created = [];
9628
9955
  const skipped = [];
9629
- const existing = existsSync55(paths.memoriesDir) ? await loadMemoriesFromDir29(paths.memoriesDir) : [];
9956
+ const existing = existsSync57(paths.memoriesDir) ? await loadMemoriesFromDir30(paths.memoriesDir) : [];
9630
9957
  for (const s of top) {
9631
9958
  const slug = slugify(s.query);
9632
9959
  if (!slug) {
@@ -9650,7 +9977,7 @@ function registerMemorySuggest(memory2) {
9650
9977
  const body = renderTemplate(s);
9651
9978
  const file = memoryFilePath10(paths, fm.scope, fm.id, fm.module);
9652
9979
  await mkdir16(path37.dirname(file), { recursive: true });
9653
- if (existsSync55(file)) {
9980
+ if (existsSync57(file)) {
9654
9981
  skipped.push({ query: s.query, reason: `file already exists at ${path37.relative(root, file)}` });
9655
9982
  continue;
9656
9983
  }
@@ -9748,14 +10075,14 @@ function truncate2(text, max) {
9748
10075
  }
9749
10076
 
9750
10077
  // src/commands/memory-archive.ts
9751
- import { existsSync as existsSync56 } from "fs";
10078
+ import { existsSync as existsSync58 } from "fs";
9752
10079
  import { writeFile as writeFile29 } from "fs/promises";
9753
10080
  import path38 from "path";
9754
10081
  import "commander";
9755
10082
  import {
9756
10083
  findProjectRoot as findProjectRoot37,
9757
10084
  getUsage as getUsage18,
9758
- loadMemoriesFromDir as loadMemoriesFromDir30,
10085
+ loadMemoriesFromDir as loadMemoriesFromDir31,
9759
10086
  loadUsageIndex as loadUsageIndex24,
9760
10087
  resolveHaivePaths as resolveHaivePaths34,
9761
10088
  serializeMemory as serializeMemory25
@@ -9767,7 +10094,7 @@ function registerMemoryArchive(memory2) {
9767
10094
  ).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) => {
9768
10095
  const root = findProjectRoot37(opts.dir);
9769
10096
  const paths = resolveHaivePaths34(root);
9770
- if (!existsSync56(paths.memoriesDir)) {
10097
+ if (!existsSync58(paths.memoriesDir)) {
9771
10098
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
9772
10099
  process.exitCode = 1;
9773
10100
  return;
@@ -9779,7 +10106,7 @@ function registerMemoryArchive(memory2) {
9779
10106
  return;
9780
10107
  }
9781
10108
  const cutoff = Date.now() - minDays * MS_PER_DAY2;
9782
- const all = await loadMemoriesFromDir30(paths.memoriesDir);
10109
+ const all = await loadMemoriesFromDir31(paths.memoriesDir);
9783
10110
  const usage = await loadUsageIndex24(paths);
9784
10111
  const typeFilter = opts.type === "all" ? null : opts.type ?? "attempt";
9785
10112
  const candidates = [];
@@ -9788,7 +10115,7 @@ function registerMemoryArchive(memory2) {
9788
10115
  if (typeFilter && fm.type !== typeFilter) continue;
9789
10116
  if (fm.status === "deprecated" || fm.status === "rejected") continue;
9790
10117
  const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
9791
- const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync56(path38.join(paths.root, p)));
10118
+ const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync58(path38.join(paths.root, p)));
9792
10119
  const isAnchorless = !hasAnyAnchor;
9793
10120
  if (!isAnchorless && !allPathsGone) continue;
9794
10121
  const u = getUsage18(usage, fm.id);
@@ -9862,7 +10189,7 @@ function parseDays(input) {
9862
10189
  }
9863
10190
 
9864
10191
  // src/commands/doctor.ts
9865
- import { existsSync as existsSync57 } from "fs";
10192
+ import { existsSync as existsSync59 } from "fs";
9866
10193
  import { stat } from "fs/promises";
9867
10194
  import "path";
9868
10195
  import { execSync as execSync3 } from "child_process";
@@ -9873,7 +10200,7 @@ import {
9873
10200
  getUsage as getUsage19,
9874
10201
  loadCodeMap as loadCodeMap5,
9875
10202
  loadConfig as loadConfig7,
9876
- loadMemoriesFromDir as loadMemoriesFromDir31,
10203
+ loadMemoriesFromDir as loadMemoriesFromDir32,
9877
10204
  loadUsageIndex as loadUsageIndex25,
9878
10205
  readUsageEvents as readUsageEvents4,
9879
10206
  resolveHaivePaths as resolveHaivePaths35
@@ -9886,7 +10213,7 @@ function registerDoctor(program2) {
9886
10213
  const root = findProjectRoot38(opts.dir);
9887
10214
  const paths = resolveHaivePaths35(root);
9888
10215
  const findings = [];
9889
- if (!existsSync57(paths.haiveDir)) {
10216
+ if (!existsSync59(paths.haiveDir)) {
9890
10217
  findings.push({
9891
10218
  severity: "error",
9892
10219
  code: "not-initialized",
@@ -9895,7 +10222,7 @@ function registerDoctor(program2) {
9895
10222
  });
9896
10223
  return emit(findings, opts);
9897
10224
  }
9898
- if (!existsSync57(paths.projectContext)) {
10225
+ if (!existsSync59(paths.projectContext)) {
9899
10226
  findings.push({
9900
10227
  severity: "warn",
9901
10228
  code: "no-project-context",
@@ -9915,7 +10242,7 @@ function registerDoctor(program2) {
9915
10242
  });
9916
10243
  }
9917
10244
  }
9918
- const memories = existsSync57(paths.memoriesDir) ? await loadMemoriesFromDir31(paths.memoriesDir) : [];
10245
+ const memories = existsSync59(paths.memoriesDir) ? await loadMemoriesFromDir32(paths.memoriesDir) : [];
9919
10246
  const now = Date.now();
9920
10247
  if (memories.length === 0) {
9921
10248
  findings.push({
@@ -10039,7 +10366,7 @@ function registerDoctor(program2) {
10039
10366
  timeout: 3e3,
10040
10367
  stdio: ["ignore", "pipe", "ignore"]
10041
10368
  }).trim();
10042
- const cliVersion = "0.9.3";
10369
+ const cliVersion = "0.9.6";
10043
10370
  if (legacyRaw && legacyRaw !== cliVersion) {
10044
10371
  findings.push({
10045
10372
  severity: "warn",
@@ -10088,11 +10415,11 @@ function isSearchTool(name) {
10088
10415
  }
10089
10416
 
10090
10417
  // src/commands/playback.ts
10091
- import { existsSync as existsSync58 } from "fs";
10418
+ import { existsSync as existsSync60 } from "fs";
10092
10419
  import "commander";
10093
10420
  import {
10094
10421
  findProjectRoot as findProjectRoot39,
10095
- loadMemoriesFromDir as loadMemoriesFromDir32,
10422
+ loadMemoriesFromDir as loadMemoriesFromDir33,
10096
10423
  parseSince as parseSince3,
10097
10424
  readUsageEvents as readUsageEvents5,
10098
10425
  resolveHaivePaths as resolveHaivePaths36
@@ -10118,7 +10445,7 @@ function registerPlayback(program2) {
10118
10445
  const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
10119
10446
  const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
10120
10447
  const sessions = bucketSessions(filtered, gapMs);
10121
- const all = existsSync58(paths.memoriesDir) ? await loadMemoriesFromDir32(paths.memoriesDir) : [];
10448
+ const all = existsSync60(paths.memoriesDir) ? await loadMemoriesFromDir33(paths.memoriesDir) : [];
10122
10449
  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);
10123
10450
  const enriched = sessions.map((s, i) => {
10124
10451
  const startMs = Date.parse(s.start);
@@ -10312,11 +10639,11 @@ function runCommand3(cmd, args, cwd) {
10312
10639
  }
10313
10640
 
10314
10641
  // src/commands/welcome.ts
10315
- import { existsSync as existsSync59 } from "fs";
10642
+ import { existsSync as existsSync61 } from "fs";
10316
10643
  import "commander";
10317
10644
  import {
10318
10645
  findProjectRoot as findProjectRoot41,
10319
- loadMemoriesFromDir as loadMemoriesFromDir33,
10646
+ loadMemoriesFromDir as loadMemoriesFromDir34,
10320
10647
  resolveHaivePaths as resolveHaivePaths38
10321
10648
  } from "@hiveai/core";
10322
10649
  var TYPE_RANK = {
@@ -10333,12 +10660,12 @@ function registerWelcome(program2) {
10333
10660
  ).option("--limit <n>", "maximum memories listed", "20").option("-d, --dir <dir>", "project root").action(async (opts) => {
10334
10661
  const root = findProjectRoot41(opts.dir);
10335
10662
  const paths = resolveHaivePaths38(root);
10336
- if (!existsSync59(paths.memoriesDir)) {
10663
+ if (!existsSync61(paths.memoriesDir)) {
10337
10664
  ui.error(`No memories at ${paths.memoriesDir}. Run 'haive init' first.`);
10338
10665
  process.exitCode = 1;
10339
10666
  return;
10340
10667
  }
10341
- const all = await loadMemoriesFromDir33(paths.memoriesDir);
10668
+ const all = await loadMemoriesFromDir34(paths.memoriesDir);
10342
10669
  const team = all.filter(
10343
10670
  ({ memory: memory2 }) => memory2.frontmatter.scope === "team" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && memory2.frontmatter.status !== "stale" && memory2.frontmatter.type !== "session_recap"
10344
10671
  );
@@ -10374,18 +10701,18 @@ function registerWelcome(program2) {
10374
10701
  }
10375
10702
 
10376
10703
  // src/commands/memory-lint.ts
10377
- import { existsSync as existsSync60 } from "fs";
10704
+ import { existsSync as existsSync63 } from "fs";
10378
10705
  import "commander";
10379
10706
  import {
10380
10707
  findProjectRoot as findProjectRoot42,
10381
- loadMemoriesFromDir as loadMemoriesFromDir34,
10708
+ loadMemoriesFromDir as loadMemoriesFromDir35,
10382
10709
  resolveHaivePaths as resolveHaivePaths39
10383
10710
  } from "@hiveai/core";
10384
10711
  async function lintMemoriesAsync(root) {
10385
10712
  const paths = resolveHaivePaths39(root);
10386
10713
  const out = [];
10387
- if (!existsSync60(paths.memoriesDir)) return out;
10388
- const loaded = await loadMemoriesFromDir34(paths.memoriesDir);
10714
+ if (!existsSync63(paths.memoriesDir)) return out;
10715
+ const loaded = await loadMemoriesFromDir35(paths.memoriesDir);
10389
10716
  const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
10390
10717
  for (const { filePath, memory: memory2 } of loaded) {
10391
10718
  const fm = memory2.frontmatter;
@@ -10472,11 +10799,182 @@ function registerMemoryLint(parent) {
10472
10799
  });
10473
10800
  }
10474
10801
 
10802
+ // src/commands/memory-suggest-topic.ts
10803
+ import "commander";
10804
+ import { MemoryTypeSchema as MemoryTypeSchema2, suggestTopicKey as suggestTopicKey2 } from "@hiveai/core";
10805
+ function registerMemorySuggestTopic(memory2) {
10806
+ memory2.command("suggest-topic").description("Suggest a stable topic key (topic-upsert) from type + title phrase").requiredOption(
10807
+ "--type <type>",
10808
+ "convention | decision | gotcha | architecture | glossary | attempt | session_recap"
10809
+ ).argument("<title>", "Short title or phrase to slugify").action((title, opts) => {
10810
+ const parsed = MemoryTypeSchema2.safeParse(opts.type);
10811
+ if (!parsed.success) {
10812
+ ui.error(`Invalid type: ${opts.type}`);
10813
+ process.exit(1);
10814
+ }
10815
+ const suggestion = suggestTopicKey2(parsed.data, title);
10816
+ console.log(JSON.stringify({ type: parsed.data, ...suggestion }, null, 2));
10817
+ });
10818
+ }
10819
+
10820
+ // src/commands/resolve-project.ts
10821
+ import path40 from "path";
10822
+ import "commander";
10823
+ import { resolveProjectInfo as resolveProjectInfo2 } from "@hiveai/core";
10824
+ function registerResolveProject(program2) {
10825
+ program2.command("resolve-project").description(
10826
+ "Print JSON for hAIve project root resolution (HAIVE_PROJECT_ROOT, markers, .ai layout)."
10827
+ ).option("-d, --dir <dir>", "working directory", process.cwd()).action((opts) => {
10828
+ const info = resolveProjectInfo2({ cwd: path40.resolve(opts.dir) });
10829
+ console.log(JSON.stringify({ ok: true, info }, null, 2));
10830
+ });
10831
+ }
10832
+
10833
+ // src/commands/runtime-journal.ts
10834
+ import { existsSync as existsSync64 } from "fs";
10835
+ import path41 from "path";
10836
+ import "commander";
10837
+ import {
10838
+ appendRuntimeJournalEntry as appendRuntimeJournalEntry3,
10839
+ findProjectRoot as findProjectRoot43,
10840
+ readRuntimeJournalTail as readRuntimeJournalTail2,
10841
+ resolveHaivePaths as resolveHaivePaths40
10842
+ } from "@hiveai/core";
10843
+ function registerRuntime(program2) {
10844
+ const runtime = program2.command("runtime").description(
10845
+ "Local-only .ai/.runtime helpers (not versioned team memory). See session-journal.ndjson."
10846
+ );
10847
+ const journal = runtime.command("journal").description("Append or read the machine-local session journal (NDJSON)");
10848
+ journal.command("append").description("Append one JSON line to .ai/.runtime/session-journal.ndjson").argument("<message>", "short text to log").option("-k, --kind <kind>", "note | session_end | mcp", "note").option("-d, --dir <dir>", "project root", process.cwd()).action(async (message, opts) => {
10849
+ const root = path41.resolve(opts.dir ?? process.cwd());
10850
+ const paths = resolveHaivePaths40(findProjectRoot43(root));
10851
+ const raw = opts.kind ?? "note";
10852
+ const kind = ["note", "session_end", "mcp"].includes(raw) ? raw : "note";
10853
+ await appendRuntimeJournalEntry3(paths, { kind, message });
10854
+ ui.success(`Appended to ${path41.relative(root, paths.runtimeDir)}/session-journal.ndjson`);
10855
+ });
10856
+ journal.command("tail").description("Print the last N entries from the runtime session journal as JSON").option("-n, --limit <n>", "number of lines", "30").option("-d, --dir <dir>", "project root", process.cwd()).action(async (opts) => {
10857
+ const root = path41.resolve(opts.dir ?? process.cwd());
10858
+ const paths = resolveHaivePaths40(findProjectRoot43(root));
10859
+ const limit = Math.min(500, Math.max(1, parseInt(opts.limit, 10) || 30));
10860
+ if (!existsSync64(paths.haiveDir)) {
10861
+ ui.error("No .ai/ \u2014 run `haive init` first.");
10862
+ process.exitCode = 1;
10863
+ return;
10864
+ }
10865
+ const entries = await readRuntimeJournalTail2(paths, limit);
10866
+ if (entries.length === 0) {
10867
+ ui.info("Journal empty or missing.");
10868
+ return;
10869
+ }
10870
+ console.log(JSON.stringify({ entries, count: entries.length }, null, 2));
10871
+ });
10872
+ }
10873
+
10874
+ // src/commands/memory-timeline.ts
10875
+ import { existsSync as existsSync65 } from "fs";
10876
+ import path43 from "path";
10877
+ import "commander";
10878
+ import {
10879
+ collectTimelineEntries as collectTimelineEntries2,
10880
+ findProjectRoot as findProjectRoot44,
10881
+ resolveHaivePaths as resolveHaivePaths41
10882
+ } from "@hiveai/core";
10883
+ function registerMemoryTimeline(memory2) {
10884
+ memory2.command("timeline").description(
10885
+ "List related memories chronologically (topic, related_ids, anchors) \u2014 same logic as MCP mem_timeline."
10886
+ ).option("--id <id>", "seed memory id").option("--topic <key>", "filter by frontmatter.topic (use without --id for topic-only)").option("-n, --limit <n>", "max entries", "30").option("-d, --dir <dir>", "project root", process.cwd()).action(async (opts) => {
10887
+ if (!opts.id && !opts.topic) {
10888
+ ui.error("Provide --id and/or --topic.");
10889
+ process.exitCode = 1;
10890
+ return;
10891
+ }
10892
+ const root = path43.resolve(opts.dir ?? process.cwd());
10893
+ const paths = resolveHaivePaths41(findProjectRoot44(root));
10894
+ if (!existsSync65(paths.memoriesDir)) {
10895
+ ui.error("No memories \u2014 run `haive init`.");
10896
+ process.exitCode = 1;
10897
+ return;
10898
+ }
10899
+ const limit = Math.min(100, Math.max(1, parseInt(opts.limit, 10) || 30));
10900
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
10901
+ const { entries, notice } = collectTimelineEntries2(all, {
10902
+ memoryId: opts.id,
10903
+ topic: opts.topic,
10904
+ limit
10905
+ });
10906
+ if (notice) ui.warn(notice);
10907
+ console.log(JSON.stringify({ entries, total: entries.length }, null, 2));
10908
+ });
10909
+ }
10910
+
10911
+ // src/commands/memory-conflict-candidates.ts
10912
+ import { existsSync as existsSync66 } from "fs";
10913
+ import path44 from "path";
10914
+ import "commander";
10915
+ import {
10916
+ findLexicalConflictPairs as findLexicalConflictPairs2,
10917
+ findTopicStatusConflictPairs as findTopicStatusConflictPairs2,
10918
+ findProjectRoot as findProjectRoot45,
10919
+ resolveHaivePaths as resolveHaivePaths42
10920
+ } from "@hiveai/core";
10921
+ function parseTypes(csv) {
10922
+ const allowed = ["decision", "architecture", "convention", "gotcha"];
10923
+ const parts = csv.split(",").map((s) => s.trim().toLowerCase());
10924
+ const out = parts.filter((p) => allowed.includes(p));
10925
+ return out.length ? out : ["decision", "architecture"];
10926
+ }
10927
+ function registerMemoryConflictCandidates(memory2) {
10928
+ memory2.command("conflict-candidates").description(
10929
+ "Heuristic conflict candidates (lexical Jaccard + same-topic validated/rejected pairs) \u2014 aligns with MCP mem_conflict_candidates."
10930
+ ).option("-d, --dir <dir>", "project root", process.cwd()).option("--since-days <n>", "only memories created within N days (lexical scan)", "365").option(
10931
+ "--types <csv>",
10932
+ "decision,architecture,convention,gotcha (lexical scan)",
10933
+ "decision,architecture"
10934
+ ).option("--min-jaccard <x>", "minimum Jaccard for lexical pairs", "0.45").option("--max-pairs <n>", "cap lexical pairs", "20").option("--max-scan <n>", "max memories scanned (lexical)", "500").option("--max-topic-pairs <n>", "cap topic/status pairs", "20").action(async (opts) => {
10935
+ const root = path44.resolve(opts.dir ?? process.cwd());
10936
+ const paths = resolveHaivePaths42(findProjectRoot45(root));
10937
+ if (!existsSync66(paths.memoriesDir)) {
10938
+ ui.error("No memories \u2014 run `haive init`.");
10939
+ process.exitCode = 1;
10940
+ return;
10941
+ }
10942
+ const sinceDays = Math.max(1, parseInt(opts.sinceDays, 10) || 365);
10943
+ const minJaccard = parseFloat(opts.minJaccard) || 0.45;
10944
+ const maxPairs = Math.min(100, Math.max(1, parseInt(opts.maxPairs, 10) || 20));
10945
+ const maxScan = Math.min(2e3, Math.max(1, parseInt(opts.maxScan, 10) || 500));
10946
+ const maxTopicPairs = Math.min(100, Math.max(1, parseInt(opts.maxTopicPairs, 10) || 20));
10947
+ const all = await loadMemoriesFromDir25(paths.memoriesDir);
10948
+ const lexical = findLexicalConflictPairs2(all, {
10949
+ sinceDays,
10950
+ types: parseTypes(opts.types),
10951
+ minJaccard,
10952
+ maxPairs,
10953
+ maxScan
10954
+ });
10955
+ const topicStatusPairs = findTopicStatusConflictPairs2(all, maxTopicPairs);
10956
+ console.log(
10957
+ JSON.stringify(
10958
+ {
10959
+ pairs: lexical.pairs,
10960
+ topic_status_pairs: topicStatusPairs,
10961
+ scanned: lexical.scanned,
10962
+ truncated: lexical.truncated
10963
+ },
10964
+ null,
10965
+ 2
10966
+ )
10967
+ );
10968
+ });
10969
+ }
10970
+
10475
10971
  // src/index.ts
10476
- var program = new Command42();
10477
- program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.9.3");
10972
+ var program = new Command47();
10973
+ program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.9.6");
10478
10974
  registerInit(program);
10479
10975
  registerWelcome(program);
10976
+ registerResolveProject(program);
10977
+ registerRuntime(program);
10480
10978
  registerMcp(program);
10481
10979
  registerBriefing(program);
10482
10980
  registerTui(program);
@@ -10507,6 +11005,9 @@ registerMemoryImport(memory);
10507
11005
  registerMemoryImportChangelog(memory);
10508
11006
  registerMemoryDigest(memory);
10509
11007
  registerMemorySuggest(memory);
11008
+ registerMemorySuggestTopic(memory);
11009
+ registerMemoryTimeline(memory);
11010
+ registerMemoryConflictCandidates(memory);
10510
11011
  registerMemoryArchive(memory);
10511
11012
  registerMemoryLint(memory);
10512
11013
  var session = program.command("session").description("Manage session lifecycle");