@hivelore/mcp 0.36.0 → 0.38.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
@@ -1450,43 +1450,142 @@ async function memTried(input, ctx) {
1450
1450
  };
1451
1451
  }
1452
1452
 
1453
- // src/tools/ingest-findings.ts
1454
- import { existsSync as existsSync17 } from "fs";
1453
+ // src/tools/scaffold-test.ts
1454
+ import { existsSync as existsSync17, statSync } from "fs";
1455
1455
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile10 } from "fs/promises";
1456
1456
  import path7 from "path";
1457
+ import { z as z17 } from "zod";
1458
+ import {
1459
+ loadMemoriesFromDir as loadMemoriesFromDir14,
1460
+ normalizeFramework,
1461
+ parseLessonFields,
1462
+ pickTestFramework,
1463
+ scaffoldPostIncidentTest
1464
+ } from "@hivelore/core";
1465
+ var PY_SIGNALS = ["pyproject.toml", "setup.py", "pytest.ini", "requirements.txt", "tox.ini"];
1466
+ async function detectTestFrameworkForPaths(root, anchorPaths) {
1467
+ const starts = anchorPaths.length > 0 ? anchorPaths : ["."];
1468
+ for (const rel of starts) {
1469
+ let dir = path7.resolve(root, rel);
1470
+ try {
1471
+ if (!statSync(dir).isDirectory()) dir = path7.dirname(dir);
1472
+ } catch {
1473
+ if (path7.extname(dir)) dir = path7.dirname(dir);
1474
+ }
1475
+ while (dir.startsWith(root)) {
1476
+ const pkgJson = path7.join(dir, "package.json");
1477
+ const hasPkg = existsSync17(pkgJson);
1478
+ const goMod = existsSync17(path7.join(dir, "go.mod"));
1479
+ const pySignal = PY_SIGNALS.some((s) => existsSync17(path7.join(dir, s)));
1480
+ if (hasPkg || goMod || pySignal) {
1481
+ let pkg = null;
1482
+ if (hasPkg) {
1483
+ try {
1484
+ pkg = JSON.parse(await readFile4(pkgJson, "utf8"));
1485
+ } catch {
1486
+ pkg = null;
1487
+ }
1488
+ }
1489
+ const baseDir = path7.relative(root, dir).split(path7.sep).join("/");
1490
+ return { framework: pickTestFramework(pkg, { goMod, pySignal }), baseDir };
1491
+ }
1492
+ const parent = path7.dirname(dir);
1493
+ if (parent === dir || dir === root) break;
1494
+ dir = parent;
1495
+ }
1496
+ }
1497
+ return { framework: "vitest", baseDir: "" };
1498
+ }
1499
+ var ScaffoldTestInputSchema = {
1500
+ memory_id: z17.string().min(1).describe("Id of the attempt/gotcha lesson to scaffold a post-incident test from."),
1501
+ 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."),
1502
+ out_path: z17.string().optional().describe("Override the generated test file path (repo-relative)."),
1503
+ write: z17.boolean().default(true).describe("Write the file to disk (default). false = return the content for preview without writing.")
1504
+ };
1505
+ async function scaffoldTest(input, ctx) {
1506
+ const loaded = existsSync17(ctx.paths.memoriesDir) ? await loadMemoriesFromDir14(ctx.paths.memoriesDir) : [];
1507
+ const found = loaded.find(({ memory }) => memory.frontmatter.id === input.memory_id);
1508
+ if (!found) {
1509
+ return { ok: false, error: `No memory found with id ${input.memory_id}`, memory_id: input.memory_id };
1510
+ }
1511
+ const anchorPaths = found.memory.frontmatter.anchor.paths ?? [];
1512
+ const detected = await detectTestFrameworkForPaths(ctx.paths.root, anchorPaths);
1513
+ const framework = input.framework ? normalizeFramework(input.framework) ?? detected.framework : detected.framework;
1514
+ const fields = parseLessonFields(found.memory.body);
1515
+ const scaffold = scaffoldPostIncidentTest(
1516
+ {
1517
+ memoryId: input.memory_id,
1518
+ title: fields.title || input.memory_id,
1519
+ whyFailed: fields.whyFailed,
1520
+ instead: fields.instead,
1521
+ incident: found.memory.frontmatter.sensor?.incident,
1522
+ paths: anchorPaths
1523
+ },
1524
+ { framework, outPath: input.out_path, baseDir: detected.baseDir }
1525
+ );
1526
+ const abs = path7.isAbsolute(scaffold.relPath) ? scaffold.relPath : path7.resolve(ctx.paths.root, scaffold.relPath);
1527
+ let written = false;
1528
+ let alreadyExists = false;
1529
+ if (input.write) {
1530
+ if (existsSync17(abs)) {
1531
+ alreadyExists = true;
1532
+ } else {
1533
+ await mkdir4(path7.dirname(abs), { recursive: true });
1534
+ await writeFile10(abs, scaffold.content, "utf8");
1535
+ written = true;
1536
+ }
1537
+ }
1538
+ return {
1539
+ ok: true,
1540
+ memory_id: input.memory_id,
1541
+ framework,
1542
+ path: scaffold.relPath,
1543
+ run_command: scaffold.runCommand,
1544
+ propose_command: scaffold.proposeCommand,
1545
+ content: scaffold.content,
1546
+ written,
1547
+ already_exists: alreadyExists,
1548
+ notice: alreadyExists ? "File already exists \u2014 not overwritten. Delete it 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."
1549
+ };
1550
+ }
1551
+
1552
+ // src/tools/ingest-findings.ts
1553
+ import { existsSync as existsSync18 } from "fs";
1554
+ import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile11 } from "fs/promises";
1555
+ import path8 from "path";
1457
1556
  import {
1458
1557
  draftsFromFindings,
1459
1558
  filterNewDrafts,
1460
- loadMemoriesFromDir as loadMemoriesFromDir14,
1559
+ loadMemoriesFromDir as loadMemoriesFromDir15,
1461
1560
  memoryFilePath as memoryFilePath3,
1462
1561
  parseFindings,
1463
1562
  serializeMemory as serializeMemory9
1464
1563
  } from "@hivelore/core";
1465
- import { z as z17 } from "zod";
1564
+ import { z as z18 } from "zod";
1466
1565
  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")
1566
+ format: z18.enum(["sarif", "sonar"]).describe("Report format: 'sarif' (ESLint/Semgrep/CodeQL) or 'sonar' (SonarQube issues JSON)"),
1567
+ report_path: z18.string().optional().describe("Project-relative path to the findings JSON file. Provide this OR `report`."),
1568
+ report: z18.string().optional().describe("Inline findings JSON content. Provide this OR `report_path`."),
1569
+ type: z18.enum(["gotcha", "convention"]).default("gotcha").describe("Memory type for the created drafts"),
1570
+ scope: z18.enum(["personal", "team", "module"]).default("team").describe("Visibility scope for the created memories"),
1571
+ module: z18.string().optional().describe("Module name (required when scope=module)"),
1572
+ min_severity: z18.enum(["info", "minor", "major", "critical", "blocker"]).optional().describe("Ignore findings below this severity"),
1573
+ include_stylistic: z18.boolean().optional().describe("Also ingest auto-fixable stylistic rules (semi/quotes/prefer-const\u2026); off by default as low-value noise"),
1574
+ limit: z18.number().int().positive().optional().describe("Cap the number of memories created"),
1575
+ author: z18.string().optional().describe("Author handle or email"),
1576
+ dry_run: z18.boolean().default(false).describe("When true, return the drafts that WOULD be created without writing them")
1478
1577
  };
