@hiveai/cli 0.10.5 → 0.10.9

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
@@ -2743,6 +2743,7 @@ ${SEED_FOOTER(stack)}` });
2743
2743
  }
2744
2744
 
2745
2745
  // src/commands/init.ts
2746
+ var HAIVE_GITHUB_ACTION_REF = `v${"0.10.9"}`;
2746
2747
  var PROJECT_CONTEXT_TEMPLATE = `# Project context
2747
2748
 
2748
2749
  > Generated by \`haive init\`. Run \`haive init --bootstrap\` to auto-fill from your codebase,
@@ -2777,6 +2778,7 @@ This repo uses **hAIve** for shared context. The map:
2777
2778
  1. **Before editing** for a goal, call \`get_briefing\` (task + files/symbols) to load ranked context \u2014 or \`mem_relevant_to\` if project context is already loaded this session.
2778
2779
  2. **When an approach fails**, call \`mem_tried\` right away so the next agent skips the dead end.
2779
2780
  3. **Before closing** a substantive session, run the \`post_task\` prompt to capture what was learned.
2781
+ 4. **Before final response**, run \`haive enforce finish\`. If it blocks, commit/push, bump/tag shippable releases, then rerun it.
2780
2782
 
2781
2783
  If the haive MCP server is not available, tell the developer rather than silently skipping it.
2782
2784
 
@@ -2803,6 +2805,7 @@ This repository uses **hAIve**. Running \`haive init\` means the team expects ag
2803
2805
 
2804
2806
  - On failure: **\`mem_tried\`** immediately.
2805
2807
  - Before closing a substantive session: MCP prompt **\`post_task\`** when there is something worth capturing.
2808
+ - Before final response: **\`haive enforce finish\`** must pass; it checks commit/push and release version/tag protocol.
2806
2809
 
2807
2810
  ## If haive MCP is missing
2808
2811
 
@@ -2898,7 +2901,7 @@ jobs:
2898
2901
  steps:
2899
2902
  - uses: actions/checkout@v4
2900
2903
 
2901
- - uses: Doucs91/hAIve/packages/github-action@main
2904
+ - uses: Doucs91/hAIve/packages/github-action@${HAIVE_GITHUB_ACTION_REF}
2902
2905
  with:
2903
2906
  github-token: \${{ secrets.GITHUB_TOKEN }}
2904
2907
  # post-if-empty: 'true' # uncomment to always post (even when no memories found)
@@ -3746,9 +3749,8 @@ import { mkdir as mkdir52, writeFile as writeFile92, rm } from "fs/promises";
3746
3749
  import { existsSync as existsSync16 } from "fs";
3747
3750
  import path72 from "path";
3748
3751
  import { execSync } from "child_process";
3749
- import { readFile as readFile32, readdir as readdir3, writeFile as writeFile11 } from "fs/promises";
3750
- import { existsSync as existsSync18 } from "fs";
3751
- import path92 from "path";
3752
+ import { readFile as readFile42, writeFile as writeFile11 } from "fs/promises";
3753
+ import { existsSync as existsSync19 } from "fs";
3752
3754
  import {
3753
3755
  allocateBudget,
3754
3756
  DEFAULT_AUTO_PROMOTE_RULE,
@@ -3757,11 +3759,9 @@ import {
3757
3759
  extractActionsBriefBody as extractActionsBriefBody2,
3758
3760
  getUsage as getUsage5,
3759
3761
  inferModulesFromPaths as inferModulesFromPaths2,
3760
- isGlobPath,
3761
- isRetiredMemory,
3762
3762
  isAutoPromoteEligible,
3763
3763
  isDecaying,
3764
- isStackPackSeed as isStackPackSeed2,
3764
+ isRetiredMemory,
3765
3765
  literalMatchesAllTokens as literalMatchesAllTokens22,
3766
3766
  literalMatchesAnyToken as literalMatchesAnyToken22,
3767
3767
  loadCodeMap as loadCodeMap5,
@@ -3769,7 +3769,6 @@ import {
3769
3769
  loadMemoriesFromDir as loadMemoriesFromDir13,
3770
3770
  loadUsageIndex as loadUsageIndex7,
3771
3771
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths22,
3772
- pathsOverlap,
3773
3772
  queryCodeMap as queryCodeMap2,
3774
3773
  resolveBriefingBudget as resolveBriefingBudget2,
3775
3774
  serializeMemory as serializeMemory9,
@@ -3781,17 +3780,21 @@ import {
3781
3780
  writeBriefingMarker as writeBriefingMarker2
3782
3781
  } from "@hiveai/core";
3783
3782
  import { z as z17 } from "zod";
3783
+ import { readdir as readdir3, readFile as readFile32 } from "fs/promises";
3784
+ import { existsSync as existsSync18 } from "fs";
3785
+ import path92 from "path";
3786
+ import { isGlobPath, isStackPackSeed as isStackPackSeed2, pathsOverlap } from "@hiveai/core";
3784
3787
  import { estimateTokens as estimateTokens2, loadCodeMap as loadCodeMap22, queryCodeMap as queryCodeMap22 } from "@hiveai/core";
3785
3788
  import { z as z18 } from "zod";
3786
- import { existsSync as existsSync19 } from "fs";
3789
+ import { existsSync as existsSync20 } from "fs";
3787
3790
  import { loadMemoriesFromDir as loadMemoriesFromDir14 } from "@hiveai/core";
3788
3791
  import { z as z19 } from "zod";
3789
- import { existsSync as existsSync20 } from "fs";
3792
+ import { existsSync as existsSync21 } from "fs";
3790
3793
  import { loadMemoriesFromDir as loadMemoriesFromDir15 } from "@hiveai/core";
3791
3794
  import { z as z20 } from "zod";
3792
3795
  import { z as z21 } from "zod";
3793
3796
  import { z as z22 } from "zod";
3794
- import { existsSync as existsSync21 } from "fs";
3797
+ import { existsSync as existsSync222 } from "fs";
3795
3798
  import { spawn } from "child_process";
3796
3799
  import path102 from "path";
3797
3800
  import {
@@ -3803,7 +3806,7 @@ import {
3803
3806
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths3
3804
3807
  } from "@hiveai/core";
3805
3808
  import { z as z23 } from "zod";
3806
- import { existsSync as existsSync222 } from "fs";
3809
+ import { existsSync as existsSync23 } from "fs";
3807
3810
  import {
3808
3811
  addedLinesFromDiff,
3809
3812
  deriveConfidence as deriveConfidence6,
@@ -3818,13 +3821,13 @@ import {
3818
3821
  tokenizeQuery as tokenizeQuery3
3819
3822
  } from "@hiveai/core";
3820
3823
  import { z as z24 } from "zod";
3821
- import { existsSync as existsSync23 } from "fs";
3824
+ import { existsSync as existsSync24 } from "fs";
3822
3825
  import {
3823
3826
  loadMemoriesFromDir as loadMemoriesFromDir18,
3824
3827
  tokenizeQuery as tokenizeQuery4
3825
3828
  } from "@hiveai/core";
3826
3829
  import { z as z25 } from "zod";
3827
- import { existsSync as existsSync24 } from "fs";
3830
+ import { existsSync as existsSync25 } from "fs";
3828
3831
  import { spawn as spawn2 } from "child_process";
3829
3832
  import {
3830
3833
  deriveConfidence as deriveConfidence7,
@@ -3834,7 +3837,7 @@ import {
3834
3837
  pathsOverlap as singlePathsOverlap
3835
3838
  } from "@hiveai/core";
3836
3839
  import { z as z26 } from "zod";
3837
- import { existsSync as existsSync25 } from "fs";
3840
+ import { existsSync as existsSync26 } from "fs";
3838
3841
  import {
3839
3842
  deriveConfidence as deriveConfidence8,
3840
3843
  getUsage as getUsage9,
@@ -3846,7 +3849,7 @@ import {
3846
3849
  import { z as z27 } from "zod";
3847
3850
  import { z as z28 } from "zod";
3848
3851
  import { mkdir as mkdir72, writeFile as writeFile12 } from "fs/promises";
3849
- import { existsSync as existsSync26 } from "fs";
3852
+ import { existsSync as existsSync27 } from "fs";
3850
3853
  import path112 from "path";
3851
3854
  import { execSync as execSync2 } from "child_process";
3852
3855
  import {
@@ -3856,7 +3859,7 @@ import {
3856
3859
  serializeMemory as serializeMemory10
3857
3860
  } from "@hiveai/core";
3858
3861
  import { z as z29 } from "zod";
3859
- import { existsSync as existsSync27 } from "fs";
3862
+ import { existsSync as existsSync28 } from "fs";
3860
3863
  import {
3861
3864
  findLexicalConflictPairs,
3862
3865
  findTopicStatusConflictPairs,
@@ -3867,7 +3870,7 @@ import { resolveProjectInfo } from "@hiveai/core";
3867
3870
  import { z as z31 } from "zod";
3868
3871
  import { MemoryTypeSchema, suggestTopicKey } from "@hiveai/core";
3869
3872
  import { z as z32 } from "zod";
3870
- import { existsSync as existsSync28 } from "fs";
3873
+ import { existsSync as existsSync29 } from "fs";
3871
3874
  import { collectTimelineEntries, loadMemoriesFromDir as loadMemoriesFromDir222 } from "@hiveai/core";
3872
3875
  import { z as z33 } from "zod";
3873
3876
  import { appendRuntimeJournalEntry as appendRuntimeJournalEntry2 } from "@hiveai/core";
@@ -3877,7 +3880,7 @@ import { z as z35 } from "zod";
3877
3880
  import { z as z36 } from "zod";
3878
3881
  import { z as z37 } from "zod";
3879
3882
  import { z as z38 } from "zod";
3880
- import { loadConfigSync } from "@hiveai/core";
3883
+ import { hasRecentBriefingMarker, loadConfigSync } from "@hiveai/core";
3881
3884
  function createContext(options = {}) {
3882
3885
  const env = options.env ?? process.env;
3883
3886
  const cwd = options.cwd ?? process.cwd();
@@ -5108,6 +5111,144 @@ async function memSessionEnd(input, ctx) {
5108
5111
  revision_count: 0
5109
5112
  };
5110
5113
  }
5114
+ function compactSummary(body) {
5115
+ for (const line of body.split("\n")) {
5116
+ const trimmed = line.replace(/^#+\s*/, "").trim();
5117
+ if (trimmed.length > 0) return trimmed.slice(0, 120);
5118
+ }
5119
+ return body.slice(0, 120);
5120
+ }
5121
+ function classifyMemoryPriority(memory2, loaded, inputFiles, inputSymbols) {
5122
+ const fm = loaded?.memory.frontmatter;
5123
+ const directAnchor = Boolean(
5124
+ fm && inputFiles.length > 0 && fm.anchor.paths.some((p) => inputFiles.some((file) => pathsOverlap(p, file)))
5125
+ );
5126
+ const directSymbol = Boolean(
5127
+ fm && inputSymbols.length > 0 && fm.anchor.symbols.some(
5128
+ (sym) => inputSymbols.some((wanted) => wanted.toLowerCase() === sym.toLowerCase())
5129
+ )
5130
+ );
5131
+ const strongSemantic = (memory2.semantic_score ?? 0) >= 0.65;
5132
+ const usefulSemantic = (memory2.semantic_score ?? 0) >= 0.35;
5133
+ if (fm?.requires_human_approval || directAnchor || directSymbol || memory2.type === "attempt" && (memory2.match_quality === "exact" || strongSemantic) || memory2.type === "skill" && (memory2.match_quality === "exact" || strongSemantic)) {
5134
+ return "must_read";
5135
+ }
5136
+ if (isStackPackSeed2(fm)) {
5137
+ return "background";
5138
+ }
5139
+ if (memory2.type === "skill" || memory2.reasons.includes("module") || memory2.reasons.includes("domain") || memory2.match_quality === "exact" || usefulSemantic) {
5140
+ return "useful";
5141
+ }
5142
+ return "background";
5143
+ }
5144
+ function priorityRank(priority) {
5145
+ return priority === "must_read" ? 3 : priority === "useful" ? 2 : 1;
5146
+ }
5147
+ function classifyBriefingQuality(memories, context) {
5148
+ const mustRead = memories.filter((m) => m.priority === "must_read").length;
5149
+ const useful = memories.filter((m) => m.priority === "useful").length;
5150
+ const background = memories.filter((m) => m.priority === "background").length;
5151
+ const weakSemantic = memories.filter(
5152
+ (m) => m.reasons.length === 1 && m.reasons.includes("semantic") && (m.semantic_score ?? 0) > 0 && (m.semantic_score ?? 0) < 0.35
5153
+ ).length;
5154
+ const reasons = [];
5155
+ if (memories.length === 0) reasons.push("no memories matched the task or files");
5156
+ if (context.isTemplateContext && !context.autoContextGenerated) reasons.push("project context is still a template");
5157
+ if (!context.hasLastSession) reasons.push("no previous session recap");
5158
+ if (mustRead > 0) reasons.push(`${mustRead} must_read memor${mustRead === 1 ? "y" : "ies"} matched directly`);
5159
+ if (useful > 0) reasons.push(`${useful} useful memor${useful === 1 ? "y" : "ies"} matched`);
5160
+ if (background > useful + mustRead && background > 2) reasons.push(`${background} background memories dominate the result`);
5161
+ if (weakSemantic > 0) reasons.push(`${weakSemantic} weak semantic-only match${weakSemantic === 1 ? "" : "es"}`);
5162
+ if (context.searchMode === "literal_fallback") reasons.push("semantic index unavailable or empty; literal fallback used");
5163
+ if (memories.length === 0 || mustRead === 0 && useful === 0) {
5164
+ return { level: "thin", reasons };
5165
+ }
5166
+ if (background > useful + mustRead && background > 2) {
5167
+ return { level: "noisy", reasons };
5168
+ }
5169
+ return { level: "strong", reasons };
5170
+ }
5171
+ function explainWhySurfaced(memory2, loaded, inputFiles, inferredModules) {
5172
+ const why = [];
5173
+ const fm = loaded?.memory.frontmatter;
5174
+ if (memory2.reasons.includes("anchor") && fm) {
5175
+ const matching = fm.anchor.paths.filter(
5176
+ (p) => inputFiles.length === 0 || inputFiles.some((file) => pathsOverlap(p, file))
5177
+ );
5178
+ if (matching.length > 0) {
5179
+ const exact = matching.filter(
5180
+ (p) => !isGlobPath(p) && inputFiles.some((file) => p === file || pathsOverlap(p, file))
5181
+ );
5182
+ const glob = matching.filter((p) => isGlobPath(p));
5183
+ if (exact.length > 0) {
5184
+ why.push(`Exact/file anchor match: ${exact.slice(0, 4).join(", ")}`);
5185
+ }
5186
+ if (glob.length > 0) {
5187
+ why.push(`Glob anchor match: ${glob.slice(0, 4).join(", ")}`);
5188
+ }
5189
+ if (exact.length === 0 && glob.length === 0) {
5190
+ why.push(`Anchored to touched path${matching.length === 1 ? "" : "s"}: ${matching.slice(0, 4).join(", ")}`);
5191
+ }
5192
+ } else if (fm.anchor.paths.length > 0) {
5193
+ why.push(`Pulled by related anchor: ${fm.anchor.paths.slice(0, 4).join(", ")}`);
5194
+ }
5195
+ if (fm.anchor.symbols.length > 0) {
5196
+ why.push(`Anchor symbol${fm.anchor.symbols.length === 1 ? "" : "s"}: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
5197
+ }
5198
+ }
5199
+ if (memory2.reasons.includes("symbol") && fm) {
5200
+ why.push(`Explicit symbol match: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
5201
+ }
5202
+ if (memory2.reasons.includes("module")) {
5203
+ const moduleHints = [
5204
+ ...memory2.module ? [memory2.module] : [],
5205
+ ...memory2.tags.filter((tag) => inferredModules.includes(tag))
5206
+ ];
5207
+ const shown = moduleHints.length > 0 ? [...new Set(moduleHints)].join(", ") : inferredModules.join(", ");
5208
+ why.push(shown ? `Matched inferred module/tag: ${shown}` : "Matched inferred module context.");
5209
+ }
5210
+ if (memory2.reasons.includes("domain")) {
5211
+ why.push("Matched inferred domain from the target file paths.");
5212
+ }
5213
+ if (memory2.reasons.includes("semantic")) {
5214
+ const score = memory2.semantic_score !== void 0 ? ` score=${Math.round(memory2.semantic_score * 100) / 100}` : "";
5215
+ why.push(`${memory2.match_quality === "exact" ? "Literal task match" : "Semantic/task relevance"}${score}.`);
5216
+ }
5217
+ why.push(`Confidence: ${memory2.confidence}; read ${memory2.read_count} time${memory2.read_count === 1 ? "" : "s"}.`);
5218
+ if (memory2.type === "attempt") why.push("Failed-approach record; read before repeating the same path.");
5219
+ if (memory2.type === "skill") why.push("Skill (reusable procedure/playbook) \u2014 follow the steps described when doing this type of task.");
5220
+ if (memory2.status === "proposed" || memory2.status === "draft") {
5221
+ why.push("Unvalidated record; use cautiously or ask a human before treating it as policy.");
5222
+ }
5223
+ return why;
5224
+ }
5225
+ async function trySemanticHits(ctx, task, limit) {
5226
+ let mod;
5227
+ try {
5228
+ mod = await import("@hiveai/embeddings");
5229
+ } catch {
5230
+ return null;
5231
+ }
5232
+ const result = await mod.semanticSearch(ctx.paths, task, { limit });
5233
+ if (!result) return null;
5234
+ return result.hits.map((h) => ({ id: h.id, score: h.score }));
5235
+ }
5236
+ async function loadModuleContexts2(ctx, modules) {
5237
+ if (modules.length === 0) return [];
5238
+ if (!existsSync18(ctx.paths.modulesContextDir)) return [];
5239
+ const available = new Set(
5240
+ (await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
5241
+ );
5242
+ const out = [];
5243
+ for (const m of modules) {
5244
+ if (!available.has(m)) continue;
5245
+ const file = path92.join(ctx.paths.modulesContextDir, m, "context.md");
5246
+ if (existsSync18(file)) {
5247
+ out.push({ name: m, content: await readFile32(file, "utf8") });
5248
+ }
5249
+ }
5250
+ return out;
5251
+ }
5111
5252
  var GetBriefingInputSchema = {
5112
5253
  task: z17.string().optional().describe(
5113
5254
  "What you are about to do, in 1\u20132 sentences. Used to rank relevant memories semantically."
@@ -5153,7 +5294,7 @@ async function getBriefing(input, ctx) {
5153
5294
  let usage = { version: 1, updated_at: "", by_id: {} };
5154
5295
  let byId = /* @__PURE__ */ new Map();
5155
5296
  let lastSession;
5156
- if (existsSync18(ctx.paths.memoriesDir)) {
5297
+ if (existsSync19(ctx.paths.memoriesDir)) {
5157
5298
  const allLoaded = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
5158
5299
  const recaps = allLoaded.filter(({ memory: memory2 }) => memory2.frontmatter.type === "session_recap").sort(
5159
5300
  (a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
@@ -5238,34 +5379,25 @@ async function getBriefing(input, ctx) {
5238
5379
  if (input.task) {
5239
5380
  const tokens = tokenizeQuery22(input.task);
5240
5381
  const andHits = allMemories.filter((m) => literalMatchesAllTokens22(m.memory, tokens));
5241
- for (const loaded of andHits) {
5242
- addOrUpdate(loaded, "semantic", void 0, "exact");
5243
- }
5382
+ for (const loaded of andHits) addOrUpdate(loaded, "semantic", void 0, "exact");
5244
5383
  if (andHits.length === 0 && tokens.length > 1) {
5245
5384
  for (const loaded of allMemories) {
5246
- if (literalMatchesAnyToken22(loaded.memory, tokens)) {
5247
- addOrUpdate(loaded, "semantic", void 0, "partial");
5248
- }
5385
+ if (literalMatchesAnyToken22(loaded.memory, tokens)) addOrUpdate(loaded, "semantic", void 0, "partial");
5249
5386
  }
5250
5387
  }
5251
5388
  if (semanticHits) {
5252
5389
  for (const hit of semanticHits) {
5253
- if (hit.score < input.min_semantic_score) {
5254
- const existing = seen.get(hit.id);
5255
- if (!existing) continue;
5256
- }
5390
+ if (hit.score < input.min_semantic_score && !seen.has(hit.id)) continue;
5257
5391
  const loaded = byId.get(hit.id);
5258
5392
  if (loaded) addOrUpdate(loaded, "semantic", hit.score, "semantic");
5259
5393
  }
5260
5394
  }
5261
5395
  }
5262
5396
  const ranked = [...seen.values()].sort((a, b) => {
5263
- const priorityScore = (m) => priorityRank(classifyMemoryPriority(m, byId.get(m.id), input.files, input.symbols));
5264
- const reasonScore = (m) => (m.type === "attempt" ? 3 : 0) + // attempt = negative knowledge, surface first to prevent repeating mistakes
5265
- (m.reasons.includes("anchor") ? 4 : 0) + (m.reasons.includes("symbol") ? 4 : 0) + (m.reasons.includes("module") ? 2 : 0) + (m.reasons.includes("semantic") ? 2 : 0) + (m.reasons.includes("domain") ? 1 : 0);
5397
+ const reasonScore = (m) => (m.type === "attempt" ? 3 : 0) + (m.reasons.includes("anchor") ? 4 : 0) + (m.reasons.includes("symbol") ? 4 : 0) + (m.reasons.includes("module") ? 2 : 0) + (m.reasons.includes("semantic") ? 2 : 0) + (m.reasons.includes("domain") ? 1 : 0);
5266
5398
  const confidenceScore = (m) => m.confidence === "authoritative" ? 4 : m.confidence === "trusted" ? 3 : m.confidence === "low" ? 1 : m.confidence === "stale" ? -2 : 0;
5267
- const sa = priorityScore(a) * 100 + reasonScore(a) + confidenceScore(a) + (a.semantic_score ?? 0);
5268
- const sb = priorityScore(b) * 100 + reasonScore(b) + confidenceScore(b) + (b.semantic_score ?? 0);
5399
+ const sa = priorityRank(classifyMemoryPriority(a, byId.get(a.id), input.files, input.symbols)) * 100 + reasonScore(a) + confidenceScore(a) + (a.semantic_score ?? 0);
5400
+ const sb = priorityRank(classifyMemoryPriority(b, byId.get(b.id), input.files, input.symbols)) * 100 + reasonScore(b) + confidenceScore(b) + (b.semantic_score ?? 0);
5269
5401
  return sb - sa;
5270
5402
  });
5271
5403
  for (const mem of ranked.slice(0, briefingMaxMemories)) {
@@ -5294,11 +5426,7 @@ async function getBriefing(input, ctx) {
5294
5426
  if (!isAutoPromoteEligible(loaded.memory.frontmatter, u, rule)) continue;
5295
5427
  const newFm = { ...loaded.memory.frontmatter, status: "validated" };
5296
5428
  try {
5297
- await writeFile11(
5298
- loaded.filePath,
5299
- serializeMemory9({ frontmatter: newFm, body: loaded.memory.body }),
5300
- "utf8"
5301
- );
5429
+ await writeFile11(loaded.filePath, serializeMemory9({ frontmatter: newFm, body: loaded.memory.body }), "utf8");
5302
5430
  m.status = "validated";
5303
5431
  m.confidence = "trusted";
5304
5432
  } catch {
@@ -5306,12 +5434,12 @@ async function getBriefing(input, ctx) {
5306
5434
  }
5307
5435
  }
5308
5436
  }
5309
- const projectContextRaw = input.include_project_context && existsSync18(ctx.paths.projectContext) ? await readFile32(ctx.paths.projectContext, "utf8") : "";
5437
+ const projectContextRaw = input.include_project_context && existsSync19(ctx.paths.projectContext) ? await readFile42(ctx.paths.projectContext, "utf8") : "";
5310
5438
  const isTemplateContext = projectContextRaw.includes("TODO \u2014 high-level overview") || projectContextRaw.includes("Generated by `haive init`");
5311
5439
  const setupWarnings = [];
5312
5440
  let autoContextGenerated = false;
5313
5441
  let projectContext = isTemplateContext ? "" : projectContextRaw;
5314
- if ((isTemplateContext || !existsSync18(ctx.paths.projectContext)) && input.include_project_context) {
5442
+ if ((isTemplateContext || !existsSync19(ctx.paths.projectContext)) && input.include_project_context) {
5315
5443
  const haiveConfig = await loadConfig3(ctx.paths);
5316
5444
  if (haiveConfig.autoContext) {
5317
5445
  const codeMap = await loadCodeMap5(ctx.paths);
@@ -5347,15 +5475,9 @@ async function getBriefing(input, ctx) {
5347
5475
  );
5348
5476
  }
5349
5477
  } else {
5350
- if (isTemplateContext) {
5351
- setupWarnings.push(
5352
- "project-context.md still contains the default template. Invoke the bootstrap_project MCP prompt to auto-fill it from your codebase. Until then, get_briefing returns no project context."
5353
- );
5354
- } else {
5355
- setupWarnings.push(
5356
- "No project-context.md found. Run `haive init` then invoke the bootstrap_project MCP prompt."
5357
- );
5358
- }
5478
+ setupWarnings.push(
5479
+ isTemplateContext ? "project-context.md still contains the default template. Invoke the bootstrap_project MCP prompt to auto-fill it from your codebase. Until then, get_briefing returns no project context." : "No project-context.md found. Run `haive init` then invoke the bootstrap_project MCP prompt."
5480
+ );
5359
5481
  }
5360
5482
  }
5361
5483
  const moduleContents = briefingIncludeModules ? await loadModuleContexts2(ctx, inferred) : [];
@@ -5418,10 +5540,7 @@ ${m.content}`).join("\n\n---\n\n"),
5418
5540
  const createdAt = loaded?.memory.frontmatter.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
5419
5541
  if (isDecaying(u, createdAt)) decayWarnings.push(m.id);
5420
5542
  }
