@hiveai/cli 0.9.14 → 0.9.15

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
@@ -3063,6 +3063,7 @@ import {
3063
3063
  loadMemoriesFromDir as loadMemoriesFromDir13,
3064
3064
  loadUsageIndex as loadUsageIndex7,
3065
3065
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths22,
3066
+ pathsOverlap,
3066
3067
  queryCodeMap as queryCodeMap2,
3067
3068
  resolveBriefingBudget as resolveBriefingBudget2,
3068
3069
  serializeMemory as serializeMemory9,
@@ -3126,7 +3127,7 @@ import {
3126
3127
  getUsage as getUsage9,
3127
3128
  loadMemoriesFromDir as loadMemoriesFromDir20,
3128
3129
  loadUsageIndex as loadUsageIndex11,
3129
- pathsOverlap,
3130
+ pathsOverlap as pathsOverlap2,
3130
3131
  tokenizeQuery as tokenizeQuery5
3131
3132
  } from "@hiveai/core";
3132
3133
  import { z as z27 } from "zod";
@@ -4654,10 +4655,14 @@ ${m.content}`).join("\n\n---\n\n"),
4654
4655
  const createdAt = loaded?.memory.frontmatter.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
4655
4656
  if (isDecaying(u, createdAt)) decayWarnings.push(m.id);
4656
4657
  }
4657
- const outputMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : input.format === "actions" ? trimmedMemories.map((m) => ({
4658
+ const formattedMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : input.format === "actions" ? trimmedMemories.map((m) => ({
4658
4659
  ...m,
4659
4660
  body: extractActionsBriefBody2(m.body)
4660
4661
  })) : trimmedMemories;
4662
+ const outputMemories = formattedMemories.map((m) => ({
4663
+ ...m,
4664
+ why: explainWhySurfaced(m, byId.get(m.id), input.files, inferred)
4665
+ }));
4661
4666
  let symbolLocations;
4662
4667
  const symbolsToLookup = new Set(input.symbols);
4663
4668
  for (const m of outputMemories) {
@@ -4823,6 +4828,44 @@ function compactSummary(body) {
4823
4828
  }
4824
4829
  return body.slice(0, 120);
4825
4830
  }
4831
+ function explainWhySurfaced(memory2, loaded, inputFiles, inferredModules) {
4832
+ const why = [];
4833
+ const fm = loaded?.memory.frontmatter;
4834
+ if (memory2.reasons.includes("anchor") && fm) {
4835
+ const matching = fm.anchor.paths.filter(
4836
+ (p) => inputFiles.length === 0 || inputFiles.some((file) => pathsOverlap(p, file))
4837
+ );
4838
+ if (matching.length > 0) {
4839
+ why.push(`Anchored to touched path${matching.length === 1 ? "" : "s"}: ${matching.slice(0, 4).join(", ")}`);
4840
+ } else if (fm.anchor.paths.length > 0) {
4841
+ why.push(`Pulled by related anchor: ${fm.anchor.paths.slice(0, 4).join(", ")}`);
4842
+ }
4843
+ if (fm.anchor.symbols.length > 0) {
4844
+ why.push(`Anchor symbol${fm.anchor.symbols.length === 1 ? "" : "s"}: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
4845
+ }
4846
+ }
4847
+ if (memory2.reasons.includes("module")) {
4848
+ const moduleHints = [
4849
+ ...memory2.module ? [memory2.module] : [],
4850
+ ...memory2.tags.filter((tag) => inferredModules.includes(tag))
4851
+ ];
4852
+ const shown = moduleHints.length > 0 ? [...new Set(moduleHints)].join(", ") : inferredModules.join(", ");
4853
+ why.push(shown ? `Matched inferred module/tag: ${shown}` : "Matched inferred module context.");
4854
+ }
4855
+ if (memory2.reasons.includes("domain")) {
4856
+ why.push("Matched inferred domain from the target file paths.");
4857
+ }
4858
+ if (memory2.reasons.includes("semantic")) {
4859
+ const score = memory2.semantic_score !== void 0 ? ` score=${Math.round(memory2.semantic_score * 100) / 100}` : "";
4860
+ why.push(`${memory2.match_quality === "exact" ? "Literal task match" : "Semantic/task relevance"}${score}.`);
4861
+ }
4862
+ why.push(`Confidence: ${memory2.confidence}; read ${memory2.read_count} time${memory2.read_count === 1 ? "" : "s"}.`);
4863
+ if (memory2.type === "attempt") why.push("Failed-approach record; read before repeating the same path.");
4864
+ if (memory2.status === "proposed" || memory2.status === "draft") {
4865
+ why.push("Unvalidated record; use cautiously or ask a human before treating it as policy.");
4866
+ }
4867
+ return why;
4868
+ }
4826
4869
  async function trySemanticHits(ctx, task, limit) {
4827
4870
  let mod;
4828
4871
  try {
@@ -5585,7 +5628,7 @@ async function memConflicts(input, ctx) {
5585
5628
  const otherText = (other.memory.body + " " + fm.tags.join(" ")).toLowerCase();
5586
5629
  const reasons = [];
5587
5630
  const sim = simScores?.get(fm.id) ?? null;
5588
- const hasPathOverlap = fm.anchor.paths.some((p) => targetPaths.some((tp) => pathsOverlap(p, tp)));
5631
+ const hasPathOverlap = fm.anchor.paths.some((p) => targetPaths.some((tp) => pathsOverlap2(p, tp)));
5589
5632
  const otherTokens = new Set(tokenizeQuery5(otherText));
5590
5633
  const tokenOverlap = countIntersection(targetTokens, otherTokens);
5591
5634
  const isSemanticNeighbor = sim !== null && sim >= input.min_score;
@@ -5620,7 +5663,7 @@ async function memConflicts(input, ctx) {
5620
5663
  body_preview: other.memory.body.split("\n").slice(0, 4).join("\n").slice(0, 300),
5621
5664
  similarity: sim,
5622
5665
  reasons,
5623
- shared_paths: fm.anchor.paths.filter((p) => targetPaths.some((tp) => pathsOverlap(p, tp)))
5666
+ shared_paths: fm.anchor.paths.filter((p) => targetPaths.some((tp) => pathsOverlap2(p, tp)))
5624
5667
  });
5625
5668
  }
5626
5669
  conflicts.sort((a, b) => {
@@ -5707,10 +5750,13 @@ async function preCommitCheck(input, ctx) {
5707
5750
  const filesTouching = new Set(relevantMatches.map((m) => m.id));
5708
5751
  const staleHits = verifyResult.results.filter((r) => r.stale && filesTouching.has(r.id));
5709
5752
  const blockOn = input.block_on;
5710
- const blockingWarnings = apResult.warnings.filter(isBlockingWarning);
5753
+ const classifiedWarnings = apResult.warnings.map(classifyWarning);
5754
+ const blockingWarnings = classifiedWarnings.filter((w) => w.level === "blocking");
5755
+ const reviewWarnings = classifiedWarnings.filter((w) => w.level === "review");
5756
+ const infoWarnings = classifiedWarnings.filter((w) => w.level === "info");
5711
5757
  let should_block = false;
5712
5758
  if (blockOn !== "never") {
5713
- if (blockOn === "any" && (apResult.warnings.length > 0 || staleHits.length > 0)) should_block = true;
5759
+ if (blockOn === "any" && (blockingWarnings.length > 0 || reviewWarnings.length > 0 || staleHits.length > 0)) should_block = true;
5714
5760
  if (blockOn === "high-confidence" && (blockingWarnings.length > 0 || staleHits.length > 0)) should_block = true;
5715
5761
  }
5716
5762
  const relevant_memories = relevantMatches.slice(0, 8).map((m) => ({
@@ -5724,10 +5770,12 @@ async function preCommitCheck(input, ctx) {
5724
5770
  summary: {
5725
5771
  anti_patterns: apResult.warnings.length,
5726
5772
  blocking_warnings: blockingWarnings.length,
5773
+ review_warnings: reviewWarnings.length,
5774
+ info_warnings: infoWarnings.length,
5727
5775
  relevant_memories: relevant_memories.length,
5728
5776
  stale_anchors: staleHits.length
5729
5777
  },
5730
- warnings: apResult.warnings,
5778
+ warnings: classifiedWarnings,
5731
5779
  relevant_memories,
5732
5780
  stale_anchors: staleHits.map((r) => ({
5733
5781
  id: r.id,
@@ -5737,6 +5785,30 @@ async function preCommitCheck(input, ctx) {
5737
5785
  }))
5738
5786
  };
5739
5787
  }
5788
+ function classifyWarning(warning) {
5789
+ if (isBlockingWarning(warning)) {
5790
+ return {
5791
+ ...warning,
5792
+ level: "blocking",
5793
+ rationale: "authoritative/trusted memory plus strong semantic match to the diff (score >= 0.65)"
5794
+ };
5795
+ }
5796
+ const hasSemantic = warning.reasons.includes("semantic");
5797
+ const semanticScore = warning.semantic_score ?? 0;
5798
+ const highConfidence = warning.confidence === "authoritative" || warning.confidence === "trusted";
5799
+ if (hasSemantic && semanticScore >= 0.45 || highConfidence && warning.reasons.includes("anchor") && warning.reasons.includes("literal")) {
5800
+ return {
5801
+ ...warning,
5802
+ 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"
5804
+ };
5805
+ }
5806
+ return {
5807
+ ...warning,
5808
+ level: "info",
5809
+ rationale: "weak signal only (literal/anchor/low semantic evidence); surfaced for audit, hidden in concise CLI output"
5810
+ };
5811
+ }
5740
5812
  function isBlockingWarning(warning) {
5741
5813
  const highConfidence = warning.confidence === "authoritative" || warning.confidence === "trusted";
5742
5814
  if (!highConfidence) return false;
@@ -6273,7 +6345,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
6273
6345
  };
6274
6346
  }
6275
6347
  var SERVER_NAME = "haive";
6276
- var SERVER_VERSION = "0.9.14";
6348
+ var SERVER_VERSION = "0.9.15";
6277
6349
  function jsonResult(data) {
6278
6350
  return {
6279
6351
  content: [
@@ -6287,9 +6359,12 @@ function jsonResult(data) {
6287
6359
  var ENFORCEMENT_PROFILE_TOOLS = /* @__PURE__ */ new Set([
6288
6360
  "get_briefing",
6289
6361
  "mem_save",
6362
+ "mem_tried",
6290
6363
  "mem_search",
6364
+ "mem_get",
6291
6365
  "mem_verify",
6292
6366
  "mem_relevant_to",
6367
+ "code_map",
6293
6368
  "pre_commit_check",
6294
6369
  "mem_session_end"
6295
6370
  ]);
@@ -10641,9 +10716,9 @@ function parseDays(input) {
10641
10716
 
10642
10717
  // src/commands/doctor.ts
10643
10718
  import { existsSync as existsSync60 } from "fs";
10644
- import { stat } from "fs/promises";
10719
+ import { readFile as readFile17, stat } from "fs/promises";
10645
10720
  import path41 from "path";
10646
- import { execSync as execSync3 } from "child_process";
10721
+ import { execFileSync, execSync as execSync3 } from "child_process";
10647
10722
  import "commander";
10648
10723
  import {
10649
10724
  codeMapPath as codeMapPath2,
@@ -10681,8 +10756,8 @@ function registerDoctor(program2) {
10681
10756
  fix: "haive init"
10682
10757
  });
10683
10758
  } else {
10684
- const { readFile: readFile18 } = await import("fs/promises");
10685
- const content = await readFile18(paths.projectContext, "utf8");
10759
+ const { readFile: readFile19 } = await import("fs/promises");
10760
+ const content = await readFile19(paths.projectContext, "utf8");
10686
10761
  const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
10687
10762
  if (isTemplate) {
10688
10763
  findings.push({
@@ -10808,8 +10883,8 @@ function registerDoctor(program2) {
10808
10883
  let hasClaudeEnforcement = false;
10809
10884
  if (existsSync60(claudeSettings)) {
10810
10885
  try {
10811
- const { readFile: readFile18 } = await import("fs/promises");
10812
- const raw = await readFile18(claudeSettings, "utf8");
10886
+ const { readFile: readFile19 } = await import("fs/promises");
10887
+ const raw = await readFile19(claudeSettings, "utf8");
10813
10888
  hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
10814
10889
  } catch {
10815
10890
  hasClaudeEnforcement = false;
@@ -10832,13 +10907,14 @@ function registerDoctor(program2) {
10832
10907
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
10833
10908
  });
10834
10909
  }
10910
+ findings.push(...await collectInstallFindings(root, "0.9.15"));
10835
10911
  try {
10836
10912
  const legacyRaw = execSync3("haive-mcp --version", {
10837
10913
  encoding: "utf8",
10838
10914
  timeout: 3e3,
10839
10915
  stdio: ["ignore", "pipe", "ignore"]
10840
10916
  }).trim();
10841
- const cliVersion = "0.9.14";
10917
+ const cliVersion = "0.9.15";
10842
10918
  if (legacyRaw && legacyRaw !== cliVersion) {
10843
10919
  findings.push({
10844
10920
  severity: "warn",
@@ -10885,6 +10961,103 @@ function emit(findings, opts) {
10885
10961
  function isSearchTool(name) {
10886
10962
  return ["mem_search", "code_search", "mem_relevant_to", "get_briefing"].includes(name);
10887
10963
  }
10964
+ async function collectInstallFindings(root, expectedVersion) {
10965
+ const findings = [];
10966
+ const haiveBins = listHaiveBins();
10967
+ if (haiveBins.length === 0) {
10968
+ findings.push({
10969
+ severity: "warn",
10970
+ code: "haive-not-on-path",
10971
+ message: "No `haive` binary was found on PATH. Hooks and MCP configs that call `haive` may fail.",
10972
+ fix: `npm install -g @hiveai/cli@${expectedVersion}`
10973
+ });
10974
+ } else {
10975
+ const first = haiveBins[0];
10976
+ const firstVersion = versionForBinary(first);
10977
+ if (firstVersion && firstVersion !== expectedVersion) {
10978
+ findings.push({
10979
+ severity: "warn",
10980
+ code: "path-haive-version-mismatch",
10981
+ message: `PATH resolves haive to ${first} (${firstVersion}), but this build expects ${expectedVersion}.`,
10982
+ fix: `npm install -g @hiveai/cli@${expectedVersion}
10983
+ which -a haive`
10984
+ });
10985
+ }
10986
+ const skewed = haiveBins.map((bin) => ({ bin, version: versionForBinary(bin) })).filter((item) => item.version && item.version !== expectedVersion);
10987
+ if (skewed.length > 0) {
10988
+ findings.push({
10989
+ severity: "info",
10990
+ code: "multiple-haive-binaries",
10991
+ message: `Found ${haiveBins.length} haive binar${haiveBins.length === 1 ? "y" : "ies"} on PATH; ${skewed.length} do not match ${expectedVersion}.`,
10992
+ fix: "Remove stale global installs or ensure hooks call the intended `haive` binary."
10993
+ });
10994
+ }
10995
+ }
10996
+ const integrationFiles = [
10997
+ ".git/hooks/pre-commit",
10998
+ ".git/hooks/pre-push",
10999
+ ".claude/settings.local.json",
11000
+ ".mcp.json",
11001
+ ".cursor/mcp.json",
11002
+ ".vscode/mcp.json"
11003
+ ];
11004
+ for (const rel of integrationFiles) {
11005
+ const file = path41.join(root, rel);
11006
+ if (!existsSync60(file)) continue;
11007
+ const text = await readFile17(file, "utf8").catch(() => "");
11008
+ for (const bin of extractAbsoluteHaiveBins(text)) {
11009
+ const version = versionForBinary(bin);
11010
+ if (!version) {
11011
+ findings.push({
11012
+ severity: "warn",
11013
+ code: "integration-haive-binary-missing",
11014
+ message: `${rel} references ${bin}, but it could not be executed.`,
11015
+ fix: "Run `haive agent setup --no-global` or `haive enforce install` to rewrite project integrations."
11016
+ });
11017
+ } else if (version !== expectedVersion) {
11018
+ findings.push({
11019
+ severity: "warn",
11020
+ code: "integration-haive-version-mismatch",
11021
+ message: `${rel} references ${bin} (${version}), but current hAIve is ${expectedVersion}.`,
11022
+ fix: "Run `haive agent setup --no-global` and `haive enforce install` to refresh stale paths."
11023
+ });
11024
+ }
11025
+ }
11026
+ }
11027
+ return findings;
11028
+ }
11029
+ function listHaiveBins() {
11030
+ try {
11031
+ return execSync3("which -a haive", {
11032
+ encoding: "utf8",
11033
+ timeout: 3e3,
11034
+ stdio: ["ignore", "pipe", "ignore"]
11035
+ }).split("\n").map((line) => line.trim()).filter(Boolean);
11036
+ } catch {
11037
+ return [];
11038
+ }
11039
+ }
11040
+ function versionForBinary(bin) {
11041
+ try {
11042
+ const out = execFileSync(bin, ["--version"], {
11043
+ encoding: "utf8",
11044
+ timeout: 3e3,
11045
+ stdio: ["ignore", "pipe", "ignore"]
11046
+ }).trim();
11047
+ return out.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
11048
+ } catch {
11049
+ return null;
11050
+ }
11051
+ }
11052
+ function extractAbsoluteHaiveBins(text) {
11053
+ const out = /* @__PURE__ */ new Set();
11054
+ const re = /(["'\s])((?:\/[^"'\s]+)*\/haive)\b/g;
11055
+ let match;
11056
+ while (match = re.exec(text)) {
11057
+ if (match[2]) out.add(match[2]);
11058
+ }
11059
+ return [...out].sort();
11060
+ }
10888
11061
 
10889
11062
  // src/commands/playback.ts
10890
11063
  import { existsSync as existsSync61 } from "fs";
@@ -11050,19 +11223,21 @@ function registerPrecommit(program2) {
11050
11223
  console.log(ui.bold(`hAIve precommit \u2014 ${touchedPaths.length} file(s)`));
11051
11224
  console.log(
11052
11225
  ui.dim(
11053
- ` anti-patterns: ${result.summary.anti_patterns} blocking: ${result.summary.blocking_warnings ?? result.summary.anti_patterns} relevant memories: ${result.summary.relevant_memories} stale anchors: ${result.summary.stale_anchors}`
11226
+ ` anti-patterns: ${result.summary.anti_patterns} blocking: ${result.summary.blocking_warnings ?? result.summary.anti_patterns} review: ${result.summary.review_warnings ?? 0} info: ${result.summary.info_warnings ?? 0} relevant memories: ${result.summary.relevant_memories} stale anchors: ${result.summary.stale_anchors}`
11054
11227
  )
11055
11228
  );
11056
11229
  console.log();
11057
- if (result.warnings.length > 0) {
11058
- console.log(ui.bold("\u26A0 Anti-patterns matched:"));
11059
- for (const w of result.warnings.slice(0, 10)) {
11060
- console.log(` ${ui.yellow("\u26A0")} ${w.id} ${ui.dim(`(${w.type}, ${w.confidence})`)}`);
11061
- for (const line of w.body_preview.split("\n").slice(0, 3)) {
11062
- console.log(` ${ui.dim(line)}`);
11063
- }
11064
- console.log(` ${ui.dim("reasons:")} ${w.reasons.join(", ")}`);
11065
- }
11230
+ const blocking = result.warnings.filter((w) => w.level === "blocking");
11231
+ const review = result.warnings.filter((w) => w.level === "review");
11232
+ const info = result.warnings.filter((w) => w.level === "info");
11233
+ printWarnings("Blocking anti-patterns", blocking, "error");
11234
+ printWarnings("Review anti-patterns", review.slice(0, 8), "warn");
11235
+ if (info.length > 0) {
11236
+ console.log(
11237
+ ui.dim(
11238
+ `${info.length} weak anti-pattern signal${info.length === 1 ? "" : "s"} hidden. Use --json to inspect FYI matches.`
11239
+ )
11240
+ );
11066
11241
  console.log();
11067
11242
  }
11068
11243
  if (result.relevant_memories.length > 0) {
@@ -11091,6 +11266,20 @@ function registerPrecommit(program2) {
11091
11266
  }
11092
11267
  });
11093
11268
  }
11269
+ function printWarnings(title, warnings, tone) {
11270
+ if (warnings.length === 0) return;
11271
+ console.log(ui.bold(tone === "error" ? `\u2717 ${title}:` : `\u26A0 ${title}:`));
11272
+ for (const w of warnings) {
11273
+ const marker = tone === "error" ? ui.red("\u2717") : ui.yellow("\u26A0");
11274
+ console.log(` ${marker} ${w.id} ${ui.dim(`(${w.type}, ${w.confidence})`)}`);
11275
+ for (const line of w.body_preview.split("\n").slice(0, 3)) {
11276
+ console.log(` ${ui.dim(line)}`);
11277
+ }
11278
+ console.log(` ${ui.dim("reasons:")} ${w.reasons.join(", ")}`);
11279
+ if (w.rationale) console.log(` ${ui.dim("why shown:")} ${w.rationale}`);
11280
+ }
11281
+ console.log();
11282
+ }
11094
11283
  function runCommand3(cmd, args, cwd) {
11095
11284
  return new Promise((resolve, reject) => {
11096
11285
  const proc = spawn4(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
@@ -11177,7 +11366,9 @@ import { existsSync as existsSync64 } from "fs";
11177
11366
  import "commander";
11178
11367
  import {
11179
11368
  findProjectRoot as findProjectRoot44,
11369
+ getUsage as getUsage20,
11180
11370
  loadMemoriesFromDir as loadMemoriesFromDir35,
11371
+ loadUsageIndex as loadUsageIndex26,
11181
11372
  resolveHaivePaths as resolveHaivePaths40
11182
11373
  } from "@hiveai/core";
11183
11374
  async function lintMemoriesAsync(root) {
@@ -11185,7 +11376,9 @@ async function lintMemoriesAsync(root) {
11185
11376
  const out = [];
11186
11377
  if (!existsSync64(paths.memoriesDir)) return out;
11187
11378
  const loaded = await loadMemoriesFromDir35(paths.memoriesDir);
11379
+ const usage = await loadUsageIndex26(paths);
11188
11380
  const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
11381
+ const actionableWords = /\b(always|never|prefer|use|avoid|because|instead|why|rationale|do not|must|should)\b/i;
11189
11382
  for (const { filePath, memory: memory2 } of loaded) {
11190
11383
  const fm = memory2.frontmatter;
11191
11384
  if (fm.type === "session_recap") continue;
@@ -11200,6 +11393,15 @@ async function lintMemoriesAsync(root) {
11200
11393
  message: "Body looks very short (< ~40 chars of prose after headings). Prefer actionable detail."
11201
11394
  });
11202
11395
  }
11396
+ if (["decision", "gotcha", "convention", "architecture", "attempt"].includes(fm.type) && fm.status !== "rejected" && !actionableWords.test(naked)) {
11397
+ out.push({
11398
+ file: filePath,
11399
+ id: fm.id,
11400
+ severity: "info",
11401
+ code: "LOW_ACTIONABILITY",
11402
+ message: "Record does not contain obvious action/rationale words. Add the concrete rule, why it exists, and what to do instead."
11403
+ });
11404
+ }
11203
11405
  if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated") {
11204
11406
  out.push({
11205
11407
  file: filePath,
@@ -11237,9 +11439,64 @@ async function lintMemoriesAsync(root) {
11237
11439
  message: "No Markdown heading (#/##/###) \u2014 add one so humans and auditors can skim the memo quickly."
11238
11440
  });
11239
11441
  }
11442
+ const u = getUsage20(usage, fm.id);
11443
+ if (fm.status === "validated" && u.read_count === 0) {
11444
+ out.push({
11445
+ file: filePath,
11446
+ id: fm.id,
11447
+ severity: "info",
11448
+ code: "NEVER_READ",
11449
+ message: "Validated record has never been surfaced/read. Consider improving tags/anchors or archiving it if it is not useful."
11450
+ });
11451
+ }
11452
+ }
11453
+ for (const dup of nearDuplicatePairs(loaded)) {
11454
+ out.push({
11455
+ file: dup.file,
11456
+ id: dup.id,
11457
+ severity: "warn",
11458
+ code: "NEAR_DUPLICATE",
11459
+ message: `Body overlaps ~${Math.round(dup.score * 100)}% with ${dup.otherId}. Merge or deprecate one record to reduce briefing noise.`
11460
+ });
11461
+ }
11462
+ return out;
11463
+ }
11464
+ function nearDuplicatePairs(loaded) {
11465
+ const out = [];
11466
+ const candidates = loaded.filter(({ memory: memory2 }) => {
11467
+ const fm = memory2.frontmatter;
11468
+ return fm.type !== "session_recap" && fm.status !== "rejected" && fm.status !== "deprecated";
11469
+ });
11470
+ for (let i = 0; i < candidates.length; i++) {
11471
+ for (let j = i + 1; j < candidates.length; j++) {
11472
+ const a = candidates[i];
11473
+ const b = candidates[j];
11474
+ if (a.memory.frontmatter.scope !== b.memory.frontmatter.scope) continue;
11475
+ if (a.memory.frontmatter.type !== b.memory.frontmatter.type) continue;
11476
+ const score = jaccard2(tokenSet(a.memory.body), tokenSet(b.memory.body));
11477
+ if (score >= 0.72) {
11478
+ out.push({
11479
+ id: a.memory.frontmatter.id,
11480
+ otherId: b.memory.frontmatter.id,
11481
+ file: a.filePath,
11482
+ score
11483
+ });
11484
+ }
11485
+ }
11240
11486
  }
11241
11487
  return out;
11242
11488
  }
11489
+ function tokenSet(body) {
11490
+ return new Set(
11491
+ (body.toLowerCase().match(/\b[a-z0-9]{4,}\b/g) ?? []).filter((word) => !["this", "that", "with", "from", "have"].includes(word))
11492
+ );
11493
+ }
11494
+ function jaccard2(a, b) {
11495
+ if (a.size === 0 || b.size === 0) return 0;
11496
+ let inter = 0;
11497
+ for (const item of a) if (b.has(item)) inter++;
11498
+ return inter / (a.size + b.size - inter);
11499
+ }
11243
11500
  function registerMemoryLint(parent) {
11244
11501
  parent.command("lint").description(
11245
11502
  "Heuristic corpus checks (anchors on key types, headings, verbosity). Static analysis only."
@@ -11441,9 +11698,9 @@ function registerMemoryConflictCandidates(memory2) {
11441
11698
  }
11442
11699
 
11443
11700
  // src/commands/enforce.ts
11444
- import { spawn as spawn5 } from "child_process";
11701
+ import { execFileSync as execFileSync2, spawn as spawn5 } from "child_process";
11445
11702
  import { existsSync as existsSync68 } from "fs";
11446
- import { chmod as chmod2, mkdir as mkdir19, readFile as readFile17, rm as rm3, writeFile as writeFile31 } from "fs/promises";
11703
+ import { chmod as chmod2, mkdir as mkdir19, readFile as readFile18, rm as rm3, writeFile as writeFile31 } from "fs/promises";
11447
11704
  import path47 from "path";
11448
11705
  import "commander";
11449
11706
  import {
@@ -11749,6 +12006,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
11749
12006
  findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
11750
12007
  };
11751
12008
  }
12009
+ findings.push(...await inspectIntegrationVersions(root, "0.9.15"));
11752
12010
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
11753
12011
  const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
11754
12012
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
@@ -11937,6 +12195,71 @@ async function findGeneratedArtifacts(paths) {
11937
12195
  impact: 10
11938
12196
  }];
11939
12197
  }
12198
+ async function inspectIntegrationVersions(root, expectedVersion) {
12199
+ const files = [
12200
+ ".git/hooks/pre-commit",
12201
+ ".git/hooks/pre-push",
12202
+ ".claude/settings.local.json",
12203
+ ".mcp.json",
12204
+ ".cursor/mcp.json",
12205
+ ".vscode/mcp.json"
12206
+ ];
12207
+ const findings = [];
12208
+ for (const rel of files) {
12209
+ const file = path47.join(root, rel);
12210
+ if (!existsSync68(file)) continue;
12211
+ const text = await readFile18(file, "utf8").catch(() => "");
12212
+ for (const bin of extractAbsoluteHaiveBins2(text)) {
12213
+ const version = versionForBinary2(bin);
12214
+ if (!version) {
12215
+ findings.push({
12216
+ severity: "warn",
12217
+ code: "integration-haive-binary-missing",
12218
+ message: `${rel} references ${bin}, but that binary could not be executed.`,
12219
+ fix: "Run `haive agent setup --no-global` or `haive enforce install` to refresh project integrations.",
12220
+ impact: 0
12221
+ });
12222
+ } else if (version !== expectedVersion) {
12223
+ findings.push({
12224
+ severity: "warn",
12225
+ code: "integration-haive-version-mismatch",
12226
+ message: `${rel} references hAIve ${version} at ${bin}; current hAIve is ${expectedVersion}.`,
12227
+ fix: "Run `haive agent setup --no-global` and `haive enforce install` to repair stale hooks/configs.",
12228
+ impact: 0
12229
+ });
12230
+ }
12231
+ }
12232
+ }
12233
+ if (findings.length === 0) {
12234
+ return [{
12235
+ severity: "ok",
12236
+ code: "integration-version-check",
12237
+ message: "No stale absolute hAIve binary paths were found in project hooks/MCP configs."
12238
+ }];
12239
+ }
12240
+ return findings;
12241
+ }
12242
+ function extractAbsoluteHaiveBins2(text) {
12243
+ const out = /* @__PURE__ */ new Set();
12244
+ const re = /(["'\s])((?:\/[^"'\s]+)*\/haive)\b/g;
12245
+ let match;
12246
+ while (match = re.exec(text)) {
12247
+ if (match[2]) out.add(match[2]);
12248
+ }
12249
+ return [...out].sort();
12250
+ }
12251
+ function versionForBinary2(bin) {
12252
+ try {
12253
+ const out = execFileSync2(bin, ["--version"], {
12254
+ encoding: "utf8",
12255
+ timeout: 3e3,
12256
+ stdio: ["ignore", "pipe", "ignore"]
12257
+ }).trim();
12258
+ return out.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
12259
+ } catch {
12260
+ return null;
12261
+ }
12262
+ }
11940
12263
  async function getChangedFiles(root, stage) {
11941
12264
  const commands = stage === "pre-commit" ? [["diff", "--cached", "--name-only"]] : [
11942
12265
  ["diff", "--cached", "--name-only"],
@@ -11996,7 +12319,7 @@ haive enforce check --stage pre-push --dir . || exit $?
11996
12319
  for (const hook of hooks) {
11997
12320
  const file = path47.join(hooksDir, hook.name);
11998
12321
  if (existsSync68(file)) {
11999
- const current = await readFile17(file, "utf8").catch(() => "");
12322
+ const current = await readFile18(file, "utf8").catch(() => "");
12000
12323
  if (current.includes(ENFORCE_HOOK_MARKER)) {
12001
12324
  await writeFile31(file, hook.body, "utf8");
12002
12325
  } else {
@@ -12141,7 +12464,7 @@ function registerRun(program2) {
12141
12464
 
12142
12465
  // src/index.ts
12143
12466
  var program = new Command51();
12144
- program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.14");
12467
+ program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.15");
12145
12468
  registerInit(program);
12146
12469
  registerWelcome(program);
12147
12470
  registerResolveProject(program);