@hiveai/cli 0.13.9 → 0.15.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 Command58 } 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";
@@ -199,7 +199,7 @@ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
199
199
  if (!f) continue;
200
200
  counts.set(f, (counts.get(f) ?? 0) + 1);
201
201
  }
202
- let entries = [...counts.entries()].map(([path55, changes]) => ({ path: path55, changes }));
202
+ let entries = [...counts.entries()].map(([path58, changes]) => ({ path: path58, changes }));
203
203
  const lowerPaths = filePaths.map((p) => p.toLowerCase());
204
204
  if (lowerPaths.length > 0) {
205
205
  entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
@@ -3019,7 +3019,7 @@ ${SEED_FOOTER(stack)}` });
3019
3019
  }
3020
3020
 
3021
3021
  // src/commands/init.ts
3022
- var HAIVE_GITHUB_ACTION_REF = `v${"0.13.9"}`;
3022
+ var HAIVE_GITHUB_ACTION_REF = `v${"0.15.0"}`;
3023
3023
  var PROJECT_CONTEXT_TEMPLATE = `# Project context
3024
3024
 
3025
3025
  > Generated by \`haive init\`. Run \`haive init --bootstrap\` to auto-fill from your codebase,
@@ -3202,7 +3202,11 @@ jobs:
3202
3202
  run: npm install -g @hiveai/cli
3203
3203
 
3204
3204
  - name: harness quality regression gate
3205
- run: haive eval --regression-gate
3205
+ run: haive eval --regression-gate --record --ref "\${{ github.sha }}"
3206
+
3207
+ - name: harness quality trend
3208
+ if: always()
3209
+ run: haive eval --trend || true
3206
3210
 
3207
3211
  # On push to main: push shared memories to the hub (if hubPath is configured)
3208
3212
  # Uncomment and configure hubPath in .ai/haive.config.json to enable.
@@ -4089,10 +4093,13 @@ import {
4089
4093
  literalMatchesAnyToken as literalMatchesAnyToken22,
4090
4094
  loadCodeMap as loadCodeMap5,
4091
4095
  loadConfig as loadConfig3,
4096
+ hashProjectContext,
4092
4097
  loadMemoriesFromDir as loadMemoriesFromDir15,
4093
4098
  loadUsageIndex as loadUsageIndex8,
4094
4099
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths22,
4100
+ projectContextRecentlyEmitted,
4095
4101
  rankMemoriesLexical as rankMemoriesLexical2,
4102
+ recordProjectContextEmission,
4096
4103
  queryCodeMap as queryCodeMap2,
4097
4104
  resolveBriefingBudget as resolveBriefingBudget2,
4098
4105
  serializeMemory as serializeMemory10,
@@ -5711,6 +5718,9 @@ var GetBriefingInputSchema = {
5711
5718
  ),
5712
5719
  max_memories: z19.number().int().positive().default(8).describe("Cap on memories surfaced regardless of token budget"),
5713
5720
  include_project_context: z19.boolean().default(true),
5721
+ dedupe_project_context: z19.boolean().optional().describe(
5722
+ "Token saver (default ON): skip re-emitting the project-context body if an identical copy was already sent within the last few minutes this session (the agent still has it). Set false to always include it."
5723
+ ),
5714
5724
  include_module_contexts: z19.boolean().default(true),
5715
5725
  semantic: z19.boolean().default(true).describe(
5716
5726
  "Use semantic ranking when a task is provided (requires `haive embeddings index`)."
@@ -5918,7 +5928,17 @@ async function getBriefing(input, ctx) {
5918
5928
  }
5919
5929
  }
5920
5930
  }
5921
- const projectContextRaw = input.include_project_context && existsSync21(ctx.paths.projectContext) ? await readFile52(ctx.paths.projectContext, "utf8") : "";
5931
+ let projectContextRaw = input.include_project_context && existsSync21(ctx.paths.projectContext) ? await readFile52(ctx.paths.projectContext, "utf8") : "";
5932
+ let contextOmittedRecent = false;
5933
+ if (projectContextRaw && input.dedupe_project_context !== false) {
5934
+ const ctxHash = hashProjectContext(projectContextRaw);
5935
+ if (await projectContextRecentlyEmitted(ctx.paths, ctxHash)) {
5936
+ contextOmittedRecent = true;
5937
+ projectContextRaw = "";
5938
+ } else {
5939
+ await recordProjectContextEmission(ctx.paths, ctxHash);
5940
+ }
5941
+ }
5922
5942
  const isTemplateContext = projectContextRaw.includes("TODO \u2014 high-level overview") || projectContextRaw.includes("Generated by `haive init`");
5923
5943
  const setupWarnings = [];
5924
5944
  let autoContextGenerated = false;
@@ -6186,7 +6206,11 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
6186
6206
  search_mode: searchMode,
6187
6207
  inferred_modules: inferred,
6188
6208
  ...lastSession ? { last_session: lastSession } : {},
6189
- project_context: adaptiveTrim ? {
6209
+ project_context: contextOmittedRecent ? {
6210
+ content: "(project context unchanged \u2014 omitted to save tokens; it was provided earlier this session. Pass dedupe_project_context:false to force a full copy.)",
6211
+ truncated: false,
6212
+ omitted_recent: true
6213
+ } : adaptiveTrim ? {
6190
6214
  content: "(adaptive briefing: auto-generated context omitted \u2014 no team-specific policy matched, so a capable model needs nothing extra here)",
6191
6215
  truncated: false,
6192
6216
  ...isTemplateContext && !autoContextGenerated ? { is_template: true } : {},
@@ -7504,7 +7528,7 @@ async function patternDetect(input, ctx) {
7504
7528
  for (const [p, { count, tools }] of pathCounts) {
7505
7529
  if (count < HOT_FILE_MIN) continue;
7506
7530
  if (tools.has("mem_tried") || tools.has("mem_observe")) continue;
7507
- if (CONFIG_PATTERNS.some((cp) => path122.basename(p).includes(cp))) continue;
7531
+ if (CONFIG_PATTERNS.some((cp2) => path122.basename(p).includes(cp2))) continue;
7508
7532
  const slug = p.replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 40);
7509
7533
  matches.push({
7510
7534
  kind: "hot_file",
@@ -7938,7 +7962,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
7938
7962
  };
7939
7963
  }
7940
7964
  var SERVER_NAME = "haive";
7941
- var SERVER_VERSION = "0.13.9";
7965
+ var SERVER_VERSION = "0.15.0";
7942
7966
  function jsonResult(data) {
7943
7967
  return {
7944
7968
  content: [
@@ -8687,6 +8711,7 @@ function createHaiveServer(options = {}) {
8687
8711
  "anti_patterns_check",
8688
8712
  [
8689
8713
  "Scan a diff (or set of paths) against documented attempt/gotcha memories.",
8714
+ "[Diff-scan layer: the MEMORY-MATCH component. `pre_commit_check` combines this with sensors + stale checks; `haive enforce check` is the gate.]",
8690
8715
  "Surfaces 'you are about to repeat a known mistake' warnings BEFORE you commit.",
8691
8716
  "",
8692
8717
  "USE BEFORE finalizing a non-trivial change. Cheap and high-signal: the only",
@@ -8830,6 +8855,7 @@ function createHaiveServer(options = {}) {
8830
8855
  "pre_commit_check",
8831
8856
  [
8832
8857
  "One-shot 'should I block this commit?' check. Combines three signals:",
8858
+ "[Diff-scan layer: the COMBINED check (sensors + anti-patterns + stale). `haive enforce check` is the gate that runs this at commit time.]",
8833
8859
  "",
8834
8860
  " 1. anti_patterns_check \u2014 known gotchas/attempts that match the diff",
8835
8861
  " 2. mem_for_files \u2014 conventions/decisions anchored to touched files",
@@ -12687,9 +12713,12 @@ import "commander";
12687
12713
  import {
12688
12714
  aggregateRetrieval,
12689
12715
  aggregateSensors,
12716
+ appendEvalHistory,
12690
12717
  buildReport,
12691
12718
  compareEvalReports,
12719
+ computeEvalTrend,
12692
12720
  findProjectRoot as findProjectRoot42,
12721
+ loadEvalHistory,
12693
12722
  resolveHaivePaths as resolveHaivePaths38,
12694
12723
  scoreRetrievalCase,
12695
12724
  scoreSensorCase,
@@ -12698,7 +12727,7 @@ import {
12698
12727
  function registerEval(program2) {
12699
12728
  program2.command("eval").description(
12700
12729
  "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."
12701
- ).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) => {
12730
+ ).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) => {
12702
12731
  const root = findProjectRoot42(opts.dir);
12703
12732
  const paths = resolveHaivePaths38(root);
12704
12733
  if (!existsSync65(paths.memoriesDir)) {
@@ -12706,6 +12735,22 @@ function registerEval(program2) {
12706
12735
  process.exitCode = 1;
12707
12736
  return;
12708
12737
  }
12738
+ if (opts.trend) {
12739
+ const trend = computeEvalTrend(await loadEvalHistory(paths));
12740
+ if (opts.json) {
12741
+ console.log(JSON.stringify(trend, null, 2));
12742
+ return;
12743
+ }
12744
+ if (trend.runs === 0) {
12745
+ ui.info("No eval history yet. Run `haive eval --record` to start trending the harness.");
12746
+ return;
12747
+ }
12748
+ const spark = trend.recent.map((s) => "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588"[Math.min(7, Math.round(s / 100 * 7))]).join("");
12749
+ const arrow = trend.regressed ? ui.red("\u25BC") : (trend.delta ?? 0) > 0 ? ui.green("\u25B2") : ui.dim("=");
12750
+ console.log(ui.bold("hAIve eval trend"));
12751
+ console.log(` ${spark} latest ${arrow} ${trend.latest}/100 ${ui.dim(`(best ${trend.best}, ${trend.runs} run${trend.runs === 1 ? "" : "s"})`)}`);
12752
+ return;
12753
+ }
12709
12754
  const k = Math.max(1, parseInt(opts.top ?? "8", 10) || 8);
12710
12755
  const ctx = { paths };
12711
12756
  const resolvedSpec = await resolveSpec(opts, root, paths.memoriesDir);
@@ -12733,6 +12778,17 @@ function registerEval(program2) {
12733
12778
  sensorAgg = aggregateSensors(results);
12734
12779
  }
12735
12780
  const report = buildReport(retrievalAgg, sensorAgg);
12781
+ if (opts.record) {
12782
+ await appendEvalHistory(paths, {
12783
+ at: (/* @__PURE__ */ new Date()).toISOString(),
12784
+ score: report.score,
12785
+ ...report.retrieval ? { mean_recall: report.retrieval.mean_recall, mrr: report.retrieval.mrr } : {},
12786
+ ...report.sensors ? { catch_rate: report.sensors.catch_rate } : {},
12787
+ ...opts.ref ? { ref: opts.ref } : {}
12788
+ }).catch(() => {
12789
+ });
12790
+ if (!opts.json) ui.success(`Recorded eval score ${report.score}/100 to history.`);
12791
+ }
12736
12792
  const baselineFile = opts.baselineFile ? path43.isAbsolute(opts.baselineFile) ? opts.baselineFile : path43.join(root, opts.baselineFile) : path43.join(root, ".ai", "eval", "baseline.json");
12737
12793
  if (opts.baseline) {
12738
12794
  const snapshot = {
@@ -13308,8 +13364,8 @@ function registerDoctor(program2) {
13308
13364
  fix: "haive init"
13309
13365
  });
13310
13366
  } else {
13311
- const { readFile: readFile26 } = await import("fs/promises");
13312
- const content = await readFile26(paths.projectContext, "utf8");
13367
+ const { readFile: readFile27 } = await import("fs/promises");
13368
+ const content = await readFile27(paths.projectContext, "utf8");
13313
13369
  const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
13314
13370
  if (isTemplate) {
13315
13371
  findings.push({
@@ -13483,8 +13539,8 @@ function registerDoctor(program2) {
13483
13539
  let hasClaudeEnforcement = false;
13484
13540
  if (existsSync68(claudeSettings)) {
13485
13541
  try {
13486
- const { readFile: readFile26 } = await import("fs/promises");
13487
- const raw = await readFile26(claudeSettings, "utf8");
13542
+ const { readFile: readFile27 } = await import("fs/promises");
13543
+ const raw = await readFile27(claudeSettings, "utf8");
13488
13544
  hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
13489
13545
  } catch {
13490
13546
  hasClaudeEnforcement = false;
@@ -13507,7 +13563,7 @@ function registerDoctor(program2) {
13507
13563
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
13508
13564
  });
13509
13565
  }
13510
- findings.push(...await collectInstallFindings(root, "0.13.9"));
13566
+ findings.push(...await collectInstallFindings(root, "0.15.0"));
13511
13567
  findings.push(...await collectToolchainFindings(root));
13512
13568
  try {
13513
13569
  const legacyRaw = execSync3("haive-mcp --version", {
@@ -13515,7 +13571,7 @@ function registerDoctor(program2) {
13515
13571
  timeout: 3e3,
13516
13572
  stdio: ["ignore", "pipe", "ignore"]
13517
13573
  }).trim();
13518
- const cliVersion = "0.13.9";
13574
+ const cliVersion = "0.15.0";
13519
13575
  if (legacyRaw && legacyRaw !== cliVersion) {
13520
13576
  findings.push({
13521
13577
  severity: "warn",
@@ -14577,6 +14633,7 @@ import "commander";
14577
14633
  import {
14578
14634
  antiPatternGateParams as antiPatternGateParams2,
14579
14635
  findProjectRoot as findProjectRoot52,
14636
+ findUncapturedFailures,
14580
14637
  hasRecentBriefingMarker as hasRecentBriefingMarker2,
14581
14638
  isFreshIsoDate,
14582
14639
  loadConfig as loadConfig13,
@@ -14748,52 +14805,73 @@ ${briefing.project_context.content.slice(0, 1800)}`);
14748
14805
  [setup warning] ${warning}`);
14749
14806
  }
14750
14807
  });
14751
- enforce.command("pre-tool-use").description("Claude Code PreToolUse hook: block writes until hAIve briefing has been loaded.").option("-d, --dir <dir>", "project root").action(async (opts) => {
14808
+ enforce.command("pre-tool-use").description("Claude Code PreToolUse hook: surface the relevant team policy for the edited file (advise; configurable to block).").option("-d, --dir <dir>", "project root").action(async (opts) => {
14752
14809
  const payload = await readHookPayload();
14753
14810
  const root = resolveRoot(opts.dir, payload);
14754
14811
  if (!root) return;
14755
14812
  const paths = resolveHaivePaths48(root);
14756
14813
  if (!existsSync75(paths.haiveDir)) return;
14757
14814
  if (!isWriteLikeTool(payload)) return;
14758
- const ok = await hasRecentBriefingMarker2(paths, payload.session_id);
14759
- if (ok) {
14760
- const targetFiles = extractToolPaths(payload, root);
14761
- if (targetFiles.length === 0) return;
14762
- const missing = await missingRequiredMemoriesForFiles(paths, targetFiles, payload.session_id);
14763
- if (missing.length === 0) return;
14764
- const ids = missing.slice(0, 6).map((memory2) => memory2.memory.frontmatter.id);
14815
+ const config = await loadConfig13(paths);
14816
+ if (config.enforcement?.requireBriefingFirst === false) return;
14817
+ const gate = config.enforcement?.preEditGate ?? "advise";
14818
+ const targetFiles = extractToolPaths(payload, root);
14819
+ const hasMarker = await hasRecentBriefingMarker2(paths, payload.session_id);
14820
+ const missing = targetFiles.length > 0 ? await missingRequiredMemoriesForFiles(paths, targetFiles, payload.session_id) : [];
14821
+ if (hasMarker && missing.length === 0) return;
14822
+ if (targetFiles.length > 0) {
14823
+ await recordFilesIntoBriefingMarker(paths, targetFiles, missing, payload.session_id).catch(() => {
14824
+ });
14825
+ }
14826
+ const contextText = buildPreEditContext(payload.tool_name ?? "write tool", targetFiles, missing, hasMarker);
14827
+ if (gate === "block") {
14765
14828
  console.error(
14766
- [
14767
- "hAIve enforcement blocked this action.",
14768
- `Tool: ${payload.tool_name ?? "write tool"}`,
14769
- `Files: ${targetFiles.slice(0, 6).join(", ")}`,
14770
- "",
14771
- "These files have required hAIve context that was not in the current briefing:",
14772
- ...ids.map((id) => ` - ${id}`),
14773
- "",
14774
- "Load the targeted briefing before editing:",
14775
- ` ${briefingCommandForFiles(targetFiles)}`
14776
- ].join("\n")
14829
+ contextText + '\n\nThe relevant context is now recorded \u2014 re-issue the same edit to proceed (no `haive briefing` command needed). To make this advisory instead of blocking, set `{ "enforcement": { "preEditGate": "advise" } }` in .ai/haive.config.json.'
14777
14830
  );
14778
14831
  process.exit(2);
14779
14832
  }
14780
- const tool = payload.tool_name ?? "write tool";
14781
- console.error(
14782
- [
14783
- "hAIve enforcement blocked this action.",
14784
- `Tool: ${tool}`,
14785
- "",
14786
- "This project is initialized with hAIve. Load the team briefing before editing:",
14787
- " haive enforce session-start",
14788
- "or call MCP get_briefing / mem_relevant_to from your AI client.",
14789
- "",
14790
- "If this is intentional, a human can disable enforcement in .ai/haive.config.json:",
14791
- ' { "enforcement": { "requireBriefingFirst": false } }'
14792
- ].join("\n")
14793
- );
14794
- process.exit(2);
14833
+ emitPreToolUseContext(contextText);
14795
14834
  });
14796
14835
  }
14836
+ async function recordFilesIntoBriefingMarker(paths, files, missing, sessionId) {
14837
+ const existing = await readRecentBriefingMarker(paths, sessionId);
14838
+ const ids = new Set(existing?.memory_ids ?? []);
14839
+ for (const { memory: memory2 } of missing) ids.add(memory2.frontmatter.id);
14840
+ await writeBriefingMarker3(paths, {
14841
+ sessionId,
14842
+ task: existing?.task ?? "pre-edit auto-briefing",
14843
+ source: "haive-pre-edit",
14844
+ files,
14845
+ memoryIds: [...ids]
14846
+ });
14847
+ }
14848
+ function buildPreEditContext(tool, files, missing, hasMarker) {
14849
+ const lines = ["hAIve \u2014 relevant team policy for this edit", `Tool: ${tool}`];
14850
+ if (files.length > 0) lines.push(`Files: ${files.slice(0, 6).join(", ")}`);
14851
+ if (missing.length > 0) {
14852
+ lines.push("", "Consult these before editing (anchored to the files you are touching):");
14853
+ for (const { memory: memory2 } of missing.slice(0, 5)) {
14854
+ const fm = memory2.frontmatter;
14855
+ lines.push("", `### ${fm.id} (${fm.scope}/${fm.type})`, memory2.body.trim().slice(0, 900));
14856
+ }
14857
+ } else if (!hasMarker) {
14858
+ lines.push(
14859
+ "",
14860
+ "No team briefing was loaded yet this session. Proceeding \u2014 but for substantive work call get_briefing / mem_relevant_to for richer context."
14861
+ );
14862
+ }
14863
+ return lines.join("\n");
14864
+ }
14865
+ function emitPreToolUseContext(text) {
14866
+ console.log(
14867
+ JSON.stringify({
14868
+ hookSpecificOutput: {
14869
+ hookEventName: "PreToolUse",
14870
+ additionalContext: text
14871
+ }
14872
+ })
14873
+ );
14874
+ }
14797
14875
  async function buildFinishReport(dir) {
14798
14876
  const root = findProjectRoot52(dir);
14799
14877
  const paths = resolveHaivePaths48(root);
@@ -14817,6 +14895,7 @@ async function buildFinishReport(dir) {
14817
14895
  }]