5421
- const formattedMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : input.format === "actions" ? trimmedMemories.map((m) => ({
5422
- ...m,
5423
- body: extractActionsBriefBody2(m.body)
5424
- })) : trimmedMemories;
5543
+ const formattedMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : input.format === "actions" ? trimmedMemories.map((m) => ({ ...m, body: extractActionsBriefBody2(m.body) })) : trimmedMemories;
5425
5544
  const outputMemories = formattedMemories.map((m) => ({
5426
5545
  ...m,
5427
5546
  priority: classifyMemoryPriority(m, byId.get(m.id), input.files, input.symbols),
@@ -5436,8 +5555,7 @@ ${m.content}`).join("\n\n---\n\n"),
5436
5555
  let symbolLocations;
5437
5556
  const symbolsToLookup = new Set(input.symbols);
5438
5557
  for (const m of outputMemories) {
5439
- const loaded = byId.get(m.id);
5440
- for (const sym of loaded?.memory.frontmatter.anchor.symbols ?? []) {
5558
+ for (const sym of byId.get(m.id)?.memory.frontmatter.anchor.symbols ?? []) {
5441
5559
  symbolsToLookup.add(sym);
5442
5560
  }
5443
5561
  }
@@ -5465,41 +5583,37 @@ ${m.content}`).join("\n\n---\n\n"),
5465
5583
  }
5466
5584
  }
5467
5585
  const actionRequired = [];
