@hiveai/cli 0.9.2 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command40 } from "commander";
4
+ import { Command as Command42 } from "commander";
5
5
 
6
6
  // src/commands/briefing.ts
7
7
  import { existsSync } from "fs";
@@ -9,6 +9,7 @@ import { readFile } from "fs/promises";
9
9
  import path from "path";
10
10
  import "commander";
11
11
  import {
12
+ extractActionsBriefBody,
12
13
  findProjectRoot,
13
14
  literalMatchesAllTokens,
14
15
  literalMatchesAnyToken,
@@ -17,6 +18,7 @@ import {
17
18
  loadUsageIndex,
18
19
  memoryMatchesAnchorPaths,
19
20
  queryCodeMap,
21
+ resolveBriefingBudget,
20
22
  resolveHaivePaths,
21
23
  tokenizeQuery,
22
24
  trackReads
@@ -195,7 +197,7 @@ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
195
197
  if (!f) continue;
196
198
  counts.set(f, (counts.get(f) ?? 0) + 1);
197
199
  }
198
- let entries = [...counts.entries()].map(([path39, changes]) => ({ path: path39, changes }));
200
+ let entries = [...counts.entries()].map(([path40, changes]) => ({ path: path40, changes }));
199
201
  const lowerPaths = filePaths.map((p) => p.toLowerCase());
200
202
  if (lowerPaths.length > 0) {
201
203
  entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
@@ -284,8 +286,16 @@ var TokenBudgetWriter = class {
284
286
  };
285
287
  function registerBriefing(program2) {
286
288
  program2.command("briefing").description(
287
- 'Print the full project briefing: last session recap + project context + relevant memories.\n Equivalent to calling get_briefing via MCP. Run before starting any task.\n\n Examples:\n haive briefing\n haive briefing --task "add Stripe payment" --files src/payments/PaymentService.ts\n haive briefing --symbols PaymentService,TenantFilter # look up where symbols live\n'
289
+ 'Print the full project briefing: last session recap + project context + relevant memories.\n Equivalent to calling get_briefing via MCP. Run before starting any task.\n\n Examples:\n haive briefing\n haive briefing --task "add Stripe payment" --files src/payments/PaymentService.ts\n haive briefing --budget quick --task "tiny fix"\n'
288
290
  ).option("--task <text>", "what you are about to do \u2014 filters memories by relevance").option("--files <csv>", "comma-separated file paths being worked on (surfaces anchored memories)").option("--symbols <csv>", "symbol names to look up in the code-map (e.g. PaymentService,TenantFilter) \u2014 requires haive index code").option("--max-memories <n>", "cap on memories surfaced", "10").option("--max-tokens <n>", "approximate token budget for the entire briefing (truncates if exceeded)").option("--explain-source", "annotate each memory with [source: <relative-path> \xB7 anchors: <files>] for traceable citations").option("--radar", "force project radar (recent commits, open TODOs, hot files) even when memories are plentiful").option("--no-radar", "disable the project radar even when memories are scarce").option(
291
+ "--budget <preset>",
292
+ "align with MCP get_briefing budget_preset: quick | balanced | deep \u2014 sets cap + truncation budget (overrides --max-memories / replaces default open-ended output)",
293
+ void 0
294
+ ).option(
295
+ "--memory-format <mode>",
296
+ "printed memory bodies: full (default) | actions (cheap bullet-focused excerpt)",
297
+ "full"
298
+ ).option(
289
299
  "--scope <scope>",
290
300
  "personal | team | shared | all (default: all \u2014 includes team + shared cross-repo memories)",
291
301
  "all"
@@ -297,8 +307,24 @@ function registerBriefing(program2) {
297
307
  ).option("-d, --dir <dir>", "project root").action(async (opts) => {
298
308
  const root = findProjectRoot(opts.dir);
299
309
  const paths = resolveHaivePaths(root);
300
- const budgetTokens = opts.maxTokens ? Math.max(100, Number(opts.maxTokens)) : null;
301
- const writer = budgetTokens ? new TokenBudgetWriter(budgetTokens * CHARS_PER_TOKEN) : null;
310
+ let budgetPreset = null;
311
+ if (opts.budget) {
312
+ const b = opts.budget.trim().toLowerCase();
313
+ if (b === "quick" || b === "balanced" || b === "deep") budgetPreset = b;
314
+ else ui.warn(`Unknown --budget '${opts.budget}' \u2014 ignoring (use quick|balanced|deep).`);
315
+ }
316
+ let maxMemories = Math.max(1, Number(opts.maxMemories ?? 10));
317
+ let budgetTokensCap = opts.maxTokens ? Math.max(100, Number(opts.maxTokens)) : null;
318
+ if (budgetPreset !== null) {
319
+ const presetNums = resolveBriefingBudget(budgetPreset, {
320
+ max_tokens: 8e3,
321
+ max_memories: 8,
322
+ include_module_contexts: true
323
+ });
324
+ budgetTokensCap = presetNums.max_tokens;
325
+ maxMemories = presetNums.max_memories;
326
+ }
327
+ const writer = budgetTokensCap !== null ? new TokenBudgetWriter(budgetTokensCap * CHARS_PER_TOKEN) : null;
302
328
  const out = (text) => {
303
329
  if (writer) return writer.write(text);
304
330
  console.log(text);
@@ -352,7 +378,6 @@ function registerBriefing(program2) {
352
378
  const all = ownMemories;
353
379
  const filePaths = parseCsv(opts.files);
354
380
  const tokens = opts.task ? tokenizeQuery(opts.task) : null;
355
- const maxMemories = Math.max(1, Number(opts.maxMemories ?? 10));
356
381
  const scopeFilter = opts.scope ?? "all";
357
382
  const recaps = all.filter(({ memory: mem }) => mem.frontmatter.type === "session_recap").sort(
358
383
  (a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
@@ -454,7 +479,8 @@ function registerBriefing(program2) {
454
479
  if (anchorSymbols.length > 0) parts.push(`symbols: ${anchorSymbols.join(", ")}`);
455
480
  out(ui.dim(` [${parts.join(" \xB7 ")}]`));
456
481
  }
457
- out(item.memory.body.trim());
482
+ const memBody = opts.memoryFormat?.toLowerCase() === "actions" ? extractActionsBriefBody(item.memory.body) : item.memory.body.trim();
483
+ out(memBody);
458
484
  out("");
459
485
  }
460
486
  if (!stopped()) out(ui.dim(`${top.length} memor${top.length === 1 ? "y" : "ies"} surfaced`));
@@ -2715,6 +2741,7 @@ import {
2715
2741
  DEFAULT_AUTO_PROMOTE_RULE,
2716
2742
  deriveConfidence as deriveConfidence4,
2717
2743
  estimateTokens,
2744
+ extractActionsBriefBody as extractActionsBriefBody2,
2718
2745
  getUsage as getUsage5,
2719
2746
  inferModulesFromPaths as inferModulesFromPaths2,
2720
2747
  isAutoPromoteEligible,
@@ -2727,6 +2754,7 @@ import {
2727
2754
  loadUsageIndex as loadUsageIndex7,
2728
2755
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths22,
2729
2756
  queryCodeMap as queryCodeMap2,
2757
+ resolveBriefingBudget as resolveBriefingBudget2,
2730
2758
  serializeMemory as serializeMemory9,
2731
2759
  tokenizeQuery as tokenizeQuery22,
2732
2760
  trackReads as trackReads3,
@@ -2927,6 +2955,30 @@ var MemSaveInputSchema = {
2927
2955
  function bodyHash(body) {
2928
2956
  return createHash("sha256").update(body.trim()).digest("hex").slice(0, 12);
2929
2957
  }
2958
+ var WORD_RE = /\b[a-z0-9]{3,}\b/gi;
2959
+ function bodyTokenSet(body) {
2960
+ const raw = body.toLowerCase().match(WORD_RE) ?? [];
2961
+ return new Set(raw);
2962
+ }
2963
+ function maxBodySimilarity(incomingTokens, memories, scope, type, excludeIds) {
2964
+ if (incomingTokens.size < 6) return null;
2965
+ let best = null;
2966
+ const skip = excludeIds ?? /* @__PURE__ */ new Set();
2967
+ for (const { memory: memory2 } of memories) {
2968
+ const fm = memory2.frontmatter;
2969
+ if (skip.has(fm.id)) continue;
2970
+ if (fm.scope !== scope || fm.type !== type) continue;
2971
+ if (fm.status === "rejected" || fm.status === "deprecated") continue;
2972
+ const other = bodyTokenSet(memory2.body);
2973
+ if (other.size === 0) continue;
2974
+ let inter = 0;
2975
+ for (const t of incomingTokens) if (other.has(t)) inter++;
2976
+ const uni = incomingTokens.size + other.size - inter;
2977
+ const j = uni === 0 ? 0 : inter / uni;
2978
+ if (j >= 0.72 && (!best || j > best.score)) best = { score: j, id: fm.id };
2979
+ }
2980
+ return best;
2981
+ }
2930
2982
  async function memSave(input, ctx) {
2931
2983
  if (!existsSync42(ctx.paths.haiveDir)) {
2932
2984
  throw new Error(
@@ -2948,12 +3000,26 @@ async function memSave(input, ctx) {
2948
3000
  `Duplicate content detected \u2014 identical body already saved as "${hashDuplicate.memory.frontmatter.id}". Use mem_update to modify it, or change the body to add new information.`
2949
3001
  );
2950
3002
  }
3003
+ const incomingTokens = bodyTokenSet(input.body);
3004
+ function bodySimilarWarnings(excludeIds) {
3005
+ const dup = maxBodySimilarity(incomingTokens, existing, resolvedScope, input.type, excludeIds);
3006
+ if (!dup?.id) return {};
3007
+ const body_similar = {
3008
+ id: dup.id,
3009
+ score: Math.round(dup.score * 100) / 100
3010
+ };
3011
+ return {
3012
+ similarityWarning: `Body is ~${Math.round(dup.score * 100)}% similar (token overlap) to existing "${dup.id}" \u2014 consolidate if redundant.`,
3013
+ body_similar
3014
+ };
3015
+ }
2951
3016
  if (input.topic) {
2952
3017
  const topicMatch = existing.find(
2953
3018
  ({ memory: memory2 }) => memory2.frontmatter.topic === input.topic && memory2.frontmatter.scope === resolvedScope && (!input.module || memory2.frontmatter.module === input.module)
2954
3019
  );
2955
3020
  if (topicMatch) {
2956
3021
  const fm = topicMatch.memory.frontmatter;
3022
+ const { similarityWarning: simW, body_similar: bs } = bodySimilarWarnings(/* @__PURE__ */ new Set([fm.id]));
2957
3023
  const newFrontmatter = {
2958
3024
  ...fm,
2959
3025
  body: input.body,
@@ -2970,13 +3036,19 @@ async function memSave(input, ctx) {
2970
3036
  serializeMemory2({ frontmatter: newFrontmatter, body: input.body }),
2971
3037
  "utf8"
2972
3038
  );
3039
+ const mergedTw = [
3040
+ invalidPaths.length > 0 ? `Anchor path(s) not found in project: ${invalidPaths.join(", ")}. They will be marked stale by haive sync.` : null,
3041
+ simW ?? null
3042
+ ].filter(Boolean).join(" \u2014 ") || void 0;
2973
3043
  return {
2974
3044
  id: fm.id,
2975
3045
  scope: fm.scope,
2976
3046
  file_path: topicMatch.filePath,
2977
3047
  action: "updated",
2978
3048
  revision_count: newFrontmatter.revision_count,
2979
- ...invalidPaths.length > 0 ? { invalid_paths: invalidPaths, warning: `Anchor path(s) not found in project: ${invalidPaths.join(", ")}. They will be marked stale by haive sync.` } : {}
3049
+ ...mergedTw ? { warning: mergedTw } : {},
3050
+ ...bs ? { body_similar: bs } : {},
3051
+ ...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {}
2980
3052
  };
2981
3053
  }
2982
3054
  }
@@ -3018,9 +3090,11 @@ async function memSave(input, ctx) {
3018
3090
  }
3019
3091
  }
3020
3092
  await writeFile22(file, serializeMemory2({ frontmatter, body: input.body }), "utf8");
3093
+ const { similarityWarning: simWarnNew, body_similar: bsNew } = bodySimilarWarnings();
3021
3094
  const finalWarning = [
3022
3095
  invalidPaths.length > 0 ? `Anchor path(s) not found in project: ${invalidPaths.join(", ")}. They will be marked stale by \`haive sync\`.` : null,
3023
- warning ?? null
3096
+ warning ?? null,
3097
+ simWarnNew ?? null
3024
3098
  ].filter(Boolean).join(" \u2014 ") || void 0;
3025
3099
  return {
3026
3100
  id: frontmatter.id,
@@ -3029,6 +3103,7 @@ async function memSave(input, ctx) {
3029
3103
  action: "created",
3030
3104
  ...finalWarning ? { warning: finalWarning } : {},
3031
3105
  ...similar_found ? { similar_found } : {},
3106
+ ...bsNew ? { body_similar: bsNew } : {},
3032
3107
  ...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {}
3033
3108
  };
3034
3109
  }
@@ -3930,17 +4005,29 @@ var GetBriefingInputSchema = {
3930
4005
  ),
3931
4006
  include_stale: z17.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
3932
4007
  track: z17.boolean().default(true).describe("Increment read_count on returned memories"),
3933
- format: z17.enum(["full", "compact"]).default("full").describe(
3934
- "Output format: 'full' returns complete memory bodies; 'compact' returns id + 1-line summary only (call mem_get for details)."
4008
+ format: z17.enum(["full", "compact", "actions"]).default("full").describe(
4009
+ "Output format: 'full' returns memory bodies (honors token budget via truncation); 'compact' returns a 1-line summary per memory (call mem_get for detail); 'actions' squeezes bodies to actionable bullet lines \u2014 fewer tokens vs full."
3935
4010
  ),
3936
4011
  symbols: z17.array(z17.string()).default([]).describe(
3937
4012
  "Symbol names to look up in the code-map (e.g. ['PaymentService', 'TenantFilter']). Returns the file(s) exporting each symbol so agents don't need to grep. Requires `haive index code` to have been run."
3938
4013
  ),
3939
4014
  min_semantic_score: z17.number().min(0).max(1).default(0).describe(
3940
4015
  "Drop semantic-only memory hits whose cosine score is below this threshold. Useful to avoid weakly-related noise when the task is short or the corpus is broad. Has no effect on memories matched via anchor/module/literal \u2014 those are always kept. Try 0.25\u20130.4 for stricter matching."
4016
+ ),
4017
+ budget_preset: z17.enum(["quick", "balanced", "deep"]).optional().describe(
4018
+ "Shortcut token budget: 'quick' minimizes tokens/skip module CONTEXT slices; 'balanced' mirrors historical defaults; 'deep' uses a larger briefing. When set, overrides max_tokens, max_memories, and include_module_contexts."
3941
4019
  )
3942
4020
  };
4021
+ var GetBriefingZod = z17.object(GetBriefingInputSchema);
3943
4022
  async function getBriefing(input, ctx) {
4023
+ const resolvedBudget = resolveBriefingBudget2(input.budget_preset, {
4024
+ max_tokens: input.max_tokens,
4025
+ max_memories: input.max_memories,
4026
+ include_module_contexts: input.include_module_contexts
4027
+ });
4028
+ const briefingMaxTokens = resolvedBudget.max_tokens;
4029
+ const briefingMaxMemories = resolvedBudget.max_memories;
4030
+ const briefingIncludeModules = resolvedBudget.include_module_contexts;
3944
4031
  const inferred = inferModulesFromPaths2(input.files);
3945
4032
  const memories = [];
3946
4033
  let searchMode = "literal";
@@ -4052,8 +4139,8 @@ async function getBriefing(input, ctx) {
4052
4139
  const sb = reasonScore(b) + confidenceScore(b) + (b.semantic_score ?? 0);
4053
4140
  return sb - sa;
4054
4141
  });
4055
- for (const mem of ranked.slice(0, input.max_memories)) {
4056
- if (seen.size >= input.max_memories * 2) break;
4142
+ for (const mem of ranked.slice(0, briefingMaxMemories)) {
4143
+ if (seen.size >= briefingMaxMemories * 2) break;
4057
4144
  const loaded = byId.get(mem.id);
4058
4145
  if (!loaded) continue;
4059
4146
  for (const relId of loaded.memory.frontmatter.related_ids ?? []) {
@@ -4062,7 +4149,7 @@ async function getBriefing(input, ctx) {
4062
4149
  if (related) addOrUpdate(related, "anchor", void 0, "partial");
4063
4150
  }
4064
4151
  }
4065
- memories.push(...ranked.slice(0, input.max_memories));
4152
+ memories.push(...ranked.slice(0, briefingMaxMemories));
4066
4153
  if (input.track && memories.length > 0) {
4067
4154
  await trackReads3(ctx.paths, memories.map((m) => m.id));
4068
4155
  const freshUsage = await loadUsageIndex7(ctx.paths);
@@ -4142,7 +4229,7 @@ async function getBriefing(input, ctx) {
4142
4229
  }
4143
4230
  }
4144
4231
  }
4145
- const moduleContents = input.include_module_contexts ? await loadModuleContexts2(ctx, inferred) : [];
4232
+ const moduleContents = briefingIncludeModules ? await loadModuleContexts2(ctx, inferred) : [];
4146
4233
  const memoriesText = memories.map((m) => {
4147
4234
  const unverified = m.status === "proposed" ? " [UNVERIFIED \u2014 not yet validated]" : "";
4148
4235
  return `### ${m.id} (${m.scope}/${m.type}, ${m.confidence})${unverified}
@@ -4160,7 +4247,7 @@ ${m.content}`).join("\n\n---\n\n"),
4160
4247
  },
4161
4248
  { key: "memories", text: memoriesText, weight: 4, mode: "head" }
4162
4249
  ],
4163
- input.max_tokens
4250
+ briefingMaxTokens
4164
4251
  );
4165
4252
  const projectSlice = slices.find((s) => s.key === "project");
4166
4253
  const modulesSlice = slices.find((s) => s.key === "modules");
@@ -4202,7 +4289,10 @@ ${m.content}`).join("\n\n---\n\n"),
4202
4289
  const createdAt = loaded?.memory.frontmatter.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
4203
4290
  if (isDecaying(u, createdAt)) decayWarnings.push(m.id);
4204
4291
  }
4205
- const outputMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : trimmedMemories;
4292
+ const outputMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : input.format === "actions" ? trimmedMemories.map((m) => ({
4293
+ ...m,
4294
+ body: extractActionsBriefBody2(m.body)
4295
+ })) : trimmedMemories;
4206
4296
  let symbolLocations;
4207
4297
  const symbolsToLookup = new Set(input.symbols);
4208
4298
  for (const m of outputMemories) {
@@ -4324,6 +4414,11 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
4324
4414
  "After completing the task: capture new gotchas with mem_observe, failed approaches with mem_tried, validated patterns with mem_save."
4325
4415
  );
4326
4416
  }
4417
+ if (outputMemories.length > 2 && !input.budget_preset && input.task && !hints.some((h) => h.includes("budget_preset"))) {
4418
+ hints.push(
4419
+ "For tighter token budgets on small tasks pass budget_preset:'quick'; for refactor-sized work use budget_preset:'deep'."
4420
+ );
4421
+ }
4327
4422
  }
4328
4423
  return {
4329
4424
  ...input.task ? { task: input.task } : {},
@@ -4346,7 +4441,8 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
4346
4441
  ...hints.length > 0 ? { hints } : {},
4347
4442
  estimated_tokens: totalTokens,
4348
4443
  budget: {
4349
- max_tokens: input.max_tokens,
4444
+ max_tokens: briefingMaxTokens,
4445
+ ...input.budget_preset ? { preset_applied: input.budget_preset } : {},
4350
4446
  spent: {
4351
4447
  project: projectSlice.estimatedTokens,
4352
4448
  modules: modulesSlice.estimatedTokens,
@@ -4542,7 +4638,7 @@ var MemRelevantToInputSchema = {
4542
4638
  files: z21.array(z21.string()).default([]).describe("Optional: files you are about to edit \u2014 surfaces anchored memories."),
4543
4639
  limit: z21.number().int().positive().max(30).default(8).describe("Cap on returned memories."),
4544
4640
  min_semantic_score: z21.number().min(0).max(1).default(0.25).describe("Drop weakly-related semantic hits below this cosine threshold."),
4545
- format: z21.enum(["full", "compact"]).default("full").describe("'compact' = id + 1-line summary; 'full' = complete bodies.")
4641
+ format: z21.enum(["full", "compact", "actions"]).default("full").describe("'compact' = id + 1-line summary; 'full' = complete bodies; 'actions' = bullet-first excerpts.")
4546
4642
  };
4547
4643
  async function memRelevantTo(input, ctx) {
4548
4644
  const briefingInput = {
@@ -5711,7 +5807,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
5711
5807
  };
5712
5808
  }
5713
5809
  var SERVER_NAME = "haive";
5714
- var SERVER_VERSION = "0.9.2";
5810
+ var SERVER_VERSION = "0.9.3";
5715
5811
  function jsonResult(data) {
5716
5812
  return {
5717
5813
  content: [
@@ -5877,7 +5973,8 @@ function createHaiveServer(options = {}) {
5877
5973
  " task \u2014 what you are about to do (1\u20132 sentences) \u2014 ALWAYS provide this",
5878
5974
  " files \u2014 files you are about to edit \u2014 surfaces anchored memories",
5879
5975
  " symbols \u2014 symbol names to look up in the code-map (e.g. ['PaymentService'])",
5880
- " format \u2014 'full' (default) | 'compact' (1-line summaries, use when token budget is tight)",
5976
+ " format \u2014 'full' (default) | 'compact' (1-line) | 'actions' (bullet-first excerpts)",
5977
+ " budget_preset \u2014 'quick' | 'balanced' | 'deep' \u2014 scales max_tokens/memories/module contexts",
5881
5978
  "",
5882
5979
  "EXAMPLE USAGE:",
5883
5980
  " get_briefing({ task: 'add a Stripe payment integration', files: ['src/payments/'], symbols: ['PaymentService'] })",
@@ -6183,6 +6280,7 @@ function createHaiveServer(options = {}) {
6183
6280
  " files \u2014 files you'll edit (surfaces anchored memories)",
6184
6281
  " limit \u2014 cap on returned memories (default 8)",
6185
6282
  " min_semantic_score \u2014 drop weak semantic hits below this cosine (default 0.25)",
6283
+ " format \u2014 'full' | 'compact' | 'actions' (inherits get_briefing memory framing)",
6186
6284
  "",
6187
6285
  "RETURNS: { task, search_mode, memories: [...], hints?: [...], empty?: true }"
6188
6286
  ].join("\n"),
@@ -8703,6 +8801,7 @@ function registerSessionEnd(session2) {
8703
8801
  if (!opts.quiet) {
8704
8802
  ui.success(`Session recap updated (revision #${revisionCount})`);
8705
8803
  ui.info(`id=${fm.id} file=${path33.relative(root, topicMatch.filePath)}`);
8804
+ ui.info("Tip: `haive stats --export-report` generates a usage JSON suitable for dashboards.");
8706
8805
  }
8707
8806
  return;
8708
8807
  }
@@ -8725,6 +8824,7 @@ function registerSessionEnd(session2) {
8725
8824
  ui.success(`Session recap created`);
8726
8825
  ui.info(`id=${frontmatter.id} scope=${scope} file=${path33.relative(root, file)}`);
8727
8826
  ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
8827
+ ui.info("Tip: export a local MCP usage rollup with `haive stats --export-report .ai/tool-usage-roi-report.json`.");
8728
8828
  }
8729
8829
  });
8730
8830
  }
@@ -9156,9 +9256,13 @@ Next steps:
9156
9256
 
9157
9257
  // src/commands/stats.ts
9158
9258
  import "commander";
9259
+ import { existsSync as existsSync54 } from "fs";
9260
+ import { mkdir as mkdir15, writeFile as writeFile27 } from "fs/promises";
9261
+ import path36 from "path";
9159
9262
  import {
9160
9263
  aggregateUsage,
9161
9264
  findProjectRoot as findProjectRoot34,
9265
+ loadMemoriesFromDir as loadMemoriesFromDir28,
9162
9266
  loadUsageIndex as loadUsageIndex23,
9163
9267
  parseSince,
9164
9268
  readUsageEvents as readUsageEvents2,
@@ -9166,9 +9270,17 @@ import {
9166
9270
  usageLogSize
9167
9271
  } from "@hiveai/core";
9168
9272
  function registerStats(program2) {
9169
- program2.command("stats").description("Show MCP tool-usage stats over a window (e.g. --since 7d).").option("--since <window>", "ISO date or relative (e.g. '7d', '24h', '30m')", "30d").option("--json", "emit JSON instead of human-readable output", false).option("--memory-hits", "show top-read memories (which mems are actually being used)", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
9273
+ program2.command("stats").description("Show MCP tool-usage stats over a window (e.g. --since 7d).").option("--since <window>", "ISO date or relative (e.g. '7d', '24h', '30m')", "30d").option("--json", "emit JSON instead of human-readable output", false).option("--memory-hits", "show top-read memories (which mems are actually being used)", false).option(
9274
+ "--export-report <path>",
9275
+ "write a JSON rollup (tools + briefing counts + heuristic ROI hints). Parent dirs are created if needed.",
9276
+ void 0
9277
+ ).option("-d, --dir <dir>", "project root").action(async (opts) => {
9170
9278
  const root = findProjectRoot34(opts.dir);
9171
9279
  const paths = resolveHaivePaths31(root);
9280
+ if (opts.exportReport) {
9281
+ await writeRoiReport(paths, root, opts.since ?? "30d", opts.exportReport);
9282
+ return;
9283
+ }
9172
9284
  if (opts.memoryHits) {
9173
9285
  await renderMemoryHits(paths, opts);
9174
9286
  return;
@@ -9217,6 +9329,57 @@ function registerStats(program2) {
9217
9329
  }
9218
9330
  });
9219
9331
  }
9332
+ async function writeRoiReport(paths, root, sinceRaw, outRelative) {
9333
+ const outAbs = path36.isAbsolute(outRelative) ? path36.resolve(outRelative) : path36.resolve(root, outRelative);
9334
+ const size = await usageLogSize(paths);
9335
+ let events = await readUsageEvents2(paths);
9336
+ let memoryCount = { team: 0, personal: 0, total_skipped_session: 0 };
9337
+ if (existsSync54(paths.memoriesDir)) {
9338
+ const mems = await loadMemoriesFromDir28(paths.memoriesDir);
9339
+ for (const { memory: memory2 } of mems) {
9340
+ const fm = memory2.frontmatter;
9341
+ if (fm.type === "session_recap") memoryCount.total_skipped_session++;
9342
+ else if (fm.scope === "team") memoryCount.team++;
9343
+ else if (fm.scope === "personal") memoryCount.personal++;
9344
+ }
9345
+ }
9346
+ const sinceDt = parseSince(sinceRaw) ?? void 0;
9347
+ const aggregate = aggregateUsage(events, sinceDt);
9348
+ const inWindow = (at) => sinceDt === void 0 || Date.parse(at) >= sinceDt.getTime();
9349
+ const briefingCalls = events.filter((e) => inWindow(e.at) && e.tool === "get_briefing").length;
9350
+ let memoryHitsLeader = null;
9351
+ try {
9352
+ const usageIdx = await loadUsageIndex23(paths);
9353
+ const tops = Object.entries(usageIdx.by_id).map(([id, v]) => ({ id, read_count: v.read_count })).filter((x) => x.read_count > 0).sort((a, b) => b.read_count - a.read_count);
9354
+ memoryHitsLeader = tops[0] ?? null;
9355
+ } catch {
9356
+ memoryHitsLeader = null;
9357
+ }
9358
+ const roiHints = [
9359
+ "Correlate get_briefing calls with skipped multi-file exploration \u2014 proxies available via `pnpm benchmark:roi` at repo root.",
9360
+ "Prefer get_briefing(format:'actions') or budget_preset:'quick' for low-risk edits to reduce token pressure.",
9361
+ "Run `haive memory lint` in CI to keep the corpus actionable.",
9362
+ "Install the haive VS Code extension (packages/vscode) for always-on memory surfacing beside the editor."
9363
+ ];
9364
+ if (!size.exists || events.length === 0) {
9365
+ ui.warn("Usage log missing or empty \u2014 report still written with partial data.");
9366
+ events = [];
9367
+ }
9368
+ await mkdir15(path36.dirname(outAbs), { recursive: true });
9369
+ const payload = {
9370
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
9371
+ project_root: root,
9372
+ window_since: sinceRaw,
9373
+ usage_log_meta: size,
9374
+ memory_inventory: memoryCount,
9375
+ aggregate,
9376
+ get_briefing_calls_in_window: briefingCalls,
9377
+ top_memory_reads: memoryHitsLeader,
9378
+ roi_hints: roiHints
9379
+ };
9380
+ await writeFile27(outAbs, JSON.stringify(payload, null, 2), "utf8");
9381
+ ui.success(`Wrote ROI / usage rollup \u2192 ${outAbs}`);
9382
+ }
9220
9383
  async function renderMemoryHits(paths, opts) {
9221
9384
  const index = await loadUsageIndex23(paths);
9222
9385
  const since = parseSince(opts.since ?? "30d");
@@ -9391,15 +9554,15 @@ function summarize(name, t0, payload, notes) {
9391
9554
  }
9392
9555
 
9393
9556
  // src/commands/memory-suggest.ts
9394
- import { mkdir as mkdir15, writeFile as writeFile27 } from "fs/promises";
9395
- import { existsSync as existsSync54 } from "fs";
9396
- import path36 from "path";
9557
+ import { mkdir as mkdir16, writeFile as writeFile28 } from "fs/promises";
9558
+ import { existsSync as existsSync55 } from "fs";
9559
+ import path37 from "path";
9397
9560
  import "commander";
9398
9561
  import {
9399
9562
  aggregateUsage as aggregateUsage2,
9400
9563
  buildFrontmatter as buildFrontmatter11,
9401
9564
  findProjectRoot as findProjectRoot36,
9402
- loadMemoriesFromDir as loadMemoriesFromDir28,
9565
+ loadMemoriesFromDir as loadMemoriesFromDir29,
9403
9566
  memoryFilePath as memoryFilePath10,
9404
9567
  parseSince as parseSince2,
9405
9568
  readUsageEvents as readUsageEvents3,
@@ -9463,7 +9626,7 @@ function registerMemorySuggest(memory2) {
9463
9626
  }
9464
9627
  const created = [];
9465
9628
  const skipped = [];
9466
- const existing = existsSync54(paths.memoriesDir) ? await loadMemoriesFromDir28(paths.memoriesDir) : [];
9629
+ const existing = existsSync55(paths.memoriesDir) ? await loadMemoriesFromDir29(paths.memoriesDir) : [];
9467
9630
  for (const s of top) {
9468
9631
  const slug = slugify(s.query);
9469
9632
  if (!slug) {
@@ -9486,13 +9649,13 @@ function registerMemorySuggest(memory2) {
9486
9649
  fm.status = "draft";
9487
9650
  const body = renderTemplate(s);
9488
9651
  const file = memoryFilePath10(paths, fm.scope, fm.id, fm.module);
9489
- await mkdir15(path36.dirname(file), { recursive: true });
9490
- if (existsSync54(file)) {
9491
- skipped.push({ query: s.query, reason: `file already exists at ${path36.relative(root, file)}` });
9652
+ await mkdir16(path37.dirname(file), { recursive: true });
9653
+ if (existsSync55(file)) {
9654
+ skipped.push({ query: s.query, reason: `file already exists at ${path37.relative(root, file)}` });
9492
9655
  continue;
9493
9656
  }
9494
- await writeFile27(file, serializeMemory24({ frontmatter: fm, body }), "utf8");
9495
- created.push({ id: fm.id, file: path36.relative(root, file), query: s.query });
9657
+ await writeFile28(file, serializeMemory24({ frontmatter: fm, body }), "utf8");
9658
+ created.push({ id: fm.id, file: path37.relative(root, file), query: s.query });
9496
9659
  }
9497
9660
  if (opts.json) {
9498
9661
  console.log(JSON.stringify({ created, skipped }, null, 2));
@@ -9585,14 +9748,14 @@ function truncate2(text, max) {
9585
9748
  }
9586
9749
 
9587
9750
  // src/commands/memory-archive.ts
9588
- import { existsSync as existsSync55 } from "fs";
9589
- import { writeFile as writeFile28 } from "fs/promises";
9590
- import path37 from "path";
9751
+ import { existsSync as existsSync56 } from "fs";
9752
+ import { writeFile as writeFile29 } from "fs/promises";
9753
+ import path38 from "path";
9591
9754
  import "commander";
9592
9755
  import {
9593
9756
  findProjectRoot as findProjectRoot37,
9594
9757
  getUsage as getUsage18,
9595
- loadMemoriesFromDir as loadMemoriesFromDir29,
9758
+ loadMemoriesFromDir as loadMemoriesFromDir30,
9596
9759
  loadUsageIndex as loadUsageIndex24,
9597
9760
  resolveHaivePaths as resolveHaivePaths34,
9598
9761
  serializeMemory as serializeMemory25
@@ -9604,7 +9767,7 @@ function registerMemoryArchive(memory2) {
9604
9767
  ).option("--since <window>", "minimum age since last read (e.g. '180d', '6m')", "180d").option("--type <type>", "limit to a memory type (default 'attempt'). Pass 'all' to scan all types.", "attempt").option("--apply", "actually rewrite files (default: dry run)", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
9605
9768
  const root = findProjectRoot37(opts.dir);
9606
9769
  const paths = resolveHaivePaths34(root);
9607
- if (!existsSync55(paths.memoriesDir)) {
9770
+ if (!existsSync56(paths.memoriesDir)) {
9608
9771
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
9609
9772
  process.exitCode = 1;
9610
9773
  return;
@@ -9616,7 +9779,7 @@ function registerMemoryArchive(memory2) {
9616
9779
  return;
9617
9780
  }
9618
9781
  const cutoff = Date.now() - minDays * MS_PER_DAY2;
9619
- const all = await loadMemoriesFromDir29(paths.memoriesDir);
9782
+ const all = await loadMemoriesFromDir30(paths.memoriesDir);
9620
9783
  const usage = await loadUsageIndex24(paths);
9621
9784
  const typeFilter = opts.type === "all" ? null : opts.type ?? "attempt";
9622
9785
  const candidates = [];
@@ -9625,7 +9788,7 @@ function registerMemoryArchive(memory2) {
9625
9788
  if (typeFilter && fm.type !== typeFilter) continue;
9626
9789
  if (fm.status === "deprecated" || fm.status === "rejected") continue;
9627
9790
  const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
9628
- const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync55(path37.join(paths.root, p)));
9791
+ const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync56(path38.join(paths.root, p)));
9629
9792
  const isAnchorless = !hasAnyAnchor;
9630
9793
  if (!isAnchorless && !allPathsGone) continue;
9631
9794
  const u = getUsage18(usage, fm.id);
@@ -9673,7 +9836,7 @@ function registerMemoryArchive(memory2) {
9673
9836
  if (!found) continue;
9674
9837
  const fm = { ...found.memory.frontmatter, status: "deprecated" };
9675
9838
  try {
9676
- await writeFile28(c.filePath, serializeMemory25({ frontmatter: fm, body: found.memory.body }), "utf8");
9839
+ await writeFile29(c.filePath, serializeMemory25({ frontmatter: fm, body: found.memory.body }), "utf8");
9677
9840
  archived++;
9678
9841
  } catch (err) {
9679
9842
  if (!opts.json) {
@@ -9699,7 +9862,7 @@ function parseDays(input) {
9699
9862
  }
9700
9863
 
9701
9864
  // src/commands/doctor.ts
9702
- import { existsSync as existsSync56 } from "fs";
9865
+ import { existsSync as existsSync57 } from "fs";
9703
9866
  import { stat } from "fs/promises";
9704
9867
  import "path";
9705
9868
  import { execSync as execSync3 } from "child_process";
@@ -9710,7 +9873,7 @@ import {
9710
9873
  getUsage as getUsage19,
9711
9874
  loadCodeMap as loadCodeMap5,
9712
9875
  loadConfig as loadConfig7,
9713
- loadMemoriesFromDir as loadMemoriesFromDir30,
9876
+ loadMemoriesFromDir as loadMemoriesFromDir31,
9714
9877
  loadUsageIndex as loadUsageIndex25,
9715
9878
  readUsageEvents as readUsageEvents4,
9716
9879
  resolveHaivePaths as resolveHaivePaths35
@@ -9723,7 +9886,7 @@ function registerDoctor(program2) {
9723
9886
  const root = findProjectRoot38(opts.dir);
9724
9887
  const paths = resolveHaivePaths35(root);
9725
9888
  const findings = [];
9726
- if (!existsSync56(paths.haiveDir)) {
9889
+ if (!existsSync57(paths.haiveDir)) {
9727
9890
  findings.push({
9728
9891
  severity: "error",
9729
9892
  code: "not-initialized",
@@ -9732,7 +9895,7 @@ function registerDoctor(program2) {
9732
9895
  });
9733
9896
  return emit(findings, opts);
9734
9897
  }
9735
- if (!existsSync56(paths.projectContext)) {
9898
+ if (!existsSync57(paths.projectContext)) {
9736
9899
  findings.push({
9737
9900
  severity: "warn",
9738
9901
  code: "no-project-context",
@@ -9752,7 +9915,7 @@ function registerDoctor(program2) {
9752
9915
  });
9753
9916
  }
9754
9917
  }
9755
- const memories = existsSync56(paths.memoriesDir) ? await loadMemoriesFromDir30(paths.memoriesDir) : [];
9918
+ const memories = existsSync57(paths.memoriesDir) ? await loadMemoriesFromDir31(paths.memoriesDir) : [];
9756
9919
  const now = Date.now();
9757
9920
  if (memories.length === 0) {
9758
9921
  findings.push({
@@ -9876,7 +10039,7 @@ function registerDoctor(program2) {
9876
10039
  timeout: 3e3,
9877
10040
  stdio: ["ignore", "pipe", "ignore"]
9878
10041
  }).trim();
9879
- const cliVersion = "0.9.2";
10042
+ const cliVersion = "0.9.3";
9880
10043
  if (legacyRaw && legacyRaw !== cliVersion) {
9881
10044
  findings.push({
9882
10045
  severity: "warn",
@@ -9925,11 +10088,11 @@ function isSearchTool(name) {
9925
10088
  }
9926
10089
 
9927
10090
  // src/commands/playback.ts
9928
- import { existsSync as existsSync57 } from "fs";
10091
+ import { existsSync as existsSync58 } from "fs";
9929
10092
  import "commander";
9930
10093
  import {
9931
10094
  findProjectRoot as findProjectRoot39,
9932
- loadMemoriesFromDir as loadMemoriesFromDir31,
10095
+ loadMemoriesFromDir as loadMemoriesFromDir32,
9933
10096
  parseSince as parseSince3,
9934
10097
  readUsageEvents as readUsageEvents5,
9935
10098
  resolveHaivePaths as resolveHaivePaths36
@@ -9955,7 +10118,7 @@ function registerPlayback(program2) {
9955
10118
  const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
9956
10119
  const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
9957
10120
  const sessions = bucketSessions(filtered, gapMs);
9958
- const all = existsSync57(paths.memoriesDir) ? await loadMemoriesFromDir31(paths.memoriesDir) : [];
10121
+ const all = existsSync58(paths.memoriesDir) ? await loadMemoriesFromDir32(paths.memoriesDir) : [];
9959
10122
  const memByCreatedAt = all.filter(({ memory: memory2 }) => memory2.frontmatter.type !== "session_recap").map(({ memory: memory2 }) => ({ id: memory2.frontmatter.id, at: Date.parse(memory2.frontmatter.created_at) })).sort((a, b) => a.at - b.at);
9960
10123
  const enriched = sessions.map((s, i) => {
9961
10124
  const startMs = Date.parse(s.start);
@@ -10148,10 +10311,172 @@ function runCommand3(cmd, args, cwd) {
10148
10311
  });
10149
10312
  }
10150
10313
 
10314
+ // src/commands/welcome.ts
10315
+ import { existsSync as existsSync59 } from "fs";
10316
+ import "commander";
10317
+ import {
10318
+ findProjectRoot as findProjectRoot41,
10319
+ loadMemoriesFromDir as loadMemoriesFromDir33,
10320
+ resolveHaivePaths as resolveHaivePaths38
10321
+ } from "@hiveai/core";
10322
+ var TYPE_RANK = {
10323
+ decision: 0,
10324
+ architecture: 1,
10325
+ convention: 2,
10326
+ glossary: 3,
10327
+ gotcha: 4,
10328
+ attempt: 5
10329
+ };
10330
+ function registerWelcome(program2) {
10331
+ program2.command("welcome").description(
10332
+ "Onboarding checklist: ranks validated/proposed **team** memories by type.\nUse after `haive init` so new devs skim institutional knowledge quickly.\n\n haive welcome\n haive welcome --limit 15\n"
10333
+ ).option("--limit <n>", "maximum memories listed", "20").option("-d, --dir <dir>", "project root").action(async (opts) => {
10334
+ const root = findProjectRoot41(opts.dir);
10335
+ const paths = resolveHaivePaths38(root);
10336
+ if (!existsSync59(paths.memoriesDir)) {
10337
+ ui.error(`No memories at ${paths.memoriesDir}. Run 'haive init' first.`);
10338
+ process.exitCode = 1;
10339
+ return;
10340
+ }
10341
+ const all = await loadMemoriesFromDir33(paths.memoriesDir);
10342
+ const team = all.filter(
10343
+ ({ memory: memory2 }) => memory2.frontmatter.scope === "team" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && memory2.frontmatter.status !== "stale" && memory2.frontmatter.type !== "session_recap"
10344
+ );
10345
+ team.sort((a, b) => {
10346
+ const ta = TYPE_RANK[a.memory.frontmatter.type] ?? 99;
10347
+ const tb = TYPE_RANK[b.memory.frontmatter.type] ?? 99;
10348
+ if (ta !== tb) return ta - tb;
10349
+ const sta = a.memory.frontmatter.status === "validated" ? 0 : 1;
10350
+ const stb = b.memory.frontmatter.status === "validated" ? 0 : 1;
10351
+ if (sta !== stb) return sta - stb;
10352
+ return b.memory.frontmatter.created_at.localeCompare(a.memory.frontmatter.created_at);
10353
+ });
10354
+ const cap = Math.max(1, Math.min(500, Number(opts.limit) || 20));
10355
+ const pick = team.slice(0, cap);
10356
+ console.log(ui.bold(`hAIve welcome \u2014 ${pick.length} team memories (${root})`));
10357
+ console.log(ui.dim(`Next: invoke get_briefing with your task or run 'haive briefing --task "\u2026"'`));
10358
+ if (pick.length === 0) {
10359
+ ui.warn("No team memories yet \u2014 add some with 'haive memory add' or promote personal ones.");
10360
+ return;
10361
+ }
10362
+ let i = 1;
10363
+ for (const { memory: memory2 } of pick) {
10364
+ const fm = memory2.frontmatter;
10365
+ const head = memory2.body.match(/^#\s+(.+)/m)?.[1]?.trim();
10366
+ const line = head ?? fm.id;
10367
+ console.log(
10368
+ `${String(i).padStart(2, " ")} ${fm.type.padEnd(12)} ${fm.status.padEnd(10)} ${ui.dim(fm.id)}
10369
+ ${line}`
10370
+ );
10371
+ i++;
10372
+ }
10373
+ });
10374
+ }
10375
+
10376
+ // src/commands/memory-lint.ts
10377
+ import { existsSync as existsSync60 } from "fs";
10378
+ import "commander";
10379
+ import {
10380
+ findProjectRoot as findProjectRoot42,
10381
+ loadMemoriesFromDir as loadMemoriesFromDir34,
10382
+ resolveHaivePaths as resolveHaivePaths39
10383
+ } from "@hiveai/core";
10384
+ async function lintMemoriesAsync(root) {
10385
+ const paths = resolveHaivePaths39(root);
10386
+ const out = [];
10387
+ if (!existsSync60(paths.memoriesDir)) return out;
10388
+ const loaded = await loadMemoriesFromDir34(paths.memoriesDir);
10389
+ const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
10390
+ for (const { filePath, memory: memory2 } of loaded) {
10391
+ const fm = memory2.frontmatter;
10392
+ if (fm.type === "session_recap") continue;
10393
+ const body = memory2.body.trim();
10394
+ const naked = body.replace(/^#.*$/gm, "").replace(/```[\s\S]*?```/g, "").trim();
10395
+ if (naked.length < 40 && fm.status !== "rejected") {
10396
+ out.push({
10397
+ file: filePath,
10398
+ id: fm.id,
10399
+ severity: "warn",
10400
+ code: "SHORT_BODY",
10401
+ message: "Body looks very short (< ~40 chars of prose after headings). Prefer actionable detail."
10402
+ });
10403
+ }
10404
+ if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated") {
10405
+ out.push({
10406
+ file: filePath,
10407
+ id: fm.id,
10408
+ severity: "warn",
10409
+ code: "MISSING_ANCHOR",
10410
+ message: `${fm.type} is validated without anchor paths \u2014 add anchor.paths so haive sync can flag staleness.`
10411
+ });
10412
+ }
10413
+ if (fm.status === "stale" && !fm.stale_reason) {
10414
+ out.push({
10415
+ file: filePath,
10416
+ id: fm.id,
10417
+ severity: "info",
10418
+ code: "STALE_NO_REASON",
10419
+ message: "Status is stale but stale_reason is empty \u2014 document why when possible."
10420
+ });
10421
+ }
10422
+ if (fm.type === "glossary" && naked.length > 6e3) {
10423
+ out.push({
10424
+ file: filePath,
10425
+ id: fm.id,
10426
+ severity: "info",
10427
+ code: "LONG_GLOSSARY",
10428
+ message: "Very long glossary \u2014 consider splitting concepts for tighter briefings."
10429
+ });
10430
+ }
10431
+ const hasMarkdownHeading = /^#{1,3}\s+\S/m.test(memory2.body.trim());
10432
+ if (!hasMarkdownHeading) {
10433
+ out.push({
10434
+ file: filePath,
10435
+ id: fm.id,
10436
+ severity: "warn",
10437
+ code: "NO_MD_HEADING",
10438
+ message: "No Markdown heading (#/##/###) \u2014 add one so humans and auditors can skim the memo quickly."
10439
+ });
10440
+ }
10441
+ }
10442
+ return out;
10443
+ }
10444
+ function registerMemoryLint(parent) {
10445
+ parent.command("lint").description(
10446
+ "Heuristic corpus checks (anchors on key types, headings, verbosity). Static analysis only."
10447
+ ).option("--json", "emit findings as JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
10448
+ const root = findProjectRoot42(opts.dir);
10449
+ const findings = await lintMemoriesAsync(root);
10450
+ if (opts.json) {
10451
+ console.log(JSON.stringify({ findings_count: findings.length, findings }, null, 2));
10452
+ process.exitCode = findings.some((f) => f.severity === "error") ? 1 : 0;
10453
+ return;
10454
+ }
10455
+ if (findings.length === 0) {
10456
+ ui.success(`memory lint OK \u2014 ${root}`);
10457
+ return;
10458
+ }
10459
+ console.log(ui.bold(`memory lint (${findings.length} finding${findings.length === 1 ? "" : "s"})`) + `
10460
+ `);
10461
+ const order = { error: 0, warn: 1, info: 2 };
10462
+ findings.sort((a, b) => order[a.severity] - order[b.severity] || a.id.localeCompare(b.id));
10463
+ for (const f of findings) {
10464
+ const color = f.severity === "error" ? ui.red : f.severity === "warn" ? ui.yellow : ui.dim;
10465
+ console.log(
10466
+ `${color(f.severity.padEnd(5))} ${ui.dim(f.code)} ${f.id}`
10467
+ );
10468
+ console.log(` ${f.message}`);
10469
+ console.log(ui.dim(` \u2192 ${f.file}`));
10470
+ }
10471
+ process.exitCode = findings.some((x) => x.severity === "error") ? 1 : 0;
10472
+ });
10473
+ }
10474
+
10151
10475
  // src/index.ts
10152
- var program = new Command40();
10153
- program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.9.2");
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");
10154
10478
  registerInit(program);
10479
+ registerWelcome(program);
10155
10480
  registerMcp(program);
10156
10481
  registerBriefing(program);
10157
10482
  registerTui(program);
@@ -10183,6 +10508,7 @@ registerMemoryImportChangelog(memory);
10183
10508
  registerMemoryDigest(memory);
10184
10509
  registerMemorySuggest(memory);
10185
10510
  registerMemoryArchive(memory);
10511
+ registerMemoryLint(memory);
10186
10512
  var session = program.command("session").description("Manage session lifecycle");
10187
10513
  registerSessionEnd(session);
10188
10514
  registerSnapshot(program);