14818
14896
  });
14819
14897
  }
14898
+ findings.push(...await checkFailureCapture(paths, config));
14820
14899
  const status = await getGitSyncStatus(root);
14821
14900
  if (!status.available) {
14822
14901
  findings.push({
@@ -14980,6 +15059,47 @@ async function buildFinishReport(dir) {
14980
15059
  findings.push(...await verifyGithubActionsForHead(root, status));
14981
15060
  return finishReport(root, initialized, mode, findings, config);
14982
15061
  }
15062
+ async function checkFailureCapture(paths, config) {
15063
+ const gate = config.enforcement?.failureCaptureGate ?? "warn";
15064
+ if (gate === "off") return [];
15065
+ const obsFile = path51.join(paths.haiveDir, ".cache", "observations.jsonl");
15066
+ if (!existsSync75(obsFile)) return [];
15067
+ const failures = [];
15068
+ try {
15069
+ const raw = await readFile23(obsFile, "utf8");
15070
+ for (const line of raw.split("\n")) {
15071
+ const trimmed = line.trim();
15072
+ if (!trimmed) continue;
15073
+ try {
15074
+ const o = JSON.parse(trimmed);
15075
+ if (o.failure_hint && o.ts) failures.push({ ts: o.ts, tool: o.tool ?? "?", summary: o.summary ?? "" });
15076
+ } catch {
15077
+ }
15078
+ }
15079
+ } catch {
15080
+ return [];
15081
+ }
15082
+ if (failures.length === 0) return [];
15083
+ const memories = existsSync75(paths.memoriesDir) ? await loadMemoriesFromDir38(paths.memoriesDir) : [];
15084
+ const captureTimes = memories.filter(({ memory: memory2 }) => ["attempt", "gotcha"].includes(memory2.frontmatter.type)).map(({ memory: memory2 }) => memory2.frontmatter.created_at);
15085
+ const uncaptured = findUncapturedFailures(failures, captureTimes);
15086
+ if (uncaptured.length === 0) {
15087
+ return [{
15088
+ severity: "ok",
15089
+ code: "failure-capture-clean",
15090
+ message: "No uncaptured hard failures from this session."
15091
+ }];
15092
+ }
15093
+ return [{
15094
+ severity: gate === "block" ? "error" : "info",
15095
+ code: "uncaptured-failures",
15096
+ message: `${uncaptured.length} hard failure(s) this session were never captured as a lesson (mem_tried).`,
15097
+ 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.",
15098
+ 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.",
15099
+ affected_files: uncaptured.slice(0, 8).map((f) => `${f.tool}: ${f.summary}`.slice(0, 100)),
15100
+ ...gate === "block" ? { impact: 30 } : {}
15101
+ }];
15102
+ }
14983
15103
  function finishReport(root, initialized, mode, findings, config) {
14984
15104
  const score = buildScore(findings, config.enforcement?.scoreThreshold);
14985
15105
  const hasErrors = findings.some((f) => f.severity === "error");
@@ -15124,7 +15244,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
15124
15244
  findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
15125
15245
  });
15126
15246
  }
15127
- findings.push(...await inspectIntegrationVersions(root, "0.13.9"));
15247
+ findings.push(...await inspectIntegrationVersions(root, "0.15.0"));
15128
15248
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
15129
15249
  const hasBriefing = await hasRecentBriefingMarker2(paths, sessionId);
15130
15250
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
@@ -15242,7 +15362,7 @@ async function verifyMemoryPolicy(paths, config) {
15242
15362
  }
15243
15363
  async function verifyDecisionCoverage(paths, stage, sessionId) {
15244
15364
  if (!existsSync75(paths.memoriesDir)) return [];
15245
- const changedFiles = await getChangedFiles(paths.root, stage);
15365
+ const changedFiles = (await getChangedFiles(paths.root, stage)).filter((f) => !isGeneratedArtifact(f));
15246
15366
  if (changedFiles.length === 0) {
15247
15367
  return [{ severity: "info", code: "decision-coverage-no-changes", message: "No changed files to match against policy memories." }];
15248
15368
  }
@@ -16039,8 +16159,10 @@ async function missingRequiredMemoriesForFiles(paths, files, sessionId) {
16039
16159
  return memoryMatchesAnchorPaths6(memory2, files);
16040
16160
  }).map(({ memory: memory2, filePath }) => ({ memory: memory2, filePath }));
16041
16161
  }
