@hiveai/cli 0.9.10 → 0.9.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command49 } from "commander";
4
+ import { Command as Command50 } from "commander";
5
5
 
6
6
  // src/commands/briefing.ts
7
7
  import { existsSync } from "fs";
@@ -198,7 +198,7 @@ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
198
198
  if (!f) continue;
199
199
  counts.set(f, (counts.get(f) ?? 0) + 1);
200
200
  }
201
- let entries = [...counts.entries()].map(([path46, changes]) => ({ path: path46, changes }));
201
+ let entries = [...counts.entries()].map(([path47, changes]) => ({ path: path47, changes }));
202
202
  const lowerPaths = filePaths.map((p) => p.toLowerCase());
203
203
  if (lowerPaths.length > 0) {
204
204
  entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
@@ -308,12 +308,14 @@ function registerBriefing(program2) {
308
308
  ).option("-d, --dir <dir>", "project root").action(async (opts) => {
309
309
  const root = findProjectRoot(opts.dir);
310
310
  const paths = resolveHaivePaths(root);
311
+ const markerFiles = parseCsv(opts.files);
311
312
  if (existsSync(paths.haiveDir)) {
312
313
  await mkdir(paths.runtimeDir, { recursive: true });
313
314
  await writeBriefingMarker(paths, {
314
315
  task: opts.task ?? "CLI briefing",
315
316
  source: "haive-briefing-cli",
316
- sessionId: process.env.HAIVE_SESSION_ID
317
+ sessionId: process.env.HAIVE_SESSION_ID,
318
+ files: markerFiles
317
319
  }).catch(() => {
318
320
  });
319
321
  }
@@ -386,7 +388,7 @@ function registerBriefing(program2) {
386
388
  }
387
389
  }
388
390
  const all = ownMemories;
389
- const filePaths = parseCsv(opts.files);
391
+ const filePaths = markerFiles;
390
392
  const tokens = opts.task ? tokenizeQuery(opts.task) : null;
391
393
  const scopeFilter = opts.scope ?? "all";
392
394
  const recaps = all.filter(({ memory: mem }) => mem.frontmatter.type === "session_recap").sort(
@@ -498,6 +500,14 @@ function registerBriefing(program2) {
498
500
  if (ids.length > 0) {
499
501
  await trackReads(paths, ids).catch(() => {
500
502
  });
503
+ await writeBriefingMarker(paths, {
504
+ task: opts.task ?? "CLI briefing",
505
+ source: "haive-briefing-cli",
506
+ sessionId: process.env.HAIVE_SESSION_ID,
507
+ memoryIds: ids,
508
+ files: filePaths
509
+ }).catch(() => {
510
+ });
501
511
  }
502
512
  const radarForced = opts.radar === true;
503
513
  const radarAuto = opts.radar !== false && top.length < RADAR_AUTO_THRESHOLD;
@@ -6072,7 +6082,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
6072
6082
  };
6073
6083
  }
6074
6084
  var SERVER_NAME = "haive";
6075
- var SERVER_VERSION = "0.9.10";
6085
+ var SERVER_VERSION = "0.9.11";
6076
6086
  function jsonResult(data) {
6077
6087
  return {
6078
6088
  content: [
@@ -6086,14 +6096,11 @@ function jsonResult(data) {
6086
6096
  var ENFORCEMENT_PROFILE_TOOLS = /* @__PURE__ */ new Set([
6087
6097
  "get_briefing",
6088
6098
  "mem_save",
6089
- "mem_tried",
6090
6099
  "mem_search",
6091
- "mem_get",
6092
- "mem_update",
6093
6100
  "mem_verify",
6094
6101
  "mem_relevant_to",
6095
- "code_map",
6096
- "pre_commit_check"
6102
+ "pre_commit_check",
6103
+ "mem_session_end"
6097
6104
  ]);
6098
6105
  var BRIEFING_TOOLS = /* @__PURE__ */ new Set(["get_briefing", "mem_relevant_to"]);
6099
6106
  var MUTATING_TOOLS = /* @__PURE__ */ new Set([
@@ -8821,8 +8828,8 @@ function parseChangelog(content) {
8821
8828
  const entries = [];
8822
8829
  const versionRe = /^#{1,3}\s+(?:\[?)([0-9]+\.[0-9]+[.0-9]*)/m;
8823
8830
  const sections = content.split(/^#{1,3}\s+/m).slice(1);
8824
- for (const section of sections) {
8825
- const versionMatch = section.match(/^(?:\[?)([0-9]+\.[0-9]+[.0-9]*)/);
8831
+ for (const section2 of sections) {
8832
+ const versionMatch = section2.match(/^(?:\[?)([0-9]+\.[0-9]+[.0-9]*)/);
8826
8833
  const version = versionMatch?.[1];
8827
8834
  if (!version) continue;
8828
8835
  const entry = {
@@ -8833,7 +8840,7 @@ function parseChangelog(content) {
8833
8840
  fixed: [],
8834
8841
  added: []
8835
8842
  };
8836
- const subSections = section.split(/^#{2,4}\s+/m);
8843
+ const subSections = section2.split(/^#{2,4}\s+/m);
8837
8844
  for (const sub of subSections) {
8838
8845
  const firstLine = (sub.split("\n")[0] ?? "").toLowerCase().trim();
8839
8846
  const items = sub.split("\n").slice(1).filter((l) => l.trim().startsWith("-") || l.trim().startsWith("*")).map((l) => l.replace(/^[\s\-*]+/, "").trim()).filter(Boolean);
@@ -8859,7 +8866,7 @@ function parseChangelog(content) {
8859
8866
  }
8860
8867
  }
8861
8868
  if (entry.breaking.length === 0) {
8862
- for (const line of section.split("\n")) {
8869
+ for (const line of section2.split("\n")) {
8863
8870
  if (/breaking|⚠|deprecated|removed/.test(line.toLowerCase())) {
8864
8871
  const item = line.replace(/^[\s\-*#]+/, "").trim();
8865
8872
  if (item) entry.breaking.push(item);
@@ -9611,8 +9618,8 @@ Next steps:
9611
9618
  return;
9612
9619
  }
9613
9620
  const projectName = path35.basename(root);
9614
- const { readdir: readdir5 } = await import("fs/promises");
9615
- const projectDirs = (await readdir5(hubSharedDir, { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== projectName).map((d) => d.name);
9621
+ const { readdir: readdir6 } = await import("fs/promises");
9622
+ const projectDirs = (await readdir6(hubSharedDir, { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== projectName).map((d) => d.name);
9616
9623
  if (projectDirs.length === 0) {
9617
9624
  console.log(ui.dim("No other projects have pushed to the hub yet."));
9618
9625
  return;
@@ -9623,7 +9630,7 @@ Next steps:
9623
9630
  const sourceDir = path35.join(hubSharedDir, sourceName);
9624
9631
  const destDir = path35.join(paths.memoriesDir, "shared", sourceName);
9625
9632
  await mkdir15(destDir, { recursive: true });
9626
- const sourceFiles = (await readdir5(sourceDir)).filter((f) => f.endsWith(".md"));
9633
+ const sourceFiles = (await readdir6(sourceDir)).filter((f) => f.endsWith(".md"));
9627
9634
  const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
9628
9635
  const existingInDest = await loadDir(destDir);
9629
9636
  const existingIds = new Set(existingInDest.map(({ memory: memory2 }) => memory2.frontmatter.id));
@@ -9662,12 +9669,12 @@ Next steps:
9662
9669
  );
9663
9670
  const sharedDir = path35.join(paths.memoriesDir, "shared");
9664
9671
  if (existsSync55(sharedDir)) {
9665
- const { readdir: readdir5 } = await import("fs/promises");
9666
- const sources = (await readdir5(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
9672
+ const { readdir: readdir6 } = await import("fs/promises");
9673
+ const sources = (await readdir6(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
9667
9674
  console.log(`
9668
9675
  Imported from ${sources.length} source(s):`);
9669
9676
  for (const src of sources) {
9670
- const files = (await readdir5(path35.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
9677
+ const files = (await readdir6(path35.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
9671
9678
  console.log(` ${src}: ${files.length} memor${files.length === 1 ? "y" : "ies"}`);
9672
9679
  }
9673
9680
  } else {
@@ -9987,15 +9994,160 @@ function summarize(name, t0, payload, notes) {
9987
9994
  };
9988
9995
  }
9989
9996
 
9990
- // src/commands/memory-suggest.ts
9991
- import { mkdir as mkdir17, writeFile as writeFile28 } from "fs/promises";
9997
+ // src/commands/benchmark.ts
9992
9998
  import { existsSync as existsSync57 } from "fs";
9999
+ import { readdir as readdir5, readFile as readFile16, writeFile as writeFile28 } from "fs/promises";
9993
10000
  import path37 from "path";
9994
10001
  import "commander";
10002
+ import { estimateTokens as estimateTokens4, findProjectRoot as findProjectRoot36 } from "@hiveai/core";
10003
+ function registerBenchmark(program2) {
10004
+ const benchmark = program2.command("benchmark").description("Official hAIve benchmark/demo utilities for measuring agent enforcement value.");
10005
+ benchmark.command("report").description("Summarize BENCHMARK_AGENT_REPORT.md files from a paired hAIve/plain agent benchmark.").option("-d, --dir <dir>", "benchmark root", "benchmarks/agent-benchmark").option("--out <file>", "write a Markdown report").option("--json", "emit JSON", false).action(async (opts) => {
10006
+ const root = resolveBenchmarkRoot(opts.dir);
10007
+ const rows = await collectRows(root);
10008
+ const summary = summarizeRows(rows);
10009
+ if (opts.json) {
10010
+ console.log(JSON.stringify({ root, summary, rows }, null, 2));
10011
+ return;
10012
+ }
10013
+ const markdown = renderMarkdown(root, summary, rows);
10014
+ if (opts.out) {
10015
+ const outFile = path37.isAbsolute(opts.out) ? opts.out : path37.join(root, opts.out);
10016
+ await writeFile28(outFile, markdown, "utf8");
10017
+ ui.success(`wrote ${path37.relative(process.cwd(), outFile)}`);
10018
+ return;
10019
+ }
10020
+ console.log(markdown);
10021
+ });
10022
+ benchmark.command("demo").description("Print the recommended protocol for running a hAIve vs plain agent benchmark.").action(() => {
10023
+ console.log([
10024
+ "# hAIve Agent Benchmark Demo",
10025
+ "",
10026
+ "1. Create paired fixtures: one `*-haive`, one `*-plain`.",
10027
+ "2. Put the same failing tests in both fixtures.",
10028
+ "3. Add precise `.ai/memories/team/*.md` policy memories only to the hAIve fixture.",
10029
+ "4. Run equal agents in parallel:",
10030
+ " - hAIve agents must run `haive briefing --files ... --task ...` first.",
10031
+ " - Plain agents must not read `.ai` or call hAIve.",
10032
+ "5. Require every agent to write `BENCHMARK_AGENT_REPORT.md`.",
10033
+ "6. Run `haive benchmark report --dir <benchmark-root> --out RESULTS.md`.",
10034
+ "",
10035
+ "Recommended metrics: pass rate, test iterations, files read, files changed, visible artifacts, decision quality, and token proxy."
10036
+ ].join("\n"));
10037
+ });
10038
+ }
10039
+ function resolveBenchmarkRoot(dir) {
10040
+ const candidate = dir ?? "benchmarks/agent-benchmark";
10041
+ if (path37.isAbsolute(candidate)) return candidate;
10042
+ const projectRoot = findProjectRoot36(process.cwd());
10043
+ return path37.join(projectRoot, candidate);
10044
+ }
10045
+ async function collectRows(root) {
10046
+ if (!existsSync57(root)) throw new Error(`Benchmark directory not found: ${root}`);
10047
+ const entries = await readdir5(root, { withFileTypes: true });
10048
+ const rows = [];
10049
+ for (const entry of entries) {
10050
+ if (!entry.isDirectory()) continue;
10051
+ const fixtureDir = path37.join(root, entry.name);
10052
+ const reportFile = path37.join(fixtureDir, "BENCHMARK_AGENT_REPORT.md");
10053
+ if (!existsSync57(reportFile)) continue;
10054
+ const report = await readFile16(reportFile, "utf8");
10055
+ rows.push(parseAgentReport(entry.name, report));
10056
+ }
10057
+ return rows.sort((a, b) => a.fixture.localeCompare(b.fixture));
10058
+ }
10059
+ function parseAgentReport(fixture, report) {
10060
+ const group = fixture.endsWith("-haive") ? "haive" : fixture.endsWith("-plain") ? "plain" : "unknown";
10061
+ return {
10062
+ fixture,
10063
+ group,
10064
+ commands: sectionBulletCount(report, "Commands"),
10065
+ files_read: sectionBulletCount(report, "Files Read"),
10066
+ files_modified: sectionBulletCount(report, "Files Modified"),
10067
+ test_iterations: countMatches(section(report, "Test Iterations"), /Iteration\s+\d+|^- /gim),
10068
+ terminal_failures: countMatches(section(report, "Terminal Errors"), /fail|error|not raised|exited with code 1/gi),
10069
+ decision_mentions: sectionBulletCount(report, "Key Decisions"),
10070
+ token_proxy: estimateTokens4(report),
10071
+ haive_impact: /hAIve Memory Impact[\s\S]*?\b(yes|directly|changed|shaped|confirmed)\b/i.test(report)
10072
+ };
10073
+ }
10074
+ function summarizeRows(rows) {
10075
+ const byGroup = (group) => rows.filter((r) => r.group === group);
10076
+ return {
10077
+ fixtures: rows.length,
10078
+ haive: summarizeGroup(byGroup("haive")),
10079
+ plain: summarizeGroup(byGroup("plain"))
10080
+ };
10081
+ }
10082
+ function summarizeGroup(rows) {
10083
+ const sum = (key) => rows.reduce((total, row) => total + Number(row[key] ?? 0), 0);
10084
+ return {
10085
+ fixtures: rows.length,
10086
+ commands: sum("commands"),
10087
+ files_read: sum("files_read"),
10088
+ files_modified: sum("files_modified"),
10089
+ test_iterations: sum("test_iterations"),
10090
+ terminal_failures: sum("terminal_failures"),
10091
+ decision_mentions: sum("decision_mentions"),
10092
+ token_proxy: sum("token_proxy"),
10093
+ haive_impact_count: rows.filter((r) => r.haive_impact).length
10094
+ };
10095
+ }
10096
+ function renderMarkdown(root, summary, rows) {
10097
+ const lines = [
10098
+ "# hAIve Agent Benchmark Report",
10099
+ "",
10100
+ `Benchmark root: \`${root}\``,
10101
+ "",
10102
+ "## Summary",
10103
+ "",
10104
+ "| Group | Fixtures | Commands | Files read | Files modified | Test iterations | Terminal failures | Decision mentions | Token proxy | hAIve impact |",
10105
+ "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
10106
+ groupLine("hAIve", summary.haive),
10107
+ groupLine("Plain", summary.plain),
10108
+ "",
10109
+ "## Fixtures",
10110
+ "",
10111
+ "| Fixture | Group | Commands | Files read | Files modified | Test iterations | Terminal failures | Decisions | Token proxy | hAIve impact |",
10112
+ "| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |",
10113
+ ...rows.map(
10114
+ (row) => `| \`${row.fixture}\` | ${row.group} | ${row.commands} | ${row.files_read} | ${row.files_modified} | ${row.test_iterations} | ${row.terminal_failures} | ${row.decision_mentions} | ${row.token_proxy} | ${row.haive_impact ? "yes" : "no"} |`
10115
+ ),
10116
+ "",
10117
+ "## Reading",
10118
+ "",
10119
+ "The token proxy is estimated from the agent report size, not from private model billing data.",
10120
+ "Use this report to compare relative effort and decision quality, then pair it with final test results and a human review of the diffs.",
10121
+ ""
10122
+ ];
10123
+ return lines.join("\n");
10124
+ }
10125
+ function groupLine(label, group) {
10126
+ return `| ${label} | ${group.fixtures} | ${group.commands} | ${group.files_read} | ${group.files_modified} | ${group.test_iterations} | ${group.terminal_failures} | ${group.decision_mentions} | ${group.token_proxy} | ${group.haive_impact_count} |`;
10127
+ }
10128
+ function sectionBulletCount(markdown, title) {
10129
+ return countMatches(section(markdown, title), /^- |^\d+\.\s/gm);
10130
+ }
10131
+ function section(markdown, title) {
10132
+ const re = new RegExp(`##\\s+[^\\n]*${escapeRegExp(title)}[^\\n]*\\n([\\s\\S]*?)(?=\\n##\\s+|$)`, "i");
10133
+ return re.exec(markdown)?.[1] ?? "";
10134
+ }
10135
+ function countMatches(text, re) {
10136
+ return [...text.matchAll(re)].length;
10137
+ }
10138
+ function escapeRegExp(value) {
10139
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10140
+ }
10141
+
10142
+ // src/commands/memory-suggest.ts
10143
+ import { mkdir as mkdir17, writeFile as writeFile29 } from "fs/promises";
10144
+ import { existsSync as existsSync58 } from "fs";
10145
+ import path38 from "path";
10146
+ import "commander";
9995
10147
  import {
9996
10148
  aggregateUsage as aggregateUsage2,
9997
10149
  buildFrontmatter as buildFrontmatter11,
9998
- findProjectRoot as findProjectRoot36,
10150
+ findProjectRoot as findProjectRoot37,
9999
10151
  loadMemoriesFromDir as loadMemoriesFromDir30,
10000
10152
  memoryFilePath as memoryFilePath10,
10001
10153
  parseSince as parseSince2,
@@ -10013,7 +10165,7 @@ function registerMemorySuggest(memory2) {
10013
10165
  memory2.command("suggest").description(
10014
10166
  "Suggest memories to create based on recurring search queries in the usage log.\n\n Use --auto-save to draft the top-N suggestions as draft memories. They land\n in personal scope by default with status=draft, ready for you to edit and promote."
10015
10167
  ).option("--since <window>", "ISO date or relative (e.g. '7d', '24h')", "30d").option("--min <count>", "minimum repeat count to surface a query", "2").option("--top-n <n>", "with --auto-save, draft this many top suggestions", "3").option("--scope <scope>", "with --auto-save, scope of drafted memories (personal | team)", "personal").option("--auto-save", "draft top-N suggestions as draft memories on disk", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
10016
- const root = findProjectRoot36(opts.dir);
10168
+ const root = findProjectRoot37(opts.dir);
10017
10169
  const paths = resolveHaivePaths33(root);
10018
10170
  const events = await readUsageEvents3(paths);
10019
10171
  if (events.length === 0) {
@@ -10060,7 +10212,7 @@ function registerMemorySuggest(memory2) {
10060
10212
  }
10061
10213
  const created = [];
10062
10214
  const skipped = [];
10063
- const existing = existsSync57(paths.memoriesDir) ? await loadMemoriesFromDir30(paths.memoriesDir) : [];
10215
+ const existing = existsSync58(paths.memoriesDir) ? await loadMemoriesFromDir30(paths.memoriesDir) : [];
10064
10216
  for (const s of top) {
10065
10217
  const slug = slugify(s.query);
10066
10218
  if (!slug) {
@@ -10083,13 +10235,13 @@ function registerMemorySuggest(memory2) {
10083
10235
  fm.status = "draft";
10084
10236
  const body = renderTemplate(s);
10085
10237
  const file = memoryFilePath10(paths, fm.scope, fm.id, fm.module);
10086
- await mkdir17(path37.dirname(file), { recursive: true });
10087
- if (existsSync57(file)) {
10088
- skipped.push({ query: s.query, reason: `file already exists at ${path37.relative(root, file)}` });
10238
+ await mkdir17(path38.dirname(file), { recursive: true });
10239
+ if (existsSync58(file)) {
10240
+ skipped.push({ query: s.query, reason: `file already exists at ${path38.relative(root, file)}` });
10089
10241
  continue;
10090
10242
  }
10091
- await writeFile28(file, serializeMemory24({ frontmatter: fm, body }), "utf8");
10092
- created.push({ id: fm.id, file: path37.relative(root, file), query: s.query });
10243
+ await writeFile29(file, serializeMemory24({ frontmatter: fm, body }), "utf8");
10244
+ created.push({ id: fm.id, file: path38.relative(root, file), query: s.query });
10093
10245
  }
10094
10246
  if (opts.json) {
10095
10247
  console.log(JSON.stringify({ created, skipped }, null, 2));
@@ -10182,12 +10334,12 @@ function truncate2(text, max) {
10182
10334
  }
10183
10335
 
10184
10336
  // src/commands/memory-archive.ts
10185
- import { existsSync as existsSync58 } from "fs";
10186
- import { writeFile as writeFile29 } from "fs/promises";
10187
- import path38 from "path";
10337
+ import { existsSync as existsSync59 } from "fs";
10338
+ import { writeFile as writeFile30 } from "fs/promises";
10339
+ import path39 from "path";
10188
10340
  import "commander";
10189
10341
  import {
10190
- findProjectRoot as findProjectRoot37,
10342
+ findProjectRoot as findProjectRoot38,
10191
10343
  getUsage as getUsage18,
10192
10344
  loadMemoriesFromDir as loadMemoriesFromDir31,
10193
10345
  loadUsageIndex as loadUsageIndex24,
@@ -10199,9 +10351,9 @@ function registerMemoryArchive(memory2) {
10199
10351
  memory2.command("archive").description(
10200
10352
  "Archive obsolete memories: marks status='deprecated' for memories not read in N days\n whose anchored paths have all disappeared (or have no anchor at all).\n\n Defaults to a DRY RUN \u2014 pass --apply to actually rewrite files.\n Targets `attempt` memories by default since they age the fastest.\n\n Recover later with `haive memory edit <id>` to set status back to validated."
10201
10353
  ).option("--since <window>", "minimum age since last read (e.g. '180d', '6m')", "180d").option("--type <type>", "limit to a memory type (default 'attempt'). Pass 'all' to scan all types.", "attempt").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) => {
10202
- const root = findProjectRoot37(opts.dir);
10354
+ const root = findProjectRoot38(opts.dir);
10203
10355
  const paths = resolveHaivePaths34(root);
10204
- if (!existsSync58(paths.memoriesDir)) {
10356
+ if (!existsSync59(paths.memoriesDir)) {
10205
10357
  ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
10206
10358
  process.exitCode = 1;
10207
10359
  return;
@@ -10222,7 +10374,7 @@ function registerMemoryArchive(memory2) {
10222
10374
  if (typeFilter && fm.type !== typeFilter) continue;
10223
10375
  if (fm.status === "deprecated" || fm.status === "rejected") continue;
10224
10376
  const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
10225
- const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync58(path38.join(paths.root, p)));
10377
+ const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync59(path39.join(paths.root, p)));
10226
10378
  const isAnchorless = !hasAnyAnchor;
10227
10379
  if (!isAnchorless && !allPathsGone) continue;
10228
10380
  const u = getUsage18(usage, fm.id);
@@ -10270,7 +10422,7 @@ function registerMemoryArchive(memory2) {
10270
10422
  if (!found) continue;
10271
10423
  const fm = { ...found.memory.frontmatter, status: "deprecated" };
10272
10424
  try {
10273
- await writeFile29(c.filePath, serializeMemory25({ frontmatter: fm, body: found.memory.body }), "utf8");
10425
+ await writeFile30(c.filePath, serializeMemory25({ frontmatter: fm, body: found.memory.body }), "utf8");
10274
10426
  archived++;
10275
10427
  } catch (err) {
10276
10428
  if (!opts.json) {
@@ -10296,14 +10448,14 @@ function parseDays(input) {
10296
10448
  }
10297
10449
 
10298
10450
  // src/commands/doctor.ts
10299
- import { existsSync as existsSync59 } from "fs";
10451
+ import { existsSync as existsSync60 } from "fs";
10300
10452
  import { stat } from "fs/promises";
10301
- import path39 from "path";
10453
+ import path40 from "path";
10302
10454
  import { execSync as execSync3 } from "child_process";
10303
10455
  import "commander";
10304
10456
  import {
10305
10457
  codeMapPath as codeMapPath2,
10306
- findProjectRoot as findProjectRoot38,
10458
+ findProjectRoot as findProjectRoot39,
10307
10459
  getUsage as getUsage19,
10308
10460
  loadCodeMap as loadCodeMap5,
10309
10461
  loadConfig as loadConfig7,
@@ -10317,10 +10469,10 @@ function registerDoctor(program2) {
10317
10469
  program2.command("doctor").description(
10318
10470
  "Analyze the local hAIve setup and emit actionable recommendations.\n\n Inspects: project-context status, memory health (stale/anchorless/decay/pending),\n code-map freshness, usage log signals (low-hit briefings, repeated empty searches).\n\n Read-only by default. Pass --fix to suggest commands you can copy-paste."
10319
10471
  ).option("--json", "emit JSON instead of human-readable output", false).option("--fix", "include suggested fix commands in human output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
10320
- const root = findProjectRoot38(opts.dir);
10472
+ const root = findProjectRoot39(opts.dir);
10321
10473
  const paths = resolveHaivePaths35(root);
10322
10474
  const findings = [];
10323
- if (!existsSync59(paths.haiveDir)) {
10475
+ if (!existsSync60(paths.haiveDir)) {
10324
10476
  findings.push({
10325
10477
  severity: "error",
10326
10478
  code: "not-initialized",
@@ -10329,7 +10481,7 @@ function registerDoctor(program2) {
10329
10481
  });
10330
10482
  return emit(findings, opts);
10331
10483
  }
10332
- if (!existsSync59(paths.projectContext)) {
10484
+ if (!existsSync60(paths.projectContext)) {
10333
10485
  findings.push({
10334
10486
  severity: "warn",
10335
10487
  code: "no-project-context",
@@ -10337,8 +10489,8 @@ function registerDoctor(program2) {
10337
10489
  fix: "haive init"
10338
10490
  });
10339
10491
  } else {
10340
- const { readFile: readFile17 } = await import("fs/promises");
10341
- const content = await readFile17(paths.projectContext, "utf8");
10492
+ const { readFile: readFile18 } = await import("fs/promises");
10493
+ const content = await readFile18(paths.projectContext, "utf8");
10342
10494
  const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
10343
10495
  if (isTemplate) {
10344
10496
  findings.push({
@@ -10349,7 +10501,7 @@ function registerDoctor(program2) {
10349
10501
  });
10350
10502
  }
10351
10503
  }
10352
- const memories = existsSync59(paths.memoriesDir) ? await loadMemoriesFromDir32(paths.memoriesDir) : [];
10504
+ const memories = existsSync60(paths.memoriesDir) ? await loadMemoriesFromDir32(paths.memoriesDir) : [];
10353
10505
  const now = Date.now();
10354
10506
  if (memories.length === 0) {
10355
10507
  findings.push({
@@ -10460,12 +10612,12 @@ function registerDoctor(program2) {
10460
10612
  }
10461
10613
  const config = await loadConfig7(paths);
10462
10614
  if (config.enforcement?.requireBriefingFirst) {
10463
- const claudeSettings = path39.join(root, ".claude", "settings.local.json");
10615
+ const claudeSettings = path40.join(root, ".claude", "settings.local.json");
10464
10616
  let hasClaudeEnforcement = false;
10465
- if (existsSync59(claudeSettings)) {
10617
+ if (existsSync60(claudeSettings)) {
10466
10618
  try {
10467
- const { readFile: readFile17 } = await import("fs/promises");
10468
- const raw = await readFile17(claudeSettings, "utf8");
10619
+ const { readFile: readFile18 } = await import("fs/promises");
10620
+ const raw = await readFile18(claudeSettings, "utf8");
10469
10621
  hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
10470
10622
  } catch {
10471
10623
  hasClaudeEnforcement = false;
@@ -10494,7 +10646,7 @@ function registerDoctor(program2) {
10494
10646
  timeout: 3e3,
10495
10647
  stdio: ["ignore", "pipe", "ignore"]
10496
10648
  }).trim();
10497
- const cliVersion = "0.9.10";
10649
+ const cliVersion = "0.9.11";
10498
10650
  if (legacyRaw && legacyRaw !== cliVersion) {
10499
10651
  findings.push({
10500
10652
  severity: "warn",
@@ -10543,10 +10695,10 @@ function isSearchTool(name) {
10543
10695
  }
10544
10696
 
10545
10697
  // src/commands/playback.ts
10546
- import { existsSync as existsSync60 } from "fs";
10698
+ import { existsSync as existsSync61 } from "fs";
10547
10699
  import "commander";
10548
10700
  import {
10549
- findProjectRoot as findProjectRoot39,
10701
+ findProjectRoot as findProjectRoot40,
10550
10702
  loadMemoriesFromDir as loadMemoriesFromDir33,
10551
10703
  parseSince as parseSince3,
10552
10704
  readUsageEvents as readUsageEvents5,
@@ -10557,7 +10709,7 @@ function registerPlayback(program2) {
10557
10709
  program2.command("playback").description(
10558
10710
  "Replay past sessions from the usage log. For each session, show:\n - tool calls (what kind, how many)\n - briefing tasks asked\n - memories that have been created since then (that the session didn't have)\n\n Useful to ask 'would today's haive have helped past me on this task?'"
10559
10711
  ).option("--since <window>", "limit to events in this window (e.g. '7d')", "30d").option("--session-gap <minutes>", "minutes of inactivity that splits a session", "30").option("--limit <n>", "show at most this many sessions (newest first)", "10").option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
10560
- const root = findProjectRoot39(opts.dir);
10712
+ const root = findProjectRoot40(opts.dir);
10561
10713
  const paths = resolveHaivePaths36(root);
10562
10714
  const events = await readUsageEvents5(paths);
10563
10715
  if (events.length === 0) {
@@ -10573,7 +10725,7 @@ function registerPlayback(program2) {
10573
10725
  const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
10574
10726
  const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
10575
10727
  const sessions = bucketSessions(filtered, gapMs);
10576
- const all = existsSync60(paths.memoriesDir) ? await loadMemoriesFromDir33(paths.memoriesDir) : [];
10728
+ const all = existsSync61(paths.memoriesDir) ? await loadMemoriesFromDir33(paths.memoriesDir) : [];
10577
10729
  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);
10578
10730
  const enriched = sessions.map((s, i) => {
10579
10731
  const startMs = Date.parse(s.start);
@@ -10663,7 +10815,7 @@ function truncate3(text, max) {
10663
10815
  import { spawn as spawn4 } from "child_process";
10664
10816
  import "commander";
10665
10817
  import {
10666
- findProjectRoot as findProjectRoot40,
10818
+ findProjectRoot as findProjectRoot41,
10667
10819
  resolveHaivePaths as resolveHaivePaths37
10668
10820
  } from "@hiveai/core";
10669
10821
  function registerPrecommit(program2) {
@@ -10674,7 +10826,7 @@ function registerPrecommit(program2) {
10674
10826
  "'any' | 'high-confidence' (default) | 'never' (report only)",
10675
10827
  "high-confidence"
10676
10828
  ).option("--no-semantic", "disable semantic search in anti-patterns matching").option("--json", "emit JSON instead of human-readable output", false).option("--paths <paths...>", "explicit paths to check (skips git diff)").option("-d, --dir <dir>", "project root").action(async (opts) => {
10677
- const root = findProjectRoot40(opts.dir);
10829
+ const root = findProjectRoot41(opts.dir);
10678
10830
  const paths = resolveHaivePaths37(root);
10679
10831
  const ctx = { paths };
10680
10832
  let diff = "";
@@ -10767,10 +10919,10 @@ function runCommand3(cmd, args, cwd) {
10767
10919
  }
10768
10920
 
10769
10921
  // src/commands/welcome.ts
10770
- import { existsSync as existsSync61 } from "fs";
10922
+ import { existsSync as existsSync63 } from "fs";
10771
10923
  import "commander";
10772
10924
  import {
10773
- findProjectRoot as findProjectRoot41,
10925
+ findProjectRoot as findProjectRoot42,
10774
10926
  loadMemoriesFromDir as loadMemoriesFromDir34,
10775
10927
  resolveHaivePaths as resolveHaivePaths38
10776
10928
  } from "@hiveai/core";
@@ -10786,9 +10938,9 @@ function registerWelcome(program2) {
10786
10938
  program2.command("welcome").description(
10787
10939
  "Onboarding checklist: ranks validated/proposed **team** memories by type.\nUse after `haive init` so new devs skim institutional knowledge quickly.\n\n haive welcome\n haive welcome --limit 15\n"
10788
10940
  ).option("--limit <n>", "maximum memories listed", "20").option("-d, --dir <dir>", "project root").action(async (opts) => {
10789
- const root = findProjectRoot41(opts.dir);
10941
+ const root = findProjectRoot42(opts.dir);
10790
10942
  const paths = resolveHaivePaths38(root);
10791
- if (!existsSync61(paths.memoriesDir)) {
10943
+ if (!existsSync63(paths.memoriesDir)) {
10792
10944
  ui.error(`No memories at ${paths.memoriesDir}. Run 'haive init' first.`);
10793
10945
  process.exitCode = 1;
10794
10946
  return;
@@ -10829,17 +10981,17 @@ function registerWelcome(program2) {
10829
10981
  }
10830
10982
 
10831
10983
  // src/commands/memory-lint.ts
10832
- import { existsSync as existsSync63 } from "fs";
10984
+ import { existsSync as existsSync64 } from "fs";
10833
10985
  import "commander";
10834
10986
  import {
10835
- findProjectRoot as findProjectRoot42,
10987
+ findProjectRoot as findProjectRoot43,
10836
10988
  loadMemoriesFromDir as loadMemoriesFromDir35,
10837
10989
  resolveHaivePaths as resolveHaivePaths39
10838
10990
  } from "@hiveai/core";
10839
10991
  async function lintMemoriesAsync(root) {
10840
10992
  const paths = resolveHaivePaths39(root);
10841
10993
  const out = [];
10842
- if (!existsSync63(paths.memoriesDir)) return out;
10994
+ if (!existsSync64(paths.memoriesDir)) return out;
10843
10995
  const loaded = await loadMemoriesFromDir35(paths.memoriesDir);
10844
10996
  const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
10845
10997
  for (const { filePath, memory: memory2 } of loaded) {
@@ -10900,7 +11052,7 @@ function registerMemoryLint(parent) {
10900
11052
  parent.command("lint").description(
10901
11053
  "Heuristic corpus checks (anchors on key types, headings, verbosity). Static analysis only."
10902
11054
  ).option("--json", "emit findings as JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
10903
- const root = findProjectRoot42(opts.dir);
11055
+ const root = findProjectRoot43(opts.dir);
10904
11056
  const findings = await lintMemoriesAsync(root);
10905
11057
  if (opts.json) {
10906
11058
  console.log(JSON.stringify({ findings_count: findings.length, findings }, null, 2));
@@ -10946,25 +11098,25 @@ function registerMemorySuggestTopic(memory2) {
10946
11098
  }
10947
11099
 
10948
11100
  // src/commands/resolve-project.ts
10949
- import path40 from "path";
11101
+ import path41 from "path";
10950
11102
  import "commander";
10951
11103
  import { resolveProjectInfo as resolveProjectInfo2 } from "@hiveai/core";
10952
11104
  function registerResolveProject(program2) {
10953
11105
  program2.command("resolve-project").description(
10954
11106
  "Print JSON for hAIve project root resolution (HAIVE_PROJECT_ROOT, markers, .ai layout)."
10955
11107
  ).option("-d, --dir <dir>", "working directory", process.cwd()).action((opts) => {
10956
- const info = resolveProjectInfo2({ cwd: path40.resolve(opts.dir) });
11108
+ const info = resolveProjectInfo2({ cwd: path41.resolve(opts.dir) });
10957
11109
  console.log(JSON.stringify({ ok: true, info }, null, 2));
10958
11110
  });
10959
11111
  }
10960
11112
 
10961
11113
  // src/commands/runtime-journal.ts
10962
- import { existsSync as existsSync64 } from "fs";
10963
- import path41 from "path";
11114
+ import { existsSync as existsSync65 } from "fs";
11115
+ import path43 from "path";
10964
11116
  import "commander";
10965
11117
  import {
10966
11118
  appendRuntimeJournalEntry as appendRuntimeJournalEntry3,
10967
- findProjectRoot as findProjectRoot43,
11119
+ findProjectRoot as findProjectRoot44,
10968
11120
  readRuntimeJournalTail as readRuntimeJournalTail2,
10969
11121
  resolveHaivePaths as resolveHaivePaths40
10970
11122
  } from "@hiveai/core";
@@ -10974,18 +11126,18 @@ function registerRuntime(program2) {
10974
11126
  );
10975
11127
  const journal = runtime.command("journal").description("Append or read the machine-local session journal (NDJSON)");
10976
11128
  journal.command("append").description("Append one JSON line to .ai/.runtime/session-journal.ndjson").argument("<message>", "short text to log").option("-k, --kind <kind>", "note | session_end | mcp", "note").option("-d, --dir <dir>", "project root", process.cwd()).action(async (message, opts) => {
10977
- const root = path41.resolve(opts.dir ?? process.cwd());
10978
- const paths = resolveHaivePaths40(findProjectRoot43(root));
11129
+ const root = path43.resolve(opts.dir ?? process.cwd());
11130
+ const paths = resolveHaivePaths40(findProjectRoot44(root));
10979
11131
  const raw = opts.kind ?? "note";
10980
11132
  const kind = ["note", "session_end", "mcp"].includes(raw) ? raw : "note";
10981
11133
  await appendRuntimeJournalEntry3(paths, { kind, message });
10982
- ui.success(`Appended to ${path41.relative(root, paths.runtimeDir)}/session-journal.ndjson`);
11134
+ ui.success(`Appended to ${path43.relative(root, paths.runtimeDir)}/session-journal.ndjson`);
10983
11135
  });
10984
11136
  journal.command("tail").description("Print the last N entries from the runtime session journal as JSON").option("-n, --limit <n>", "number of lines", "30").option("-d, --dir <dir>", "project root", process.cwd()).action(async (opts) => {
10985
- const root = path41.resolve(opts.dir ?? process.cwd());
10986
- const paths = resolveHaivePaths40(findProjectRoot43(root));
11137
+ const root = path43.resolve(opts.dir ?? process.cwd());
11138
+ const paths = resolveHaivePaths40(findProjectRoot44(root));
10987
11139
  const limit = Math.min(500, Math.max(1, parseInt(opts.limit, 10) || 30));
10988
- if (!existsSync64(paths.haiveDir)) {
11140
+ if (!existsSync65(paths.haiveDir)) {
10989
11141
  ui.error("No .ai/ \u2014 run `haive init` first.");
10990
11142
  process.exitCode = 1;
10991
11143
  return;
@@ -11000,12 +11152,12 @@ function registerRuntime(program2) {
11000
11152
  }
11001
11153
 
11002
11154
  // src/commands/memory-timeline.ts
11003
- import { existsSync as existsSync65 } from "fs";
11004
- import path43 from "path";
11155
+ import { existsSync as existsSync66 } from "fs";
11156
+ import path44 from "path";
11005
11157
  import "commander";
11006
11158
  import {
11007
11159
  collectTimelineEntries as collectTimelineEntries2,
11008
- findProjectRoot as findProjectRoot44,
11160
+ findProjectRoot as findProjectRoot45,
11009
11161
  resolveHaivePaths as resolveHaivePaths41
11010
11162
  } from "@hiveai/core";
11011
11163
  function registerMemoryTimeline(memory2) {
@@ -11017,9 +11169,9 @@ function registerMemoryTimeline(memory2) {
11017
11169
  process.exitCode = 1;
11018
11170
  return;
11019
11171
  }
11020
- const root = path43.resolve(opts.dir ?? process.cwd());
11021
- const paths = resolveHaivePaths41(findProjectRoot44(root));
11022
- if (!existsSync65(paths.memoriesDir)) {
11172
+ const root = path44.resolve(opts.dir ?? process.cwd());
11173
+ const paths = resolveHaivePaths41(findProjectRoot45(root));
11174
+ if (!existsSync66(paths.memoriesDir)) {
11023
11175
  ui.error("No memories \u2014 run `haive init`.");
11024
11176
  process.exitCode = 1;
11025
11177
  return;
@@ -11037,13 +11189,13 @@ function registerMemoryTimeline(memory2) {
11037
11189
  }
11038
11190
 
11039
11191
  // src/commands/memory-conflict-candidates.ts
11040
- import { existsSync as existsSync66 } from "fs";
11041
- import path44 from "path";
11192
+ import { existsSync as existsSync67 } from "fs";
11193
+ import path45 from "path";
11042
11194
  import "commander";
11043
11195
  import {
11044
11196
  findLexicalConflictPairs as findLexicalConflictPairs2,
11045
11197
  findTopicStatusConflictPairs as findTopicStatusConflictPairs2,
11046
- findProjectRoot as findProjectRoot45,
11198
+ findProjectRoot as findProjectRoot46,
11047
11199
  resolveHaivePaths as resolveHaivePaths42
11048
11200
  } from "@hiveai/core";
11049
11201
  function parseTypes(csv) {
@@ -11060,9 +11212,9 @@ function registerMemoryConflictCandidates(memory2) {
11060
11212
  "decision,architecture,convention,gotcha (lexical scan)",
11061
11213
  "decision,architecture"
11062
11214
  ).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) => {
11063
- const root = path44.resolve(opts.dir ?? process.cwd());
11064
- const paths = resolveHaivePaths42(findProjectRoot45(root));
11065
- if (!existsSync66(paths.memoriesDir)) {
11215
+ const root = path45.resolve(opts.dir ?? process.cwd());
11216
+ const paths = resolveHaivePaths42(findProjectRoot46(root));
11217
+ if (!existsSync67(paths.memoriesDir)) {
11066
11218
  ui.error("No memories \u2014 run `haive init`.");
11067
11219
  process.exitCode = 1;
11068
11220
  return;
@@ -11098,16 +11250,18 @@ function registerMemoryConflictCandidates(memory2) {
11098
11250
 
11099
11251
  // src/commands/enforce.ts
11100
11252
  import { spawn as spawn5 } from "child_process";
11101
- import { existsSync as existsSync67 } from "fs";
11102
- import { chmod as chmod2, mkdir as mkdir18, readFile as readFile16, writeFile as writeFile30 } from "fs/promises";
11103
- import path45 from "path";
11253
+ import { existsSync as existsSync68 } from "fs";
11254
+ import { chmod as chmod2, mkdir as mkdir18, readFile as readFile17, rm as rm3, writeFile as writeFile31 } from "fs/promises";
11255
+ import path46 from "path";
11104
11256
  import "commander";
11105
11257
  import {
11106
- findProjectRoot as findProjectRoot46,
11258
+ findProjectRoot as findProjectRoot47,
11107
11259
  hasRecentBriefingMarker,
11108
11260
  isFreshIsoDate,
11109
11261
  loadConfig as loadConfig8,
11110
11262
  loadMemoriesFromDir as loadMemoriesFromDir36,
11263
+ memoryMatchesAnchorPaths as memoryMatchesAnchorPaths6,
11264
+ readRecentBriefingMarker,
11111
11265
  resolveBriefingBudget as resolveBriefingBudget3,
11112
11266
  resolveHaivePaths as resolveHaivePaths43,
11113
11267
  saveConfig as saveConfig3,
@@ -11122,7 +11276,7 @@ function registerEnforce(program2) {
11122
11276
  "Agent-agnostic enforcement helpers: install policy gates, report status, and block unsafe workflows."
11123
11277
  );
11124
11278
  enforce.command("install").description("Install hAIve enforcement across MCP config, git hooks, CI template, and supported client hooks.").option("-d, --dir <dir>", "project root").option("--no-git", "skip git pre-commit/pre-push enforcement hooks").option("--no-claude", "skip Claude Code hooks").option("--no-ci", "skip GitHub Actions enforcement workflow").action(async (opts) => {
11125
- const root = findProjectRoot46(opts.dir);
11279
+ const root = findProjectRoot47(opts.dir);
11126
11280
  const paths = resolveHaivePaths43(root);
11127
11281
  await mkdir18(paths.haiveDir, { recursive: true });
11128
11282
  const current = await loadConfig8(paths);
@@ -11135,7 +11289,11 @@ function registerEnforce(program2) {
11135
11289
  requireSessionRecap: true,
11136
11290
  requireMemoryVerify: true,
11137
11291
  blockStaleDecisionChanges: true,
11138
- toolProfile: "enforcement"
11292
+ requireDecisionCoverage: true,
11293
+ scoreThreshold: 85,
11294
+ cleanupGeneratedArtifacts: true,
11295
+ toolProfile: "enforcement",
11296
+ policyPacks: ["architecture", "gotchas", "security", "domain", "release"]
11139
11297
  }
11140
11298
  });
11141
11299
  ui.success("hAIve strict enforcement enabled in .ai/haive.config.json");
@@ -11144,7 +11302,7 @@ function registerEnforce(program2) {
11144
11302
  if (opts.claude !== false) {
11145
11303
  try {
11146
11304
  const result = await installClaudeHooksAtPath(defaultClaudeSettingsPath("project", root));
11147
- ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${path45.relative(root, result.settingsPath)})`);
11305
+ ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${path46.relative(root, result.settingsPath)})`);
11148
11306
  } catch (err) {
11149
11307
  ui.warn(`Claude Code hooks not installed: ${err instanceof Error ? err.message : String(err)}`);
11150
11308
  }
@@ -11162,6 +11320,23 @@ function registerEnforce(program2) {
11162
11320
  printReport(report, Boolean(opts.json));
11163
11321
  if (report.should_block) process.exit(2);
11164
11322
  });
11323
+ enforce.command("cleanup").description("Remove generated hAIve runtime/cache artifacts that should not appear in commits.").option("-d, --dir <dir>", "project root").option("--dry-run", "print what would be removed without deleting", false).action(async (opts) => {
11324
+ const root = findProjectRoot47(opts.dir);
11325
+ const paths = resolveHaivePaths43(root);
11326
+ const targets = [
11327
+ path46.join(paths.haiveDir, ".cache"),
11328
+ path46.join(paths.haiveDir, ".runtime")
11329
+ ];
11330
+ for (const target of targets) {
11331
+ if (!existsSync68(target)) continue;
11332
+ const rel = path46.relative(root, target);
11333
+ if (opts.dryRun) ui.info(`would remove ${rel}`);
11334
+ else {
11335
+ await rm3(target, { recursive: true, force: true });
11336
+ ui.success(`removed ${rel}`);
11337
+ }
11338
+ }
11339
+ });
11165
11340
  enforce.command("ci").description("CI entrypoint: fail if the repository violates hAIve enforcement policy.").option("-d, --dir <dir>", "project root").option("--json", "emit JSON", false).action(async (opts) => {
11166
11341
  const report = await buildEnforcementReport(opts.dir, "ci");
11167
11342
  printReport(report, Boolean(opts.json));
@@ -11172,15 +11347,10 @@ function registerEnforce(program2) {
11172
11347
  const root = resolveRoot(opts.dir, payload);
11173
11348
  if (!root) return;
11174
11349
  const paths = resolveHaivePaths43(root);
11175
- if (!existsSync67(paths.haiveDir)) return;
11350
+ if (!existsSync68(paths.haiveDir)) return;
11176
11351
  await mkdir18(paths.runtimeDir, { recursive: true });
11177
11352
  const sessionId = opts.sessionId ?? payload.session_id;
11178
11353
  const task = opts.task ?? payload.prompt ?? "Start an AI coding session in this hAIve-initialized project.";
11179
- await writeBriefingMarker2(paths, {
11180
- sessionId,
11181
- task,
11182
- source: opts.source ?? "claude-session-start"
11183
- });
11184
11354
  const budget = resolveBriefingBudget3("quick", {
11185
11355
  max_tokens: 2500,
11186
11356
  max_memories: 5,
@@ -11204,6 +11374,12 @@ function registerEnforce(program2) {
11204
11374
  },
11205
11375
  { paths }
11206
11376
  );
11377
+ await writeBriefingMarker2(paths, {
11378
+ sessionId,
11379
+ task,
11380
+ source: opts.source ?? "claude-session-start",
11381
+ memoryIds: briefing.memories.map((m) => m.id)
11382
+ });
11207
11383
  console.log("hAIve briefing loaded. Agents must consult this before editing.");
11208
11384
  if (briefing.last_session) {
11209
11385
  console.log(`
@@ -11233,7 +11409,7 @@ ${briefing.project_context.content.slice(0, 1800)}`);
11233
11409
  const root = resolveRoot(opts.dir, payload);
11234
11410
  if (!root) return;
11235
11411
  const paths = resolveHaivePaths43(root);
11236
- if (!existsSync67(paths.haiveDir)) return;
11412
+ if (!existsSync68(paths.haiveDir)) return;
11237
11413
  if (!isWriteLikeTool(payload)) return;
11238
11414
  const ok = await hasRecentBriefingMarker(paths, payload.session_id);
11239
11415
  if (ok) return;
@@ -11255,9 +11431,9 @@ ${briefing.project_context.content.slice(0, 1800)}`);
11255
11431
  });
11256
11432
  }
11257
11433
  async function runWithEnforcement(command, args, opts) {
11258
- const root = findProjectRoot46(opts.dir);
11434
+ const root = findProjectRoot47(opts.dir);
11259
11435
  const paths = resolveHaivePaths43(root);
11260
- if (!existsSync67(paths.haiveDir)) {
11436
+ if (!existsSync68(paths.haiveDir)) {
11261
11437
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
11262
11438
  process.exit(1);
11263
11439
  }
@@ -11276,7 +11452,7 @@ async function runWithEnforcement(command, args, opts) {
11276
11452
  process.exit(2);
11277
11453
  }
11278
11454
  ui.info(`hAIve briefing marker created for wrapped agent session: ${sessionId}`);
11279
- ui.info(`Briefing written to ${path45.relative(root, briefingFile)} and exported as HAIVE_BRIEFING_FILE`);
11455
+ ui.info(`Briefing written to ${path46.relative(root, briefingFile)} and exported as HAIVE_BRIEFING_FILE`);
11280
11456
  const child = spawn5(command, args, {
11281
11457
  cwd: root,
11282
11458
  stdio: "inherit",
@@ -11319,9 +11495,15 @@ async function writeWrapperBriefing(paths, sessionId, task) {
11319
11495
  min_semantic_score: 0.25,
11320
11496
  budget_preset: "quick"
11321
11497
  }, { paths });
11322
- const dir = path45.join(paths.runtimeDir, "enforcement", "briefings");
11498
+ await writeBriefingMarker2(paths, {
11499
+ sessionId,
11500
+ task,
11501
+ source: "haive-run",
11502
+ memoryIds: briefing.memories.map((m) => m.id)
11503
+ });
11504
+ const dir = path46.join(paths.runtimeDir, "enforcement", "briefings");
11323
11505
  await mkdir18(dir, { recursive: true });
11324
- const file = path45.join(dir, `${sessionId}.md`);
11506
+ const file = path46.join(dir, `${sessionId}.md`);
11325
11507
  const parts = [
11326
11508
  "# hAIve Briefing",
11327
11509
  "",
@@ -11339,13 +11521,13 @@ async function writeWrapperBriefing(paths, sessionId, task) {
11339
11521
  if (briefing.setup_warnings.length > 0) {
11340
11522
  parts.push("", "## Setup Warnings", ...briefing.setup_warnings.map((w) => `- ${w}`));
11341
11523
  }
11342
- await writeFile30(file, parts.join("\n") + "\n", "utf8");
11524
+ await writeFile31(file, parts.join("\n") + "\n", "utf8");
11343
11525
  return file;
11344
11526
  }
11345
11527
  async function buildEnforcementReport(dir, stage, sessionId) {
11346
- const root = findProjectRoot46(dir);
11528
+ const root = findProjectRoot47(dir);
11347
11529
  const paths = resolveHaivePaths43(root);
11348
- const initialized = existsSync67(paths.haiveDir);
11530
+ const initialized = existsSync68(paths.haiveDir);
11349
11531
  const config = initialized ? await loadConfig8(paths) : {};
11350
11532
  const mode = config.enforcement?.mode ?? "strict";
11351
11533
  const findings = [];
@@ -11354,12 +11536,14 @@ async function buildEnforcementReport(dir, stage, sessionId) {
11354
11536
  root,
11355
11537
  initialized,
11356
11538
  mode,
11539
+ score: buildScore([], config.enforcement?.scoreThreshold),
11357
11540
  should_block: true,
11358
11541
  findings: [{
11359
11542
  severity: "error",
11360
11543
  code: "not-initialized",
11361
11544
  message: "This repository is not initialized with hAIve.",
11362
- fix: "Run `haive init` or `haive enforce install`."
11545
+ fix: "Run `haive init` or `haive enforce install`.",
11546
+ impact: 100
11363
11547
  }]
11364
11548
  };
11365
11549
  }
@@ -11368,6 +11552,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
11368
11552
  root,
11369
11553
  initialized,
11370
11554
  mode,
11555
+ score: buildScore([], config.enforcement?.scoreThreshold),
11371
11556
  should_block: false,
11372
11557
  findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
11373
11558
  };
@@ -11378,42 +11563,67 @@ async function buildEnforcementReport(dir, stage, sessionId) {
11378
11563
  severity: "error",
11379
11564
  code: "briefing-missing",
11380
11565
  message: "No recent hAIve briefing marker was found for this workflow.",
11381
- fix: 'Run `haive briefing --task "..."`, `haive enforce session-start`, or wrap the agent with `haive run -- <agent>`.'
11566
+ fix: 'Run `haive briefing --task "..."`, `haive enforce session-start`, or wrap the agent with `haive run -- <agent>`.',
11567
+ impact: 35
11382
11568
  });
11383
11569
  }
11384
11570
  if (config.enforcement?.requireSessionRecap !== false && (stage === "pre-push" || stage === "ci")) {
11385
11571
  const hasRecap = await hasRecentSessionRecap(paths);
11386
- findings.push(hasRecap ? { severity: "ok", code: "session-recap-present", message: "A recent session_recap memory exists." } : {
11572
+ findings.push(hasRecap ? { severity: "ok", code: "session-recap-present", message: "A recent session_recap memory exists." } : stage === "ci" ? {
11573
+ severity: "warn",
11574
+ code: "session-recap-missing",
11575
+ message: "No recent session_recap memory was found. CI reports this as a warning because personal recaps are usually not committed.",
11576
+ fix: "Run `haive session end --scope team --goal ... --accomplished ...` if you want a team recap visible in CI.",
11577
+ impact: 5
11578
+ } : {
11387
11579
  severity: "error",
11388
11580
  code: "session-recap-missing",
11389
11581
  message: "No recent session_recap memory was found.",
11390
- fix: "Run `haive session end --goal ... --accomplished ...` before pushing."
11582
+ fix: "Run `haive session end --goal ... --accomplished ...` before pushing.",
11583
+ impact: 20
11391
11584
  });
11392
11585
  }
11393
11586
  if (config.enforcement?.requireMemoryVerify !== false) {
11394
11587
  findings.push(...await verifyMemoryPolicy(paths, config));
11395
11588
  }
11589
+ if (config.enforcement?.requireDecisionCoverage !== false) {
11590
+ findings.push(...await verifyDecisionCoverage(paths, stage, sessionId));
11591
+ }
11396
11592
  if (stage === "pre-commit" || stage === "ci") {
11397
11593
  findings.push(...await runPrecommitPolicy(paths));
11398
11594
  }
11595
+ if (config.enforcement?.cleanupGeneratedArtifacts !== false) {
11596
+ findings.push(...await findGeneratedArtifacts(paths));
11597
+ }
11598
+ const score = buildScore(findings, config.enforcement?.scoreThreshold);
11599
+ if (score.score < score.threshold) {
11600
+ findings.push({
11601
+ severity: "error",
11602
+ code: "enforcement-score-below-threshold",
11603
+ message: `Enforcement score ${score.score}% is below required threshold ${score.threshold}%.`,
11604
+ fix: "Load the relevant briefing, address policy findings, then rerun `haive enforce check`.",
11605
+ impact: 0
11606
+ });
11607
+ }
11399
11608
  const hasErrors = findings.some((f) => f.severity === "error");
11400
11609
  return {
11401
11610
  root,
11402
11611
  initialized,
11403
11612
  mode,
11613
+ score: buildScore(findings, config.enforcement?.scoreThreshold),
11404
11614
  should_block: mode === "strict" && hasErrors,
11405
11615
  findings
11406
11616
  };
11407
11617
  }
11408
11618
  async function hasRecentSessionRecap(paths) {
11409
- if (!existsSync67(paths.memoriesDir)) return false;
11619
+ if (!existsSync68(paths.memoriesDir)) return false;
11410
11620
  const all = await loadMemoriesFromDir36(paths.memoriesDir);
11411
11621
  return all.some(
11412
11622
  ({ memory: memory2 }) => memory2.frontmatter.type === "session_recap" && memory2.frontmatter.status !== "rejected" && isFreshIsoDate(memory2.frontmatter.created_at, SESSION_RECAP_TTL_MS)
11413
11623
  );
11414
11624
  }
11415
11625
  async function verifyMemoryPolicy(paths, config) {
11416
- if (!existsSync67(paths.memoriesDir)) return [];
11626
+ if (!existsSync68(paths.memoriesDir)) return [];
11417
11627
  const all = await loadMemoriesFromDir36(paths.memoriesDir);
11418
11628
  const findings = [];
11419
11629
  const staleImportant = [];
@@ -11444,11 +11654,51 @@ async function verifyMemoryPolicy(paths, config) {
11444
11654
  severity: "error",
11445
11655
  code: "stale-important-memories",
11446
11656
  message: `${staleImportant.length} important anchored memories are stale: ${staleImportant.slice(0, 8).join(", ")}`,
11447
- fix: "Run `haive memory verify --update`, then update or delete stale decisions/gotchas before merging."
11657
+ fix: "Run `haive memory verify --update`, then update or delete stale decisions/gotchas before merging.",
11658
+ impact: 40
11448
11659
  });
11449
11660
  }
11450
11661
  return findings;
11451
11662
  }
11663
+ async function verifyDecisionCoverage(paths, stage, sessionId) {
11664
+ if (!existsSync68(paths.memoriesDir)) return [];
11665
+ const changedFiles = await getChangedFiles(paths.root, stage);
11666
+ if (changedFiles.length === 0) {
11667
+ return [{ severity: "info", code: "decision-coverage-no-changes", message: "No changed files to match against policy memories." }];
11668
+ }
11669
+ const all = await loadMemoriesFromDir36(paths.memoriesDir);
11670
+ const policyTypes = /* @__PURE__ */ new Set(["decision", "gotcha", "architecture", "convention"]);
11671
+ const relevant = all.map(({ memory: memory2 }) => memory2).filter((memory2) => {
11672
+ const fm = memory2.frontmatter;
11673
+ if (!policyTypes.has(fm.type)) return false;
11674
+ if (fm.status === "rejected" || fm.status === "deprecated" || fm.status === "stale") return false;
11675
+ return memoryMatchesAnchorPaths6(memory2, changedFiles);
11676
+ });
11677
+ if (relevant.length === 0) {
11678
+ return [{
11679
+ severity: "ok",
11680
+ code: "decision-coverage-none-required",
11681
+ message: `No anchored decisions or policies matched ${changedFiles.length} changed file(s).`
11682
+ }];
11683
+ }
11684
+ const marker = await readRecentBriefingMarker(paths, sessionId);
11685
+ const consulted = new Set(marker?.memory_ids ?? []);
11686
+ const missing = relevant.filter((memory2) => !consulted.has(memory2.frontmatter.id));
11687
+ if (missing.length === 0) {
11688
+ return [{
11689
+ severity: "ok",
11690
+ code: "decision-coverage-pass",
11691
+ message: `Relevant decisions/policies were surfaced for ${changedFiles.length} changed file(s): ${relevant.length}/${relevant.length}.`
11692
+ }];
11693
+ }
11694
+ return [{
11695
+ severity: stage === "local" ? "warn" : "error",
11696
+ code: "decision-coverage-missing",
11697
+ message: `${missing.length}/${relevant.length} relevant anchored decisions/policies were not present in the latest briefing: ${missing.slice(0, 6).map((m) => m.frontmatter.id).join(", ")}`,
11698
+ fix: `Run \`haive briefing --files "${changedFiles.slice(0, 10).join(",")}" --task "..."\` before committing.`,
11699
+ impact: Math.min(35, 10 + missing.length * 5)
11700
+ }];
11701
+ }
11452
11702
  async function runPrecommitPolicy(paths) {
11453
11703
  const staged = await runCommand4("git", ["diff", "--cached", "--name-only"], paths.root).catch(() => "");
11454
11704
  const touchedPaths = staged.split("\n").map((s) => s.trim()).filter(Boolean);
@@ -11473,12 +11723,62 @@ async function runPrecommitPolicy(paths) {
11473
11723
  severity: "error",
11474
11724
  code: "precommit-policy-block",
11475
11725
  message: `Pre-commit policy matched ${result.summary.anti_patterns} anti-pattern(s), ${result.summary.stale_anchors} stale anchor(s).`,
11476
- fix: "Review the hAIve warnings, then update the code or the relevant memories."
11726
+ fix: "Review the hAIve warnings, then update the code or the relevant memories.",
11727
+ impact: 45
11477
11728
  }];
11478
11729
  }
11730
+ async function findGeneratedArtifacts(paths) {
11731
+ const dirty = await runCommand4("git", ["status", "--short", "--untracked-files=all"], paths.root).catch(() => "");
11732
+ const generated = dirty.split("\n").map((line) => line.trim()).filter(Boolean).filter(
11733
+ (line) => line.includes(".ai/.cache/") || line.includes(".ai/.runtime/") || line.includes("__pycache__/") || line.endsWith(".pyc")
11734
+ );
11735
+ if (generated.length === 0) {
11736
+ return [{ severity: "ok", code: "generated-artifacts-clean", message: "No generated runtime/cache artifacts are visible to git." }];
11737
+ }
11738
+ return [{
11739
+ severity: "warn",
11740
+ code: "generated-artifacts-visible",
11741
+ message: `${generated.length} generated artifact(s) are visible in git status.`,
11742
+ fix: "Run `haive enforce cleanup`, update .gitignore, or remove test/runtime outputs before committing.",
11743
+ impact: 10
11744
+ }];
11745
+ }
11746
+ async function getChangedFiles(root, stage) {
11747
+ const commands = stage === "pre-commit" ? [["diff", "--cached", "--name-only"]] : [
11748
+ ["diff", "--cached", "--name-only"],
11749
+ ["diff", "--name-only"]
11750
+ ];
11751
+ const files = /* @__PURE__ */ new Set();
11752
+ for (const args of commands) {
11753
+ const out = await runCommand4("git", args, root).catch(() => "");
11754
+ for (const line of out.split("\n")) {
11755
+ const file = line.trim();
11756
+ if (file) files.add(file);
11757
+ }
11758
+ }
11759
+ return [...files].filter((file) => !file.startsWith(".ai/.runtime/") && !file.startsWith(".ai/.cache/"));
11760
+ }
11761
+ function buildScore(findings, threshold = 80) {
11762
+ const checks = {
11763
+ total: findings.length,
11764
+ ok: findings.filter((f) => f.severity === "ok").length,
11765
+ warn: findings.filter((f) => f.severity === "warn").length,
11766
+ error: findings.filter((f) => f.severity === "error").length
11767
+ };
11768
+ const penalty = findings.reduce((sum, f) => {
11769
+ if (f.severity === "error") return sum + (f.impact ?? 25);
11770
+ if (f.severity === "warn") return sum + (f.impact ?? 8);
11771
+ return sum;
11772
+ }, 0);
11773
+ return {
11774
+ score: Math.max(0, Math.min(100, 100 - penalty)),
11775
+ threshold,
11776
+ checks
11777
+ };
11778
+ }
11479
11779
  async function installGitEnforcement(root) {
11480
- const hooksDir = path45.join(root, ".git", "hooks");
11481
- if (!existsSync67(path45.join(root, ".git"))) {
11780
+ const hooksDir = path46.join(root, ".git", "hooks");
11781
+ if (!existsSync68(path46.join(root, ".git"))) {
11482
11782
  ui.warn("No .git directory found; git enforcement hooks skipped.");
11483
11783
  return;
11484
11784
  }
@@ -11500,31 +11800,31 @@ haive enforce check --stage pre-push --dir . || exit $?
11500
11800
  }
11501
11801
  ];
11502
11802
  for (const hook of hooks) {
11503
- const file = path45.join(hooksDir, hook.name);
11504
- if (existsSync67(file)) {
11505
- const current = await readFile16(file, "utf8").catch(() => "");
11803
+ const file = path46.join(hooksDir, hook.name);
11804
+ if (existsSync68(file)) {
11805
+ const current = await readFile17(file, "utf8").catch(() => "");
11506
11806
  if (current.includes(ENFORCE_HOOK_MARKER)) {
11507
- await writeFile30(file, hook.body, "utf8");
11807
+ await writeFile31(file, hook.body, "utf8");
11508
11808
  } else {
11509
- await writeFile30(file, `${current.trimEnd()}
11809
+ await writeFile31(file, `${current.trimEnd()}
11510
11810
 
11511
11811
  ${hook.body}`, "utf8");
11512
11812
  }
11513
11813
  } else {
11514
- await writeFile30(file, hook.body, "utf8");
11814
+ await writeFile31(file, hook.body, "utf8");
11515
11815
  }
11516
11816
  await chmod2(file, 493);
11517
11817
  }
11518
11818
  ui.success("Installed blocking git enforcement hooks: pre-commit, pre-push");
11519
11819
  }
11520
11820
  async function installCiEnforcement(root) {
11521
- const workflowPath = path45.join(root, ".github", "workflows", "haive-enforcement.yml");
11522
- await mkdir18(path45.dirname(workflowPath), { recursive: true });
11523
- if (existsSync67(workflowPath)) {
11821
+ const workflowPath = path46.join(root, ".github", "workflows", "haive-enforcement.yml");
11822
+ await mkdir18(path46.dirname(workflowPath), { recursive: true });
11823
+ if (existsSync68(workflowPath)) {
11524
11824
  ui.info("GitHub Actions enforcement workflow already exists \u2014 skipped");
11525
11825
  return;
11526
11826
  }
11527
- await writeFile30(workflowPath, `name: haive-enforcement
11827
+ await writeFile31(workflowPath, `name: haive-enforcement
11528
11828
 
11529
11829
  on:
11530
11830
  pull_request:
@@ -11548,7 +11848,7 @@ jobs:
11548
11848
  - name: Enforce hAIve policy
11549
11849
  run: haive enforce ci
11550
11850
  `, "utf8");
11551
- ui.success(`Created ${path45.relative(root, workflowPath)}`);
11851
+ ui.success(`Created ${path46.relative(root, workflowPath)}`);
11552
11852
  }
11553
11853
  function printReport(report, json) {
11554
11854
  if (json) {
@@ -11557,6 +11857,7 @@ function printReport(report, json) {
11557
11857
  }
11558
11858
  console.log(ui.bold(`hAIve enforcement \u2014 ${report.mode}`));
11559
11859
  console.log(ui.dim(` root: ${report.root}`));
11860
+ console.log(ui.dim(` score: ${report.score.score}% / threshold ${report.score.threshold}%`));
11560
11861
  for (const finding of report.findings) {
11561
11862
  const marker = finding.severity === "error" ? ui.red("\u2717") : finding.severity === "warn" ? ui.yellow("\u26A0") : finding.severity === "ok" ? ui.green("\u2713") : ui.dim("\u2022");
11562
11863
  console.log(`${marker} ${finding.code}: ${finding.message}`);
@@ -11576,7 +11877,7 @@ async function readHookPayload() {
11576
11877
  }
11577
11878
  function resolveRoot(dir, payload) {
11578
11879
  try {
11579
- return findProjectRoot46(dir ?? payload.cwd);
11880
+ return findProjectRoot47(dir ?? payload.cwd);
11580
11881
  } catch {
11581
11882
  return null;
11582
11883
  }
@@ -11645,8 +11946,8 @@ function registerRun(program2) {
11645
11946
  }
11646
11947
 
11647
11948
  // src/index.ts
11648
- var program = new Command49();
11649
- program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.9.10");
11949
+ var program = new Command50();
11950
+ program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.11");
11650
11951
  registerInit(program);
11651
11952
  registerWelcome(program);
11652
11953
  registerResolveProject(program);
@@ -11694,6 +11995,7 @@ registerSnapshot(program);
11694
11995
  registerHub(program);
11695
11996
  registerStats(program);
11696
11997
  registerBench(program);
11998
+ registerBenchmark(program);
11697
11999
  registerDoctor(program);
11698
12000
  registerPlayback(program);
11699
12001
  registerPrecommit(program);