5468
- for (const m of outputMemories) {
5469
- const loaded = byId.get(m.id);
5470
- if (!loaded?.memory.frontmatter.requires_human_approval) continue;
5471
- const bodyLines = loaded.memory.body.split("\n");
5586
+ const extractActionItem = (id, body) => {
5587
+ const bodyLines = body.split("\n");
5472
5588
  const quoteBlock = bodyLines.filter((l) => l.startsWith("> ")).map((l) => l.slice(2)).join(" ").replace(/^\*«\s*/, "").replace(/\s*»\*$/, "").trim();
5473
5589
  const headingLine = bodyLines.find((l) => l.startsWith("## "));
5474
- const summary = headingLine?.replace(/^##\s*/, "").trim() ?? m.id;
5475
- actionRequired.push({
5476
- id: m.id,
5590
+ const summary = headingLine?.replace(/^##\s*/, "").trim() ?? id;
5591
+ return {
5592
+ id,
5477
5593
  summary,
5478
- developer_message: quoteBlock || `Une modification externe potentiellement incompatible a \xE9t\xE9 d\xE9tect\xE9e (${m.id}). Veux-tu que j'analyse l'impact et que je propose des mises \xE0 jour ?`
5479
- });
5594
+ developer_message: quoteBlock || `Une modification externe potentiellement incompatible a \xE9t\xE9 d\xE9tect\xE9e (${id}). Veux-tu que j'analyse l'impact et que je propose des mises \xE0 jour ?`
5595
+ };
5596
+ };
5597
+ for (const m of outputMemories) {
5598
+ const loaded = byId.get(m.id);
5599
+ if (loaded?.memory.frontmatter.requires_human_approval) {
5600
+ actionRequired.push(extractActionItem(m.id, loaded.memory.body));
5601
+ }
5480
5602
  }
5481
- if (existsSync18(ctx.paths.memoriesDir)) {
5603
+ if (existsSync19(ctx.paths.memoriesDir)) {
5482
5604
  const allMems = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
5483
5605
  for (const { memory: memory2 } of allMems) {
5484
5606
  const fm = memory2.frontmatter;
5485
5607
  if (!fm.requires_human_approval) continue;
5486
5608
  if (fm.status === "rejected" || fm.status === "deprecated") continue;
5487
5609
  if (actionRequired.some((a) => a.id === fm.id)) continue;
5488
- const bodyLines = memory2.body.split("\n");
5489
- const quoteBlock = bodyLines.filter((l) => l.startsWith("> ")).map((l) => l.slice(2)).join(" ").replace(/^\*«\s*/, "").replace(/\s*»\*$/, "").trim();
5490
- const headingLine = bodyLines.find((l) => l.startsWith("## "));
5491
- const summary = headingLine?.replace(/^##\s*/, "").trim() ?? fm.id;
5492
- actionRequired.push({
5493
- id: fm.id,
5494
- summary,
5495
- developer_message: quoteBlock || `Une modification externe potentiellement incompatible a \xE9t\xE9 d\xE9tect\xE9e (${fm.id}). Veux-tu que j'analyse l'impact et que je propose des mises \xE0 jour ?`
5496
- });
5610
+ actionRequired.push(extractActionItem(fm.id, memory2.body));
5497
5611
  }
5498
5612
  }
5499
5613
  const pendingDistillFile = pendingDistillPath(ctx);
5500
- if (existsSync18(pendingDistillFile)) {
5614
+ if (existsSync19(pendingDistillFile)) {
5501
5615
  try {
5502
- const raw = await readFile32(pendingDistillFile, "utf8");
5616
+ const raw = await readFile42(pendingDistillFile, "utf8");
5503
5617
  const pd = JSON.parse(raw);
5504
5618
  const ageMs = Date.now() - new Date(pd.session_end).getTime();
5505
5619
  const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1e3;
@@ -5526,7 +5640,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
5526
5640
  }
5527
5641
  }
5528
5642
  const memoriesEmpty = outputMemories.length === 0;
5529
- const hasMemoriesDir = existsSync18(ctx.paths.memoriesDir);
5643
+ const hasMemoriesDir = existsSync19(ctx.paths.memoriesDir);
5530
5644
  const isColdStart = isTemplateContext && memoriesEmpty && !lastSession && !autoContextGenerated;
5531
5645
  const hasUnguessableSignal = outputMemories.some(
5532
5646
  (m) => (m.priority === "must_read" || m.priority === "useful") && specificityScore2(m.body) >= GUESSABLE_THRESHOLD
@@ -5573,7 +5687,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
5573
5687
  "No team-specific policy matched these files/task \u2014 nothing here a capable model can't infer. The auto-generated project context was trimmed to keep this briefing near-zero-cost; proceed with normal Read/Grep."
5574
5688
  );
5575
5689
  }
5576
- if (existsSync18(ctx.paths.haiveDir)) {
5690
+ if (existsSync19(ctx.paths.haiveDir)) {
5577
5691
  await writeBriefingMarker2(ctx.paths, {
5578
5692
  sessionId: process.env.HAIVE_SESSION_ID,
5579
5693
  ...input.task ? { task: input.task } : {},
@@ -5621,144 +5735,6 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
5621
5735
  }
5622
5736
  };
5623
5737
  }
5624
- function compactSummary(body) {
5625
- for (const line of body.split("\n")) {
5626
- const trimmed = line.replace(/^#+\s*/, "").trim();
5627
- if (trimmed.length > 0) return trimmed.slice(0, 120);
5628
- }
5629
- return body.slice(0, 120);
5630
- }
5631
- function classifyMemoryPriority(memory2, loaded, inputFiles, inputSymbols) {
5632
- const fm = loaded?.memory.frontmatter;
5633
- const directAnchor = Boolean(
5634
- fm && inputFiles.length > 0 && fm.anchor.paths.some((p) => inputFiles.some((file) => pathsOverlap(p, file)))
5635
- );
5636
- const directSymbol = Boolean(
5637
- fm && inputSymbols.length > 0 && fm.anchor.symbols.some(
5638
- (sym) => inputSymbols.some((wanted) => wanted.toLowerCase() === sym.toLowerCase())
5639
- )
5640
- );
5641
- const strongSemantic = (memory2.semantic_score ?? 0) >= 0.65;
5642
- const usefulSemantic = (memory2.semantic_score ?? 0) >= 0.35;
5643
- if (fm?.requires_human_approval || directAnchor || directSymbol || memory2.type === "attempt" && (memory2.match_quality === "exact" || strongSemantic) || memory2.type === "skill" && (memory2.match_quality === "exact" || strongSemantic)) {
5644
- return "must_read";
5645
- }
5646
- if (isStackPackSeed2(fm)) {
5647
- return "background";
5648
- }
5649
- if (memory2.type === "skill" || memory2.reasons.includes("module") || memory2.reasons.includes("domain") || memory2.match_quality === "exact" || usefulSemantic) {
5650
- return "useful";
5651
- }
5652
- return "background";
5653
- }
5654
- function priorityRank(priority) {
5655
- return priority === "must_read" ? 3 : priority === "useful" ? 2 : 1;
5656
- }
5657
- function classifyBriefingQuality(memories, context) {
5658
- const mustRead = memories.filter((m) => m.priority === "must_read").length;
5659
- const useful = memories.filter((m) => m.priority === "useful").length;
5660
- const background = memories.filter((m) => m.priority === "background").length;
5661
- const weakSemantic = memories.filter(
5662
- (m) => m.reasons.length === 1 && m.reasons.includes("semantic") && (m.semantic_score ?? 0) > 0 && (m.semantic_score ?? 0) < 0.35
5663
- ).length;
5664
- const reasons = [];
5665
- if (memories.length === 0) reasons.push("no memories matched the task or files");
5666
- if (context.isTemplateContext && !context.autoContextGenerated) reasons.push("project context is still a template");
5667
- if (!context.hasLastSession) reasons.push("no previous session recap");
5668
- if (mustRead > 0) reasons.push(`${mustRead} must_read memor${mustRead === 1 ? "y" : "ies"} matched directly`);
5669
- if (useful > 0) reasons.push(`${useful} useful memor${useful === 1 ? "y" : "ies"} matched`);
5670
- if (background > useful + mustRead && background > 2) reasons.push(`${background} background memories dominate the result`);
5671
- if (weakSemantic > 0) reasons.push(`${weakSemantic} weak semantic-only match${weakSemantic === 1 ? "" : "es"}`);
5672
- if (context.searchMode === "literal_fallback") reasons.push("semantic index unavailable or empty; literal fallback used");
5673
- if (memories.length === 0 || mustRead === 0 && useful === 0) {
5674
- return { level: "thin", reasons };
5675
- }
5676
- if (background > useful + mustRead && background > 2) {
5677
- return { level: "noisy", reasons };
5678
- }
5679
- return { level: "strong", reasons };
5680
- }
5681
- function explainWhySurfaced(memory2, loaded, inputFiles, inferredModules) {
5682
- const why = [];
5683
- const fm = loaded?.memory.frontmatter;
5684
- if (memory2.reasons.includes("anchor") && fm) {
5685
- const matching = fm.anchor.paths.filter(
5686
- (p) => inputFiles.length === 0 || inputFiles.some((file) => pathsOverlap(p, file))
5687
- );
5688
- if (matching.length > 0) {
5689
- const exact = matching.filter(
5690
- (p) => !isGlobPath(p) && inputFiles.some((file) => p === file || pathsOverlap(p, file))
5691
- );
5692
- const glob = matching.filter((p) => isGlobPath(p));
5693
- if (exact.length > 0) {
5694
- why.push(`Exact/file anchor match: ${exact.slice(0, 4).join(", ")}`);
5695
- }
5696
- if (glob.length > 0) {
5697
- why.push(`Glob anchor match: ${glob.slice(0, 4).join(", ")}`);
5698
- }
5699
- if (exact.length === 0 && glob.length === 0) {
5700
- why.push(`Anchored to touched path${matching.length === 1 ? "" : "s"}: ${matching.slice(0, 4).join(", ")}`);
5701
- }
5702
- } else if (fm.anchor.paths.length > 0) {
5703
- why.push(`Pulled by related anchor: ${fm.anchor.paths.slice(0, 4).join(", ")}`);
5704
- }
5705
- if (fm.anchor.symbols.length > 0) {
5706
- why.push(`Anchor symbol${fm.anchor.symbols.length === 1 ? "" : "s"}: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
5707
- }
5708
- }
5709
- if (memory2.reasons.includes("symbol") && fm) {
5710
- why.push(`Explicit symbol match: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
5711
- }
5712
- if (memory2.reasons.includes("module")) {
5713
- const moduleHints = [
5714
- ...memory2.module ? [memory2.module] : [],
5715
- ...memory2.tags.filter((tag) => inferredModules.includes(tag))
5716
- ];
5717
- const shown = moduleHints.length > 0 ? [...new Set(moduleHints)].join(", ") : inferredModules.join(", ");
5718
- why.push(shown ? `Matched inferred module/tag: ${shown}` : "Matched inferred module context.");
5719
- }
5720
- if (memory2.reasons.includes("domain")) {
5721
- why.push("Matched inferred domain from the target file paths.");
5722
- }
5723
- if (memory2.reasons.includes("semantic")) {
5724
- const score = memory2.semantic_score !== void 0 ? ` score=${Math.round(memory2.semantic_score * 100) / 100}` : "";
5725
- why.push(`${memory2.match_quality === "exact" ? "Literal task match" : "Semantic/task relevance"}${score}.`);
5726
- }
5727
- why.push(`Confidence: ${memory2.confidence}; read ${memory2.read_count} time${memory2.read_count === 1 ? "" : "s"}.`);
5728
- if (memory2.type === "attempt") why.push("Failed-approach record; read before repeating the same path.");
5729
- if (memory2.type === "skill") why.push("Skill (reusable procedure/playbook) \u2014 follow the steps described when doing this type of task.");
5730
- if (memory2.status === "proposed" || memory2.status === "draft") {
5731
- why.push("Unvalidated record; use cautiously or ask a human before treating it as policy.");
5732
- }
5733
- return why;
5734
- }
5735
- async function trySemanticHits(ctx, task, limit) {
5736
- let mod;
5737
- try {
5738
- mod = await import("@hiveai/embeddings");
5739
- } catch {
5740
- return null;
5741
- }
5742
- const result = await mod.semanticSearch(ctx.paths, task, { limit });
5743
- if (!result) return null;
5744
- return result.hits.map((h) => ({ id: h.id, score: h.score }));
5745
- }
5746
- async function loadModuleContexts2(ctx, modules) {
5747
- if (modules.length === 0) return [];
5748
- if (!existsSync18(ctx.paths.modulesContextDir)) return [];
5749
- const available = new Set(
5750
- (await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
5751
- );
5752
- const out = [];
5753
- for (const m of modules) {
5754
- if (!available.has(m)) continue;
5755
- const file = path92.join(ctx.paths.modulesContextDir, m, "context.md");
5756
- if (existsSync18(file)) {
5757
- out.push({ name: m, content: await readFile32(file, "utf8") });
5758
- }
5759
- }
5760
- return out;
5761
- }
5762
5738
  var CodeMapInputSchema = {
5763
5739
  file: z18.string().optional().describe("Filter to files whose path contains this substring"),
5764
5740
  symbol: z18.string().optional().describe("Filter to files exporting a symbol whose name contains this substring"),
@@ -5842,7 +5818,7 @@ var MemDiffInputSchema = {
5842
5818
  id_b: z19.string().min(1).describe("Second memory id")
5843
5819
  };
5844
5820
  async function memDiff(input, ctx) {
5845
- if (!existsSync19(ctx.paths.memoriesDir)) {
5821
+ if (!existsSync20(ctx.paths.memoriesDir)) {
5846
5822
  throw new Error(`No .ai/memories at ${ctx.paths.root}.`);
5847
5823
  }
5848
5824
  const all = await loadMemoriesFromDir14(ctx.paths.memoriesDir);
@@ -5883,7 +5859,7 @@ var GetRecapInputSchema = {
5883
5859
  )
5884
5860
  };
5885
5861
  async function getRecap(input, ctx) {
5886
- if (!existsSync20(ctx.paths.memoriesDir)) {
5862
+ if (!existsSync21(ctx.paths.memoriesDir)) {
5887
5863
  return { recap: null, notice: "No .ai/memories directory \u2014 haive not initialized here." };
5888
5864
  }
5889
5865
  const all = await loadMemoriesFromDir15(ctx.paths.memoriesDir);
@@ -5982,7 +5958,7 @@ var WhyThisFileInputSchema = {
5982
5958
  memory_limit: z23.number().int().positive().max(20).default(5).describe("Cap on memories anchored to this path.")
5983
5959
  };
5984
5960
  async function whyThisFile(input, ctx) {
5985
- const fileExists = existsSync21(path102.join(ctx.paths.root, input.path));
5961
+ const fileExists = existsSync222(path102.join(ctx.paths.root, input.path));
5986
5962
  const [commits, memories, codeMap] = await Promise.all([
5987
5963
  runGitLog(ctx.paths.root, input.path, input.git_log_limit).catch(() => []),
5988
5964
  collectAnchoredMemories(ctx, input.path, input.memory_limit),
@@ -6023,7 +5999,7 @@ async function whyThisFile(input, ctx) {
6023
5999
  };
6024
6000
  }
6025
6001
  async function collectAnchoredMemories(ctx, filePath, limit) {
6026
- if (!existsSync21(ctx.paths.memoriesDir)) return [];
6002
+ if (!existsSync222(ctx.paths.memoriesDir)) return [];
6027
6003
  const all = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
6028
6004
  const usage = await loadUsageIndex8(ctx.paths);
6029
6005
  const out = [];
@@ -6155,7 +6131,7 @@ async function antiPatternsCheck(input, ctx) {
6155
6131
  notice: "Nothing to check \u2014 provide either `diff` text or `paths`."
6156
6132
  };
6157
6133
  }
6158
- if (!existsSync222(ctx.paths.memoriesDir)) {
6134
+ if (!existsSync23(ctx.paths.memoriesDir)) {
6159
6135
  return { scanned: 0, warnings: [], notice: "No .ai/memories directory \u2014 nothing to check against." };
6160
6136
  }
6161
6137
  const all = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
@@ -6190,6 +6166,7 @@ async function antiPatternsCheck(input, ctx) {
6190
6166
  reasons: [reason],
6191
6167
  tags: fm.tags ?? [],
6192
6168
  anchor_paths: fm.anchor?.paths ?? [],
6169
+ ...fm.sensor != null ? { has_sensor: true } : {},
6193
6170
  ...score !== void 0 ? { semantic_score: score } : {}
6194
6171
  });
6195
6172
  };
@@ -6302,7 +6279,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
6302
6279
  "error"
6303
6280
  ]);
6304
6281
  async function memDistill(input, ctx) {
6305
- if (!existsSync23(ctx.paths.memoriesDir)) {
6282
+ if (!existsSync24(ctx.paths.memoriesDir)) {
6306
6283
  return { scanned: 0, singletons: 0, clusters: [], notice: "No .ai/memories directory." };
6307
6284
  }
6308
6285
  const cutoff = Date.now() - input.since_days * MS_PER_DAY;
@@ -6413,7 +6390,7 @@ var WhyThisDecisionInputSchema = {
6413
6390
  git_log_limit: z26.number().int().positive().max(20).default(5).describe("How many recent commits per anchor path to surface.")
6414
6391
  };
6415
6392
  async function whyThisDecision(input, ctx) {
6416
- if (!existsSync24(ctx.paths.memoriesDir)) {
6393
+ if (!existsSync25(ctx.paths.memoriesDir)) {
6417
6394
  return {
6418
6395
  found: false,
6419
6396
  related: [],
@@ -6551,7 +6528,7 @@ var MemConflictsInputSchema = {
6551
6528
  var POSITIVE_PATTERNS = /\b(use|prefer|always|should use|do this|recommended|ok to)\b/i;
6552
6529
  var NEGATIVE_PATTERNS = /\b(do not use|don'?t use|never|avoid|forbidden|deprecated|stop using|do NOT|❌)\b/i;
6553
6530
  async function memConflicts(input, ctx) {
6554
- if (!existsSync25(ctx.paths.memoriesDir)) {
6531
+ if (!existsSync26(ctx.paths.memoriesDir)) {
6555
6532
  return { found: false, scanned: 0, conflicts: [], notice: "No .ai/memories directory." };
6556
6533
  }
6557
6534
  const all = await loadMemoriesFromDir20(ctx.paths.memoriesDir);
@@ -6741,7 +6718,9 @@ async function preCommitCheck(input, ctx) {
6741
6718
  };
6742
6719
  }
6743
6720
  function classifyWarning(warning, paths, anchoredBlocks = false) {
6744
- const affectedFiles = paths.filter((p) => !p.startsWith(".ai/.usage/"));
6721
+ const affectedFiles = paths.filter(
6722
+ (p) => !p.startsWith(".ai/.usage/") && !p.startsWith(".ai/.cache/") && !p.startsWith(".ai/.runtime/")
6723
+ );
6745
6724
  const repairCommand = repairCommandForWarning(warning, affectedFiles);
6746
6725
  const fileDowngrade = fileTypeDowngradeReason(warning, affectedFiles);
6747
6726
  if (fileDowngrade) {
@@ -6772,6 +6751,24 @@ function classifyWarning(warning, paths, anchoredBlocks = false) {
6772
6751
  };
6773
6752
  }
6774
6753
  if (isBlockingWarning(warning)) {
6754
+ if (warning.scope === "personal") {
6755
+ return {
6756
+ ...warning,
6757
+ level: "review",
6758
+ rationale: "personal anti-pattern memories are review guidance unless a deterministic block-severity sensor fires",
6759
+ affected_files: affectedFiles,
6760
+ repair_command: repairCommand
6761
+ };
6762
+ }
6763
+ if (warning.has_sensor && !warning.reasons.includes("sensor")) {
6764
+ return {
6765
+ ...warning,
6766
+ level: "review",
6767
+ rationale: "memory has a sensor that did not fire \u2014 sensor is the authoritative check; strong semantic match alone is insufficient to block",
6768
+ affected_files: affectedFiles,
6769
+ repair_command: repairCommand
6770
+ };
6771
+ }
6775
6772
  return {
6776
6773
  ...warning,
6777
6774
  level: "blocking",
@@ -6783,7 +6780,16 @@ function classifyWarning(warning, paths, anchoredBlocks = false) {
6783
6780
  const hasSemantic = warning.reasons.includes("semantic");
6784
6781
  const semanticScore = warning.semantic_score ?? 0;
6785
6782
  const highConfidence = warning.confidence === "authoritative" || warning.confidence === "trusted";
6786
- if (anchoredBlocks && highConfidence && warning.reasons.includes("anchor") && (warning.reasons.includes("literal") || hasSemantic && semanticScore >= 0.45)) {
6783
+ if (anchoredBlocks && highConfidence && warning.scope !== "personal" && warning.reasons.includes("anchor") && (warning.reasons.includes("literal") || hasSemantic && semanticScore >= 0.45)) {
6784
+ if (warning.has_sensor && !warning.reasons.includes("sensor")) {
6785
+ return {
6786
+ ...warning,
6787
+ level: "review",
6788
+ rationale: "memory has a sensor that did not fire \u2014 literal match alone is insufficient to block; sensor is the authoritative check",
6789
+ affected_files: affectedFiles,
6790
+ repair_command: repairCommand
6791
+ };
6792
+ }
6787
6793
  return {
6788
6794
  ...warning,
6789
6795
  level: "blocking",
@@ -6908,8 +6914,20 @@ function isJsonConfigFile(base) {
6908
6914
  return false;
6909
6915
  }
6910
6916
  function repairCommandForWarning(warning, paths) {
6911
- const firstPath = paths[0];
6912
- return firstPath ? `haive briefing --files "${firstPath}" --task "review ${warning.id}"` : `haive memory show ${warning.id}`;
6917
+ const targetPath = repairTargetPathForWarning(warning, paths);
6918
+ return targetPath ? `haive briefing --files "${targetPath}" --task "review ${warning.id}"` : `haive memory show ${warning.id}`;
6919
+ }
6920
+ function repairTargetPathForWarning(warning, paths) {
6921
+ const usablePaths = paths.filter(
6922
+ (p) => !p.startsWith(".ai/.usage/") && !p.startsWith(".ai/.cache/") && !p.startsWith(".ai/.runtime/")
6923
+ );
6924
+ const anchors = warning.anchor_paths ?? [];
6925
+ for (const file of usablePaths) {
6926
+ if (anchors.some((anchor) => anchor === file || file.startsWith(`${anchor}/`) || anchor.startsWith(`${file}/`))) {
6927
+ return file;
6928
+ }
6929
+ }
6930
+ return usablePaths[0];
6913
6931
  }
6914
6932
  var CONFIG_PATTERNS = [
6915
6933
  ".eslintrc",
@@ -6938,7 +6956,7 @@ var PatternDetectInputSchema = {
6938
6956
  scope: z29.enum(["personal", "team"]).default("team").describe("Scope for proposed memories.")
6939
6957
  };
6940
6958
  async function patternDetect(input, ctx) {
6941
- if (!existsSync26(ctx.paths.haiveDir)) {
6959
+ if (!existsSync27(ctx.paths.haiveDir)) {
6942
6960
  return {
6943
6961
  scanned_events: 0,
6944
6962
  matches: [],
@@ -7067,7 +7085,7 @@ async function patternDetect(input, ctx) {
7067
7085
  fm.id,
7068
7086
  void 0
7069
7087
  );
7070
- if (existsSync26(file)) continue;
7088
+ if (existsSync27(file)) continue;
7071
7089
  await mkdir72(path112.dirname(file), { recursive: true });
7072
7090
  await writeFile12(
7073
7091
  file,
@@ -7122,7 +7140,7 @@ var MemConflictCandidatesInputSchema = {
7122
7140
  )
7123
7141
  };
7124
7142
  async function memConflictCandidates(input, ctx) {
7125
- if (!existsSync27(ctx.paths.memoriesDir)) {
7143
+ if (!existsSync28(ctx.paths.memoriesDir)) {
7126
7144
  return {
7127
7145
  pairs: [],
7128
7146
  topic_status_pairs: [],
@@ -7170,7 +7188,7 @@ var MemTimelineInputSchema = {
7170
7188
  limit: z33.number().int().positive().max(100).default(30).describe("Max timeline entries returned")
7171
7189
  };
7172
7190
  async function memTimeline(input, ctx) {
7173
- if (!existsSync28(ctx.paths.memoriesDir)) {
7191
+ if (!existsSync29(ctx.paths.memoriesDir)) {
7174
7192
  return { entries: [], total: 0, notice: "No .ai/memories directory." };
7175
7193
  }
7176
7194
  const all = await loadMemoriesFromDir222(ctx.paths.memoriesDir);
@@ -7363,7 +7381,19 @@ This creates/updates a single rolling recap that **get_briefing automatically su
7363
7381
 
7364
7382
  Calling \`mem_session_end\` also **clears the pending-distill marker** (if any), confirming that this session's learnings have been properly captured rather than left as an auto-recap skeleton.
7365
7383
 
7366
- When done, respond with a brief summary: "Saved N memories: [list of IDs]. Session recap saved."
7384
+ ### 7. Verify the git/release exit protocol \u2014 always
7385
+ Run **\`haive enforce finish\`** before your final response.
7386
+
7387
+ This executable gate checks the multi-agent git-sync decision:
7388
+ - no completed work is left as an uncommitted local diff
7389
+ - shippable package changes have a lockstep version bump
7390
+ - the release tag \`vX.Y.Z\` exists when a version was bumped
7391
+ - commits and tags have been pushed
7392
+ - agents never run \`npm publish\` (publication remains human-owned)
7393
+
7394
+ If it blocks, fix the reported Git/version/tag/push issue before telling the developer the task is done.
7395
+
7396
+ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Session recap saved. hAIve finish gate passed."
7367
7397
  `;
7368
7398
  return {
7369
7399
  description: "Post-task reflection: capture what you learned before closing the session",
@@ -7442,7 +7472,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
7442
7472
  };
7443
7473
  }
7444
7474
  var SERVER_NAME = "haive";
7445
- var SERVER_VERSION = "0.10.5";
7475
+ var SERVER_VERSION = "0.10.9";
7446
7476
  function jsonResult(data) {
7447
7477
  return {
7448
7478
  content: [
@@ -7546,11 +7576,16 @@ function createHaiveServer(options = {}) {
7546
7576
  return await handler(input);
7547
7577
  }
7548
7578
  if (requireBriefingFirst && MUTATING_TOOLS.has(name) && !briefingLoaded) {
7549
- return jsonResult({
7550
- error: "haive_briefing_required",
7551
- message: "This hAIve project requires get_briefing or mem_relevant_to before state-changing hAIve tools. Call get_briefing({ task: '...' }) first.",
7552
- tool: name
7553
- });
7579
+ const hasDiskMarker = await hasRecentBriefingMarker(context.paths).catch(() => false);
7580
+ if (hasDiskMarker) {
7581
+ briefingLoaded = true;
7582
+ } else {
7583
+ return jsonResult({
7584
+ error: "haive_briefing_required",
7585
+ message: "This hAIve project requires get_briefing or mem_relevant_to before state-changing hAIve tools. Call get_briefing({ task: '...' }) first.",
7586
+ tool: name
7587
+ });
7588
+ }
7554
7589
  }
7555
7590
  return await handler(input);
7556
7591
  }
@@ -8412,7 +8447,7 @@ function registerMcp(program2) {
8412
8447
  // src/commands/sync.ts
8413
8448
  import { spawnSync as spawnSync4 } from "child_process";
8414
8449
  import { readFile as readFile9, writeFile as writeFile13, mkdir as mkdir10 } from "fs/promises";
8415
- import { existsSync as existsSync29 } from "fs";
8450
+ import { existsSync as existsSync30 } from "fs";
8416
8451
  import path15 from "path";
8417
8452
  import "commander";
8418
8453
  import {
@@ -8449,7 +8484,7 @@ function registerSync(program2) {
8449
8484
  ).option("--bridge-file <path>", "bridge file to inject into (default: CLAUDE.md)").option("--bridge-max-memories <n>", "max memories to inject into bridge file", "5").option("--embed", "rebuild embeddings index after sync (requires @haive/embeddings)").option("--no-cross-repo", "skip cross-repo memory pull even if crossRepoSources is configured").option("--no-deps", "skip dependency version tracking").option("--no-contracts", "skip contract file diff checking").option("--dry-run", "report what would change without writing any files").action(async (opts) => {
8450
8485
  const root = findProjectRoot12(opts.dir);
8451
8486
  const paths = resolveHaivePaths9(root);
8452
- if (!existsSync29(paths.memoriesDir)) {
8487
+ if (!existsSync30(paths.memoriesDir)) {
8453
8488
  if (!opts.quiet) ui.warn(`No .ai/memories at ${root}. Run \`haive init\` first.`);
8454
8489
  process.exitCode = 1;
8455
8490
  return;
@@ -8873,7 +8908,7 @@ function bridgeSummaryLine(body) {
8873
8908
  return oneLine.length > 140 ? oneLine.slice(0, 137) + "\u2026" : oneLine;
8874
8909
  }
8875
8910
  async function injectBridge(bridgeFile, memoriesDir, maxMemories, root, quiet) {
8876
- if (!existsSync29(memoriesDir)) return;
8911
+ if (!existsSync30(memoriesDir)) return;
8877
8912
  const all = await loadMemoriesFromDir23(memoriesDir);
8878
8913
  const top = all.filter(({ memory: memory2 }) => {
8879
8914
  const s = memory2.frontmatter.status;
@@ -8899,7 +8934,7 @@ async function injectBridge(bridgeFile, memoriesDir, maxMemories, root, quiet) {
8899
8934
  ` + block + `
8900
8935
 
8901
8936
  ${BRIDGE_END}`;
8902
- const fileExists = existsSync29(bridgeFile);
8937
+ const fileExists = existsSync30(bridgeFile);
8903
8938
  let existing = fileExists ? await readFile9(bridgeFile, "utf8") : "";
8904
8939
  existing = existing.replace(/\r\n/g, "\n");
8905
8940
  const startIdx = existing.indexOf(BRIDGE_START);
@@ -8950,7 +8985,7 @@ function collectSinceChanges(root, ref) {
8950
8985
  // src/commands/memory-add.ts
8951
8986
  import { createHash as createHash2 } from "crypto";
8952
8987
  import { mkdir as mkdir11, readFile as readFile10, writeFile as writeFile14 } from "fs/promises";
8953
- import { existsSync as existsSync30 } from "fs";
8988
+ import { existsSync as existsSync31 } from "fs";
8954
8989
  import path16 from "path";
8955
8990
  import "commander";
8956
8991
  import {
@@ -8992,7 +9027,7 @@ function registerMemoryAdd(memory2) {
8992
9027
  ).requiredOption("--type <type>", "skill | convention | decision | gotcha | architecture | glossary | attempt").option("--slug <slug>", "short kebab-case identifier used in the file name (auto-derived from --title/--body when omitted)").option("--title <text>", "memory title \u2014 becomes the first heading of the body").option("--scope <scope>", "personal | team | module (default: config default; team in autopilot)").option("--module <name>", "module name (required when scope=module)").option("--tags <csv>", "comma-separated tags for easier retrieval").option("--domain <domain>", "domain (e.g. transactions)").option("--author <author>", "author email or handle").option("--paths <csv>", "anchor to source files \u2014 used for staleness detection by haive sync").option("--files <csv>", "alias for --paths (matches the MCP `files` parameter)").option("--symbols <csv>", "anchor to specific symbols (class/function names)").option("--commit <sha>", "anchor to a specific commit SHA").option("--body <text>", "memory body content (Markdown) \u2014 overrides --title default body").option("--body-file <path>", "read memory body from a Markdown file \u2014 for long content").option("--no-auto-tag", "disable automatic tag suggestions inferred from anchor paths").option("--topic <key>", "stable key for upsert: if a memory with this topic+scope already exists, update it in-place (revision_count++)").option("-d, --dir <dir>", "project root").action(async (opts) => {
8993
9028
  const root = findProjectRoot13(opts.dir);
8994
9029
  const paths = resolveHaivePaths10(root);
8995
- if (!existsSync30(paths.haiveDir)) {
9030
+ if (!existsSync31(paths.haiveDir)) {
8996
9031
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
8997
9032
  process.exitCode = 1;
8998
9033
  return;
@@ -9004,7 +9039,7 @@ function registerMemoryAdd(memory2) {
9004
9039
  const inferredTags = autoTagsEnabled ? inferModulesFromPaths3(anchorPaths) : [];
9005
9040
  const mergedTags = Array.from(/* @__PURE__ */ new Set([...userTags, ...inferredTags]));
9006
9041
  if (anchorPaths.length > 0) {
9007
- const missing = anchorPaths.filter((p) => !existsSync30(path16.resolve(root, p)));
9042
+ const missing = anchorPaths.filter((p) => !existsSync31(path16.resolve(root, p)));
9008
9043
  if (missing.length > 0) {
9009
9044
  ui.warn(`Anchor path${missing.length > 1 ? "s" : ""} not found in project:`);
9010
9045
  for (const p of missing) ui.warn(` \u2717 ${p}`);
@@ -9017,7 +9052,7 @@ function registerMemoryAdd(memory2) {
9017
9052
  const slug = slugify(opts.slug ?? opts.title ?? opts.topic ?? opts.body ?? `${opts.type}-memory`);
9018
9053
  let body;
9019
9054
  if (opts.bodyFile !== void 0) {
9020
- if (!existsSync30(opts.bodyFile)) {
9055
+ if (!existsSync31(opts.bodyFile)) {
9021
9056
  ui.error(`--body-file not found: ${opts.bodyFile}`);
9022
9057
  process.exitCode = 1;
9023
9058
  return;
@@ -9033,7 +9068,7 @@ TODO \u2014 write the memory body.
9033
9068
  `;
9034
9069
  }
9035
9070
  const scope = opts.scope ?? config.defaultScope ?? "personal";
9036
- if (existsSync30(paths.memoriesDir)) {
9071
+ if (existsSync31(paths.memoriesDir)) {
9037
9072
  const incomingHash = createHash2("sha256").update(body.trim()).digest("hex").slice(0, 12);
9038
9073
  const allForHash = await loadMemoriesFromDir24(paths.memoriesDir);
9039
9074
  const hashDup = allForHash.find(
@@ -9046,7 +9081,7 @@ TODO \u2014 write the memory body.
9046
9081
  return;
9047
9082
  }
9048
9083
  }
9049
- if (opts.topic && existsSync30(paths.memoriesDir)) {
9084
+ if (opts.topic && existsSync31(paths.memoriesDir)) {
9050
9085
  const existing = await loadMemoriesFromDir24(paths.memoriesDir);
9051
9086
  const topicMatch = existing.find(
9052
9087
  ({ memory: memory3 }) => memory3.frontmatter.topic === opts.topic && memory3.frontmatter.scope === scope && (!opts.module || memory3.frontmatter.module === opts.module)
@@ -9091,12 +9126,12 @@ TODO \u2014 write the memory body.
9091
9126
  });
9092
9127
  const file = memoryFilePath6(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
9093
9128
  await mkdir11(path16.dirname(file), { recursive: true });
9094
- if (existsSync30(file)) {
9129
+ if (existsSync31(file)) {
9095
9130
  ui.error(`Memory already exists at ${file}`);
9096
9131
  process.exitCode = 1;
9097
9132
  return;
9098
9133
  }
9099
- if (existsSync30(paths.memoriesDir)) {
9134
+ if (existsSync31(paths.memoriesDir)) {
9100
9135
  const existing = await loadMemoriesFromDir24(paths.memoriesDir);
9101
9136
  const slugTokens = slug.toLowerCase().split(/[-_\s]+/).filter(Boolean);
9102
9137
  const similar = existing.filter(({ memory: memory3 }) => {
@@ -9188,7 +9223,7 @@ function slugify(value) {
9188
9223
  }
9189
9224
 
9190
9225
  // src/commands/memory-list.ts
9191
- import { existsSync as existsSync31 } from "fs";
9226
+ import { existsSync as existsSync33 } from "fs";
9192
9227
  import path17 from "path";
9193
9228
  import "commander";
9194
9229
  import { findProjectRoot as findProjectRoot14, resolveHaivePaths as resolveHaivePaths11 } from "@hiveai/core";
@@ -9205,7 +9240,7 @@ function registerMemoryList(memory2) {
9205
9240
  memory2.command("list").description("List memories with optional filters").option("--scope <scope>", "personal | team | module").option("--type <type>", "filter by type").option("--tag <tag>", "filter by tag").option("--module <name>", "filter by module name").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected,deprecated)").option("--show-rejected", "include rejected memories (hidden by default)").option("--limit <n>", "max memories to display").option("-d, --dir <dir>", "project root").action(async (opts) => {
9206
9241
  const root = findProjectRoot14(opts.dir);
9207
9242
  const paths = resolveHaivePaths11(root);
9208
- if (!existsSync31(paths.memoriesDir)) {
9243
+ if (!existsSync33(paths.memoriesDir)) {
9209
9244
  ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
9210
9245
  process.exitCode = 1;
9211
9246
  return;
@@ -9280,7 +9315,7 @@ function matchesFilters(loaded, opts) {
9280
9315
 
9281
9316
  // src/commands/memory-promote.ts
9282
9317
  import { mkdir as mkdir12, unlink as unlink2, writeFile as writeFile15 } from "fs/promises";
9283
- import { existsSync as existsSync33 } from "fs";
9318
+ import { existsSync as existsSync34 } from "fs";
9284
9319
  import path18 from "path";
9285
9320
  import "commander";
9286
9321
  import {
@@ -9293,7 +9328,7 @@ function registerMemoryPromote(memory2) {
9293
9328
  memory2.command("promote <id>").description("Promote a personal memory to team scope (status -> proposed)").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
9294
9329
  const root = findProjectRoot15(opts.dir);
9295
9330
  const paths = resolveHaivePaths12(root);
9296
- if (!existsSync33(paths.memoriesDir)) {
9331
+ if (!existsSync34(paths.memoriesDir)) {
9297
9332
  ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
9298
9333
  process.exitCode = 1;
9299
9334
  return;
@@ -9338,7 +9373,7 @@ function registerMemoryPromote(memory2) {
9338
9373
  }
9339
9374
 
9340
9375
  // src/commands/memory-approve.ts
9341
- import { existsSync as existsSync34 } from "fs";
9376
+ import { existsSync as existsSync35 } from "fs";
9342
9377
  import { writeFile as writeFile16 } from "fs/promises";
9343
9378
  import path19 from "path";
9344
9379
  import "commander";
@@ -9351,7 +9386,7 @@ function registerMemoryApprove(memory2) {
9351
9386
  memory2.command("approve [id]").description("Mark a memory as 'validated'. Use --all to bulk-approve all proposed/draft memories.").option("--all", "approve all proposed and draft memories at once").option("--pending", "approve all memories with status 'proposed'").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
9352
9387
  const root = findProjectRoot16(opts.dir);
9353
9388
  const paths = resolveHaivePaths13(root);
9354
- if (!existsSync34(paths.memoriesDir)) {
9389
+ if (!existsSync35(paths.memoriesDir)) {
9355
9390
  ui.error(`No .ai/memories at ${root}.`);
9356
9391
  process.exitCode = 1;
9357
9392
  return;
@@ -9410,7 +9445,7 @@ function registerMemoryApprove(memory2) {
9410
9445
 
9411
9446
  // src/commands/memory-update.ts
9412
9447
  import { readFile as readFile11, writeFile as writeFile17 } from "fs/promises";
9413
- import { existsSync as existsSync35 } from "fs";
9448
+ import { existsSync as existsSync36 } from "fs";
9414
9449
  import path20 from "path";
9415
9450
  import "commander";
9416
9451
  import {
@@ -9422,7 +9457,7 @@ function registerMemoryUpdate(memory2) {
9422
9457
  memory2.command("update <id>").description("Update body, tags, or anchor of an existing memory (preserves id and usage history)").option("--type <type>", "change the memory type (convention | decision | gotcha | architecture | glossary | skill | attempt)").option("--title <text>", "new title \u2014 replaces the first heading of the body").option("--body <text>", "new Markdown body \u2014 replaces the existing body").option("--body-file <path>", "read new body from a Markdown file \u2014 for long content").option("--tags <csv>", "new tags, comma-separated \u2014 fully replaces existing tags").option("--paths <csv>", "new anchor paths, comma-separated").option("--files <csv>", "alias for --paths (matches the MCP `files` parameter)").option("--symbols <csv>", "new anchor symbols, comma-separated").option("--commit <sha>", "new anchor commit SHA").option("--domain <domain>", "new domain label").option("--author <author>", "new author handle or email").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
9423
9458
  const root = findProjectRoot17(opts.dir);
9424
9459
  const paths = resolveHaivePaths14(root);
9425
- if (!existsSync35(paths.memoriesDir)) {
9460
+ if (!existsSync36(paths.memoriesDir)) {
9426
9461
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
9427
9462
  process.exitCode = 1;
9428
9463
  return;
@@ -9464,7 +9499,7 @@ function registerMemoryUpdate(memory2) {
9464
9499
  if (opts.author !== void 0) updated.push("author");
9465
9500
  let newBody;
9466
9501
  if (opts.bodyFile !== void 0) {
9467
- if (!existsSync35(opts.bodyFile)) {
9502
+ if (!existsSync36(opts.bodyFile)) {
9468
9503
  ui.error(`--body-file not found: ${opts.bodyFile}`);
9469
9504
  process.exitCode = 1;
9470
9505
  return;
@@ -9510,7 +9545,7 @@ function parseCsv3(value) {
9510
9545
 
9511
9546
  // src/commands/memory-auto-promote.ts
9512
9547
  import { writeFile as writeFile18 } from "fs/promises";
9513
- import { existsSync as existsSync36 } from "fs";
9548
+ import { existsSync as existsSync37 } from "fs";
9514
9549
  import path21 from "path";
9515
9550
  import "commander";
9516
9551
  import {
@@ -9530,7 +9565,7 @@ function registerMemoryAutoPromote(memory2) {
9530
9565
  ).option("--apply", "actually write status=validated to disk (default: dry-run)").option("-d, --dir <dir>", "project root").action(async (opts) => {
9531
9566
  const root = findProjectRoot18(opts.dir);
9532
9567
  const paths = resolveHaivePaths15(root);
9533
- if (!existsSync36(paths.memoriesDir)) {
9568
+ if (!existsSync37(paths.memoriesDir)) {
9534
9569
  ui.error(`No .ai/memories at ${root}.`);
9535
9570
  process.exitCode = 1;
9536
9571
  return;
@@ -9573,7 +9608,7 @@ function registerMemoryAutoPromote(memory2) {
9573
9608
 
9574
9609
  // src/commands/memory-edit.ts
9575
9610
  import { spawn as spawn3 } from "child_process";
9576
- import { existsSync as existsSync37 } from "fs";
9611
+ import { existsSync as existsSync38 } from "fs";
9577
9612
  import { readFile as readFile12 } from "fs/promises";
9578
9613
  import path23 from "path";
9579
9614
  import "commander";
@@ -9586,7 +9621,7 @@ function registerMemoryEdit(memory2) {
9586
9621
  memory2.command("edit <id>").description("Open a memory in $EDITOR and re-validate when you save").option("-e, --editor <cmd>", "editor command (defaults to $EDITOR or 'vi')").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
9587
9622
  const root = findProjectRoot19(opts.dir);
9588
9623
  const paths = resolveHaivePaths16(root);
9589
- if (!existsSync37(paths.memoriesDir)) {
9624
+ if (!existsSync38(paths.memoriesDir)) {
9590
9625
  ui.error(`No .ai/memories at ${root}.`);
9591
9626
  process.exitCode = 1;
9592
9627
  return;
@@ -9626,7 +9661,7 @@ function runEditor(editor, file) {
9626
9661
  }
9627
9662
 
9628
9663
  // src/commands/memory-for-files.ts
9629
- import { existsSync as existsSync38 } from "fs";
9664
+ import { existsSync as existsSync39 } from "fs";
9630
9665
  import path24 from "path";
9631
9666
  import "commander";
9632
9667
  import {
@@ -9642,7 +9677,7 @@ function registerMemoryForFiles(memory2) {
9642
9677
  memory2.command("for-files <files...>").description("Show memories relevant to the given files (anchor overlap, module, domain)").option("-d, --dir <dir>", "project root").action(async (files, opts) => {
9643
9678
  const root = findProjectRoot20(opts.dir);
9644
9679
  const paths = resolveHaivePaths17(root);
9645
- if (!existsSync38(paths.memoriesDir)) {
9680
+ if (!existsSync39(paths.memoriesDir)) {
9646
9681
  ui.error(`No .ai/memories at ${root}.`);
9647
9682
  process.exitCode = 1;
9648
9683
  return;
@@ -9754,7 +9789,7 @@ function printGroup(root, label, loaded, usage) {
9754
9789
  }
9755
9790
 
9756
9791
  // src/commands/memory-hot.ts
9757
- import { existsSync as existsSync39 } from "fs";
9792
+ import { existsSync as existsSync40 } from "fs";
9758
9793
  import path25 from "path";
9759
9794
  import "commander";
9760
9795
  import {
@@ -9769,7 +9804,7 @@ function registerMemoryHot(memory2) {
9769
9804
  ).option("--threshold <n>", "minimum read_count to qualify (default: 3)", "3").option("--status <status>", "limit to one status (default: draft + proposed)").option("-d, --dir <dir>", "project root").action(async (opts) => {
9770
9805
  const root = findProjectRoot21(opts.dir);
9771
9806
  const paths = resolveHaivePaths18(root);
9772
- if (!existsSync39(paths.memoriesDir)) {
9807
+ if (!existsSync40(paths.memoriesDir)) {
9773
9808
  ui.error(`No .ai/memories at ${root}.`);
9774
9809
  process.exitCode = 1;
9775
9810
  return;
@@ -9808,7 +9843,7 @@ function registerMemoryHot(memory2) {
9808
9843
 
9809
9844
  // src/commands/memory-tried.ts
9810
9845
  import { mkdir as mkdir13, writeFile as writeFile19 } from "fs/promises";
9811
- import { existsSync as existsSync40 } from "fs";
9846
+ import { existsSync as existsSync41 } from "fs";
9812
9847
  import path26 from "path";
9813
9848
  import "commander";
9814
9849
  import {
@@ -9838,7 +9873,7 @@ function registerMemoryTried(memory2) {
9838
9873
  ).requiredOption("--what <text>", "what approach was tried (short, descriptive title)").requiredOption("--why-failed <text>", "why it failed or should NOT be used (include the exact error if possible)").option("--instead <text>", "the correct approach to use instead").option("--scope <scope>", "personal | team | module", "personal").option("--module <name>", "module name (required when scope=module)").option("--tags <csv>", "comma-separated tags").option("--paths <csv>", "anchor paths, comma-separated").option("--files <csv>", "alias for --paths (matches the MCP `files` parameter)").option("--author <author>", "author email or handle").option("-d, --dir <dir>", "project root").action(async (opts) => {
9839
9874
  const root = findProjectRoot22(opts.dir);
9840
9875
  const paths = resolveHaivePaths19(root);
9841
- if (!existsSync40(paths.haiveDir)) {
9876
+ if (!existsSync41(paths.haiveDir)) {
9842
9877
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
9843
9878
  process.exitCode = 1;
9844
9879
  return;
@@ -9866,7 +9901,7 @@ function registerMemoryTried(memory2) {
9866
9901
  }
9867
9902
  const file = memoryFilePath8(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
9868
9903
  await mkdir13(path26.dirname(file), { recursive: true });
9869
- if (existsSync40(file)) {
9904
+ if (existsSync41(file)) {
9870
9905
  ui.error(`Memory already exists at ${file}`);
9871
9906
  process.exitCode = 1;
9872
9907
  return;
@@ -9884,7 +9919,7 @@ function parseCsv4(value) {
9884
9919
 
9885
9920
  // src/commands/memory-seed.ts
9886
9921
  import { readFile as readFile13 } from "fs/promises";
9887
- import { existsSync as existsSync41 } from "fs";
9922
+ import { existsSync as existsSync43 } from "fs";
9888
9923
  import path27 from "path";
9889
9924
  import "commander";
9890
9925
  import {
@@ -9924,7 +9959,7 @@ function registerMemorySeed(memory2) {
9924
9959
  }
9925
9960
  return;
9926
9961
  }
9927
- if (!existsSync41(paths.haiveDir)) {
9962
+ if (!existsSync43(paths.haiveDir)) {
9928
9963
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
9929
9964
  process.exitCode = 1;
9930
9965
  return;
@@ -9980,7 +10015,7 @@ function registerMemorySeed(memory2) {
9980
10015
  }
9981
10016
 
9982
10017
  // src/commands/memory-pending.ts
9983
- import { existsSync as existsSync43 } from "fs";
10018
+ import { existsSync as existsSync44 } from "fs";
9984
10019
  import path28 from "path";
9985
10020
  import "commander";
9986
10021
  import {
@@ -9993,7 +10028,7 @@ function registerMemoryPending(memory2) {
9993
10028
  memory2.command("pending").description("List draft and proposed memories awaiting review (sorted by reads desc).\n\n draft = created but not yet activated \xB7 proposed = promoted, awaiting team validation").option("--scope <scope>", "filter by scope (personal | team | module)").option("-d, --dir <dir>", "project root").action(async (opts) => {
9994
10029
  const root = findProjectRoot24(opts.dir);
9995
10030
  const paths = resolveHaivePaths21(root);
9996
- if (!existsSync43(paths.memoriesDir)) {
10031
+ if (!existsSync44(paths.memoriesDir)) {
9997
10032
  ui.error(`No .ai/memories at ${root}.`);
9998
10033
  process.exitCode = 1;
9999
10034
  return;
@@ -10051,7 +10086,7 @@ function registerMemoryPending(memory2) {
10051
10086
  }
10052
10087
 
10053
10088
  // src/commands/memory-query.ts
10054
- import { existsSync as existsSync44 } from "fs";
10089
+ import { existsSync as existsSync45 } from "fs";
10055
10090
  import path29 from "path";
10056
10091
  import "commander";
10057
10092
  import {
@@ -10068,7 +10103,7 @@ function registerMemoryQuery(memory2) {
10068
10103
  memory2.command("query <text>").alias("search").description("Search memories by id, tag, or substring (AND, OR fallback). Alias: search").option("-d, --dir <dir>", "project root").option("--limit <n>", "max results", "20").option("--scope <scope>", "personal | team | module").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected)").option("--show-rejected", "include rejected memories (hidden by default)").action(async (text, opts) => {
10069
10104
  const root = findProjectRoot25(opts.dir);
10070
10105
  const paths = resolveHaivePaths22(root);
10071
- if (!existsSync44(paths.memoriesDir)) {
10106
+ if (!existsSync45(paths.memoriesDir)) {
10072
10107
  ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
10073
10108
  process.exitCode = 1;
10074
10109
  return;
@@ -10127,7 +10162,7 @@ ${top.length} of ${matches.length} match${matches.length === 1 ? "" : "es"}`)
10127
10162
 
10128
10163
  // src/commands/memory-reject.ts
10129
10164
  import { writeFile as writeFile20 } from "fs/promises";
10130
- import { existsSync as existsSync45 } from "fs";
10165
+ import { existsSync as existsSync46 } from "fs";
10131
10166
  import "commander";
10132
10167
  import {
10133
10168
  findProjectRoot as findProjectRoot26,
@@ -10141,7 +10176,7 @@ function registerMemoryReject(memory2) {
10141
10176
  memory2.command("reject <id>").description("Record a rejection (blocks auto-promotion and lowers confidence)").option("-r, --reason <reason>", "why this memory is being rejected").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
10142
10177
  const root = findProjectRoot26(opts.dir);
10143
10178
  const paths = resolveHaivePaths23(root);
10144
- if (!existsSync45(paths.memoriesDir)) {
10179
+ if (!existsSync46(paths.memoriesDir)) {
10145
10180
  ui.error(`No .ai/memories at ${root}.`);
10146
10181
  process.exitCode = 1;
10147
10182
  return;
@@ -10177,7 +10212,7 @@ function registerMemoryReject(memory2) {
10177
10212
  }
10178
10213
 
10179
10214
  // src/commands/memory-rm.ts
10180
- import { existsSync as existsSync46 } from "fs";
10215
+ import { existsSync as existsSync47 } from "fs";
10181
10216
  import { unlink as unlink3 } from "fs/promises";
10182
10217
  import path30 from "path";
10183
10218
  import { createInterface as createInterface2 } from "readline/promises";
@@ -10192,7 +10227,7 @@ function registerMemoryRm(memory2) {
10192
10227
  memory2.command("rm <id>").description("Delete a memory file (and its usage entry by default)").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) => {
10193
10228
  const root = findProjectRoot27(opts.dir);
10194
10229
  const paths = resolveHaivePaths24(root);
10195
- if (!existsSync46(paths.memoriesDir)) {
10230
+ if (!existsSync47(paths.memoriesDir)) {
10196
10231
  ui.error(`No .ai/memories at ${root}.`);
10197
10232
  process.exitCode = 1;
10198
10233
  return;
@@ -10228,7 +10263,7 @@ function registerMemoryRm(memory2) {
10228
10263
  }
10229
10264
 
10230
10265
  // src/commands/memory-show.ts
10231
- import { existsSync as existsSync47 } from "fs";
10266
+ import { existsSync as existsSync48 } from "fs";
10232
10267
  import { readFile as readFile14 } from "fs/promises";
10233
10268
  import path31 from "path";
10234
10269
  import "commander";
@@ -10243,7 +10278,7 @@ function registerMemoryShow(memory2) {
10243
10278
  memory2.command("show <id>").description("Print a memory's frontmatter, body, and confidence/usage").option("--raw", "print the raw file contents instead of a summary").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
10244
10279
  const root = findProjectRoot28(opts.dir);
10245
10280
  const paths = resolveHaivePaths25(root);
10246
- if (!existsSync47(paths.memoriesDir)) {
10281
+ if (!existsSync48(paths.memoriesDir)) {
10247
10282
  ui.error(`No .ai/memories at ${root}.`);
10248
10283
  process.exitCode = 1;
10249
10284
  return;
@@ -10287,7 +10322,7 @@ function registerMemoryShow(memory2) {
10287
10322
  }
10288
10323
 
10289
10324
  // src/commands/memory-stats.ts
10290
- import { existsSync as existsSync48 } from "fs";
10325
+ import { existsSync as existsSync49 } from "fs";
10291
10326
  import path33 from "path";
10292
10327
  import "commander";
10293
10328
  import {
@@ -10301,7 +10336,7 @@ function registerMemoryStats(memory2) {
10301
10336
  memory2.command("stats").description("Show usage stats and confidence levels per memory").option("--id <id>", "show stats for a single memory id").option("-d, --dir <dir>", "project root").action(async (opts) => {
10302
10337
  const root = findProjectRoot29(opts.dir);
10303
10338
  const paths = resolveHaivePaths26(root);
10304
- if (!existsSync48(paths.memoriesDir)) {
10339
+ if (!existsSync49(paths.memoriesDir)) {
10305
10340
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
10306
10341
  process.exitCode = 1;
10307
10342
  return;
@@ -10333,7 +10368,7 @@ function registerMemoryStats(memory2) {
10333
10368
 
10334
10369
  // src/commands/memory-verify.ts
10335
10370
  import { writeFile as writeFile21 } from "fs/promises";
10336
- import { existsSync as existsSync49 } from "fs";
10371
+ import { existsSync as existsSync50 } from "fs";
10337
10372
  import path34 from "path";
10338
10373
  import "commander";
10339
10374
  import {
@@ -10348,7 +10383,7 @@ function registerMemoryVerify(memory2) {
10348
10383
  ).option("--id <id>", "verify a single memory by id").option("--all", "verify every memory (default if --id is omitted)").option("--update", "write status=stale or status=validated back to disk").option("--json", "emit machine-readable JSON (for CI / agents)").option("-d, --dir <dir>", "project root").action(async (opts) => {
10349
10384
  const root = findProjectRoot30(opts.dir);
10350
10385
  const paths = resolveHaivePaths27(root);
10351
- if (!existsSync49(paths.memoriesDir)) {
10386
+ if (!existsSync50(paths.memoriesDir)) {
10352
10387
  if (opts.json) {
10353
10388
  console.log(JSON.stringify({ error: "not-initialized", root }, null, 2));
10354
10389
  } else {
@@ -10469,7 +10504,7 @@ function applyVerification2(mem, result) {
10469
10504
 
10470
10505
  // src/commands/memory-import.ts
10471
10506
  import { readFile as readFile15 } from "fs/promises";
10472
- import { existsSync as existsSync50 } from "fs";
10507
+ import { existsSync as existsSync51 } from "fs";
10473
10508
  import "commander";
10474
10509
  import {
10475
10510
  findProjectRoot as findProjectRoot31,
@@ -10481,12 +10516,12 @@ function registerMemoryImport(memory2) {
10481
10516
  ).requiredOption("--from <file>", "Markdown/text file to import from").option("--scope <scope>", "personal | team (default: team)", "team").option("-d, --dir <dir>", "project root").action(async (opts) => {
10482
10517
  const root = findProjectRoot31(opts.dir);
10483
10518
  const paths = resolveHaivePaths28(root);
10484
- if (!existsSync50(paths.haiveDir)) {
10519
+ if (!existsSync51(paths.haiveDir)) {
10485
10520
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
10486
10521
  process.exitCode = 1;
10487
10522
  return;
10488
10523
  }
10489
- if (!existsSync50(opts.from)) {
10524
+ if (!existsSync51(opts.from)) {
10490
10525
  ui.error(`File not found: ${opts.from}`);
10491
10526
  process.exitCode = 1;
10492
10527
  return;
@@ -10519,7 +10554,7 @@ function registerMemoryImport(memory2) {
10519
10554
  }
10520
10555
 
10521
10556
  // src/commands/memory-import-changelog.ts
10522
- import { existsSync as existsSync51 } from "fs";
10557
+ import { existsSync as existsSync53 } from "fs";
10523
10558
  import { readFile as readFile16, mkdir as mkdir14, writeFile as writeFile23 } from "fs/promises";
10524
10559
  import path35 from "path";
10525
10560
  import "commander";
@@ -10594,7 +10629,7 @@ function registerMemoryImportChangelog(memory2) {
10594
10629
  const root = findProjectRoot32(opts.dir);
10595
10630
  const paths = resolveHaivePaths29(root);
10596
10631
  const changelogPath = path35.resolve(root, opts.fromChangelog);
10597
- if (!existsSync51(changelogPath)) {
10632
+ if (!existsSync53(changelogPath)) {
10598
10633
  ui.error(`CHANGELOG not found: ${changelogPath}`);
10599
10634
  process.exitCode = 1;
10600
10635
  return;
@@ -10681,7 +10716,7 @@ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${p
10681
10716
  }
10682
10717
 
10683
10718
  // src/commands/memory-digest.ts
10684
- import { existsSync as existsSync53 } from "fs";
10719
+ import { existsSync as existsSync54 } from "fs";
10685
10720
  import { writeFile as writeFile24 } from "fs/promises";
10686
10721
  import path36 from "path";
10687
10722
  import "commander";
@@ -10706,7 +10741,7 @@ function registerMemoryDigest(program2) {
10706
10741
  ).option("--days <n>", "look-back window in days (default: 7)", "7").option("--scope <scope>", "personal | team | module | all (default: team)", "team").option("--out <file>", "write digest to a file instead of stdout").option("-d, --dir <dir>", "project root").action(async (opts) => {
10707
10742
  const root = findProjectRoot33(opts.dir);
10708
10743
  const paths = resolveHaivePaths30(root);
10709
- if (!existsSync53(paths.memoriesDir)) {
10744
+ if (!existsSync54(paths.memoriesDir)) {
10710
10745
  ui.error("No .ai/memories found. Run `haive init` first.");
10711
10746
  process.exitCode = 1;
10712
10747
  return;
@@ -10789,7 +10824,7 @@ function registerMemoryDigest(program2) {
10789
10824
 
10790
10825
  // src/commands/session-end.ts
10791
10826
  import { writeFile as writeFile25, mkdir as mkdir15, readFile as readFile17, rm as rm2 } from "fs/promises";
10792
- import { existsSync as existsSync54 } from "fs";
10827
+ import { existsSync as existsSync55 } from "fs";
10793
10828
  import { spawn as spawn4 } from "child_process";
10794
10829
  import path37 from "path";
10795
10830
  import "commander";
@@ -10803,7 +10838,7 @@ import {
10803
10838
  } from "@hiveai/core";
10804
10839
  async function buildAutoRecap(paths) {
10805
10840
  const obsFile = path37.join(paths.haiveDir, ".cache", "observations.jsonl");
10806
- if (!existsSync54(obsFile)) return await buildGitAutoRecap(paths);
10841
+ if (!existsSync55(obsFile)) return await buildGitAutoRecap(paths);
10807
10842
  const raw = await readFile17(obsFile, "utf8").catch(() => "");
10808
10843
  if (!raw.trim()) return await buildGitAutoRecap(paths);
10809
10844
  const lines = raw.split("\n").filter(Boolean);
@@ -10994,7 +11029,7 @@ function registerSessionEnd(session2) {
10994
11029
  ).option("--goal <text>", "what you were trying to accomplish (1\u20132 sentences)").option("--accomplished <text>", "what was actually done (bullet list recommended)").option("--discoveries <text>", "bugs, surprises, or inconsistencies found during this session").option("--files <csv>", "key files touched, comma-separated (used as anchor for staleness detection)").option("--next <text>", "what should happen next (for the next session or a teammate)").option("--scope <scope>", "personal | team | module (default: personal)", "personal").option("--module <name>", "module name (required when scope=module)").option("--auto", "synthesize the recap from .ai/.cache/observations.jsonl (used by Claude Code SessionEnd hook)").option("--quiet", "suppress non-error output (for hook use)").option("-d, --dir <dir>", "project root").action(async (opts) => {
10995
11030
  const root = findProjectRoot34(opts.dir);
10996
11031
  const paths = resolveHaivePaths31(root);
10997
- if (!existsSync54(paths.haiveDir)) {
11032
+ if (!existsSync55(paths.haiveDir)) {
10998
11033
  if (opts.auto || opts.quiet) return;
10999
11034
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
11000
11035
  process.exitCode = 1;
@@ -11027,7 +11062,7 @@ function registerSessionEnd(session2) {
11027
11062
  });
11028
11063
  const topic = recapTopic2(scope, opts.module);
11029
11064
  const filesTouched = parseCsv5(resolvedFiles).map((p) => normalizeAnchorPath(root, p));
11030
- const missingPaths = filesTouched.filter((p) => !existsSync54(path37.resolve(root, p)));
11065
+ const missingPaths = filesTouched.filter((p) => !existsSync55(path37.resolve(root, p)));
11031
11066
  if (missingPaths.length > 0 && !opts.quiet) {
11032
11067
  ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
11033
11068
  for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
@@ -11035,10 +11070,10 @@ function registerSessionEnd(session2) {
11035
11070
  const cleanupObservations = async () => {
11036
11071
  if (!opts.auto) return;
11037
11072
  const obsFile = path37.join(paths.haiveDir, ".cache", "observations.jsonl");
11038
- if (existsSync54(obsFile)) await rm2(obsFile).catch(() => {
11073
+ if (existsSync55(obsFile)) await rm2(obsFile).catch(() => {
11039
11074
  });
11040
11075
  };
11041
- if (existsSync54(paths.memoriesDir)) {
11076
+ if (existsSync55(paths.memoriesDir)) {
11042
11077
  const existing = await loadMemoriesFromDir27(paths.memoriesDir);
11043
11078
  const topicMatch = existing.find(
11044
11079
  ({ memory: memory2 }) => memory2.frontmatter.topic === topic && memory2.frontmatter.scope === scope && (!opts.module || memory2.frontmatter.module === opts.module)
@@ -11100,7 +11135,7 @@ function normalizeAnchorPath(root, filePath) {
11100
11135
  }
11101
11136
 
11102
11137
  // src/commands/snapshot.ts
11103
- import { existsSync as existsSync55 } from "fs";
11138
+ import { existsSync as existsSync56 } from "fs";
11104
11139
  import { readdir as readdir4 } from "fs/promises";
11105
11140
  import path38 from "path";
11106
11141
  import "commander";
@@ -11135,14 +11170,14 @@ function registerSnapshot(program2) {
11135
11170
  ).option("--diff", "compare the contract against its stored snapshot").option("--list", "list all stored contract snapshots").option("-d, --dir <dir>", "project root").action(async (opts) => {
11136
11171
  const root = findProjectRoot35(opts.dir);
11137
11172
  const paths = resolveHaivePaths32(root);
11138
- if (!existsSync55(paths.haiveDir)) {
11173
+ if (!existsSync56(paths.haiveDir)) {
11139
11174
  ui.error("No .ai/ found. Run `haive init` first.");
11140
11175
  process.exitCode = 1;
11141
11176
  return;
11142
11177
  }
11143
11178
  if (opts.list) {
11144
11179
  const contractsDir = path38.join(paths.haiveDir, "contracts");
11145
- if (!existsSync55(contractsDir)) {
11180
+ if (!existsSync56(contractsDir)) {
11146
11181
  console.log(ui.dim("No contract snapshots found."));
11147
11182
  return;
11148
11183
  }
@@ -11266,7 +11301,7 @@ function detectFormat(filePath) {
11266
11301
  }
11267
11302
 
11268
11303
  // src/commands/hub.ts
11269
- import { existsSync as existsSync56 } from "fs";
11304
+ import { existsSync as existsSync57 } from "fs";
11270
11305
  import { mkdir as mkdir16, readFile as readFile18, writeFile as writeFile26, copyFile } from "fs/promises";
11271
11306
  import path39 from "path";
11272
11307
  import { spawnSync as spawnSync5 } from "child_process";
@@ -11368,7 +11403,7 @@ Next steps:
11368
11403
  return;
11369
11404
  }
11370
11405
  const hubRoot = path39.resolve(root, config.hubPath);
11371
- if (!existsSync56(hubRoot)) {
11406
+ if (!existsSync57(hubRoot)) {
11372
11407
  ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
11373
11408
  process.exitCode = 1;
11374
11409
  return;
@@ -11438,7 +11473,7 @@ Next steps:
11438
11473
  }
11439
11474
  const hubRoot = path39.resolve(root, config.hubPath);
11440
11475
  const hubSharedDir = path39.join(hubRoot, ".ai", "memories", "shared");
11441
- if (!existsSync56(hubSharedDir)) {
11476
+ if (!existsSync57(hubSharedDir)) {
11442
11477
  ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
11443
11478
  return;
11444
11479
  }
@@ -11493,7 +11528,7 @@ Next steps:
11493
11528
  ` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
11494
11529
  );
11495
11530
  const sharedDir = path39.join(paths.memoriesDir, "shared");
11496
- if (existsSync56(sharedDir)) {
11531
+ if (existsSync57(sharedDir)) {
11497
11532
  const { readdir: readdir7 } = await import("fs/promises");
11498
11533
  const sources = (await readdir7(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
11499
11534
  console.log(`
@@ -11522,7 +11557,7 @@ Next steps:
11522
11557
 
11523
11558
  // src/commands/stats.ts
11524
11559
  import "commander";
11525
- import { existsSync as existsSync57 } from "fs";
11560
+ import { existsSync as existsSync58 } from "fs";
11526
11561
  import { mkdir as mkdir17, writeFile as writeFile27 } from "fs/promises";
11527
11562
  import path40 from "path";
11528
11563
  import {
@@ -11600,7 +11635,7 @@ async function writeRoiReport(paths, root, sinceRaw, outRelative) {
11600
11635
  const size = await usageLogSize(paths);
11601
11636
  let events = await readUsageEvents2(paths);
11602
11637
  let memoryCount = { team: 0, personal: 0, total_skipped_session: 0 };
11603
- if (existsSync57(paths.memoriesDir)) {
11638
+ if (existsSync58(paths.memoriesDir)) {
11604
11639
  const mems = await loadMemoriesFromDir29(paths.memoriesDir);
11605
11640
  for (const { memory: memory2 } of mems) {
11606
11641
  const fm = memory2.frontmatter;
@@ -11820,7 +11855,7 @@ function summarize(name, t0, payload, notes) {
11820
11855
  }
11821
11856
 
11822
11857
  // src/commands/benchmark.ts
11823
- import { existsSync as existsSync58 } from "fs";
11858
+ import { existsSync as existsSync59 } from "fs";
11824
11859
  import { readdir as readdir5, readFile as readFile19, writeFile as writeFile28 } from "fs/promises";
11825
11860
  import path41 from "path";
11826
11861
  import "commander";
@@ -11868,14 +11903,14 @@ function resolveBenchmarkRoot(dir) {
11868
11903
  return path41.join(projectRoot, candidate);
11869
11904
  }
11870
11905
  async function collectRows(root) {
11871
- if (!existsSync58(root)) throw new Error(`Benchmark directory not found: ${root}`);
11906
+ if (!existsSync59(root)) throw new Error(`Benchmark directory not found: ${root}`);
11872
11907
  const entries = await readdir5(root, { withFileTypes: true });
11873
11908
  const rows = [];
11874
11909
  for (const entry of entries) {
11875
11910
  if (!entry.isDirectory()) continue;
11876
11911
  const fixtureDir = path41.join(root, entry.name);
11877
11912
  const reportFile = path41.join(fixtureDir, "BENCHMARK_AGENT_REPORT.md");
11878
- if (!existsSync58(reportFile)) continue;
11913
+ if (!existsSync59(reportFile)) continue;
11879
11914
  const report = await readFile19(reportFile, "utf8");
11880
11915
  rows.push(parseAgentReport(entry.name, report));
11881
11916
  }
@@ -11966,7 +12001,7 @@ function escapeRegExp(value) {
11966
12001
 
11967
12002
  // src/commands/memory-suggest.ts
11968
12003
  import { mkdir as mkdir18, writeFile as writeFile29 } from "fs/promises";
11969
- import { existsSync as existsSync59 } from "fs";
12004
+ import { existsSync as existsSync60 } from "fs";
11970
12005
  import path43 from "path";
11971
12006
  import "commander";
11972
12007
  import {
@@ -12045,7 +12080,7 @@ function registerMemorySuggest(memory2) {
12045
12080
  }
12046
12081
  const created = [];
12047
12082
  const skipped = [];
12048
- const existing = existsSync59(paths.memoriesDir) ? await loadMemoriesFromDir30(paths.memoriesDir) : [];
12083
+ const existing = existsSync60(paths.memoriesDir) ? await loadMemoriesFromDir30(paths.memoriesDir) : [];
12049
12084
  for (const s of top) {
12050
12085
  const slug = slugify2(s.query);
12051
12086
  if (!slug) {
@@ -12069,7 +12104,7 @@ function registerMemorySuggest(memory2) {
12069
12104
  const body = renderTemplate(s, fm.id, status);
12070
12105
  const file = memoryFilePath10(paths, fm.scope, fm.id, fm.module);
12071
12106
  await mkdir18(path43.dirname(file), { recursive: true });
12072
- if (existsSync59(file)) {
12107
+ if (existsSync60(file)) {
12073
12108
  skipped.push({ query: s.query, reason: `file already exists at ${path43.relative(root, file)}` });
12074
12109
  continue;
12075
12110
  }
@@ -12172,7 +12207,7 @@ function truncate2(text, max) {
12172
12207
  }
12173
12208
 
12174
12209
  // src/commands/memory-archive.ts
12175
- import { existsSync as existsSync60 } from "fs";
12210
+ import { existsSync as existsSync61 } from "fs";
12176
12211
  import { writeFile as writeFile30 } from "fs/promises";
12177
12212
  import path44 from "path";
12178
12213
  import "commander";
@@ -12193,7 +12228,7 @@ function registerMemoryArchive(memory2) {
12193
12228
  ).option("--since <window>", "minimum age since last read (e.g. '180d', '6m'). Default: enforcement.decayAfterDays or 180d").option("--type <type>", "limit to a memory type (default 'attempt'). Pass 'all' to scan all types.", "attempt").option("--unread", "decay by unread-age ALONE (ignore anchor status) \u2014 more aggressive corpus hygiene", false).option("--apply", "actually rewrite files (default: dry run)", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
12194
12229
  const root = findProjectRoot41(opts.dir);
12195
12230
  const paths = resolveHaivePaths37(root);
12196
- if (!existsSync60(paths.memoriesDir)) {
12231
+ if (!existsSync61(paths.memoriesDir)) {
12197
12232
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
12198
12233
  process.exitCode = 1;
12199
12234
  return;
@@ -12218,7 +12253,7 @@ function registerMemoryArchive(memory2) {
12218
12253
  if (fm.status === "deprecated" || fm.status === "rejected") continue;
12219
12254
  const retired = retirementSignal2(fm, mem.body);
12220
12255
  const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
12221
- const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync60(path44.join(paths.root, p)));
12256
+ const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync61(path44.join(paths.root, p)));
12222
12257
  const isAnchorless = !hasAnyAnchor;
12223
12258
  if (!retired.retired && !opts.unread && !isAnchorless && !allPathsGone) continue;
12224
12259
  const u = getUsage18(usage, fm.id);
@@ -12293,7 +12328,7 @@ function parseDays(input) {
12293
12328
  }
12294
12329
 
12295
12330
  // src/commands/doctor.ts
12296
- import { existsSync as existsSync61, statSync as statSync2 } from "fs";
12331
+ import { existsSync as existsSync63, statSync as statSync2 } from "fs";
12297
12332
  import { readFile as readFile20, stat, writeFile as writeFile31 } from "fs/promises";
12298
12333
  import path45 from "path";
12299
12334
  import { execFileSync, execSync as execSync3 } from "child_process";
@@ -12302,6 +12337,7 @@ import {
12302
12337
  codeMapPath as codeMapPath2,
12303
12338
  findProjectRoot as findProjectRoot42,
12304
12339
  getUsage as getUsage19,
12340
+ isStackPackSeed as isStackPackSeed4,
12305
12341
  loadCodeMap as loadCodeMap7,
12306
12342
  loadConfig as loadConfig11,
12307
12343
  loadMemoriesFromDir as loadMemoriesFromDir33,
@@ -12319,7 +12355,7 @@ function registerDoctor(program2) {
12319
12355
  const findings = [];
12320
12356
  const repairs = [];
12321
12357
  const config = await loadConfig11(paths);
12322
- if (!existsSync61(paths.haiveDir)) {
12358
+ if (!existsSync63(paths.haiveDir)) {
12323
12359
  findings.push({
12324
12360
  severity: "error",
12325
12361
  code: "not-initialized",
@@ -12340,7 +12376,7 @@ function registerDoctor(program2) {
12340
12376
  })
12341
12377
  );
12342
12378
  }
12343
- if (!existsSync61(paths.projectContext)) {
12379
+ if (!existsSync63(paths.projectContext)) {
12344
12380
  findings.push({
12345
12381
  severity: "warn",
12346
12382
  code: "no-project-context",
@@ -12369,7 +12405,7 @@ function registerDoctor(program2) {
12369
12405
  });
12370
12406
  }
12371
12407
  }
12372
- const memories = existsSync61(paths.memoriesDir) ? await loadMemoriesFromDir33(paths.memoriesDir) : [];
12408
+ const memories = existsSync63(paths.memoriesDir) ? await loadMemoriesFromDir33(paths.memoriesDir) : [];
12373
12409
  const now = Date.now();
12374
12410
  if (memories.length === 0) {
12375
12411
  findings.push({
@@ -12413,16 +12449,25 @@ function registerDoctor(program2) {
12413
12449
  fix: "haive memory approve <id> # activate\nhaive memory rm <id> # or delete if obsolete"
12414
12450
  });
12415
12451
  }
12416
- const anchorless = memories.filter(
12452
+ const policyMemories = memories.filter((m) => !isStackPackSeed4(m.memory.frontmatter));
12453
+ const anchorless = policyMemories.filter(
12417
12454
  (m) => m.memory.frontmatter.anchor.paths.length === 0 && m.memory.frontmatter.anchor.symbols.length === 0 && m.memory.frontmatter.type !== "session_recap" && m.memory.frontmatter.type !== "glossary" && m.memory.frontmatter.type !== "skill"
12418
12455
  );
12419
- if (anchorless.length / Math.max(memories.length, 1) > 0.3) {
12456
+ const stackSeeds = memories.filter((m) => isStackPackSeed4(m.memory.frontmatter));
12457
+ if (anchorless.length / Math.max(policyMemories.length, 1) > 0.3) {
12420
12458
  findings.push({
12421
12459
  severity: "warn",
12422
12460
  code: "anchorless-majority",
12423
- message: `${anchorless.length}/${memories.length} memories have no anchor path/symbol \u2014 staleness undetectable.`,
12461
+ message: `${anchorless.length}/${policyMemories.length} repo-specific memories have no anchor path/symbol \u2014 staleness undetectable.`,
12424
12462
  fix: "Add `paths:` + `symbols:` to mem_save calls to enable haive memory verify."
12425
12463
  });
12464
+ } else if (stackSeeds.length > 0 && policyMemories.length === 0) {
12465
+ findings.push({
12466
+ severity: "info",
12467
+ code: "stack-pack-seeds",
12468
+ message: `${stackSeeds.length} starter stack memor${stackSeeds.length === 1 ? "y is" : "ies are"} present as generic background guidance.`,
12469
+ fix: "Replace or anchor stack-pack seeds when they become repo-specific policy."
12470
+ });
12426
12471
  }
12427
12472
  const decayCandidates = memories.filter((m) => {
12428
12473
  if (m.memory.frontmatter.status !== "validated") return false;
@@ -12512,7 +12557,7 @@ function registerDoctor(program2) {
12512
12557
  if (config.enforcement?.requireBriefingFirst) {
12513
12558
  const claudeSettings = path45.join(root, ".claude", "settings.local.json");
12514
12559
  let hasClaudeEnforcement = false;
12515
- if (existsSync61(claudeSettings)) {
12560
+ if (existsSync63(claudeSettings)) {
12516
12561
  try {
12517
12562
  const { readFile: readFile24 } = await import("fs/promises");
12518
12563
  const raw = await readFile24(claudeSettings, "utf8");
@@ -12538,14 +12583,14 @@ function registerDoctor(program2) {
12538
12583
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
12539
12584
  });
12540
12585
  }
12541
- findings.push(...await collectInstallFindings(root, "0.10.5"));
12586
+ findings.push(...await collectInstallFindings(root, "0.10.9"));
12542
12587
  try {
12543
12588
  const legacyRaw = execSync3("haive-mcp --version", {
12544
12589
  encoding: "utf8",
12545
12590
  timeout: 3e3,
12546
12591
  stdio: ["ignore", "pipe", "ignore"]
12547
12592
  }).trim();
12548
- const cliVersion = "0.10.5";
12593
+ const cliVersion = "0.10.9";
12549
12594
  if (legacyRaw && legacyRaw !== cliVersion) {
12550
12595
  findings.push({
12551
12596
  severity: "warn",
@@ -12567,7 +12612,7 @@ npm uninstall -g @hiveai/mcp`
12567
12612
  ];
12568
12613
  const staleConfigs = [];
12569
12614
  for (const cfgPath of configPaths) {
12570
- if (!existsSync61(cfgPath)) continue;
12615
+ if (!existsSync63(cfgPath)) continue;
12571
12616
  try {
12572
12617
  const raw = await readFile20(cfgPath, "utf8");
12573
12618
  if (raw.includes('"haive-mcp"') || raw.includes("'haive-mcp'")) {
@@ -12862,7 +12907,7 @@ which -a haive`
12862
12907
  ];
12863
12908
  for (const rel of integrationFiles) {
12864
12909
  const file = path45.join(root, rel);
12865
- if (!existsSync61(file)) continue;
12910
+ if (!existsSync63(file)) continue;
12866
12911
  const text = await readFile20(file, "utf8").catch(() => "");
12867
12912
  for (const bin of extractAbsoluteHaiveBins(text)) {
12868
12913
  const version = versionForBinary(bin);
@@ -12958,7 +13003,7 @@ function collectGlobalHivemoduleFindings(expectedVersion) {
12958
13003
  }
12959
13004
  }
12960
13005
  async function readJson(file) {
12961
- if (!existsSync61(file)) return null;
13006
+ if (!existsSync63(file)) return null;
12962
13007
  try {
12963
13008
  return JSON.parse(await readFile20(file, "utf8"));
12964
13009
  } catch {
@@ -13005,7 +13050,7 @@ function extractAbsoluteHaiveBins(text) {
13005
13050
  }
13006
13051
 
13007
13052
  // src/commands/playback.ts
13008
- import { existsSync as existsSync63 } from "fs";
13053
+ import { existsSync as existsSync64 } from "fs";
13009
13054
  import "commander";
13010
13055
  import {
13011
13056
  findProjectRoot as findProjectRoot43,
@@ -13035,7 +13080,7 @@ function registerPlayback(program2) {
13035
13080
  const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
13036
13081
  const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
13037
13082
  const sessions = bucketSessions(filtered, gapMs);
13038
- const all = existsSync63(paths.memoriesDir) ? await loadMemoriesFromDir34(paths.memoriesDir) : [];
13083
+ const all = existsSync64(paths.memoriesDir) ? await loadMemoriesFromDir34(paths.memoriesDir) : [];
13039
13084
  const memByCreatedAt = all.filter(({ memory: memory2 }) => memory2.frontmatter.type !== "session_recap").map(({ memory: memory2 }) => ({ id: memory2.frontmatter.id, at: Date.parse(memory2.frontmatter.created_at) })).sort((a, b) => a.at - b.at);
13040
13085
  const enriched = sessions.map((s, i) => {
13041
13086
  const startMs = Date.parse(s.start);
@@ -13277,7 +13322,7 @@ function runCommand3(cmd, args, cwd) {
13277
13322
  }
13278
13323
 
13279
13324
  // src/commands/welcome.ts
13280
- import { existsSync as existsSync64 } from "fs";
13325
+ import { existsSync as existsSync65 } from "fs";
13281
13326
  import "commander";
13282
13327
  import {
13283
13328
  findProjectRoot as findProjectRoot45,
@@ -13299,7 +13344,7 @@ function registerWelcome(program2) {
13299
13344
  ).option("--limit <n>", "maximum memories listed", "20").option("-d, --dir <dir>", "project root").action(async (opts) => {
13300
13345
  const root = findProjectRoot45(opts.dir);
13301
13346
  const paths = resolveHaivePaths41(root);
13302
- if (!existsSync64(paths.memoriesDir)) {
13347
+ if (!existsSync65(paths.memoriesDir)) {
13303
13348
  ui.error(`No memories at ${paths.memoriesDir}. Run 'haive init' first.`);
13304
13349
  process.exitCode = 1;
13305
13350
  return;
@@ -13371,7 +13416,7 @@ function registerResolveProject(program2) {
13371
13416
  }
13372
13417
 
13373
13418
  // src/commands/runtime-journal.ts
13374
- import { existsSync as existsSync65 } from "fs";
13419
+ import { existsSync as existsSync66 } from "fs";
13375
13420
  import path47 from "path";
13376
13421
  import "commander";
13377
13422
  import {
@@ -13397,7 +13442,7 @@ function registerRuntime(program2) {
13397
13442
  const root = path47.resolve(opts.dir ?? process.cwd());
13398
13443
  const paths = resolveHaivePaths42(findProjectRoot46(root));
13399
13444
  const limit = Math.min(500, Math.max(1, parseInt(opts.limit, 10) || 30));
13400
- if (!existsSync65(paths.haiveDir)) {
13445
+ if (!existsSync66(paths.haiveDir)) {
13401
13446
  ui.error("No .ai/ \u2014 run `haive init` first.");
13402
13447
  process.exitCode = 1;
13403
13448
  return;
@@ -13412,7 +13457,7 @@ function registerRuntime(program2) {
13412
13457
  }
13413
13458
 
13414
13459
  // src/commands/memory-timeline.ts
13415
- import { existsSync as existsSync66 } from "fs";
13460
+ import { existsSync as existsSync67 } from "fs";
13416
13461
  import path48 from "path";
13417
13462
  import "commander";
13418
13463
  import {
@@ -13431,7 +13476,7 @@ function registerMemoryTimeline(memory2) {
13431
13476
  }
13432
13477
  const root = path48.resolve(opts.dir ?? process.cwd());
13433
13478
  const paths = resolveHaivePaths43(findProjectRoot47(root));
13434
- if (!existsSync66(paths.memoriesDir)) {
13479
+ if (!existsSync67(paths.memoriesDir)) {
13435
13480
  ui.error("No memories \u2014 run `haive init`.");
13436
13481
  process.exitCode = 1;
13437
13482
  return;
@@ -13449,7 +13494,7 @@ function registerMemoryTimeline(memory2) {
13449
13494
  }
13450
13495
 
13451
13496
  // src/commands/memory-conflict-candidates.ts
13452
- import { existsSync as existsSync67 } from "fs";
13497
+ import { existsSync as existsSync68 } from "fs";
13453
13498
  import path49 from "path";
13454
13499
  import "commander";
13455
13500
  import {
@@ -13474,7 +13519,7 @@ function registerMemoryConflictCandidates(memory2) {
13474
13519
  ).option("--min-jaccard <x>", "minimum Jaccard for lexical pairs", "0.45").option("--max-pairs <n>", "cap lexical pairs", "20").option("--max-scan <n>", "max memories scanned (lexical)", "500").option("--max-topic-pairs <n>", "cap topic/status pairs", "20").action(async (opts) => {
13475
13520
  const root = path49.resolve(opts.dir ?? process.cwd());
13476
13521
  const paths = resolveHaivePaths44(findProjectRoot48(root));
13477
- if (!existsSync67(paths.memoriesDir)) {
13522
+ if (!existsSync68(paths.memoriesDir)) {
13478
13523
  ui.error("No memories \u2014 run `haive init`.");
13479
13524
  process.exitCode = 1;
13480
13525
  return;
@@ -13510,14 +13555,14 @@ function registerMemoryConflictCandidates(memory2) {
13510
13555
 
13511
13556
  // src/commands/enforce.ts
13512
13557
  import { execFileSync as execFileSync2, spawn as spawn6 } from "child_process";
13513
- import { existsSync as existsSync68, statSync as statSync3 } from "fs";
13558
+ import { existsSync as existsSync69, statSync as statSync3 } from "fs";
13514
13559
  import { chmod as chmod2, mkdir as mkdir19, readFile as readFile21, readdir as readdir6, rm as rm3, writeFile as writeFile33 } from "fs/promises";
13515
13560
  import path50 from "path";
13516
13561
  import "commander";
13517
13562
  import {
13518
13563
  antiPatternGateParams as antiPatternGateParams2,
13519
13564
  findProjectRoot as findProjectRoot49,
13520
- hasRecentBriefingMarker,
13565
+ hasRecentBriefingMarker as hasRecentBriefingMarker2,
13521
13566
  isFreshIsoDate,
13522
13567
  loadConfig as loadConfig13,
13523
13568
  loadMemoriesFromDir as loadMemoriesFromDir36,
@@ -13585,14 +13630,14 @@ function registerEnforce(program2) {
13585
13630
  const root = findProjectRoot49(opts.dir);
13586
13631
  const paths = resolveHaivePaths45(root);
13587
13632
  const cacheDir = path50.join(paths.haiveDir, ".cache");
13588
- if (existsSync68(cacheDir)) {
13633
+ if (existsSync69(cacheDir)) {
13589
13634
  if (opts.dryRun) ui.info(`would clean ${path50.relative(root, cacheDir)} (preserving .gitignore)`);
13590
13635
  else {
13591
13636
  const removed = await cleanupCacheDir(cacheDir);
13592
13637
  ui.success(`cleaned ${path50.relative(root, cacheDir)}${removed > 0 ? ` (${removed} item${removed === 1 ? "" : "s"} removed)` : ""}`);
13593
13638
  }
13594
13639
  }
13595
- if (existsSync68(paths.runtimeDir)) {
13640
+ if (existsSync69(paths.runtimeDir)) {
13596
13641
  if (opts.dryRun) ui.info(`would clean ${path50.relative(root, paths.runtimeDir)} (preserving briefing markers)`);
13597
13642
  else {
13598
13643
  const removed = await cleanupRuntimeDir(paths.runtimeDir);
@@ -13605,12 +13650,19 @@ function registerEnforce(program2) {
13605
13650
  printReport(report, Boolean(opts.json), Boolean(opts.explain));
13606
13651
  if (report.should_block) process.exit(2);
13607
13652
  });
13653
+ enforce.command("finish").alias("completion").description(
13654
+ "Final agent-exit gate: verify the git sync/release protocol before reporting a task done."
13655
+ ).option("-d, --dir <dir>", "project root").option("--explain", "group findings by blocking/review/info and show repair commands", false).option("--json", "emit JSON", false).action(async (opts) => {
13656
+ const report = await buildFinishReport(opts.dir);
13657
+ printReport(report, Boolean(opts.json), Boolean(opts.explain));
13658
+ if (report.should_block) process.exit(2);
13659
+ });
13608
13660
  enforce.command("session-start").description("Claude Code SessionStart hook: inject briefing and write a local briefing marker.").option("-d, --dir <dir>", "project root").option("--task <text>", "task text to rank memories").option("--source <name>", "marker source", "claude-session-start").option("--session-id <id>", "agent session id").action(async (opts) => {
13609
13661
  const payload = await readHookPayload();
13610
13662
  const root = resolveRoot(opts.dir, payload);
13611
13663
  if (!root) return;
13612
13664
  const paths = resolveHaivePaths45(root);
13613
- if (!existsSync68(paths.haiveDir)) return;
13665
+ if (!existsSync69(paths.haiveDir)) return;
13614
13666
  await mkdir19(paths.runtimeDir, { recursive: true });
13615
13667
  const sessionId = opts.sessionId ?? payload.session_id;
13616
13668
  const task = opts.task ?? payload.prompt ?? "Start an AI coding session in this hAIve-initialized project.";
@@ -13673,9 +13725,9 @@ ${briefing.project_context.content.slice(0, 1800)}`);
13673
13725
  const root = resolveRoot(opts.dir, payload);
13674
13726
  if (!root) return;
13675
13727
  const paths = resolveHaivePaths45(root);
13676
- if (!existsSync68(paths.haiveDir)) return;
13728
+ if (!existsSync69(paths.haiveDir)) return;
13677
13729
  if (!isWriteLikeTool(payload)) return;
13678
- const ok = await hasRecentBriefingMarker(paths, payload.session_id);
13730
+ const ok = await hasRecentBriefingMarker2(paths, payload.session_id);
13679
13731
  if (ok) {
13680
13732
  const targetFiles = extractToolPaths(payload, root);
13681
13733
  if (targetFiles.length === 0) return;
@@ -13714,10 +13766,206 @@ ${briefing.project_context.content.slice(0, 1800)}`);
13714
13766
  process.exit(2);
13715
13767
  });
13716
13768
  }
13769
+ async function buildFinishReport(dir) {
13770
+ const root = findProjectRoot49(dir);
13771
+ const paths = resolveHaivePaths45(root);
13772
+ const initialized = existsSync69(paths.haiveDir);
13773
+ const config = initialized ? await loadConfig13(paths) : {};
13774
+ const mode = config.enforcement?.mode ?? "strict";
13775
+ const findings = [];
13776
+ if (!initialized) {
13777
+ return withCategories({
13778
+ root,
13779
+ initialized,
13780
+ mode,
13781
+ score: buildScore([], config.enforcement?.scoreThreshold),
13782
+ should_block: true,
13783
+ findings: [{
13784
+ severity: "error",
13785
+ code: "not-initialized",
13786
+ message: "This repository is not initialized with hAIve.",
13787
+ fix: "Run `haive init` or `haive enforce install`.",
13788
+ impact: 100
13789
+ }]
13790
+ });
13791
+ }
13792
+ const status = await getGitSyncStatus(root);
13793
+ if (!status.available) {
13794
+ findings.push({
13795
+ severity: "error",
13796
+ code: "git-unavailable",
13797
+ message: "Git status could not be inspected, so hAIve cannot verify the exit protocol.",
13798
+ fix: "Run `git status` manually, then commit/push according to the hAIve git-sync protocol.",
13799
+ impact: 100
13800
+ });
13801
+ return finishReport(root, initialized, mode, findings, config);
13802
+ }
13803
+ const shippableDirty = status.dirtyFiles.filter(isShippablePath);
13804
+ if (status.dirtyFiles.length > 0) {
13805
+ findings.push({
13806
+ severity: "error",
13807
+ code: shippableDirty.length > 0 ? "git-sync-uncommitted-shippable" : "git-sync-uncommitted-changes",
13808
+ message: shippableDirty.length > 0 ? `${shippableDirty.length} shippable file(s) are modified but not committed.` : `${status.dirtyFiles.length} file(s) are modified but not committed.`,
13809
+ fix: shippableDirty.length > 0 ? "Bump the lockstep package version if needed, then `git add`, `git commit`, `git tag vX.Y.Z`, `git push && git push --tags`." : "Commit and push these changes before reporting the task done.",
13810
+ reason: "The multi-agent git-sync decision requires agents to leave completed work committed and pushed, not as a local diff.",
13811
+ affected_files: status.dirtyFiles.slice(0, 12),
13812
+ impact: 100
13813
+ });
13814
+ return finishReport(root, initialized, mode, findings, config);
13815
+ }
13816
+ findings.push({
13817
+ severity: "ok",
13818
+ code: "git-worktree-clean",
13819
+ message: "No uncommitted worktree changes remain."
13820
+ });
13821
+ if (!status.upstream) {
13822
+ findings.push({
13823
+ severity: "warn",
13824
+ code: "git-sync-no-upstream",
13825
+ message: "This branch has no upstream, so hAIve cannot verify that commits/tags were pushed.",
13826
+ fix: "Set an upstream with `git push -u origin <branch>`.",
13827
+ impact: 15
13828
+ });
13829
+ return finishReport(root, initialized, mode, findings, config);
13830
+ }
13831
+ if (status.behind > 0) {
13832
+ findings.push({
13833
+ severity: "error",
13834
+ code: "git-sync-behind-upstream",
13835
+ message: `This branch is ${status.behind} commit(s) behind ${status.upstream}.`,
13836
+ fix: "Run `git pull --ff-only` and resolve any conflicts before finishing.",
13837
+ impact: 40
13838
+ });
13839
+ }
13840
+ if (status.ahead > 0) {
13841
+ findings.push({
13842
+ severity: "error",
13843
+ code: "git-sync-unpushed-commits",
13844
+ message: `This branch is ${status.ahead} commit(s) ahead of ${status.upstream}.`,
13845
+ fix: "Run `git push` before reporting the task done.",
13846
+ reason: "The multi-agent git-sync decision requires agents to push completed commits.",
13847
+ impact: 60
13848
+ });
13849
+ } else {
13850
+ findings.push({
13851
+ severity: "ok",
13852
+ code: "git-sync-pushed",
13853
+ message: `Branch is not ahead of ${status.upstream}.`
13854
+ });
13855
+ }
13856
+ const releaseChangedFiles = status.releaseChangedFiles ?? status.changedSinceUpstream;
13857
+ const releaseBaseRef = status.releaseBaseRef ?? status.upstream;
13858
+ const shippableChanged = releaseChangedFiles.filter(isShippablePath);
13859
+ if (shippableChanged.length === 0) {
13860
+ findings.push({
13861
+ severity: "ok",
13862
+ code: "release-version-not-required",
13863
+ message: "No shippable package code changed since upstream; no version/tag required."
13864
+ });
13865
+ return finishReport(root, initialized, mode, findings, config);
13866
+ }
13867
+ findings.push({
13868
+ severity: "info",
13869
+ code: "release-shippable-changes",
13870
+ message: `${shippableChanged.length} shippable file(s) changed since ${releaseBaseRef}.`,
13871
+ affected_files: shippableChanged.slice(0, 12)
13872
+ });
13873
+ const versionState = await inspectReleaseVersionState(root, releaseBaseRef);
13874
+ if (!versionState.lockstep) {
13875
+ findings.push({
13876
+ severity: "error",
13877
+ code: "release-version-not-lockstep",
13878
+ message: `Publishable package versions are not in lockstep: ${versionState.localVersionsLabel}.`,
13879
+ fix: "Set root, core, cli, mcp, and embeddings package.json versions to the same X.Y.Z.",
13880
+ impact: 60
13881
+ });
13882
+ return finishReport(root, initialized, mode, findings, config);
13883
+ }
13884
+ const version = versionState.version;
13885
+ if (!version) {
13886
+ findings.push({
13887
+ severity: "error",
13888
+ code: "release-version-unreadable",
13889
+ message: "Could not read the lockstep package version.",
13890
+ fix: "Verify package.json files are valid JSON.",
13891
+ impact: 60
13892
+ });
13893
+ return finishReport(root, initialized, mode, findings, config);
13894
+ }
13895
+ if (versionState.baseVersion && compareSemver(version, versionState.baseVersion) <= 0) {
13896
+ findings.push({
13897
+ severity: "error",
13898
+ code: "release-version-missing",
13899
+ message: `Shippable code changed, but version stayed at ${version} (base: ${versionState.baseVersion}).`,
13900
+ fix: "Bump the lockstep package version (patch by default), commit the bump, tag it, then push code and tags.",
13901
+ impact: 70
13902
+ });
13903
+ } else {
13904
+ findings.push({
13905
+ severity: "ok",
13906
+ code: "release-version-bumped",
13907
+ message: versionState.baseVersion ? `Lockstep version bumped from ${versionState.baseVersion} to ${version}.` : `Lockstep version is ${version}.`
13908
+ });
13909
+ }
13910
+ const tag = `v${version}`;
13911
+ const localTagAtHead = await tagPointsAtHead(root, tag);
13912
+ if (!localTagAtHead) {
13913
+ findings.push({
13914
+ severity: "error",
13915
+ code: "release-tag-missing",
13916
+ message: `Expected git tag ${tag} to point at HEAD.`,
13917
+ fix: `Run \`git tag ${tag}\` after committing the version bump.`,
13918
+ impact: 50
13919
+ });
13920
+ } else {
13921
+ findings.push({
13922
+ severity: "ok",
13923
+ code: "release-tag-present",
13924
+ message: `Tag ${tag} points at HEAD.`
13925
+ });
13926
+ }
13927
+ const remoteTag = await remoteTagExists(root, tag);
13928
+ if (remoteTag === false) {
13929
+ findings.push({
13930
+ severity: "error",
13931
+ code: "release-tag-unpushed",
13932
+ message: `Tag ${tag} is not present on the remote.`,
13933
+ fix: "Run `git push --tags`.",
13934
+ impact: 50
13935
+ });
13936
+ } else if (remoteTag === true) {
13937
+ findings.push({
13938
+ severity: "ok",
13939
+ code: "release-tag-pushed",
13940
+ message: `Tag ${tag} exists on the remote.`
13941
+ });
13942
+ } else {
13943
+ findings.push({
13944
+ severity: "warn",
13945
+ code: "release-tag-remote-unverified",
13946
+ message: `Could not verify whether tag ${tag} exists on the remote.`,
13947
+ fix: "Run `git push --tags` if you have not already.",
13948
+ impact: 10
13949
+ });
13950
+ }
13951
+ return finishReport(root, initialized, mode, findings, config);
13952
+ }
13953
+ function finishReport(root, initialized, mode, findings, config) {
13954
+ const score = buildScore(findings, config.enforcement?.scoreThreshold);
13955
+ const hasErrors = findings.some((f) => f.severity === "error");
13956
+ return withCategories({
13957
+ root,
13958
+ initialized,
13959
+ mode,
13960
+ score,
13961
+ should_block: mode === "strict" && hasErrors,
13962
+ findings
13963
+ });
13964
+ }
13717
13965
  async function runWithEnforcement(command, args, opts) {
13718
13966
  const root = findProjectRoot49(opts.dir);
13719
13967
  const paths = resolveHaivePaths45(root);
13720
- if (!existsSync68(paths.haiveDir)) {
13968
+ if (!existsSync69(paths.haiveDir)) {
13721
13969
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
13722
13970
  process.exit(1);
13723
13971
  }
@@ -13812,7 +14060,7 @@ async function writeWrapperBriefing(paths, sessionId, task) {
13812
14060
  async function buildEnforcementReport(dir, stage, sessionId) {
13813
14061
  const root = findProjectRoot49(dir);
13814
14062
  const paths = resolveHaivePaths45(root);
13815
- const initialized = existsSync68(paths.haiveDir);
14063
+ const initialized = existsSync69(paths.haiveDir);
13816
14064
  const config = initialized ? await loadConfig13(paths) : {};
13817
14065
  if (initialized) await applyLightweightRepairs(root, paths);
13818
14066
  const mode = config.enforcement?.mode ?? "strict";
@@ -13843,9 +14091,9 @@ async function buildEnforcementReport(dir, stage, sessionId) {
13843
14091
  findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
13844
14092
  });
13845
14093
  }
13846
- findings.push(...await inspectIntegrationVersions(root, "0.10.5"));
14094
+ findings.push(...await inspectIntegrationVersions(root, "0.10.9"));
13847
14095
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
13848
- const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
14096
+ const hasBriefing = await hasRecentBriefingMarker2(paths, sessionId);
13849
14097
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
13850
14098
  severity: "error",
13851
14099
  code: "briefing-missing",
@@ -13877,7 +14125,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
13877
14125
  findings.push(...await verifyDecisionCoverage(paths, stage, sessionId));
13878
14126
  }
13879
14127
  if (stage === "pre-commit" || stage === "ci") {
13880
- findings.push(...await runPrecommitPolicy(paths, config.enforcement?.antiPatternGate ?? "anchored"));
14128
+ findings.push(...await runPrecommitPolicy(paths, config.enforcement?.antiPatternGate ?? "anchored", stage));
13881
14129
  }
13882
14130
  if (config.enforcement?.cleanupGeneratedArtifacts !== false) {
13883
14131
  findings.push(...await findGeneratedArtifacts(paths));
@@ -13913,7 +14161,7 @@ function withCategories(report) {
13913
14161
  };
13914
14162
  }
13915
14163
  async function hasRecentSessionRecap(paths) {
13916
- if (!existsSync68(paths.memoriesDir)) return false;
14164
+ if (!existsSync69(paths.memoriesDir)) return false;
13917
14165
  const all = await loadMemoriesFromDir36(paths.memoriesDir);
13918
14166
  return all.some(({ memory: memory2 }) => {
13919
14167
  const fm = memory2.frontmatter;
@@ -13922,7 +14170,7 @@ async function hasRecentSessionRecap(paths) {
13922
14170
  });
13923
14171
  }
13924
14172
  async function verifyMemoryPolicy(paths, config) {
13925
- if (!existsSync68(paths.memoriesDir)) return [];
14173
+ if (!existsSync69(paths.memoriesDir)) return [];
13926
14174
  const all = await loadMemoriesFromDir36(paths.memoriesDir);
13927
14175
  const findings = [];
13928
14176
  const staleImportant = [];
@@ -13960,7 +14208,7 @@ async function verifyMemoryPolicy(paths, config) {
13960
14208
  return findings;
13961
14209
  }
13962
14210
  async function verifyDecisionCoverage(paths, stage, sessionId) {
13963
- if (!existsSync68(paths.memoriesDir)) return [];
14211
+ if (!existsSync69(paths.memoriesDir)) return [];
13964
14212
  const changedFiles = await getChangedFiles(paths.root, stage);
13965
14213
  if (changedFiles.length === 0) {
13966
14214
  return [{ severity: "info", code: "decision-coverage-no-changes", message: "No changed files to match against policy memories." }];
@@ -14001,19 +14249,20 @@ async function verifyDecisionCoverage(paths, stage, sessionId) {
14001
14249
  impact: Math.min(35, 10 + missing.length * 5)
14002
14250
  }];
14003
14251
  }
14004
- async function runPrecommitPolicy(paths, gate) {
14252
+ async function runPrecommitPolicy(paths, gate, stage) {
14005
14253
  if (gate === "off") {
14006
14254
  return [{ severity: "info", code: "precommit-policy-off", message: "Anti-pattern gate is disabled (enforcement.antiPatternGate=off)." }];
14007
14255
  }
14008
- const staged = await runCommand4("git", ["diff", "--cached", "--name-only"], paths.root).catch(() => "");
14009
- const touchedPaths = staged.split("\n").map((s) => s.trim()).filter(Boolean);
14256
+ const snapshot = await getPolicyDiffSnapshot(paths.root, stage);
14257
+ const touchedPaths = snapshot.paths;
14010
14258
  if (touchedPaths.length === 0) {
14011
- return [{ severity: "info", code: "no-staged-changes", message: "No staged changes found for pre-commit policy." }];
14259
+ const code = stage === "ci" ? "no-ci-diff-changes" : "no-staged-changes";
14260
+ const message = stage === "ci" ? "No changed files found for CI policy diff." : "No staged changes found for pre-commit policy.";
14261
+ return [{ severity: "info", code, message }];
14012
14262
  }
14013
- const diff = await runCommand4("git", ["diff", "--cached"], paths.root).catch(() => "");
14014
14263
  const { block_on, anchored_blocks } = antiPatternGateParams2(gate);
14015
14264
  const result = await preCommitCheck({
14016
- diff,
14265
+ diff: snapshot.diff,
14017
14266
  paths: touchedPaths,
14018
14267
  block_on,
14019
14268
  anchored_blocks,
@@ -14023,7 +14272,7 @@ async function runPrecommitPolicy(paths, gate) {
14023
14272
  return [{
14024
14273
  severity: "ok",
14025
14274
  code: "precommit-policy-pass",
14026
- message: `Pre-commit policy passed for ${touchedPaths.length} staged file(s).`
14275
+ message: `${stage === "ci" ? "CI" : "Pre-commit"} policy passed for ${touchedPaths.length} changed file(s).`
14027
14276
  }];
14028
14277
  }
14029
14278
  return [{
@@ -14071,7 +14320,7 @@ async function cleanupRuntimeDir(runtimeDir) {
14071
14320
  removed++;
14072
14321
  }
14073
14322
  await writeFile33(path50.join(runtimeDir, ".gitignore"), "*\n!.gitignore\n!README.md\n", "utf8");
14074
- if (!existsSync68(path50.join(runtimeDir, "README.md"))) {
14323
+ if (!existsSync69(path50.join(runtimeDir, "README.md"))) {
14075
14324
  await writeFile33(
14076
14325
  path50.join(runtimeDir, "README.md"),
14077
14326
  "# .ai/.runtime \u2014 disposable local layer\n\nRuntime data is local. hAIve cleanup preserves briefing markers so enforcement state remains valid.\n",
@@ -14114,7 +14363,7 @@ async function inspectIntegrationVersions(root, expectedVersion) {
14114
14363
  const findings = [];
14115
14364
  for (const rel of files) {
14116
14365
  const file = path50.join(root, rel);
14117
- if (!existsSync68(file)) continue;
14366
+ if (!existsSync69(file)) continue;
14118
14367
  const text = await readFile21(file, "utf8").catch(() => "");
14119
14368
  for (const bin of extractAbsoluteHaiveBins2(text)) {
14120
14369
  const version = versionForBinary2(bin);
@@ -14174,22 +14423,217 @@ function versionForBinary2(bin) {
14174
14423
  }
14175
14424
  }
14176
14425
  async function getChangedFiles(root, stage) {
14177
- const commands = stage === "pre-commit" ? [["diff", "--cached", "--name-only"]] : [
14178
- ["diff", "--cached", "--name-only"],
14179
- ["diff", "--name-only"]
14180
- ];
14426
+ if (stage === "ci") {
14427
+ return (await getPolicyDiffSnapshot(root, "ci")).paths;
14428
+ }
14429
+ if (stage === "pre-commit") {
14430
+ return normalizeChangedFileList(
14431
+ await runCommand4("git", ["diff", "--cached", "--name-only"], root).catch(() => "")
14432
+ );
14433
+ }
14181
14434
  const files = /* @__PURE__ */ new Set();
14182
- for (const args of commands) {
14183
- const out = await runCommand4("git", args, root).catch(() => "");
14184
- for (const line of out.split("\n")) {
14185
- const file = line.trim();
14186
- if (file) files.add(file);
14187
- }
14435
+ for (const args of [["diff", "--cached", "--name-only"], ["diff", "--name-only"]]) {
14436
+ for (const file of normalizeChangedFileList(await runCommand4("git", args, root).catch(() => ""))) {
14437
+ files.add(file);
14438
+ }
14439
+ }
14440
+ return [...files];
14441
+ }
14442
+ async function getPolicyDiffSnapshot(root, stage) {
14443
+ if (stage === "pre-commit") {
14444
+ const diff = await runCommand4("git", ["diff", "--cached"], root).catch(() => "");
14445
+ const names = await runCommand4("git", ["diff", "--cached", "--name-only"], root).catch(() => "");
14446
+ return { diff, paths: normalizeChangedFileList(names), source: "staged" };
14447
+ }
14448
+ const range = await resolveCiDiffRange(root);
14449
+ if (range) {
14450
+ const diff = await runCommand4("git", ["diff", range], root).catch(() => "");
14451
+ const names = await runCommand4("git", ["diff", "--name-only", range], root).catch(() => "");
14452
+ return { diff, paths: normalizeChangedFileList(names), source: range };
14453
+ }
14454
+ return { diff: "", paths: [], source: "none" };
14455
+ }
14456
+ async function resolveCiDiffRange(root) {
14457
+ const explicitBase = cleanGitSha(process.env.HAIVE_BASE_SHA ?? process.env.HAIVE_BASE_REF);
14458
+ const explicitHead = cleanGitSha(process.env.HAIVE_HEAD_SHA ?? process.env.GITHUB_SHA) ?? "HEAD";
14459
+ if (explicitBase && await gitCommitExists(root, explicitBase)) {
14460
+ return `${explicitBase}...${explicitHead}`;
14461
+ }
14462
+ const eventRange = await resolveGithubEventRange(root);
14463
+ if (eventRange) return eventRange;
14464
+ const baseRef = process.env.GITHUB_BASE_REF?.trim();
14465
+ if (baseRef) {
14466
+ const remoteRef = `origin/${baseRef}`;
14467
+ if (await gitCommitExists(root, remoteRef)) return `${remoteRef}...${explicitHead}`;
14468
+ }
14469
+ if (await gitCommitExists(root, "origin/main")) return `origin/main...${explicitHead}`;
14470
+ if (await gitCommitExists(root, "origin/master")) return `origin/master...${explicitHead}`;
14471
+ if (await gitCommitExists(root, "HEAD^")) return `HEAD^..${explicitHead}`;
14472
+ return null;
14473
+ }
14474
+ async function resolveGithubEventRange(root) {
14475
+ const eventPath = process.env.GITHUB_EVENT_PATH;
14476
+ if (!eventPath || !existsSync69(eventPath)) return null;
14477
+ try {
14478
+ const event = JSON.parse(await readFile21(eventPath, "utf8"));
14479
+ const prBase = cleanGitSha(event.pull_request?.base?.sha);
14480
+ const prHead = cleanGitSha(event.pull_request?.head?.sha ?? event.after ?? process.env.GITHUB_SHA) ?? "HEAD";
14481
+ if (prBase && await gitCommitExists(root, prBase)) return `${prBase}...${prHead}`;
14482
+ const pushBase = cleanGitSha(event.before);
14483
+ const pushHead = cleanGitSha(event.after ?? process.env.GITHUB_SHA) ?? "HEAD";
14484
+ if (pushBase && await gitCommitExists(root, pushBase)) return `${pushBase}..${pushHead}`;
14485
+ } catch {
14486
+ return null;
14487
+ }
14488
+ return null;
14489
+ }
14490
+ function cleanGitSha(value) {
14491
+ const trimmed = value?.trim();
14492
+ if (!trimmed || /^0+$/.test(trimmed)) return null;
14493
+ return trimmed;
14494
+ }
14495
+ async function gitCommitExists(root, ref) {
14496
+ try {
14497
+ await runCommand4("git", ["rev-parse", "--verify", `${ref}^{commit}`], root);
14498
+ return true;
14499
+ } catch {
14500
+ return false;
14188
14501
  }
14189
- return [...files].filter(
14502
+ }
14503
+ function normalizeChangedFileList(raw) {
14504
+ return raw.split("\n").map((s) => s.trim()).filter(Boolean).filter(
14190
14505
  (file) => !file.startsWith(".ai/.runtime/") && !file.startsWith(".ai/.cache/") && !file.startsWith(".ai/.usage/") && file !== ".ai/.usage/tool-usage.jsonl"
14191
14506
  );
14192
14507
  }
14508
+ async function getGitSyncStatus(root) {
14509
+ const dirty = (await runCommand4("git", ["status", "--short", "--untracked-files=all"], root).catch(() => "")).split("\n").map((line) => statusLineToPath(line.trim())).filter(Boolean).filter((file) => normalizeChangedFileList(file).length > 0);
14510
+ const branch = (await runCommand4("git", ["branch", "--show-current"], root).catch(() => "")).trim() || void 0;
14511
+ const upstream = (await runCommand4("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], root).catch(() => "")).trim() || void 0;
14512
+ if (!branch && !upstream) {
14513
+ const inside = (await runCommand4("git", ["rev-parse", "--is-inside-work-tree"], root).catch(() => "")).trim();
14514
+ if (inside !== "true") return { available: false, ahead: 0, behind: 0, dirtyFiles: [], changedSinceUpstream: [] };
14515
+ }
14516
+ let ahead = 0;
14517
+ let behind = 0;
14518
+ let changedSinceUpstream = [];
14519
+ let releaseBaseRef;
14520
+ let releaseChangedFiles;
14521
+ if (upstream) {
14522
+ const counts = (await runCommand4("git", ["rev-list", "--left-right", "--count", `${upstream}...HEAD`], root).catch(() => "")).trim();
14523
+ const [behindRaw, aheadRaw] = counts.split(/\s+/);
14524
+ behind = Number.parseInt(behindRaw ?? "0", 10) || 0;
14525
+ ahead = Number.parseInt(aheadRaw ?? "0", 10) || 0;
14526
+ changedSinceUpstream = normalizeChangedFileList(
14527
+ await runCommand4("git", ["diff", "--name-only", `${upstream}...HEAD`], root).catch(() => "")
14528
+ );
14529
+ if (changedSinceUpstream.length > 0) {
14530
+ releaseBaseRef = upstream;
14531
+ releaseChangedFiles = changedSinceUpstream;
14532
+ }
14533
+ }
14534
+ if (!releaseChangedFiles || releaseChangedFiles.length === 0) {
14535
+ const hasParent = (await runCommand4("git", ["rev-parse", "--verify", "--quiet", "HEAD^"], root).catch(() => "")).trim().length > 0;
14536
+ if (hasParent) {
14537
+ const changedSinceParent = normalizeChangedFileList(
14538
+ await runCommand4("git", ["diff", "--name-only", "HEAD^..HEAD"], root).catch(() => "")
14539
+ );
14540
+ if (changedSinceParent.length > 0) {
14541
+ releaseBaseRef = "HEAD^";
14542
+ releaseChangedFiles = changedSinceParent;
14543
+ }
14544
+ }
14545
+ }
14546
+ return {
14547
+ available: true,
14548
+ branch,
14549
+ upstream,
14550
+ ahead,
14551
+ behind,
14552
+ dirtyFiles: dirty,
14553
+ changedSinceUpstream,
14554
+ ...releaseBaseRef ? { releaseBaseRef } : {},
14555
+ ...releaseChangedFiles ? { releaseChangedFiles } : {}
14556
+ };
14557
+ }
14558
+ function statusLineToPath(line) {
14559
+ const body = line.replace(/^[ MADRCU?!]{1,2}\s+/, "").trim();
14560
+ const renamed = body.match(/.+ -> (.+)$/);
14561
+ return renamed?.[1]?.trim() ?? body;
14562
+ }
14563
+ var VERSION_FILES = [
14564
+ "package.json",
14565
+ "packages/core/package.json",
14566
+ "packages/cli/package.json",
14567
+ "packages/mcp/package.json",
14568
+ "packages/embeddings/package.json"
14569
+ ];
14570
+ var SHIPPABLE_PATH_PREFIXES = [
14571
+ "packages/core/src/",
14572
+ "packages/cli/src/",
14573
+ "packages/mcp/src/",
14574
+ "packages/embeddings/src/"
14575
+ ];
14576
+ function isShippablePath(file) {
14577
+ return SHIPPABLE_PATH_PREFIXES.some((prefix) => file.startsWith(prefix)) || VERSION_FILES.includes(file);
14578
+ }
14579
+ async function inspectReleaseVersionState(root, upstream) {
14580
+ const localEntries = await Promise.all(VERSION_FILES.map(async (file) => [file, await readPackageVersion(root, file)]));
14581
+ const localVersions = new Map(localEntries);
14582
+ const unique = new Set([...localVersions.values()].filter(Boolean));
14583
+ const version = unique.size === 1 ? [...unique][0] : void 0;
14584
+ const localVersionsLabel = VERSION_FILES.map((file) => `${file}=${localVersions.get(file) ?? "unreadable"}`).join(", ");
14585
+ const baseVersion = await readPackageVersionAtRef(root, upstream, "package.json");
14586
+ return {
14587
+ lockstep: unique.size === 1 && localVersions.size === VERSION_FILES.length,
14588
+ ...version ? { version } : {},
14589
+ ...baseVersion ? { baseVersion } : {},
14590
+ localVersionsLabel
14591
+ };
14592
+ }
14593
+ async function readPackageVersion(root, relPath) {
14594
+ try {
14595
+ const data = JSON.parse(await readFile21(path50.join(root, relPath), "utf8"));
14596
+ return typeof data.version === "string" ? data.version : void 0;
14597
+ } catch {
14598
+ return void 0;
14599
+ }
14600
+ }
14601
+ async function readPackageVersionAtRef(root, ref, relPath) {
14602
+ try {
14603
+ const raw = await runCommand4("git", ["show", `${ref}:${relPath}`], root);
14604
+ const data = JSON.parse(raw);
14605
+ return typeof data.version === "string" ? data.version : void 0;
14606
+ } catch {
14607
+ return void 0;
14608
+ }
14609
+ }
14610
+ function compareSemver(a, b) {
14611
+ const pa = a.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
14612
+ const pb = b.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
14613
+ const len = Math.max(pa.length, pb.length, 3);
14614
+ for (let i = 0; i < len; i++) {
14615
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
14616
+ if (diff !== 0) return diff;
14617
+ }
14618
+ return 0;
14619
+ }
14620
+ async function tagPointsAtHead(root, tag) {
14621
+ const tags = await runCommand4("git", ["tag", "--points-at", "HEAD"], root).catch(() => "");
14622
+ return tags.split("\n").map((line) => line.trim()).includes(tag);
14623
+ }
14624
+ async function remoteTagExists(root, tag) {
14625
+ const branch = (await runCommand4("git", ["branch", "--show-current"], root).catch(() => "")).trim();
14626
+ const branchRemote = branch ? (await runCommand4("git", ["config", "--get", `branch.${branch}.remote`], root).catch(() => "")).trim() : "";
14627
+ const hasOrigin = (await runCommand4("git", ["config", "--get", "remote.origin.url"], root).catch(() => "")).trim().length > 0;
14628
+ const remote = branchRemote || (hasOrigin ? "origin" : "");
14629
+ if (!remote) return null;
14630
+ try {
14631
+ const out = await runCommand4("git", ["ls-remote", "--tags", remote, `refs/tags/${tag}`], root);
14632
+ return out.trim().length > 0;
14633
+ } catch {
14634
+ return null;
14635
+ }
14636
+ }
14193
14637
  function buildScore(findings, threshold = 80) {
14194
14638
  const checks = {
14195
14639
  total: findings.length,
@@ -14210,7 +14654,7 @@ function buildScore(findings, threshold = 80) {
14210
14654
  }
14211
14655
  async function installGitEnforcement(root) {
14212
14656
  const hooksDir = path50.join(root, ".git", "hooks");
14213
- if (!existsSync68(path50.join(root, ".git"))) {
14657
+ if (!existsSync69(path50.join(root, ".git"))) {
14214
14658
  ui.warn("No .git directory found; git enforcement hooks skipped.");
14215
14659
  return;
14216
14660
  }
@@ -14233,7 +14677,7 @@ haive enforce check --stage pre-push --dir . || exit $?
14233
14677
  ];
14234
14678
  for (const hook of hooks) {
14235
14679
  const file = path50.join(hooksDir, hook.name);
14236
- if (existsSync68(file)) {
14680
+ if (existsSync69(file)) {
14237
14681
  const current = await readFile21(file, "utf8").catch(() => "");
14238
14682
  if (current.includes(ENFORCE_HOOK_MARKER)) {
14239
14683
  await writeFile33(file, hook.body, "utf8");
@@ -14252,7 +14696,7 @@ ${hook.body}`, "utf8");
14252
14696
  async function installCiEnforcement(root) {
14253
14697
  const workflowPath = path50.join(root, ".github", "workflows", "haive-enforcement.yml");
14254
14698
  await mkdir19(path50.dirname(workflowPath), { recursive: true });
14255
- if (existsSync68(workflowPath)) {
14699
+ if (existsSync69(workflowPath)) {
14256
14700
  ui.info("GitHub Actions enforcement workflow already exists \u2014 skipped");
14257
14701
  return;
14258
14702
  }
@@ -14278,6 +14722,9 @@ jobs:
14278
14722
  - name: Install hAIve
14279
14723
  run: npm install -g @hiveai/cli
14280
14724
  - name: Enforce hAIve policy
14725
+ env:
14726
+ HAIVE_BASE_SHA: \${{ github.event.pull_request.base.sha || github.event.before }}
14727
+ HAIVE_HEAD_SHA: \${{ github.event.pull_request.head.sha || github.sha }}
14281
14728
  run: haive enforce ci
14282
14729
  `, "utf8");
14283
14730
  ui.success(`Created ${path50.relative(root, workflowPath)}`);
@@ -14381,7 +14828,7 @@ function normalizeToolPath(file, root) {
14381
14828
  return path50.relative(root, normalized).replace(/\\/g, "/");
14382
14829
  }
14383
14830
  async function missingRequiredMemoriesForFiles(paths, files, sessionId) {
14384
- if (!existsSync68(paths.memoriesDir)) return [];
14831
+ if (!existsSync69(paths.memoriesDir)) return [];
14385
14832
  const marker = await readRecentBriefingMarker(paths, sessionId);
14386
14833
  const consulted = new Set(marker?.memory_ids ?? []);
14387
14834
  const policyTypes = /* @__PURE__ */ new Set(["decision", "gotcha", "architecture", "convention", "attempt"]);
@@ -14455,7 +14902,7 @@ function registerRun(program2) {
14455
14902
 
14456
14903
  // src/commands/sensors.ts
14457
14904
  import { execFile as execFile2 } from "child_process";
14458
- import { existsSync as existsSync69 } from "fs";
14905
+ import { existsSync as existsSync70 } from "fs";
14459
14906
  import { chmod as chmod3, mkdir as mkdir20, readFile as readFile23, writeFile as writeFile34 } from "fs/promises";
14460
14907
  import path51 from "path";
14461
14908
  import { promisify as promisify2 } from "util";
@@ -14538,7 +14985,7 @@ function registerSensors(program2) {
14538
14985
  }
14539
14986
  const root = findProjectRoot50(opts.dir);
14540
14987
  const paths = resolveHaivePaths46(root);
14541
- const loaded = existsSync69(paths.memoriesDir) ? await loadMemoriesFromDir37(paths.memoriesDir) : [];
14988
+ const loaded = existsSync70(paths.memoriesDir) ? await loadMemoriesFromDir37(paths.memoriesDir) : [];
14542
14989
  const found = loaded.find(({ memory: memory2 }) => memory2.frontmatter.id === id);
14543
14990
  if (!found) {
14544
14991
  ui.error(`No memory found with id ${id}`);
@@ -14600,7 +15047,7 @@ async function sensorRows(paths) {
14600
15047
  });
14601
15048
  }
14602
15049
  async function runnableSensorMemories(paths, regexOnly = true) {
14603
- if (!existsSync69(paths.memoriesDir)) return [];
15050
+ if (!existsSync70(paths.memoriesDir)) return [];
14604
15051
  const loaded = await loadMemoriesFromDir37(paths.memoriesDir);
14605
15052
  return loaded.map(({ memory: memory2 }) => memory2).filter((memory2) => {
14606
15053
  const sensor = memory2.frontmatter.sensor;
@@ -14643,7 +15090,7 @@ function shellQuote(value) {
14643
15090
 
14644
15091
  // src/index.ts
14645
15092
  var program = new Command53();
14646
- program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.10.5").option("--advanced", "show maintenance and experimental commands in help");
15093
+ program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.10.9").option("--advanced", "show maintenance and experimental commands in help");
14647
15094
  registerInit(program);
14648
15095
  registerWelcome(program);
14649
15096
  registerResolveProject(program);