16042
- function briefingCommandForFiles(files) {
16043
- return `haive briefing --files "${files.slice(0, 10).join(",")}" --task "edit ${files.slice(0, 3).join(", ")}"`;
16162
+ function isGeneratedArtifact(file) {
16163
+ if (file === ".ai/project-context.md" || file === ".ai/code-map.json") return true;
16164
+ if (file.startsWith(".ai/.cache/") || file.startsWith(".ai/.runtime/") || file.startsWith(".ai/.usage/")) return true;
16165
+ return false;
16044
16166
  }
16045
16167
  async function readStdin2(maxBytes) {
16046
16168
  if (process.stdin.isTTY) return "";
@@ -16118,12 +16240,14 @@ import {
16118
16240
  appendPreventionEvent as appendPreventionEvent2,
16119
16241
  findProjectRoot as findProjectRoot53,
16120
16242
  isRetiredMemory as isRetiredMemory3,
16243
+ loadConfig as loadConfig14,
16121
16244
  loadMemoriesFromDir as loadMemoriesFromDir39,
16122
16245
  loadUsageIndex as loadUsageIndex29,
16123
16246
  recordPrevention as recordPrevention2,
16124
16247
  resolveHaivePaths as resolveHaivePaths49,
16125
16248
  runSensors as runSensors2,
16126
16249
  saveUsageIndex as saveUsageIndex8,
16250
+ selectCommandSensors,
16127
16251
  sensorTargetsFromDiff as sensorTargetsFromDiff2,
16128
16252
  serializeMemory as serializeMemory27
16129
16253
  } from "@hiveai/core";
@@ -16151,14 +16275,38 @@ function registerSensors(program2) {
16151
16275
  if (row.last_fired) console.log(` ${ui.dim("last fired:")} ${row.last_fired}`);
16152
16276
  }
16153
16277
  });
