@hiveai/cli 0.14.0 → 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 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";
@@ -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(([path56, changes]) => ({ path: path56, 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.14.0"}`;
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.
@@ -7958,7 +7962,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
7958
7962
  };
7959
7963
  }
7960
7964
  var SERVER_NAME = "haive";
7961
- var SERVER_VERSION = "0.14.0";
7965
+ var SERVER_VERSION = "0.15.0";
7962
7966
  function jsonResult(data) {
7963
7967
  return {
7964
7968
  content: [
@@ -12709,9 +12713,12 @@ import "commander";
12709
12713
  import {
12710
12714
  aggregateRetrieval,
12711
12715
  aggregateSensors,
12716
+ appendEvalHistory,
12712
12717
  buildReport,
12713
12718
  compareEvalReports,
12719
+ computeEvalTrend,
12714
12720
  findProjectRoot as findProjectRoot42,
12721
+ loadEvalHistory,
12715
12722
  resolveHaivePaths as resolveHaivePaths38,
12716
12723
  scoreRetrievalCase,
12717
12724
  scoreSensorCase,
@@ -12720,7 +12727,7 @@ import {
12720
12727
  function registerEval(program2) {
12721
12728
  program2.command("eval").description(
12722
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."
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) => {
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) => {
12724
12731
  const root = findProjectRoot42(opts.dir);
12725
12732
  const paths = resolveHaivePaths38(root);
12726
12733
  if (!existsSync65(paths.memoriesDir)) {
@@ -12728,6 +12735,22 @@ function registerEval(program2) {
12728
12735
  process.exitCode = 1;
12729
12736
  return;
12730
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
+ }
12731
12754
  const k = Math.max(1, parseInt(opts.top ?? "8", 10) || 8);
12732
12755
  const ctx = { paths };
12733
12756
  const resolvedSpec = await resolveSpec(opts, root, paths.memoriesDir);
@@ -12755,6 +12778,17 @@ function registerEval(program2) {
12755
12778
  sensorAgg = aggregateSensors(results);
12756
12779
  }
12757
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
+ }
12758
12792
  const baselineFile = opts.baselineFile ? path43.isAbsolute(opts.baselineFile) ? opts.baselineFile : path43.join(root, opts.baselineFile) : path43.join(root, ".ai", "eval", "baseline.json");
12759
12793
  if (opts.baseline) {
12760
12794
  const snapshot = {
@@ -13529,7 +13563,7 @@ function registerDoctor(program2) {
13529
13563
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
13530
13564
  });
13531
13565
  }
13532
- findings.push(...await collectInstallFindings(root, "0.14.0"));
13566
+ findings.push(...await collectInstallFindings(root, "0.15.0"));
13533
13567
  findings.push(...await collectToolchainFindings(root));
13534
13568
  try {
13535
13569
  const legacyRaw = execSync3("haive-mcp --version", {
@@ -13537,7 +13571,7 @@ function registerDoctor(program2) {
13537
13571
  timeout: 3e3,
13538
13572
  stdio: ["ignore", "pipe", "ignore"]
13539
13573
  }).trim();
13540
- const cliVersion = "0.14.0";
13574
+ const cliVersion = "0.15.0";
13541
13575
  if (legacyRaw && legacyRaw !== cliVersion) {
13542
13576
  findings.push({
13543
13577
  severity: "warn",
@@ -14599,6 +14633,7 @@ import "commander";
14599
14633
  import {
14600
14634
  antiPatternGateParams as antiPatternGateParams2,
14601
14635
  findProjectRoot as findProjectRoot52,
14636
+ findUncapturedFailures,
14602
14637
  hasRecentBriefingMarker as hasRecentBriefingMarker2,
14603
14638
  isFreshIsoDate,
14604
14639
  loadConfig as loadConfig13,
@@ -14860,6 +14895,7 @@ async function buildFinishReport(dir) {
14860
14895
  }]
14861
14896
  });
14862
14897
  }
14898
+ findings.push(...await checkFailureCapture(paths, config));
14863
14899
  const status = await getGitSyncStatus(root);
14864
14900
  if (!status.available) {
14865
14901
  findings.push({
@@ -15023,6 +15059,47 @@ async function buildFinishReport(dir) {
15023
15059
  findings.push(...await verifyGithubActionsForHead(root, status));
15024
15060
  return finishReport(root, initialized, mode, findings, config);
15025
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
+ }
15026
15103
  function finishReport(root, initialized, mode, findings, config) {
15027
15104
  const score = buildScore(findings, config.enforcement?.scoreThreshold);
15028
15105
  const hasErrors = findings.some((f) => f.severity === "error");
@@ -15167,7 +15244,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
15167
15244
  findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
15168
15245
  });
15169
15246
  }
15170
- findings.push(...await inspectIntegrationVersions(root, "0.14.0"));
15247
+ findings.push(...await inspectIntegrationVersions(root, "0.15.0"));
15171
15248
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
15172
15249
  const hasBriefing = await hasRecentBriefingMarker2(paths, sessionId);
15173
15250
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
@@ -16163,12 +16240,14 @@ import {
16163
16240
  appendPreventionEvent as appendPreventionEvent2,
16164
16241
  findProjectRoot as findProjectRoot53,
16165
16242
  isRetiredMemory as isRetiredMemory3,
16243
+ loadConfig as loadConfig14,
16166
16244
  loadMemoriesFromDir as loadMemoriesFromDir39,
16167
16245
  loadUsageIndex as loadUsageIndex29,
16168
16246
  recordPrevention as recordPrevention2,
16169
16247
  resolveHaivePaths as resolveHaivePaths49,
16170
16248
  runSensors as runSensors2,
16171
16249
  saveUsageIndex as saveUsageIndex8,
16250
+ selectCommandSensors,
16172
16251
  sensorTargetsFromDiff as sensorTargetsFromDiff2,
16173
16252
  serializeMemory as serializeMemory27
16174
16253
  } from "@hiveai/core";
@@ -16198,14 +16277,36 @@ function registerSensors(program2) {
16198
16277
  });
16199
16278
  sensors.command("check").description(
16200
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."
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) => {
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) => {
16202
16281
  const root = findProjectRoot53(opts.dir);
16203
16282
  const paths = resolveHaivePaths49(root);
16204
16283
  const memories = await runnableSensorMemories(paths);
16205
16284
  const diff = opts.diffFile ? await readFile24(path53.resolve(root, opts.diffFile), "utf8") : await stagedDiff(root);
16206
16285
  const targets = sensorTargetsFromDiff2(diff);
16207
16286
  const hits = runSensors2(memories, targets.length > 0 ? targets : [{ path: "", content: diff }]);
16208
- 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))];
16209
16310
  if (firedIds.length > 0) {
16210
16311
  const usage = await loadUsageIndex29(paths);
16211
16312
  const recordedIds = [];
@@ -16228,12 +16329,15 @@ function registerSensors(program2) {
16228
16329
  severity: hit.severity,
16229
16330
  message: hit.message,
16230
16331
  matched_line: hit.matched_line
16231
- }))
16332
+ })),
16333
+ command_hits: commandHits,
16334
+ command_skipped: commandSkipped
16232
16335
  };
16233
16336
  if (opts.json) {
16234
16337
  console.log(JSON.stringify(output, null, 2));
16235
16338
  } else {
16236
- 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)`));
16237
16341
  for (const hit of hits) {
16238
16342
  const marker = hit.severity === "block" ? ui.red("\u2717") : ui.yellow("\u26A0");
16239
16343
  console.log(` ${marker} ${hit.memory_id} ${ui.dim(`(${hit.severity})`)}`);
@@ -16241,8 +16345,17 @@ function registerSensors(program2) {
16241
16345
  console.log(` ${hit.message}`);
16242
16346
  if (hit.matched_line) console.log(` ${ui.dim(hit.matched_line)}`);
16243
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
+ }
16244
16357
  }
16245
- if (hits.some((hit) => hit.severity === "block")) process.exitCode = 1;
16358
+ if ([...hits, ...commandHits].some((hit) => hit.severity === "block")) process.exitCode = 1;
16246
16359
  });
16247
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) => {
16248
16361
  const severity = opts.severity ?? "block";
@@ -16329,6 +16442,14 @@ async function runnableSensorMemories(paths, regexOnly = true) {
16329
16442
  return !isRetiredMemory3(memory2.frontmatter, memory2.body);
16330
16443
  });
16331
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
+ }
16332
16453
  async function stagedDiff(root) {
16333
16454
  try {
16334
16455
  const { stdout } = await exec2("git", ["diff", "--cached"], { cwd: root });
@@ -16562,6 +16683,7 @@ import "commander";
16562
16683
  import {
16563
16684
  buildDashboard,
16564
16685
  findProjectRoot as findProjectRoot55,
16686
+ loadConfig as loadConfig15,
16565
16687
  loadMemoriesFromDir as loadMemoriesFromDir41,
16566
16688
  loadPreventionEvents,
16567
16689
  loadUsageIndex as loadUsageIndex30,
@@ -16581,11 +16703,13 @@ function registerDashboard(program2) {
16581
16703
  const memories = existsSync78(paths.memoriesDir) ? await loadMemoriesFromDir41(paths.memoriesDir) : [];
16582
16704
  const usage = await loadUsageIndex30(paths);
16583
16705
  const preventionEvents = await loadPreventionEvents(paths);
16706
+ const config = await loadConfig15(paths);
16584
16707
  const top = Math.max(1, Number.parseInt(opts.top ?? "10", 10) || 10);
16585
16708
  const dormantDays = opts.dormantDays ? Number.parseInt(opts.dormantDays, 10) : void 0;
16586
16709
  const report = buildDashboard(memories, usage, {
16587
16710
  top,
16588
16711
  preventionEvents,
16712
+ antiPatternGate: config.enforcement?.antiPatternGate ?? "anchored",
16589
16713
  ...dormantDays !== void 0 && Number.isFinite(dormantDays) ? { dormantDays } : {}
16590
16714
  });
16591
16715
  if (opts.json) {
@@ -16596,7 +16720,7 @@ function registerDashboard(program2) {
16596
16720
  });
16597
16721
  }
16598
16722
  function renderDashboard(r) {
16599
- const { inventory: inv, impact, sensors, health, decay, corpus, prevention } = r;
16723
+ const { inventory: inv, impact, sensors, health, decay, corpus, prevention, gate_precision: gate } = r;
16600
16724
  console.log(ui.bold("hAIve dashboard"));
16601
16725
  console.log(
16602
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`
@@ -16647,6 +16771,15 @@ function renderDashboard(r) {
16647
16771
  }
16648
16772
  }
16649
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();
16650
16783
  console.log(ui.bold("Health"));
16651
16784
  console.log(
16652
16785
  ` stale ${warnNum(health.stale)} \xB7 anchorless ${warnNum(health.anchorless)} \xB7 pending ${health.pending} \xB7 prune candidates ${warnNum(health.prune_candidates)}`
@@ -16744,9 +16877,268 @@ function registerDevLink(program2) {
16744
16877
  });
16745
16878
  }
16746
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
+
16747
17139
  // 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");
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");
16750
17142
  registerInit(program);
16751
17143
  registerWelcome(program);
16752
17144
  registerResolveProject(program);
@@ -16757,6 +17149,8 @@ registerAgent(program);
16757
17149
  registerSensors(program);
16758
17150
  registerIngest(program);
16759
17151
  registerDashboard(program);
17152
+ registerCoverage(program);
17153
+ registerMergeDriver(program);
16760
17154
  registerDevLink(program);
16761
17155
  registerMcp(program);
16762
17156
  registerBriefing(program);
@@ -16787,6 +17181,8 @@ registerMemoryUpdate(memory);
16787
17181
  registerMemoryHot(memory);
16788
17182
  registerMemoryTried(memory);
16789
17183
  registerMemorySeed(memory);
17184
+ registerMemorySeedGit(memory);
17185
+ registerMemoryResolveConflict(memory);
16790
17186
  registerMemoryImport(memory);
16791
17187
  registerMemoryImportChangelog(memory);
16792
17188
  registerMemoryDigest(memory);