@hiveai/cli 0.9.25 → 0.9.27

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
@@ -4815,7 +4815,8 @@ var SessionTracker = class {
4815
4815
  const ranPostTask = this.events.some(
4816
4816
  (e) => e.tool === "mem_session_end" && !e.summary?.startsWith("Auto-captured")
4817
4817
  );
4818
- if (!ranPostTask && existsSync16(this.ctx.paths.haiveDir)) {
4818
+ const isSubstantialSession = totalCalls >= 3 || writingTools.length > 0;
4819
+ if (!ranPostTask && isSubstantialSession && existsSync16(this.ctx.paths.haiveDir)) {
4819
4820
  try {
4820
4821
  const memoriesSaved = writingTools.map((e) => e.summary ?? "").filter(Boolean).slice(0, 20);
4821
4822
  const payload = {
@@ -4907,7 +4908,12 @@ async function memSessionEnd(input, ctx) {
4907
4908
  }
4908
4909
  const body = buildBody(input);
4909
4910
  const topic = recapTopic(input.scope, input.module);
4910
- const invalidPaths = input.files_touched.filter(
4911
+ const normalizedFiles = input.files_touched.map((p) => {
4912
+ if (!p || !path82.isAbsolute(p)) return p;
4913
+ const rel = path82.relative(ctx.paths.root, p);
4914
+ return rel.startsWith("..") ? p : rel;
4915
+ });
4916
+ const invalidPaths = normalizedFiles.filter(
4911
4917
  (p) => !existsSync17(path82.resolve(ctx.paths.root, p))
4912
4918
  );
4913
4919
  if (invalidPaths.length > 0) {
@@ -4926,7 +4932,7 @@ async function memSessionEnd(input, ctx) {
4926
4932
  revision_count: revisionCount,
4927
4933
  anchor: {
4928
4934
  ...fm.anchor,
4929
- paths: input.files_touched.length ? input.files_touched : fm.anchor.paths
4935
+ paths: normalizedFiles.length ? normalizedFiles : fm.anchor.paths
4930
4936
  }
4931
4937
  };
4932
4938
  await writeFile10(
@@ -4949,7 +4955,7 @@ async function memSessionEnd(input, ctx) {
4949
4955
  scope: input.scope,
4950
4956
  module: input.module,
4951
4957
  tags: ["session", "recap"],
4952
- paths: input.files_touched,
4958
+ paths: normalizedFiles,
4953
4959
  topic,
4954
4960
  status: "validated"
4955
4961
  });
@@ -6558,7 +6564,36 @@ function isDocLikePath(file) {
6558
6564
  }
6559
6565
  function isPackageOrConfigPath(file) {
6560
6566
  const lower = file.toLowerCase();
6561
- return lower.endsWith("package.json") || lower.endsWith("package-lock.json") || lower.endsWith("pnpm-lock.yaml") || lower.endsWith("yarn.lock") || lower.endsWith("bun.lockb") || lower.endsWith(".config.ts") || lower.endsWith(".config.js") || lower.endsWith(".json") || lower.endsWith(".yml") || lower.endsWith(".yaml") || lower.startsWith(".github/workflows/");
6567
+ const base = lower.split("/").pop() ?? lower;
6568
+ return lower.endsWith("package.json") || lower.endsWith("package-lock.json") || lower.endsWith("pnpm-lock.yaml") || lower.endsWith("yarn.lock") || lower.endsWith("bun.lockb") || lower.endsWith(".config.ts") || lower.endsWith(".config.js") || isJsonConfigFile(base) || lower.endsWith(".yml") || lower.endsWith(".yaml") || lower.endsWith(".toml") || lower.startsWith(".github/workflows/") || lower.startsWith(".github/") && lower.endsWith(".yml") || // Dotfiles that are pure configuration/tooling — never trigger runtime gotchas
6569
+ base === ".gitignore" || base === ".gitattributes" || base === ".gitmodules" || base === ".editorconfig" || base === ".nvmrc" || base === ".node-version" || base === ".npmrc" || base === ".yarnrc" || base === ".yarnrc.yml" || base === ".dockerignore" || base === "dockerfile" || base.startsWith("dockerfile.") || base === ".env.example" || base === ".env.template" || lower.endsWith(".prettierrc") || lower.endsWith(".eslintrc") || lower.endsWith(".eslintignore") || lower.endsWith(".prettierignore") || lower.endsWith(".stylelintrc") || lower.endsWith(".browserslistrc");
6570
+ }
6571
+ function isJsonConfigFile(base) {
6572
+ const knownConfigs = /* @__PURE__ */ new Set([
6573
+ "tsconfig.json",
6574
+ "jsconfig.json",
6575
+ "deno.json",
6576
+ "deno.jsonc",
6577
+ "nx.json",
6578
+ "turbo.json",
6579
+ "lerna.json",
6580
+ "rush.json",
6581
+ "jest.config.json",
6582
+ "vitest.config.json",
6583
+ "babel.config.json",
6584
+ ".babelrc.json",
6585
+ ".swcrc",
6586
+ ".mocharc.json",
6587
+ "renovate.json",
6588
+ "dependabot.json",
6589
+ ".prettierrc.json",
6590
+ ".eslintrc.json",
6591
+ ".stylelintrc.json"
6592
+ ]);
6593
+ if (knownConfigs.has(base)) return true;
6594
+ if (/^tsconfig\..+\.json$/.test(base)) return true;
6595
+ if (/^\.[a-z]+rc\.json$/.test(base)) return true;
6596
+ return false;
6562
6597
  }
6563
6598
  function repairCommandForWarning(warning, paths) {
6564
6599
  const firstPath = paths[0];
@@ -7095,7 +7130,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
7095
7130
  };
7096
7131
  }
7097
7132
  var SERVER_NAME = "haive";
7098
- var SERVER_VERSION = "0.9.25";
7133
+ var SERVER_VERSION = "0.9.27";
7099
7134
  function jsonResult(data) {
7100
7135
  return {
7101
7136
  content: [
@@ -8092,14 +8127,14 @@ var BRIDGE_START = "<!-- haive:memories-start -->";
8092
8127
  var BRIDGE_END = "<!-- haive:memories-end -->";
8093
8128
  function registerSync(program2) {
8094
8129
  program2.command("sync").description(
8095
- "Refresh memory state after a git pull or merge.\n What it does:\n 1. Verifies anchor paths \u2014 marks stale if files/symbols moved or deleted\n 2. Re-validates previously stale memories that are now fresh\n 3. Auto-promotes proposed memories (by usage count or time delay in autopilot)\n 4. Auto-refreshes code-map if source files changed\n 5. Reports decay warnings for memories unused >90 days\n\n Install git hooks to run sync automatically: haive install-hooks\n\n Examples:\n haive sync\n haive sync --since main # also report memories changed since main\n haive sync --embed # also rebuild embeddings index\n"
8130
+ "Refresh memory state after a git pull or merge.\n What it does:\n 1. Verifies anchor paths \u2014 marks stale if files/symbols moved or deleted\n 2. Re-validates previously stale memories that are now fresh\n 3. Auto-promotes proposed memories (by usage count or time delay in autopilot)\n 4. Auto-refreshes code-map if source files changed\n 5. Reports decay warnings for memories unused >90 days\n\n Install git hooks to run sync automatically: haive install-hooks\n\n Examples:\n haive sync\n haive sync --dry-run # preview what would change without writing\n haive sync --since main # also report memories changed since main\n haive sync --embed # also rebuild embeddings index\n"
8096
8131
  ).option("-d, --dir <dir>", "project root").option("--quiet", "minimal output (suitable for git hooks)").option(
8097
8132
  "--since <ref>",
8098
8133
  "git ref/commit to compare against; report memories added/modified/removed since"
8099
8134
  ).option("--no-verify", "skip the anchor verification step").option("--no-promote", "skip the auto-promotion step").option(
8100
8135
  "--inject-bridge",
8101
8136
  "inject top validated memories into CLAUDE.md (or --bridge-file) between <!-- haive:memories-start/end --> markers"
8102
- ).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").action(async (opts) => {
8137
+ ).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) => {
8103
8138
  const root = findProjectRoot12(opts.dir);
8104
8139
  const paths = resolveHaivePaths9(root);
8105
8140
  if (!existsSync29(paths.memoriesDir)) {
@@ -8114,6 +8149,8 @@ function registerSync(program2) {
8114
8149
  const autoApproveDelayHours = config.autoApproveDelayHours ?? null;
8115
8150
  const autoPromoteMinReads = config.autoPromoteMinReads ?? DEFAULT_AUTO_PROMOTE_RULE2.minReads;
8116
8151
  const autoRepair = config.autoRepair ?? {};
8152
+ const dryRun = opts.dryRun === true;
8153
+ if (dryRun) log(ui.yellow("(dry run \u2014 no files will be written)"));
8117
8154
  let staleMarked = 0;
8118
8155
  let revalidated = 0;
8119
8156
  let promoted = 0;
@@ -8123,19 +8160,21 @@ function registerSync(program2) {
8123
8160
  for (const { memory: memory2, filePath } of memories) {
8124
8161
  if (memory2.frontmatter.type === "session_recap") {
8125
8162
  if (memory2.frontmatter.status === "stale") {
8126
- await writeFile13(
8127
- filePath,
8128
- serializeMemory11({
8129
- frontmatter: {
8130
- ...memory2.frontmatter,
8131
- status: "validated",
8132
- stale_reason: null,
8133
- verified_at: (/* @__PURE__ */ new Date()).toISOString()
8134
- },
8135
- body: memory2.body
8136
- }),
8137
- "utf8"
8138
- );
8163
+ if (!dryRun) {
8164
+ await writeFile13(
8165
+ filePath,
8166
+ serializeMemory11({
8167
+ frontmatter: {
8168
+ ...memory2.frontmatter,
8169
+ status: "validated",
8170
+ stale_reason: null,
8171
+ verified_at: (/* @__PURE__ */ new Date()).toISOString()
8172
+ },
8173
+ body: memory2.body
8174
+ }),
8175
+ "utf8"
8176
+ );
8177
+ }
8139
8178
  revalidated++;
8140
8179
  }
8141
8180
  continue;
@@ -8146,35 +8185,39 @@ function registerSync(program2) {
8146
8185
  const verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
8147
8186
  if (result.stale) {
8148
8187
  if (memory2.frontmatter.status !== "stale") {
8188
+ if (!dryRun) {
8189
+ await writeFile13(
8190
+ filePath,
8191
+ serializeMemory11({
8192
+ frontmatter: {
8193
+ ...memory2.frontmatter,
8194
+ status: "stale",
8195
+ verified_at: verifiedAt,
8196
+ stale_reason: result.reason
8197
+ },
8198
+ body: memory2.body
8199
+ }),
8200
+ "utf8"
8201
+ );
8202
+ }
8203
+ staleMarked++;
8204
+ }
8205
+ } else if (memory2.frontmatter.status === "stale") {
8206
+ if (!dryRun) {
8149
8207
  await writeFile13(
8150
8208
  filePath,
8151
8209
  serializeMemory11({
8152
8210
  frontmatter: {
8153
8211
  ...memory2.frontmatter,
8154
- status: "stale",
8212
+ status: "validated",
8155
8213
  verified_at: verifiedAt,
8156
- stale_reason: result.reason
8214
+ stale_reason: null
8157
8215
  },
8158
8216
  body: memory2.body
8159
8217
  }),
8160
8218
  "utf8"
8161
8219
  );
8162
- staleMarked++;
8163
8220
  }
8164
- } else if (memory2.frontmatter.status === "stale") {
8165
- await writeFile13(
8166
- filePath,
8167
- serializeMemory11({
8168
- frontmatter: {
8169
- ...memory2.frontmatter,
8170
- status: "validated",
8171
- verified_at: verifiedAt,
8172
- stale_reason: null
8173
- },
8174
- body: memory2.body
8175
- }),
8176
- "utf8"
8177
- );
8178
8221
  revalidated++;
8179
8222
  }
8180
8223
  }
@@ -8190,35 +8233,39 @@ function registerSync(program2) {
8190
8233
  minReads: autoPromoteMinReads,
8191
8234
  maxRejections: DEFAULT_AUTO_PROMOTE_RULE2.maxRejections
8192
8235
  })) {
8193
- await writeFile13(
8194
- filePath,
8195
- serializeMemory11({ frontmatter: { ...fm, status: "validated" }, body: memory2.body }),
8196
- "utf8"
8197
- );
8236
+ if (!dryRun) {
8237
+ await writeFile13(
8238
+ filePath,
8239
+ serializeMemory11({ frontmatter: { ...fm, status: "validated" }, body: memory2.body }),
8240
+ "utf8"
8241
+ );
8242
+ }
8198
8243
  promoted++;
8199
8244
  continue;
8200
8245
  }
8201
8246
  if (autoApproveDelayHours !== null && fm.status === "proposed" && fm.scope === "team") {
8202
8247
  const ageHours = (nowMs - new Date(fm.created_at).getTime()) / (1e3 * 60 * 60);
8203
8248
  if (ageHours >= autoApproveDelayHours) {
8204
- await writeFile13(
8205
- filePath,
8206
- serializeMemory11({
8207
- frontmatter: {
8208
- ...fm,
8209
- status: "validated",
8210
- verified_at: (/* @__PURE__ */ new Date()).toISOString()
8211
- },
8212
- body: memory2.body
8213
- }),
8214
- "utf8"
8215
- );
8249
+ if (!dryRun) {
8250
+ await writeFile13(
8251
+ filePath,
8252
+ serializeMemory11({
8253
+ frontmatter: {
8254
+ ...fm,
8255
+ status: "validated",
8256
+ verified_at: (/* @__PURE__ */ new Date()).toISOString()
8257
+ },
8258
+ body: memory2.body
8259
+ }),
8260
+ "utf8"
8261
+ );
8262
+ }
8216
8263
  autoApproved++;
8217
8264
  }
8218
8265
  }
8219
8266
  }
8220
8267
  }
8221
- if (config.autopilot || autoRepair.context || autoRepair.corpus) {
8268
+ if (!dryRun && (config.autopilot || autoRepair.context || autoRepair.corpus)) {
8222
8269
  const repairs = await applyAutopilotRepairs(root, paths, {
8223
8270
  applyContext: autoRepair.context ?? config.autopilot,
8224
8271
  applyCorpus: autoRepair.corpus ?? config.autopilot,
@@ -8350,14 +8397,16 @@ Attends une **confirmation explicite** avant d'agir.
8350
8397
  paths: [result.file],
8351
8398
  topic: `dep-bump-${slugParts}`
8352
8399
  });
8353
- const teamDir = path15.join(paths.memoriesDir, "team");
8354
- await mkdir10(teamDir, { recursive: true });
8355
- await writeFile13(
8356
- path15.join(teamDir, `${fm.id}.md`),
8357
- serializeMemory11({ frontmatter: { ...fm, requires_human_approval: true }, body }),
8358
- "utf8"
8359
- );
8360
- log(ui.yellow(` \u2192 memory created: ${fm.id}`));
8400
+ if (!dryRun) {
8401
+ const teamDir = path15.join(paths.memoriesDir, "team");
8402
+ await mkdir10(teamDir, { recursive: true });
8403
+ await writeFile13(
8404
+ path15.join(teamDir, `${fm.id}.md`),
8405
+ serializeMemory11({ frontmatter: { ...fm, requires_human_approval: true }, body }),
8406
+ "utf8"
8407
+ );
8408
+ }
8409
+ log(ui.yellow(` \u2192 memory${dryRun ? " would be" : ""} created: ${fm.id}`));
8361
8410
  }
8362
8411
  }
8363
8412
  }
@@ -8417,14 +8466,16 @@ Attends une **confirmation explicite** avant d'agir.
8417
8466
  paths: [diff.file],
8418
8467
  topic: `contract-breaking-${diff.contract}`
8419
8468
  });
8420
- const teamDir = path15.join(paths.memoriesDir, "team");
8421
- await mkdir10(teamDir, { recursive: true });
8422
- await writeFile13(
8423
- path15.join(teamDir, `${fm.id}.md`),
8424
- serializeMemory11({ frontmatter: { ...fm, requires_human_approval: true }, body }),
8425
- "utf8"
8426
- );
8427
- log(ui.yellow(` \u2192 memory created: ${fm.id}`));
8469
+ if (!dryRun) {
8470
+ const teamDir = path15.join(paths.memoriesDir, "team");
8471
+ await mkdir10(teamDir, { recursive: true });
8472
+ await writeFile13(
8473
+ path15.join(teamDir, `${fm.id}.md`),
8474
+ serializeMemory11({ frontmatter: { ...fm, requires_human_approval: true }, body }),
8475
+ "utf8"
8476
+ );
8477
+ }
8478
+ log(ui.yellow(` \u2192 memory${dryRun ? " would be" : ""} created: ${fm.id}`));
8428
8479
  }
8429
8480
  }
8430
8481
  } catch (err) {
@@ -8432,7 +8483,7 @@ Attends une **confirmation explicite** avant d'agir.
8432
8483
  }
8433
8484
  }
8434
8485
  const existingMap = await loadCodeMap5(paths);
8435
- if (!existingMap && (config.autopilot || autoRepair.codeMap)) {
8486
+ if (!dryRun && !existingMap && (config.autopilot || autoRepair.codeMap)) {
8436
8487
  try {
8437
8488
  const { buildCodeMap: buildCodeMap4, saveCodeMap: saveCodeMap4 } = await import("@hiveai/core");
8438
8489
  log(ui.dim("code-map: missing \u2014 building index\u2026"));
@@ -8467,17 +8518,21 @@ Attends une **confirmation explicite** avant d'agir.
8467
8518
  );
8468
8519
  const changedSourceFiles = (gitResult.stdout ?? "").trim();
8469
8520
  if (changedSourceFiles.length > 0) {
8470
- try {
8471
- const { buildCodeMap: buildCodeMap4, saveCodeMap: saveCodeMap4 } = await import("@hiveai/core");
8472
- log(ui.dim("code-map: source files changed \u2014 refreshing index\u2026"));
8473
- const newMap = await buildCodeMap4(root);
8474
- await saveCodeMap4(paths, newMap);
8475
- log(ui.dim(`code-map: refreshed (${Object.keys(newMap.files).length} files)`));
8476
- } catch {
8521
+ if (!dryRun) {
8522
+ try {
8523
+ const { buildCodeMap: buildCodeMap4, saveCodeMap: saveCodeMap4 } = await import("@hiveai/core");
8524
+ log(ui.dim("code-map: source files changed \u2014 refreshing index\u2026"));
8525
+ const newMap = await buildCodeMap4(root);
8526
+ await saveCodeMap4(paths, newMap);
8527
+ log(ui.dim(`code-map: refreshed (${Object.keys(newMap.files).length} files)`));
8528
+ } catch {
8529
+ }
8530
+ } else {
8531
+ log(ui.dim("code-map: source files changed \u2014 would refresh index (skipped in dry-run)"));
8477
8532
  }
8478
8533
  }
8479
8534
  }
8480
- if (opts.embed || autoRepair.codeSearch) {
8535
+ if (!dryRun && (opts.embed || autoRepair.codeSearch)) {
8481
8536
  try {
8482
8537
  const { Embedder, rebuildCodeIndex, rebuildIndex } = await import("@hiveai/embeddings");
8483
8538
  log(ui.dim("embed: rebuilding index\u2026"));
@@ -8817,7 +8872,7 @@ import {
8817
8872
 
8818
8873
  // src/commands/memory-list.ts
8819
8874
  function registerMemoryList(memory2) {
8820
- 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("-d, --dir <dir>", "project root").action(async (opts) => {
8875
+ 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) => {
8821
8876
  const root = findProjectRoot14(opts.dir);
8822
8877
  const paths = resolveHaivePaths11(root);
8823
8878
  if (!existsSync31(paths.memoriesDir)) {
@@ -8827,6 +8882,7 @@ function registerMemoryList(memory2) {
8827
8882
  }
8828
8883
  const all = await loadMemoriesFromDir25(paths.memoriesDir);
8829
8884
  const statusFilter = opts.status ? opts.status.split(",").map((s) => s.trim()) : null;
8885
+ const limit = opts.limit ? Math.max(1, parseInt(opts.limit, 10)) : void 0;
8830
8886
  const filtered = all.filter((m) => {
8831
8887
  if (!matchesFilters(m, opts)) return false;
8832
8888
  const status = m.memory.frontmatter.status;
@@ -8844,7 +8900,9 @@ function registerMemoryList(memory2) {
8844
8900
  }
8845
8901
  return;
8846
8902
  }
8847
- for (const { memory: mem, filePath } of filtered) {
8903
+ const displayed = limit !== void 0 ? filtered.slice(0, limit) : filtered;
8904
+ const clipped = filtered.length - displayed.length;
8905
+ for (const { memory: mem, filePath } of displayed) {
8848
8906
  const fm = mem.frontmatter;
8849
8907
  const tagStr = fm.tags.length ? ui.dim(` [${fm.tags.join(", ")}]`) : "";
8850
8908
  const moduleStr = fm.module ? ui.dim(` (${fm.module})`) : "";
@@ -8856,8 +8914,10 @@ function registerMemoryList(memory2) {
8856
8914
  if (title && title !== fm.id) console.log(` ${title}`);
8857
8915
  console.log(` ${ui.dim(path17.relative(root, filePath))}`);
8858
8916
  }
8859
- console.log(ui.dim(`
8860
- ${filtered.length} memor${filtered.length === 1 ? "y" : "ies"}`));
8917
+ const totalLabel = clipped > 0 ? `
8918
+ ${displayed.length} of ${filtered.length} memories shown (use --limit to adjust)` : `
8919
+ ${filtered.length} memor${filtered.length === 1 ? "y" : "ies"}`;
8920
+ console.log(ui.dim(totalLabel));
8861
8921
  if (hiddenRejectedCount > 0) {
8862
8922
  console.log(
8863
8923
  ui.dim(`(${hiddenRejectedCount} rejected hidden \u2014 use --show-rejected to include)`)
@@ -9373,7 +9433,9 @@ import {
9373
9433
  resolveHaivePaths as resolveHaivePaths18
9374
9434
  } from "@hiveai/core";
9375
9435
  function registerMemoryHot(memory2) {
9376
- memory2.command("hot").description("List memories actively used but not yet validated (good promotion candidates)").option("--threshold <n>", "minimum read_count to qualify", "3").option("--status <status>", "limit to one status (default: draft + proposed)").option("-d, --dir <dir>", "project root").action(async (opts) => {
9436
+ memory2.command("hot").description(
9437
+ "List unvalidated memories with high read_count \u2014 proven-useful promotion candidates.\n\n Unlike `haive memory pending` (which lists ALL draft/proposed by status),\n `hot` filters by usage: only memories read \u2265N times qualify.\n Use it to quickly find memories that agents are already relying on\n but that haven't been formally validated yet."
9438
+ ).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) => {
9377
9439
  const root = findProjectRoot21(opts.dir);
9378
9440
  const paths = resolveHaivePaths18(root);
9379
9441
  if (!existsSync39(paths.memoriesDir)) {
@@ -9407,7 +9469,8 @@ function registerMemoryHot(memory2) {
9407
9469
  console.log(` ${ui.dim(path25.relative(root, filePath))}`);
9408
9470
  }
9409
9471
  ui.info(
9410
- `${candidates.length} hot \u2014 promote drafts with \`haive memory promote <id>\`, then \`haive memory auto-promote --apply\`.`
9472
+ `${candidates.length} hot (read \u2265${threshold}\xD7) \u2014 agents rely on these; promote with \`haive memory promote <id>\`.
9473
+ Tip: \`haive memory pending\` lists ALL unvalidated memories regardless of read count.`
9411
9474
  );
9412
9475
  });
9413
9476
  }
@@ -10285,18 +10348,37 @@ async function buildAutoRecap(paths) {
10285
10348
  }
10286
10349
  if (obs.length === 0) return await buildGitAutoRecap(paths);
10287
10350
  const toolCounts = /* @__PURE__ */ new Map();
10288
- const fileCounts = /* @__PURE__ */ new Map();
10289
- const summaries = [];
10351
+ const writeFiles = /* @__PURE__ */ new Set();
10352
+ const readFiles = /* @__PURE__ */ new Set();
10290
10353
  for (const o of obs) {
10291
10354
  toolCounts.set(o.tool, (toolCounts.get(o.tool) ?? 0) + 1);
10292
- for (const f of o.files ?? []) fileCounts.set(f, (fileCounts.get(f) ?? 0) + 1);
10293
- if (summaries.length < 10) summaries.push(`- ${o.summary}`);
10355
+ const isWrite = ["Edit", "Write", "NotebookEdit"].includes(o.tool);
10356
+ for (const f of o.files ?? []) {
10357
+ const rel = normalizeAnchorPath(paths.root, f);
10358
+ if (isWrite) writeFiles.add(rel);
10359
+ else readFiles.add(rel);
10360
+ }
10294
10361
  }
10362
+ for (const f of writeFiles) readFiles.delete(f);
10295
10363
  const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([t, c]) => `${t} \xD7${c}`).join(", ");
10296
- const topFiles = [...fileCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8);
10297
- const goal = `Auto-captured session \u2014 ${obs.length} tool calls (${topTools})`;
10298
- const accomplished = summaries.length ? `Recent activity:
10299
- ${summaries.join("\n")}` : `Activity captured but no parseable summaries.`;
10364
+ const recentCommits = await runGit(paths.root, ["log", "--oneline", "-5"]).catch(() => "");
10365
+ const accomplishedParts = [];
10366
+ if (writeFiles.size > 0) {
10367
+ accomplishedParts.push(
10368
+ `**Files modified (${writeFiles.size}):**`,
10369
+ ...[...writeFiles].slice(0, 10).map((f) => `- \`${f}\``),
10370
+ ...writeFiles.size > 10 ? [`- ...and ${writeFiles.size - 10} more`] : []
10371
+ );
10372
+ }
10373
+ if (recentCommits.trim()) {
10374
+ accomplishedParts.push("", "**Recent commits:**");
10375
+ for (const line of recentCommits.trim().split("\n").slice(0, 5)) {
10376
+ accomplishedParts.push(`- ${line}`);
10377
+ }
10378
+ }
10379
+ if (accomplishedParts.length === 0) {
10380
+ accomplishedParts.push(`${obs.length} tool calls (${topTools}) \u2014 no file writes detected.`);
10381
+ }
10300
10382
  const failures = obs.filter((o) => o.failure_hint);
10301
10383
  const discoveriesParts = [];
10302
10384
  if (failures.length > 0) {
@@ -10305,37 +10387,75 @@ ${summaries.join("\n")}` : `Activity captured but no parseable summaries.`;
10305
10387
  ...failures.slice(0, 8).map((o) => `- ${o.summary.slice(0, 180)}`)
10306
10388
  );
10307
10389
  }
10390
+ const goal = writeFiles.size > 0 ? `Edited ${writeFiles.size} file${writeFiles.size === 1 ? "" : "s"} across ${obs.length} tool calls` : `Session with ${obs.length} tool calls (${topTools}) \u2014 read-only or no writes captured`;
10308
10391
  return {
10309
10392
  goal,
10310
- accomplished,
10393
+ accomplished: accomplishedParts.join("\n"),
10311
10394
  ...discoveriesParts.length > 0 ? { discoveries: discoveriesParts.join("\n") } : {},
10312
- files: topFiles.map(([f]) => f),
10395
+ files: [...writeFiles].slice(0, 12),
10313
10396
  rawCount: obs.length
10314
10397
  };
10315
10398
  }
10316
10399
  async function buildGitAutoRecap(paths) {
10317
10400
  const changed = await runGit(paths.root, ["diff", "--name-only"]).catch(() => "");
10318
10401
  const staged = await runGit(paths.root, ["diff", "--cached", "--name-only"]).catch(() => "");
10319
- const status = await runGit(paths.root, ["status", "--porcelain", "--untracked-files=all"]).catch(() => "");
10402
+ const statusRaw = await runGit(paths.root, ["status", "--porcelain"]).catch(() => "");
10403
+ const recentLog = await runGit(paths.root, ["log", "--oneline", "-5"]).catch(() => "");
10404
+ const diffStat = await runGit(paths.root, ["diff", "--stat", "HEAD"]).catch(() => "");
10320
10405
  const files = Array.from(new Set(
10321
10406
  [
10322
10407
  ...changed.split("\n"),
10323
10408
  ...staged.split("\n"),
10324
- ...status.split("\n").map((line) => line.replace(/^[ MADRCU?!]{1,2}\s+/, ""))
10409
+ ...statusRaw.split("\n").map((line) => line.replace(/^[ MADRCU?!]{1,2}\s+/, ""))
10325
10410
  ].map((s) => s.trim()).filter(Boolean).filter((file) => !file.startsWith(".ai/.runtime/") && !file.startsWith(".ai/.cache/"))
10326
10411
  )).sort();
10327
- if (files.length === 0) return null;
10328
- const diffStat = await runGit(paths.root, ["diff", "--stat"]).catch(() => "");
10412
+ const modified = [];
10413
+ const added = [];
10414
+ const deleted = [];
10415
+ for (const line of statusRaw.split("\n")) {
10416
+ const code = line.substring(0, 2).trim();
10417
+ const file = line.substring(3).trim().replace(/".+"/g, (m) => m.slice(1, -1));
10418
+ if (!file || file.startsWith(".ai/.runtime/") || file.startsWith(".ai/.cache/")) continue;
10419
+ if (code === "D" || code === "DD") deleted.push(file);
10420
+ else if (code === "A" || code === "??") added.push(file);
10421
+ else if (file) modified.push(file);
10422
+ }
10423
+ const accomplishedParts = [];
10424
+ if (modified.length > 0) {
10425
+ accomplishedParts.push(`**Modified (${modified.length}):**`);
10426
+ for (const f of modified.slice(0, 8)) accomplishedParts.push(`- \`${f}\``);
10427
+ if (modified.length > 8) accomplishedParts.push(`- ...and ${modified.length - 8} more`);
10428
+ }
10429
+ if (added.length > 0) {
10430
+ accomplishedParts.push(`
10431
+ **Added (${added.length}):**`);
10432
+ for (const f of added.slice(0, 5)) accomplishedParts.push(`- \`${f}\``);
10433
+ if (added.length > 5) accomplishedParts.push(`- ...and ${added.length - 5} more`);
10434
+ }
10435
+ if (deleted.length > 0) {
10436
+ accomplishedParts.push(`
10437
+ **Deleted (${deleted.length}):**`);
10438
+ for (const f of deleted.slice(0, 5)) accomplishedParts.push(`- \`${f}\``);
10439
+ }
10440
+ if (recentLog.trim()) {
10441
+ accomplishedParts.push("\n**Recent commits:**");
10442
+ for (const line of recentLog.trim().split("\n").slice(0, 5)) {
10443
+ accomplishedParts.push(`- ${line}`);
10444
+ }
10445
+ }
10446
+ if (accomplishedParts.length === 0 && files.length === 0) return null;
10447
+ if (accomplishedParts.length === 0) {
10448
+ accomplishedParts.push(...files.slice(0, 12).map((f) => `- \`${f}\``));
10449
+ if (files.length > 12) accomplishedParts.push(`- ...and ${files.length - 12} more`);
10450
+ }
10329
10451
  return {
10330
- goal: `Auto-captured session \u2014 ${files.length} changed file${files.length === 1 ? "" : "s"}`,
10331
- accomplished: [
10332
- "Detected local changes:",
10333
- ...files.slice(0, 12).map((file) => `- ${file}`),
10334
- files.length > 12 ? `- ...and ${files.length - 12} more` : ""
10335
- ].filter(Boolean).join("\n"),
10336
- discoveries: diffStat.trim() ? `Git diff summary:
10337
- ${diffStat.trim()}` : void 0,
10338
- files,
10452
+ goal: files.length > 0 ? `Session with ${files.length} changed file${files.length === 1 ? "" : "s"}` : `Session with recent commits (no uncommitted changes)`,
10453
+ accomplished: accomplishedParts.join("\n"),
10454
+ discoveries: diffStat.trim() ? `Git diff stat:
10455
+ \`\`\`
10456
+ ${diffStat.trim()}
10457
+ \`\`\`` : void 0,
10458
+ files: files.slice(0, 12),
10339
10459
  rawCount: files.length
10340
10460
  };
10341
10461
  }
@@ -10438,7 +10558,7 @@ function registerSessionEnd(session2) {
10438
10558
  next: opts.next
10439
10559
  });
10440
10560
  const topic = recapTopic2(scope, opts.module);
10441
- const filesTouched = parseCsv5(resolvedFiles);
10561
+ const filesTouched = parseCsv5(resolvedFiles).map((p) => normalizeAnchorPath(root, p));
10442
10562
  const missingPaths = filesTouched.filter((p) => !existsSync53(path36.resolve(root, p)));
10443
10563
  if (missingPaths.length > 0 && !opts.quiet) {
10444
10564
  ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
@@ -10503,6 +10623,13 @@ function parseCsv5(value) {
10503
10623
  if (!value) return [];
10504
10624
  return value.split(",").map((s) => s.trim()).filter(Boolean);
10505
10625
  }
10626
+ function normalizeAnchorPath(root, filePath) {
10627
+ if (!filePath) return filePath;
10628
+ if (!path36.isAbsolute(filePath)) return filePath;
10629
+ const rel = path36.relative(root, filePath);
10630
+ if (rel.startsWith("..")) return filePath;
10631
+ return rel;
10632
+ }
10506
10633
 
10507
10634
  // src/commands/snapshot.ts
10508
10635
  import { existsSync as existsSync54 } from "fs";
@@ -11691,8 +11818,8 @@ function parseDays(input) {
11691
11818
  }
11692
11819
 
11693
11820
  // src/commands/doctor.ts
11694
- import { existsSync as existsSync60 } from "fs";
11695
- import { readFile as readFile19, stat } from "fs/promises";
11821
+ import { existsSync as existsSync60, statSync } from "fs";
11822
+ import { readFile as readFile19, stat, writeFile as writeFile31 } from "fs/promises";
11696
11823
  import path44 from "path";
11697
11824
  import { execFileSync, execSync as execSync3 } from "child_process";
11698
11825
  import "commander";
@@ -11795,6 +11922,22 @@ function registerDoctor(program2) {
11795
11922
  fix: "haive memory pending # list them\nhaive memory auto-promote # promote those with high read_count"
11796
11923
  });
11797
11924
  }
11925
+ const OLD_DRAFT_DAYS = 30;
11926
+ const oldDrafts = memories.filter((m) => {
11927
+ if (m.memory.frontmatter.status !== "draft") return false;
11928
+ const age = (now - Date.parse(m.memory.frontmatter.created_at)) / MS_PER_DAY3;
11929
+ return age > OLD_DRAFT_DAYS;
11930
+ });
11931
+ if (oldDrafts.length > 0) {
11932
+ const ids = oldDrafts.slice(0, 4).map((m) => m.memory.frontmatter.id).join(", ");
11933
+ const more = oldDrafts.length > 4 ? ` (+${oldDrafts.length - 4} more)` : "";
11934
+ findings.push({
11935
+ severity: "warn",
11936
+ code: "stale-draft-memories",
11937
+ message: `${oldDrafts.length} draft memor${oldDrafts.length === 1 ? "y has" : "ies have"} been in draft status for 30+ days: ${ids}${more}`,
11938
+ fix: "haive memory approve <id> # activate\nhaive memory rm <id> # or delete if obsolete"
11939
+ });
11940
+ }
11798
11941
  const anchorless = memories.filter(
11799
11942
  (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"
11800
11943
  );
@@ -11920,26 +12063,58 @@ function registerDoctor(program2) {
11920
12063
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
11921
12064
  });
11922
12065
  }
11923
- findings.push(...await collectInstallFindings(root, "0.9.25"));
12066
+ findings.push(...await collectInstallFindings(root, "0.9.27"));
11924
12067
  try {
11925
12068
  const legacyRaw = execSync3("haive-mcp --version", {
11926
12069
  encoding: "utf8",
11927
12070
  timeout: 3e3,
11928
12071
  stdio: ["ignore", "pipe", "ignore"]
11929
12072
  }).trim();
11930
- const cliVersion = "0.9.25";
12073
+ const cliVersion = "0.9.27";
11931
12074
  if (legacyRaw && legacyRaw !== cliVersion) {
11932
12075
  findings.push({
11933
12076
  severity: "warn",
11934
12077
  code: "legacy-haive-mcp-stale",
11935
- message: `Standalone haive-mcp on PATH is v${legacyRaw} but haive CLI is v${cliVersion}. Prefer MCP client config with command "haive" and args ["mcp", "--stdio"] \u2014 then removing @hiveai/mcp avoids version skew.`,
11936
- fix: `npm install -g @hiveai/cli@${cliVersion}
11937
- # optionally uninstall duplicate:
12078
+ message: `Standalone haive-mcp on PATH is v${legacyRaw} but haive CLI is v${cliVersion}. MCP is now bundled in haive itself \u2014 switch your client configs to command "haive" + args ["mcp", "--stdio"], then uninstall @hiveai/mcp.`,
12079
+ fix: `# 1. Run haive init to regenerate MCP configs pointing to bundled server:
12080
+ haive init
12081
+ # 2. Optionally remove the now-redundant standalone package:
11938
12082
  npm uninstall -g @hiveai/mcp`
11939
12083
  });
11940
12084
  }
11941
12085
  } catch {
11942
12086
  }
12087
+ {
12088
+ const configPaths = [
12089
+ path44.join(root, ".mcp.json"),
12090
+ path44.join(root, ".cursor", "mcp.json"),
12091
+ path44.join(root, ".vscode", "mcp.json")
12092
+ ];
12093
+ const staleConfigs = [];
12094
+ for (const cfgPath of configPaths) {
12095
+ if (!existsSync60(cfgPath)) continue;
12096
+ try {
12097
+ const raw = await readFile19(cfgPath, "utf8");
12098
+ if (raw.includes('"haive-mcp"') || raw.includes("'haive-mcp'")) {
12099
+ staleConfigs.push(path44.relative(root, cfgPath));
12100
+ if (opts.fix && !opts.dryRun) {
12101
+ const updated = raw.replace(/"command"\s*:\s*"haive-mcp"/g, '"command": "haive"').replace(/"args"\s*:\s*\[\]/g, '"args": ["mcp", "--stdio"]');
12102
+ await writeFile31(cfgPath, updated, "utf8");
12103
+ }
12104
+ }
12105
+ } catch {
12106
+ }
12107
+ }
12108
+ if (staleConfigs.length > 0) {
12109
+ findings.push({
12110
+ severity: "warn",
12111
+ code: "legacy-mcp-config",
12112
+ message: `${staleConfigs.length} MCP config file${staleConfigs.length === 1 ? "" : "s"} still reference the old "haive-mcp" command: ` + staleConfigs.join(", ") + `. Run \`haive doctor --fix\` to auto-migrate to the bundled server.`,
12113
+ fix: "haive doctor --fix",
12114
+ section: "Protection"
12115
+ });
12116
+ }
12117
+ }
11943
12118
  if (repairs.length > 0) {
11944
12119
  findings.push({
11945
12120
  severity: "info",
@@ -12063,9 +12238,21 @@ function groupBySection(findings) {
12063
12238
  function nextActions(findings) {
12064
12239
  return [...new Set(findings.flatMap((finding) => finding.fix ? finding.fix.split("\n") : []))].filter(Boolean);
12065
12240
  }
12241
+ function isLowValueCoverageFile(file) {
12242
+ const lower = file.toLowerCase();
12243
+ const base = lower.split("/").pop() ?? lower;
12244
+ if (lower.includes(".test.") || lower.includes(".spec.")) return true;
12245
+ if (lower.includes("/__tests__/") || lower.includes("/test/") || lower.includes("/tests/")) return true;
12246
+ if (lower.endsWith(".d.ts")) return true;
12247
+ if (base === "tsup.config.ts" || base === "vitest.config.ts" || base === "jest.config.ts") return true;
12248
+ if (base.endsWith(".config.ts") || base.endsWith(".config.js")) return true;
12249
+ if (base === "vite.config.ts" || base === "vite.config.js") return true;
12250
+ return false;
12251
+ }
12066
12252
  async function collectHarnessCoverageFindings(codeMap, memories) {
12067
12253
  if (!codeMap) return [];
12068
- const codeFiles = Object.keys(codeMap.files);
12254
+ const allFiles = Object.keys(codeMap.files);
12255
+ const codeFiles = allFiles.filter((f) => !isLowValueCoverageFile(f));
12069
12256
  const total = codeFiles.length;
12070
12257
  if (total === 0) return [];
12071
12258
  const validatedWithAnchors = memories.filter(
@@ -12084,13 +12271,22 @@ async function collectHarnessCoverageFindings(codeMap, memories) {
12084
12271
  }
12085
12272
  const covered = coveredFiles.size;
12086
12273
  const pct = Math.round(covered / total * 100);
12274
+ const uncovered = codeFiles.filter((f) => !coveredFiles.has(f)).sort((a, b) => {
12275
+ const depthA = a.split("/").length;
12276
+ const depthB = b.split("/").length;
12277
+ if (depthA !== depthB) return depthA - depthB;
12278
+ return a.localeCompare(b);
12279
+ }).slice(0, 5);
12280
+ const coverageDesc = pct < 10 && total > 10 ? "Low coverage \u2014 add memory anchors on key modules to improve harness enforcement." : pct < 50 ? "Partial coverage \u2014 useful but not yet broad enough to call the harness mature." : pct < 80 ? "Good coverage \u2014 critical modules are increasingly protected." : "Good harness coverage.";
12281
+ const uncoveredHint = uncovered.length > 0 ? `
12282
+ Top uncovered: ${uncovered.map((f) => `\`${f}\``).join(", ")}` : "";
12087
12283
  const findings = [];
12088
12284
  findings.push({
12089
12285
  severity: "info",
12090
12286
  code: "harness-coverage",
12091
12287
  coverage_percent: pct,
12092
- message: `${covered}/${total} code-map files have validated memory anchors (${pct}%). ` + (pct < 10 && total > 10 ? "Low coverage \u2014 add memory anchors on key modules to improve harness enforcement." : pct < 50 ? "Partial coverage \u2014 useful but not yet broad enough to call the harness mature." : pct < 80 ? "Good coverage \u2014 critical modules are increasingly protected." : "Good harness coverage."),
12093
- fix: pct < 50 && total > 10 ? "haive memory add --type gotcha|convention|architecture --paths <key-file> --scope team" : void 0,
12288
+ message: `${covered}/${total} code-map files have validated memory anchors (${pct}%). ` + coverageDesc + uncoveredHint,
12289
+ fix: pct < 50 && total > 10 ? `haive memory add --type gotcha|convention|architecture --paths <key-file> --scope team` : void 0,
12094
12290
  section: "Harness coverage"
12095
12291
  });
12096
12292
  return findings;
@@ -12322,7 +12518,13 @@ function extractAbsoluteHaiveBins(text) {
12322
12518
  const re = /(["'\s])((?:\/[^"'\s]+)*\/haive)\b/g;
12323
12519
  let match;
12324
12520
  while (match = re.exec(text)) {
12325
- if (match[2]) out.add(match[2]);
12521
+ const p = match[2];
12522
+ if (!p) continue;
12523
+ try {
12524
+ if (statSync(p).isDirectory()) continue;
12525
+ } catch {
12526
+ }
12527
+ out.add(p);
12326
12528
  }
12327
12529
  return [...out].sort();
12328
12530
  }
@@ -12823,8 +13025,8 @@ function registerMemoryConflictCandidates(memory2) {
12823
13025
 
12824
13026
  // src/commands/enforce.ts
12825
13027
  import { execFileSync as execFileSync2, spawn as spawn6 } from "child_process";
12826
- import { existsSync as existsSync67 } from "fs";
12827
- import { chmod as chmod2, mkdir as mkdir19, readFile as readFile20, readdir as readdir6, rm as rm3, writeFile as writeFile31 } from "fs/promises";
13028
+ import { existsSync as existsSync67, statSync as statSync2 } from "fs";
13029
+ import { chmod as chmod2, mkdir as mkdir19, readFile as readFile20, readdir as readdir6, rm as rm3, writeFile as writeFile33 } from "fs/promises";
12828
13030
  import path49 from "path";
12829
13031
  import "commander";
12830
13032
  import {
@@ -13118,7 +13320,7 @@ async function writeWrapperBriefing(paths, sessionId, task) {
13118
13320
  if (briefing.setup_warnings.length > 0) {
13119
13321
  parts.push("", "## Setup Warnings", ...briefing.setup_warnings.map((w) => `- ${w}`));
13120
13322
  }
13121
- await writeFile31(file, parts.join("\n") + "\n", "utf8");
13323
+ await writeFile33(file, parts.join("\n") + "\n", "utf8");
13122
13324
  return file;
13123
13325
  }
13124
13326
  async function buildEnforcementReport(dir, stage, sessionId) {
@@ -13155,7 +13357,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
13155
13357
  findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
13156
13358
  });
13157
13359
  }
13158
- findings.push(...await inspectIntegrationVersions(root, "0.9.25"));
13360
+ findings.push(...await inspectIntegrationVersions(root, "0.9.27"));
13159
13361
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
13160
13362
  const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
13161
13363
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
@@ -13377,9 +13579,9 @@ async function cleanupRuntimeDir(runtimeDir) {
13377
13579
  await rm3(path49.join(runtimeDir, entry.name), { recursive: true, force: true });
13378
13580
  removed++;
13379
13581
  }
13380
- await writeFile31(path49.join(runtimeDir, ".gitignore"), "*\n!.gitignore\n!README.md\n", "utf8");
13582
+ await writeFile33(path49.join(runtimeDir, ".gitignore"), "*\n!.gitignore\n!README.md\n", "utf8");
13381
13583
  if (!existsSync67(path49.join(runtimeDir, "README.md"))) {
13382
- await writeFile31(
13584
+ await writeFile33(
13383
13585
  path49.join(runtimeDir, "README.md"),
13384
13586
  "# .ai/.runtime \u2014 disposable local layer\n\nRuntime data is local. hAIve cleanup preserves briefing markers so enforcement state remains valid.\n",
13385
13587
  "utf8"
@@ -13396,7 +13598,7 @@ async function cleanupCacheDir(cacheDir) {
13396
13598
  await rm3(path49.join(cacheDir, entry.name), { recursive: true, force: true });
13397
13599
  removed++;
13398
13600
  }
13399
- await writeFile31(path49.join(cacheDir, ".gitignore"), "*\n!.gitignore\n", "utf8");
13601
+ await writeFile33(path49.join(cacheDir, ".gitignore"), "*\n!.gitignore\n", "utf8");
13400
13602
  return removed;
13401
13603
  }
13402
13604
  async function cleanupEnforcementDir(enforcementDir) {
@@ -13458,7 +13660,13 @@ function extractAbsoluteHaiveBins2(text) {
13458
13660
  const re = /(["'\s])((?:\/[^"'\s]+)*\/haive)\b/g;
13459
13661
  let match;
13460
13662
  while (match = re.exec(text)) {
13461
- if (match[2]) out.add(match[2]);
13663
+ const p = match[2];
13664
+ if (!p) continue;
13665
+ try {
13666
+ if (statSync2(p).isDirectory()) continue;
13667
+ } catch {
13668
+ }
13669
+ out.add(p);
13462
13670
  }
13463
13671
  return [...out].sort();
13464
13672
  }
@@ -13537,14 +13745,14 @@ haive enforce check --stage pre-push --dir . || exit $?
13537
13745
  if (existsSync67(file)) {
13538
13746
  const current = await readFile20(file, "utf8").catch(() => "");
13539
13747
  if (current.includes(ENFORCE_HOOK_MARKER)) {
13540
- await writeFile31(file, hook.body, "utf8");
13748
+ await writeFile33(file, hook.body, "utf8");
13541
13749
  } else {
13542
- await writeFile31(file, `${current.trimEnd()}
13750
+ await writeFile33(file, `${current.trimEnd()}
13543
13751
 
13544
13752
  ${hook.body}`, "utf8");
13545
13753
  }
13546
13754
  } else {
13547
- await writeFile31(file, hook.body, "utf8");
13755
+ await writeFile33(file, hook.body, "utf8");
13548
13756
  }
13549
13757
  await chmod2(file, 493);
13550
13758
  }
@@ -13557,7 +13765,7 @@ async function installCiEnforcement(root) {
13557
13765
  ui.info("GitHub Actions enforcement workflow already exists \u2014 skipped");
13558
13766
  return;
13559
13767
  }
13560
- await writeFile31(workflowPath, `name: haive-enforcement
13768
+ await writeFile33(workflowPath, `name: haive-enforcement
13561
13769
 
13562
13770
  on:
13563
13771
  pull_request:
@@ -13756,7 +13964,7 @@ function registerRun(program2) {
13756
13964
 
13757
13965
  // src/index.ts
13758
13966
  var program = new Command51();
13759
- program.name("haive").description("hAIve \u2014 the memory and enforcement layer of your agent harness").version("0.9.25").option("--advanced", "show maintenance and experimental commands in help");
13967
+ program.name("haive").description("hAIve \u2014 the memory and enforcement layer of your agent harness").version("0.9.27").option("--advanced", "show maintenance and experimental commands in help");
13760
13968
  registerInit(program);
13761
13969
  registerWelcome(program);
13762
13970
  registerResolveProject(program);