@hiveai/cli 0.14.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command59 } from "commander";
4
+ import { Command as Command63 } from "commander";
5
5
 
6
6
  // src/commands/briefing.ts
7
7
  import { existsSync as existsSync3 } from "fs";
@@ -9,9 +9,10 @@ import { mkdir, readFile as readFile2 } from "fs/promises";
9
9
  import path3 from "path";
10
10
  import "commander";
11
11
  import {
12
+ classifyMemoryPriority,
13
+ compactAutoRecapBody,
12
14
  extractActionsBriefBody,
13
15
  findProjectRoot as findProjectRoot2,
14
- isStackPackSeed,
15
16
  literalMatchesAllTokens,
16
17
  literalMatchesAnyToken,
17
18
  loadCodeMap as loadCodeMap3,
@@ -199,7 +200,7 @@ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
199
200
  if (!f) continue;
200
201
  counts.set(f, (counts.get(f) ?? 0) + 1);
201
202
  }
202
- let entries = [...counts.entries()].map(([path56, changes]) => ({ path: path56, changes }));
203
+ let entries = [...counts.entries()].map(([path58, changes]) => ({ path: path58, changes }));
203
204
  const lowerPaths = filePaths.map((p) => p.toLowerCase());
204
205
  if (lowerPaths.length > 0) {
205
206
  entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
@@ -954,7 +955,7 @@ function registerBriefing(program2) {
954
955
  out(`${ui.bold("=== Last Session Recap ===")}
955
956
  `);
956
957
  out(ui.dim(`${fm.id} (${fm.scope}${rev})`));
957
- out(recap.memory.body.trim());
958
+ out(compactAutoRecapBody(recap.memory.body).trim());
958
959
  out("");
959
960
  }
960
961
  if (existsSync3(paths.projectContext) && !stopped()) {
@@ -1124,12 +1125,19 @@ ${ui.bold("=== Symbol Locations ===")}
1124
1125
  function classifyCliPriority(item, filePaths, tokens, exactTaskHit, partialTaskHit) {
1125
1126
  const fm = item.memory.frontmatter;
1126
1127
  const anchored = filePaths.length > 0 && memoryMatchesAnchorPaths(item.memory, filePaths);
1127
- if (anchored || fm.type === "attempt" && exactTaskHit) return "must_read";
1128
- if (isStackPackSeed(fm)) return "background";
1129
- if (exactTaskHit || partialTaskHit || item.score >= 4 || tokens && fm.tags.some((tag) => tokens.includes(tag))) {
1130
- return "useful";
1131
- }
1132
- return "background";
1128
+ return classifyMemoryPriority({
1129
+ type: fm.type,
1130
+ tags: fm.tags,
1131
+ requiresHumanApproval: Boolean(fm.requires_human_approval),
1132
+ directAnchor: anchored,
1133
+ directSymbol: false,
1134
+ // symbol lookup is rendered separately in the CLI, not via anchor priority
1135
+ exactTaskMatch: exactTaskHit,
1136
+ strongSemantic: false,
1137
+ usefulSemantic: partialTaskHit || item.score >= 4,
1138
+ moduleOrDomainMatch: false,
1139
+ tagTaskMatch: Boolean(tokens && fm.tags.some((tag) => tokens.includes(tag)))
1140
+ });
1133
1141
  }
1134
1142
  function priorityBadge(priority) {
1135
1143
  if (priority === "must_read") return ui.red("[must_read]");
@@ -3019,7 +3027,7 @@ ${SEED_FOOTER(stack)}` });
3019
3027
  }
3020
3028
 
3021
3029
  // src/commands/init.ts
3022
- var HAIVE_GITHUB_ACTION_REF = `v${"0.14.0"}`;
3030
+ var HAIVE_GITHUB_ACTION_REF = `v${"0.17.0"}`;
3023
3031
  var PROJECT_CONTEXT_TEMPLATE = `# Project context
3024
3032
 
3025
3033
  > Generated by \`haive init\`. Run \`haive init --bootstrap\` to auto-fill from your codebase,
@@ -3202,7 +3210,11 @@ jobs:
3202
3210
  run: npm install -g @hiveai/cli
3203
3211
 
3204
3212
  - name: harness quality regression gate
3205
- run: haive eval --regression-gate
3213
+ run: haive eval --regression-gate --record --ref "\${{ github.sha }}"
3214
+
3215
+ - name: harness quality trend
3216
+ if: always()
3217
+ run: haive eval --trend || true
3206
3218
 
3207
3219
  # On push to main: push shared memories to the hub (if hubPath is configured)
3208
3220
  # Uncomment and configure hubPath in .ai/haive.config.json to enable.
@@ -3830,14 +3842,21 @@ async function readStdin(maxBytes) {
3830
3842
  setTimeout(finish, 2e3);
3831
3843
  });
3832
3844
  }
3845
+ function isExpectedNonzeroExit(command) {
3846
+ if (!command) return false;
3847
+ if (command.includes("|")) return true;
3848
+ if (/\|\|\s*true\b/.test(command)) return true;
3849
+ return /(^|\s|;|&&)(grep|egrep|fgrep|rg|ag|find|test|diff|\[\[?)\b/.test(command);
3850
+ }
3833
3851
  function detectFailure(payload) {
3834
3852
  const response = payload.tool_response;
3835
3853
  if (!response) return false;
3836
3854
  const responseText = typeof response === "string" ? response : JSON.stringify(response);
3837
3855
  if (payload.tool_name === "Bash") {
3856
+ const command = typeof payload.tool_input?.["command"] === "string" ? payload.tool_input["command"] : "";
3838
3857
  if (typeof response === "object") {
3839
3858
  const code = response["exit_code"] ?? response["exitCode"];
3840
- if (typeof code === "number" && code !== 0) return true;
3859
+ if (typeof code === "number" && code !== 0 && !isExpectedNonzeroExit(command)) return true;
3841
3860
  }
3842
3861
  if (/\b(command not found|No such file or directory|ERR_MODULE_NOT_FOUND|ENOENT|EACCES)\b/.test(responseText)) return true;
3843
3862
  if (/\berror TS\d+:/i.test(responseText)) return true;
@@ -4079,6 +4098,7 @@ import {
4079
4098
  deriveConfidence as deriveConfidence4,
4080
4099
  estimateTokens,
4081
4100
  evaluateSkillActivation,
4101
+ compactAutoRecapBody as compactAutoRecapBody2,
4082
4102
  extractActionsBriefBody as extractActionsBriefBody2,
4083
4103
  getUsage as getUsage6,
4084
4104
  inferModulesFromPaths as inferModulesFromPaths2,
@@ -4110,7 +4130,12 @@ import { z as z19 } from "zod";
4110
4130
  import { readdir as readdir3, readFile as readFile42 } from "fs/promises";
4111
4131
  import { existsSync as existsSync20 } from "fs";
4112
4132
  import path102 from "path";
4113
- import { isGlobPath, isStackPackSeed as isStackPackSeed2, pathsOverlap } from "@hiveai/core";
4133
+ import {
4134
+ classifyMemoryPriority as coreClassifyPriority,
4135
+ isGlobPath,
4136
+ pathsOverlap,
4137
+ priorityRank as corePriorityRank
4138
+ } from "@hiveai/core";
4114
4139
  import { estimateTokens as estimateTokens2, loadCodeMap as loadCodeMap22, queryCodeMap as queryCodeMap22 } from "@hiveai/core";
4115
4140
  import { z as z20 } from "zod";
4116
4141
  import { existsSync as existsSync222 } from "fs";
@@ -5573,7 +5598,7 @@ function compactSummary(body) {
5573
5598
  }
5574
5599
  return body.slice(0, 120);
5575
5600
  }
5576
- function classifyMemoryPriority(memory2, loaded, inputFiles, inputSymbols) {
5601
+ function classifyMemoryPriority2(memory2, loaded, inputFiles, inputSymbols) {
5577
5602
  const fm = loaded?.memory.frontmatter;
5578
5603
  const directAnchor = Boolean(
5579
5604
  fm && inputFiles.length > 0 && fm.anchor.paths.some((p) => inputFiles.some((file) => pathsOverlap(p, file)))
@@ -5583,21 +5608,23 @@ function classifyMemoryPriority(memory2, loaded, inputFiles, inputSymbols) {
5583
5608
  (sym) => inputSymbols.some((wanted) => wanted.toLowerCase() === sym.toLowerCase())
5584
5609
  )
5585
5610
  );
5586
- const strongSemantic = (memory2.semantic_score ?? 0) >= 0.65;
5587
- const usefulSemantic = (memory2.semantic_score ?? 0) >= 0.35;
5588
- if (fm?.requires_human_approval || directAnchor || directSymbol || memory2.type === "attempt" && (memory2.match_quality === "exact" || strongSemantic) || memory2.type === "skill" && (memory2.match_quality === "exact" || strongSemantic)) {
5589
- return "must_read";
5590
- }
5591
- if (isStackPackSeed2(fm)) {
5592
- return "background";
5593
- }
5594
- if (memory2.type === "skill" || memory2.reasons.includes("module") || memory2.reasons.includes("domain") || memory2.match_quality === "exact" || usefulSemantic) {
5595
- return "useful";
5596
- }
5597
- return "background";
5611
+ const semantic = memory2.semantic_score ?? 0;
5612
+ return coreClassifyPriority({
5613
+ type: memory2.type,
5614
+ tags: fm?.tags ?? memory2.tags ?? [],
5615
+ requiresHumanApproval: Boolean(fm?.requires_human_approval),
5616
+ directAnchor,
5617
+ directSymbol,
5618
+ exactTaskMatch: memory2.match_quality === "exact",
5619
+ strongSemantic: semantic >= 0.65,
5620
+ usefulSemantic: semantic >= 0.35,
5621
+ moduleOrDomainMatch: memory2.reasons.includes("module") || memory2.reasons.includes("domain"),
5622
+ tagTaskMatch: false
5623
+ // MCP ranking doesn't use a separate tag-token signal
5624
+ });
5598
5625
  }
5599
5626
  function priorityRank(priority) {
5600
- return priority === "must_read" ? 3 : priority === "useful" ? 2 : 1;
5627
+ return corePriorityRank(priority);
5601
5628
  }
5602
5629
  function classifyBriefingQuality(memories, context) {
5603
5630
  const mustRead = memories.filter((m) => m.priority === "must_read").length;
@@ -5764,7 +5791,9 @@ async function getBriefing(input, ctx) {
5764
5791
  id: fm.id,
5765
5792
  scope: fm.scope,
5766
5793
  revision_count: fm.revision_count ?? 0,
5767
- body: r.memory.body
5794
+ // Auto-generated recaps are low-signal tool dumps — compact them so they inform without
5795
+ // dominating the briefing head. Human/post_task recaps pass through unchanged.
5796
+ body: compactAutoRecapBody2(r.memory.body)
5768
5797
  };
5769
5798
  }
5770
5799
  const allMemories = allLoaded.filter(({ memory: memory2 }) => {
@@ -5886,8 +5915,8 @@ async function getBriefing(input, ctx) {
5886
5915
  const impactScore = (m) => (m.impact_score ?? 0) * 3;
5887
5916
  const activationBoost = (m) => activatedSkills.has(m.id) ? 5 : 0;
5888
5917
  const lexScore = (m) => 12 * (lexNorm.get(m.id) ?? 0);
5889
- const sa = priorityRank(classifyMemoryPriority(a, byId.get(a.id), input.files, input.symbols)) * 100 + reasonScore(a) + confidenceScore(a) + impactScore(a) + activationBoost(a) + lexScore(a) + (a.semantic_score ?? 0);
5890
- const sb = priorityRank(classifyMemoryPriority(b, byId.get(b.id), input.files, input.symbols)) * 100 + reasonScore(b) + confidenceScore(b) + impactScore(b) + activationBoost(b) + lexScore(b) + (b.semantic_score ?? 0);
5918
+ const sa = priorityRank(classifyMemoryPriority2(a, byId.get(a.id), input.files, input.symbols)) * 100 + reasonScore(a) + confidenceScore(a) + impactScore(a) + activationBoost(a) + lexScore(a) + (a.semantic_score ?? 0);
5919
+ const sb = priorityRank(classifyMemoryPriority2(b, byId.get(b.id), input.files, input.symbols)) * 100 + reasonScore(b) + confidenceScore(b) + impactScore(b) + activationBoost(b) + lexScore(b) + (b.semantic_score ?? 0);
5891
5920
  return sb - sa;
5892
5921
  });
5893
5922
  for (const mem of ranked.slice(0, briefingMaxMemories)) {
@@ -6043,7 +6072,7 @@ ${m.content}`).join("\n\n---\n\n"),
6043
6072
  const formattedMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : input.format === "actions" ? trimmedMemories.map((m) => ({ ...m, body: extractActionsBriefBody2(m.body) })) : trimmedMemories;
6044
6073
  const outputMemories = formattedMemories.map((m) => ({
6045
6074
  ...m,
6046
- priority: classifyMemoryPriority(m, byId.get(m.id), input.files, input.symbols),
6075
+ priority: classifyMemoryPriority2(m, byId.get(m.id), input.files, input.symbols),
6047
6076
  why: explainWhySurfaced(m, byId.get(m.id), input.files, inferred)
6048
6077
  }));
6049
6078
  const briefingQuality = classifyBriefingQuality(outputMemories, {
@@ -6580,6 +6609,27 @@ function tokenizeDiffForLiteral(diff) {
6580
6609
  const wordTokens = source.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 4 && !CODE_STOPWORDS.has(t));
6581
6610
  return [.../* @__PURE__ */ new Set([...wsTokens, ...wordTokens])];
6582
6611
  }
6612
+ function stripAiDirHunks(diff) {
6613
+ if (!diff.includes("diff --git")) return diff;
6614
+ const out = [];
6615
+ let block = [];
6616
+ let keep = true;
6617
+ const flush = () => {
6618
+ if (keep) out.push(...block);
6619
+ block = [];
6620
+ keep = true;
6621
+ };
6622
+ for (const line of diff.split("\n")) {
6623
+ if (line.startsWith("diff --git ")) {
6624
+ flush();
6625
+ const target = line.match(/ b\/(.+)$/)?.[1] ?? "";
6626
+ keep = !target.startsWith(".ai/");
6627
+ }
6628
+ block.push(line);
6629
+ }
6630
+ flush();
6631
+ return out.join("\n");
6632
+ }
6583
6633
  async function antiPatternsCheck(input, ctx) {
6584
6634
  if (!input.diff && input.paths.length === 0) {
6585
6635
  return {
@@ -6635,10 +6685,11 @@ async function antiPatternsCheck(input, ctx) {
6635
6685
  }
6636
6686
  }
6637
6687
  }
6638
- if (input.diff) {
6639
- const tokens = tokenizeDiffForLiteral(input.diff);
6640
- const added = addedLinesFromDiff(input.diff);
6641
- const addedText = added.trim().length > 0 ? added : input.diff;
6688
+ const scanDiff = input.diff ? stripAiDirHunks(input.diff) : input.diff;
6689
+ if (scanDiff) {
6690
+ const tokens = tokenizeDiffForLiteral(scanDiff);
6691
+ const added = addedLinesFromDiff(scanDiff);
6692
+ const addedText = added.trim().length > 0 ? added : scanDiff;
6642
6693
  if (tokens.length > 0) {
6643
6694
  for (const { memory: memory2 } of negative) {
6644
6695
  if (literalMatchesAnyToken3(memory2, tokens)) {
@@ -6668,10 +6719,10 @@ async function antiPatternsCheck(input, ctx) {
6668
6719
  }
6669
6720
  }
6670
6721
  }
6671
- if (input.semantic && input.diff) {
6722
+ if (input.semantic && scanDiff) {
6672
6723
  try {
6673
6724
  const mod = await import("@hiveai/embeddings");
6674
- const result = await mod.semanticSearch(ctx.paths, input.diff, { limit: input.limit * 2 });
6725
+ const result = await mod.semanticSearch(ctx.paths, scanDiff, { limit: input.limit * 2 });
6675
6726
  if (result) {
6676
6727
  const negativeIds = new Set(negative.map(({ memory: memory2 }) => memory2.frontmatter.id));
6677
6728
  for (const hit of result.hits) {
@@ -7958,7 +8009,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
7958
8009
  };
7959
8010
  }
7960
8011
  var SERVER_NAME = "haive";
7961
- var SERVER_VERSION = "0.14.0";
8012
+ var SERVER_VERSION = "0.17.0";
7962
8013
  function jsonResult(data) {
7963
8014
  return {
7964
8015
  content: [
@@ -9001,7 +9052,7 @@ import {
9001
9052
  getUsage as getUsage11,
9002
9053
  isAutoPromoteEligible as isAutoPromoteEligible2,
9003
9054
  isDecaying as isDecaying2,
9004
- isStackPackSeed as isStackPackSeed3,
9055
+ isStackPackSeed,
9005
9056
  loadCodeMap as loadCodeMap6,
9006
9057
  loadConfig as loadConfig4,
9007
9058
  loadMemoriesFromDir as loadMemoriesFromDir25,
@@ -9466,7 +9517,7 @@ async function injectBridge(bridgeFile, memoriesDir, maxMemories, root, quiet) {
9466
9517
  const top = all.filter(({ memory: memory2 }) => {
9467
9518
  const s = memory2.frontmatter.status;
9468
9519
  if (memory2.frontmatter.type === "session_recap") return false;
9469
- if (isStackPackSeed3(memory2.frontmatter)) return false;
9520
+ if (isStackPackSeed(memory2.frontmatter)) return false;
9470
9521
  return s === "validated" || s === "proposed";
9471
9522
  }).sort((a, b) => {
9472
9523
  const score = (m) => {
@@ -12709,9 +12760,12 @@ import "commander";
12709
12760
  import {
12710
12761
  aggregateRetrieval,
12711
12762
  aggregateSensors,
12763
+ appendEvalHistory,
12712
12764
  buildReport,
12713
12765
  compareEvalReports,
12766
+ computeEvalTrend,
12714
12767
  findProjectRoot as findProjectRoot42,
12768
+ loadEvalHistory,
12715
12769
  resolveHaivePaths as resolveHaivePaths38,
12716
12770
  scoreRetrievalCase,
12717
12771
  scoreSensorCase,
@@ -12720,7 +12774,7 @@ import {
12720
12774
  function registerEval(program2) {
12721
12775
  program2.command("eval").description(
12722
12776
  "Rigorous, repeatable quality eval: do the right memories surface (retrieval) and do the right sensors fire (catch-rate)? Emits a numeric 0\u2013100 score. Uses .ai/eval cases via --spec, or auto-synthesizes cases from anchored memories."
12723
- ).option("--spec <file>", "JSON eval spec ({ retrieval: [...], sensors: [...] })").option("--semantic-only", "self-eval probes by title alone (no anchor files) \u2014 harder retrieval", false).option("-k, --top <n>", "briefing top-k considered a hit", "8").option("--json", "emit JSON", false).option("--out <file>", "write a Markdown report").option("--fail-under <score>", "exit non-zero if the overall score is below this (0\u2013100) \u2014 for CI gates").option("--baseline", "save this run as the baseline (.ai/eval/baseline.json) for future --compare", false).option("--compare", "diff this run against the saved baseline and print the delta", false).option("--baseline-file <path>", "baseline file to read/write (default: .ai/eval/baseline.json)").option("--fail-on-regression", "with --compare, exit non-zero if the score dropped vs the baseline", false).option("--regression-gate", "CI-safe gate: compare against the baseline IF one exists (fail on regression), else no-op", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
12777
+ ).option("--spec <file>", "JSON eval spec ({ retrieval: [...], sensors: [...] })").option("--semantic-only", "self-eval probes by title alone (no anchor files) \u2014 harder retrieval", false).option("-k, --top <n>", "briefing top-k considered a hit", "8").option("--json", "emit JSON", false).option("--out <file>", "write a Markdown report").option("--fail-under <score>", "exit non-zero if the overall score is below this (0\u2013100) \u2014 for CI gates").option("--baseline", "save this run as the baseline (.ai/eval/baseline.json) for future --compare", false).option("--compare", "diff this run against the saved baseline and print the delta", false).option("--baseline-file <path>", "baseline file to read/write (default: .ai/eval/baseline.json)").option("--fail-on-regression", "with --compare, exit non-zero if the score dropped vs the baseline", false).option("--regression-gate", "CI-safe gate: compare against the baseline IF one exists (fail on regression), else no-op", false).option("--record", "append this run's score to .ai/.cache/eval-history.jsonl (trend the harness over time)", false).option("--trend", "print the recorded score trend (sparkline + latest/best/delta) and exit", false).option("--ref <ref>", "version/commit label stored with a --record run").option("-d, --dir <dir>", "project root").action(async (opts) => {
12724
12778
  const root = findProjectRoot42(opts.dir);
12725
12779
  const paths = resolveHaivePaths38(root);
12726
12780
  if (!existsSync65(paths.memoriesDir)) {
@@ -12728,6 +12782,22 @@ function registerEval(program2) {
12728
12782
  process.exitCode = 1;
12729
12783
  return;
12730
12784
  }
12785
+ if (opts.trend) {
12786
+ const trend = computeEvalTrend(await loadEvalHistory(paths));
12787
+ if (opts.json) {
12788
+ console.log(JSON.stringify(trend, null, 2));
12789
+ return;
12790
+ }
12791
+ if (trend.runs === 0) {
12792
+ ui.info("No eval history yet. Run `haive eval --record` to start trending the harness.");
12793
+ return;
12794
+ }
12795
+ const spark = trend.recent.map((s) => "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588"[Math.min(7, Math.round(s / 100 * 7))]).join("");
12796
+ const arrow = trend.regressed ? ui.red("\u25BC") : (trend.delta ?? 0) > 0 ? ui.green("\u25B2") : ui.dim("=");
12797
+ console.log(ui.bold("hAIve eval trend"));
12798
+ console.log(` ${spark} latest ${arrow} ${trend.latest}/100 ${ui.dim(`(best ${trend.best}, ${trend.runs} run${trend.runs === 1 ? "" : "s"})`)}`);
12799
+ return;
12800
+ }
12731
12801
  const k = Math.max(1, parseInt(opts.top ?? "8", 10) || 8);
12732
12802
  const ctx = { paths };
12733
12803
  const resolvedSpec = await resolveSpec(opts, root, paths.memoriesDir);
@@ -12755,6 +12825,17 @@ function registerEval(program2) {
12755
12825
  sensorAgg = aggregateSensors(results);
12756
12826
  }
12757
12827
  const report = buildReport(retrievalAgg, sensorAgg);
12828
+ if (opts.record) {
12829
+ await appendEvalHistory(paths, {
12830
+ at: (/* @__PURE__ */ new Date()).toISOString(),
12831
+ score: report.score,
12832
+ ...report.retrieval ? { mean_recall: report.retrieval.mean_recall, mrr: report.retrieval.mrr } : {},
12833
+ ...report.sensors ? { catch_rate: report.sensors.catch_rate } : {},
12834
+ ...opts.ref ? { ref: opts.ref } : {}
12835
+ }).catch(() => {
12836
+ });
12837
+ if (!opts.json) ui.success(`Recorded eval score ${report.score}/100 to history.`);
12838
+ }
12758
12839
  const baselineFile = opts.baselineFile ? path43.isAbsolute(opts.baselineFile) ? opts.baselineFile : path43.join(root, opts.baselineFile) : path43.join(root, ".ai", "eval", "baseline.json");
12759
12840
  if (opts.baseline) {
12760
12841
  const snapshot = {
@@ -13283,7 +13364,7 @@ import {
13283
13364
  codeMapPath as codeMapPath2,
13284
13365
  findProjectRoot as findProjectRoot45,
13285
13366
  getUsage as getUsage23,
13286
- isStackPackSeed as isStackPackSeed4,
13367
+ isStackPackSeed as isStackPackSeed2,
13287
13368
  loadCodeMap as loadCodeMap7,
13288
13369
  loadConfig as loadConfig11,
13289
13370
  loadMemoriesFromDir as loadMemoriesFromDir35,
@@ -13395,11 +13476,11 @@ function registerDoctor(program2) {
13395
13476
  fix: "haive memory approve <id> # activate\nhaive memory delete <id> # or delete if obsolete"
13396
13477
  });
13397
13478
  }
13398
- const policyMemories = memories.filter((m) => !isStackPackSeed4(m.memory.frontmatter));
13479
+ const policyMemories = memories.filter((m) => !isStackPackSeed2(m.memory.frontmatter));
13399
13480
  const anchorless = policyMemories.filter(
13400
13481
  (m) => m.memory.frontmatter.anchor.paths.length === 0 && m.memory.frontmatter.anchor.symbols.length === 0 && m.memory.frontmatter.type !== "session_recap" && m.memory.frontmatter.type !== "glossary" && m.memory.frontmatter.type !== "skill"
13401
13482
  );
13402
- const stackSeeds = memories.filter((m) => isStackPackSeed4(m.memory.frontmatter));
13483
+ const stackSeeds = memories.filter((m) => isStackPackSeed2(m.memory.frontmatter));
13403
13484
  if (anchorless.length / Math.max(policyMemories.length, 1) > 0.3) {
13404
13485
  findings.push({
13405
13486
  severity: "warn",
@@ -13529,7 +13610,7 @@ function registerDoctor(program2) {
13529
13610
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
13530
13611
  });
13531
13612
  }
13532
- findings.push(...await collectInstallFindings(root, "0.14.0"));
13613
+ findings.push(...await collectInstallFindings(root, "0.17.0"));
13533
13614
  findings.push(...await collectToolchainFindings(root));
13534
13615
  try {
13535
13616
  const legacyRaw = execSync3("haive-mcp --version", {
@@ -13537,7 +13618,7 @@ function registerDoctor(program2) {
13537
13618
  timeout: 3e3,
13538
13619
  stdio: ["ignore", "pipe", "ignore"]
13539
13620
  }).trim();
13540
- const cliVersion = "0.14.0";
13621
+ const cliVersion = "0.17.0";
13541
13622
  if (legacyRaw && legacyRaw !== cliVersion) {
13542
13623
  findings.push({
13543
13624
  severity: "warn",
@@ -14599,6 +14680,7 @@ import "commander";
14599
14680
  import {
14600
14681
  antiPatternGateParams as antiPatternGateParams2,
14601
14682
  findProjectRoot as findProjectRoot52,
14683
+ findUncapturedFailures,
14602
14684
  hasRecentBriefingMarker as hasRecentBriefingMarker2,
14603
14685
  isFreshIsoDate,
14604
14686
  loadConfig as loadConfig13,
@@ -14860,6 +14942,7 @@ async function buildFinishReport(dir) {
14860
14942
  }]
14861
14943
  });
14862
14944
  }
14945
+ findings.push(...await checkFailureCapture(paths, config));
14863
14946
  const status = await getGitSyncStatus(root);
14864
14947
  if (!status.available) {
14865
14948
  findings.push({
@@ -14877,7 +14960,7 @@ async function buildFinishReport(dir) {
14877
14960
  severity: "error",
14878
14961
  code: shippableDirty.length > 0 ? "git-sync-uncommitted-shippable" : "git-sync-uncommitted-changes",
14879
14962
  message: shippableDirty.length > 0 ? `${shippableDirty.length} shippable file(s) are modified but not committed.` : `${status.dirtyFiles.length} file(s) are modified but not committed.`,
14880
- fix: shippableDirty.length > 0 ? "Bump the lockstep package version if needed, then `git add`, `git commit`, `git tag vX.Y.Z`, `git push && git push --tags`." : "Commit and push these changes before reporting the task done.",
14963
+ fix: shippableDirty.length > 0 ? "Bump the lockstep package version if needed, then `git add`, `git commit`, `git tag vX.Y.Z`, `git push && git push origin vX.Y.Z` (not `--tags`)." : "Commit and push these changes before reporting the task done.",
14881
14964
  reason: "The multi-agent git-sync decision requires agents to leave completed work committed and pushed, not as a local diff.",
14882
14965
  affected_files: status.dirtyFiles.slice(0, 12),
14883
14966
  impact: 100
@@ -15002,7 +15085,7 @@ async function buildFinishReport(dir) {
15002
15085
  severity: "error",
15003
15086
  code: "release-tag-unpushed",
15004
15087
  message: `Tag ${tag} is not present on the remote.`,
15005
- fix: "Run `git push --tags`.",
15088
+ fix: `Run \`git push origin ${tag}\` (avoid \`git push --tags\` \u2014 it fails on pre-existing divergent tags).`,
15006
15089
  impact: 50
15007
15090
  });
15008
15091
  } else if (remoteTag === true) {
@@ -15016,13 +15099,54 @@ async function buildFinishReport(dir) {
15016
15099
  severity: "warn",
15017
15100
  code: "release-tag-remote-unverified",
15018
15101
  message: `Could not verify whether tag ${tag} exists on the remote.`,
15019
- fix: "Run `git push --tags` if you have not already.",
15102
+ fix: `Run \`git push origin ${tag}\` if you have not already (avoid \`git push --tags\`).`,
15020
15103
  impact: 10
15021
15104
  });
15022
15105
  }
15023
15106
  findings.push(...await verifyGithubActionsForHead(root, status));
15024
15107
  return finishReport(root, initialized, mode, findings, config);
15025
15108
  }
15109
+ async function checkFailureCapture(paths, config) {
15110
+ const gate = config.enforcement?.failureCaptureGate ?? "warn";
15111
+ if (gate === "off") return [];
15112
+ const obsFile = path51.join(paths.haiveDir, ".cache", "observations.jsonl");
15113
+ if (!existsSync75(obsFile)) return [];
15114
+ const failures = [];
15115
+ try {
15116
+ const raw = await readFile23(obsFile, "utf8");
15117
+ for (const line of raw.split("\n")) {
15118
+ const trimmed = line.trim();
15119
+ if (!trimmed) continue;
15120
+ try {
15121
+ const o = JSON.parse(trimmed);
15122
+ if (o.failure_hint && o.ts) failures.push({ ts: o.ts, tool: o.tool ?? "?", summary: o.summary ?? "" });
15123
+ } catch {
15124
+ }
15125
+ }
15126
+ } catch {
15127
+ return [];
15128
+ }
15129
+ if (failures.length === 0) return [];
15130
+ const memories = existsSync75(paths.memoriesDir) ? await loadMemoriesFromDir38(paths.memoriesDir) : [];
15131
+ const captureTimes = memories.filter(({ memory: memory2 }) => ["attempt", "gotcha"].includes(memory2.frontmatter.type)).map(({ memory: memory2 }) => memory2.frontmatter.created_at);
15132
+ const uncaptured = findUncapturedFailures(failures, captureTimes);
15133
+ if (uncaptured.length === 0) {
15134
+ return [{
15135
+ severity: "ok",
15136
+ code: "failure-capture-clean",
15137
+ message: "No uncaptured hard failures from this session."
15138
+ }];
15139
+ }
15140
+ return [{
15141
+ severity: gate === "block" ? "error" : "info",
15142
+ code: "uncaptured-failures",
15143
+ message: `${uncaptured.length} hard failure(s) this session were never captured as a lesson (mem_tried).`,
15144
+ fix: "Call `mem_tried` (or `haive memory tried`) for each real failure so the next session doesn't repeat it. False positives (e.g. a grep that found nothing) can be ignored.",
15145
+ reason: "Harness ratchet: a mistake that isn't written down gets re-introduced. Set enforcement.failureCaptureGate to 'off' to disable, or 'block' to hard-fail.",
15146
+ affected_files: uncaptured.slice(0, 8).map((f) => `${f.tool}: ${f.summary}`.slice(0, 100)),
15147
+ ...gate === "block" ? { impact: 30 } : {}
15148
+ }];
15149
+ }
15026
15150
  function finishReport(root, initialized, mode, findings, config) {
15027
15151
  const score = buildScore(findings, config.enforcement?.scoreThreshold);
15028
15152
  const hasErrors = findings.some((f) => f.severity === "error");
@@ -15167,7 +15291,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
15167
15291
  findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
15168
15292
  });
15169
15293
  }
15170
- findings.push(...await inspectIntegrationVersions(root, "0.14.0"));
15294
+ findings.push(...await inspectIntegrationVersions(root, "0.17.0"));
15171
15295
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
15172
15296
  const hasBriefing = await hasRecentBriefingMarker2(paths, sessionId);
15173
15297
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
@@ -15327,7 +15451,7 @@ async function verifyDecisionCoverage(paths, stage, sessionId) {
15327
15451
  severity: stage === "local" ? "warn" : "error",
15328
15452
  code: "decision-coverage-missing",
15329
15453
  message: `${missing.length}/${relevant.length} relevant anchored decisions/policies were not present in the latest briefing: ${missing.slice(0, 6).map((m) => m.frontmatter.id).join(", ")}`,
15330
- fix: `Run \`haive briefing --files "${changedFiles.slice(0, 10).join(",")}" --task "..."\` before committing.`,
15454
+ fix: `Run \`haive briefing --files "${changedFiles.slice(0, 12).join(",")}" --max-memories 60 --task "..."\` before committing (briefings now accumulate, so several smaller briefings also work).`,
15331
15455
  reason: "Changed files overlap validated anchored policy memories that were not recorded in the latest briefing marker.",
15332
15456
  affected_files: changedFiles.slice(0, 10),
15333
15457
  memory_ids: missing.slice(0, 10).map((m) => m.frontmatter.id),
@@ -16163,12 +16287,14 @@ import {
16163
16287
  appendPreventionEvent as appendPreventionEvent2,
16164
16288
  findProjectRoot as findProjectRoot53,
16165
16289
  isRetiredMemory as isRetiredMemory3,
16290
+ loadConfig as loadConfig14,
16166
16291
  loadMemoriesFromDir as loadMemoriesFromDir39,
16167
16292
  loadUsageIndex as loadUsageIndex29,
16168
16293
  recordPrevention as recordPrevention2,
16169
16294
  resolveHaivePaths as resolveHaivePaths49,
16170
16295
  runSensors as runSensors2,
16171
16296
  saveUsageIndex as saveUsageIndex8,
16297
+ selectCommandSensors,
16172
16298
  sensorTargetsFromDiff as sensorTargetsFromDiff2,
16173
16299
  serializeMemory as serializeMemory27
16174
16300
  } from "@hiveai/core";
@@ -16198,14 +16324,36 @@ function registerSensors(program2) {
16198
16324
  });
16199
16325
  sensors.command("check").description(
16200
16326
  "Run regex sensors against a diff (the deterministic/computational layer); defaults to `git diff --cached`.\n Diff-scan layers: `sensors check` (regex) and `anti_patterns_check` (memory match) are components;\n `pre_commit_check` combines them; `haive enforce check` is THE gate that runs at commit."
16201
- ).option("--diff-file <path>", "read unified diff from a file instead of staged changes").option("--json", "emit JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
16327
+ ).option("--diff-file <path>", "read unified diff from a file instead of staged changes").option("--json", "emit JSON", false).option("--commands", "ALSO execute shell/test sensors (runs repo-authored commands)", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
16202
16328
  const root = findProjectRoot53(opts.dir);
16203
16329
  const paths = resolveHaivePaths49(root);
16204
16330
  const memories = await runnableSensorMemories(paths);
16205
16331
  const diff = opts.diffFile ? await readFile24(path53.resolve(root, opts.diffFile), "utf8") : await stagedDiff(root);
16206
16332
  const targets = sensorTargetsFromDiff2(diff);
16207
16333
  const hits = runSensors2(memories, targets.length > 0 ? targets : [{ path: "", content: diff }]);
16208
- const firedIds = [...new Set(hits.map((hit) => hit.memory_id))];
16334
+ const config = await loadConfig14(paths);
16335
+ const runCommands = opts.commands || config.enforcement?.runCommandSensors === true;
16336
+ const changedPaths = targets.map((t) => t.path).filter(Boolean);
16337
+ const allSensorMemories = await runnableSensorMemories(paths, false);
16338
+ const commandSpecs = selectCommandSensors(allSensorMemories, changedPaths);
16339
+ const commandHits = [];
16340
+ const commandSkipped = [];
16341
+ if (commandSpecs.length > 0 && runCommands) {
16342
+ for (const spec of commandSpecs) {
16343
+ const failed = await runCommandSensor(spec, root);
16344
+ if (failed) {
16345
+ commandHits.push({
16346
+ memory_id: spec.memory_id,
16347
+ severity: spec.severity,
16348
+ message: spec.message,
16349
+ matched_line: `command failed: ${spec.command}`
16350
+ });
16351
+ }
16352
+ }
16353
+ } else if (commandSpecs.length > 0) {
16354
+ for (const spec of commandSpecs) commandSkipped.push(spec.memory_id);
16355
+ }
16356
+ const firedIds = [...new Set([...hits, ...commandHits].map((hit) => hit.memory_id))];
16209
16357
  if (firedIds.length > 0) {
16210
16358
  const usage = await loadUsageIndex29(paths);
16211
16359
  const recordedIds = [];
@@ -16228,12 +16376,15 @@ function registerSensors(program2) {
16228
16376
  severity: hit.severity,
16229
16377
  message: hit.message,
16230
16378
  matched_line: hit.matched_line
16231
- }))
16379
+ })),
16380
+ command_hits: commandHits,
16381
+ command_skipped: commandSkipped
16232
16382
  };
16233
16383
  if (opts.json) {
16234
16384
  console.log(JSON.stringify(output, null, 2));
16235
16385
  } else {
16236
- console.log(ui.bold(`hAIve sensors check \u2014 ${hits.length} hit(s), ${memories.length} sensor(s)`));
16386
+ const total = hits.length + commandHits.length;
16387
+ console.log(ui.bold(`hAIve sensors check \u2014 ${total} hit(s), ${memories.length} regex + ${commandSpecs.length} command sensor(s)`));
16237
16388
  for (const hit of hits) {
16238
16389
  const marker = hit.severity === "block" ? ui.red("\u2717") : ui.yellow("\u26A0");
16239
16390
  console.log(` ${marker} ${hit.memory_id} ${ui.dim(`(${hit.severity})`)}`);
@@ -16241,8 +16392,17 @@ function registerSensors(program2) {
16241
16392
  console.log(` ${hit.message}`);
16242
16393
  if (hit.matched_line) console.log(` ${ui.dim(hit.matched_line)}`);
16243
16394
  }
16395
+ for (const hit of commandHits) {
16396
+ const marker = hit.severity === "block" ? ui.red("\u2717") : ui.yellow("\u26A0");
16397
+ console.log(` ${marker} ${hit.memory_id} ${ui.dim(`(${hit.severity}, command)`)}`);
16398
+ console.log(` ${hit.message}`);
16399
+ console.log(` ${ui.dim(hit.matched_line)}`);
16400
+ }
16401
+ if (commandSkipped.length > 0) {
16402
+ console.log(ui.dim(` ${commandSkipped.length} command sensor(s) not run \u2014 pass --commands or set enforcement.runCommandSensors.`));
16403
+ }
16244
16404
  }
16245
- if (hits.some((hit) => hit.severity === "block")) process.exitCode = 1;
16405
+ if ([...hits, ...commandHits].some((hit) => hit.severity === "block")) process.exitCode = 1;
16246
16406
  });
16247
16407
  sensors.command("promote").description("Promote or demote an existing memory sensor severity").argument("<memory-id>", "memory id carrying the sensor").option("--severity <severity>", "block | warn", "block").option("--yes", "confirm promotion to block severity", false).option("-d, --dir <dir>", "project root").action(async (id, opts) => {
16248
16408
  const severity = opts.severity ?? "block";
@@ -16329,6 +16489,14 @@ async function runnableSensorMemories(paths, regexOnly = true) {
16329
16489
  return !isRetiredMemory3(memory2.frontmatter, memory2.body);
16330
16490
  });
16331
16491
  }
16492
+ async function runCommandSensor(spec, root) {
16493
+ try {
16494
+ await exec2("bash", ["-c", spec.command], { cwd: root, timeout: 12e4, maxBuffer: 8 * 1024 * 1024 });
16495
+ return false;
16496
+ } catch {
16497
+ return true;
16498
+ }
16499
+ }
16332
16500
  async function stagedDiff(root) {
16333
16501
  try {
16334
16502
  const { stdout } = await exec2("git", ["diff", "--cached"], { cwd: root });
@@ -16562,6 +16730,7 @@ import "commander";
16562
16730
  import {
16563
16731
  buildDashboard,
16564
16732
  findProjectRoot as findProjectRoot55,
16733
+ loadConfig as loadConfig15,
16565
16734
  loadMemoriesFromDir as loadMemoriesFromDir41,
16566
16735
  loadPreventionEvents,
16567
16736
  loadUsageIndex as loadUsageIndex30,
@@ -16581,11 +16750,13 @@ function registerDashboard(program2) {
16581
16750
  const memories = existsSync78(paths.memoriesDir) ? await loadMemoriesFromDir41(paths.memoriesDir) : [];
16582
16751
  const usage = await loadUsageIndex30(paths);
16583
16752
  const preventionEvents = await loadPreventionEvents(paths);
16753
+ const config = await loadConfig15(paths);
16584
16754
  const top = Math.max(1, Number.parseInt(opts.top ?? "10", 10) || 10);
16585
16755
  const dormantDays = opts.dormantDays ? Number.parseInt(opts.dormantDays, 10) : void 0;
16586
16756
  const report = buildDashboard(memories, usage, {
16587
16757
  top,
16588
16758
  preventionEvents,
16759
+ antiPatternGate: config.enforcement?.antiPatternGate ?? "anchored",
16589
16760
  ...dormantDays !== void 0 && Number.isFinite(dormantDays) ? { dormantDays } : {}
16590
16761
  });
16591
16762
  if (opts.json) {
@@ -16596,7 +16767,7 @@ function registerDashboard(program2) {
16596
16767
  });
16597
16768
  }
16598
16769
  function renderDashboard(r) {
16599
- const { inventory: inv, impact, sensors, health, decay, corpus, prevention } = r;
16770
+ const { inventory: inv, impact, sensors, health, decay, corpus, prevention, gate_precision: gate } = r;
16600
16771
  console.log(ui.bold("hAIve dashboard"));
16601
16772
  console.log(
16602
16773
  ` ${ui.dim("corpus:")} ${inv.total} policy memor${inv.total === 1 ? "y" : "ies"} (${inv.active} active, ${inv.retired} retired) \xB7 ${inv.session_recaps} recap(s) \xB7 ~${corpus.est_tokens.toLocaleString()} tokens`
@@ -16647,6 +16818,15 @@ function renderDashboard(r) {
16647
16818
  }
16648
16819
  }
16649
16820
  console.log();
16821
+ console.log(ui.bold("Gate precision") + ui.dim(" (is the anti-pattern gate real or noisy?)"));
16822
+ const precisionLabel = gate.precision === null ? ui.dim("no signal yet") : gate.precision >= 0.7 ? ui.green(`${Math.round(gate.precision * 100)}%`) : ui.yellow(`${Math.round(gate.precision * 100)}%`);
16823
+ console.log(
16824
+ ` ${precisionLabel} precision \xB7 ${gate.useful} useful (sensor ${gate.sensor_catches} \xB7 anti-pattern ${gate.anti_pattern_catches}) \xB7 ${gate.rejections > 0 ? ui.yellow(`${gate.rejections} rejected`) : "0 rejected"}`
16825
+ );
16826
+ if (gate.suggestion) {
16827
+ ui.info(`Tuning: set enforcement.antiPatternGate="${gate.suggestion.recommended}" \u2014 ${gate.suggestion.reason}`);
16828
+ }
16829
+ console.log();
16650
16830
  console.log(ui.bold("Health"));
16651
16831
  console.log(
16652
16832
  ` stale ${warnNum(health.stale)} \xB7 anchorless ${warnNum(health.anchorless)} \xB7 pending ${health.pending} \xB7 prune candidates ${warnNum(health.prune_candidates)}`
@@ -16744,9 +16924,268 @@ function registerDevLink(program2) {
16744
16924
  });
16745
16925
  }
16746
16926
 
16927
+ // src/commands/coverage.ts
16928
+ import "commander";
16929
+ import { findCoverageGaps, findProjectRoot as findProjectRoot57, resolveHaivePaths as resolveHaivePaths52 } from "@hiveai/core";
16930
+ function isNoisePath(p) {
16931
+ if (/(^|\/)(node_modules|dist|build|coverage|\.next)\//.test(p)) return true;
16932
+ if (p.startsWith(".ai/")) return true;
16933
+ if (/\.(jsonl|lock|map|snap|min\.js)$/.test(p)) return true;
16934
+ if (/(^|\/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$/.test(p)) return true;
16935
+ if (/(^|\/)(CHANGELOG|LICENSE)(\.md)?$/.test(p)) return true;
16936
+ return false;
16937
+ }
16938
+ function registerCoverage(program2) {
16939
+ program2.command("coverage").description(
16940
+ "Coverage-gap report: frequently-edited files with no covering team memory (blind spots)."
16941
+ ).option("--json", "emit JSON", false).option("--min-changes <n>", "minimum git-churn count to flag a file", "3").option("--limit <n>", "max gaps to report", "20").option("--days <n>", "git-history lookback window in days", "90").option("-d, --dir <dir>", "project root").action(async (opts) => {
16942
+ const root = findProjectRoot57(opts.dir);
16943
+ const paths = resolveHaivePaths52(root);
16944
+ const minChanges = Math.max(1, parseInt(opts.minChanges ?? "3", 10) || 3);
16945
+ const limit = Math.max(1, parseInt(opts.limit ?? "20", 10) || 20);
16946
+ const days = Math.max(1, parseInt(opts.days ?? "90", 10) || 90);
16947
+ const radar = await buildRadar({
16948
+ root,
16949
+ taskTokens: null,
16950
+ filePaths: [],
16951
+ daysBack: Math.ceil(days / 6),
16952
+ // getHotFiles multiplies daysBack by 6
16953
+ maxHotFiles: 500
16954
+ });
16955
+ const hotFiles = radar.hotFiles.filter((h) => !isNoisePath(h.path)).map((h) => ({ path: h.path, changes: h.changes }));
16956
+ const memories = await loadMemoriesFromDir27(paths.memoriesDir);
16957
+ const gaps = findCoverageGaps(hotFiles, memories, { minChanges, limit });
16958
+ if (opts.json) {
16959
+ console.log(JSON.stringify({ root, scanned_hot_files: hotFiles.length, gaps }, null, 2));
16960
+ return;
16961
+ }
16962
+ if (!radar.insideGitRepo) {
16963
+ ui.warn("Not a git repository \u2014 coverage uses git churn to find hot files.");
16964
+ return;
16965
+ }
16966
+ if (gaps.length === 0) {
16967
+ ui.success(`No coverage gaps: every file changed \u2265${minChanges}\xD7 is covered by a team memory.`);
16968
+ return;
16969
+ }
16970
+ console.log(ui.bold(`hAIve coverage \u2014 ${gaps.length} blind spot(s) (hot files with no covering memory)`));
16971
+ for (const gap of gaps) {
16972
+ console.log(` ${ui.yellow("\u25CB")} ${gap.path} ${ui.dim(`(${gap.changes} change${gap.changes === 1 ? "" : "s"})`)}`);
16973
+ }
16974
+ console.log(
16975
+ ui.dim(
16976
+ "\nAdd a decision/convention/gotcha anchored to the top files, or a sensor, to close the gap."
16977
+ )
16978
+ );
16979
+ });
16980
+ }
16981
+
16982
+ // src/commands/merge-driver.ts
16983
+ import { execFileSync as execFileSync3 } from "child_process";
16984
+ import { readFileSync, writeFileSync, existsSync as existsSync80 } from "fs";
16985
+ import path56 from "path";
16986
+ import "commander";
16987
+ import { findProjectRoot as findProjectRoot58, mergeMemoryVersions } from "@hiveai/core";
16988
+ var GITATTRIBUTES_MARK = "# hAIve merge driver";
16989
+ var GITATTRIBUTES_BLOCK = [
16990
+ GITATTRIBUTES_MARK,
16991
+ ".ai/memories/**/*.md merge=haive",
16992
+ "# hAIve merge driver end"
16993
+ ].join("\n");
16994
+ function registerMergeDriver(program2) {
16995
+ const cmd = program2.command("merge-driver").description("Deterministic git merge driver for hAIve memory files (kills .ai/ conflict markers)");
16996
+ cmd.command("run <base> <ours> <theirs>").description("Git merge-driver entrypoint: resolve ours/theirs by frontmatter order, write into <ours>").action((base, ours, theirs) => {
16997
+ try {
16998
+ const oursContent = readFileSync(ours, "utf8");
16999
+ const theirsContent = readFileSync(theirs, "utf8");
17000
+ const result = mergeMemoryVersions(oursContent, theirsContent);
17001
+ if (result.content !== oursContent) writeFileSync(ours, result.content, "utf8");
17002
+ process.exit(0);
17003
+ } catch {
17004
+ process.exit(1);
17005
+ }
17006
+ });
17007
+ cmd.command("install").description("Configure git + .gitattributes so memory-file conflicts auto-resolve").option("-d, --dir <dir>", "project root").action((opts) => {
17008
+ const root = findProjectRoot58(opts.dir);
17009
+ try {
17010
+ execFileSync3("git", ["config", "merge.haive.name", "hAIve memory merge driver"], { cwd: root });
17011
+ execFileSync3("git", ["config", "merge.haive.driver", "haive merge-driver run %O %A %B"], { cwd: root });
17012
+ } catch {
17013
+ ui.error("Could not set git config \u2014 is this a git repository?");
17014
+ process.exitCode = 1;
17015
+ return;
17016
+ }
17017
+ const gaPath = path56.join(root, ".gitattributes");
17018
+ let content = existsSync80(gaPath) ? readFileSync(gaPath, "utf8") : "";
17019
+ if (!content.includes(GITATTRIBUTES_MARK)) {
17020
+ if (content.length > 0 && !content.endsWith("\n")) content += "\n";
17021
+ content += GITATTRIBUTES_BLOCK + "\n";
17022
+ writeFileSync(gaPath, content, "utf8");
17023
+ ui.success("Installed hAIve merge driver (git config + .gitattributes).");
17024
+ } else {
17025
+ ui.info("hAIve merge driver already present in .gitattributes \u2014 refreshed git config.");
17026
+ }
17027
+ ui.info("Memory-file conflicts under .ai/memories/ now resolve by revision_count \u2192 created_at.");
17028
+ });
17029
+ }
17030
+
17031
+ // src/commands/memory-resolve-conflict.ts
17032
+ import { writeFile as writeFile38 } from "fs/promises";
17033
+ import { existsSync as existsSync81 } from "fs";
17034
+ import "commander";
17035
+ import {
17036
+ findProjectRoot as findProjectRoot59,
17037
+ planConflictResolution,
17038
+ resolveHaivePaths as resolveHaivePaths53,
17039
+ serializeMemory as serializeMemory29
17040
+ } from "@hiveai/core";
17041
+ function registerMemoryResolveConflict(memory2) {
17042
+ memory2.command("resolve-conflict <id_a> <id_b>").description("Resolve a contradiction: keep the stronger memory, deprecate (supersede) the other").option("--yes", "apply the resolution (without this, only previews it)", false).option("--json", "emit JSON", false).option("-d, --dir <dir>", "project root").action(async (idA, idB, opts) => {
17043
+ const root = findProjectRoot59(opts.dir);
17044
+ const paths = resolveHaivePaths53(root);
17045
+ if (!existsSync81(paths.memoriesDir)) {
17046
+ ui.error(`No .ai/memories at ${root}.`);
17047
+ process.exitCode = 1;
17048
+ return;
17049
+ }
17050
+ const memories = await loadMemoriesFromDir27(paths.memoriesDir);
17051
+ const a = memories.find((m) => m.memory.frontmatter.id === idA);
17052
+ const b = memories.find((m) => m.memory.frontmatter.id === idB);
17053
+ if (!a || !b) {
17054
+ ui.error(`Memory not found: ${!a ? idA : ""} ${!b ? idB : ""}`.trim());
17055
+ process.exitCode = 1;
17056
+ return;
17057
+ }
17058
+ const plan = planConflictResolution(a, b);
17059
+ const loser = plan.supersede_id === idA ? a : b;
17060
+ if (opts.json) {
17061
+ console.log(JSON.stringify({ ...plan, applied: Boolean(opts.yes) }, null, 2));
17062
+ } else {
17063
+ console.log(ui.bold("Conflict resolution"));
17064
+ console.log(` keep: ${ui.green(plan.keep_id)}`);
17065
+ console.log(` supersede: ${ui.red(plan.supersede_id)} ${ui.dim(`\u2192 deprecated`)}`);
17066
+ console.log(` reason: ${plan.reason}`);
17067
+ }
17068
+ if (!opts.yes) {
17069
+ if (!opts.json) ui.info("Preview only \u2014 re-run with --yes to apply.");
17070
+ return;
17071
+ }
17072
+ await writeFile38(
17073
+ loser.filePath,
17074
+ serializeMemory29({
17075
+ frontmatter: {
17076
+ ...loser.memory.frontmatter,
17077
+ status: "deprecated",
17078
+ stale_reason: plan.stale_reason,
17079
+ related_ids: [.../* @__PURE__ */ new Set([...loser.memory.frontmatter.related_ids, plan.keep_id])]
17080
+ },
17081
+ body: loser.memory.body
17082
+ }),
17083
+ "utf8"
17084
+ );
17085
+ if (!opts.json) ui.success(`Deprecated ${plan.supersede_id}; ${plan.keep_id} remains authoritative.`);
17086
+ });
17087
+ }
17088
+
17089
+ // src/commands/memory-seed-git.ts
17090
+ import { execFile as execFile4 } from "child_process";
17091
+ import { mkdir as mkdir24, writeFile as writeFile39 } from "fs/promises";
17092
+ import { existsSync as existsSync83 } from "fs";
17093
+ import path57 from "path";
17094
+ import { promisify as promisify4 } from "util";
17095
+ import "commander";
17096
+ import {
17097
+ buildFrontmatter as buildFrontmatter12,
17098
+ findProjectRoot as findProjectRoot60,
17099
+ memoryFilePath as memoryFilePath13,
17100
+ proposeSeedsFromCommits,
17101
+ resolveHaivePaths as resolveHaivePaths54,
17102
+ serializeMemory as serializeMemory30
17103
+ } from "@hiveai/core";
17104
+ var exec4 = promisify4(execFile4);
17105
+ function registerMemorySeedGit(memory2) {
17106
+ memory2.command("seed-git").description("Propose draft `attempt` seeds from revert/hotfix commits in git history (cold-start)").option("--apply", "write the proposed seeds as draft memories (default: preview only)", false).option("--limit <n>", "max seeds to propose", "20").option("--days <n>", "git-history lookback window in days", "365").option("--scope <scope>", "personal | team", "team").option("--json", "emit JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
17107
+ const root = findProjectRoot60(opts.dir);
17108
+ const paths = resolveHaivePaths54(root);
17109
+ if (!existsSync83(paths.haiveDir)) {
17110
+ ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
17111
+ process.exitCode = 1;
17112
+ return;
17113
+ }
17114
+ const limit = Math.max(1, parseInt(opts.limit ?? "20", 10) || 20);
17115
+ const days = Math.max(1, parseInt(opts.days ?? "365", 10) || 365);
17116
+ const commits = await readCommits(root, days);
17117
+ const proposals = proposeSeedsFromCommits(commits, limit);
17118
+ if (opts.json) {
17119
+ console.log(JSON.stringify({ scanned_commits: commits.length, proposals, applied: Boolean(opts.apply) }, null, 2));
17120
+ } else if (proposals.length === 0) {
17121
+ ui.info("No revert/hotfix signals found in git history \u2014 nothing to seed.");
17122
+ return;
17123
+ } else {
17124
+ console.log(ui.bold(`hAIve seed-git \u2014 ${proposals.length} proposal(s) from ${commits.length} commit(s)`));
17125
+ for (const p of proposals) {
17126
+ console.log(` ${ui.yellow("\u25C6")} ${ui.dim(`[${p.kind}]`)} ${p.what}`);
17127
+ if (p.paths.length > 0) console.log(` ${ui.dim("paths:")} ${p.paths.join(", ")}`);
17128
+ }
17129
+ }
17130
+ if (!opts.apply) {
17131
+ if (!opts.json) ui.info("Preview only \u2014 re-run with --apply to write these as draft memories.");
17132
+ return;
17133
+ }
17134
+ let written = 0;
17135
+ for (const p of proposals) {
17136
+ const fm = {
17137
+ ...buildFrontmatter12({
17138
+ type: "attempt",
17139
+ slug: p.slug,
17140
+ scope: opts.scope ?? "team",
17141
+ tags: ["seed", "git-history", p.kind],
17142
+ paths: p.paths
17143
+ }),
17144
+ status: "draft"
17145
+ // human reviews before it becomes validated
17146
+ };
17147
+ const body = `# ${p.what}
17148
+
17149
+ **Why it failed / do NOT use:** ${p.why_failed}
17150
+
17151
+ _Seeded from git ${p.kind} commit ${p.source_sha}. Review and validate (or delete) \u2014 not yet authoritative._
17152
+ `;
17153
+ const file = memoryFilePath13(paths, fm.scope, fm.id, fm.module);
17154
+ if (existsSync83(file)) continue;
17155
+ await mkdir24(path57.dirname(file), { recursive: true });
17156
+ await writeFile39(file, serializeMemory30({ frontmatter: fm, body }), "utf8");
17157
+ written += 1;
17158
+ }
17159
+ if (!opts.json) {
17160
+ ui.success(`Wrote ${written} draft seed(s). Review them: \`haive memory pending\` \u2192 validate or delete.`);
17161
+ }
17162
+ });
17163
+ }
17164
+ async function readCommits(root, days) {
17165
+ try {
17166
+ const { stdout } = await exec4(
17167
+ "git",
17168
+ ["log", `--since=${days}.days.ago`, "--name-only", "--pretty=format:%x1f%h%x1f%s", "-n", "500"],
17169
+ { cwd: root, maxBuffer: 8 * 1024 * 1024 }
17170
+ );
17171
+ const blocks = stdout.split("").filter((b) => b.length > 0);
17172
+ const commits = [];
17173
+ for (let i = 0; i + 1 < blocks.length; i += 2) {
17174
+ const sha = blocks[i].trim();
17175
+ const tail = blocks[i + 1];
17176
+ const lines = tail.split("\n").map((l) => l.trim()).filter(Boolean);
17177
+ const subject = lines.shift() ?? "";
17178
+ commits.push({ sha, subject, files: lines });
17179
+ }
17180
+ return commits;
17181
+ } catch {
17182
+ return [];
17183
+ }
17184
+ }
17185
+
16747
17186
  // src/index.ts
16748
- var program = new Command59();
16749
- program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.14.0").option("--advanced", "show maintenance and experimental commands in help");
17187
+ var program = new Command63();
17188
+ program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.17.0").option("--advanced", "show maintenance and experimental commands in help");
16750
17189
  registerInit(program);
16751
17190
  registerWelcome(program);
16752
17191
  registerResolveProject(program);
@@ -16757,6 +17196,8 @@ registerAgent(program);
16757
17196
  registerSensors(program);
16758
17197
  registerIngest(program);
16759
17198
  registerDashboard(program);
17199
+ registerCoverage(program);
17200
+ registerMergeDriver(program);
16760
17201
  registerDevLink(program);
16761
17202
  registerMcp(program);
16762
17203
  registerBriefing(program);
@@ -16787,6 +17228,8 @@ registerMemoryUpdate(memory);
16787
17228
  registerMemoryHot(memory);
16788
17229
  registerMemoryTried(memory);
16789
17230
  registerMemorySeed(memory);
17231
+ registerMemorySeedGit(memory);
17232
+ registerMemoryResolveConflict(memory);
16790
17233
  registerMemoryImport(memory);
16791
17234
  registerMemoryImportChangelog(memory);
16792
17235
  registerMemoryDigest(memory);