@hiveai/cli 0.20.1 → 0.23.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
@@ -208,7 +208,7 @@ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
208
208
  if (!f) continue;
209
209
  counts.set(f, (counts.get(f) ?? 0) + 1);
210
210
  }
211
- let entries = [...counts.entries()].map(([path60, changes]) => ({ path: path60, changes }));
211
+ let entries = [...counts.entries()].map(([path61, changes]) => ({ path: path61, changes }));
212
212
  const lowerPaths = filePaths.map((p) => p.toLowerCase());
213
213
  if (lowerPaths.length > 0) {
214
214
  entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
@@ -834,7 +834,7 @@ var TokenBudgetWriter = class {
834
834
  function registerBriefing(program2) {
835
835
  program2.command("briefing").description(
836
836
  'Print the full project briefing: last session recap + project context + relevant memories.\n Equivalent to calling get_briefing via MCP. Run before starting any task.\n\n Examples:\n haive briefing\n haive briefing --task "add Stripe payment" --files src/payments/PaymentService.ts\n haive briefing --budget quick --task "tiny fix"\n'
837
- ).option("--task <text>", "what you are about to do \u2014 filters memories by relevance").option("--files <csv>", "comma-separated file paths being worked on (surfaces anchored memories)").option("--symbols <csv>", "symbol names to look up in the code-map (e.g. PaymentService,TenantFilter) \u2014 requires haive index code").option("--max-memories <n>", "cap on memories surfaced", "8").option("--max-tokens <n>", "approximate token budget for the entire briefing (truncates if exceeded)").option("--explain-source", "annotate each memory with [source: <relative-path> \xB7 anchors: <files>] for traceable citations").option("--radar", "force project radar (recent commits, open TODOs, hot files) even when memories are plentiful").option("--no-radar", "disable the project radar even when memories are scarce").option(
837
+ ).option("--task <text>", "what you are about to do \u2014 filters memories by relevance").option("--files <csv>", "comma-separated file paths being worked on (surfaces anchored memories)").option("--symbols <csv>", "symbol names to look up in the code-map (e.g. PaymentService,TenantFilter) \u2014 requires haive index code").option("--max-memories <n>", "cap on memories surfaced", "8").option("--max-tokens <n>", "approximate token budget for the entire briefing (truncates if exceeded)").option("--explain-source", "annotate each memory with [source: <relative-path> \xB7 anchors: <files>] for traceable citations").option("--radar", "force project radar (recent commits, open TODOs, hot files) even when memories are plentiful").option("--no-radar", "disable the project radar even when memories are scarce").option("--json", "emit the ranked briefing as JSON (memories + scores + priority), like the MCP get_briefing tool", false).option(
838
838
  "--budget <preset>",
839
839
  "align with MCP get_briefing budget_preset: quick | balanced | deep \u2014 sets cap + truncation budget (overrides --max-memories / replaces default open-ended output)",
840
840
  void 0
@@ -897,8 +897,10 @@ function registerBriefing(program2) {
897
897
  budgetTokensCap = presetNums.max_tokens;
898
898
  maxMemories = presetNums.max_memories;
899
899
  }
900
+ const json = opts.json === true;
900
901
  const writer = budgetTokensCap !== null ? new TokenBudgetWriter(budgetTokensCap * CHARS_PER_TOKEN) : null;
901
902
  const out = (text) => {
903
+ if (json) return true;
902
904
  if (writer) return writer.write(text);
903
905
  console.log(text);
904
906
  return true;
@@ -1015,6 +1017,10 @@ function registerBriefing(program2) {
1015
1017
  scored.sort((a, b) => b.score - a.score);
1016
1018
  const top = scored.slice(0, maxMemories);
1017
1019
  if (top.length === 0) {
1020
+ if (json) {
1021
+ console.log(JSON.stringify({ task: opts.task ?? null, memories: [], briefing_quality: "thin" }, null, 2));
1022
+ return;
1023
+ }
1018
1024
  ui.info("No relevant memories found.");
1019
1025
  const draftCount = all.filter(
1020
1026
  (m) => m.memory.frontmatter.status === "draft" && (scopeFilter === "all" || m.memory.frontmatter.scope === scopeFilter)
@@ -1046,6 +1052,26 @@ function registerBriefing(program2) {
1046
1052
  const usefulCount = priorities.filter((p) => p === "useful").length;
1047
1053
  const backgroundCount = priorities.filter((p) => p === "background").length;
1048
1054
  const quality = mustReadCount > 0 || usefulCount > 0 ? backgroundCount > mustReadCount + usefulCount && backgroundCount > 2 ? "noisy" : "strong" : "thin";
1055
+ if (json) {
1056
+ console.log(JSON.stringify({
1057
+ task: opts.task ?? null,
1058
+ files: filePaths,
1059
+ briefing_quality: quality,
1060
+ counts: { must_read: mustReadCount, useful: usefulCount, background: backgroundCount },
1061
+ recap_id: recaps[0]?.memory.frontmatter.id ?? null,
1062
+ memories: top.map((item, i) => ({
1063
+ id: item.memory.frontmatter.id,
1064
+ scope: item.memory.frontmatter.scope,
1065
+ type: item.memory.frontmatter.type,
1066
+ status: item.memory.frontmatter.status,
1067
+ priority: priorities[i],
1068
+ score: item.score,
1069
+ file: path3.relative(root, item.filePath),
1070
+ summary: (item.memory.body.split("\n").map((l) => l.replace(/^#+\s*/, "").trim()).find((l) => l.length > 0) ?? "").slice(0, 140)
1071
+ }))
1072
+ }, null, 2));
1073
+ return;
1074
+ }
1049
1075
  out(ui.dim(`briefing_quality: ${quality} \xB7 must_read=${mustReadCount} useful=${usefulCount} background=${backgroundCount}`));
1050
1076
  out("");
1051
1077
  for (const [idx, item] of top.entries()) {
@@ -3339,7 +3365,13 @@ async function seedStackPack(haivePaths, stack) {
3339
3365
  });
3340
3366
  const filePath = memoryFilePath(haivePaths, "team", fm.id);
3341
3367
  if (existsSync10(filePath)) continue;
3342
- const content = serializeMemory2({ frontmatter: fm, body: `${mem.body}
3368
+ const ruleSlug = combinedSlug.startsWith(`${stack}-`) ? combinedSlug.slice(stack.length + 1) : combinedSlug;
3369
+ const titleCase = (s) => s.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
3370
+ const heading = `${titleCase(stack)}: ${titleCase(ruleSlug)}`;
3371
+ const titledBody = /^#{1,3}\s+\S/m.test(mem.body.trim()) ? mem.body : `# ${heading}
3372
+
3373
+ ${mem.body}`;
3374
+ const content = serializeMemory2({ frontmatter: fm, body: `${titledBody}
3343
3375
 
3344
3376
  ${SEED_FOOTER(stack)}` });
3345
3377
  await mkdir5(path10.dirname(filePath), { recursive: true });
@@ -3354,7 +3386,7 @@ ${SEED_FOOTER(stack)}` });
3354
3386
 
3355
3387
  // src/commands/init.ts
3356
3388
  var execFileAsync = promisify2(execFile2);
3357
- var HAIVE_GITHUB_ACTION_REF = `v${"0.20.1"}`;
3389
+ var HAIVE_GITHUB_ACTION_REF = `v${"0.23.0"}`;
3358
3390
  var PROJECT_CONTEXT_TEMPLATE = `# Project context
3359
3391
 
3360
3392
  > Generated by \`haive init\`. Run \`haive init --bootstrap\` to auto-fill from your codebase,
@@ -4710,7 +4742,6 @@ import { z as z25 } from "zod";
4710
4742
  import { existsSync as existsSync25 } from "fs";
4711
4743
  import {
4712
4744
  addedLinesFromDiff,
4713
- appendPreventionEvent,
4714
4745
  BRIDGE_TARGET_PATH,
4715
4746
  buildDocFrequency,
4716
4747
  CODE_STOPWORDS,
@@ -4722,9 +4753,8 @@ import {
4722
4753
  loadUsageIndex as loadUsageIndex10,
4723
4754
  literalMatchesAnyToken as literalMatchesAnyToken3,
4724
4755
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths4,
4725
- recordPrevention,
4756
+ recordPreventionHits,
4726
4757
  runSensors,
4727
- saveUsageIndex as saveUsageIndex4,
4728
4758
  sensorTargetsFromDiff,
4729
4759
  tokenizeQuery as tokenizeQuery3
4730
4760
  } from "@hiveai/core";
@@ -4771,7 +4801,8 @@ import { existsSync as existsSync30 } from "fs";
4771
4801
  import {
4772
4802
  findLexicalConflictPairs,
4773
4803
  findTopicStatusConflictPairs,
4774
- loadMemoriesFromDir as loadMemoriesFromDir23
4804
+ loadMemoriesFromDir as loadMemoriesFromDir23,
4805
+ planConflictResolution
4775
4806
  } from "@hiveai/core";
4776
4807
  import { z as z32 } from "zod";
4777
4808
  import { resolveProjectInfo } from "@hiveai/core";
@@ -5775,7 +5806,15 @@ async function memTried(input, ctx) {
5775
5806
  throw new Error(`Memory already exists at ${file}`);
5776
5807
  }
5777
5808
  await writeFile82(file, serializeMemory7({ frontmatter, body }), "utf8");
5778
- return { id: frontmatter.id, scope: frontmatter.scope, file_path: file };
5809
+ const sensorGenerated = Boolean(sensor);
5810
+ const hint = sensorGenerated ? void 0 : input.paths.length === 0 ? "No sensor was generated (no `paths` given), so this lesson is feedforward-only \u2014 it will be briefed but the gate cannot block the repeat. Re-run with `paths` set to the file(s) where the mistake lives to close the loop." : "No sensor could be derived from the wording (no distinctive code token). The lesson is briefed but not enforced; add a concrete forbidden token/value, or attach a sensor manually, to make the gate block the repeat.";
5811
+ return {
5812
+ id: frontmatter.id,
5813
+ scope: frontmatter.scope,
5814
+ file_path: file,
5815
+ sensor_generated: sensorGenerated,
5816
+ ...hint ? { hint } : {}
5817
+ };
5779
5818
  }
5780
5819
  var IngestFindingsInputSchema = {
5781
5820
  format: z16.enum(["sarif", "sonar"]).describe("Report format: 'sarif' (ESLint/Semgrep/CodeQL) or 'sonar' (SonarQube issues JSON)"),
@@ -7326,19 +7365,7 @@ async function antiPatternsCheck(input, ctx) {
7326
7365
  const strongCatches = warnings.filter(
7327
7366
  (w) => w.reasons.includes("sensor") || w.reasons.includes("anchor") && w.reasons.includes("literal")
7328
7367
  );
7329
- if (strongCatches.length > 0) {
7330
- const recordedIds = [];
7331
- for (const w of strongCatches) if (recordPrevention(usage, w.id)) recordedIds.push(w.id);
7332
- if (recordedIds.length > 0) {
7333
- await saveUsageIndex4(ctx.paths, usage).catch(() => {
7334
- });
7335
- const at = (/* @__PURE__ */ new Date()).toISOString();
7336
- for (const id of recordedIds) {
7337
- await appendPreventionEvent(ctx.paths, { at, id, source: "anti-pattern" }).catch(() => {
7338
- });
7339
- }
7340
- }
7341
- }
7368
+ await recordPreventionHits(ctx.paths, strongCatches.map((w) => w.id), "anti-pattern");
7342
7369
  return {
7343
7370
  scanned: negative.length,
7344
7371
  warnings
@@ -8256,6 +8283,18 @@ function gitFileDiff(root, file, sinceDays) {
8256
8283
  return null;
8257
8284
  }
8258
8285
  }
8286
+ function suggestResolution(byId, idA, idB) {
8287
+ const a = byId.get(idA);
8288
+ const b = byId.get(idB);
8289
+ if (!a || !b) return null;
8290
+ const plan = planConflictResolution(a, b);
8291
+ return {
8292
+ keep_id: plan.keep_id,
8293
+ supersede_id: plan.supersede_id,
8294
+ reason: plan.reason,
8295
+ command: `haive memory resolve-conflict ${plan.keep_id} ${plan.supersede_id} --yes`
8296
+ };
8297
+ }
8259
8298
  var MemConflictCandidatesInputSchema = {
8260
8299
  since_days: z32.number().int().positive().max(3650).default(365).describe("Only memories created since N days ago"),
8261
8300
  types: z32.array(z32.enum(["decision", "architecture", "convention", "gotcha"])).default(["decision", "architecture"]).describe("Memory types scanned for pairwise lexical overlap"),
@@ -8277,6 +8316,7 @@ async function memConflictCandidates(input, ctx) {
8277
8316
  };
8278
8317
  }
8279
8318
  const all = await loadMemoriesFromDir23(ctx.paths.memoriesDir);
8319
+ const byId = new Map(all.map((m) => [m.memory.frontmatter.id, m]));
8280
8320
  const { pairs, scanned, truncated } = findLexicalConflictPairs(all, {
8281
8321
  sinceDays: input.since_days,
8282
8322
  types: input.types,
@@ -8285,8 +8325,22 @@ async function memConflictCandidates(input, ctx) {
8285
8325
  maxScan: input.max_scan
8286
8326
  });
8287
8327
  const topicStatusPairs = findTopicStatusConflictPairs(all, input.max_topic_pairs);
8328
+ const enrichedPairs = pairs.map((p) => ({
8329
+ ...p,
8330
+ suggested_resolution: suggestResolution(byId, p.id_a, p.id_b)
8331
+ }));
8332
+ const enrichedTopicStatusPairs = topicStatusPairs.map((p) => ({
8333
+ ...p,
8334
+ suggested_resolution: suggestResolution(byId, p.id_a, p.id_b)
8335
+ }));
8288
8336
  const notice = pairs.length === 0 && topicStatusPairs.length === 0 ? "No lexical or topic-status candidates \u2014 widen since_days/types or lower min_jaccard." : void 0;
8289
- return { pairs, topic_status_pairs: topicStatusPairs, scanned, truncated, notice };
8337
+ return {
8338
+ pairs: enrichedPairs,
8339
+ topic_status_pairs: enrichedTopicStatusPairs,
8340
+ scanned,
8341
+ truncated,
8342
+ notice
8343
+ };
8290
8344
  }
8291
8345
  var MemResolveProjectInputSchema = {
8292
8346
  cwd: z33.string().optional().describe("Directory used for root discovery when HAIVE_PROJECT_ROOT is unset.")
@@ -8600,7 +8654,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
8600
8654
  };
8601
8655
  }
8602
8656
  var SERVER_NAME = "haive";
8603
- var SERVER_VERSION = "0.20.1";
8657
+ var SERVER_VERSION = "0.23.0";
8604
8658
  function jsonResult(data) {
8605
8659
  return {
8606
8660
  content: [
@@ -11134,7 +11188,13 @@ function registerMemoryTried(memory2) {
11134
11188
  await writeFile21(file, serializeMemory19({ frontmatter, body }), "utf8");
11135
11189
  ui.success(`Recorded: ${path27.relative(root, file)}`);
11136
11190
  ui.info(`id=${frontmatter.id} type=attempt status=validated (auto-approved)`);
11137
- if (sensor) ui.info(`sensor=regex warn autogen pattern=${JSON.stringify(sensor.pattern)}`);
11191
+ if (sensor) {
11192
+ ui.info(`sensor=regex warn autogen pattern=${JSON.stringify(sensor.pattern)}`);
11193
+ } else {
11194
+ ui.warn(
11195
+ frontmatter.anchor.paths.length === 0 ? "No sensor generated (no --paths) \u2014 lesson is briefed but NOT enforced. Add --paths to close the loop." : "No sensor could be derived from the wording \u2014 lesson is briefed but NOT enforced. Use a concrete forbidden token to enable a gate block."
11196
+ );
11197
+ }
11138
11198
  });
11139
11199
  }
11140
11200
  function parseCsv4(value) {
@@ -11394,7 +11454,7 @@ import {
11394
11454
  loadUsageIndex as loadUsageIndex18,
11395
11455
  recordRejection as recordRejection3,
11396
11456
  resolveHaivePaths as resolveHaivePaths23,
11397
- saveUsageIndex as saveUsageIndex5,
11457
+ saveUsageIndex as saveUsageIndex4,
11398
11458
  serializeMemory as serializeMemory20
11399
11459
  } from "@hiveai/core";
11400
11460
  function registerMemoryReject(memory2) {
@@ -11427,7 +11487,7 @@ function registerMemoryReject(memory2) {
11427
11487
  );
11428
11488
  const idx = await loadUsageIndex18(paths);
11429
11489
  recordRejection3(idx, id, opts.reason ?? null);
11430
- await saveUsageIndex5(paths, idx);
11490
+ await saveUsageIndex4(paths, idx);
11431
11491
  const u = idx.by_id[id];
11432
11492
  ui.success(
11433
11493
  `Rejected ${id} (status=rejected, ${u.rejected_count} rejection${u.rejected_count === 1 ? "" : "s"})`
@@ -11446,7 +11506,7 @@ import {
11446
11506
  findProjectRoot as findProjectRoot27,
11447
11507
  loadUsageIndex as loadUsageIndex19,
11448
11508
  resolveHaivePaths as resolveHaivePaths24,
11449
- saveUsageIndex as saveUsageIndex6
11509
+ saveUsageIndex as saveUsageIndex5
11450
11510
  } from "@hiveai/core";
11451
11511
  function registerMemoryRm(memory2) {
11452
11512
  memory2.command("delete <id>").alias("rm").description("Delete a memory file (and its usage entry by default). Mirrors MCP mem_delete. Alias: rm").option("-y, --yes", "skip the confirmation prompt").option("--keep-usage", "do not remove the usage.json entry").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
@@ -11480,7 +11540,7 @@ function registerMemoryRm(memory2) {
11480
11540
  const idx = await loadUsageIndex19(paths);
11481
11541
  if (idx.by_id[id]) {
11482
11542
  delete idx.by_id[id];
11483
- await saveUsageIndex6(paths, idx);
11543
+ await saveUsageIndex5(paths, idx);
11484
11544
  ui.info("Removed usage entry");
11485
11545
  }
11486
11546
  }
@@ -11693,7 +11753,7 @@ import {
11693
11753
  recordRejection as recordRejection4,
11694
11754
  recommendFeedbackAdjustment as recommendFeedbackAdjustment2,
11695
11755
  resolveHaivePaths as resolveHaivePaths28,
11696
- saveUsageIndex as saveUsageIndex7,
11756
+ saveUsageIndex as saveUsageIndex6,
11697
11757
  serializeMemory as serializeMemory21
11698
11758
  } from "@hiveai/core";
11699
11759
  function registerMemoryFeedback(memory2) {
@@ -11723,7 +11783,7 @@ function registerMemoryFeedback(memory2) {
11723
11783
  const outcome = opts.applied ? "applied" : "rejected";
11724
11784
  if (opts.applied) recordApplied2(index, id);
11725
11785
  else recordRejection4(index, id, opts.reason ?? null);
11726
- await saveUsageIndex7(paths, index);
11786
+ await saveUsageIndex6(paths, index);
11727
11787
  const usage = getUsage19(index, id);
11728
11788
  const adjustment = opts.rejected ? recommendFeedbackAdjustment2(target.memory.frontmatter, usage) : { action: "none", reason: "No automatic adjustment needed." };
11729
11789
  const adjustedFrontmatter = applyFeedbackAdjustment2(target.memory.frontmatter, adjustment);
@@ -13554,6 +13614,7 @@ function registerEval(program2) {
13554
13614
  root,
13555
13615
  k,
13556
13616
  spec_source: resolvedSpec.source,
13617
+ provenance: { synthesized: resolvedSpec.synthesized, authored: resolvedSpec.authored },
13557
13618
  report,
13558
13619
  gate_precision: gatePrecision,
13559
13620
  ...delta ? { delta } : {},
@@ -13562,13 +13623,18 @@ function registerEval(program2) {
13562
13623
  applyExitGates(opts, report, delta, gatePrecision, gateDelta);
13563
13624
  return;
13564
13625
  }
13626
+ if (resolvedSpec.authored === 0 && resolvedSpec.synthesized > 0) {
13627
+ ui.warn(
13628
+ `All ${resolvedSpec.synthesized} case(s) are self-synthesized from your own memories (self-referential). Add hand-labeled cases in .ai/eval/spec.json, or run \`haive eval --spec <file>\`, for an independent score.`
13629
+ );
13630
+ }
13565
13631
  if (delta) {
13566
13632
  console.log(renderDelta(delta));
13567
13633
  }
13568
13634
  if (gateDelta) {
13569
13635
  console.log(renderGateDelta(gateDelta));
13570
13636
  }
13571
- const md = renderMarkdown2(root, k, resolvedSpec.source, report, gatePrecision);
13637
+ const md = renderMarkdown2(root, k, resolvedSpec, report, gatePrecision);
13572
13638
  if (opts.out) {
13573
13639
  const outFile = path44.isAbsolute(opts.out) ? opts.out : path44.join(root, opts.out);
13574
13640
  await writeFile33(outFile, md, "utf8");
@@ -13651,30 +13717,39 @@ function renderGateDelta(delta) {
13651
13717
  lines.push(` rejections ${delta.rejections.baseline ?? "n/a"} \u2192 ${delta.rejections.current ?? "n/a"} ${ui.dim(`(${rejectionDelta})`)}`);
13652
13718
  return lines.join("\n");
13653
13719
  }
13720
+ function countCases(spec) {
13721
+ return (spec.retrieval?.length ?? 0) + (spec.sensors?.length ?? 0);
13722
+ }
13654
13723
  async function resolveSpec(opts, root, memoriesDir) {
13655
13724
  if (opts.spec) {
13656
13725
  const file = path44.resolve(opts.spec);
13657
13726
  const raw = await readFile21(file, "utf8");
13658
- return { spec: JSON.parse(raw), source: file };
13727
+ const spec = JSON.parse(raw);
13728
+ return { spec, source: file, synthesized: 0, authored: countCases(spec) };
13659
13729
  }
13660
13730
  const defaultSpec = path44.join(root, ".ai", "eval", "spec.json");
13661
13731
  if (existsSync65(defaultSpec)) {
13662
13732
  const raw = await readFile21(defaultSpec, "utf8");
13663
13733
  const explicit = JSON.parse(raw);
13664
13734
  const memories2 = await loadMemoriesFromDir27(memoriesDir);
13665
- const synthesized = synthesizeSelfEvalCases(memories2, { includeFiles: !opts.semanticOnly });
13735
+ const synthesized2 = synthesizeSelfEvalCases(memories2, { includeFiles: !opts.semanticOnly });
13666
13736
  return {
13667
13737
  spec: {
13668
- retrieval: [...synthesized, ...explicit.retrieval ?? []],
13738
+ retrieval: [...synthesized2, ...explicit.retrieval ?? []],
13669
13739
  sensors: explicit.sensors ?? []
13670
13740
  },
13671
- source: ".ai/eval/spec.json + synthesized anchored retrieval"
13741
+ source: ".ai/eval/spec.json + synthesized anchored retrieval",
13742
+ synthesized: synthesized2.length,
13743
+ authored: countCases(explicit)
13672
13744
  };
13673
13745
  }
13674
13746
  const memories = await loadMemoriesFromDir27(memoriesDir);
13747
+ const synthesized = synthesizeSelfEvalCases(memories, { includeFiles: !opts.semanticOnly });
13675
13748
  return {
13676
- spec: { retrieval: synthesizeSelfEvalCases(memories, { includeFiles: !opts.semanticOnly }) },
13677
- source: "synthesized anchored retrieval"
13749
+ spec: { retrieval: synthesized },
13750
+ source: "synthesized anchored retrieval",
13751
+ synthesized: synthesized.length,
13752
+ authored: 0
13678
13753
  };
13679
13754
  }
13680
13755
  async function runRetrieval(c, k, ctx) {
@@ -13707,12 +13782,14 @@ async function runSensorCase(c, ctx) {
13707
13782
  function pct(n) {
13708
13783
  return `${Math.round(n * 100)}%`;
13709
13784
  }
13710
- function renderMarkdown2(root, k, source, report, gatePrecision) {
13785
+ function renderMarkdown2(root, k, resolved, report, gatePrecision) {
13786
+ const provenance = resolved.authored === 0 ? `${resolved.synthesized} synthesized (self-referential \u2014 sanity floor, not ground truth)` : resolved.synthesized === 0 ? `${resolved.authored} authored (independent ground truth)` : `${resolved.authored} authored (independent) + ${resolved.synthesized} synthesized (self-referential)`;
13711
13787
  const lines = [
13712
13788
  "# hAIve eval report",
13713
13789
  "",
13714
13790
  `Project: \`${root}\` \xB7 top-k: ${k}`,
13715
- `Spec: ${source}`,
13791
+ `Spec: ${resolved.source}`,
13792
+ `Cases: ${provenance}`,
13716
13793
  "",
13717
13794
  `## Overall score: ${report.score}/100`,
13718
13795
  ""
@@ -14153,8 +14230,8 @@ function registerDoctor(program2) {
14153
14230
  fix: "haive init"
14154
14231
  });
14155
14232
  } else {
14156
- const { readFile: readFile28 } = await import("fs/promises");
14157
- const content = await readFile28(paths.projectContext, "utf8");
14233
+ const { readFile: readFile29 } = await import("fs/promises");
14234
+ const content = await readFile29(paths.projectContext, "utf8");
14158
14235
  const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
14159
14236
  if (isTemplate) {
14160
14237
  findings.push({
@@ -14328,8 +14405,8 @@ function registerDoctor(program2) {
14328
14405
  let hasClaudeEnforcement = false;
14329
14406
  if (existsSync68(claudeSettings)) {
14330
14407
  try {
14331
- const { readFile: readFile28 } = await import("fs/promises");
14332
- const raw = await readFile28(claudeSettings, "utf8");
14408
+ const { readFile: readFile29 } = await import("fs/promises");
14409
+ const raw = await readFile29(claudeSettings, "utf8");
14333
14410
  hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
14334
14411
  } catch {
14335
14412
  hasClaudeEnforcement = false;
@@ -14352,7 +14429,7 @@ function registerDoctor(program2) {
14352
14429
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
14353
14430
  });
14354
14431
  }
14355
- findings.push(...await collectInstallFindings(root, "0.20.1"));
14432
+ findings.push(...await collectInstallFindings(root, "0.23.0"));
14356
14433
  findings.push(...await collectToolchainFindings(root));
14357
14434
  try {
14358
14435
  const legacyRaw = execSync3("haive-mcp --version", {
@@ -14360,7 +14437,7 @@ function registerDoctor(program2) {
14360
14437
  timeout: 3e3,
14361
14438
  stdio: ["ignore", "pipe", "ignore"]
14362
14439
  }).trim();
14363
- const cliVersion = "0.20.1";
14440
+ const cliVersion = "0.23.0";
14364
14441
  if (legacyRaw && legacyRaw !== cliVersion) {
14365
14442
  findings.push({
14366
14443
  severity: "warn",
@@ -15433,10 +15510,11 @@ function registerMemoryConflictCandidates(memory2) {
15433
15510
  }
15434
15511
 
15435
15512
  // src/commands/enforce.ts
15436
- import { execFileSync as execFileSync2, spawn as spawn6 } from "child_process";
15513
+ import { execFile as execFile3, execFileSync as execFileSync2, spawn as spawn6 } from "child_process";
15437
15514
  import { existsSync as existsSync75, statSync as statSync3 } from "fs";
15438
15515
  import { chmod as chmod2, mkdir as mkdir21, readFile as readFile24, readdir as readdir6, rm as rm3, writeFile as writeFile37 } from "fs/promises";
15439
15516
  import path53 from "path";
15517
+ import { promisify as promisify3 } from "util";
15440
15518
  import "commander";
15441
15519
  import {
15442
15520
  antiPatternGateParams as antiPatternGateParams2,
@@ -15450,15 +15528,18 @@ import {
15450
15528
  loadMemoriesFromDir as loadMemoriesFromDir38,
15451
15529
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths6,
15452
15530
  readRecentBriefingMarker,
15531
+ recordPreventionHits as recordPreventionHits2,
15453
15532
  resolveBriefingBudget as resolveBriefingBudget3,
15454
15533
  resolveHaivePaths as resolveHaivePaths48,
15455
15534
  runSensors as runSensors2,
15456
15535
  saveConfig as saveConfig4,
15536
+ selectCommandSensors,
15457
15537
  sensorTargetsFromDiff as sensorTargetsFromDiff2,
15458
15538
  SESSION_RECAP_TTL_MS,
15459
15539
  verifyAnchor as verifyAnchor4,
15460
15540
  writeBriefingMarker as writeBriefingMarker3
15461
15541
  } from "@hiveai/core";
15542
+ var execFileAsync2 = promisify3(execFile3);
15462
15543
  var MAX_STDIN_BYTES2 = 256 * 1024;
15463
15544
  var ENFORCE_HOOK_MARKER = "# hAIve enforcement hook";
15464
15545
  function registerEnforce(program2) {
@@ -16060,7 +16141,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
16060
16141
  findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
16061
16142
  });
16062
16143
  }
16063
- findings.push(...await inspectIntegrationVersions(root, "0.20.1"));
16144
+ findings.push(...await inspectIntegrationVersions(root, "0.23.0"));
16064
16145
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
16065
16146
  const hasBriefing = await hasRecentBriefingMarker2(paths, sessionId);
16066
16147
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
@@ -16221,6 +16302,26 @@ async function verifyDecisionCoverage(paths, stage, sessionId) {
16221
16302
  message: `Relevant decisions/policies were surfaced for ${changedFiles.length} changed file(s): ${relevant.length}/${relevant.length}.`
16222
16303
  }];
16223
16304
  }
16305
+ if (stage === "pre-commit" || stage === "pre-push") {
16306
+ const cfg = await loadConfig14(paths).catch(() => ({}));
16307
+ if (cfg.enforcement?.autoBrief !== false) {
16308
+ await writeBriefingMarker3(paths, {
16309
+ sessionId,
16310
+ source: "haive-autobrief",
16311
+ task: "decision-coverage auto-surfaced at commit",
16312
+ memoryIds: relevant.map(({ memory: memory2 }) => memory2.frontmatter.id),
16313
+ files: changedFiles
16314
+ }).catch(() => {
16315
+ });
16316
+ return [{
16317
+ severity: "ok",
16318
+ code: "decision-coverage-autosurfaced",
16319
+ message: `Surfaced ${relevant.length} relevant decision/policy memor${relevant.length === 1 ? "y" : "ies"} for ${changedFiles.length} changed file(s) at commit time` + (missing.length > 0 ? ` (${missing.length} not previously briefed \u2014 now recorded)` : "") + ". Set enforcement.autoBrief=false to require a manual briefing first.",
16320
+ memory_ids: relevant.slice(0, 12).map(({ memory: memory2 }) => memory2.frontmatter.id),
16321
+ affected_files: changedFiles.slice(0, 10)
16322
+ }];
16323
+ }
16324
+ }
16224
16325
  return [{
16225
16326
  severity: stage === "local" ? "warn" : "error",
16226
16327
  code: "decision-coverage-missing",
@@ -16292,19 +16393,19 @@ async function runSensorGate(paths, diff) {
16292
16393
  if (!diff || !existsSync75(paths.memoriesDir)) return [];
16293
16394
  try {
16294
16395
  const loaded = await loadMemoriesFromDir38(paths.memoriesDir);
16295
- const sensorMemories = loaded.map((l) => l.memory).filter((m) => {
16296
- const s = m.frontmatter.sensor;
16297
- return Boolean(s) && s.kind === "regex" && !isRetiredMemory3(m.frontmatter, m.body);
16298
- });
16299
- if (sensorMemories.length === 0) return [];
16396
+ const scannable = loaded.map((l) => l.memory).filter((m) => Boolean(m.frontmatter.sensor) && !isRetiredMemory3(m.frontmatter, m.body));
16397
+ if (scannable.length === 0) return [];
16300
16398
  const targets = sensorTargetsFromDiff2(diff).filter((t) => isSensorScannablePath(t.path));
16301
16399
  if (targets.length === 0) return [];
16302
- const hits = runSensors2(sensorMemories, targets);
16303
16400
  const findings = [];
16304
16401
  const seen = /* @__PURE__ */ new Set();
16402
+ const firedIds = /* @__PURE__ */ new Set();
16403
+ const regexSensorMemories = scannable.filter((m) => m.frontmatter.sensor.kind === "regex");
16404
+ const hits = regexSensorMemories.length > 0 ? runSensors2(regexSensorMemories, targets) : [];
16305
16405
  for (const hit of hits) {
16306
16406
  if (seen.has(hit.memory_id)) continue;
16307
16407
  seen.add(hit.memory_id);
16408
+ firedIds.add(hit.memory_id);
16308
16409
  const where = hit.file ? ` (${hit.file})` : "";
16309
16410
  if (hit.severity === "block") {
16310
16411
  findings.push({
@@ -16324,11 +16425,51 @@ async function runSensorGate(paths, diff) {
16324
16425
  });
16325
16426
  }
16326
16427
  }
16428
+ const config = await loadConfig14(paths).catch(() => null);
16429
+ if (config?.enforcement?.runCommandSensors === true) {
16430
+ const changedPaths = targets.map((t) => t.path).filter(Boolean);
16431
+ for (const spec of selectCommandSensors(scannable, changedPaths)) {
16432
+ if (seen.has(spec.memory_id)) continue;
16433
+ const failed = await runGateCommandSensor(spec, paths.root);
16434
+ if (!failed) continue;
16435
+ seen.add(spec.memory_id);
16436
+ firedIds.add(spec.memory_id);
16437
+ if (spec.severity === "block") {
16438
+ findings.push({
16439
+ severity: "error",
16440
+ code: "sensor-block",
16441
+ message: `Block command sensor fired \u2014 ${spec.memory_id}: ${spec.message} (command: ${spec.command})`,
16442
+ fix: "Fix the condition the command checks, or run `haive sensors check --commands` to inspect it.",
16443
+ impact: 45
16444
+ });
16445
+ } else {
16446
+ findings.push({
16447
+ severity: "warn",
16448
+ code: "sensor-warn",
16449
+ message: `Command sensor flagged ${spec.memory_id}: ${spec.message}`,
16450
+ fix: "Review the failing command; `haive sensors check --commands` re-runs it.",
16451
+ impact: 5
16452
+ });
16453
+ }
16454
+ }
16455
+ }
16456
+ if (firedIds.size > 0) {
16457
+ await recordPreventionHits2(paths, [...firedIds], "sensor").catch(() => {
16458
+ });
16459
+ }
16327
16460
  return findings;
16328
16461
  } catch {
16329
16462
  return [];
16330
16463
  }
16331
16464
  }
16465
+ async function runGateCommandSensor(spec, root) {
16466
+ try {
16467
+ await execFileAsync2("bash", ["-c", spec.command], { cwd: root, timeout: 12e4, maxBuffer: 8 * 1024 * 1024 });
16468
+ return false;
16469
+ } catch {
16470
+ return true;
16471
+ }
16472
+ }
16332
16473
  async function findGeneratedArtifacts(paths) {
16333
16474
  const dirty = await runCommand4("git", ["status", "--short", "--untracked-files=all"], paths.root).catch(() => "");
16334
16475
  const generated = dirty.split("\n").map((line) => line.trim()).filter(Boolean).filter(
@@ -17114,28 +17255,25 @@ function registerRun(program2) {
17114
17255
  }
17115
17256
 
17116
17257
  // src/commands/sensors.ts
17117
- import { execFile as execFile3 } from "child_process";
17258
+ import { execFile as execFile4 } from "child_process";
17118
17259
  import { existsSync as existsSync76 } from "fs";
17119
17260
  import { chmod as chmod3, mkdir as mkdir23, readFile as readFile25, writeFile as writeFile38 } from "fs/promises";
17120
17261
  import path54 from "path";
17121
- import { promisify as promisify3 } from "util";
17262
+ import { promisify as promisify4 } from "util";
17122
17263
  import "commander";
17123
17264
  import {
17124
- appendPreventionEvent as appendPreventionEvent2,
17125
17265
  findProjectRoot as findProjectRoot53,
17126
17266
  isRetiredMemory as isRetiredMemory4,
17127
17267
  loadConfig as loadConfig15,
17128
17268
  loadMemoriesFromDir as loadMemoriesFromDir39,
17129
- loadUsageIndex as loadUsageIndex31,
17130
- recordPrevention as recordPrevention2,
17269
+ recordPreventionHits as recordPreventionHits3,
17131
17270
  resolveHaivePaths as resolveHaivePaths49,
17132
17271
  runSensors as runSensors3,
17133
- saveUsageIndex as saveUsageIndex8,
17134
- selectCommandSensors,
17272
+ selectCommandSensors as selectCommandSensors2,
17135
17273
  sensorTargetsFromDiff as sensorTargetsFromDiff3,
17136
17274
  serializeMemory as serializeMemory29
17137
17275
  } from "@hiveai/core";
17138
- var exec2 = promisify3(execFile3);
17276
+ var exec2 = promisify4(execFile4);
17139
17277
  function registerSensors(program2) {
17140
17278
  const sensors = program2.command("sensors").description("Operate executable sensors derived from hAIve memories");
17141
17279
  sensors.command("list").description("List memories carrying executable sensors").option("--json", "emit JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
@@ -17172,7 +17310,7 @@ function registerSensors(program2) {
17172
17310
  const runCommands = opts.commands || config.enforcement?.runCommandSensors === true;
17173
17311
  const changedPaths = targets.map((t) => t.path).filter(Boolean);
17174
17312
  const allSensorMemories = await runnableSensorMemories(paths, false);
17175
- const commandSpecs = selectCommandSensors(allSensorMemories, changedPaths);
17313
+ const commandSpecs = selectCommandSensors2(allSensorMemories, changedPaths);
17176
17314
  const commandHits = [];
17177
17315
  const commandSkipped = [];
17178
17316
  if (commandSpecs.length > 0 && runCommands) {
@@ -17191,20 +17329,7 @@ function registerSensors(program2) {
17191
17329
  for (const spec of commandSpecs) commandSkipped.push(spec.memory_id);
17192
17330
  }
17193
17331
  const firedIds = [...new Set([...hits, ...commandHits].map((hit) => hit.memory_id))];
17194
- if (firedIds.length > 0) {
17195
- const usage = await loadUsageIndex31(paths);
17196
- const recordedIds = [];
17197
- for (const id of firedIds) if (recordPrevention2(usage, id)) recordedIds.push(id);
17198
- if (recordedIds.length > 0) {
17199
- await saveUsageIndex8(paths, usage).catch(() => {
17200
- });
17201
- const at = (/* @__PURE__ */ new Date()).toISOString();
17202
- for (const id of recordedIds) {
17203
- await appendPreventionEvent2(paths, { at, id, source: "sensor" }).catch(() => {
17204
- });
17205
- }
17206
- }
17207
- }
17332
+ await recordPreventionHits3(paths, firedIds, "sensor");
17208
17333
  const output = {
17209
17334
  scanned: memories.length,
17210
17335
  hits: hits.map((hit) => ({
@@ -17571,7 +17696,7 @@ import {
17571
17696
  loadConfig as loadConfig16,
17572
17697
  loadMemoriesFromDir as loadMemoriesFromDir41,
17573
17698
  loadPreventionEvents as loadPreventionEvents4,
17574
- loadUsageIndex as loadUsageIndex33,
17699
+ loadUsageIndex as loadUsageIndex31,
17575
17700
  resolveHaivePaths as resolveHaivePaths51
17576
17701
  } from "@hiveai/core";
17577
17702
  function registerDashboard(program2) {
@@ -17586,7 +17711,7 @@ function registerDashboard(program2) {
17586
17711
  return;
17587
17712
  }
17588
17713
  const memories = existsSync78(paths.memoriesDir) ? await loadMemoriesFromDir41(paths.memoriesDir) : [];
17589
- const usage = await loadUsageIndex33(paths);
17714
+ const usage = await loadUsageIndex31(paths);
17590
17715
  const preventionEvents = await loadPreventionEvents4(paths);
17591
17716
  const config = await loadConfig16(paths);
17592
17717
  const top = Math.max(1, Number.parseInt(opts.top ?? "10", 10) || 10);
@@ -17697,14 +17822,14 @@ function warnNum(n) {
17697
17822
  }
17698
17823
 
17699
17824
  // src/commands/dev-link.ts
17700
- import { execFile as execFile4 } from "child_process";
17825
+ import { execFile as execFile5 } from "child_process";
17701
17826
  import { cp, readFile as readFile27 } from "fs/promises";
17702
17827
  import { existsSync as existsSync79 } from "fs";
17703
17828
  import path56 from "path";
17704
- import { promisify as promisify4 } from "util";
17829
+ import { promisify as promisify5 } from "util";
17705
17830
  import "commander";
17706
17831
  import { findProjectRoot as findProjectRoot56 } from "@hiveai/core";
17707
- var exec3 = promisify4(execFile4);
17832
+ var exec3 = promisify5(execFile5);
17708
17833
  function registerDevLink(program2) {
17709
17834
  const dev = program2.commands.find((c) => c.name() === "dev") ?? program2.command("dev").description("Developer utilities for working on hAIve itself.");
17710
17835
  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) => {
@@ -17760,8 +17885,41 @@ function registerDevLink(program2) {
17760
17885
  }
17761
17886
 
17762
17887
  // src/commands/coverage.ts
17888
+ import { readFile as readFile28 } from "fs/promises";
17889
+ import { existsSync as existsSync80 } from "fs";
17890
+ import path57 from "path";
17763
17891
  import "commander";
17764
- import { findCoverageGaps, findProjectRoot as findProjectRoot57, resolveHaivePaths as resolveHaivePaths52 } from "@hiveai/core";
17892
+ import {
17893
+ findCoverageGaps,
17894
+ findProjectRoot as findProjectRoot57,
17895
+ mergeHotFiles,
17896
+ resolveHaivePaths as resolveHaivePaths52,
17897
+ tallyHotFiles
17898
+ } from "@hiveai/core";
17899
+ async function readAgentHotFiles(root, cacheFile, sinceMs) {
17900
+ if (!existsSync80(cacheFile)) return [];
17901
+ const raw = await readFile28(cacheFile, "utf8").catch(() => "");
17902
+ const files = [];
17903
+ for (const line of raw.split("\n")) {
17904
+ const trimmed = line.trim();
17905
+ if (!trimmed) continue;
17906
+ try {
17907
+ const obs = JSON.parse(trimmed);
17908
+ if (sinceMs > 0 && obs.ts) {
17909
+ const t = Date.parse(obs.ts);
17910
+ if (Number.isFinite(t) && t < sinceMs) continue;
17911
+ }
17912
+ for (const f of obs.files ?? []) {
17913
+ if (typeof f !== "string" || !f) continue;
17914
+ const rel = path57.isAbsolute(f) ? path57.relative(root, f) : f;
17915
+ if (rel.startsWith("..")) continue;
17916
+ files.push(rel);
17917
+ }
17918
+ } catch {
17919
+ }
17920
+ }
17921
+ return tallyHotFiles(files, "agent");
17922
+ }
17765
17923
  function isNoisePath(p) {
17766
17924
  if (/(^|\/)(node_modules|dist|build|coverage|\.next)\//.test(p)) return true;
17767
17925
  if (p.startsWith(".ai/")) return true;
@@ -17773,29 +17931,47 @@ function isNoisePath(p) {
17773
17931
  function registerCoverage(program2) {
17774
17932
  program2.command("coverage").description(
17775
17933
  "Coverage-gap report: frequently-edited files with no covering team memory (blind spots)."
17776
- ).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) => {
17934
+ ).option("--json", "emit JSON", false).option("--min-changes <n>", "minimum churn count to flag a file", "3").option("--limit <n>", "max gaps to report", "20").option("--days <n>", "lookback window in days (git history + agent edits)", "90").option("--source <which>", "heat source: git | agent | both", "both").option("-d, --dir <dir>", "project root").action(async (opts) => {
17777
17935
  const root = findProjectRoot57(opts.dir);
17778
17936
  const paths = resolveHaivePaths52(root);
17779
17937
  const minChanges = Math.max(1, parseInt(opts.minChanges ?? "3", 10) || 3);
17780
17938
  const limit = Math.max(1, parseInt(opts.limit ?? "20", 10) || 20);
17781
17939
  const days = Math.max(1, parseInt(opts.days ?? "90", 10) || 90);
17782
- const radar = await buildRadar({
17940
+ const source = (opts.source ?? "both").toLowerCase();
17941
+ if (!["git", "agent", "both"].includes(source)) {
17942
+ ui.error("--source must be one of: git | agent | both");
17943
+ process.exitCode = 1;
17944
+ return;
17945
+ }
17946
+ const useGit = source === "git" || source === "both";
17947
+ const useAgent = source === "agent" || source === "both";
17948
+ const radar = useGit ? await buildRadar({
17783
17949
  root,
17784
17950
  taskTokens: null,
17785
17951
  filePaths: [],
17786
17952
  daysBack: Math.ceil(days / 6),
17787
17953
  // getHotFiles multiplies daysBack by 6
17788
17954
  maxHotFiles: 500
17789
- });
17790
- const hotFiles = radar.hotFiles.filter((h) => !isNoisePath(h.path)).map((h) => ({ path: h.path, changes: h.changes }));
17955
+ }) : null;
17956
+ const gitHotFiles = (radar?.hotFiles ?? []).filter((h) => !isNoisePath(h.path)).map((h) => ({ path: h.path, changes: h.changes, source: "git" }));
17957
+ const sinceMs = Date.now() - days * 864e5;
17958
+ const agentHotFiles = useAgent ? (await readAgentHotFiles(root, path57.join(paths.haiveDir, ".cache", "observations.jsonl"), sinceMs)).filter((h) => !isNoisePath(h.path)) : [];
17959
+ const hotFiles = mergeHotFiles(gitHotFiles, agentHotFiles);
17791
17960
  const memories = await loadMemoriesFromDir27(paths.memoriesDir);
17792
17961
  const gaps = findCoverageGaps(hotFiles, memories, { minChanges, limit });
17793
17962
  if (opts.json) {
17794
- console.log(JSON.stringify({ root, scanned_hot_files: hotFiles.length, gaps }, null, 2));
17963
+ console.log(JSON.stringify({
17964
+ root,
17965
+ source,
17966
+ scanned_hot_files: hotFiles.length,
17967
+ git_hot_files: gitHotFiles.length,
17968
+ agent_hot_files: agentHotFiles.length,
17969
+ gaps
17970
+ }, null, 2));
17795
17971
  return;
17796
17972
  }
17797
- if (!radar.insideGitRepo) {
17798
- ui.warn("Not a git repository \u2014 coverage uses git churn to find hot files.");
17973
+ if (useGit && radar && !radar.insideGitRepo && agentHotFiles.length === 0) {
17974
+ ui.warn("Not a git repository and no agent-edit history \u2014 nothing to cross-check.");
17799
17975
  return;
17800
17976
  }
17801
17977
  if (gaps.length === 0) {
@@ -17804,7 +17980,8 @@ function registerCoverage(program2) {
17804
17980
  }
17805
17981
  console.log(ui.bold(`hAIve coverage \u2014 ${gaps.length} blind spot(s) (hot files with no covering memory)`));
17806
17982
  for (const gap of gaps) {
17807
- console.log(` ${ui.yellow("\u25CB")} ${gap.path} ${ui.dim(`(${gap.changes} change${gap.changes === 1 ? "" : "s"})`)}`);
17983
+ const src = gap.source ? ui.dim(` [${gap.source}]`) : "";
17984
+ console.log(` ${ui.yellow("\u25CB")} ${gap.path} ${ui.dim(`(${gap.changes} change${gap.changes === 1 ? "" : "s"})`)}${src}`);
17808
17985
  }
17809
17986
  console.log(
17810
17987
  ui.dim(
@@ -17816,8 +17993,8 @@ function registerCoverage(program2) {
17816
17993
 
17817
17994
  // src/commands/merge-driver.ts
17818
17995
  import { execFileSync as execFileSync3 } from "child_process";
17819
- import { readFileSync, writeFileSync, existsSync as existsSync80 } from "fs";
17820
- import path57 from "path";
17996
+ import { readFileSync, writeFileSync, existsSync as existsSync81 } from "fs";
17997
+ import path58 from "path";
17821
17998
  import "commander";
17822
17999
  import { findProjectRoot as findProjectRoot58, mergeMemoryVersions } from "@hiveai/core";
17823
18000
  var GITATTRIBUTES_MARK = "# hAIve merge driver";
@@ -17849,8 +18026,8 @@ function registerMergeDriver(program2) {
17849
18026
  process.exitCode = 1;
17850
18027
  return;
17851
18028
  }
17852
- const gaPath = path57.join(root, ".gitattributes");
17853
- let content = existsSync80(gaPath) ? readFileSync(gaPath, "utf8") : "";
18029
+ const gaPath = path58.join(root, ".gitattributes");
18030
+ let content = existsSync81(gaPath) ? readFileSync(gaPath, "utf8") : "";
17854
18031
  if (!content.includes(GITATTRIBUTES_MARK)) {
17855
18032
  if (content.length > 0 && !content.endsWith("\n")) content += "\n";
17856
18033
  content += GITATTRIBUTES_BLOCK + "\n";
@@ -17865,11 +18042,12 @@ function registerMergeDriver(program2) {
17865
18042
 
17866
18043
  // src/commands/memory-resolve-conflict.ts
17867
18044
  import { writeFile as writeFile40 } from "fs/promises";
17868
- import { existsSync as existsSync81 } from "fs";
18045
+ import { existsSync as existsSync83 } from "fs";
17869
18046
  import "commander";
17870
18047
  import {
18048
+ applyConflictResolution,
17871
18049
  findProjectRoot as findProjectRoot59,
17872
- planConflictResolution,
18050
+ planConflictResolution as planConflictResolution2,
17873
18051
  resolveHaivePaths as resolveHaivePaths53,
17874
18052
  serializeMemory as serializeMemory31
17875
18053
  } from "@hiveai/core";
@@ -17877,7 +18055,7 @@ function registerMemoryResolveConflict(memory2) {
17877
18055
  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) => {
17878
18056
  const root = findProjectRoot59(opts.dir);
17879
18057
  const paths = resolveHaivePaths53(root);
17880
- if (!existsSync81(paths.memoriesDir)) {
18058
+ if (!existsSync83(paths.memoriesDir)) {
17881
18059
  ui.error(`No .ai/memories at ${root}.`);
17882
18060
  process.exitCode = 1;
17883
18061
  return;
@@ -17890,43 +18068,53 @@ function registerMemoryResolveConflict(memory2) {
17890
18068
  process.exitCode = 1;
17891
18069
  return;
17892
18070
  }
17893
- const plan = planConflictResolution(a, b);
18071
+ const plan = planConflictResolution2(a, b);
18072
+ const winner = plan.keep_id === idA ? a : b;
17894
18073
  const loser = plan.supersede_id === idA ? a : b;
18074
+ const applied = applyConflictResolution(winner, loser, plan);
17895
18075
  if (opts.json) {
17896
- console.log(JSON.stringify({ ...plan, applied: Boolean(opts.yes) }, null, 2));
18076
+ console.log(JSON.stringify({
18077
+ ...plan,
18078
+ winner_revision_count: applied.winner.revision_count,
18079
+ topic: applied.topic,
18080
+ topic_adopted: applied.topic_adopted,
18081
+ applied: Boolean(opts.yes)
18082
+ }, null, 2));
17897
18083
  } else {
17898
18084
  console.log(ui.bold("Conflict resolution"));
17899
- console.log(` keep: ${ui.green(plan.keep_id)}`);
18085
+ console.log(` keep: ${ui.green(plan.keep_id)} ${ui.dim(`(rev ${winner.memory.frontmatter.revision_count}\u2192${applied.winner.revision_count})`)}`);
17900
18086
  console.log(` supersede: ${ui.red(plan.supersede_id)} ${ui.dim(`\u2192 deprecated`)}`);
17901
18087
  console.log(` reason: ${plan.reason}`);
18088
+ if (applied.topic) {
18089
+ console.log(` topic: ${applied.topic}${applied.topic_adopted ? ui.dim(" (adopted from superseded \u2014 future captures upsert into the winner)") : ""}`);
18090
+ }
17902
18091
  }
17903
18092
  if (!opts.yes) {
17904
18093
  if (!opts.json) ui.info("Preview only \u2014 re-run with --yes to apply.");
17905
18094
  return;
17906
18095
  }
18096
+ await writeFile40(
18097
+ winner.filePath,
18098
+ serializeMemory31({ frontmatter: applied.winner, body: winner.memory.body }),
18099
+ "utf8"
18100
+ );
17907
18101
  await writeFile40(
17908
18102
  loser.filePath,
17909
- serializeMemory31({
17910
- frontmatter: {
17911
- ...loser.memory.frontmatter,
17912
- status: "deprecated",
17913
- stale_reason: plan.stale_reason,
17914
- related_ids: [.../* @__PURE__ */ new Set([...loser.memory.frontmatter.related_ids, plan.keep_id])]
17915
- },
17916
- body: loser.memory.body
17917
- }),
18103
+ serializeMemory31({ frontmatter: applied.loser, body: loser.memory.body }),
17918
18104
  "utf8"
17919
18105
  );
17920
- if (!opts.json) ui.success(`Deprecated ${plan.supersede_id}; ${plan.keep_id} remains authoritative.`);
18106
+ if (!opts.json) {
18107
+ ui.success(`Deprecated ${plan.supersede_id}; promoted ${plan.keep_id} (rev ${applied.winner.revision_count}${applied.topic ? `, topic=${applied.topic}` : ""}).`);
18108
+ }
17921
18109
  });
17922
18110
  }
17923
18111
 
17924
18112
  // src/commands/memory-seed-git.ts
17925
- import { execFile as execFile5 } from "child_process";
18113
+ import { execFile as execFile6 } from "child_process";
17926
18114
  import { mkdir as mkdir25, writeFile as writeFile41 } from "fs/promises";
17927
- import { existsSync as existsSync83 } from "fs";
17928
- import path58 from "path";
17929
- import { promisify as promisify5 } from "util";
18115
+ import { existsSync as existsSync84 } from "fs";
18116
+ import path59 from "path";
18117
+ import { promisify as promisify6 } from "util";
17930
18118
  import "commander";
17931
18119
  import {
17932
18120
  buildFrontmatter as buildFrontmatter12,
@@ -17936,12 +18124,12 @@ import {
17936
18124
  resolveHaivePaths as resolveHaivePaths54,
17937
18125
  serializeMemory as serializeMemory33
17938
18126
  } from "@hiveai/core";
17939
- var exec4 = promisify5(execFile5);
18127
+ var exec4 = promisify6(execFile6);
17940
18128
  function registerMemorySeedGit(memory2) {
17941
18129
  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) => {
17942
18130
  const root = findProjectRoot60(opts.dir);
17943
18131
  const paths = resolveHaivePaths54(root);
17944
- if (!existsSync83(paths.haiveDir)) {
18132
+ if (!existsSync84(paths.haiveDir)) {
17945
18133
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
17946
18134
  process.exitCode = 1;
17947
18135
  return;
@@ -17986,8 +18174,8 @@ function registerMemorySeedGit(memory2) {
17986
18174
  _Seeded from git ${p.kind} commit ${p.source_sha}. Review and validate (or delete) \u2014 not yet authoritative._
17987
18175
  `;
17988
18176
  const file = memoryFilePath13(paths, fm.scope, fm.id, fm.module);
17989
- if (existsSync83(file)) continue;
17990
- await mkdir25(path58.dirname(file), { recursive: true });
18177
+ if (existsSync84(file)) continue;
18178
+ await mkdir25(path59.dirname(file), { recursive: true });
17991
18179
  await writeFile41(file, serializeMemory33({ frontmatter: fm, body }), "utf8");
17992
18180
  written += 1;
17993
18181
  }
@@ -18019,8 +18207,8 @@ async function readCommits(root, days) {
18019
18207
  }
18020
18208
 
18021
18209
  // src/commands/bridges.ts
18022
- import { existsSync as existsSync84 } from "fs";
18023
- import path59 from "path";
18210
+ import { existsSync as existsSync85 } from "fs";
18211
+ import path60 from "path";
18024
18212
  import "commander";
18025
18213
  import {
18026
18214
  findProjectRoot as findProjectRoot61,
@@ -18041,7 +18229,7 @@ function registerBridges(program2) {
18041
18229
  const root = findProjectRoot61(opts.dir);
18042
18230
  const paths = resolveHaivePaths55(root);
18043
18231
  const dryRun = opts.dryRun === true;
18044
- if (!existsSync84(paths.memoriesDir)) {
18232
+ if (!existsSync85(paths.memoriesDir)) {
18045
18233
  ui.warn(`No .ai/memories at ${root}. Run \`haive init\` first.`);
18046
18234
  process.exitCode = 1;
18047
18235
  return;
@@ -18060,7 +18248,7 @@ function registerBridges(program2) {
18060
18248
  targets = BRIDGE_TARGETS3;
18061
18249
  } else {
18062
18250
  targets = BRIDGE_TARGETS3.filter(
18063
- (t) => existsSync84(path59.join(root, BRIDGE_TARGET_PATH3[t]))
18251
+ (t) => existsSync85(path60.join(root, BRIDGE_TARGET_PATH3[t]))
18064
18252
  );
18065
18253
  if (targets.length === 0) {
18066
18254
  ui.info(
@@ -18089,7 +18277,7 @@ function registerBridges(program2) {
18089
18277
  console.log(ui.bold("hAIve bridge targets:"));
18090
18278
  for (const target of BRIDGE_TARGETS3) {
18091
18279
  const relPath = BRIDGE_TARGET_PATH3[target];
18092
- const exists = existsSync84(path59.join(root, relPath));
18280
+ const exists = existsSync85(path60.join(root, relPath));
18093
18281
  const marker = exists ? ui.dim("\u2713") : ui.dim("\xB7");
18094
18282
  console.log(` ${marker} ${target.padEnd(10)} ${relPath}${exists ? "" : " (not present)"}`);
18095
18283
  }
@@ -18100,7 +18288,7 @@ function registerBridges(program2) {
18100
18288
 
18101
18289
  // src/index.ts
18102
18290
  var program = new Command64();
18103
- program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.20.1").option("--advanced", "show maintenance and experimental commands in help");
18291
+ program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.23.0").option("--advanced", "show maintenance and experimental commands in help");
18104
18292
  registerInit(program);
18105
18293
  registerWelcome(program);
18106
18294
  registerResolveProject(program);