16154
- sensors.command("check").description("Run regex sensors against a diff; defaults to `git diff --cached`").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) => {
16278
+ sensors.command("check").description(
16279
+ "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."
16280
+ ).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) => {
16155
16281
  const root = findProjectRoot53(opts.dir);
16156
16282
  const paths = resolveHaivePaths49(root);
16157
16283
  const memories = await runnableSensorMemories(paths);
16158
16284
  const diff = opts.diffFile ? await readFile24(path53.resolve(root, opts.diffFile), "utf8") : await stagedDiff(root);
16159
16285
  const targets = sensorTargetsFromDiff2(diff);
16160
16286
  const hits = runSensors2(memories, targets.length > 0 ? targets : [{ path: "", content: diff }]);
16161
- const firedIds = [...new Set(hits.map((hit) => hit.memory_id))];
16287
+ const config = await loadConfig14(paths);
16288
+ const runCommands = opts.commands || config.enforcement?.runCommandSensors === true;
16289
+ const changedPaths = targets.map((t) => t.path).filter(Boolean);
16290
+ const allSensorMemories = await runnableSensorMemories(paths, false);
16291
+ const commandSpecs = selectCommandSensors(allSensorMemories, changedPaths);
16292
+ const commandHits = [];
16293
+ const commandSkipped = [];
16294
+ if (commandSpecs.length > 0 && runCommands) {
16295
+ for (const spec of commandSpecs) {
16296
+ const failed = await runCommandSensor(spec, root);
16297
+ if (failed) {
16298
+ commandHits.push({
16299
+ memory_id: spec.memory_id,
16300
+ severity: spec.severity,
16301
+ message: spec.message,
16302
+ matched_line: `command failed: ${spec.command}`
16303
+ });
16304
+ }
16305
+ }
16306
+ } else if (commandSpecs.length > 0) {
16307
+ for (const spec of commandSpecs) commandSkipped.push(spec.memory_id);
16308
+ }
16309
+ const firedIds = [...new Set([...hits, ...commandHits].map((hit) => hit.memory_id))];
16162
16310
  if (firedIds.length > 0) {
16163
16311
  const usage = await loadUsageIndex29(paths);
16164
16312
  const recordedIds = [];
@@ -16181,12 +16329,15 @@ function registerSensors(program2) {
16181
16329
  severity: hit.severity,
16182
16330
  message: hit.message,
16183
16331
  matched_line: hit.matched_line
16184
- }))
16332
+ })),
16333
+ command_hits: commandHits,
16334
+ command_skipped: commandSkipped
16185
16335
  };
