@hivelore/mcp 0.37.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1261,6 +1261,7 @@ async function proposeSensor(input, ctx) {
1261
1261
  if (!found) {
1262
1262
  throw new Error(`No memory found with id ${input.memory_id}`);
1263
1263
  }
1264
+ const personalScopeNudge = found.memory.frontmatter.scope === "personal" ? ` Note: this lesson is personal-scoped, so the sensor guards only YOUR machine (personal memories are gitignored). Promote it so the gate travels with the repo: hivelore memory promote ${input.memory_id}.` : "";
1264
1265
  if (kind !== "regex") {
1265
1266
  const verdictCmd = runCommandForValidation(input.command.trim(), ctx.paths.root, input.timeout_ms);
1266
1267
  const anchorPathsCmd = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
@@ -1295,7 +1296,7 @@ ${verdictCmd.detail}`,
1295
1296
  accepted: true,
1296
1297
  memory_id: input.memory_id,
1297
1298
  severity: input.severity,
1298
- guidance: verdictCmd.status === "passed" ? "Command oracle passes on the current tree; the gate now runs it when the diff touches the sensor's paths (requires enforcement.runCommandSensors=true)." : `Accepted at warn severity, but note: ${verdictCmd.status} on the current tree (${verdictCmd.detail}).`,
1299
+ guidance: (verdictCmd.status === "passed" ? "Command oracle passes on the current tree; the gate now runs it when the diff touches the sensor's paths (requires enforcement.runCommandSensors=true)." : `Accepted at warn severity, but note: ${verdictCmd.status} on the current tree (${verdictCmd.detail}).`) + personalScopeNudge,
1299
1300
  self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: null, fired_on: [] }
1300
1301
  };
1301
1302
  }
@@ -1344,6 +1345,7 @@ ${verdictCmd.detail}`,
1344
1345
  accepted: true,
1345
1346
  memory_id: input.memory_id,
1346
1347
  severity: input.severity,
1348
+ ...personalScopeNudge ? { guidance: personalScopeNudge.trim() } : {},
1347
1349
  self_check,
1348
1350
  file_path: found.filePath
1349
1351
  };
@@ -1354,7 +1356,9 @@ var MemTriedInputSchema = {
1354
1356
  what: z16.string().min(1).describe("Brief description of the approach that was tried"),
1355
1357
  why_failed: z16.string().min(1).describe("Why it failed or why it should NOT be used"),
1356
1358
  instead: z16.string().optional().describe("What to use or do instead (recommended alternative)"),
1357
- scope: z16.enum(["personal", "team", "module"]).default("personal").describe("Visibility scope"),
1359
+ scope: z16.enum(["personal", "team", "module"]).optional().describe(
1360
+ "Visibility scope. Defaults to personal \u2014 EXCEPT when a one-shot `sensor` is attached: an enforced lesson is team truth (the sensor must travel to every machine and CI), so it defaults to team. Pass scope explicitly to override."
1361
+ ),
1358
1362
  module: z16.string().optional().describe("Module name (required when scope=module)"),
1359
1363
  tags: z16.array(z16.string()).default([]).describe("Tags for filtering"),
1360
1364
  paths: z16.array(z16.string()).default([]).describe("Anchor file paths this applies to"),
@@ -1378,10 +1382,11 @@ async function memTried(input, ctx) {
1378
1382
  throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'hivelore init' first.`);
1379
1383
  }
1380
1384
  const slug = input.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 5).join("-");
1385
+ const scope = input.scope ?? (input.sensor ? "team" : "personal");
1381
1386
  const baseFm = buildFrontmatter2({
1382
1387
  type: "attempt",
1383
1388
  slug,
1384
- scope: input.scope,
1389
+ scope,
1385
1390
  module: input.module,
1386
1391
  tags: input.tags,
1387
1392
  paths: input.paths,
@@ -1429,7 +1434,7 @@ async function memTried(input, ctx) {
1429
1434
  ...verdict.reason ? { reason: verdict.reason } : {},
1430
1435
  ...verdict.guidance ? { guidance: verdict.guidance } : {}
1431
1436
  },
1432
- hint: verdict.accepted ? "Loop closed: the attempt is saved AND enforced \u2014 the gate now refuses a repeat deterministically." : `Attempt saved, but the sensor was rejected (${verdict.reason}). Revise per the guidance and re-propose with propose_sensor.`
1437
+ hint: (verdict.accepted ? "Loop closed: the attempt is saved AND enforced \u2014 the gate now refuses a repeat deterministically." : `Attempt saved, but the sensor was rejected (${verdict.reason}). Revise per the guidance and re-propose with propose_sensor.`) + (input.scope === void 0 ? " Saved team-scoped (an enforced lesson must travel with the repo) \u2014 pass scope:'personal' to keep it private." : "")
1433
1438
  };
1434
1439
  }
1435
1440
  const seed = input.paths.length > 0 ? suggestSensorSeed2(body, input.paths) : null;
@@ -1450,43 +1455,177 @@ async function memTried(input, ctx) {
1450
1455
  };
1451
1456
  }
1452
1457
 
1453
- // src/tools/ingest-findings.ts
1454
- import { existsSync as existsSync17 } from "fs";
1458
+ // src/tools/scaffold-test.ts
1459
+ import { existsSync as existsSync17, statSync } from "fs";
1455
1460
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile10 } from "fs/promises";
1456
1461
  import path7 from "path";
1462
+ import { z as z17 } from "zod";
1463
+ import {
1464
+ buildProposeCommand,
1465
+ loadMemoriesFromDir as loadMemoriesFromDir14,
1466
+ normalizeFramework,
1467
+ parseLessonFields,
1468
+ pickTestFramework,
1469
+ scaffoldPostIncidentTest
1470
+ } from "@hivelore/core";
1471
+ var PY_SIGNALS = ["pyproject.toml", "setup.py", "pytest.ini", "requirements.txt", "tox.ini"];
1472
+ async function detectForAnchor(root, rel) {
1473
+ let dir = path7.resolve(root, rel);
1474
+ try {
1475
+ if (!statSync(dir).isDirectory()) dir = path7.dirname(dir);
1476
+ } catch {
1477
+ if (path7.extname(dir)) dir = path7.dirname(dir);
1478
+ }
1479
+ while (dir.startsWith(root)) {
1480
+ const pkgJson = path7.join(dir, "package.json");
1481
+ const hasPkg = existsSync17(pkgJson);
1482
+ const goMod = existsSync17(path7.join(dir, "go.mod"));
1483
+ const pySignal = PY_SIGNALS.some((s) => existsSync17(path7.join(dir, s)));
1484
+ if (hasPkg || goMod || pySignal) {
1485
+ let pkg = null;
1486
+ if (hasPkg) {
1487
+ try {
1488
+ pkg = JSON.parse(await readFile4(pkgJson, "utf8"));
1489
+ } catch {
1490
+ pkg = null;
1491
+ }
1492
+ }
1493
+ const baseDir = path7.relative(root, dir).split(path7.sep).join("/");
1494
+ return { framework: pickTestFramework(pkg, { goMod, pySignal }), baseDir };
1495
+ }
1496
+ const parent = path7.dirname(dir);
1497
+ if (parent === dir || dir === root) break;
1498
+ dir = parent;
1499
+ }
1500
+ return null;
1501
+ }
1502
+ async function detectTestFrameworksForAnchors(root, anchorPaths) {
1503
+ const starts = anchorPaths.length > 0 ? anchorPaths : ["."];
1504
+ const groups = /* @__PURE__ */ new Map();
1505
+ for (const rel of starts) {
1506
+ const found = await detectForAnchor(root, rel) ?? { framework: "vitest", baseDir: "" };
1507
+ const existing = groups.get(found.baseDir);
1508
+ if (existing) existing.anchors.push(rel);
1509
+ else groups.set(found.baseDir, { ...found, anchors: [rel] });
1510
+ }
1511
+ return [...groups.values()];
1512
+ }
1513
+ var ScaffoldTestInputSchema = {
1514
+ memory_id: z17.string().min(1).describe("Id of the attempt/gotcha lesson to scaffold a post-incident test from."),
1515
+ framework: z17.enum(["vitest", "jest", "pytest", "gotest"]).optional().describe("Test framework. Auto-detected from the package that owns the lesson's anchor paths when omitted."),
1516
+ out_path: z17.string().optional().describe("Override the generated test file path (repo-relative)."),
1517
+ write: z17.boolean().default(true).describe("Write the file to disk (default). false = return the content for preview without writing.")
1518
+ };
1519
+ async function scaffoldTest(input, ctx) {
1520
+ const loaded = existsSync17(ctx.paths.memoriesDir) ? await loadMemoriesFromDir14(ctx.paths.memoriesDir) : [];
1521
+ const found = loaded.find(({ memory }) => memory.frontmatter.id === input.memory_id);
1522
+ if (!found) {
1523
+ return { ok: false, error: `No memory found with id ${input.memory_id}`, memory_id: input.memory_id };
1524
+ }
1525
+ const anchorPaths = found.memory.frontmatter.anchor.paths ?? [];
1526
+ const allGroups = await detectTestFrameworksForAnchors(ctx.paths.root, anchorPaths);
1527
+ const groups = input.out_path ? allGroups.slice(0, 1) : allGroups;
1528
+ const frameworkFor = (detected) => input.framework ? normalizeFramework(input.framework) ?? detected : detected;
1529
+ const fields = parseLessonFields(found.memory.body);
1530
+ const lesson = {
1531
+ memoryId: input.memory_id,
1532
+ title: fields.title || input.memory_id,
1533
+ whyFailed: fields.whyFailed,
1534
+ instead: fields.instead,
1535
+ incident: found.memory.frontmatter.sensor?.incident,
1536
+ paths: anchorPaths
1537
+ };
1538
+ let scaffolds = groups.map(
1539
+ (g) => scaffoldPostIncidentTest(lesson, { framework: frameworkFor(g.framework), outPath: input.out_path, baseDir: g.baseDir })
1540
+ );
1541
+ let proposeCommand = scaffolds[0].proposeCommand;
1542
+ if (scaffolds.length > 1) {
1543
+ proposeCommand = buildProposeCommand(lesson, scaffolds.map((s) => s.runCommand).join(" && "));
1544
+ scaffolds = groups.map(
1545
+ (g) => scaffoldPostIncidentTest(lesson, {
1546
+ framework: frameworkFor(g.framework),
1547
+ baseDir: g.baseDir,
1548
+ proposeCommandOverride: proposeCommand
1549
+ })
1550
+ );
1551
+ }
1552
+ const results = [];
1553
+ for (const scaffold of scaffolds) {
1554
+ const abs = path7.isAbsolute(scaffold.relPath) ? scaffold.relPath : path7.resolve(ctx.paths.root, scaffold.relPath);
1555
+ let written = false;
1556
+ let alreadyExists = false;
1557
+ if (input.write) {
1558
+ if (existsSync17(abs)) {
1559
+ alreadyExists = true;
1560
+ } else {
1561
+ await mkdir4(path7.dirname(abs), { recursive: true });
1562
+ await writeFile10(abs, scaffold.content, "utf8");
1563
+ written = true;
1564
+ }
1565
+ }
1566
+ results.push({
1567
+ framework: scaffold.framework,
1568
+ path: scaffold.relPath,
1569
+ run_command: scaffold.runCommand,
1570
+ content: scaffold.content,
1571
+ written,
1572
+ already_exists: alreadyExists
1573
+ });
1574
+ }
1575
+ const first = results[0];
1576
+ const anyExisting = results.some((r) => r.already_exists);
1577
+ return {
1578
+ ok: true,
1579
+ memory_id: input.memory_id,
1580
+ framework: first.framework,
1581
+ path: first.path,
1582
+ run_command: first.run_command,
1583
+ propose_command: proposeCommand,
1584
+ content: first.content,
1585
+ written: first.written,
1586
+ already_exists: first.already_exists,
1587
+ ...results.length > 1 ? { scaffolds: results } : {},
1588
+ notice: (results.length > 1 ? `Lesson spans ${results.length} packages \u2014 one pending test per owning package; ONE propose_command arms them all (chained oracle). ` : "") + (anyExisting ? "Some file(s) already exist \u2014 not overwritten. Delete them or pass out_path to write elsewhere." : "PENDING test scaffolded. Fill in the assertion (RED on the incident, GREEN once fixed), run it, then arm it with propose_command \u2014 propose_sensor stays the sole validated writer.")
1589
+ };
1590
+ }
1591
+
1592
+ // src/tools/ingest-findings.ts
1593
+ import { existsSync as existsSync18 } from "fs";
1594
+ import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile11 } from "fs/promises";
1595
+ import path8 from "path";
1457
1596
  import {
1458
1597
  draftsFromFindings,
1459
1598
  filterNewDrafts,
1460
- loadMemoriesFromDir as loadMemoriesFromDir14,
1599
+ loadMemoriesFromDir as loadMemoriesFromDir15,
1461
1600
  memoryFilePath as memoryFilePath3,
1462
1601
  parseFindings,
1463
1602
  serializeMemory as serializeMemory9
1464
1603
  } from "@hivelore/core";
1465
- import { z as z17 } from "zod";
1604
+ import { z as z18 } from "zod";
1466
1605
  var IngestFindingsInputSchema = {
1467
- format: z17.enum(["sarif", "sonar"]).describe("Report format: 'sarif' (ESLint/Semgrep/CodeQL) or 'sonar' (SonarQube issues JSON)"),
1468
- report_path: z17.string().optional().describe("Project-relative path to the findings JSON file. Provide this OR `report`."),
1469
- report: z17.string().optional().describe("Inline findings JSON content. Provide this OR `report_path`."),
1470
- type: z17.enum(["gotcha", "convention"]).default("gotcha").describe("Memory type for the created drafts"),
1471
- scope: z17.enum(["personal", "team", "module"]).default("team").describe("Visibility scope for the created memories"),
1472
- module: z17.string().optional().describe("Module name (required when scope=module)"),
1473
- min_severity: z17.enum(["info", "minor", "major", "critical", "blocker"]).optional().describe("Ignore findings below this severity"),
1474
- include_stylistic: z17.boolean().optional().describe("Also ingest auto-fixable stylistic rules (semi/quotes/prefer-const\u2026); off by default as low-value noise"),
1475
- limit: z17.number().int().positive().optional().describe("Cap the number of memories created"),
1476
- author: z17.string().optional().describe("Author handle or email"),
1477
- dry_run: z17.boolean().default(false).describe("When true, return the drafts that WOULD be created without writing them")
1606
+ format: z18.enum(["sarif", "sonar"]).describe("Report format: 'sarif' (ESLint/Semgrep/CodeQL) or 'sonar' (SonarQube issues JSON)"),
1607
+ report_path: z18.string().optional().describe("Project-relative path to the findings JSON file. Provide this OR `report`."),
1608
+ report: z18.string().optional().describe("Inline findings JSON content. Provide this OR `report_path`."),
1609
+ type: z18.enum(["gotcha", "convention"]).default("gotcha").describe("Memory type for the created drafts"),
1610
+ scope: z18.enum(["personal", "team", "module"]).default("team").describe("Visibility scope for the created memories"),
1611
+ module: z18.string().optional().describe("Module name (required when scope=module)"),
1612
+ min_severity: z18.enum(["info", "minor", "major", "critical", "blocker"]).optional().describe("Ignore findings below this severity"),
1613
+ include_stylistic: z18.boolean().optional().describe("Also ingest auto-fixable stylistic rules (semi/quotes/prefer-const\u2026); off by default as low-value noise"),
1614
+ limit: z18.number().int().positive().optional().describe("Cap the number of memories created"),
1615
+ author: z18.string().optional().describe("Author handle or email"),
1616
+ dry_run: z18.boolean().default(false).describe("When true, return the drafts that WOULD be created without writing them")
1478
1617
  };
1479
1618
  async function ingestFindings(input, ctx) {
1480
- if (!existsSync17(ctx.paths.haiveDir)) {
1619
+ if (!existsSync18(ctx.paths.haiveDir)) {
1481
1620
  throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'hivelore init' first.`);
1482
1621
  }
1483
1622
  let raw;
1484
1623
  if (input.report && input.report.trim()) {
1485
1624
  raw = input.report;
1486
1625
  } else if (input.report_path) {
1487
- const file = path7.resolve(ctx.paths.root, input.report_path);
1488
- if (!existsSync17(file)) throw new Error(`Report file not found: ${file}`);
1489
- raw = await readFile4(file, "utf8");
1626
+ const file = path8.resolve(ctx.paths.root, input.report_path);
1627
+ if (!existsSync18(file)) throw new Error(`Report file not found: ${file}`);
1628
+ raw = await readFile5(file, "utf8");
1490
1629
  } else {
1491
1630
  throw new Error("Provide either `report_path` or `report`.");
1492
1631
  }
@@ -1500,7 +1639,7 @@ async function ingestFindings(input, ctx) {
1500
1639
  ...input.include_stylistic ? { includeStylistic: true } : {},
1501
1640
  ...input.limit ? { limit: input.limit } : {}
1502
1641
  });
