@hiveai/cli 0.9.15 → 0.9.17

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
@@ -198,7 +198,7 @@ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
198
198
  if (!f) continue;
199
199
  counts.set(f, (counts.get(f) ?? 0) + 1);
200
200
  }
201
- let entries = [...counts.entries()].map(([path48, changes]) => ({ path: path48, changes }));
201
+ let entries = [...counts.entries()].map(([path49, changes]) => ({ path: path49, changes }));
202
202
  const lowerPaths = filePaths.map((p) => p.toLowerCase());
203
203
  if (lowerPaths.length > 0) {
204
204
  entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
@@ -288,7 +288,7 @@ var TokenBudgetWriter = class {
288
288
  function registerBriefing(program2) {
289
289
  program2.command("briefing").description(
290
290
  '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'
291
- ).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
+ ).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", "8").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(
292
292
  "--budget <preset>",
293
293
  "align with MCP get_briefing budget_preset: quick | balanced | deep \u2014 sets cap + truncation budget (overrides --max-memories / replaces default open-ended output)",
294
294
  void 0
@@ -325,7 +325,7 @@ function registerBriefing(program2) {
325
325
  if (b === "quick" || b === "balanced" || b === "deep") budgetPreset = b;
326
326
  else ui.warn(`Unknown --budget '${opts.budget}' \u2014 ignoring (use quick|balanced|deep).`);
327
327
  }
328
- let maxMemories = Math.max(1, Number(opts.maxMemories ?? 10));
328
+ let maxMemories = Math.max(1, Number(opts.maxMemories ?? 8));
329
329
  let budgetTokensCap = opts.maxTokens ? Math.max(100, Number(opts.maxTokens)) : null;
330
330
  if (budgetPreset !== null) {
331
331
  const presetNums = resolveBriefingBudget(budgetPreset, {
@@ -470,7 +470,22 @@ function registerBriefing(program2) {
470
470
  const usageIndex = await loadUsageIndex(paths).catch(() => null);
471
471
  out(`${ui.bold("=== Relevant Memories ===")}
472
472
  `);
473
- for (const item of top) {
473
+ const priorities = top.map(
474
+ (item) => classifyCliPriority(
475
+ item,
476
+ filePaths,
477
+ tokens,
478
+ Boolean(andTaskHits?.has(item.memory.frontmatter.id)),
479
+ Boolean(useOrFallback && tokens && literalMatchesAnyToken(item.memory, tokens))
480
+ )
481
+ );
482
+ const mustReadCount = priorities.filter((p) => p === "must_read").length;
483
+ const usefulCount = priorities.filter((p) => p === "useful").length;
484
+ const backgroundCount = priorities.filter((p) => p === "background").length;
485
+ const quality = mustReadCount > 0 || usefulCount > 0 ? backgroundCount > mustReadCount + usefulCount && backgroundCount > 2 ? "noisy" : "strong" : "thin";
486
+ out(ui.dim(`briefing_quality: ${quality} \xB7 must_read=${mustReadCount} useful=${usefulCount} background=${backgroundCount}`));
487
+ out("");
488
+ for (const [idx, item] of top.entries()) {
474
489
  if (stopped()) break;
475
490
  const fm = item.memory.frontmatter;
476
491
  const badge = ui.statusBadge(fm.status);
@@ -479,8 +494,9 @@ function registerBriefing(program2) {
479
494
  const originMarker = item.origin ? ` ${ui.yellow("[from " + item.origin + "]")}` : "";
480
495
  const reads = usageIndex?.by_id[fm.id]?.read_count ?? 0;
481
496
  const hitMarker = reads > 0 ? ` ${ui.dim("\xB7 " + reads + "\xD7 read")}` : "";
497
+ const priority = priorities[idx] ?? "background";
482
498
  out(
483
- `${ui.bold(fm.id)} ${ui.dim(fm.scope + "/" + fm.type)} ${badge}${draftMarker}${unverifiedMarker}${originMarker}${hitMarker}`
499
+ `${ui.bold(fm.id)} ${priorityBadge(priority)} ${ui.dim(fm.scope + "/" + fm.type)} ${badge}${draftMarker}${unverifiedMarker}${originMarker}${hitMarker}`
484
500
  );
485
501
  if (opts.explainSource) {
486
502
  const relPath = path.relative(root, item.filePath);
@@ -551,6 +567,20 @@ ${ui.bold("=== Symbol Locations ===")}
551
567
  }
552
568
  });
553
569
  }
570
+ function classifyCliPriority(item, filePaths, tokens, exactTaskHit, partialTaskHit) {
571
+ const fm = item.memory.frontmatter;
572
+ const anchored = filePaths.length > 0 && memoryMatchesAnchorPaths(item.memory, filePaths);
573
+ if (anchored || fm.type === "attempt" && exactTaskHit) return "must_read";
574
+ if (exactTaskHit || partialTaskHit || item.score >= 4 || tokens && fm.tags.some((tag) => tokens.includes(tag))) {
575
+ return "useful";
576
+ }
577
+ return "background";
578
+ }
579
+ function priorityBadge(priority) {
580
+ if (priority === "must_read") return ui.red("[must_read]");
581
+ if (priority === "useful") return ui.yellow("[useful]");
582
+ return ui.dim("[background]");
583
+ }
554
584
  function parseCsv(value) {
555
585
  if (!value) return [];
556
586
  return value.split(",").map((s) => s.trim()).filter(Boolean);
@@ -3054,6 +3084,7 @@ import {
3054
3084
  extractActionsBriefBody as extractActionsBriefBody2,
3055
3085
  getUsage as getUsage5,
3056
3086
  inferModulesFromPaths as inferModulesFromPaths2,
3087
+ isGlobPath,
3057
3088
  isAutoPromoteEligible,
3058
3089
  isDecaying,
3059
3090
  literalMatchesAllTokens as literalMatchesAllTokens22,
@@ -3352,7 +3383,6 @@ async function memSave(input, ctx) {
3352
3383
  const { similarityWarning: simW, body_similar: bs } = bodySimilarWarnings(/* @__PURE__ */ new Set([fm.id]));
3353
3384
  const newFrontmatter = {
3354
3385
  ...fm,
3355
- body: input.body,
3356
3386
  tags: input.tags.length ? input.tags : fm.tags,
3357
3387
  revision_count: (fm.revision_count ?? 0) + 1,
3358
3388
  anchor: {
@@ -3368,6 +3398,7 @@ async function memSave(input, ctx) {
3368
3398
  );
3369
3399
  const mergedTw = [
3370
3400
  invalidPaths.length > 0 ? `Anchor path(s) not found in project: ${invalidPaths.join(", ")}. They will be marked stale by haive sync.` : null,
3401
+ criticalAnchorWarning(input.type, fm.status, newFrontmatter.anchor.paths, newFrontmatter.anchor.symbols),
3371
3402
  simW ?? null
3372
3403
  ].filter(Boolean).join(" \u2014 ") || void 0;
3373
3404
  return {
@@ -3423,6 +3454,7 @@ async function memSave(input, ctx) {
3423
3454
  const { similarityWarning: simWarnNew, body_similar: bsNew } = bodySimilarWarnings();
3424
3455
  const finalWarning = [
3425
3456
  invalidPaths.length > 0 ? `Anchor path(s) not found in project: ${invalidPaths.join(", ")}. They will be marked stale by \`haive sync\`.` : null,
3457
+ criticalAnchorWarning(frontmatter.type, frontmatter.status, frontmatter.anchor.paths, frontmatter.anchor.symbols),
3426
3458
  warning ?? null,
3427
3459
  simWarnNew ?? null
3428
3460
  ].filter(Boolean).join(" \u2014 ") || void 0;
@@ -3437,6 +3469,12 @@ async function memSave(input, ctx) {
3437
3469
  ...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {}
3438
3470
  };
3439
3471
  }
3472
+ function criticalAnchorWarning(type, status, paths, symbols) {
3473
+ if (!["decision", "gotcha", "architecture"].includes(type)) return null;
3474
+ if (status !== "validated") return null;
3475
+ if (paths.length > 0 || symbols.length > 0) return null;
3476
+ return `${type} is validated without paths or symbols; add anchors so hAIve can detect drift.`;
3477
+ }
3440
3478
  var MemSearchInputSchema = {
3441
3479
  query: z5.string().describe("Substring matched against id, tags, and body"),
3442
3480
  scope: z5.enum(["personal", "team", "module"]).optional().describe("Restrict results to a single scope"),
@@ -4458,6 +4496,7 @@ async function getBriefing(input, ctx) {
4458
4496
  reasons: [reason],
4459
4497
  match_quality: matchQuality ?? "partial",
4460
4498
  ...score !== void 0 ? { semantic_score: score } : {},
4499
+ priority: "background",
4461
4500
  body: loaded.memory.body,
4462
4501
  file_path: loaded.filePath
4463
4502
  });
@@ -4473,6 +4512,13 @@ async function getBriefing(input, ctx) {
4473
4512
  if (fm.tags.some((t) => inferred.includes(t))) addOrUpdate(loaded, "module", void 0, "partial");
4474
4513
  }
4475
4514
  }
4515
+ if (input.symbols.length > 0) {
4516
+ const wanted = new Set(input.symbols.map((s) => s.toLowerCase()));
4517
+ for (const loaded of allMemories) {
4518
+ const symbols = loaded.memory.frontmatter.anchor.symbols.map((s) => s.toLowerCase());
4519
+ if (symbols.some((s) => wanted.has(s))) addOrUpdate(loaded, "symbol", void 0, "exact");
4520
+ }
4521
+ }
4476
4522
  if (input.task) {
4477
4523
  const tokens = tokenizeQuery22(input.task);
4478
4524
  const andHits = allMemories.filter((m) => literalMatchesAllTokens22(m.memory, tokens));
@@ -4498,11 +4544,12 @@ async function getBriefing(input, ctx) {
4498
4544
  }
4499
4545
  }
4500
4546
  const ranked = [...seen.values()].sort((a, b) => {
4547
+ const priorityScore = (m) => priorityRank(classifyMemoryPriority(m, byId.get(m.id), input.files, input.symbols));
4501
4548
  const reasonScore = (m) => (m.type === "attempt" ? 3 : 0) + // attempt = negative knowledge, surface first to prevent repeating mistakes
4502
- (m.reasons.includes("anchor") ? 4 : 0) + (m.reasons.includes("module") ? 2 : 0) + (m.reasons.includes("semantic") ? 2 : 0) + (m.reasons.includes("domain") ? 1 : 0);
4549
+ (m.reasons.includes("anchor") ? 4 : 0) + (m.reasons.includes("symbol") ? 4 : 0) + (m.reasons.includes("module") ? 2 : 0) + (m.reasons.includes("semantic") ? 2 : 0) + (m.reasons.includes("domain") ? 1 : 0);
4503
4550
  const confidenceScore = (m) => m.confidence === "authoritative" ? 4 : m.confidence === "trusted" ? 3 : m.confidence === "low" ? 1 : m.confidence === "stale" ? -2 : 0;
4504
- const sa = reasonScore(a) + confidenceScore(a) + (a.semantic_score ?? 0);
4505
- const sb = reasonScore(b) + confidenceScore(b) + (b.semantic_score ?? 0);
4551
+ const sa = priorityScore(a) * 100 + reasonScore(a) + confidenceScore(a) + (a.semantic_score ?? 0);
4552
+ const sb = priorityScore(b) * 100 + reasonScore(b) + confidenceScore(b) + (b.semantic_score ?? 0);
4506
4553
  return sb - sa;
4507
4554
  });
4508
4555
  for (const mem of ranked.slice(0, briefingMaxMemories)) {
@@ -4661,8 +4708,15 @@ ${m.content}`).join("\n\n---\n\n"),
4661
4708
  })) : trimmedMemories;
4662
4709
  const outputMemories = formattedMemories.map((m) => ({
4663
4710
  ...m,
4711
+ priority: classifyMemoryPriority(m, byId.get(m.id), input.files, input.symbols),
4664
4712
  why: explainWhySurfaced(m, byId.get(m.id), input.files, inferred)
4665
4713
  }));
4714
+ const briefingQuality = classifyBriefingQuality(outputMemories, {
4715
+ isTemplateContext,
4716
+ autoContextGenerated,
4717
+ hasLastSession: Boolean(lastSession),
4718
+ searchMode
4719
+ });
4666
4720
  let symbolLocations;
4667
4721
  const symbolsToLookup = new Set(input.symbols);
4668
4722
  for (const m of outputMemories) {
@@ -4803,6 +4857,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
4803
4857
  } : null,
4804
4858
  module_contexts: trimmedModules,
4805
4859
  memories: outputMemories,
4860
+ briefing_quality: briefingQuality,
4806
4861
  ...symbolLocations ? { symbol_locations: symbolLocations } : {},
4807
4862
  action_required: actionRequired,
4808
4863
  decay_warnings: decayWarnings,
@@ -4828,6 +4883,53 @@ function compactSummary(body) {
4828
4883
  }
4829
4884
  return body.slice(0, 120);
4830
4885
  }
4886
+ function classifyMemoryPriority(memory2, loaded, inputFiles, inputSymbols) {
4887
+ const fm = loaded?.memory.frontmatter;
4888
+ const directAnchor = Boolean(
4889
+ fm && inputFiles.length > 0 && fm.anchor.paths.some((p) => inputFiles.some((file) => pathsOverlap(p, file)))
4890
+ );
4891
+ const directSymbol = Boolean(
4892
+ fm && inputSymbols.length > 0 && fm.anchor.symbols.some(
4893
+ (sym) => inputSymbols.some((wanted) => wanted.toLowerCase() === sym.toLowerCase())
4894
+ )
4895
+ );
4896
+ const strongSemantic = (memory2.semantic_score ?? 0) >= 0.65;
4897
+ const usefulSemantic = (memory2.semantic_score ?? 0) >= 0.35;
4898
+ if (fm?.requires_human_approval || directAnchor || directSymbol || memory2.type === "attempt" && (memory2.match_quality === "exact" || strongSemantic)) {
4899
+ return "must_read";
4900
+ }
4901
+ if (memory2.reasons.includes("module") || memory2.reasons.includes("domain") || memory2.match_quality === "exact" || usefulSemantic) {
4902
+ return "useful";
4903
+ }
4904
+ return "background";
4905
+ }
4906
+ function priorityRank(priority) {
4907
+ return priority === "must_read" ? 3 : priority === "useful" ? 2 : 1;
4908
+ }
4909
+ function classifyBriefingQuality(memories, context) {
4910
+ const mustRead = memories.filter((m) => m.priority === "must_read").length;
4911
+ const useful = memories.filter((m) => m.priority === "useful").length;
4912
+ const background = memories.filter((m) => m.priority === "background").length;
4913
+ const weakSemantic = memories.filter(
4914
+ (m) => m.reasons.length === 1 && m.reasons.includes("semantic") && (m.semantic_score ?? 0) > 0 && (m.semantic_score ?? 0) < 0.35
4915
+ ).length;
4916
+ const reasons = [];
4917
+ if (memories.length === 0) reasons.push("no memories matched the task or files");
4918
+ if (context.isTemplateContext && !context.autoContextGenerated) reasons.push("project context is still a template");
4919
+ if (!context.hasLastSession) reasons.push("no previous session recap");
4920
+ if (mustRead > 0) reasons.push(`${mustRead} must_read memor${mustRead === 1 ? "y" : "ies"} matched directly`);
4921
+ if (useful > 0) reasons.push(`${useful} useful memor${useful === 1 ? "y" : "ies"} matched`);
4922
+ if (background > useful + mustRead && background > 2) reasons.push(`${background} background memories dominate the result`);
4923
+ if (weakSemantic > 0) reasons.push(`${weakSemantic} weak semantic-only match${weakSemantic === 1 ? "" : "es"}`);
4924
+ if (context.searchMode === "literal_fallback") reasons.push("semantic index unavailable or empty; literal fallback used");
4925
+ if (memories.length === 0 || mustRead === 0 && useful === 0) {
4926
+ return { level: "thin", reasons };
4927
+ }
4928
+ if (background > useful + mustRead && background > 2) {
4929
+ return { level: "noisy", reasons };
4930
+ }
4931
+ return { level: "strong", reasons };
4932
+ }
4831
4933
  function explainWhySurfaced(memory2, loaded, inputFiles, inferredModules) {
4832
4934
  const why = [];
4833
4935
  const fm = loaded?.memory.frontmatter;
@@ -4836,7 +4938,19 @@ function explainWhySurfaced(memory2, loaded, inputFiles, inferredModules) {
4836
4938
  (p) => inputFiles.length === 0 || inputFiles.some((file) => pathsOverlap(p, file))
4837
4939
  );
4838
4940
  if (matching.length > 0) {
4839
- why.push(`Anchored to touched path${matching.length === 1 ? "" : "s"}: ${matching.slice(0, 4).join(", ")}`);
4941
+ const exact = matching.filter(
4942
+ (p) => !isGlobPath(p) && inputFiles.some((file) => p === file || pathsOverlap(p, file))
4943
+ );
4944
+ const glob = matching.filter((p) => isGlobPath(p));
4945
+ if (exact.length > 0) {
4946
+ why.push(`Exact/file anchor match: ${exact.slice(0, 4).join(", ")}`);
4947
+ }
4948
+ if (glob.length > 0) {
4949
+ why.push(`Glob anchor match: ${glob.slice(0, 4).join(", ")}`);
4950
+ }
4951
+ if (exact.length === 0 && glob.length === 0) {
4952
+ why.push(`Anchored to touched path${matching.length === 1 ? "" : "s"}: ${matching.slice(0, 4).join(", ")}`);
4953
+ }
4840
4954
  } else if (fm.anchor.paths.length > 0) {
4841
4955
  why.push(`Pulled by related anchor: ${fm.anchor.paths.slice(0, 4).join(", ")}`);
4842
4956
  }
@@ -4844,6 +4958,9 @@ function explainWhySurfaced(memory2, loaded, inputFiles, inferredModules) {
4844
4958
  why.push(`Anchor symbol${fm.anchor.symbols.length === 1 ? "" : "s"}: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
4845
4959
  }
4846
4960
  }
4961
+ if (memory2.reasons.includes("symbol") && fm) {
4962
+ why.push(`Explicit symbol match: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
4963
+ }
4847
4964
  if (memory2.reasons.includes("module")) {
4848
4965
  const moduleHints = [
4849
4966
  ...memory2.module ? [memory2.module] : [],
@@ -5068,7 +5185,8 @@ async function memRelevantTo(input, ctx) {
5068
5185
  const out = {
5069
5186
  task: input.task,
5070
5187
  search_mode: briefing.search_mode,
5071
- memories: briefing.memories
5188
+ memories: briefing.memories,
5189
+ briefing_quality: briefing.briefing_quality
5072
5190
  };
5073
5191
  if (briefing.hints && briefing.hints.length > 0) out.hints = briefing.hints;
5074
5192
  if (briefing.memories.length === 0) out.empty = true;
@@ -5750,7 +5868,7 @@ async function preCommitCheck(input, ctx) {
5750
5868
  const filesTouching = new Set(relevantMatches.map((m) => m.id));
5751
5869
  const staleHits = verifyResult.results.filter((r) => r.stale && filesTouching.has(r.id));
5752
5870
  const blockOn = input.block_on;
5753
- const classifiedWarnings = apResult.warnings.map(classifyWarning);
5871
+ const classifiedWarnings = apResult.warnings.map((warning) => classifyWarning(warning, input.paths));
5754
5872
  const blockingWarnings = classifiedWarnings.filter((w) => w.level === "blocking");
5755
5873
  const reviewWarnings = classifiedWarnings.filter((w) => w.level === "review");
5756
5874
  const infoWarnings = classifiedWarnings.filter((w) => w.level === "info");
@@ -5785,12 +5903,26 @@ async function preCommitCheck(input, ctx) {
5785
5903
  }))
5786
5904
  };
5787
5905
  }
5788
- function classifyWarning(warning) {
5906
+ function classifyWarning(warning, paths) {
5907
+ const affectedFiles = paths.filter((p) => !p.startsWith(".ai/.usage/"));
5908
+ const repairCommand = repairCommandForWarning(warning, affectedFiles);
5909
+ const fileDowngrade = fileTypeDowngradeReason(warning, affectedFiles);
5910
+ if (fileDowngrade) {
5911
+ return {
5912
+ ...warning,
5913
+ level: "info",
5914
+ rationale: fileDowngrade,
5915
+ affected_files: affectedFiles,
5916
+ repair_command: repairCommand
5917
+ };
5918
+ }
5789
5919
  if (isBlockingWarning(warning)) {
5790
5920
  return {
5791
5921
  ...warning,
5792
5922
  level: "blocking",
5793
- rationale: "authoritative/trusted memory plus strong semantic match to the diff (score >= 0.65)"
5923
+ rationale: "authoritative/trusted memory plus strong semantic match to the diff (score >= 0.65)",
5924
+ affected_files: affectedFiles,
5925
+ repair_command: repairCommand
5794
5926
  };
5795
5927
  }
5796
5928
  const hasSemantic = warning.reasons.includes("semantic");
@@ -5800,13 +5932,17 @@ function classifyWarning(warning) {
5800
5932
  return {
5801
5933
  ...warning,
5802
5934
  level: "review",
5803
- rationale: hasSemantic ? "semantic match is plausible but below blocking threshold" : "anchored high-confidence memory also matched diff tokens, but no strong semantic proof"
5935
+ rationale: hasSemantic ? "semantic match is plausible but below blocking threshold" : "anchored high-confidence memory also matched diff tokens, but no strong semantic proof",
5936
+ affected_files: affectedFiles,
5937
+ repair_command: repairCommand
5804
5938
  };
5805
5939
  }
5806
5940
  return {
5807
5941
  ...warning,
5808
5942
  level: "info",
5809
- rationale: "weak signal only (literal/anchor/low semantic evidence); surfaced for audit, hidden in concise CLI output"
5943
+ rationale: "weak signal only (literal/anchor/low semantic evidence); surfaced for audit, hidden in concise CLI output",
5944
+ affected_files: affectedFiles,
5945
+ repair_command: repairCommand
5810
5946
  };
5811
5947
  }
5812
5948
  function isBlockingWarning(warning) {
@@ -5814,6 +5950,40 @@ function isBlockingWarning(warning) {
5814
5950
  if (!highConfidence) return false;
5815
5951
  return warning.reasons.includes("semantic") && (warning.semantic_score ?? 0) >= 0.65;
5816
5952
  }
5953
+ function fileTypeDowngradeReason(warning, paths) {
5954
+ if (paths.length === 0) return null;
5955
+ if (paths.every((p) => p.startsWith(".ai/.usage/") || p === ".ai/.usage/tool-usage.jsonl")) {
5956
+ return ".ai usage logs are local telemetry and never block commits.";
5957
+ }
5958
+ const docsOnly = paths.every(isDocLikePath);
5959
+ if (docsOnly && !hasStrongSemantic(warning)) {
5960
+ return "docs/changelog-only change; anti-pattern is downgraded unless semantic evidence is strong.";
5961
+ }
5962
+ const configOnly = paths.every(isPackageOrConfigPath);
5963
+ if (configOnly && looksRuntimeSpecific(warning) && !warning.reasons.includes("anchor") && !hasStrongSemantic(warning)) {
5964
+ return "package/config-only change; runtime-specific gotcha is not anchored or semantically strong.";
5965
+ }
5966
+ return null;
5967
+ }
5968
+ function hasStrongSemantic(warning) {
5969
+ return warning.reasons.includes("semantic") && (warning.semantic_score ?? 0) >= 0.65;
5970
+ }
5971
+ function isDocLikePath(file) {
5972
+ const lower = file.toLowerCase();
5973
+ return lower.endsWith(".md") || lower.includes("changelog") || lower.startsWith("docs/") || lower.startsWith(".github/") && lower.endsWith(".md");
5974
+ }
5975
+ function isPackageOrConfigPath(file) {
5976
+ const lower = file.toLowerCase();
5977
+ return lower.endsWith("package.json") || lower.endsWith("package-lock.json") || lower.endsWith("pnpm-lock.yaml") || lower.endsWith("yarn.lock") || lower.endsWith("bun.lockb") || lower.endsWith(".config.ts") || lower.endsWith(".config.js") || lower.endsWith(".json") || lower.endsWith(".yml") || lower.endsWith(".yaml") || lower.startsWith(".github/workflows/");
5978
+ }
5979
+ function looksRuntimeSpecific(warning) {
5980
+ const text = `${warning.body_preview} ${warning.id}`.toLowerCase();
5981
+ return /\b(runtime|controller|request|response|database|transaction|auth|cache|production|service|api|endpoint)\b/.test(text);
5982
+ }
5983
+ function repairCommandForWarning(warning, paths) {
5984
+ const firstPath = paths[0];
5985
+ return firstPath ? `haive briefing --files "${firstPath}" --task "review ${warning.id}"` : `haive memory show ${warning.id}`;
5986
+ }
5817
5987
  var CONFIG_PATTERNS = [
5818
5988
  ".eslintrc",
5819
5989
  "eslint.config",
@@ -6345,7 +6515,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
6345
6515
  };
6346
6516
  }
6347
6517
  var SERVER_NAME = "haive";
6348
- var SERVER_VERSION = "0.9.15";
6518
+ var SERVER_VERSION = "0.9.17";
6349
6519
  function jsonResult(data) {
6350
6520
  return {
6351
6521
  content: [
@@ -6356,7 +6526,7 @@ function jsonResult(data) {
6356
6526
  ]
6357
6527
  };
6358
6528
  }
6359
- var ENFORCEMENT_PROFILE_TOOLS = /* @__PURE__ */ new Set([
6529
+ var ENFORCEMENT_PROFILE_TOOLS = [
6360
6530
  "get_briefing",
6361
6531
  "mem_save",
6362
6532
  "mem_tried",
@@ -6367,7 +6537,47 @@ var ENFORCEMENT_PROFILE_TOOLS = /* @__PURE__ */ new Set([
6367
6537
  "code_map",
6368
6538
  "pre_commit_check",
6369
6539
  "mem_session_end"
6370
- ]);
6540
+ ];
6541
+ var MAINTENANCE_PROFILE_TOOLS = [
6542
+ ...ENFORCEMENT_PROFILE_TOOLS,
6543
+ "mem_suggest_topic",
6544
+ "mem_for_files",
6545
+ "mem_list",
6546
+ "get_project_context",
6547
+ "bootstrap_project_save",
6548
+ "mem_resolve_project",
6549
+ "mem_update",
6550
+ "mem_approve",
6551
+ "mem_reject",
6552
+ "mem_pending",
6553
+ "mem_delete",
6554
+ "mem_diff",
6555
+ "get_recap",
6556
+ "code_search",
6557
+ "anti_patterns_check",
6558
+ "mem_distill",
6559
+ "mem_timeline",
6560
+ "mem_conflict_candidates"
6561
+ ];
6562
+ var EXPERIMENTAL_PROFILE_TOOLS = [
6563
+ ...MAINTENANCE_PROFILE_TOOLS,
6564
+ "mem_observe",
6565
+ "why_this_file",
6566
+ "why_this_decision",
6567
+ "mem_conflicts_with",
6568
+ "pattern_detect",
6569
+ "runtime_journal_append",
6570
+ "runtime_journal_tail"
6571
+ ];
6572
+ var TOOL_PROFILES = {
6573
+ enforcement: new Set(ENFORCEMENT_PROFILE_TOOLS),
6574
+ maintenance: new Set(MAINTENANCE_PROFILE_TOOLS),
6575
+ experimental: new Set(EXPERIMENTAL_PROFILE_TOOLS)
6576
+ };
6577
+ function getAllowedToolsForProfile(profile) {
6578
+ if (profile === "full") return TOOL_PROFILES.experimental;
6579
+ return TOOL_PROFILES[profile] ?? TOOL_PROFILES.enforcement;
6580
+ }
6371
6581
  var BRIEFING_TOOLS = /* @__PURE__ */ new Set(["get_briefing", "mem_relevant_to"]);
6372
6582
  var MUTATING_TOOLS = /* @__PURE__ */ new Set([
6373
6583
  "mem_save",
@@ -6394,7 +6604,8 @@ function createHaiveServer(options = {}) {
6394
6604
  { name: SERVER_NAME, version: SERVER_VERSION },
6395
6605
  { capabilities: { tools: {}, prompts: {} } }
6396
6606
  );
6397
- const shouldRegisterTool = (name) => toolProfile === "full" || ENFORCEMENT_PROFILE_TOOLS.has(name);
6607
+ const allowedTools = getAllowedToolsForProfile(toolProfile);
6608
+ const shouldRegisterTool = (name) => allowedTools.has(name);
6398
6609
  const registerTool = (name, description, schema, handler) => {
6399
6610
  if (!shouldRegisterTool(name)) return;
6400
6611
  const tool = server.tool.bind(server);
@@ -6418,7 +6629,11 @@ function createHaiveServer(options = {}) {
6418
6629
  }
6419
6630
  );
6420
6631
  };
6421
- const shouldRegisterPrompt = (name) => toolProfile === "full" || name === "bootstrap_project" || name === "post_task";
6632
+ const shouldRegisterPrompt = (name) => {
6633
+ if (name === "bootstrap_project" || name === "post_task") return true;
6634
+ if (name === "import_docs") return toolProfile !== "enforcement";
6635
+ return toolProfile === "experimental" || toolProfile === "full";
6636
+ };
6422
6637
  registerTool(
6423
6638
  "mem_save",
6424
6639
  [
@@ -10735,7 +10950,7 @@ var MS_PER_DAY3 = 24 * 60 * 60 * 1e3;
10735
10950
  function registerDoctor(program2) {
10736
10951
  program2.command("doctor").description(
10737
10952
  "Analyze the local hAIve setup and emit actionable recommendations.\n\n Inspects: project-context status, memory health (stale/anchorless/decay/pending),\n code-map freshness, usage log signals (low-hit briefings, repeated empty searches).\n\n Read-only by default. Pass --fix to suggest commands you can copy-paste."
10738
- ).option("--json", "emit JSON instead of human-readable output", false).option("--fix", "include suggested fix commands in human output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
10953
+ ).option("--json", "emit JSON instead of human-readable output", false).option("--fix", "include suggested fix commands in human output", false).option("--dry-run", "with --fix, show delegated repairs without applying them", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
10739
10954
  const root = findProjectRoot40(opts.dir);
10740
10955
  const paths = resolveHaivePaths36(root);
10741
10956
  const findings = [];
@@ -10907,14 +11122,14 @@ function registerDoctor(program2) {
10907
11122
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
10908
11123
  });
10909
11124
  }
10910
- findings.push(...await collectInstallFindings(root, "0.9.15"));
11125
+ findings.push(...await collectInstallFindings(root, "0.9.17"));
10911
11126
  try {
10912
11127
  const legacyRaw = execSync3("haive-mcp --version", {
10913
11128
  encoding: "utf8",
10914
11129
  timeout: 3e3,
10915
11130
  stdio: ["ignore", "pipe", "ignore"]
10916
11131
  }).trim();
10917
- const cliVersion = "0.9.15";
11132
+ const cliVersion = "0.9.17";
10918
11133
  if (legacyRaw && legacyRaw !== cliVersion) {
10919
11134
  findings.push({
10920
11135
  severity: "warn",
@@ -10931,33 +11146,105 @@ npm uninstall -g @hiveai/mcp`
10931
11146
  });
10932
11147
  }
10933
11148
  function emit(findings, opts) {
11149
+ const classified = findings.map((finding) => ({
11150
+ ...finding,
11151
+ section: finding.section ?? sectionForFinding(finding)
11152
+ }));
11153
+ const scores = computeDoctorScores(classified);
10934
11154
  if (opts.json) {
10935
- console.log(JSON.stringify({ findings }, null, 2));
11155
+ console.log(JSON.stringify({
11156
+ scores,
11157
+ findings: classified,
11158
+ sections: groupBySection(classified),
11159
+ next_actions: nextActions(classified),
11160
+ fix_mode: opts.fix ? opts.dryRun ? "dry-run" : "suggest" : "off"
11161
+ }, null, 2));
10936
11162
  return;
10937
11163
  }
10938
- if (findings.length === 0) {
11164
+ if (classified.length === 0) {
10939
11165
  ui.success("hAIve doctor \u2014 no issues found.");
10940
11166
  return;
10941
11167
  }
10942
- console.log(ui.bold(`hAIve doctor \u2014 ${findings.length} finding${findings.length === 1 ? "" : "s"}`));
11168
+ console.log(ui.bold(`hAIve doctor \u2014 ${classified.length} finding${classified.length === 1 ? "" : "s"}`));
11169
+ console.log(
11170
+ ui.dim(
11171
+ ` protection=${scores.protection_score} context=${scores.context_quality_score} corpus=${scores.corpus_quality_score}`
11172
+ )
11173
+ );
10943
11174
  console.log();
10944
- const order = ["error", "warn", "info"];
10945
- for (const sev of order) {
10946
- for (const f of findings.filter((x) => x.severity === sev)) {
10947
- const icon = sev === "error" ? ui.red("\u2717") : sev === "warn" ? ui.yellow("\u26A0") : ui.dim("\u2139");
10948
- console.log(`${icon} ${ui.bold(f.code)} ${f.message}`);
10949
- if (opts.fix && f.fix) {
10950
- for (const line of f.fix.split("\n")) {
10951
- console.log(` ${ui.dim("$")} ${line}`);
11175
+ const sectionOrder = [
11176
+ "Protection",
11177
+ "Agent coverage",
11178
+ "Context quality",
11179
+ "Corpus health",
11180
+ "Index health",
11181
+ "Next actions"
11182
+ ];
11183
+ const severityOrder = ["error", "warn", "info"];
11184
+ for (const section2 of sectionOrder) {
11185
+ const sectionFindings = classified.filter((f) => f.section === section2);
11186
+ if (sectionFindings.length === 0) continue;
11187
+ console.log(ui.bold(section2));
11188
+ for (const sev of severityOrder) {
11189
+ for (const f of sectionFindings.filter((x) => x.severity === sev)) {
11190
+ const icon = sev === "error" ? ui.red("\u2717") : sev === "warn" ? ui.yellow("\u26A0") : ui.dim("\u2139");
11191
+ console.log(`${icon} ${ui.bold(f.code)} ${f.message}`);
11192
+ if (opts.fix && f.fix) {
11193
+ for (const line of f.fix.split("\n")) {
11194
+ console.log(` ${ui.dim(opts.dryRun ? "would run:" : "$")} ${line}`);
11195
+ }
10952
11196
  }
10953
11197
  }
10954
11198
  }
10955
- }
10956
- if (!opts.fix && findings.some((f) => f.fix)) {
10957
11199
  console.log();
11200
+ }
11201
+ const actions = nextActions(classified);
11202
+ if (actions.length > 0) {
11203
+ console.log(ui.bold("Next actions"));
11204
+ for (const action of actions.slice(0, 5)) console.log(` ${ui.dim("$")} ${action}`);
11205
+ } else if (!opts.fix && classified.some((f) => f.fix)) {
10958
11206
  ui.info("Re-run with --fix to see suggested commands.");
10959
11207
  }
10960
11208
  }
11209
+ function sectionForFinding(finding) {
11210
+ if (finding.code.includes("haive") || finding.code.includes("integration") || finding.code.includes("claude") || finding.code.includes("autopilot")) return "Agent coverage";
11211
+ if (finding.code.includes("context") || finding.code.includes("briefing") || finding.code.includes("search")) return "Context quality";
11212
+ if (finding.code.includes("code-map") || finding.code.includes("index")) return "Index health";
11213
+ if (finding.code.includes("memory") || finding.code.includes("anchor") || finding.code.includes("pending") || finding.code.includes("decay")) return "Corpus health";
11214
+ if (finding.severity === "error") return "Protection";
11215
+ return "Next actions";
11216
+ }
11217
+ function computeDoctorScores(findings) {
11218
+ const scoreFor = (sections) => {
11219
+ const scoped = findings.filter((f) => sections.includes(f.section ?? sectionForFinding(f)));
11220
+ const penalty = scoped.reduce((sum, f) => {
11221
+ if (f.severity === "error") return sum + 35;
11222
+ if (f.severity === "warn") return sum + 15;
11223
+ return sum + 4;
11224
+ }, 0);
11225
+ return Math.max(0, 100 - penalty);
11226
+ };
11227
+ return {
11228
+ protection_score: scoreFor(["Protection", "Agent coverage"]),
11229
+ context_quality_score: scoreFor(["Context quality", "Index health"]),
11230
+ corpus_quality_score: scoreFor(["Corpus health"])
11231
+ };
11232
+ }
11233
+ function groupBySection(findings) {
11234
+ const out = {
11235
+ "Protection": [],
11236
+ "Agent coverage": [],
11237
+ "Context quality": [],
11238
+ "Corpus health": [],
11239
+ "Index health": [],
11240
+ "Next actions": []
11241
+ };
11242
+ for (const finding of findings) out[finding.section ?? sectionForFinding(finding)].push(finding);
11243
+ return out;
11244
+ }
11245
+ function nextActions(findings) {
11246
+ return [...new Set(findings.flatMap((finding) => finding.fix ? finding.fix.split("\n") : []))].filter(Boolean);
11247
+ }
10961
11248
  function isSearchTool(name) {
10962
11249
  return ["mem_search", "code_search", "mem_relevant_to", "get_briefing"].includes(name);
10963
11250
  }
@@ -11276,7 +11563,11 @@ function printWarnings(title, warnings, tone) {
11276
11563
  console.log(` ${ui.dim(line)}`);
11277
11564
  }
11278
11565
  console.log(` ${ui.dim("reasons:")} ${w.reasons.join(", ")}`);
11566
+ if (w.affected_files && w.affected_files.length > 0) {
11567
+ console.log(` ${ui.dim("files:")} ${w.affected_files.slice(0, 4).join(", ")}`);
11568
+ }
11279
11569
  if (w.rationale) console.log(` ${ui.dim("why shown:")} ${w.rationale}`);
11570
+ if (w.repair_command) console.log(` ${ui.dim("repair:")} ${w.repair_command}`);
11280
11571
  }
11281
11572
  console.log();
11282
11573
  }
@@ -11363,20 +11654,26 @@ function registerWelcome(program2) {
11363
11654
 
11364
11655
  // src/commands/memory-lint.ts
11365
11656
  import { existsSync as existsSync64 } from "fs";
11657
+ import { writeFile as writeFile31 } from "fs/promises";
11658
+ import path43 from "path";
11366
11659
  import "commander";
11367
11660
  import {
11368
11661
  findProjectRoot as findProjectRoot44,
11369
11662
  getUsage as getUsage20,
11663
+ loadCodeMap as loadCodeMap6,
11370
11664
  loadMemoriesFromDir as loadMemoriesFromDir35,
11371
11665
  loadUsageIndex as loadUsageIndex26,
11372
- resolveHaivePaths as resolveHaivePaths40
11666
+ resolveHaivePaths as resolveHaivePaths40,
11667
+ serializeMemory as serializeMemory26
11373
11668
  } from "@hiveai/core";
11374
- async function lintMemoriesAsync(root) {
11669
+ async function lintMemoriesAsync(root, options = {}) {
11375
11670
  const paths = resolveHaivePaths40(root);
11376
11671
  const out = [];
11377
- if (!existsSync64(paths.memoriesDir)) return out;
11672
+ const fixes = [];
11673
+ if (!existsSync64(paths.memoriesDir)) return { findings: out, fixes };
11378
11674
  const loaded = await loadMemoriesFromDir35(paths.memoriesDir);
11379
11675
  const usage = await loadUsageIndex26(paths);
11676
+ const codeMap = await loadCodeMap6(paths);
11380
11677
  const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
11381
11678
  const actionableWords = /\b(always|never|prefer|use|avoid|because|instead|why|rationale|do not|must|should)\b/i;
11382
11679
  for (const { filePath, memory: memory2 } of loaded) {
@@ -11402,13 +11699,15 @@ async function lintMemoriesAsync(root) {
11402
11699
  message: "Record does not contain obvious action/rationale words. Add the concrete rule, why it exists, and what to do instead."
11403
11700
  });
11404
11701
  }
11702
+ const suggestedAnchors = suggestAnchors(root, { filePath, memory: memory2 }, codeMap);
11405
11703
  if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated") {
11406
11704
  out.push({
11407
11705
  file: filePath,
11408
11706
  id: fm.id,
11409
11707
  severity: "warn",
11410
11708
  code: "MISSING_ANCHOR",
11411
- message: `${fm.type} is validated without anchor paths \u2014 add anchor.paths so haive sync can flag staleness.`
11709
+ message: `${fm.type} is validated without anchor paths \u2014 add anchor.paths so haive sync can flag staleness.`,
11710
+ ...suggestedAnchors.paths.length > 0 || suggestedAnchors.symbols.length > 0 ? { suggested_anchors: suggestedAnchors } : {}
11412
11711
  });
11413
11712
  }
11414
11713
  if (fm.status === "stale" && !fm.stale_reason) {
@@ -11449,6 +11748,34 @@ async function lintMemoriesAsync(root) {
11449
11748
  message: "Validated record has never been surfaced/read. Consider improving tags/anchors or archiving it if it is not useful."
11450
11749
  });
11451
11750
  }
11751
+ if (options.fix) {
11752
+ const actions = [];
11753
+ let nextBody = memory2.body;
11754
+ let nextFrontmatter = memory2.frontmatter;
11755
+ if (!hasMarkdownHeading) {
11756
+ nextBody = `# ${titleFromId(fm.id)}
11757
+
11758
+ ${nextBody.trim()}`;
11759
+ actions.push("add missing Markdown heading");
11760
+ }
11761
+ if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.anchor.symbols.length === 0 && fm.status === "validated" && !fm.tags.includes("needs_anchor")) {
11762
+ nextFrontmatter = {
11763
+ ...nextFrontmatter,
11764
+ tags: [...nextFrontmatter.tags, "needs_anchor"]
11765
+ };
11766
+ actions.push("tag validated anchorless record with needs_anchor");
11767
+ }
11768
+ if (actions.length > 0) {
11769
+ fixes.push({ file: filePath, id: fm.id, actions, applied: Boolean(options.apply) });
11770
+ if (options.apply) {
11771
+ await writeFile31(
11772
+ filePath,
11773
+ serializeMemory26({ frontmatter: nextFrontmatter, body: nextBody }),
11774
+ "utf8"
11775
+ );
11776
+ }
11777
+ }
11778
+ }
11452
11779
  }
11453
11780
  for (const dup of nearDuplicatePairs(loaded)) {
11454
11781
  out.push({
@@ -11459,7 +11786,39 @@ async function lintMemoriesAsync(root) {
11459
11786
  message: `Body overlaps ~${Math.round(dup.score * 100)}% with ${dup.otherId}. Merge or deprecate one record to reduce briefing noise.`
11460
11787
  });
11461
11788
  }
11462
- return out;
11789
+ return { findings: out, fixes };
11790
+ }
11791
+ function titleFromId(id) {
11792
+ const withoutDate = id.replace(/^\d{4}-\d{2}-\d{2}-/, "");
11793
+ return withoutDate.split("-").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
11794
+ }
11795
+ function suggestAnchors(root, loaded, codeMap) {
11796
+ const body = loaded.memory.body;
11797
+ const paths = /* @__PURE__ */ new Set();
11798
+ const symbols = /* @__PURE__ */ new Set();
11799
+ for (const match of body.matchAll(/`([^`\n]+\.[A-Za-z0-9]+)`|(?:^|\s)([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)/gm)) {
11800
+ const candidate = (match[1] ?? match[2] ?? "").replace(/^\.?\//, "");
11801
+ if (!candidate || candidate.startsWith("http")) continue;
11802
+ if (existsSync64(path43.join(root, candidate))) paths.add(candidate);
11803
+ }
11804
+ if (codeMap) {
11805
+ const lowered = body.toLowerCase();
11806
+ for (const [file, entry] of Object.entries(codeMap.files)) {
11807
+ for (const exp of entry.exports) {
11808
+ if (!exp.name || exp.name.length < 4) continue;
11809
+ if (lowered.includes(exp.name.toLowerCase())) {
11810
+ paths.add(file);
11811
+ symbols.add(exp.name);
11812
+ }
11813
+ if (paths.size >= 5 && symbols.size >= 5) break;
11814
+ }
11815
+ if (paths.size >= 5 && symbols.size >= 5) break;
11816
+ }
11817
+ }
11818
+ return {
11819
+ paths: [...paths].slice(0, 5),
11820
+ symbols: [...symbols].slice(0, 5)
11821
+ };
11463
11822
  }
11464
11823
  function nearDuplicatePairs(loaded) {
11465
11824
  const out = [];
@@ -11500,11 +11859,20 @@ function jaccard2(a, b) {
11500
11859
  function registerMemoryLint(parent) {
11501
11860
  parent.command("lint").description(
11502
11861
  "Heuristic corpus checks (anchors on key types, headings, verbosity). Static analysis only."
11503
- ).option("--json", "emit findings as JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
11862
+ ).option("--json", "emit findings as JSON", false).option("--fix", "prepare simple automatic fixes (use with --dry-run or --apply)", false).option("--dry-run", "with --fix, show files that would change without writing", false).option("--apply", "with --fix, write simple fixes to disk", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
11504
11863
  const root = findProjectRoot44(opts.dir);
11505
- const findings = await lintMemoriesAsync(root);
11864
+ const apply = Boolean(opts.fix && opts.apply);
11865
+ const dryRun = Boolean(opts.fix && (opts.dryRun || !opts.apply));
11866
+ const report = await lintMemoriesAsync(root, { fix: Boolean(opts.fix), apply });
11867
+ const findings = report.findings;
11506
11868
  if (opts.json) {
11507
- console.log(JSON.stringify({ findings_count: findings.length, findings }, null, 2));
11869
+ console.log(JSON.stringify({
11870
+ findings_count: findings.length,
11871
+ findings,
11872
+ fixes_count: report.fixes.length,
11873
+ fixes: report.fixes,
11874
+ fix_mode: opts.fix ? apply ? "apply" : "dry-run" : "off"
11875
+ }, null, 2));
11508
11876
  process.exitCode = findings.some((f) => f.severity === "error") ? 1 : 0;
11509
11877
  return;
11510
11878
  }
@@ -11514,6 +11882,16 @@ function registerMemoryLint(parent) {
11514
11882
  }
11515
11883
  console.log(ui.bold(`memory lint (${findings.length} finding${findings.length === 1 ? "" : "s"})`) + `
11516
11884
  `);
11885
+ if (opts.fix) {
11886
+ const mode = apply ? "apply" : dryRun ? "dry-run" : "dry-run";
11887
+ const verb = apply ? "changed" : "would change";
11888
+ console.log(ui.bold(`fix ${mode}: ${report.fixes.length} file${report.fixes.length === 1 ? "" : "s"} ${verb}`));
11889
+ for (const fix of report.fixes) {
11890
+ console.log(` ${ui.dim(fix.id)} ${fix.actions.join("; ")}`);
11891
+ console.log(ui.dim(` \u2192 ${fix.file}`));
11892
+ }
11893
+ console.log();
11894
+ }
11517
11895
  const order = { error: 0, warn: 1, info: 2 };
11518
11896
  findings.sort((a, b) => order[a.severity] - order[b.severity] || a.id.localeCompare(b.id));
11519
11897
  for (const f of findings) {
@@ -11522,6 +11900,11 @@ function registerMemoryLint(parent) {
11522
11900
  `${color(f.severity.padEnd(5))} ${ui.dim(f.code)} ${f.id}`
11523
11901
  );
11524
11902
  console.log(` ${f.message}`);
11903
+ if (f.suggested_anchors) {
11904
+ const pathHints = f.suggested_anchors.paths.length > 0 ? `paths: ${f.suggested_anchors.paths.join(", ")}` : "";
11905
+ const symbolHints = f.suggested_anchors.symbols.length > 0 ? `symbols: ${f.suggested_anchors.symbols.join(", ")}` : "";
11906
+ console.log(ui.dim(` suggested anchors: ${[pathHints, symbolHints].filter(Boolean).join(" \xB7 ")}`));
11907
+ }
11525
11908
  console.log(ui.dim(` \u2192 ${f.file}`));
11526
11909
  }
11527
11910
  process.exitCode = findings.some((x) => x.severity === "error") ? 1 : 0;
@@ -11547,21 +11930,21 @@ function registerMemorySuggestTopic(memory2) {
11547
11930
  }
11548
11931
 
11549
11932
  // src/commands/resolve-project.ts
11550
- import path43 from "path";
11933
+ import path44 from "path";
11551
11934
  import "commander";
11552
11935
  import { resolveProjectInfo as resolveProjectInfo2 } from "@hiveai/core";
11553
11936
  function registerResolveProject(program2) {
11554
11937
  program2.command("resolve-project").description(
11555
11938
  "Print JSON for hAIve project root resolution (HAIVE_PROJECT_ROOT, markers, .ai layout)."
11556
11939
  ).option("-d, --dir <dir>", "working directory", process.cwd()).action((opts) => {
11557
- const info = resolveProjectInfo2({ cwd: path43.resolve(opts.dir) });
11940
+ const info = resolveProjectInfo2({ cwd: path44.resolve(opts.dir) });
11558
11941
  console.log(JSON.stringify({ ok: true, info }, null, 2));
11559
11942
  });
11560
11943
  }
11561
11944
 
11562
11945
  // src/commands/runtime-journal.ts
11563
11946
  import { existsSync as existsSync65 } from "fs";
11564
- import path44 from "path";
11947
+ import path45 from "path";
11565
11948
  import "commander";
11566
11949
  import {
11567
11950
  appendRuntimeJournalEntry as appendRuntimeJournalEntry3,
@@ -11575,15 +11958,15 @@ function registerRuntime(program2) {
11575
11958
  );
11576
11959
  const journal = runtime.command("journal").description("Append or read the machine-local session journal (NDJSON)");
11577
11960
  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) => {
11578
- const root = path44.resolve(opts.dir ?? process.cwd());
11961
+ const root = path45.resolve(opts.dir ?? process.cwd());
11579
11962
  const paths = resolveHaivePaths41(findProjectRoot45(root));
11580
11963
  const raw = opts.kind ?? "note";
11581
11964
  const kind = ["note", "session_end", "mcp"].includes(raw) ? raw : "note";
11582
11965
  await appendRuntimeJournalEntry3(paths, { kind, message });
11583
- ui.success(`Appended to ${path44.relative(root, paths.runtimeDir)}/session-journal.ndjson`);
11966
+ ui.success(`Appended to ${path45.relative(root, paths.runtimeDir)}/session-journal.ndjson`);
11584
11967
  });
11585
11968
  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) => {
11586
- const root = path44.resolve(opts.dir ?? process.cwd());
11969
+ const root = path45.resolve(opts.dir ?? process.cwd());
11587
11970
  const paths = resolveHaivePaths41(findProjectRoot45(root));
11588
11971
  const limit = Math.min(500, Math.max(1, parseInt(opts.limit, 10) || 30));
11589
11972
  if (!existsSync65(paths.haiveDir)) {
@@ -11602,7 +11985,7 @@ function registerRuntime(program2) {
11602
11985
 
11603
11986
  // src/commands/memory-timeline.ts
11604
11987
  import { existsSync as existsSync66 } from "fs";
11605
- import path45 from "path";
11988
+ import path46 from "path";
11606
11989
  import "commander";
11607
11990
  import {
11608
11991
  collectTimelineEntries as collectTimelineEntries2,
@@ -11618,7 +12001,7 @@ function registerMemoryTimeline(memory2) {
11618
12001
  process.exitCode = 1;
11619
12002
  return;
11620
12003
  }
11621
- const root = path45.resolve(opts.dir ?? process.cwd());
12004
+ const root = path46.resolve(opts.dir ?? process.cwd());
11622
12005
  const paths = resolveHaivePaths42(findProjectRoot46(root));
11623
12006
  if (!existsSync66(paths.memoriesDir)) {
11624
12007
  ui.error("No memories \u2014 run `haive init`.");
@@ -11639,7 +12022,7 @@ function registerMemoryTimeline(memory2) {
11639
12022
 
11640
12023
  // src/commands/memory-conflict-candidates.ts
11641
12024
  import { existsSync as existsSync67 } from "fs";
11642
- import path46 from "path";
12025
+ import path47 from "path";
11643
12026
  import "commander";
11644
12027
  import {
11645
12028
  findLexicalConflictPairs as findLexicalConflictPairs2,
@@ -11661,7 +12044,7 @@ function registerMemoryConflictCandidates(memory2) {
11661
12044
  "decision,architecture,convention,gotcha (lexical scan)",
11662
12045
  "decision,architecture"
11663
12046
  ).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) => {
11664
- const root = path46.resolve(opts.dir ?? process.cwd());
12047
+ const root = path47.resolve(opts.dir ?? process.cwd());
11665
12048
  const paths = resolveHaivePaths43(findProjectRoot47(root));
11666
12049
  if (!existsSync67(paths.memoriesDir)) {
11667
12050
  ui.error("No memories \u2014 run `haive init`.");
@@ -11700,8 +12083,8 @@ function registerMemoryConflictCandidates(memory2) {
11700
12083
  // src/commands/enforce.ts
11701
12084
  import { execFileSync as execFileSync2, spawn as spawn5 } from "child_process";
11702
12085
  import { existsSync as existsSync68 } from "fs";
11703
- import { chmod as chmod2, mkdir as mkdir19, readFile as readFile18, rm as rm3, writeFile as writeFile31 } from "fs/promises";
11704
- import path47 from "path";
12086
+ import { chmod as chmod2, mkdir as mkdir19, readFile as readFile18, rm as rm3, writeFile as writeFile33 } from "fs/promises";
12087
+ import path48 from "path";
11705
12088
  import "commander";
11706
12089
  import {
11707
12090
  findProjectRoot as findProjectRoot48,
@@ -11751,7 +12134,7 @@ function registerEnforce(program2) {
11751
12134
  if (opts.claude !== false) {
11752
12135
  try {
11753
12136
  const result = await installClaudeHooksAtPath(defaultClaudeSettingsPath("project", root));
11754
- ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${path47.relative(root, result.settingsPath)})`);
12137
+ ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${path48.relative(root, result.settingsPath)})`);
11755
12138
  } catch (err) {
11756
12139
  ui.warn(`Claude Code hooks not installed: ${err instanceof Error ? err.message : String(err)}`);
11757
12140
  }
@@ -11759,26 +12142,26 @@ function registerEnforce(program2) {
11759
12142
  ui.info("Agent-agnostic gates are now active at workflow level: MCP, git, CI, and optional client hooks.");
11760
12143
  ui.info("Use `haive run -- <agent command>` for agents that do not expose blocking hooks.");
11761
12144
  });
11762
- enforce.command("status").description("Show whether this project has agent-agnostic hAIve enforcement installed.").option("-d, --dir <dir>", "project root").option("--json", "emit JSON", false).action(async (opts) => {
12145
+ enforce.command("status").description("Show whether this project has agent-agnostic hAIve enforcement installed.").option("-d, --dir <dir>", "project root").option("--explain", "group findings by blocking/review/info and show repair commands", false).option("--json", "emit JSON", false).action(async (opts) => {
11763
12146
  const report = await buildEnforcementReport(opts.dir, "local");
11764
- printReport(report, Boolean(opts.json));
12147
+ printReport(report, Boolean(opts.json), Boolean(opts.explain));
11765
12148
  if (report.should_block) process.exitCode = 1;
11766
12149
  });
11767
- enforce.command("check").description("Run the hAIve policy gate. Intended for pre-commit, pre-push, wrappers, and any agent client.").option("-d, --dir <dir>", "project root").option("--stage <stage>", "local | pre-commit | pre-push | ci", "local").option("--json", "emit JSON", false).action(async (opts) => {
12150
+ enforce.command("check").description("Run the hAIve policy gate. Intended for pre-commit, pre-push, wrappers, and any agent client.").option("-d, --dir <dir>", "project root").option("--stage <stage>", "local | pre-commit | pre-push | ci", "local").option("--explain", "group findings by blocking/review/info and show repair commands", false).option("--json", "emit JSON", false).action(async (opts) => {
11768
12151
  const report = await buildEnforcementReport(opts.dir, opts.stage ?? "local");
11769
- printReport(report, Boolean(opts.json));
12152
+ printReport(report, Boolean(opts.json), Boolean(opts.explain));
11770
12153
  if (report.should_block) process.exit(2);
11771
12154
  });
11772
12155
  enforce.command("cleanup").description("Remove generated hAIve runtime/cache artifacts that should not appear in commits.").option("-d, --dir <dir>", "project root").option("--dry-run", "print what would be removed without deleting", false).action(async (opts) => {
11773
12156
  const root = findProjectRoot48(opts.dir);
11774
12157
  const paths = resolveHaivePaths44(root);
11775
12158
  const targets = [
11776
- path47.join(paths.haiveDir, ".cache"),
11777
- path47.join(paths.haiveDir, ".runtime")
12159
+ path48.join(paths.haiveDir, ".cache"),
12160
+ path48.join(paths.haiveDir, ".runtime")
11778
12161
  ];
11779
12162
  for (const target of targets) {
11780
12163
  if (!existsSync68(target)) continue;
11781
- const rel = path47.relative(root, target);
12164
+ const rel = path48.relative(root, target);
11782
12165
  if (opts.dryRun) ui.info(`would remove ${rel}`);
11783
12166
  else {
11784
12167
  await rm3(target, { recursive: true, force: true });
@@ -11786,9 +12169,9 @@ function registerEnforce(program2) {
11786
12169
  }
11787
12170
  }
11788
12171
  });
11789
- enforce.command("ci").description("CI entrypoint: fail if the repository violates hAIve enforcement policy.").option("-d, --dir <dir>", "project root").option("--json", "emit JSON", false).action(async (opts) => {
12172
+ enforce.command("ci").description("CI entrypoint: fail if the repository violates hAIve enforcement policy.").option("-d, --dir <dir>", "project root").option("--explain", "group findings by blocking/review/info and show repair commands", false).option("--json", "emit JSON", false).action(async (opts) => {
11790
12173
  const report = await buildEnforcementReport(opts.dir, "ci");
11791
- printReport(report, Boolean(opts.json));
12174
+ printReport(report, Boolean(opts.json), Boolean(opts.explain));
11792
12175
  if (report.should_block) process.exit(2);
11793
12176
  });
11794
12177
  enforce.command("session-start").description("Claude Code SessionStart hook: inject briefing and write a local briefing marker.").option("-d, --dir <dir>", "project root").option("--task <text>", "task text to rank memories").option("--source <name>", "marker source", "claude-session-start").option("--session-id <id>", "agent session id").action(async (opts) => {
@@ -11901,7 +12284,7 @@ async function runWithEnforcement(command, args, opts) {
11901
12284
  process.exit(2);
11902
12285
  }
11903
12286
  ui.info(`hAIve briefing marker created for wrapped agent session: ${sessionId}`);
11904
- ui.info(`Briefing written to ${path47.relative(root, briefingFile)} and exported as HAIVE_BRIEFING_FILE`);
12287
+ ui.info(`Briefing written to ${path48.relative(root, briefingFile)} and exported as HAIVE_BRIEFING_FILE`);
11905
12288
  const child = spawn5(command, args, {
11906
12289
  cwd: root,
11907
12290
  stdio: "inherit",
@@ -11950,9 +12333,9 @@ async function writeWrapperBriefing(paths, sessionId, task) {
11950
12333
  source: "haive-run",
11951
12334
  memoryIds: briefing.memories.map((m) => m.id)
11952
12335
  });
11953
- const dir = path47.join(paths.runtimeDir, "enforcement", "briefings");
12336
+ const dir = path48.join(paths.runtimeDir, "enforcement", "briefings");
11954
12337
  await mkdir19(dir, { recursive: true });
11955
- const file = path47.join(dir, `${sessionId}.md`);
12338
+ const file = path48.join(dir, `${sessionId}.md`);
11956
12339
  const parts = [
11957
12340
  "# hAIve Briefing",
11958
12341
  "",
@@ -11970,7 +12353,7 @@ async function writeWrapperBriefing(paths, sessionId, task) {
11970
12353
  if (briefing.setup_warnings.length > 0) {
11971
12354
  parts.push("", "## Setup Warnings", ...briefing.setup_warnings.map((w) => `- ${w}`));
11972
12355
  }
11973
- await writeFile31(file, parts.join("\n") + "\n", "utf8");
12356
+ await writeFile33(file, parts.join("\n") + "\n", "utf8");
11974
12357
  return file;
11975
12358
  }
11976
12359
  async function buildEnforcementReport(dir, stage, sessionId) {
@@ -11981,7 +12364,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
11981
12364
  const mode = config.enforcement?.mode ?? "strict";
11982
12365
  const findings = [];
11983
12366
  if (!initialized) {
11984
- return {
12367
+ return withCategories({
11985
12368
  root,
11986
12369
  initialized,
11987
12370
  mode,
@@ -11994,19 +12377,19 @@ async function buildEnforcementReport(dir, stage, sessionId) {
11994
12377
  fix: "Run `haive init` or `haive enforce install`.",
11995
12378
  impact: 100
11996
12379
  }]
11997
- };
12380
+ });
11998
12381
  }
11999
12382
  if (mode === "off") {
12000
- return {
12383
+ return withCategories({
12001
12384
  root,
12002
12385
  initialized,
12003
12386
  mode,
12004
12387
  score: buildScore([], config.enforcement?.scoreThreshold),
12005
12388
  should_block: false,
12006
12389
  findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
12007
- };
12390
+ });
12008
12391
  }
12009
- findings.push(...await inspectIntegrationVersions(root, "0.9.15"));
12392
+ findings.push(...await inspectIntegrationVersions(root, "0.9.17"));
12010
12393
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
12011
12394
  const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
12012
12395
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
@@ -12056,13 +12439,23 @@ async function buildEnforcementReport(dir, stage, sessionId) {
12056
12439
  });
12057
12440
  }
12058
12441
  const hasErrors = findings.some((f) => f.severity === "error");
12059
- return {
12442
+ return withCategories({
12060
12443
  root,
12061
12444
  initialized,
12062
12445
  mode,
12063
12446
  score: buildScore(findings, config.enforcement?.scoreThreshold),
12064
12447
  should_block: mode === "strict" && hasErrors,
12065
12448
  findings
12449
+ });
12450
+ }
12451
+ function withCategories(report) {
12452
+ return {
12453
+ ...report,
12454
+ categories: {
12455
+ blocking: report.findings.filter((f) => f.severity === "error"),
12456
+ review: report.findings.filter((f) => f.severity === "warn"),
12457
+ info: report.findings.filter((f) => f.severity === "info" || f.severity === "ok")
12458
+ }
12066
12459
  };
12067
12460
  }
12068
12461
  async function hasRecentSessionRecap(paths) {
@@ -12123,7 +12516,7 @@ async function verifyDecisionCoverage(paths, stage, sessionId) {
12123
12516
  const relevant = all.map(({ memory: memory2 }) => memory2).filter((memory2) => {
12124
12517
  const fm = memory2.frontmatter;
12125
12518
  if (!policyTypes.has(fm.type)) return false;
12126
- if (fm.status === "rejected" || fm.status === "deprecated" || fm.status === "stale") return false;
12519
+ if (fm.status !== "validated") return false;
12127
12520
  return memoryMatchesAnchorPaths6(memory2, changedFiles);
12128
12521
  });
12129
12522
  if (relevant.length === 0) {
@@ -12206,7 +12599,7 @@ async function inspectIntegrationVersions(root, expectedVersion) {
12206
12599
  ];
12207
12600
  const findings = [];
12208
12601
  for (const rel of files) {
12209
- const file = path47.join(root, rel);
12602
+ const file = path48.join(root, rel);
12210
12603
  if (!existsSync68(file)) continue;
12211
12604
  const text = await readFile18(file, "utf8").catch(() => "");
12212
12605
  for (const bin of extractAbsoluteHaiveBins2(text)) {
@@ -12273,7 +12666,9 @@ async function getChangedFiles(root, stage) {
12273
12666
  if (file) files.add(file);
12274
12667
  }
12275
12668
  }
12276
- return [...files].filter((file) => !file.startsWith(".ai/.runtime/") && !file.startsWith(".ai/.cache/"));
12669
+ return [...files].filter(
12670
+ (file) => !file.startsWith(".ai/.runtime/") && !file.startsWith(".ai/.cache/") && !file.startsWith(".ai/.usage/") && file !== ".ai/.usage/tool-usage.jsonl"
12671
+ );
12277
12672
  }
12278
12673
  function buildScore(findings, threshold = 80) {
12279
12674
  const checks = {
@@ -12294,8 +12689,8 @@ function buildScore(findings, threshold = 80) {
12294
12689
  };
12295
12690
  }
12296
12691
  async function installGitEnforcement(root) {
12297
- const hooksDir = path47.join(root, ".git", "hooks");
12298
- if (!existsSync68(path47.join(root, ".git"))) {
12692
+ const hooksDir = path48.join(root, ".git", "hooks");
12693
+ if (!existsSync68(path48.join(root, ".git"))) {
12299
12694
  ui.warn("No .git directory found; git enforcement hooks skipped.");
12300
12695
  return;
12301
12696
  }
@@ -12317,31 +12712,31 @@ haive enforce check --stage pre-push --dir . || exit $?
12317
12712
  }
12318
12713
  ];
12319
12714
  for (const hook of hooks) {
12320
- const file = path47.join(hooksDir, hook.name);
12715
+ const file = path48.join(hooksDir, hook.name);
12321
12716
  if (existsSync68(file)) {
12322
12717
  const current = await readFile18(file, "utf8").catch(() => "");
12323
12718
  if (current.includes(ENFORCE_HOOK_MARKER)) {
12324
- await writeFile31(file, hook.body, "utf8");
12719
+ await writeFile33(file, hook.body, "utf8");
12325
12720
  } else {
12326
- await writeFile31(file, `${current.trimEnd()}
12721
+ await writeFile33(file, `${current.trimEnd()}
12327
12722
 
12328
12723
  ${hook.body}`, "utf8");
12329
12724
  }
12330
12725
  } else {
12331
- await writeFile31(file, hook.body, "utf8");
12726
+ await writeFile33(file, hook.body, "utf8");
12332
12727
  }
12333
12728
  await chmod2(file, 493);
12334
12729
  }
12335
12730
  ui.success("Installed blocking git enforcement hooks: pre-commit, pre-push");
12336
12731
  }
12337
12732
  async function installCiEnforcement(root) {
12338
- const workflowPath = path47.join(root, ".github", "workflows", "haive-enforcement.yml");
12339
- await mkdir19(path47.dirname(workflowPath), { recursive: true });
12733
+ const workflowPath = path48.join(root, ".github", "workflows", "haive-enforcement.yml");
12734
+ await mkdir19(path48.dirname(workflowPath), { recursive: true });
12340
12735
  if (existsSync68(workflowPath)) {
12341
12736
  ui.info("GitHub Actions enforcement workflow already exists \u2014 skipped");
12342
12737
  return;
12343
12738
  }
12344
- await writeFile31(workflowPath, `name: haive-enforcement
12739
+ await writeFile33(workflowPath, `name: haive-enforcement
12345
12740
 
12346
12741
  on:
12347
12742
  pull_request:
@@ -12365,9 +12760,9 @@ jobs:
12365
12760
  - name: Enforce hAIve policy
12366
12761
  run: haive enforce ci
12367
12762
  `, "utf8");
12368
- ui.success(`Created ${path47.relative(root, workflowPath)}`);
12763
+ ui.success(`Created ${path48.relative(root, workflowPath)}`);
12369
12764
  }
12370
- function printReport(report, json) {
12765
+ function printReport(report, json, explain = false) {
12371
12766
  if (json) {
12372
12767
  console.log(JSON.stringify(report, null, 2));
12373
12768
  return;
@@ -12375,14 +12770,32 @@ function printReport(report, json) {
12375
12770
  console.log(ui.bold(`hAIve enforcement \u2014 ${report.mode}`));
12376
12771
  console.log(ui.dim(` root: ${report.root}`));
12377
12772
  console.log(ui.dim(` score: ${report.score.score}% / threshold ${report.score.threshold}%`));
12378
- for (const finding of report.findings) {
12379
- const marker = finding.severity === "error" ? ui.red("\u2717") : finding.severity === "warn" ? ui.yellow("\u26A0") : finding.severity === "ok" ? ui.green("\u2713") : ui.dim("\u2022");
12380
- console.log(`${marker} ${finding.code}: ${finding.message}`);
12381
- if (finding.fix) console.log(ui.dim(` fix: ${finding.fix}`));
12773
+ if (explain) {
12774
+ printFindingGroup("Blocking", report.categories.blocking, "error");
12775
+ printFindingGroup("Review", report.categories.review, "warn");
12776
+ printFindingGroup("Info", report.categories.info, "info");
12777
+ } else {
12778
+ for (const finding of report.findings) printFinding(finding);
12382
12779
  }
12383
12780
  if (report.should_block) ui.error("hAIve enforcement gate failed.");
12384
12781
  else ui.success("hAIve enforcement gate passed.");
12385
12782
  }
12783
+ function printFindingGroup(title, findings, tone) {
12784
+ if (findings.length === 0) return;
12785
+ console.log();
12786
+ const heading = tone === "error" ? ui.red(title) : tone === "warn" ? ui.yellow(title) : ui.bold(title);
12787
+ console.log(ui.bold(`${heading} (${findings.length})`));
12788
+ const scoreFinding = findings.find((f) => f.code === "enforcement-score-below-threshold");
12789
+ for (const finding of findings.filter((f) => f.code !== "enforcement-score-below-threshold")) {
12790
+ printFinding(finding, true);
12791
+ }
12792
+ if (scoreFinding) printFinding(scoreFinding, true);
12793
+ }
12794
+ function printFinding(finding, explain = false) {
12795
+ const marker = finding.severity === "error" ? ui.red("\u2717") : finding.severity === "warn" ? ui.yellow("\u26A0") : finding.severity === "ok" ? ui.green("\u2713") : ui.dim("\u2022");
12796
+ console.log(`${marker} ${finding.code}: ${finding.message}`);
12797
+ if (finding.fix) console.log(ui.dim(`${explain ? " repair: " : " fix: "}${finding.fix}`));
12798
+ }
12386
12799
  async function readHookPayload() {
12387
12800
  const raw = await readStdin2(MAX_STDIN_BYTES2);
12388
12801
  if (!raw.trim()) return {};
@@ -12464,7 +12877,7 @@ function registerRun(program2) {
12464
12877
 
12465
12878
  // src/index.ts
12466
12879
  var program = new Command51();
12467
- program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.15");
12880
+ program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.17").option("--advanced", "show maintenance and experimental commands in help");
12468
12881
  registerInit(program);
12469
12882
  registerWelcome(program);
12470
12883
  registerResolveProject(program);
@@ -12517,6 +12930,32 @@ registerBenchmark(program);
12517
12930
  registerDoctor(program);
12518
12931
  registerPlayback(program);
12519
12932
  registerPrecommit(program);
12933
+ var CORE_ROOT_COMMANDS = /* @__PURE__ */ new Set([
12934
+ "init",
12935
+ "doctor",
12936
+ "agent",
12937
+ "enforce",
12938
+ "run",
12939
+ "briefing",
12940
+ "sync",
12941
+ "mcp",
12942
+ "memory",
12943
+ "session",
12944
+ "precommit",
12945
+ "welcome"
12946
+ ]);
12947
+ var CORE_MEMORY_COMMANDS = /* @__PURE__ */ new Set([
12948
+ "add",
12949
+ "list",
12950
+ "query",
12951
+ "show",
12952
+ "verify",
12953
+ "lint",
12954
+ "tried",
12955
+ "rm"
12956
+ ]);
12957
+ var CORE_SESSION_COMMANDS = /* @__PURE__ */ new Set(["end"]);
12958
+ applySurfaceVisibility(program);
12520
12959
  program.parseAsync(process.argv).catch((err) => {
12521
12960
  if (isZodError(err)) {
12522
12961
  for (const issue of err.issues) {
@@ -12528,6 +12967,44 @@ program.parseAsync(process.argv).catch((err) => {
12528
12967
  }
12529
12968
  process.exit(1);
12530
12969
  });
12970
+ function applySurfaceVisibility(root) {
12971
+ const showAdvanced = process.argv.includes("--advanced") || process.env.HAIVE_SHOW_ADVANCED === "1";
12972
+ if (!showAdvanced) hideNonCoreCommands(root);
12973
+ root.addHelpText(
12974
+ "after",
12975
+ [
12976
+ "",
12977
+ "Default help shows the core hAIve harness: init, doctor, agent setup, briefing, enforcement,",
12978
+ "sync, session recaps, and high-signal memory commands.",
12979
+ "Run `haive --advanced --help` or set HAIVE_SHOW_ADVANCED=1 to show maintenance and experimental commands."
12980
+ ].join("\n")
12981
+ );
12982
+ const memoryCommand = root.commands.find((cmd) => cmd.name() === "memory");
12983
+ memoryCommand?.addHelpText(
12984
+ "after",
12985
+ [
12986
+ "",
12987
+ "Default help shows the memory commands that support the core harness workflow.",
12988
+ "Run `haive --advanced memory --help` or set HAIVE_SHOW_ADVANCED=1 to show review, import, digest, timeline, and conflict tools."
12989
+ ].join("\n")
12990
+ );
12991
+ }
12992
+ function hideNonCoreCommands(command) {
12993
+ for (const child of command.commands) {
12994
+ if (!isCoreCommand(command, child)) {
12995
+ child._hidden = true;
12996
+ }
12997
+ hideNonCoreCommands(child);
12998
+ }
12999
+ }
13000
+ function isCoreCommand(parent, child) {
13001
+ const parentName = parent.name();
13002
+ const childName = child.name();
13003
+ if (parentName === "haive") return CORE_ROOT_COMMANDS.has(childName);
13004
+ if (parentName === "memory") return CORE_MEMORY_COMMANDS.has(childName);
13005
+ if (parentName === "session") return CORE_SESSION_COMMANDS.has(childName);
13006
+ return true;
13007
+ }
12531
13008
  function isZodError(err) {
12532
13009
  return err !== null && typeof err === "object" && "issues" in err && Array.isArray(err.issues);
12533
13010
  }