16186
16336
  if (opts.json) {
16187
16337
  console.log(JSON.stringify(output, null, 2));
16188
16338
  } else {
16189
- console.log(ui.bold(`hAIve sensors check \u2014 ${hits.length} hit(s), ${memories.length} sensor(s)`));
16339
+ const total = hits.length + commandHits.length;
16340
+ console.log(ui.bold(`hAIve sensors check \u2014 ${total} hit(s), ${memories.length} regex + ${commandSpecs.length} command sensor(s)`));
16190
16341
  for (const hit of hits) {
16191
16342
  const marker = hit.severity === "block" ? ui.red("\u2717") : ui.yellow("\u26A0");
16192
16343
  console.log(` ${marker} ${hit.memory_id} ${ui.dim(`(${hit.severity})`)}`);
@@ -16194,8 +16345,17 @@ function registerSensors(program2) {
16194
16345
  console.log(` ${hit.message}`);
16195
16346
  if (hit.matched_line) console.log(` ${ui.dim(hit.matched_line)}`);
16196
16347
  }
16348
+ for (const hit of commandHits) {
16349
+ const marker = hit.severity === "block" ? ui.red("\u2717") : ui.yellow("\u26A0");
16350
+ console.log(` ${marker} ${hit.memory_id} ${ui.dim(`(${hit.severity}, command)`)}`);
16351
+ console.log(` ${hit.message}`);
16352
+ console.log(` ${ui.dim(hit.matched_line)}`);
16353
+ }
16354
+ if (commandSkipped.length > 0) {
16355
+ console.log(ui.dim(` ${commandSkipped.length} command sensor(s) not run \u2014 pass --commands or set enforcement.runCommandSensors.`));
16356
+ }
16197
16357
  }
16198
- if (hits.some((hit) => hit.severity === "block")) process.exitCode = 1;
16358
+ if ([...hits, ...commandHits].some((hit) => hit.severity === "block")) process.exitCode = 1;
16199
16359
  });
16200
16360
  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) => {
16201
16361
  const severity = opts.severity ?? "block";
@@ -16282,6 +16442,14 @@ async function runnableSensorMemories(paths, regexOnly = true) {
16282
16442
  return !isRetiredMemory3(memory2.frontmatter, memory2.body);
16283
16443
  });
16284
16444
  }