1503
- const existing = existsSync17(ctx.paths.memoriesDir) ? await loadMemoriesFromDir14(ctx.paths.memoriesDir) : [];
1642
+ const existing = existsSync18(ctx.paths.memoriesDir) ? await loadMemoriesFromDir15(ctx.paths.memoriesDir) : [];
1504
1643
  const existingTopics = new Set(
1505
1644
  existing.map(({ memory }) => memory.frontmatter.topic).filter((t) => Boolean(t))
1506
1645
  );
@@ -1538,22 +1677,22 @@ async function writeDraft(ctx, draft) {
1538
1677
  draft.frontmatter.id,
1539
1678
  draft.frontmatter.module
1540
1679
  );
1541
- await mkdir4(path7.dirname(file), { recursive: true });
1542
- await writeFile10(file, serializeMemory9({ frontmatter: draft.frontmatter, body: draft.body }), "utf8");
1680
+ await mkdir5(path8.dirname(file), { recursive: true });
1681
+ await writeFile11(file, serializeMemory9({ frontmatter: draft.frontmatter, body: draft.body }), "utf8");
1543
1682
  return file;
1544
1683
  }
1545
1684
 
1546
1685
  // src/tools/mem-session-end.ts
1547
- import { writeFile as writeFile12, mkdir as mkdir6 } from "fs/promises";
1548
- import { existsSync as existsSync19 } from "fs";
1549
- import path9 from "path";
1686
+ import { writeFile as writeFile13, mkdir as mkdir7 } from "fs/promises";
1687
+ import { existsSync as existsSync20 } from "fs";
1688
+ import path10 from "path";
1550
1689
  import {
1551
1690
  buildFrontmatter as buildFrontmatter3,
1552
- loadMemoriesFromDir as loadMemoriesFromDir15,
1691
+ loadMemoriesFromDir as loadMemoriesFromDir16,
1553
1692
  memoryFilePath as memoryFilePath4,
1554
1693
  serializeMemory as serializeMemory10
1555
1694
  } from "@hivelore/core";