1479
1578
  async function ingestFindings(input, ctx) {
1480
- if (!existsSync17(ctx.paths.haiveDir)) {
1579
+ if (!existsSync18(ctx.paths.haiveDir)) {
1481
1580
  throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'hivelore init' first.`);
1482
1581
  }
1483
1582
  let raw;
1484
1583
  if (input.report && input.report.trim()) {
1485
1584
  raw = input.report;
1486
1585
  } 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");
1586
+ const file = path8.resolve(ctx.paths.root, input.report_path);
1587
+ if (!existsSync18(file)) throw new Error(`Report file not found: ${file}`);
1588
+ raw = await readFile5(file, "utf8");
1490
1589
  } else {
1491
1590
  throw new Error("Provide either `report_path` or `report`.");
1492
1591
  }
@@ -1500,7 +1599,7 @@ async function ingestFindings(input, ctx) {
1500
1599
  ...input.include_stylistic ? { includeStylistic: true } : {},
1501
1600
  ...input.limit ? { limit: input.limit } : {}
1502
1601
  });
1503
- const existing = existsSync17(ctx.paths.memoriesDir) ? await loadMemoriesFromDir14(ctx.paths.memoriesDir) : [];
1602
+ const existing = existsSync18(ctx.paths.memoriesDir) ? await loadMemoriesFromDir15(ctx.paths.memoriesDir) : [];
1504
1603
  const existingTopics = new Set(
1505
1604
  existing.map(({ memory }) => memory.frontmatter.topic).filter((t) => Boolean(t))
1506
1605
  );
@@ -1538,22 +1637,22 @@ async function writeDraft(ctx, draft) {
1538
1637
  draft.frontmatter.id,
1539
1638
  draft.frontmatter.module
1540
1639
  );
1541
- await mkdir4(path7.dirname(file), { recursive: true });
1542
- await writeFile10(file, serializeMemory9({ frontmatter: draft.frontmatter, body: draft.body }), "utf8");
1640
+ await mkdir5(path8.dirname(file), { recursive: true });
1641
+ await writeFile11(file, serializeMemory9({ frontmatter: draft.frontmatter, body: draft.body }), "utf8");
1543
1642
  return file;
1544
1643
  }
1545
1644
 
1546
1645
  // 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";
1646
+ import { writeFile as writeFile13, mkdir as mkdir7 } from "fs/promises";
1647
+ import { existsSync as existsSync20 } from "fs";
1648
+ import path10 from "path";
1550
1649
  import {
1551
1650
  buildFrontmatter as buildFrontmatter3,
1552
- loadMemoriesFromDir as loadMemoriesFromDir15,
1651
+ loadMemoriesFromDir as loadMemoriesFromDir16,
1553
1652
  memoryFilePath as memoryFilePath4,
1554
1653
  serializeMemory as serializeMemory10
1555
1654
  } from "@hivelore/core";
1556
- import { z as z18 } from "zod";
1655
+ import { z as z19 } from "zod";
1557
1656
 
1558
1657
  // src/session-tracker.ts
1559
1658
  import {
@@ -1562,12 +1661,12 @@ import {
1562
1661
  loadConfig as loadConfig2,
1563
1662
  writeSessionHandoff
1564
1663
  } 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";
1664
+ import { mkdir as mkdir6, writeFile as writeFile12, rm } from "fs/promises";
1665
+ import { existsSync as existsSync19 } from "fs";
1666
+ import path9 from "path";
1568
1667
  import { execSync as execSync2 } from "child_process";
1569
1668
  function pendingDistillPath(ctx) {
1570
- return path8.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
1669
+ return path9.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
1571
1670
  }
1572
1671
  var SessionTracker = class {
1573
1672
  events = [];
@@ -1671,7 +1770,7 @@ var SessionTracker = class {
1671
1770
  (e) => e.tool === "mem_session_end" && !e.summary?.startsWith("Auto-captured")
1672
1771
  );
1673
1772
  const isSubstantialSession = totalCalls >= 3 || writingTools.length > 0;
1674
- if (!ranPostTask && isSubstantialSession && existsSync18(this.ctx.paths.haiveDir)) {
1773
+ if (!ranPostTask && isSubstantialSession && existsSync19(this.ctx.paths.haiveDir)) {
1675
1774
  try {
1676
1775
  const memoriesSaved = writingTools.map((e) => e.summary ?? "").filter(Boolean).slice(0, 20);
1677
1776
  const payload = {
@@ -1684,9 +1783,9 @@ var SessionTracker = class {
1684
1783
  ...gitDiff ? { git_diff: gitDiff } : {},
1685
1784
  ...recapId ? { recap_id: recapId } : {}
1686
1785
  };
1687
- const cacheDir = path8.join(this.ctx.paths.haiveDir, ".cache");
1688
- await mkdir5(cacheDir, { recursive: true });
1689
- await writeFile11(
1786
+ const cacheDir = path9.join(this.ctx.paths.haiveDir, ".cache");
1787
+ await mkdir6(cacheDir, { recursive: true });
1788
+ await writeFile12(
1690
1789
  pendingDistillPath(this.ctx),
1691
1790
  JSON.stringify(payload, null, 2) + "\n",
1692
1791
  "utf8"
@@ -1705,7 +1804,7 @@ var SessionTracker = class {
1705
1804
  };
1706
1805
  async function clearPendingDistill(ctx) {
1707
1806
  const p = pendingDistillPath(ctx);
1708
- if (existsSync18(p)) {
1807
+ if (existsSync19(p)) {
1709
1808
  try {
1710
1809
  await rm(p);
1711
1810
  } catch {
@@ -1722,15 +1821,15 @@ function summarizeTools(events) {
1722
1821
 
1723
1822
  // src/tools/mem-session-end.ts
1724
1823
  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(
1824
+ goal: z19.string().min(1).describe("What you were trying to accomplish this session (1\u20132 sentences)"),
1825
+ accomplished: z19.string().describe("What was actually done \u2014 bullet list recommended"),
1826
+ discoveries: z19.string().default("").describe(
1728
1827
  "Any bugs, inconsistencies, surprises, or missing knowledge found during this session. Empty if nothing surprising was found."
1729
1828
  ),
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)")
1829
+ files_touched: z19.array(z19.string()).default([]).describe("Key files that were read or modified \u2014 used as anchor paths"),
1830
+ next_steps: z19.string().default("").describe("What should happen next (for the next session or a teammate)"),
1831
+ scope: z19.enum(["personal", "team", "module"]).default("personal").describe("Visibility: personal = private to you, team = shared with the team"),
1832
+ module: z19.string().optional().describe("Module name (required when scope=module)")
1734
1833
  };
1735
1834
  function recapTopic(scope, module) {
1736
1835
  return module ? `session-recap-${scope}-${module}` : `session-recap-${scope}`;
@@ -1760,23 +1859,23 @@ ${input.next_steps}`);
1760
1859
  return lines.join("\n");
1761
1860
  }
1762
1861
  async function memSessionEnd(input, ctx) {
1763
- if (!existsSync19(ctx.paths.haiveDir)) {
1862
+ if (!existsSync20(ctx.paths.haiveDir)) {
1764
1863
  throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'hivelore init' first.`);
1765
1864
  }
1766
1865
  const body = buildBody(input);
1767
1866
  const topic = recapTopic(input.scope, input.module);
1768
1867
  const normalizedFiles = input.files_touched.map((p) => {
1769
- if (!p || !path9.isAbsolute(p)) return p;
1770
- const rel = path9.relative(ctx.paths.root, p);
1868
+ if (!p || !path10.isAbsolute(p)) return p;
1869
+ const rel = path10.relative(ctx.paths.root, p);
1771
1870
  return rel.startsWith("..") ? p : rel;
1772
1871
  });
1773
1872
  const invalidPaths = normalizedFiles.filter(
1774
- (p) => !existsSync19(path9.resolve(ctx.paths.root, p))
1873
+ (p) => !existsSync20(path10.resolve(ctx.paths.root, p))
1775
1874
  );
1776
1875
  if (invalidPaths.length > 0) {
1777
1876
  console.warn(`[haive] session end: anchor path(s) not found: ${invalidPaths.join(", ")}`);
1778
1877
  }
1779
- const existing = existsSync19(ctx.paths.memoriesDir) ? await loadMemoriesFromDir15(ctx.paths.memoriesDir) : [];
1878
+ const existing = existsSync20(ctx.paths.memoriesDir) ? await loadMemoriesFromDir16(ctx.paths.memoriesDir) : [];
1780
1879
  const topicMatch = existing.find(
1781
1880
  ({ memory }) => memory.frontmatter.topic === topic && memory.frontmatter.scope === input.scope && (!input.module || memory.frontmatter.module === input.module)
1782
1881
  );
@@ -1792,7 +1891,7 @@ async function memSessionEnd(input, ctx) {
1792
1891
  paths: normalizedFiles.length ? normalizedFiles : fm.anchor.paths
1793
1892
  }
1794
1893
  };
1795
- await writeFile12(
1894
+ await writeFile13(
1796
1895
  topicMatch.filePath,
1797
1896
  serializeMemory10({ frontmatter: newFrontmatter, body }),
1798
1897
  "utf8"
@@ -1822,8 +1921,8 @@ async function memSessionEnd(input, ctx) {
1822
1921
  frontmatter.id,
1823
1922
  frontmatter.module
1824
1923
  );
1825
- await mkdir6(path9.dirname(file), { recursive: true });
1826
- await writeFile12(file, serializeMemory10({ frontmatter, body }), "utf8");
1924
+ await mkdir7(path10.dirname(file), { recursive: true });
1925
+ await writeFile13(file, serializeMemory10({ frontmatter, body }), "utf8");
1827
1926
  await clearPendingDistill(ctx);
1828
1927
  return {
1829
1928
  id: frontmatter.id,
@@ -1835,9 +1934,9 @@ async function memSessionEnd(input, ctx) {
1835
1934
  }
1836
1935
 
1837
1936
  // 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";
1937
+ import { readFile as readFile7, writeFile as writeFile14, readdir as readdir4 } from "fs/promises";
1938
+ import { existsSync as existsSync22 } from "fs";
1939
+ import path12 from "path";
1841
1940
  import {
1842
1941
  allocateBudget,
1843
1942
  assessBootstrapState,
@@ -1861,7 +1960,7 @@ import {
1861
1960
  loadConfig as loadConfig3,
1862
1961
  memoryHasExcludedTag,
1863
1962
  hashProjectContext,
1864
- loadMemoriesFromDir as loadMemoriesFromDir16,
1963
+ loadMemoriesFromDir as loadMemoriesFromDir17,
1865
1964
  loadPreventionEvents,
1866
1965
  loadUsageIndex as loadUsageIndex8,
1867
1966
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
@@ -1879,12 +1978,12 @@ import {
1879
1978
  truncateToTokens,
1880
1979
  writeBriefingMarker
1881
1980
  } from "@hivelore/core";
1882
- import { z as z19 } from "zod";
1981
+ import { z as z20 } from "zod";
1883
1982
 
1884
1983
  // 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";
1984
+ import { readdir as readdir3, readFile as readFile6 } from "fs/promises";
1985
+ import { existsSync as existsSync21 } from "fs";
1986
+ import path11 from "path";
1888
1987
  import {
1889
1988
  classifyMemoryPriority as coreClassifyPriority,
1890
1989
  isGlobPath,
@@ -2017,16 +2116,16 @@ async function trySemanticHits(ctx, task, limit) {
2017
2116
  }
2018
2117
  async function loadModuleContexts2(ctx, modules) {
2019
2118
  if (modules.length === 0) return [];
2020
- if (!existsSync20(ctx.paths.modulesContextDir)) return [];
2119
+ if (!existsSync21(ctx.paths.modulesContextDir)) return [];
2021
2120
  const available = new Set(
2022
2121
  (await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
2023
2122
  );
2024
2123
  const out = [];
2025
2124
  for (const m of modules) {
2026
2125
  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") });
2126
+ const file = path11.join(ctx.paths.modulesContextDir, m, "context.md");
2127
+ if (existsSync21(file)) {
2128
+ out.push({ name: m, content: await readFile6(file, "utf8") });
2030
2129
  }
2031
2130
  }
2032
2131
  return out;
@@ -2034,38 +2133,38 @@ async function loadModuleContexts2(ctx, modules) {
2034
2133
 
2035
2134
  // src/tools/get-briefing.ts
2036
2135
  var GetBriefingInputSchema = {
2037
- task: z19.string().optional().describe(
2136
+ task: z20.string().optional().describe(
2038
2137
  "What you are about to do, in 1\u20132 sentences. Used to rank relevant memories semantically."
2039
2138
  ),
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(
2139
+ files: z20.array(z20.string()).default([]).describe("Project-relative file paths the agent is currently looking at or about to edit"),
2140
+ max_tokens: z20.number().int().positive().default(8e3).describe(
2042
2141
  "Approximate token budget for the entire briefing. Each section is allocated a share and truncated to fit."
2043
2142
  ),
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(
2143
+ max_memories: z20.number().int().positive().default(8).describe("Cap on memories surfaced regardless of token budget"),
2144
+ include_project_context: z20.boolean().default(true),
2145
+ dedupe_project_context: z20.boolean().optional().describe(
2047
2146
  "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
2147
  ),
2049
- include_module_contexts: z19.boolean().default(true),
2050
- semantic: z19.boolean().default(true).describe(
2148
+ include_module_contexts: z20.boolean().default(true),
2149
+ semantic: z20.boolean().default(true).describe(
2051
2150
  "Use semantic ranking when a task is provided (requires `hivelore embeddings index`)."
2052
2151
  ),
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(
2152
+ include_stale: z20.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
2153
+ track: z20.boolean().default(true).describe("Increment read_count on returned memories"),
2154
+ format: z20.enum(["full", "compact", "actions"]).default("full").describe(
2056
2155
  "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
2156
  ),
2058
- symbols: z19.array(z19.string()).default([]).describe(
2157
+ symbols: z20.array(z20.string()).default([]).describe(
2059
2158
  "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
2159
  ),
2061
- min_semantic_score: z19.number().min(0).max(1).default(0).describe(
2160
+ min_semantic_score: z20.number().min(0).max(1).default(0).describe(
2062
2161
  "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
2162
  ),
2064
- budget_preset: z19.enum(["quick", "balanced", "deep"]).optional().describe(
2163
+ budget_preset: z20.enum(["quick", "balanced", "deep"]).optional().describe(
2065
2164
  "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
2165
  )
2067
2166
  };
2068
- var GetBriefingZod = z19.object(GetBriefingInputSchema);
2167
+ var GetBriefingZod = z20.object(GetBriefingInputSchema);
2069
2168
  async function getBriefing(input, ctx) {
2070
2169
  const resolvedBudget = resolveBriefingBudget(input.budget_preset, {
2071
2170
  max_tokens: input.max_tokens,
@@ -2081,8 +2180,8 @@ async function getBriefing(input, ctx) {
2081
2180
  let usage = { version: 1, updated_at: "", by_id: {} };
2082
2181
  let byId = /* @__PURE__ */ new Map();
2083
2182
  let lastSession;
2084
- if (existsSync21(ctx.paths.memoriesDir)) {
2085
- const allLoaded = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
2183
+ if (existsSync22(ctx.paths.memoriesDir)) {
2184
+ const allLoaded = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
2086
2185
  const recaps = allLoaded.filter(({ memory }) => memory.frontmatter.type === "session_recap").sort(
2087
2186
  (a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
2088
2187
  );
@@ -2256,7 +2355,7 @@ async function getBriefing(input, ctx) {
2256
2355
  if (!isAutoPromoteEligible(loaded.memory.frontmatter, u, rule)) continue;
2257
2356
  const newFm = { ...loaded.memory.frontmatter, status: "validated", validated_by: "auto" };
2258
2357
  try {
2259
- await writeFile13(loaded.filePath, serializeMemory11({ frontmatter: newFm, body: loaded.memory.body }), "utf8");
2358
+ await writeFile14(loaded.filePath, serializeMemory11({ frontmatter: newFm, body: loaded.memory.body }), "utf8");
2260
2359
  m.status = "validated";
2261
2360
  m.confidence = "trusted";
2262
2361
  } catch {
@@ -2264,7 +2363,7 @@ async function getBriefing(input, ctx) {
2264
2363
  }
2265
2364
  }
2266
2365
  }
2267
- let projectContextRaw = input.include_project_context && existsSync21(ctx.paths.projectContext) ? await readFile6(ctx.paths.projectContext, "utf8") : "";
2366
+ let projectContextRaw = input.include_project_context && existsSync22(ctx.paths.projectContext) ? await readFile7(ctx.paths.projectContext, "utf8") : "";
2268
2367
  let contextOmittedRecent = false;
2269
2368
  if (projectContextRaw && input.dedupe_project_context !== false) {
2270
2369
  const ctxHash = hashProjectContext(projectContextRaw);
@@ -2279,7 +2378,7 @@ async function getBriefing(input, ctx) {
2279
2378
  const setupWarnings = [];
2280
2379
  let autoContextGenerated = false;
2281
2380
  let projectContext = isTemplateContext ? "" : projectContextRaw;
2282
- if ((isTemplateContext || !existsSync21(ctx.paths.projectContext)) && input.include_project_context) {
2381
+ if ((isTemplateContext || !existsSync22(ctx.paths.projectContext)) && input.include_project_context) {
2283
2382
  const haiveConfig = await loadConfig3(ctx.paths);
2284
2383
  if (haiveConfig.autoContext) {
2285
2384
  const codeMap = await loadCodeMap(ctx.paths);
@@ -2453,8 +2552,8 @@ ${m.content}`).join("\n\n---\n\n"),
2453
2552
  actionRequired.push(extractActionItem(m.id, loaded.memory.body));
2454
2553
  }
2455
2554
  }
2456
- if (existsSync21(ctx.paths.memoriesDir)) {
2457
- const allMems = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
2555
+ if (existsSync22(ctx.paths.memoriesDir)) {
2556
+ const allMems = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
2458
2557
  for (const { memory } of allMems) {
2459
2558
  const fm = memory.frontmatter;
2460
2559
  if (!fm.requires_human_approval) continue;
@@ -2464,9 +2563,9 @@ ${m.content}`).join("\n\n---\n\n"),
2464
2563
  }
2465
2564
  }
2466
2565
  const pendingDistillFile = pendingDistillPath(ctx);
2467
- if (existsSync21(pendingDistillFile)) {
2566
+ if (existsSync22(pendingDistillFile)) {
2468
2567
  try {
2469
- const raw = await readFile6(pendingDistillFile, "utf8");
2568
+ const raw = await readFile7(pendingDistillFile, "utf8");
2470
2569
  const pd = JSON.parse(raw);
2471
2570
  const ageMs = Date.now() - new Date(pd.session_end).getTime();
2472
2571
  const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1e3;
@@ -2493,7 +2592,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
2493
2592
  }
2494
2593
  }
2495
2594
  const memoriesEmpty = outputMemories.length === 0;
2496
- const hasMemoriesDir = existsSync21(ctx.paths.memoriesDir);
2595
+ const hasMemoriesDir = existsSync22(ctx.paths.memoriesDir);
2497
2596
  const isColdStart = isTemplateContext && memoriesEmpty && !lastSession && !autoContextGenerated;
2498
2597
  const hasUnguessableSignal = outputMemories.some(
2499
2598
  (m) => (m.priority === "must_read" || m.priority === "useful") && specificityScore(m.body) >= GUESSABLE_THRESHOLD
@@ -2508,10 +2607,10 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
2508
2607
  try {
2509
2608
  let pcRaw = "";
2510
2609
  try {
2511
- pcRaw = await readFile6(ctx.paths.projectContext, "utf8");
2610
+ pcRaw = await readFile7(ctx.paths.projectContext, "utf8");
2512
2611
  } catch {
2513
2612
  }
2514
- const allForBootstrap = existsSync21(ctx.paths.memoriesDir) ? await loadMemoriesFromDir16(ctx.paths.memoriesDir) : [];
2613
+ const allForBootstrap = existsSync22(ctx.paths.memoriesDir) ? await loadMemoriesFromDir17(ctx.paths.memoriesDir) : [];
2515
2614
  const cmForBootstrap = await loadCodeMap(ctx.paths);
2516
2615
  let existingModules = [];
2517
2616
  try {
@@ -2575,7 +2674,7 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
2575
2674
  "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
2675
  );
2577
2676
  }
2578
- if (outputMemories.length > 0 && existsSync21(ctx.paths.haiveDir)) {
2677
+ if (outputMemories.length > 0 && existsSync22(ctx.paths.haiveDir)) {
2579
2678
  const proof = briefingProofLine(await loadPreventionEvents(ctx.paths));
2580
2679
  if (proof) hints.push(proof);
2581
2680
  }
@@ -2589,7 +2688,7 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
2589
2688
  adaptiveTrim
2590
2689
  });
2591
2690
  const breadcrumbTokens = breadcrumbs ? estimateTokens([...breadcrumbs.start_here, ...breadcrumbs.drill_down, breadcrumbs.note ?? ""].join("\n")) : 0;
2592
- if (existsSync21(ctx.paths.haiveDir)) {
2691
+ if (existsSync22(ctx.paths.haiveDir)) {
2593
2692
  await writeBriefingMarker(ctx.paths, {
2594
2693
  sessionId: process.env.HAIVE_SESSION_ID,
2595
2694
  ...input.task ? { task: input.task } : {},
@@ -2645,10 +2744,10 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
2645
2744
  };
2646
2745
  }
2647
2746
  async function detectRunCommands(root) {
2648
- const pkgPath = path11.join(root, "package.json");
2649
- if (!existsSync21(pkgPath)) return null;
2747
+ const pkgPath = path12.join(root, "package.json");
2748
+ if (!existsSync22(pkgPath)) return null;
2650
2749
  try {
2651
- const pkg = JSON.parse(await readFile6(pkgPath, "utf8"));
2750
+ const pkg = JSON.parse(await readFile7(pkgPath, "utf8"));
2652
2751
  const scripts = pkg.scripts ?? {};
2653
2752
  const order = ["test", "build", "lint", "typecheck", "type-check", "dev", "start"];
2654
2753
  const lines = order.filter((name) => typeof scripts[name] === "string" && scripts[name].trim() !== "").map((name) => `- \`${name}\`: \`${scripts[name]}\``);
@@ -2713,24 +2812,24 @@ function oneLine(value) {
2713
2812
  return value.replace(/\s+/g, " ").replace(/"/g, '\\"').trim().slice(0, 120);
2714
2813
  }
2715
2814
  function serverVersion() {
2716
- return true ? "0.36.0" : "dev";
2815
+ return true ? "0.38.0" : "dev";
2717
2816
  }
2718
2817
 
2719
2818
  // src/tools/code-map.ts
2720
2819
  import { estimateTokens as estimateTokens2, loadCodeMap as loadCodeMap2, queryCodeMap as queryCodeMap2 } from "@hivelore/core";
2721
- import { z as z20 } from "zod";
2820
+ import { z as z21 } from "zod";
2722
2821
  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(
2822
+ file: z21.string().optional().describe("Filter to files whose path contains this substring"),
2823
+ symbol: z21.string().optional().describe("Filter to files exporting a symbol whose name contains this substring"),
2824
+ paths: z21.array(z21.string()).default([]).describe(
2726
2825
  "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
2826
  ),
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(
2827
+ max_files: z21.number().int().positive().default(40).describe("Cap on returned files (hard limit, applied after token budget)"),
2828
+ max_tokens: z21.number().int().positive().optional().describe(
2730
2829
  "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
2830
  )
2732
2831
  };
2733
- var CodeMapInputZod = z20.object(CodeMapInputSchema);
2832
+ var CodeMapInputZod = z21.object(CodeMapInputSchema);
2734
2833
  async function codeMapTool(input, ctx) {
2735
2834
  const map = await loadCodeMap2(ctx.paths);
2736
2835
  if (!map) {
@@ -2799,18 +2898,18 @@ function estimateFileEntryTokens(f) {
2799
2898
  }
2800
2899
 
2801
2900
  // 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";
2901
+ import { existsSync as existsSync23 } from "fs";
2902
+ import { loadMemoriesFromDir as loadMemoriesFromDir18 } from "@hivelore/core";
2903
+ import { z as z22 } from "zod";
2805
2904
  var MemDiffInputSchema = {
2806
- id_a: z21.string().min(1).describe("First memory id"),
2807
- id_b: z21.string().min(1).describe("Second memory id")
2905
+ id_a: z22.string().min(1).describe("First memory id"),
2906
+ id_b: z22.string().min(1).describe("Second memory id")
2808
2907
  };
2809
2908
  async function memDiff(input, ctx) {
2810
- if (!existsSync22(ctx.paths.memoriesDir)) {
2909
+ if (!existsSync23(ctx.paths.memoriesDir)) {
2811
2910
  throw new Error(`No .ai/memories at ${ctx.paths.root}.`);
2812
2911
  }
2813
- const all = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
2912
+ const all = await loadMemoriesFromDir18(ctx.paths.memoriesDir);
2814
2913
  const foundA = all.find((m) => m.memory.frontmatter.id === input.id_a);
2815
2914
  const foundB = all.find((m) => m.memory.frontmatter.id === input.id_b);
2816
2915
  if (!foundA) throw new Error(`No memory with id "${input.id_a}".`);
@@ -2844,19 +2943,19 @@ async function memDiff(input, ctx) {
2844
2943
  }
2845
2944
 
2846
2945
  // 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";
2946
+ import { existsSync as existsSync24 } from "fs";
2947
+ import { loadMemoriesFromDir as loadMemoriesFromDir19 } from "@hivelore/core";
2948
+ import { z as z23 } from "zod";
2850
2949
  var GetRecapInputSchema = {
2851
- scope: z22.enum(["personal", "team", "any"]).default("any").describe(
2950
+ scope: z23.enum(["personal", "team", "any"]).default("any").describe(
2852
2951
  "Limit to a specific scope's recap. Default 'any' returns the most recent recap across both personal and team scopes."
2853
2952
  )
2854
2953
  };
2855
2954
  async function getRecap(input, ctx) {
2856
- if (!existsSync23(ctx.paths.memoriesDir)) {
2955
+ if (!existsSync24(ctx.paths.memoriesDir)) {
2857
2956
  return { recap: null, notice: "No .ai/memories directory \u2014 haive not initialized here." };
2858
2957
  }
2859
- const all = await loadMemoriesFromDir18(ctx.paths.memoriesDir);
2958
+ const all = await loadMemoriesFromDir19(ctx.paths.memoriesDir);
2860
2959
  const recaps = all.filter(({ memory }) => memory.frontmatter.type === "session_recap").filter(({ memory }) => input.scope === "any" || memory.frontmatter.scope === input.scope).sort(
2861
2960
  (a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
2862
2961
  );
@@ -2880,13 +2979,13 @@ async function getRecap(input, ctx) {
2880
2979
  }
2881
2980
 
2882
2981
  // src/tools/mem-relevant-to.ts
2883
- import { z as z23 } from "zod";
2982
+ import { z as z24 } from "zod";
2884
2983
  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.")
2984
+ task: z24.string().min(1).describe("What you are about to do, in 1\u20132 sentences. Used to rank relevant memories."),
2985
+ files: z24.array(z24.string()).default([]).describe("Optional: files you are about to edit \u2014 surfaces anchored memories."),
2986
+ limit: z24.number().int().positive().max(30).default(8).describe("Cap on returned memories."),
2987
+ min_semantic_score: z24.number().min(0).max(1).default(0.25).describe("Drop weakly-related semantic hits below this cosine threshold."),
2988
+ format: z24.enum(["full", "compact", "actions"]).default("full").describe("'compact' = id + 1-line summary; 'full' = complete bodies; 'actions' = bullet-first excerpts.")
2890
2989
  };
2891
2990
  async function memRelevantTo(input, ctx) {
2892
2991
  const briefingInput = {
@@ -2916,14 +3015,14 @@ async function memRelevantTo(input, ctx) {
2916
3015
  }
2917
3016
 
2918
3017
  // src/tools/code-search.ts
2919
- import { z as z24 } from "zod";
3018
+ import { z as z25 } from "zod";
2920
3019
  import { loadCodeMap as loadCodeMap3 } from "@hivelore/core";
2921
3020
  var CodeSearchInputSchema = {
2922
- query: z24.string().min(1).describe(
3021
+ query: z25.string().min(1).describe(
2923
3022
  "Natural-language description of what you are looking for in the codebase (e.g. 'function that hashes passwords', 'JWT signing logic', 'route registration')."
2924
3023
  ),
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(
3024
+ k: z25.number().int().positive().max(50).default(5).describe("Number of top hits to return."),
3025
+ min_score: z25.number().min(0).max(1).default(0.2).describe(
2927
3026
  "Minimum cosine similarity. Hits below this threshold are dropped to avoid noise. Try 0.3+ for stricter matching."
2928
3027
  )
2929
3028
  };
@@ -2966,7 +3065,7 @@ async function codeSearch(input, ctx) {
2966
3065
  }
2967
3066
 
2968
3067
  // src/tools/anti-patterns-check.ts
2969
- import { existsSync as existsSync24 } from "fs";
3068
+ import { existsSync as existsSync25 } from "fs";
2970
3069
  import {
2971
3070
  addedLinesFromDiff,
2972
3071
  BRIDGE_TARGET_PATH,
@@ -2976,7 +3075,7 @@ import {
2976
3075
  diffHasDistinctiveOverlap,
2977
3076
  getUsage as getUsage7,
2978
3077
  isRetiredMemory as isRetiredMemory2,
2979
- loadMemoriesFromDir as loadMemoriesFromDir19,
3078
+ loadMemoriesFromDir as loadMemoriesFromDir20,
2980
3079
  loadUsageIndex as loadUsageIndex9,
2981
3080
  literalMatchesAnyToken as literalMatchesAnyToken3,
2982
3081
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths3,
@@ -2985,19 +3084,19 @@ import {
2985
3084
  sensorTargetsFromDiff,
2986
3085
  tokenizeQuery as tokenizeQuery3
2987
3086
  } from "@hivelore/core";
2988
- import { z as z25 } from "zod";
3087
+ import { z as z26 } from "zod";
2989
3088
  var AntiPatternsCheckInputSchema = {
2990
- diff: z25.string().optional().describe(
3089
+ diff: z26.string().optional().describe(
2991
3090
  "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
3091
  ),
2993
- paths: z25.array(z25.string()).default([]).describe(
3092
+ paths: z26.array(z26.string()).default([]).describe(
2994
3093
  "File paths affected by the change. Memories anchored to any of these paths are surfaced regardless of the diff content."
2995
3094
  ),
2996
- limit: z25.number().int().positive().max(20).default(8).describe("Cap on returned warnings."),
2997
- semantic: z25.boolean().default(true).describe(
3095
+ limit: z26.number().int().positive().max(20).default(8).describe("Cap on returned warnings."),
3096
+ semantic: z26.boolean().default(true).describe(
2998
3097
  "When true, also use semantic search (requires @hivelore/embeddings + memory index) to find related anti-patterns."
2999
3098
  ),
3000
- min_semantic_score: z25.number().min(0).max(1).default(0.45).describe(
3099
+ min_semantic_score: z26.number().min(0).max(1).default(0.45).describe(
3001
3100
  "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
3101
  )
3003
3102
  };
@@ -3081,10 +3180,10 @@ async function antiPatternsCheck(input, ctx) {
3081
3180
  notice: "Nothing to check \u2014 provide either `diff` text or `paths`."
3082
3181
  };
3083
3182
  }
3084
- if (!existsSync24(ctx.paths.memoriesDir)) {
3183
+ if (!existsSync25(ctx.paths.memoriesDir)) {
3085
3184
  return { scanned: 0, warnings: [], notice: "No .ai/memories directory \u2014 nothing to check against." };
3086
3185
  }
3087
- const all = await loadMemoriesFromDir19(ctx.paths.memoriesDir);
3186
+ const all = await loadMemoriesFromDir20(ctx.paths.memoriesDir);
3088
3187
  const minSemanticScore = input.min_semantic_score ?? 0.45;
3089
3188
  const negative = all.filter(({ memory }) => {
3090
3189
  const t = memory.frontmatter.type;
@@ -3200,19 +3299,19 @@ async function antiPatternsCheck(input, ctx) {
3200
3299
  }
3201
3300
 
3202
3301
  // src/tools/mem-distill.ts
3203
- import { existsSync as existsSync25 } from "fs";
3302
+ import { existsSync as existsSync26 } from "fs";
3204
3303
  import {
3205
- loadMemoriesFromDir as loadMemoriesFromDir20,
3304
+ loadMemoriesFromDir as loadMemoriesFromDir21,
3206
3305
  tokenizeQuery as tokenizeQuery4
3207
3306
  } from "@hivelore/core";
3208
- import { z as z26 } from "zod";
3307
+ import { z as z27 } from "zod";
3209
3308
  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(
3309
+ since_days: z27.number().int().positive().default(30).describe("Only consider memories created in the last N days."),
3310
+ min_cluster: z27.number().int().min(2).default(3).describe("Minimum cluster size to surface."),
3311
+ type_filter: z27.enum(["gotcha", "attempt", "all"]).default("gotcha").describe(
3213
3312
  "Memory type to scan. 'gotcha' targets observe-style discoveries that recur, 'attempt' surfaces failed approaches that repeat, 'all' considers both."
3214
3313
  ),
3215
- scope: z26.enum(["personal", "team", "module", "any"]).default("any").describe("Restrict to a specific scope.")
3314
+ scope: z27.enum(["personal", "team", "module", "any"]).default("any").describe("Restrict to a specific scope.")
3216
3315
  };
3217
3316
  var MS_PER_DAY = 24 * 60 * 60 * 1e3;
3218
3317
  var STOP_WORDS = /* @__PURE__ */ new Set([
@@ -3252,11 +3351,11 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
3252
3351
  "error"
3253
3352
  ]);
3254
3353
  async function memDistill(input, ctx) {
3255
- if (!existsSync25(ctx.paths.memoriesDir)) {
3354
+ if (!existsSync26(ctx.paths.memoriesDir)) {
3256
3355
  return { scanned: 0, singletons: 0, clusters: [], notice: "No .ai/memories directory." };
3257
3356
  }
3258
3357
  const cutoff = Date.now() - input.since_days * MS_PER_DAY;
3259
- const all = await loadMemoriesFromDir20(ctx.paths.memoriesDir);
3358
+ const all = await loadMemoriesFromDir21(ctx.paths.memoriesDir);
3260
3359
  const candidates = all.filter(({ memory }) => {
3261
3360
  const fm = memory.frontmatter;
3262
3361
  if (fm.status === "rejected" || fm.status === "deprecated" || fm.status === "stale") return false;
@@ -3361,17 +3460,17 @@ function firstHeading(body) {
3361
3460
 
3362
3461
  // src/tools/precommit-check.ts
3363
3462
  import { pathsOverlap as pathsOverlap2 } from "@hivelore/core";
3364
- import { z as z27 } from "zod";
3463
+ import { z as z28 } from "zod";
3365
3464
  var PreCommitCheckInputSchema = {
3366
- diff: z27.string().optional().describe(
3465
+ diff: z28.string().optional().describe(
3367
3466
  "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
3467
  ),
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(
3468
+ paths: z28.array(z28.string()).default([]).describe("Project-relative paths affected by the change. At least one of `diff` or `paths` should be provided."),
3469
+ block_on: z28.enum(["any", "high-confidence", "never"]).default("high-confidence").describe(
3371
3470
  "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
3471
  ),
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(
3472
+ semantic: z28.boolean().default(true).describe("Enable semantic search in anti_patterns_check (requires embeddings index)."),
3473
+ anchored_blocks: z28.boolean().default(false).describe(
3375
3474
  "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
3475
  )
3377
3476
  };
@@ -3677,14 +3776,14 @@ function repairTargetPathForWarning(warning, paths) {
3677
3776
  }
3678
3777
 
3679
3778
  // src/tools/mem-conflict-candidates.ts
3680
- import { existsSync as existsSync26 } from "fs";
3779
+ import { existsSync as existsSync27 } from "fs";
3681
3780
  import {
3682
3781
  findLexicalConflictPairs,
3683
3782
  findTopicStatusConflictPairs,
3684
- loadMemoriesFromDir as loadMemoriesFromDir21,
3783
+ loadMemoriesFromDir as loadMemoriesFromDir22,
3685
3784
  planConflictResolution
3686
3785
  } from "@hivelore/core";
3687
- import { z as z28 } from "zod";
3786
+ import { z as z29 } from "zod";
3688
3787
  function suggestResolution(byId, idA, idB) {
3689
3788
  const a = byId.get(idA);
3690
3789
  const b = byId.get(idB);
@@ -3698,17 +3797,17 @@ function suggestResolution(byId, idA, idB) {
3698
3797
  };
3699
3798
  }
3700
3799
  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(
3800
+ since_days: z29.number().int().positive().max(3650).default(365).describe("Only memories created since N days ago"),
3801
+ types: z29.array(z29.enum(["decision", "architecture", "convention", "gotcha"])).default(["decision", "architecture"]).describe("Memory types scanned for pairwise lexical overlap"),
3802
+ min_jaccard: z29.number().min(0).max(1).default(0.45).describe("Minimum Jaccard token similarity to surface as a candidate pair"),
3803
+ max_pairs: z29.number().int().positive().max(100).default(20).describe("Cap pairs returned"),
3804
+ 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."),
3805
+ max_topic_pairs: z29.number().int().positive().max(100).default(20).describe(
3707
3806
  "Cap for extra signal: memories sharing the same topic with validated vs rejected status."
3708
3807
  )
3709
3808
  };
3710
3809
  async function memConflictCandidates(input, ctx) {
3711
- if (!existsSync26(ctx.paths.memoriesDir)) {
3810
+ if (!existsSync27(ctx.paths.memoriesDir)) {
3712
3811
  return {
3713
3812
  pairs: [],
3714
3813
  topic_status_pairs: [],
@@ -3717,7 +3816,7 @@ async function memConflictCandidates(input, ctx) {
3717
3816
  notice: "No .ai/memories directory."
3718
3817
  };
3719
3818
  }
3720
- const all = await loadMemoriesFromDir21(ctx.paths.memoriesDir);
3819
+ const all = await loadMemoriesFromDir22(ctx.paths.memoriesDir);
3721
3820
  const byId = new Map(all.map((m) => [m.memory.frontmatter.id, m]));
3722
3821
  const { pairs, scanned, truncated } = findLexicalConflictPairs(all, {
3723
3822
  sinceDays: input.since_days,
@@ -3747,9 +3846,9 @@ async function memConflictCandidates(input, ctx) {
3747
3846
 
3748
3847
  // src/tools/mem-resolve-project.ts
3749
3848
  import { resolveProjectInfo } from "@hivelore/core";
3750
- import { z as z29 } from "zod";
3849
+ import { z as z30 } from "zod";
3751
3850
  var MemResolveProjectInputSchema = {
3752
- cwd: z29.string().optional().describe("Directory used for root discovery when HAIVE_PROJECT_ROOT is unset.")
3851
+ cwd: z30.string().optional().describe("Directory used for root discovery when HAIVE_PROJECT_ROOT is unset.")
3753
3852
  };
3754
3853
  async function memResolveProject(input, _ctx) {
3755
3854
  void _ctx;
@@ -3763,10 +3862,10 @@ async function memResolveProject(input, _ctx) {
3763
3862
 
3764
3863
  // src/tools/mem-suggest-topic.ts
3765
3864
  import { MemoryTypeSchema, suggestTopicKey } from "@hivelore/core";
3766
- import { z as z30 } from "zod";
3865
+ import { z as z31 } from "zod";
3767
3866
  var MemSuggestTopicInputSchema = {
3768
3867
  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")
3868
+ title: z31.string().min(1).describe("Short title or phrase (headers, headings) \u2014 turned into slug")
3770
3869
  };
3771
3870
  async function memSuggestTopic(input, _ctx) {
3772
3871
  void _ctx;
@@ -3775,19 +3874,19 @@ async function memSuggestTopic(input, _ctx) {
3775
3874
  }
3776
3875
 
3777
3876
  // 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";
3877
+ import { existsSync as existsSync28 } from "fs";
3878
+ import { collectTimelineEntries, loadMemoriesFromDir as loadMemoriesFromDir23 } from "@hivelore/core";
3879
+ import { z as z32 } from "zod";
3781
3880
  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")
3881
+ memory_id: z32.string().optional().describe("Seed id \u2014 expands via related_ids, topic, anchors"),
3882
+ topic: z32.string().optional().describe("Frontmatter.topic value \u2014 chronological list when memory_id omitted"),
3883
+ limit: z32.number().int().positive().max(100).default(30).describe("Max timeline entries returned")
3785
3884
  };
3786
3885
  async function memTimeline(input, ctx) {
3787
- if (!existsSync27(ctx.paths.memoriesDir)) {
3886
+ if (!existsSync28(ctx.paths.memoriesDir)) {
3788
3887
  return { entries: [], total: 0, notice: "No .ai/memories directory." };
3789
3888
  }
3790
- const all = await loadMemoriesFromDir22(ctx.paths.memoriesDir);
3889
+ const all = await loadMemoriesFromDir23(ctx.paths.memoriesDir);
3791
3890
  const { entries, notice } = collectTimelineEntries(all, {
3792
3891
  memoryId: input.memory_id,
3793
3892
  topic: input.topic,
@@ -3797,12 +3896,12 @@ async function memTimeline(input, ctx) {
3797
3896
  }
3798
3897
 
3799
3898
  // src/prompts/bootstrap-project.ts
3800
- import { z as z32 } from "zod";
3899
+ import { z as z33 } from "zod";
3801
3900
  var BootstrapProjectArgsSchema = {
3802
- module: z32.string().optional().describe(
3901
+ module: z33.string().optional().describe(
3803
3902
  "Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
3804
3903
  ),
3805
- focus: z32.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
3904
+ focus: z33.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
3806
3905
  };
3807
3906
  var ROOT_TEMPLATE = `# Project context
3808
3907
 
@@ -3884,25 +3983,25 @@ ${template}\`\`\`
3884
3983
  }
3885
3984
 
3886
3985
  // src/prompts/bootstrap-repo.ts
3887
- import { readFile as readFile7, readdir as readdir5 } from "fs/promises";
3888
- import { existsSync as existsSync28 } from "fs";
3986
+ import { readFile as readFile8, readdir as readdir5 } from "fs/promises";
3987
+ import { existsSync as existsSync29 } from "fs";
3889
3988
  import {
3890
3989
  assessBootstrapState as assessBootstrapState2,
3891
3990
  loadCodeMap as loadCodeMap4,
3892
- loadMemoriesFromDir as loadMemoriesFromDir23,
3991
+ loadMemoriesFromDir as loadMemoriesFromDir24,
3893
3992
  renderBootstrapChecklist as renderBootstrapChecklist2
3894
3993
  } from "@hivelore/core";
3895
- import { z as z33 } from "zod";
3994
+ import { z as z34 } from "zod";
3896
3995
  var BootstrapRepoArgsSchema = {
3897
- focus: z33.string().optional().describe("Optional area to emphasize first (e.g. 'payments', 'auth').")
3996
+ focus: z34.string().optional().describe("Optional area to emphasize first (e.g. 'payments', 'auth').")
3898
3997
  };
3899
3998
  async function currentAssessment(ctx) {
3900
3999
  let projectContextRaw = "";
3901
4000
  try {
3902
- projectContextRaw = await readFile7(ctx.paths.projectContext, "utf8");
4001
+ projectContextRaw = await readFile8(ctx.paths.projectContext, "utf8");
3903
4002
  } catch {
3904
4003
  }
3905
- const memories = existsSync28(ctx.paths.memoriesDir) ? await loadMemoriesFromDir23(ctx.paths.memoriesDir) : [];
4004
+ const memories = existsSync29(ctx.paths.memoriesDir) ? await loadMemoriesFromDir24(ctx.paths.memoriesDir) : [];
3906
4005
  const codeMap = await loadCodeMap4(ctx.paths);
3907
4006
  let existingModules = [];
3908
4007
  try {
@@ -3970,10 +4069,10 @@ Main code areas detected: ${areas}
3970
4069
  }
3971
4070
 
3972
4071
  // src/prompts/post-task.ts
3973
- import { z as z34 } from "zod";
4072
+ import { z as z35 } from "zod";
3974
4073
  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")
4074
+ task_summary: z35.string().optional().describe("One sentence describing what you just did"),
4075
+ files_touched: z35.array(z35.string()).optional().describe("Files you created or modified during the task")
3977
4076
  };
3978
4077
  function postTaskPrompt(args, ctx) {
3979
4078
  const taskLine = args.task_summary ? `
@@ -4070,12 +4169,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
4070
4169
  }
4071
4170
 
4072
4171
  // src/prompts/import-docs.ts
4073
- import { z as z35 } from "zod";
4172
+ import { z as z36 } from "zod";
4074
4173
  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")
4174
+ content: z36.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
4175
+ source: z36.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
4176
+ scope: z36.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
4177
+ dry_run: z36.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
4079
4178
  };
4080
4179
  function importDocsPrompt(args, ctx) {
4081
4180
  const sourceLine = args.source ? `
@@ -4141,7 +4240,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
4141
4240
  // src/server.ts
4142
4241
  import { hasRecentBriefingMarker, loadConfigSync } from "@hivelore/core";
4143
4242
  var SERVER_NAME = "hivelore";
4144
- var SERVER_VERSION = "0.36.0";
4243
+ var SERVER_VERSION = "0.38.0";
4145
4244
  function jsonResult(data) {
4146
4245
  return {
4147
4246
  content: [
@@ -4164,7 +4263,8 @@ var ENFORCEMENT_PROFILE_TOOLS = [
4164
4263
  "code_search",
4165
4264
  "pre_commit_check",
4166
4265
  "mem_session_end",
4167
- "propose_sensor"
4266
+ "propose_sensor",
4267
+ "scaffold_test"
4168
4268
  ];
4169
4269
  var MAINTENANCE_PROFILE_TOOLS = [
4170
4270
  ...ENFORCEMENT_PROFILE_TOOLS,
@@ -4370,6 +4470,35 @@ function createHaiveServer(options = {}) {
4370
4470
  return jsonResult(await proposeSensor(input, context));
4371
4471
  }
4372
4472
  );
4473
+ registerTool(
4474
+ "scaffold_test",
4475
+ [
4476
+ "Generate a PENDING post-incident test from a lesson (attempt/gotcha) \u2014 the on-ramp to a command",
4477
+ "sensor. A command sensor routes YOUR test as its oracle, but someone has to write it; this writes",
4478
+ "the skeleton so you only fill in the assertion.",
4479
+ "",
4480
+ "USE THIS right after mem_tried when the mistake is behavioural (a regex can't express it): it",
4481
+ "writes a stub carrying the incident's provenance and returns the exact `sensors propose --kind",
4482
+ "test` command to arm it.",
4483
+ "",
4484
+ "It DOES NOT arm a sensor \u2014 propose_sensor stays the sole validated writer, and the stub is left",
4485
+ "PENDING (todo/skip) so the suite stays green until you write the assertion. Monorepo-aware: the",
4486
+ "framework and location come from the package that owns the lesson's anchor paths.",
4487
+ "",
4488
+ "PARAMETERS:",
4489
+ " memory_id \u2014 the attempt/gotcha to scaffold from",
4490
+ " framework \u2014 vitest | jest | pytest | gotest (auto-detected when omitted)",
4491
+ " out_path \u2014 override the test file path (repo-relative)",
4492
+ " write \u2014 write the file (default true); false returns the content for preview",
4493
+ "",
4494
+ "RETURNS: { ok, path, run_command, propose_command, content, written, already_exists, notice }"
4495
+ ].join("\n"),
4496
+ ScaffoldTestInputSchema,
4497
+ async (input) => {
4498
+ tracker.record("scaffold_test", input.memory_id);
4499
+ return jsonResult(await scaffoldTest(input, context));
4500
+ }
4501
+ );
4373
4502
  registerTool(
4374
4503
  "ingest_findings",
4375
4504
  [