16445
+ async function runCommandSensor(spec, root) {
16446
+ try {
16447
+ await exec2("bash", ["-c", spec.command], { cwd: root, timeout: 12e4, maxBuffer: 8 * 1024 * 1024 });
16448
+ return false;
16449
+ } catch {
16450
+ return true;
16451
+ }
16452
+ }
16285
16453
  async function stagedDiff(root) {
16286
16454
  try {
16287
16455
  const { stdout } = await exec2("git", ["diff", "--cached"], { cwd: root });
@@ -16515,6 +16683,7 @@ import "commander";
16515
16683
  import {
16516
16684
  buildDashboard,
16517
16685
  findProjectRoot as findProjectRoot55,
16686
+ loadConfig as loadConfig15,
16518
16687
  loadMemoriesFromDir as loadMemoriesFromDir41,
16519
16688
  loadPreventionEvents,
16520
16689
  loadUsageIndex as loadUsageIndex30,
@@ -16534,11 +16703,13 @@ function registerDashboard(program2) {
16534
16703
  const memories = existsSync78(paths.memoriesDir) ? await loadMemoriesFromDir41(paths.memoriesDir) : [];
16535
16704
  const usage = await loadUsageIndex30(paths);
16536
16705
  const preventionEvents = await loadPreventionEvents(paths);
16706
+ const config = await loadConfig15(paths);
16537
16707
  const top = Math.max(1, Number.parseInt(opts.top ?? "10", 10) || 10);
16538
16708
  const dormantDays = opts.dormantDays ? Number.parseInt(opts.dormantDays, 10) : void 0;
16539
16709
  const report = buildDashboard(memories, usage, {
16540
16710
  top,
16541
16711
  preventionEvents,
16712
+ antiPatternGate: config.enforcement?.antiPatternGate ?? "anchored",
16542
16713
  ...dormantDays !== void 0 && Number.isFinite(dormantDays) ? { dormantDays } : {}
16543
16714
  });
16544
16715
  if (opts.json) {
@@ -16549,7 +16720,7 @@ function registerDashboard(program2) {
16549
16720
  });
16550
16721
  }
16551
16722
  function renderDashboard(r) {
16552
- const { inventory: inv, impact, sensors, health, decay, corpus, prevention } = r;
16723
+ const { inventory: inv, impact, sensors, health, decay, corpus, prevention, gate_precision: gate } = r;
16553
16724
  console.log(ui.bold("hAIve dashboard"));
16554
16725
  console.log(
16555
16726
  ` ${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`
@@ -16600,6 +16771,15 @@ function renderDashboard(r) {
16600
16771
  }
16601
16772
  }
16602
16773
  console.log();
16774
+ console.log(ui.bold("Gate precision") + ui.dim(" (is the anti-pattern gate real or noisy?)"));
16775
+ 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)}%`);
16776
+ console.log(
16777
+ ` ${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"}`
16778
+ );
16779
+ if (gate.suggestion) {
16780
+ ui.info(`Tuning: set enforcement.antiPatternGate="${gate.suggestion.recommended}" \u2014 ${gate.suggestion.reason}`);
16781
+ }
16782
+ console.log();
16603
16783
  console.log(ui.bold("Health"));
16604
16784
  console.log(
16605
16785
  ` stale ${warnNum(health.stale)} \xB7 anchorless ${warnNum(health.anchorless)} \xB7 pending ${health.pending} \xB7 prune candidates ${warnNum(health.prune_candidates)}`
@@ -16634,9 +16814,331 @@ function warnNum(n) {
16634
16814
  return n > 0 ? ui.yellow(String(n)) : String(n);
16635
16815
  }
16636
16816
 
16817
+ // src/commands/dev-link.ts
16818
+ import { execFile as execFile3 } from "child_process";
16819
+ import { cp, readFile as readFile26 } from "fs/promises";
16820
+ import { existsSync as existsSync79 } from "fs";
16821
+ import path55 from "path";
16822
+ import { promisify as promisify3 } from "util";
16823
+ import "commander";
16824
+ import { findProjectRoot as findProjectRoot56 } from "@hiveai/core";
16825
+ var exec3 = promisify3(execFile3);
16826
+ function registerDevLink(program2) {
16827
+ const dev = program2.commands.find((c) => c.name() === "dev") ?? program2.command("dev").description("Developer utilities for working on hAIve itself.");
16828
+ dev.command("link").description("Hot-swap this repo's built dist into the global @hiveai install so `haive` runs your local code.").option("-d, --dir <dir>", "repo root (default: discovered from cwd)").option("--json", "emit a machine-readable summary", false).action(async (opts) => {
16829
+ const root = findProjectRoot56(opts.dir);
16830
+ if (!existsSync79(path55.join(root, "packages", "cli", "dist", "index.js"))) {
16831
+ ui.error(`Not the hAIve monorepo (no packages/cli/dist) at ${root}. Run \`pnpm -r build\` first, or pass --dir.`);
16832
+ process.exitCode = 1;
16833
+ return;
16834
+ }
16835
+ let globalModules;
16836
+ try {
16837
+ globalModules = (await exec3("npm", ["root", "-g"])).stdout.trim();
16838
+ } catch {
16839
+ globalModules = path55.join(path55.dirname(path55.dirname(process.execPath)), "lib", "node_modules");
16840
+ }
16841
+ const globalHive = path55.join(globalModules, "@hiveai");
16842
+ if (!existsSync79(globalHive)) {
16843
+ ui.error(`No global @hiveai install at ${globalHive}. Install once with \`npm i -g @hiveai/cli\`, then re-run.`);
16844
+ process.exitCode = 1;
16845
+ return;
16846
+ }
16847
+ const linked = [];
16848
+ const copyDist = async (fromPkg, toDistDir) => {
16849
+ const from = path55.join(root, "packages", fromPkg, "dist");
16850
+ if (!existsSync79(from) || !existsSync79(path55.dirname(toDistDir))) return;
16851
+ await cp(from, toDistDir, { recursive: true });
16852
+ linked.push(path55.relative(globalModules, toDistDir));
16853
+ };
16854
+ for (const pkg of ["cli", "mcp"]) {
16855
+ await copyDist(pkg, path55.join(globalHive, pkg, "dist"));
16856
+ for (const nested of ["core", "embeddings"]) {
16857
+ await copyDist(nested, path55.join(globalHive, pkg, "node_modules", "@hiveai", nested, "dist"));
16858
+ }
16859
+ }
16860
+ await copyDist("core", path55.join(globalHive, "core", "dist"));
16861
+ let version = "unknown";
16862
+ try {
16863
+ version = JSON.parse(await readFile26(path55.join(root, "package.json"), "utf8")).version ?? "unknown";
16864
+ } catch {
16865
+ }
16866
+ if (opts.json) {
16867
+ console.log(JSON.stringify({ ok: linked.length > 0, version, global_root: globalHive, linked }, null, 2));
16868
+ return;
16869
+ }
16870
+ if (linked.length === 0) {
16871
+ ui.warn("Nothing linked \u2014 no matching dist targets were found in the global install.");
16872
+ return;
16873
+ }
16874
+ ui.success(`Linked local dist (v${version}) into the global @hiveai install:`);
16875
+ for (const t of linked) console.log(` ${ui.dim("\u2192")} ${t}`);
16876
+ console.log(ui.dim("The global `haive` now runs your local build (git hooks + MCP included)."));
16877
+ });
16878
+ }
16879
+
16880
+ // src/commands/coverage.ts
16881
+ import "commander";
16882
+ import { findCoverageGaps, findProjectRoot as findProjectRoot57, resolveHaivePaths as resolveHaivePaths52 } from "@hiveai/core";
16883
+ function isNoisePath(p) {
16884
+ if (/(^|\/)(node_modules|dist|build|coverage|\.next)\//.test(p)) return true;
16885
+ if (p.startsWith(".ai/")) return true;
16886
+ if (/\.(jsonl|lock|map|snap|min\.js)$/.test(p)) return true;
16887
+ if (/(^|\/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$/.test(p)) return true;
16888
+ if (/(^|\/)(CHANGELOG|LICENSE)(\.md)?$/.test(p)) return true;
16889
+ return false;
16890
+ }
16891
+ function registerCoverage(program2) {
16892
+ program2.command("coverage").description(
16893
+ "Coverage-gap report: frequently-edited files with no covering team memory (blind spots)."
16894
+ ).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) => {
16895
+ const root = findProjectRoot57(opts.dir);
16896
+ const paths = resolveHaivePaths52(root);
16897
+ const minChanges = Math.max(1, parseInt(opts.minChanges ?? "3", 10) || 3);
16898
+ const limit = Math.max(1, parseInt(opts.limit ?? "20", 10) || 20);
16899
+ const days = Math.max(1, parseInt(opts.days ?? "90", 10) || 90);
16900
+ const radar = await buildRadar({
16901
+ root,
16902
+ taskTokens: null,
16903
+ filePaths: [],
16904
+ daysBack: Math.ceil(days / 6),
16905
+ // getHotFiles multiplies daysBack by 6
16906
+ maxHotFiles: 500
16907
+ });
16908
+ const hotFiles = radar.hotFiles.filter((h) => !isNoisePath(h.path)).map((h) => ({ path: h.path, changes: h.changes }));
16909
+ const memories = await loadMemoriesFromDir27(paths.memoriesDir);
16910
+ const gaps = findCoverageGaps(hotFiles, memories, { minChanges, limit });
16911
+ if (opts.json) {
16912
+ console.log(JSON.stringify({ root, scanned_hot_files: hotFiles.length, gaps }, null, 2));
16913
+ return;
16914
+ }
16915
+ if (!radar.insideGitRepo) {
16916
+ ui.warn("Not a git repository \u2014 coverage uses git churn to find hot files.");
16917
+ return;
16918
+ }
16919
+ if (gaps.length === 0) {
16920
+ ui.success(`No coverage gaps: every file changed \u2265${minChanges}\xD7 is covered by a team memory.`);
16921
+ return;
16922
+ }
16923
+ console.log(ui.bold(`hAIve coverage \u2014 ${gaps.length} blind spot(s) (hot files with no covering memory)`));
16924
+ for (const gap of gaps) {
16925
+ console.log(` ${ui.yellow("\u25CB")} ${gap.path} ${ui.dim(`(${gap.changes} change${gap.changes === 1 ? "" : "s"})`)}`);
16926
+ }
16927
+ console.log(
16928
+ ui.dim(
16929
+ "\nAdd a decision/convention/gotcha anchored to the top files, or a sensor, to close the gap."
16930
+ )
16931
+ );
16932
+ });
16933
+ }
16934
+
16935
+ // src/commands/merge-driver.ts
16936
+ import { execFileSync as execFileSync3 } from "child_process";
16937
+ import { readFileSync, writeFileSync, existsSync as existsSync80 } from "fs";
16938
+ import path56 from "path";
16939
+ import "commander";
16940
+ import { findProjectRoot as findProjectRoot58, mergeMemoryVersions } from "@hiveai/core";
16941
+ var GITATTRIBUTES_MARK = "# hAIve merge driver";
16942
+ var GITATTRIBUTES_BLOCK = [
16943
+ GITATTRIBUTES_MARK,
16944
+ ".ai/memories/**/*.md merge=haive",
16945
+ "# hAIve merge driver end"
16946
+ ].join("\n");
16947
+ function registerMergeDriver(program2) {
16948
+ const cmd = program2.command("merge-driver").description("Deterministic git merge driver for hAIve memory files (kills .ai/ conflict markers)");
16949
+ cmd.command("run <base> <ours> <theirs>").description("Git merge-driver entrypoint: resolve ours/theirs by frontmatter order, write into <ours>").action((base, ours, theirs) => {
16950
+ try {
16951
+ const oursContent = readFileSync(ours, "utf8");
16952
+ const theirsContent = readFileSync(theirs, "utf8");
16953
+ const result = mergeMemoryVersions(oursContent, theirsContent);
16954
+ if (result.content !== oursContent) writeFileSync(ours, result.content, "utf8");
16955
+ process.exit(0);
16956
+ } catch {
16957
+ process.exit(1);
16958
+ }
16959
+ });
16960
+ cmd.command("install").description("Configure git + .gitattributes so memory-file conflicts auto-resolve").option("-d, --dir <dir>", "project root").action((opts) => {
16961
+ const root = findProjectRoot58(opts.dir);
16962
+ try {
16963
+ execFileSync3("git", ["config", "merge.haive.name", "hAIve memory merge driver"], { cwd: root });
16964
+ execFileSync3("git", ["config", "merge.haive.driver", "haive merge-driver run %O %A %B"], { cwd: root });
16965
+ } catch {
16966
+ ui.error("Could not set git config \u2014 is this a git repository?");
16967
+ process.exitCode = 1;
16968
+ return;
16969
+ }
16970
+ const gaPath = path56.join(root, ".gitattributes");
16971
+ let content = existsSync80(gaPath) ? readFileSync(gaPath, "utf8") : "";
16972
+ if (!content.includes(GITATTRIBUTES_MARK)) {
16973
+ if (content.length > 0 && !content.endsWith("\n")) content += "\n";
16974
+ content += GITATTRIBUTES_BLOCK + "\n";
16975
+ writeFileSync(gaPath, content, "utf8");
16976
+ ui.success("Installed hAIve merge driver (git config + .gitattributes).");
16977
+ } else {
16978
+ ui.info("hAIve merge driver already present in .gitattributes \u2014 refreshed git config.");
16979
+ }
16980
+ ui.info("Memory-file conflicts under .ai/memories/ now resolve by revision_count \u2192 created_at.");
16981
+ });
16982
+ }
16983
+
16984
+ // src/commands/memory-resolve-conflict.ts
16985
+ import { writeFile as writeFile38 } from "fs/promises";
16986
+ import { existsSync as existsSync81 } from "fs";
16987
+ import "commander";
16988
+ import {
16989
+ findProjectRoot as findProjectRoot59,
16990
+ planConflictResolution,
16991
+ resolveHaivePaths as resolveHaivePaths53,
16992
+ serializeMemory as serializeMemory29
16993
+ } from "@hiveai/core";
16994
+ function registerMemoryResolveConflict(memory2) {
16995
+ 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) => {
16996
+ const root = findProjectRoot59(opts.dir);
16997
+ const paths = resolveHaivePaths53(root);
16998
+ if (!existsSync81(paths.memoriesDir)) {
16999
+ ui.error(`No .ai/memories at ${root}.`);
17000
+ process.exitCode = 1;
17001
+ return;
17002
+ }
17003
+ const memories = await loadMemoriesFromDir27(paths.memoriesDir);
17004
+ const a = memories.find((m) => m.memory.frontmatter.id === idA);
17005
+ const b = memories.find((m) => m.memory.frontmatter.id === idB);
17006
+ if (!a || !b) {
17007
+ ui.error(`Memory not found: ${!a ? idA : ""} ${!b ? idB : ""}`.trim());
17008
+ process.exitCode = 1;
17009
+ return;
17010
+ }
17011
+ const plan = planConflictResolution(a, b);
17012
+ const loser = plan.supersede_id === idA ? a : b;
17013
+ if (opts.json) {
17014
+ console.log(JSON.stringify({ ...plan, applied: Boolean(opts.yes) }, null, 2));
17015
+ } else {
17016
+ console.log(ui.bold("Conflict resolution"));
17017
+ console.log(` keep: ${ui.green(plan.keep_id)}`);
17018
+ console.log(` supersede: ${ui.red(plan.supersede_id)} ${ui.dim(`\u2192 deprecated`)}`);
17019
+ console.log(` reason: ${plan.reason}`);
17020
+ }
17021
+ if (!opts.yes) {
17022
+ if (!opts.json) ui.info("Preview only \u2014 re-run with --yes to apply.");
17023
+ return;
17024
+ }
17025
+ await writeFile38(
17026
+ loser.filePath,
17027
+ serializeMemory29({
17028
+ frontmatter: {
17029
+ ...loser.memory.frontmatter,
17030
+ status: "deprecated",
17031
+ stale_reason: plan.stale_reason,
17032
+ related_ids: [.../* @__PURE__ */ new Set([...loser.memory.frontmatter.related_ids, plan.keep_id])]
17033
+ },
17034
+ body: loser.memory.body
17035
+ }),
17036
+ "utf8"
17037
+ );
17038
+ if (!opts.json) ui.success(`Deprecated ${plan.supersede_id}; ${plan.keep_id} remains authoritative.`);
17039
+ });
17040
+ }
17041
+
17042
+ // src/commands/memory-seed-git.ts
17043
+ import { execFile as execFile4 } from "child_process";
17044
+ import { mkdir as mkdir24, writeFile as writeFile39 } from "fs/promises";
17045
+ import { existsSync as existsSync83 } from "fs";
17046
+ import path57 from "path";
17047
+ import { promisify as promisify4 } from "util";
17048
+ import "commander";
17049
+ import {
17050
+ buildFrontmatter as buildFrontmatter12,
17051
+ findProjectRoot as findProjectRoot60,
17052
+ memoryFilePath as memoryFilePath13,
17053
+ proposeSeedsFromCommits,
17054
+ resolveHaivePaths as resolveHaivePaths54,
17055
+ serializeMemory as serializeMemory30
17056
+ } from "@hiveai/core";
17057
+ var exec4 = promisify4(execFile4);
17058
+ function registerMemorySeedGit(memory2) {
17059
+ 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) => {
17060
+ const root = findProjectRoot60(opts.dir);
17061
+ const paths = resolveHaivePaths54(root);
17062
+ if (!existsSync83(paths.haiveDir)) {
17063
+ ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
17064
+ process.exitCode = 1;
17065
+ return;
17066
+ }
17067
+ const limit = Math.max(1, parseInt(opts.limit ?? "20", 10) || 20);
17068
+ const days = Math.max(1, parseInt(opts.days ?? "365", 10) || 365);
17069
+ const commits = await readCommits(root, days);
17070
+ const proposals = proposeSeedsFromCommits(commits, limit);
17071
+ if (opts.json) {
17072
+ console.log(JSON.stringify({ scanned_commits: commits.length, proposals, applied: Boolean(opts.apply) }, null, 2));
17073
+ } else if (proposals.length === 0) {
17074
+ ui.info("No revert/hotfix signals found in git history \u2014 nothing to seed.");
17075
+ return;
17076
+ } else {
17077
+ console.log(ui.bold(`hAIve seed-git \u2014 ${proposals.length} proposal(s) from ${commits.length} commit(s)`));
17078
+ for (const p of proposals) {
17079
+ console.log(` ${ui.yellow("\u25C6")} ${ui.dim(`[${p.kind}]`)} ${p.what}`);
17080
+ if (p.paths.length > 0) console.log(` ${ui.dim("paths:")} ${p.paths.join(", ")}`);
17081
+ }
17082
+ }
17083
+ if (!opts.apply) {
17084
+ if (!opts.json) ui.info("Preview only \u2014 re-run with --apply to write these as draft memories.");
17085
+ return;
17086
+ }
17087
+ let written = 0;
17088
+ for (const p of proposals) {
17089
+ const fm = {
17090
+ ...buildFrontmatter12({
17091
+ type: "attempt",
17092
+ slug: p.slug,
17093
+ scope: opts.scope ?? "team",
17094
+ tags: ["seed", "git-history", p.kind],
17095
+ paths: p.paths
17096
+ }),
17097
+ status: "draft"
17098
+ // human reviews before it becomes validated
17099
+ };
17100
+ const body = `# ${p.what}
17101
+
17102
+ **Why it failed / do NOT use:** ${p.why_failed}
17103
+
17104
+ _Seeded from git ${p.kind} commit ${p.source_sha}. Review and validate (or delete) \u2014 not yet authoritative._
17105
+ `;
17106
+ const file = memoryFilePath13(paths, fm.scope, fm.id, fm.module);
17107
+ if (existsSync83(file)) continue;
17108
+ await mkdir24(path57.dirname(file), { recursive: true });
17109
+ await writeFile39(file, serializeMemory30({ frontmatter: fm, body }), "utf8");
17110
+ written += 1;
17111
+ }
17112
+ if (!opts.json) {
17113
+ ui.success(`Wrote ${written} draft seed(s). Review them: \`haive memory pending\` \u2192 validate or delete.`);
17114
+ }
17115
+ });
17116
+ }
17117
+ async function readCommits(root, days) {
17118
+ try {
17119
+ const { stdout } = await exec4(
17120
+ "git",
17121
+ ["log", `--since=${days}.days.ago`, "--name-only", "--pretty=format:%x1f%h%x1f%s", "-n", "500"],
17122
+ { cwd: root, maxBuffer: 8 * 1024 * 1024 }
17123
+ );
17124
+ const blocks = stdout.split("").filter((b) => b.length > 0);
17125
+ const commits = [];
17126
+ for (let i = 0; i + 1 < blocks.length; i += 2) {
17127
+ const sha = blocks[i].trim();
17128
+ const tail = blocks[i + 1];
17129
+ const lines = tail.split("\n").map((l) => l.trim()).filter(Boolean);
17130
+ const subject = lines.shift() ?? "";
17131
+ commits.push({ sha, subject, files: lines });
17132
+ }
17133
+ return commits;
17134
+ } catch {
17135
+ return [];
17136
+ }
17137
+ }
17138
+
16637
17139
  // src/index.ts