1556
- import { z as z18 } from "zod";
1695
+ import { z as z19 } from "zod";
1557
1696
 
1558
1697
  // src/session-tracker.ts
1559
1698
  import {
@@ -1562,12 +1701,12 @@ import {
1562
1701
  loadConfig as loadConfig2,
1563
1702
  writeSessionHandoff
1564
1703
  } from "@hivelore/core";
1565
- import { mkdir as mkdir5, writeFile as writeFile11, rm } from "fs/promises";
1566
- import { existsSync as existsSync18 } from "fs";
1567
- import path8 from "path";
1704
+ import { mkdir as mkdir6, writeFile as writeFile12, rm } from "fs/promises";
1705
+ import { existsSync as existsSync19 } from "fs";
1706
+ import path9 from "path";
1568
1707
  import { execSync as execSync2 } from "child_process";
1569
1708
  function pendingDistillPath(ctx) {
1570
- return path8.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
1709
+ return path9.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
1571
1710
  }
1572
1711
  var SessionTracker = class {
1573
1712
  events = [];
@@ -1671,7 +1810,7 @@ var SessionTracker = class {
1671
1810
  (e) => e.tool === "mem_session_end" && !e.summary?.startsWith("Auto-captured")
1672
1811
  );
1673
1812
  const isSubstantialSession = totalCalls >= 3 || writingTools.length > 0;
1674
- if (!ranPostTask && isSubstantialSession && existsSync18(this.ctx.paths.haiveDir)) {
1813
+ if (!ranPostTask && isSubstantialSession && existsSync19(this.ctx.paths.haiveDir)) {
1675
1814
  try {
1676
1815
  const memoriesSaved = writingTools.map((e) => e.summary ?? "").filter(Boolean).slice(0, 20);
1677
1816
  const payload = {
@@ -1684,9 +1823,9 @@ var SessionTracker = class {
1684
1823
  ...gitDiff ? { git_diff: gitDiff } : {},
1685
1824
  ...recapId ? { recap_id: recapId } : {}
1686
1825
  };
1687
- const cacheDir = path8.join(this.ctx.paths.haiveDir, ".cache");
1688
- await mkdir5(cacheDir, { recursive: true });
1689
- await writeFile11(
1826
+ const cacheDir = path9.join(this.ctx.paths.haiveDir, ".cache");
1827
+ await mkdir6(cacheDir, { recursive: true });
1828
+ await writeFile12(
1690
1829
  pendingDistillPath(this.ctx),
1691
1830
  JSON.stringify(payload, null, 2) + "\n",
1692
1831
  "utf8"
@@ -1705,7 +1844,7 @@ var SessionTracker = class {
1705
1844
  };
1706
1845
  async function clearPendingDistill(ctx) {
1707
1846
  const p = pendingDistillPath(ctx);
1708
- if (existsSync18(p)) {
1847
+ if (existsSync19(p)) {
1709
1848
  try {
1710
1849
  await rm(p);
1711
1850
  } catch {
@@ -1722,15 +1861,15 @@ function summarizeTools(events) {
1722
1861
 
1723
1862
  // src/tools/mem-session-end.ts
1724
1863
  var MemSessionEndInputSchema = {
1725
- goal: z18.string().min(1).describe("What you were trying to accomplish this session (1\u20132 sentences)"),
1726
- accomplished: z18.string().describe("What was actually done \u2014 bullet list recommended"),
1727
- discoveries: z18.string().default("").describe(
1864
+ goal: z19.string().min(1).describe("What you were trying to accomplish this session (1\u20132 sentences)"),
1865
+ accomplished: z19.string().describe("What was actually done \u2014 bullet list recommended"),
1866
+ discoveries: z19.string().default("").describe(
1728
1867
  "Any bugs, inconsistencies, surprises, or missing knowledge found during this session. Empty if nothing surprising was found."
1729
1868
  ),
1730
- files_touched: z18.array(z18.string()).default([]).describe("Key files that were read or modified \u2014 used as anchor paths"),
1731
- next_steps: z18.string().default("").describe("What should happen next (for the next session or a teammate)"),
1732
- scope: z18.enum(["personal", "team", "module"]).default("personal").describe("Visibility: personal = private to you, team = shared with the team"),
1733
- module: z18.string().optional().describe("Module name (required when scope=module)")
1869
+ files_touched: z19.array(z19.string()).default([]).describe("Key files that were read or modified \u2014 used as anchor paths"),
1870
+ next_steps: z19.string().default("").describe("What should happen next (for the next session or a teammate)"),
1871
+ scope: z19.enum(["personal", "team", "module"]).default("personal").describe("Visibility: personal = private to you, team = shared with the team"),
1872
+ module: z19.string().optional().describe("Module name (required when scope=module)")
1734
1873
  };
1735
1874
  function recapTopic(scope, module) {
1736
1875
  return module ? `session-recap-${scope}-${module}` : `session-recap-${scope}`;
@@ -1760,23 +1899,23 @@ ${input.next_steps}`);
1760
1899
  return lines.join("\n");
1761
1900
  }
1762
1901
  async function memSessionEnd(input, ctx) {
1763
- if (!existsSync19(ctx.paths.haiveDir)) {
1902
+ if (!existsSync20(ctx.paths.haiveDir)) {
1764
1903
  throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'hivelore init' first.`);
1765
1904
  }
1766
1905
  const body = buildBody(input);
1767
1906
  const topic = recapTopic(input.scope, input.module);
1768
1907
  const normalizedFiles = input.files_touched.map((p) => {
1769
- if (!p || !path9.isAbsolute(p)) return p;
1770
- const rel = path9.relative(ctx.paths.root, p);
1908
+ if (!p || !path10.isAbsolute(p)) return p;
1909
+ const rel = path10.relative(ctx.paths.root, p);
1771
1910
  return rel.startsWith("..") ? p : rel;
1772
1911
  });
1773
1912
  const invalidPaths = normalizedFiles.filter(
1774
- (p) => !existsSync19(path9.resolve(ctx.paths.root, p))
1913
+ (p) => !existsSync20(path10.resolve(ctx.paths.root, p))
1775
1914
  );
1776
1915
  if (invalidPaths.length > 0) {
1777
1916
  console.warn(`[haive] session end: anchor path(s) not found: ${invalidPaths.join(", ")}`);
1778
1917
  }
1779
- const existing = existsSync19(ctx.paths.memoriesDir) ? await loadMemoriesFromDir15(ctx.paths.memoriesDir) : [];
1918
+ const existing = existsSync20(ctx.paths.memoriesDir) ? await loadMemoriesFromDir16(ctx.paths.memoriesDir) : [];
1780
1919
  const topicMatch = existing.find(
1781
1920
  ({ memory }) => memory.frontmatter.topic === topic && memory.frontmatter.scope === input.scope && (!input.module || memory.frontmatter.module === input.module)
1782
1921
  );
@@ -1792,7 +1931,7 @@ async function memSessionEnd(input, ctx) {
1792
1931
  paths: normalizedFiles.length ? normalizedFiles : fm.anchor.paths
1793
1932
  }
1794
1933
  };
1795
- await writeFile12(
1934
+ await writeFile13(
1796
1935
  topicMatch.filePath,
1797
1936
  serializeMemory10({ frontmatter: newFrontmatter, body }),
1798
1937
  "utf8"
@@ -1822,8 +1961,8 @@ async function memSessionEnd(input, ctx) {
1822
1961
  frontmatter.id,
1823
1962
  frontmatter.module
1824
1963
  );
1825
- await mkdir6(path9.dirname(file), { recursive: true });
1826
- await writeFile12(file, serializeMemory10({ frontmatter, body }), "utf8");
1964
+ await mkdir7(path10.dirname(file), { recursive: true });
1965
+ await writeFile13(file, serializeMemory10({ frontmatter, body }), "utf8");
1827
1966
  await clearPendingDistill(ctx);
1828
1967
  return {
1829
1968
  id: frontmatter.id,
@@ -1835,9 +1974,9 @@ async function memSessionEnd(input, ctx) {
1835
1974
  }
1836
1975
 
1837
1976
  // src/tools/get-briefing.ts
1838
- import { readFile as readFile6, writeFile as writeFile13, readdir as readdir4 } from "fs/promises";
1839
- import { existsSync as existsSync21 } from "fs";
1840
- import path11 from "path";
1977
+ import { readFile as readFile7, writeFile as writeFile14, readdir as readdir4 } from "fs/promises";
1978
+ import { existsSync as existsSync22 } from "fs";
1979
+ import path12 from "path";
1841
1980
  import {
1842
1981
  allocateBudget,
1843
1982
  assessBootstrapState,
@@ -1861,7 +2000,7 @@ import {
1861
2000
  loadConfig as loadConfig3,
1862
2001
  memoryHasExcludedTag,
1863
2002
  hashProjectContext,
1864
- loadMemoriesFromDir as loadMemoriesFromDir16,
2003
+ loadMemoriesFromDir as loadMemoriesFromDir17,
1865
2004
  loadPreventionEvents,
1866
2005
  loadUsageIndex as loadUsageIndex8,
1867
2006
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
@@ -1879,12 +2018,12 @@ import {
1879
2018
  truncateToTokens,
1880
2019
  writeBriefingMarker
1881
2020
  } from "@hivelore/core";
1882
- import { z as z19 } from "zod";
2021
+ import { z as z20 } from "zod";
1883
2022
 
1884
2023
  // src/tools/briefing-helpers.ts
1885
- import { readdir as readdir3, readFile as readFile5 } from "fs/promises";
1886
- import { existsSync as existsSync20 } from "fs";
1887
- import path10 from "path";
2024
+ import { readdir as readdir3, readFile as readFile6 } from "fs/promises";
2025
+ import { existsSync as existsSync21 } from "fs";
2026
+ import path11 from "path";
1888
2027
  import {
1889
2028
  classifyMemoryPriority as coreClassifyPriority,
1890
2029
  isGlobPath,
@@ -2017,16 +2156,16 @@ async function trySemanticHits(ctx, task, limit) {
2017
2156
  }
2018
2157
  async function loadModuleContexts2(ctx, modules) {
2019
2158
  if (modules.length === 0) return [];
2020
- if (!existsSync20(ctx.paths.modulesContextDir)) return [];
2159
+ if (!existsSync21(ctx.paths.modulesContextDir)) return [];
2021
2160
  const available = new Set(
2022
2161
  (await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
2023
2162
  );
2024
2163
  const out = [];
2025
2164
  for (const m of modules) {
2026
2165
  if (!available.has(m)) continue;
2027
- const file = path10.join(ctx.paths.modulesContextDir, m, "context.md");
2028
- if (existsSync20(file)) {
2029
- out.push({ name: m, content: await readFile5(file, "utf8") });
2166
+ const file = path11.join(ctx.paths.modulesContextDir, m, "context.md");
2167
+ if (existsSync21(file)) {
2168
+ out.push({ name: m, content: await readFile6(file, "utf8") });
2030
2169
  }
2031
2170
  }
2032
2171
  return out;
@@ -2034,38 +2173,38 @@ async function loadModuleContexts2(ctx, modules) {
2034
2173
 
2035
2174
  // src/tools/get-briefing.ts
2036
2175
  var GetBriefingInputSchema = {
2037
- task: z19.string().optional().describe(
2176
+ task: z20.string().optional().describe(
2038
2177
  "What you are about to do, in 1\u20132 sentences. Used to rank relevant memories semantically."
2039
2178
  ),
2040
- files: z19.array(z19.string()).default([]).describe("Project-relative file paths the agent is currently looking at or about to edit"),
2041
- max_tokens: z19.number().int().positive().default(8e3).describe(
2179
+ files: z20.array(z20.string()).default([]).describe("Project-relative file paths the agent is currently looking at or about to edit"),
2180
+ max_tokens: z20.number().int().positive().default(8e3).describe(
2042
2181
  "Approximate token budget for the entire briefing. Each section is allocated a share and truncated to fit."
2043
2182
  ),
2044
- max_memories: z19.number().int().positive().default(8).describe("Cap on memories surfaced regardless of token budget"),
2045
- include_project_context: z19.boolean().default(true),
2046
- dedupe_project_context: z19.boolean().optional().describe(
2183
+ max_memories: z20.number().int().positive().default(8).describe("Cap on memories surfaced regardless of token budget"),
2184
+ include_project_context: z20.boolean().default(true),
2185
+ dedupe_project_context: z20.boolean().optional().describe(
2047
2186
  "Token saver (default ON): skip re-emitting the project-context body if an identical copy was already sent within the last few minutes this session (the agent still has it). Set false to always include it."
2048
2187
  ),
2049
- include_module_contexts: z19.boolean().default(true),
2050
- semantic: z19.boolean().default(true).describe(
2188
+ include_module_contexts: z20.boolean().default(true),
2189
+ semantic: z20.boolean().default(true).describe(
2051
2190
  "Use semantic ranking when a task is provided (requires `hivelore embeddings index`)."
2052
2191
  ),
2053
- include_stale: z19.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
2054
- track: z19.boolean().default(true).describe("Increment read_count on returned memories"),
2055
- format: z19.enum(["full", "compact", "actions"]).default("full").describe(
2192
+ include_stale: z20.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
2193
+ track: z20.boolean().default(true).describe("Increment read_count on returned memories"),
2194
+ format: z20.enum(["full", "compact", "actions"]).default("full").describe(
2056
2195
  "Output format: 'full' returns memory bodies (honors token budget via truncation); 'compact' returns a 1-line summary per memory (call mem_get for detail); 'actions' squeezes bodies to actionable bullet lines \u2014 fewer tokens vs full."
2057
2196
  ),
2058
- symbols: z19.array(z19.string()).default([]).describe(
2197
+ symbols: z20.array(z20.string()).default([]).describe(
2059
2198
  "Symbol names to look up in the code-map (e.g. ['PaymentService', 'TenantFilter']). Returns the file(s) exporting each symbol so agents don't need to grep. Requires `hivelore index code` to have been run."
2060
2199
  ),
2061
- min_semantic_score: z19.number().min(0).max(1).default(0).describe(
2200
+ min_semantic_score: z20.number().min(0).max(1).default(0).describe(
2062
2201
  "Drop semantic-only memory hits whose cosine score is below this threshold. Useful to avoid weakly-related noise when the task is short or the corpus is broad. Has no effect on memories matched via anchor/module/literal \u2014 those are always kept. Try 0.25\u20130.4 for stricter matching."
2063
2202
  ),
2064
- budget_preset: z19.enum(["quick", "balanced", "deep"]).optional().describe(
2203
+ budget_preset: z20.enum(["quick", "balanced", "deep"]).optional().describe(
2065
2204
  "Shortcut token budget: 'quick' minimizes tokens/skip module CONTEXT slices; 'balanced' mirrors historical defaults; 'deep' uses a larger briefing. When set, overrides max_tokens, max_memories, and include_module_contexts."
2066
2205
  )
2067
2206
  };
2068
- var GetBriefingZod = z19.object(GetBriefingInputSchema);
2207
+ var GetBriefingZod = z20.object(GetBriefingInputSchema);
2069
2208
  async function getBriefing(input, ctx) {
2070
2209
  const resolvedBudget = resolveBriefingBudget(input.budget_preset, {
2071
2210
  max_tokens: input.max_tokens,
@@ -2081,8 +2220,8 @@ async function getBriefing(input, ctx) {
2081
2220
  let usage = { version: 1, updated_at: "", by_id: {} };
2082
2221
  let byId = /* @__PURE__ */ new Map();
2083
2222
  let lastSession;
2084
- if (existsSync21(ctx.paths.memoriesDir)) {
2085
- const allLoaded = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
2223
+ if (existsSync22(ctx.paths.memoriesDir)) {
2224
+ const allLoaded = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
2086
2225
  const recaps = allLoaded.filter(({ memory }) => memory.frontmatter.type === "session_recap").sort(
2087
2226
  (a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
2088
2227
  );
@@ -2256,7 +2395,7 @@ async function getBriefing(input, ctx) {
2256
2395
  if (!isAutoPromoteEligible(loaded.memory.frontmatter, u, rule)) continue;
2257
2396
  const newFm = { ...loaded.memory.frontmatter, status: "validated", validated_by: "auto" };
2258
2397
  try {
2259
- await writeFile13(loaded.filePath, serializeMemory11({ frontmatter: newFm, body: loaded.memory.body }), "utf8");
2398
+ await writeFile14(loaded.filePath, serializeMemory11({ frontmatter: newFm, body: loaded.memory.body }), "utf8");
2260
2399
  m.status = "validated";
2261
2400
  m.confidence = "trusted";
2262
2401
  } catch {
@@ -2264,7 +2403,7 @@ async function getBriefing(input, ctx) {
2264
2403
  }
2265
2404
  }
2266
2405
  }
2267
- let projectContextRaw = input.include_project_context && existsSync21(ctx.paths.projectContext) ? await readFile6(ctx.paths.projectContext, "utf8") : "";
2406
+ let projectContextRaw = input.include_project_context && existsSync22(ctx.paths.projectContext) ? await readFile7(ctx.paths.projectContext, "utf8") : "";
2268
2407
  let contextOmittedRecent = false;
2269
2408
  if (projectContextRaw && input.dedupe_project_context !== false) {
2270
2409
  const ctxHash = hashProjectContext(projectContextRaw);
@@ -2279,7 +2418,7 @@ async function getBriefing(input, ctx) {
2279
2418
  const setupWarnings = [];
2280
2419
  let autoContextGenerated = false;
2281
2420
  let projectContext = isTemplateContext ? "" : projectContextRaw;
2282
- if ((isTemplateContext || !existsSync21(ctx.paths.projectContext)) && input.include_project_context) {
2421
+ if ((isTemplateContext || !existsSync22(ctx.paths.projectContext)) && input.include_project_context) {
2283
2422
  const haiveConfig = await loadConfig3(ctx.paths);
2284
2423
  if (haiveConfig.autoContext) {
2285
2424
  const codeMap = await loadCodeMap(ctx.paths);
@@ -2453,8 +2592,8 @@ ${m.content}`).join("\n\n---\n\n"),
2453
2592
  actionRequired.push(extractActionItem(m.id, loaded.memory.body));
2454
2593
  }
2455
2594
  }
2456
- if (existsSync21(ctx.paths.memoriesDir)) {
2457
- const allMems = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
2595
+ if (existsSync22(ctx.paths.memoriesDir)) {
2596
+ const allMems = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
2458
2597
  for (const { memory } of allMems) {
2459
2598
  const fm = memory.frontmatter;
2460
2599
  if (!fm.requires_human_approval) continue;
@@ -2464,9 +2603,9 @@ ${m.content}`).join("\n\n---\n\n"),
2464
2603
  }
2465
2604
  }
2466
2605
  const pendingDistillFile = pendingDistillPath(ctx);
2467
- if (existsSync21(pendingDistillFile)) {
2606
+ if (existsSync22(pendingDistillFile)) {
2468
2607
  try {
2469
- const raw = await readFile6(pendingDistillFile, "utf8");
2608
+ const raw = await readFile7(pendingDistillFile, "utf8");
2470
2609
  const pd = JSON.parse(raw);
2471
2610
  const ageMs = Date.now() - new Date(pd.session_end).getTime();
2472
2611
  const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1e3;
@@ -2493,7 +2632,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
2493
2632
  }
2494
2633
  }
2495
2634
  const memoriesEmpty = outputMemories.length === 0;
2496
- const hasMemoriesDir = existsSync21(ctx.paths.memoriesDir);
2635
+ const hasMemoriesDir = existsSync22(ctx.paths.memoriesDir);
2497
2636
  const isColdStart = isTemplateContext && memoriesEmpty && !lastSession && !autoContextGenerated;
2498
2637
  const hasUnguessableSignal = outputMemories.some(
2499
2638
  (m) => (m.priority === "must_read" || m.priority === "useful") && specificityScore(m.body) >= GUESSABLE_THRESHOLD
@@ -2508,10 +2647,10 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
2508
2647
  try {
2509
2648
  let pcRaw = "";
2510
2649
  try {
2511
- pcRaw = await readFile6(ctx.paths.projectContext, "utf8");
2650
+ pcRaw = await readFile7(ctx.paths.projectContext, "utf8");
2512
2651
  } catch {
2513
2652
  }
2514
- const allForBootstrap = existsSync21(ctx.paths.memoriesDir) ? await loadMemoriesFromDir16(ctx.paths.memoriesDir) : [];
2653
+ const allForBootstrap = existsSync22(ctx.paths.memoriesDir) ? await loadMemoriesFromDir17(ctx.paths.memoriesDir) : [];
2515
2654
  const cmForBootstrap = await loadCodeMap(ctx.paths);
2516
2655
  let existingModules = [];
2517
2656
  try {
@@ -2575,7 +2714,7 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
2575
2714
  "No team-specific policy matched these files/task \u2014 nothing here a capable model can't infer. The auto-generated project context was trimmed to keep this briefing near-zero-cost; proceed with normal Read/Grep."
2576
2715
  );
2577
2716
  }
2578
- if (outputMemories.length > 0 && existsSync21(ctx.paths.haiveDir)) {
2717
+ if (outputMemories.length > 0 && existsSync22(ctx.paths.haiveDir)) {
2579
2718
  const proof = briefingProofLine(await loadPreventionEvents(ctx.paths));
2580
2719
  if (proof) hints.push(proof);
2581
2720
  }
@@ -2589,7 +2728,7 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
2589
2728
  adaptiveTrim
2590
2729
  });
2591
2730
  const breadcrumbTokens = breadcrumbs ? estimateTokens([...breadcrumbs.start_here, ...breadcrumbs.drill_down, breadcrumbs.note ?? ""].join("\n")) : 0;
2592
- if (existsSync21(ctx.paths.haiveDir)) {
2731
+ if (existsSync22(ctx.paths.haiveDir)) {
2593
2732
  await writeBriefingMarker(ctx.paths, {
2594
2733
  sessionId: process.env.HAIVE_SESSION_ID,
2595
2734
  ...input.task ? { task: input.task } : {},
@@ -2645,10 +2784,10 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
2645
2784
  };
2646
2785
  }
2647
2786
  async function detectRunCommands(root) {
2648
- const pkgPath = path11.join(root, "package.json");
2649
- if (!existsSync21(pkgPath)) return null;
2787
+ const pkgPath = path12.join(root, "package.json");
2788
+ if (!existsSync22(pkgPath)) return null;
2650
2789
  try {
2651
- const pkg = JSON.parse(await readFile6(pkgPath, "utf8"));
2790
+ const pkg = JSON.parse(await readFile7(pkgPath, "utf8"));
2652
2791
  const scripts = pkg.scripts ?? {};
2653
2792
  const order = ["test", "build", "lint", "typecheck", "type-check", "dev", "start"];
2654
2793
  const lines = order.filter((name) => typeof scripts[name] === "string" && scripts[name].trim() !== "").map((name) => `- \`${name}\`: \`${scripts[name]}\``);
@@ -2713,24 +2852,24 @@ function oneLine(value) {
2713
2852
  return value.replace(/\s+/g, " ").replace(/"/g, '\\"').trim().slice(0, 120);
2714
2853
  }
2715
2854
  function serverVersion() {
2716
- return true ? "0.37.0" : "dev";
2855
+ return true ? "0.39.0" : "dev";
2717
2856
  }
2718
2857
 
2719
2858
  // src/tools/code-map.ts
2720
2859
  import { estimateTokens as estimateTokens2, loadCodeMap as loadCodeMap2, queryCodeMap as queryCodeMap2 } from "@hivelore/core";
2721
- import { z as z20 } from "zod";
2860
+ import { z as z21 } from "zod";
2722
2861
  var CodeMapInputSchema = {
2723
- file: z20.string().optional().describe("Filter to files whose path contains this substring"),
2724
- symbol: z20.string().optional().describe("Filter to files exporting a symbol whose name contains this substring"),
2725
- paths: z20.array(z20.string()).default([]).describe(
2862
+ file: z21.string().optional().describe("Filter to files whose path contains this substring"),
2863
+ symbol: z21.string().optional().describe("Filter to files exporting a symbol whose name contains this substring"),
2864
+ paths: z21.array(z21.string()).default([]).describe(
2726
2865
  "Filter to files under any of these path prefixes (e.g. ['packages/mcp/src/tools/', 'src/auth/']). OR-joined with `file` substring; useful to get a focused view of one module."
2727
2866
  ),
2728
- max_files: z20.number().int().positive().default(40).describe("Cap on returned files (hard limit, applied after token budget)"),
2729
- max_tokens: z20.number().int().positive().optional().describe(
2867
+ max_files: z21.number().int().positive().default(40).describe("Cap on returned files (hard limit, applied after token budget)"),
2868
+ max_tokens: z21.number().int().positive().optional().describe(
2730
2869
  "Approximate token budget for the response. When the matching set exceeds it, files are ranked by export density (exports per LOC) and the highest-signal ones are kept first. Omit to disable budgeting (legacy behavior)."
2731
2870
  )
2732
2871
  };
2733
- var CodeMapInputZod = z20.object(CodeMapInputSchema);
2872
+ var CodeMapInputZod = z21.object(CodeMapInputSchema);
2734
2873
  async function codeMapTool(input, ctx) {
2735
2874
  const map = await loadCodeMap2(ctx.paths);
2736
2875
  if (!map) {
@@ -2799,18 +2938,18 @@ function estimateFileEntryTokens(f) {
2799
2938
  }
2800
2939
 
2801
2940
  // src/tools/mem-diff.ts
2802
- import { existsSync as existsSync22 } from "fs";
2803
- import { loadMemoriesFromDir as loadMemoriesFromDir17 } from "@hivelore/core";
2804
- import { z as z21 } from "zod";
2941
+ import { existsSync as existsSync23 } from "fs";
2942
+ import { loadMemoriesFromDir as loadMemoriesFromDir18 } from "@hivelore/core";
2943
+ import { z as z22 } from "zod";
2805
2944
  var MemDiffInputSchema = {
2806
- id_a: z21.string().min(1).describe("First memory id"),
2807
- id_b: z21.string().min(1).describe("Second memory id")
2945
+ id_a: z22.string().min(1).describe("First memory id"),
2946
+ id_b: z22.string().min(1).describe("Second memory id")
2808
2947
  };
2809
2948
  async function memDiff(input, ctx) {
2810
- if (!existsSync22(ctx.paths.memoriesDir)) {
2949
+ if (!existsSync23(ctx.paths.memoriesDir)) {
2811
2950
  throw new Error(`No .ai/memories at ${ctx.paths.root}.`);
2812
2951
  }
2813
- const all = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
2952
+ const all = await loadMemoriesFromDir18(ctx.paths.memoriesDir);
2814
2953
  const foundA = all.find((m) => m.memory.frontmatter.id === input.id_a);
2815
2954
  const foundB = all.find((m) => m.memory.frontmatter.id === input.id_b);
2816
2955
  if (!foundA) throw new Error(`No memory with id "${input.id_a}".`);
@@ -2844,19 +2983,19 @@ async function memDiff(input, ctx) {
2844
2983
  }
2845
2984
 
2846
2985
  // src/tools/get-recap.ts
2847
- import { existsSync as existsSync23 } from "fs";
2848
- import { loadMemoriesFromDir as loadMemoriesFromDir18 } from "@hivelore/core";
2849
- import { z as z22 } from "zod";
2986
+ import { existsSync as existsSync24 } from "fs";
2987
+ import { loadMemoriesFromDir as loadMemoriesFromDir19 } from "@hivelore/core";
2988
+ import { z as z23 } from "zod";
2850
2989
  var GetRecapInputSchema = {
2851
- scope: z22.enum(["personal", "team", "any"]).default("any").describe(
2990
+ scope: z23.enum(["personal", "team", "any"]).default("any").describe(
2852
2991
  "Limit to a specific scope's recap. Default 'any' returns the most recent recap across both personal and team scopes."
2853
2992
  )
2854
2993
  };
2855
2994
  async function getRecap(input, ctx) {
2856
- if (!existsSync23(ctx.paths.memoriesDir)) {
2995
+ if (!existsSync24(ctx.paths.memoriesDir)) {
2857
2996
  return { recap: null, notice: "No .ai/memories directory \u2014 haive not initialized here." };
2858
2997
  }
2859
- const all = await loadMemoriesFromDir18(ctx.paths.memoriesDir);
2998
+ const all = await loadMemoriesFromDir19(ctx.paths.memoriesDir);
2860
2999
  const recaps = all.filter(({ memory }) => memory.frontmatter.type === "session_recap").filter(({ memory }) => input.scope === "any" || memory.frontmatter.scope === input.scope).sort(
2861
3000
  (a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
2862
3001
  );
@@ -2880,13 +3019,13 @@ async function getRecap(input, ctx) {
2880
3019
  }
2881
3020
 
2882
3021
  // src/tools/mem-relevant-to.ts
2883
- import { z as z23 } from "zod";
3022
+ import { z as z24 } from "zod";
2884
3023
  var MemRelevantToInputSchema = {
2885
- task: z23.string().min(1).describe("What you are about to do, in 1\u20132 sentences. Used to rank relevant memories."),
2886
- files: z23.array(z23.string()).default([]).describe("Optional: files you are about to edit \u2014 surfaces anchored memories."),
2887
- limit: z23.number().int().positive().max(30).default(8).describe("Cap on returned memories."),
2888
- min_semantic_score: z23.number().min(0).max(1).default(0.25).describe("Drop weakly-related semantic hits below this cosine threshold."),
2889
- format: z23.enum(["full", "compact", "actions"]).default("full").describe("'compact' = id + 1-line summary; 'full' = complete bodies; 'actions' = bullet-first excerpts.")
3024
+ task: z24.string().min(1).describe("What you are about to do, in 1\u20132 sentences. Used to rank relevant memories."),
3025
+ files: z24.array(z24.string()).default([]).describe("Optional: files you are about to edit \u2014 surfaces anchored memories."),
3026
+ limit: z24.number().int().positive().max(30).default(8).describe("Cap on returned memories."),
3027
+ min_semantic_score: z24.number().min(0).max(1).default(0.25).describe("Drop weakly-related semantic hits below this cosine threshold."),
3028
+ format: z24.enum(["full", "compact", "actions"]).default("full").describe("'compact' = id + 1-line summary; 'full' = complete bodies; 'actions' = bullet-first excerpts.")
2890
3029
  };
2891
3030
  async function memRelevantTo(input, ctx) {
2892
3031
  const briefingInput = {
@@ -2916,14 +3055,14 @@ async function memRelevantTo(input, ctx) {
2916
3055
  }
2917
3056
 
2918
3057
  // src/tools/code-search.ts
2919
- import { z as z24 } from "zod";
3058
+ import { z as z25 } from "zod";
2920
3059
  import { loadCodeMap as loadCodeMap3 } from "@hivelore/core";
2921
3060
  var CodeSearchInputSchema = {
2922
- query: z24.string().min(1).describe(
3061
+ query: z25.string().min(1).describe(
2923
3062
  "Natural-language description of what you are looking for in the codebase (e.g. 'function that hashes passwords', 'JWT signing logic', 'route registration')."
2924
3063
  ),
2925
- k: z24.number().int().positive().max(50).default(5).describe("Number of top hits to return."),
2926
- min_score: z24.number().min(0).max(1).default(0.2).describe(
3064
+ k: z25.number().int().positive().max(50).default(5).describe("Number of top hits to return."),
3065
+ min_score: z25.number().min(0).max(1).default(0.2).describe(
2927
3066
  "Minimum cosine similarity. Hits below this threshold are dropped to avoid noise. Try 0.3+ for stricter matching."
2928
3067
  )
2929
3068
  };
@@ -2966,7 +3105,7 @@ async function codeSearch(input, ctx) {
2966
3105
  }
2967
3106
 
2968
3107
  // src/tools/anti-patterns-check.ts
2969
- import { existsSync as existsSync24 } from "fs";
3108
+ import { existsSync as existsSync25 } from "fs";
2970
3109
  import {
2971
3110
  addedLinesFromDiff,
2972
3111
  BRIDGE_TARGET_PATH,
@@ -2976,7 +3115,7 @@ import {
2976
3115
  diffHasDistinctiveOverlap,
2977
3116
  getUsage as getUsage7,
2978
3117
  isRetiredMemory as isRetiredMemory2,
2979
- loadMemoriesFromDir as loadMemoriesFromDir19,
3118
+ loadMemoriesFromDir as loadMemoriesFromDir20,
2980
3119
  loadUsageIndex as loadUsageIndex9,
2981
3120
  literalMatchesAnyToken as literalMatchesAnyToken3,
2982
3121
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths3,
@@ -2985,19 +3124,19 @@ import {
2985
3124
  sensorTargetsFromDiff,
2986
3125
  tokenizeQuery as tokenizeQuery3
2987
3126
  } from "@hivelore/core";
2988
- import { z as z25 } from "zod";
3127
+ import { z as z26 } from "zod";
2989
3128
  var AntiPatternsCheckInputSchema = {
2990
- diff: z25.string().optional().describe(
3129
+ diff: z26.string().optional().describe(
2991
3130
  "Raw unified diff text (or any code/text snippet) to scan for previously documented anti-patterns. Tokens from the diff are used to match memory bodies and the embeddings index."
2992
3131
  ),
2993
- paths: z25.array(z25.string()).default([]).describe(
3132
+ paths: z26.array(z26.string()).default([]).describe(
2994
3133
  "File paths affected by the change. Memories anchored to any of these paths are surfaced regardless of the diff content."
2995
3134
  ),
2996
- limit: z25.number().int().positive().max(20).default(8).describe("Cap on returned warnings."),
2997
- semantic: z25.boolean().default(true).describe(
3135
+ limit: z26.number().int().positive().max(20).default(8).describe("Cap on returned warnings."),
3136
+ semantic: z26.boolean().default(true).describe(
2998
3137
  "When true, also use semantic search (requires @hivelore/embeddings + memory index) to find related anti-patterns."
2999
3138
  ),
3000
- min_semantic_score: z25.number().min(0).max(1).default(0.45).describe(
3139
+ min_semantic_score: z26.number().min(0).max(1).default(0.45).describe(
3001
3140
  "Minimum cosine score for semantic-only anti-pattern hits. Anchor/literal matches still surface. Default 0.45 keeps broad, weakly-related memories out of review noise."
3002
3141
  )
3003
3142
  };
@@ -3081,10 +3220,10 @@ async function antiPatternsCheck(input, ctx) {
3081
3220
  notice: "Nothing to check \u2014 provide either `diff` text or `paths`."
3082
3221
  };
3083
3222
  }
3084
- if (!existsSync24(ctx.paths.memoriesDir)) {
3223
+ if (!existsSync25(ctx.paths.memoriesDir)) {
3085
3224
  return { scanned: 0, warnings: [], notice: "No .ai/memories directory \u2014 nothing to check against." };
3086
3225
  }
3087
- const all = await loadMemoriesFromDir19(ctx.paths.memoriesDir);
3226
+ const all = await loadMemoriesFromDir20(ctx.paths.memoriesDir);
3088
3227
  const minSemanticScore = input.min_semantic_score ?? 0.45;
3089
3228
  const negative = all.filter(({ memory }) => {
3090
3229
  const t = memory.frontmatter.type;
@@ -3200,19 +3339,19 @@ async function antiPatternsCheck(input, ctx) {
3200
3339
  }
3201
3340
 
3202
3341
  // src/tools/mem-distill.ts
3203
- import { existsSync as existsSync25 } from "fs";
3342
+ import { existsSync as existsSync26 } from "fs";
3204
3343
  import {
3205
- loadMemoriesFromDir as loadMemoriesFromDir20,
3344
+ loadMemoriesFromDir as loadMemoriesFromDir21,
3206
3345
  tokenizeQuery as tokenizeQuery4
3207
3346
  } from "@hivelore/core";
3208
- import { z as z26 } from "zod";
3347
+ import { z as z27 } from "zod";
3209
3348
  var MemDistillInputSchema = {
3210
- since_days: z26.number().int().positive().default(30).describe("Only consider memories created in the last N days."),
3211
- min_cluster: z26.number().int().min(2).default(3).describe("Minimum cluster size to surface."),
3212
- type_filter: z26.enum(["gotcha", "attempt", "all"]).default("gotcha").describe(
3349
+ since_days: z27.number().int().positive().default(30).describe("Only consider memories created in the last N days."),
3350
+ min_cluster: z27.number().int().min(2).default(3).describe("Minimum cluster size to surface."),
3351
+ type_filter: z27.enum(["gotcha", "attempt", "all"]).default("gotcha").describe(
3213
3352
  "Memory type to scan. 'gotcha' targets observe-style discoveries that recur, 'attempt' surfaces failed approaches that repeat, 'all' considers both."
3214
3353
  ),
3215
- scope: z26.enum(["personal", "team", "module", "any"]).default("any").describe("Restrict to a specific scope.")
3354
+ scope: z27.enum(["personal", "team", "module", "any"]).default("any").describe("Restrict to a specific scope.")
3216
3355
  };
3217
3356
  var MS_PER_DAY = 24 * 60 * 60 * 1e3;
3218
3357
  var STOP_WORDS = /* @__PURE__ */ new Set([
@@ -3252,11 +3391,11 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
3252
3391
  "error"
3253
3392
  ]);
3254
3393
  async function memDistill(input, ctx) {
3255
- if (!existsSync25(ctx.paths.memoriesDir)) {
3394
+ if (!existsSync26(ctx.paths.memoriesDir)) {
3256
3395
  return { scanned: 0, singletons: 0, clusters: [], notice: "No .ai/memories directory." };
3257
3396
  }
3258
3397
  const cutoff = Date.now() - input.since_days * MS_PER_DAY;
3259
- const all = await loadMemoriesFromDir20(ctx.paths.memoriesDir);
3398
+ const all = await loadMemoriesFromDir21(ctx.paths.memoriesDir);
3260
3399
  const candidates = all.filter(({ memory }) => {
3261
3400
  const fm = memory.frontmatter;
3262
3401
  if (fm.status === "rejected" || fm.status === "deprecated" || fm.status === "stale") return false;
@@ -3361,17 +3500,17 @@ function firstHeading(body) {
3361
3500
 
3362
3501
  // src/tools/precommit-check.ts
3363
3502
  import { pathsOverlap as pathsOverlap2 } from "@hivelore/core";
3364
- import { z as z27 } from "zod";
3503
+ import { z as z28 } from "zod";
3365
3504
  var PreCommitCheckInputSchema = {
3366
- diff: z27.string().optional().describe(
3505
+ diff: z28.string().optional().describe(
3367
3506
  "Raw unified diff text to scan. If omitted, only `paths` is used. When called from a pre-commit hook, pipe the output of `git diff --cached`."
3368
3507
  ),
3369
- paths: z27.array(z27.string()).default([]).describe("Project-relative paths affected by the change. At least one of `diff` or `paths` should be provided."),
3370
- block_on: z27.enum(["any", "high-confidence", "never"]).default("high-confidence").describe(
3508
+ paths: z28.array(z28.string()).default([]).describe("Project-relative paths affected by the change. At least one of `diff` or `paths` should be provided."),
3509
+ block_on: z28.enum(["any", "high-confidence", "never"]).default("high-confidence").describe(
3371
3510
  "When to set should_block=true: 'any' = any warning blocks; 'high-confidence' = only warnings from authoritative/trusted memories block; 'never' = report only, never block."
3372
3511
  ),
3373
- semantic: z27.boolean().default(true).describe("Enable semantic search in anti_patterns_check (requires embeddings index)."),
3374
- anchored_blocks: z27.boolean().default(false).describe(
3512
+ semantic: z28.boolean().default(true).describe("Enable semantic search in anti_patterns_check (requires embeddings index)."),
3513
+ anchored_blocks: z28.boolean().default(false).describe(
3375
3514
  "When true, ALSO block a high-confidence anti-pattern (attempt/gotcha) that is anchored to a touched file AND corroborated by the diff (literal token overlap, or semantic >= 0.45) \u2014 not just very strong semantic matches. Powers the 'anchored' enforcement gate. Config/docs-only commits are still downgraded. Default false preserves the soft, semantic-only blocking behavior."
3376
3515
  )
3377
3516
  };
@@ -3677,14 +3816,14 @@ function repairTargetPathForWarning(warning, paths) {
3677
3816
  }
3678
3817
 
3679
3818
  // src/tools/mem-conflict-candidates.ts
3680
- import { existsSync as existsSync26 } from "fs";
3819
+ import { existsSync as existsSync27 } from "fs";
3681
3820
  import {
3682
3821
  findLexicalConflictPairs,
3683
3822
  findTopicStatusConflictPairs,
3684
- loadMemoriesFromDir as loadMemoriesFromDir21,
3823
+ loadMemoriesFromDir as loadMemoriesFromDir22,
3685
3824
  planConflictResolution
3686
3825
  } from "@hivelore/core";
3687
- import { z as z28 } from "zod";
3826
+ import { z as z29 } from "zod";
3688
3827
  function suggestResolution(byId, idA, idB) {
3689
3828
  const a = byId.get(idA);
3690
3829
  const b = byId.get(idB);
@@ -3698,17 +3837,17 @@ function suggestResolution(byId, idA, idB) {
3698
3837
  };
3699
3838
  }
3700
3839
  var MemConflictCandidatesInputSchema = {
3701
- since_days: z28.number().int().positive().max(3650).default(365).describe("Only memories created since N days ago"),
3702
- types: z28.array(z28.enum(["decision", "architecture", "convention", "gotcha"])).default(["decision", "architecture"]).describe("Memory types scanned for pairwise lexical overlap"),
3703
- min_jaccard: z28.number().min(0).max(1).default(0.45).describe("Minimum Jaccard token similarity to surface as a candidate pair"),
3704
- max_pairs: z28.number().int().positive().max(100).default(20).describe("Cap pairs returned"),
3705
- max_scan: z28.number().int().positive().max(2e3).default(500).describe("Maximum memories sampled for O(n\xB2) scan \u2014 excess dropped after chronological sort."),
3706
- max_topic_pairs: z28.number().int().positive().max(100).default(20).describe(
3840
+ since_days: z29.number().int().positive().max(3650).default(365).describe("Only memories created since N days ago"),
3841
+ types: z29.array(z29.enum(["decision", "architecture", "convention", "gotcha"])).default(["decision", "architecture"]).describe("Memory types scanned for pairwise lexical overlap"),
3842
+ min_jaccard: z29.number().min(0).max(1).default(0.45).describe("Minimum Jaccard token similarity to surface as a candidate pair"),
3843
+ max_pairs: z29.number().int().positive().max(100).default(20).describe("Cap pairs returned"),
3844
+ max_scan: z29.number().int().positive().max(2e3).default(500).describe("Maximum memories sampled for O(n\xB2) scan \u2014 excess dropped after chronological sort."),
3845
+ max_topic_pairs: z29.number().int().positive().max(100).default(20).describe(
3707
3846
  "Cap for extra signal: memories sharing the same topic with validated vs rejected status."
3708
3847
  )
3709
3848
  };
3710
3849
  async function memConflictCandidates(input, ctx) {
3711
- if (!existsSync26(ctx.paths.memoriesDir)) {
3850
+ if (!existsSync27(ctx.paths.memoriesDir)) {
3712
3851
  return {
3713
3852
  pairs: [],
3714
3853
  topic_status_pairs: [],
@@ -3717,7 +3856,7 @@ async function memConflictCandidates(input, ctx) {
3717
3856
  notice: "No .ai/memories directory."
3718
3857
  };
3719
3858
  }
3720
- const all = await loadMemoriesFromDir21(ctx.paths.memoriesDir);
3859
+ const all = await loadMemoriesFromDir22(ctx.paths.memoriesDir);
3721
3860
  const byId = new Map(all.map((m) => [m.memory.frontmatter.id, m]));
3722
3861
  const { pairs, scanned, truncated } = findLexicalConflictPairs(all, {
3723
3862
  sinceDays: input.since_days,
@@ -3747,9 +3886,9 @@ async function memConflictCandidates(input, ctx) {
3747
3886
 
3748
3887
  // src/tools/mem-resolve-project.ts
3749
3888
  import { resolveProjectInfo } from "@hivelore/core";
3750
- import { z as z29 } from "zod";
3889
+ import { z as z30 } from "zod";
3751
3890
  var MemResolveProjectInputSchema = {
3752
- cwd: z29.string().optional().describe("Directory used for root discovery when HAIVE_PROJECT_ROOT is unset.")
3891
+ cwd: z30.string().optional().describe("Directory used for root discovery when HAIVE_PROJECT_ROOT is unset.")
3753
3892
  };
3754
3893
  async function memResolveProject(input, _ctx) {
3755
3894
  void _ctx;
@@ -3763,10 +3902,10 @@ async function memResolveProject(input, _ctx) {
3763
3902
 
3764
3903
  // src/tools/mem-suggest-topic.ts
3765
3904
  import { MemoryTypeSchema, suggestTopicKey } from "@hivelore/core";
3766
- import { z as z30 } from "zod";
3905
+ import { z as z31 } from "zod";
3767
3906
  var MemSuggestTopicInputSchema = {
3768
3907
  type: MemoryTypeSchema.describe("Memory kind \u2014 drives the suggested topic family."),
3769
- title: z30.string().min(1).describe("Short title or phrase (headers, headings) \u2014 turned into slug")
3908
+ title: z31.string().min(1).describe("Short title or phrase (headers, headings) \u2014 turned into slug")
3770
3909
  };
3771
3910
  async function memSuggestTopic(input, _ctx) {
3772
3911
  void _ctx;
@@ -3775,19 +3914,19 @@ async function memSuggestTopic(input, _ctx) {
3775
3914
  }
3776
3915
 
3777
3916
  // src/tools/mem-timeline.ts
3778
- import { existsSync as existsSync27 } from "fs";
3779
- import { collectTimelineEntries, loadMemoriesFromDir as loadMemoriesFromDir22 } from "@hivelore/core";
3780
- import { z as z31 } from "zod";
3917
+ import { existsSync as existsSync28 } from "fs";
3918
+ import { collectTimelineEntries, loadMemoriesFromDir as loadMemoriesFromDir23 } from "@hivelore/core";
3919
+ import { z as z32 } from "zod";
3781
3920
  var MemTimelineInputSchema = {
3782
- memory_id: z31.string().optional().describe("Seed id \u2014 expands via related_ids, topic, anchors"),
3783
- topic: z31.string().optional().describe("Frontmatter.topic value \u2014 chronological list when memory_id omitted"),
3784
- limit: z31.number().int().positive().max(100).default(30).describe("Max timeline entries returned")
3921
+ memory_id: z32.string().optional().describe("Seed id \u2014 expands via related_ids, topic, anchors"),
3922
+ topic: z32.string().optional().describe("Frontmatter.topic value \u2014 chronological list when memory_id omitted"),
3923
+ limit: z32.number().int().positive().max(100).default(30).describe("Max timeline entries returned")
3785
3924
  };
3786
3925
  async function memTimeline(input, ctx) {
3787
- if (!existsSync27(ctx.paths.memoriesDir)) {
3926
+ if (!existsSync28(ctx.paths.memoriesDir)) {
3788
3927
  return { entries: [], total: 0, notice: "No .ai/memories directory." };
3789
3928
  }
3790
- const all = await loadMemoriesFromDir22(ctx.paths.memoriesDir);
3929
+ const all = await loadMemoriesFromDir23(ctx.paths.memoriesDir);
3791
3930
  const { entries, notice } = collectTimelineEntries(all, {
3792
3931
  memoryId: input.memory_id,
3793
3932
  topic: input.topic,
@@ -3797,12 +3936,12 @@ async function memTimeline(input, ctx) {
3797
3936
  }
3798
3937
 
3799
3938
  // src/prompts/bootstrap-project.ts
3800
- import { z as z32 } from "zod";
3939
+ import { z as z33 } from "zod";
3801
3940
  var BootstrapProjectArgsSchema = {
3802
- module: z32.string().optional().describe(
3941
+ module: z33.string().optional().describe(
3803
3942
  "Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
3804
3943
  ),
3805
- focus: z32.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
3944
+ focus: z33.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
3806
3945
  };
3807
3946
  var ROOT_TEMPLATE = `# Project context
3808
3947
 
@@ -3884,25 +4023,25 @@ ${template}\`\`\`
3884
4023
  }
3885
4024
 
3886
4025
  // src/prompts/bootstrap-repo.ts
3887
- import { readFile as readFile7, readdir as readdir5 } from "fs/promises";
3888
- import { existsSync as existsSync28 } from "fs";
4026
+ import { readFile as readFile8, readdir as readdir5 } from "fs/promises";
4027
+ import { existsSync as existsSync29 } from "fs";
3889
4028
  import {
3890
4029
  assessBootstrapState as assessBootstrapState2,
3891
4030
  loadCodeMap as loadCodeMap4,
3892
- loadMemoriesFromDir as loadMemoriesFromDir23,
4031
+ loadMemoriesFromDir as loadMemoriesFromDir24,
3893
4032
  renderBootstrapChecklist as renderBootstrapChecklist2
3894
4033
  } from "@hivelore/core";
3895
- import { z as z33 } from "zod";
4034
+ import { z as z34 } from "zod";
3896
4035
  var BootstrapRepoArgsSchema = {
3897
- focus: z33.string().optional().describe("Optional area to emphasize first (e.g. 'payments', 'auth').")
4036
+ focus: z34.string().optional().describe("Optional area to emphasize first (e.g. 'payments', 'auth').")
3898
4037
  };
3899
4038
  async function currentAssessment(ctx) {
3900
4039
  let projectContextRaw = "";
3901
4040
  try {
3902
- projectContextRaw = await readFile7(ctx.paths.projectContext, "utf8");
4041
+ projectContextRaw = await readFile8(ctx.paths.projectContext, "utf8");
3903
4042
  } catch {
3904
4043
  }
3905
- const memories = existsSync28(ctx.paths.memoriesDir) ? await loadMemoriesFromDir23(ctx.paths.memoriesDir) : [];
4044
+ const memories = existsSync29(ctx.paths.memoriesDir) ? await loadMemoriesFromDir24(ctx.paths.memoriesDir) : [];
3906
4045
  const codeMap = await loadCodeMap4(ctx.paths);
3907
4046
  let existingModules = [];
3908
4047
  try {
@@ -3970,10 +4109,10 @@ Main code areas detected: ${areas}
3970
4109
  }
3971
4110
 
3972
4111
  // src/prompts/post-task.ts
3973
- import { z as z34 } from "zod";
4112
+ import { z as z35 } from "zod";
3974
4113
  var PostTaskArgsSchema = {
3975
- task_summary: z34.string().optional().describe("One sentence describing what you just did"),
3976
- files_touched: z34.array(z34.string()).optional().describe("Files you created or modified during the task")
4114
+ task_summary: z35.string().optional().describe("One sentence describing what you just did"),
4115
+ files_touched: z35.array(z35.string()).optional().describe("Files you created or modified during the task")
3977
4116
  };
3978
4117
  function postTaskPrompt(args, ctx) {
3979
4118
  const taskLine = args.task_summary ? `
@@ -4070,12 +4209,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
4070
4209
  }
4071
4210
 
4072
4211
  // src/prompts/import-docs.ts
4073
- import { z as z35 } from "zod";
4212
+ import { z as z36 } from "zod";
4074
4213
  var ImportDocsArgsSchema = {
4075
- content: z35.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
4076
- source: z35.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
4077
- scope: z35.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
4078
- dry_run: z35.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
4214
+ content: z36.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
4215
+ source: z36.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
4216
+ scope: z36.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
4217
+ dry_run: z36.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
4079
4218
  };
4080
4219
  function importDocsPrompt(args, ctx) {
4081
4220
  const sourceLine = args.source ? `
@@ -4141,7 +4280,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
4141
4280
  // src/server.ts
4142
4281
  import { hasRecentBriefingMarker, loadConfigSync } from "@hivelore/core";
4143
4282
  var SERVER_NAME = "hivelore";
4144
- var SERVER_VERSION = "0.37.0";
4283
+ var SERVER_VERSION = "0.39.0";
4145
4284
  function jsonResult(data) {
4146
4285
  return {
4147
4286
  content: [
@@ -4164,7 +4303,8 @@ var ENFORCEMENT_PROFILE_TOOLS = [
4164
4303
  "code_search",
4165
4304
  "pre_commit_check",
4166
4305
  "mem_session_end",
4167
- "propose_sensor"
4306
+ "propose_sensor",
4307
+ "scaffold_test"
4168
4308
  ];
4169
4309
  var MAINTENANCE_PROFILE_TOOLS = [
4170
4310
  ...ENFORCEMENT_PROFILE_TOOLS,
@@ -4370,6 +4510,35 @@ function createHaiveServer(options = {}) {
4370
4510
  return jsonResult(await proposeSensor(input, context));
4371
4511
  }
4372
4512
  );
4513
+ registerTool(
4514
+ "scaffold_test",
4515
+ [
4516
+ "Generate a PENDING post-incident test from a lesson (attempt/gotcha) \u2014 the on-ramp to a command",
4517
+ "sensor. A command sensor routes YOUR test as its oracle, but someone has to write it; this writes",
4518
+ "the skeleton so you only fill in the assertion.",
4519
+ "",
4520
+ "USE THIS right after mem_tried when the mistake is behavioural (a regex can't express it): it",
4521
+ "writes a stub carrying the incident's provenance and returns the exact `sensors propose --kind",
4522
+ "test` command to arm it.",
4523
+ "",
4524
+ "It DOES NOT arm a sensor \u2014 propose_sensor stays the sole validated writer, and the stub is left",
4525
+ "PENDING (todo/skip) so the suite stays green until you write the assertion. Monorepo-aware: the",
4526
+ "framework and location come from the package that owns the lesson's anchor paths.",
4527
+ "",
4528
+ "PARAMETERS:",
4529
+ " memory_id \u2014 the attempt/gotcha to scaffold from",
4530
+ " framework \u2014 vitest | jest | pytest | gotest (auto-detected when omitted)",
4531
+ " out_path \u2014 override the test file path (repo-relative)",
4532
+ " write \u2014 write the file (default true); false returns the content for preview",
4533
+ "",
4534
+ "RETURNS: { ok, path, run_command, propose_command, content, written, already_exists, notice }"
4535
+ ].join("\n"),
4536
+ ScaffoldTestInputSchema,
4537
+ async (input) => {
4538
+ tracker.record("scaffold_test", input.memory_id);
4539
+ return jsonResult(await scaffoldTest(input, context));
4540
+ }
4541
+ );
4373
4542
  registerTool(
4374
4543
  "ingest_findings",
4375
4544
  [