16638
- var program = new Command58();
16639
- program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.13.9").option("--advanced", "show maintenance and experimental commands in help");
17140
+ var program = new Command63();
17141
+ program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.15.0").option("--advanced", "show maintenance and experimental commands in help");
16640
17142
  registerInit(program);
16641
17143
  registerWelcome(program);
16642
17144
  registerResolveProject(program);
@@ -16647,6 +17149,9 @@ registerAgent(program);
16647
17149
  registerSensors(program);
16648
17150
  registerIngest(program);
16649
17151
  registerDashboard(program);
17152
+ registerCoverage(program);
17153
+ registerMergeDriver(program);
17154
+ registerDevLink(program);
16650
17155
  registerMcp(program);
16651
17156
  registerBriefing(program);
16652
17157
  registerTui(program);
@@ -16676,6 +17181,8 @@ registerMemoryUpdate(memory);
16676
17181
  registerMemoryHot(memory);
16677
17182
  registerMemoryTried(memory);
16678
17183
  registerMemorySeed(memory);
17184
+ registerMemorySeedGit(memory);
17185
+ registerMemoryResolveConflict(memory);
16679
17186
  registerMemoryImport(memory);
16680
17187
  registerMemoryImportChangelog(memory);
16681
17188
  registerMemoryDigest(memory);