@hivelore/mcp 0.42.1 → 0.44.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/README.md CHANGED
@@ -420,7 +420,7 @@ Post-task reflection checklist. Guides the AI through capturing failed approache
420
420
  ```
421
421
  Use the post_task prompt with:
422
422
  task_summary: "Added Stripe payment integration"
423
- files_touched: ["src/payments/StripeService.ts", "src/payments/PaymentController.ts"]
423
+ files_touched: '["src/payments/StripeService.ts", "src/payments/PaymentController.ts"]'
424
424
  ```
425
425
 
426
426
  ### `bootstrap_project`
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@
3
3
  // src/server.ts
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { mkdir as mkdir8, writeFile as writeFile15 } from "fs/promises";
7
+ import path15 from "path";
6
8
 
7
9
  // src/context.ts
8
10
  import { findProjectRoot, resolveHaivePaths } from "@hivelore/core";
@@ -1134,7 +1136,7 @@ import {
1134
1136
  import { z as z16 } from "zod";
1135
1137
 
1136
1138
  // src/tools/propose-sensor.ts
1137
- import { execSync } from "child_process";
1139
+ import { execFileSync } from "child_process";
1138
1140
  import { readFile as readFile3, writeFile as writeFile8 } from "fs/promises";
1139
1141
  import { existsSync as existsSync15, rmSync, symlinkSync } from "fs";
1140
1142
  import os from "os";
@@ -1154,10 +1156,32 @@ import { z as z15 } from "zod";
1154
1156
  // src/ast-sensors.ts
1155
1157
  import path5 from "path";
1156
1158
  var cachedEngine;
1159
+ var registeredDynamicLanguages = /* @__PURE__ */ new Set();
1160
+ async function loadDynamicLanguages(engine) {
1161
+ const registrations = {};
1162
+ const candidates = [
1163
+ ["python", () => import("@ast-grep/lang-python")],
1164
+ ["go", () => import("@ast-grep/lang-go")],
1165
+ ["rust", () => import("@ast-grep/lang-rust")],
1166
+ ["java", () => import("@ast-grep/lang-java")]
1167
+ ];
1168
+ for (const [name, load] of candidates) {
1169
+ try {
1170
+ const mod = await load();
1171
+ registrations[name] = mod.default;
1172
+ registeredDynamicLanguages.add(name);
1173
+ } catch {
1174
+ }
1175
+ }
1176
+ if (Object.keys(registrations).length > 0) {
1177
+ engine.registerDynamicLanguage(registrations);
1178
+ }
1179
+ }
1157
1180
  async function loadAstEngine() {
1158
1181
  if (cachedEngine !== void 0) return cachedEngine;
1159
1182
  try {
1160
1183
  cachedEngine = await import("@ast-grep/napi");
1184
+ await loadDynamicLanguages(cachedEngine);
1161
1185
  } catch {
1162
1186
  cachedEngine = null;
1163
1187
  }
@@ -1166,11 +1190,22 @@ async function loadAstEngine() {
1166
1190
  async function astEngineAvailable() {
1167
1191
  return await loadAstEngine() !== null;
1168
1192
  }
1169
- function astLangForPath(filePath) {
1193
+ function astLangForPath(filePath, explicitLanguage) {
1194
+ if (explicitLanguage) {
1195
+ const normalized = explicitLanguage.toLowerCase();
1196
+ if (["typescript", "tsx", "javascript", "html", "css"].includes(normalized)) {
1197
+ return normalized === "typescript" ? "TypeScript" : normalized === "tsx" ? "Tsx" : normalized === "javascript" ? "JavaScript" : normalized[0].toUpperCase() + normalized.slice(1);
1198
+ }
1199
+ return registeredDynamicLanguages.has(normalized) ? normalized : null;
1200
+ }
1170
1201
  const ext = path5.extname(filePath).toLowerCase();
1171
1202
  if (ext === ".ts" || ext === ".mts" || ext === ".cts") return "TypeScript";
1172
1203
  if (ext === ".tsx") return "Tsx";
1173
1204
  if (ext === ".js" || ext === ".jsx" || ext === ".mjs" || ext === ".cjs") return "JavaScript";
1205
+ if (ext === ".py" && registeredDynamicLanguages.has("python")) return "python";
1206
+ if (ext === ".go" && registeredDynamicLanguages.has("go")) return "go";
1207
+ if (ext === ".rs" && registeredDynamicLanguages.has("rust")) return "rust";
1208
+ if (ext === ".java" && registeredDynamicLanguages.has("java")) return "java";
1174
1209
  return null;
1175
1210
  }
1176
1211
  function absentPresentInNode(node, absent) {
@@ -1185,12 +1220,12 @@ function absentPresentInNode(node, absent) {
1185
1220
  return text.includes(absent);
1186
1221
  }
1187
1222
  }
1188
- async function runAstPattern(content, filePath, pattern, absent) {
1223
+ async function runAstPattern(content, filePath, pattern, absent, rule, language) {
1189
1224
  const engine = await loadAstEngine();
1190
1225
  if (!engine) return { status: "engine-missing", matches: [] };
1191
- const langName = astLangForPath(filePath);
1226
+ const langName = astLangForPath(filePath, language);
1192
1227
  if (!langName) return { status: "unsupported-language", matches: [] };
1193
- const lang = engine.Lang[langName];
1228
+ const lang = langName in engine.Lang ? engine.Lang[langName] : langName;
1194
1229
  let root;
1195
1230
  try {
1196
1231
  root = engine.parse(lang, content).root();
@@ -1199,7 +1234,9 @@ async function runAstPattern(content, filePath, pattern, absent) {
1199
1234
  }
1200
1235
  let nodes;
1201
1236
  try {
1202
- nodes = root.findAll(pattern);
1237
+ const matcher = rule ? { rule: pattern ? { all: [{ pattern }, rule] } : rule } : pattern;
1238
+ if (!matcher) return { status: "invalid-pattern", matches: [], detail: "missing pattern/rule" };
1239
+ nodes = root.findAll(matcher);
1203
1240
  } catch (err) {
1204
1241
  return { status: "invalid-pattern", matches: [], detail: String(err).slice(0, 200) };
1205
1242
  }
@@ -1216,7 +1253,7 @@ async function runAstPattern(content, filePath, pattern, absent) {
1216
1253
  return { status: "ok", matches };
1217
1254
  }
1218
1255
  async function runAstSensorOnContent(input) {
1219
- const scan = await runAstPattern(input.content, input.filePath, input.pattern, input.absent);
1256
+ const scan = await runAstPattern(input.content, input.filePath, input.pattern, input.absent, input.rule, input.language);
1220
1257
  if (scan.status !== "ok" || !input.addedLines || input.addedLines.size === 0) return scan;
1221
1258
  const added = input.addedLines;
1222
1259
  return {
@@ -1236,7 +1273,9 @@ var ProposeSensorInputSchema = {
1236
1273
  kind: z15.enum(["regex", "ast", "shell", "test"]).default("regex").describe(
1237
1274
  "regex = pattern matched on added diff lines (default). ast = an ast-grep STRUCTURAL pattern (e.g. 'stripe.paymentIntents.create($$$)') matched on the AST of changed files \u2014 comments and strings can never false-positive; `absent` is a sub-pattern that must be missing INSIDE the match (requires the optional @ast-grep/napi engine). shell|test = a COMMAND the gate runs when the diff touches the sensor's paths \u2014 routes the team's own oracle (an existing test, an invariant script) to this lesson. Command sensors only execute where enforcement.runCommandSensors=true."
1238
1275
  ),
1239
- pattern: z15.string().optional().describe("kind=regex: regex matching the FAULTY usage (the risky call/token), e.g. 'stripe\\.paymentIntents\\.create'."),
1276
+ pattern: z15.string().optional().describe("kind=regex: regex matching the faulty usage; kind=ast: optional structural pattern (may be combined with `rule`)."),
1277
+ rule: z15.record(z15.unknown()).optional().describe("kind=ast: full ast-grep Rule object (kind/inside/has/not/all/any/etc.). May be used alone or combined with pattern."),
1278
+ language: z15.string().optional().describe("kind=ast: explicit built-in/dynamic language name for non-standard file extensions."),
1240
1279
  command: z15.string().optional().describe("kind=shell|test: command to execute (e.g. 'npx vitest run tests/payments/refund.spec.ts'). Non-zero exit = the lesson fires."),
1241
1280
  timeout_ms: z15.number().int().positive().optional().describe("kind=shell|test: max runtime before the executor kills the command (default 120000)."),
1242
1281
  absent: z15.string().optional().describe(
@@ -1267,7 +1306,7 @@ async function readPresumedCorrectTargets(root, relPaths) {
1267
1306
  const targets = [];
1268
1307
  for (const rel of relPaths) {
1269
1308
  try {
1270
- const content = execSync(`git show ${JSON.stringify(`HEAD:./${rel}`)}`, {
1309
+ const content = execFileSync("git", ["show", `HEAD:./${rel}`], {
1271
1310
  cwd: root,
1272
1311
  encoding: "utf8",
1273
1312
  maxBuffer: 10 * 1024 * 1024,
@@ -1288,7 +1327,7 @@ async function readPresumedCorrectTargets(root, relPaths) {
1288
1327
  }
1289
1328
  function runCommandForValidation(command, root, timeoutMs = 12e4) {
1290
1329
  try {
1291
- execSync(`bash -c ${JSON.stringify(command)}`, {
1330
+ execFileSync("bash", ["-c", command], {
1292
1331
  cwd: root,
1293
1332
  timeout: timeoutMs,
1294
1333
  maxBuffer: 8 * 1024 * 1024,
@@ -1313,7 +1352,7 @@ function proveRedOnIncident(command, root, redRef, timeoutMs) {
1313
1352
  let added = false;
1314
1353
  try {
1315
1354
  try {
1316
- execSync(`git worktree add --detach ${JSON.stringify(worktree)} ${JSON.stringify(redRef)}`, {
1355
+ execFileSync("git", ["worktree", "add", "--detach", worktree, redRef], {
1317
1356
  cwd: root,
1318
1357
  stdio: ["ignore", "pipe", "pipe"],
1319
1358
  timeout: 6e4
@@ -1340,7 +1379,7 @@ function proveRedOnIncident(command, root, redRef, timeoutMs) {
1340
1379
  } finally {
1341
1380
  if (added) {
1342
1381
  try {
1343
- execSync(`git worktree remove --force ${JSON.stringify(worktree)}`, { cwd: root, stdio: "ignore", timeout: 6e4 });
1382
+ execFileSync("git", ["worktree", "remove", "--force", worktree], { cwd: root, stdio: "ignore", timeout: 6e4 });
1344
1383
  } catch {
1345
1384
  try {
1346
1385
  rmSync(worktree, { recursive: true, force: true });
@@ -1380,13 +1419,13 @@ async function proposeSensor(input, ctx) {
1380
1419
  };
1381
1420
  }
1382
1421
  } else if (kind === "ast") {
1383
- if (!input.pattern?.trim()) {
1422
+ if (!input.pattern?.trim() && !input.rule) {
1384
1423
  return {
1385
1424
  accepted: false,
1386
1425
  memory_id: input.memory_id,
1387
1426
  severity: input.severity,
1388
1427
  reason: "invalid-pattern",
1389
- guidance: "kind=ast requires a `pattern` (an ast-grep structural pattern).",
1428
+ guidance: "kind=ast requires a structural `pattern` or a full ast-grep `rule` object.",
1390
1429
  self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1391
1430
  };
1392
1431
  }
@@ -1407,7 +1446,7 @@ async function proposeSensor(input, ctx) {
1407
1446
  }
1408
1447
  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}.` : "";
1409
1448
  if (kind === "ast") {
1410
- const pattern = input.pattern.trim();
1449
+ const pattern = input.pattern?.trim();
1411
1450
  if (!await astEngineAvailable() && input.severity === "block") {
1412
1451
  return {
1413
1452
  accepted: false,
@@ -1418,7 +1457,7 @@ async function proposeSensor(input, ctx) {
1418
1457
  self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1419
1458
  };
1420
1459
  }
1421
- const brittleAst = sensorPatternBrittleness(pattern);
1460
+ const brittleAst = pattern ? sensorPatternBrittleness(pattern) : null;
1422
1461
  if (brittleAst && input.severity === "block") {
1423
1462
  return {
1424
1463
  accepted: false,
@@ -1433,7 +1472,7 @@ async function proposeSensor(input, ctx) {
1433
1472
  const currentTargetsAst = await readPresumedCorrectTargets(ctx.paths.root, anchorPathsAst);
1434
1473
  const firedOnAst = [];
1435
1474
  for (const target of currentTargetsAst) {
1436
- const scan = await runAstSensorOnContent({ pattern, absent: input.absent, content: target.content, filePath: target.path });
1475
+ const scan = await runAstSensorOnContent({ pattern, rule: input.rule, language: input.language, absent: input.absent, content: target.content, filePath: target.path });
1437
1476
  if (scan.status === "invalid-pattern") {
1438
1477
  return {
1439
1478
  accepted: false,
@@ -1462,10 +1501,10 @@ async function proposeSensor(input, ctx) {
1462
1501
  ];
1463
1502
  let firesOnBadAst = null;
1464
1503
  if (badExamplesAst.length > 0 && await astEngineAvailable()) {
1465
- const exampleLang = anchorPathsAst.find((p) => astLangForPath(p) !== null) ?? "example.tsx";
1504
+ const exampleLang = anchorPathsAst.find((p) => astLangForPath(p, input.language) !== null) ?? "example.tsx";
1466
1505
  firesOnBadAst = false;
1467
1506
  for (const example of badExamplesAst) {
1468
- const scan = await runAstSensorOnContent({ pattern, absent: input.absent, content: example, filePath: exampleLang });
1507
+ const scan = await runAstSensorOnContent({ pattern, rule: input.rule, language: input.language, absent: input.absent, content: example, filePath: exampleLang });
1469
1508
  if (scan.status === "ok" && scan.matches.length > 0) {
1470
1509
  firesOnBadAst = true;
1471
1510
  break;
@@ -1484,10 +1523,12 @@ async function proposeSensor(input, ctx) {
1484
1523
  }
1485
1524
  const sensorAst = {
1486
1525
  kind: "ast",
1487
- pattern,
1526
+ ...pattern ? { pattern } : {},
1527
+ ...input.rule ? { rule: input.rule } : {},
1528
+ ...input.language ? { language: input.language } : {},
1488
1529
  ...input.absent ? { absent: input.absent } : {},
1489
1530
  paths: anchorPathsAst,
1490
- message: input.message?.trim() || deriveMessage(found.memory.body, pattern, input.absent),
1531
+ message: input.message?.trim() || deriveMessage(found.memory.body, pattern ?? "AST rule", input.absent),
1491
1532
  ...input.incident?.trim() ? { incident: input.incident.trim() } : {},
1492
1533
  severity: input.severity,
1493
1534
  autogen: false,
@@ -1550,6 +1591,16 @@ ${verdictCmd.detail}`,
1550
1591
  self_check: { silent_on_current: false, fires_on_bad: null, fired_on: anchorPathsCmd }
1551
1592
  };
1552
1593
  }
1594
+ if (input.severity === "block" && !input.red_ref?.trim()) {
1595
+ return {
1596
+ accepted: false,
1597
+ memory_id: input.memory_id,
1598
+ severity: input.severity,
1599
+ reason: "red-required",
1600
+ guidance: "A blocking shell/test sensor requires `red_ref`: the oracle must PASS on the current tree and FAIL on the pre-fix incident state. Pass the incident commit/ref, or propose the sensor at warn severity until RED can be proven.",
1601
+ self_check: { silent_on_current: true, fires_on_bad: null, fired_on: [] }
1602
+ };
1603
+ }
1553
1604
  const sensorCmd = {
1554
1605
  kind,
1555
1606
  command: input.command.trim(),
@@ -1571,7 +1622,7 @@ ${verdictCmd.detail}`,
1571
1622
  accepted: true,
1572
1623
  memory_id: input.memory_id,
1573
1624
  severity: input.severity,
1574
- 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}).`) + (redProven ? " RED proven: the oracle demonstrably FAILS on the incident state (red_ref) \u2014 recorded as red_proven." : " Tip: pass red_ref (the pre-fix commit) to PROVE the oracle catches the incident, not merely that it passes today.") + (pendingTests.length > 0 ? ` Note: the routed test is still a PENDING stub (${pendingTests.join(", ")}) \u2014 it passes on anything; write the assertion to make this oracle real.` : "") + personalScopeNudge,
1625
+ 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}).`) + (redProven ? " RED proven: the oracle demonstrably FAILS on the incident state (red_ref) \u2014 recorded as red_proven." : " This warn-only oracle is not RED-proven; pass red_ref before promoting it to block.") + (pendingTests.length > 0 ? ` Note: the routed test is still a PENDING stub (${pendingTests.join(", ")}) \u2014 it passes on anything; write the assertion to make this oracle real.` : "") + personalScopeNudge,
1575
1626
  self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: redProven ? true : null, fired_on: [] },
1576
1627
  file_path: found.filePath
1577
1628
  };
@@ -1640,8 +1691,10 @@ var MemTriedInputSchema = {
1640
1691
  paths: z16.array(z16.string()).default([]).describe("Anchor file paths this applies to"),
1641
1692
  author: z16.string().optional().describe("Author handle or email"),
1642
1693
  sensor: z16.object({
1643
- kind: z16.enum(["regex", "shell", "test"]).default("regex").describe("regex pattern, or a shell/test COMMAND the gate executes (behaviour bridge)"),
1644
- pattern: z16.string().optional().describe("kind=regex: regex matching the FAULTY usage (added diff lines)"),
1694
+ kind: z16.enum(["regex", "ast", "shell", "test"]).default("regex").describe("regex/AST pattern, or a shell/test command"),
1695
+ pattern: z16.string().optional().describe("kind=regex|ast: pattern matching the faulty usage"),
1696
+ rule: z16.record(z16.unknown()).optional().describe("kind=ast: full ast-grep Rule object"),
1697
+ language: z16.string().optional().describe("kind=ast: explicit built-in/dynamic language"),
1645
1698
  command: z16.string().optional().describe("kind=shell|test: command the gate runs when the diff touches the sensor's paths (non-zero exit = lesson fires)"),
1646
1699
  timeout_ms: z16.number().int().positive().optional().describe("kind=shell|test: max runtime (default 120000)"),
1647
1700
  absent: z16.string().optional().describe("kind=regex: regex marking CORRECT usage nearby \u2014 excludes it from firing"),
@@ -1691,6 +1744,8 @@ async function memTried(input, ctx) {
1691
1744
  memory_id: frontmatter.id,
1692
1745
  kind: input.sensor.kind ?? "regex",
1693
1746
  pattern: input.sensor.pattern,
1747
+ rule: input.sensor.rule,
1748
+ language: input.sensor.language,
1694
1749
  command: input.sensor.command,
1695
1750
  timeout_ms: input.sensor.timeout_ms,
1696
1751
  absent: input.sensor.absent,
@@ -1985,7 +2040,7 @@ import {
1985
2040
  import { mkdir as mkdir6, writeFile as writeFile12, rm } from "fs/promises";
1986
2041
  import { existsSync as existsSync19 } from "fs";
1987
2042
  import path10 from "path";
1988
- import { execSync as execSync2 } from "child_process";
2043
+ import { execSync } from "child_process";
1989
2044
  function pendingDistillPath(ctx) {
1990
2045
  return path10.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
1991
2046
  }
@@ -2028,7 +2083,7 @@ var SessionTracker = class {
2028
2083
  }
2029
2084
  let gitDiff;
2030
2085
  try {
2031
- const raw = execSync2("git diff HEAD", {
2086
+ const raw = execSync("git diff HEAD", {
2032
2087
  cwd: this.ctx.paths.root,
2033
2088
  timeout: 5e3,
2034
2089
  encoding: "utf8",
@@ -2060,7 +2115,7 @@ var SessionTracker = class {
2060
2115
  const triedThreads = this.events.filter((e) => e.tool === "mem_tried").map((e) => e.summary ?? "").filter(Boolean);
2061
2116
  let diffStat;
2062
2117
  try {
2063
- diffStat = execSync2("git diff --stat HEAD", {
2118
+ diffStat = execSync("git diff --stat HEAD", {
2064
2119
  cwd: this.ctx.paths.root,
2065
2120
  timeout: 5e3,
2066
2121
  encoding: "utf8",
@@ -2472,6 +2527,8 @@ var GetBriefingInputSchema = {
2472
2527
  ),
2473
2528
  include_stale: z20.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
2474
2529
  track: z20.boolean().default(true).describe("Increment read_count on returned memories"),
2530
+ memory_scopes: z20.array(z20.enum(["personal", "team", "module", "shared"])).optional().describe("Restrict the candidate corpus to selected scopes. Omit to include every scope."),
2531
+ deterministic: z20.boolean().optional().describe("Ignore machine-local usage/impact signals so repeated evaluations rank the shared corpus reproducibly."),
2475
2532
  format: z20.enum(["full", "compact", "actions"]).default("full").describe(
2476
2533
  "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."
2477
2534
  ),
@@ -2500,9 +2557,11 @@ async function getBriefing(input, ctx) {
2500
2557
  let searchMode = "literal";
2501
2558
  let usage = { version: 1, updated_at: "", by_id: {} };
2502
2559
  let byId = /* @__PURE__ */ new Map();
2560
+ const allowedScopes = input.memory_scopes ? new Set(input.memory_scopes) : null;
2561
+ const scopeAllowed = (loaded) => allowedScopes === null || allowedScopes.has(loaded.memory.frontmatter.scope);
2503
2562
  let lastSession;
2504
2563
  if (existsSync22(ctx.paths.memoriesDir)) {
2505
- const allLoaded = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
2564
+ const allLoaded = (await loadMemoriesFromDir17(ctx.paths.memoriesDir)).filter(scopeAllowed);
2506
2565
  const recaps = allLoaded.filter(({ memory }) => memory.frontmatter.type === "session_recap").sort(
2507
2566
  (a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
2508
2567
  );
@@ -2535,10 +2594,11 @@ async function getBriefing(input, ctx) {
2535
2594
  if (memoryHasExcludedTag(memory.frontmatter, excludeTags)) return false;
2536
2595
  return true;
2537
2596
  });
2538
- usage = await loadUsageIndex8(ctx.paths);
2597
+ usage = input.deterministic ? { version: 1, updated_at: "", by_id: {} } : await loadUsageIndex8(ctx.paths);
2539
2598
  byId = new Map(allMemories.map((m) => [m.memory.frontmatter.id, m]));
2540
- const semanticHits = input.task && input.semantic ? await trySemanticHits(ctx, input.task, allMemories.length * 2) : null;
2541
- if (input.task && input.semantic) {
2599
+ const semanticEnabled = input.semantic && input.deterministic !== true;
2600
+ const semanticHits = input.task && semanticEnabled ? await trySemanticHits(ctx, input.task, allMemories.length * 2) : null;
2601
+ if (input.task && semanticEnabled) {
2542
2602
  searchMode = semanticHits ? "semantic" : "literal_fallback";
2543
2603
  }
2544
2604
  const seen = /* @__PURE__ */ new Map();
@@ -2874,7 +2934,7 @@ ${m.content}`).join("\n\n---\n\n"),
2874
2934
  }
2875
2935
  }
2876
2936
  if (existsSync22(ctx.paths.memoriesDir)) {
2877
- const allMems = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
2937
+ const allMems = (await loadMemoriesFromDir17(ctx.paths.memoriesDir)).filter(scopeAllowed);
2878
2938
  for (const { memory } of allMems) {
2879
2939
  const fm = memory.frontmatter;
2880
2940
  if (!fm.requires_human_approval) continue;
@@ -2931,7 +2991,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
2931
2991
  pcRaw = await readFile7(ctx.paths.projectContext, "utf8");
2932
2992
  } catch {
2933
2993
  }
2934
- const allForBootstrap = existsSync22(ctx.paths.memoriesDir) ? await loadMemoriesFromDir17(ctx.paths.memoriesDir) : [];
2994
+ const allForBootstrap = existsSync22(ctx.paths.memoriesDir) ? (await loadMemoriesFromDir17(ctx.paths.memoriesDir)).filter(scopeAllowed) : [];
2935
2995
  const cmForBootstrap = await loadCodeMap(ctx.paths);
2936
2996
  let existingModules = [];
2937
2997
  try {
@@ -2942,7 +3002,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
2942
3002
  const bootstrap = assessBootstrapState({
2943
3003
  projectContextRaw: pcRaw,
2944
3004
  memories: allForBootstrap,
2945
- codeFiles: cmForBootstrap ? Object.keys(cmForBootstrap.files) : [],
3005
+ codeFiles: cmForBootstrap ? Object.keys(cmForBootstrap.files).filter((file) => existsSync22(path13.join(ctx.paths.root, file))) : [],
2946
3006
  existingModules
2947
3007
  });
2948
3008
  if (bootstrap.state !== "ready" && bootstrap.metrics.mainAreas > 0) {
@@ -3133,7 +3193,7 @@ function oneLine(value) {
3133
3193
  return value.replace(/\s+/g, " ").replace(/"/g, '\\"').trim().slice(0, 120);
3134
3194
  }
3135
3195
  function serverVersion() {
3136
- return true ? "0.42.1" : "dev";
3196
+ return true ? "0.44.0" : "dev";
3137
3197
  }
3138
3198
 
3139
3199
  // src/tools/code-map.ts
@@ -3419,7 +3479,8 @@ var AntiPatternsCheckInputSchema = {
3419
3479
  ),
3420
3480
  min_semantic_score: z26.number().min(0).max(1).default(0.45).describe(
3421
3481
  "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."
3422
- )
3482
+ ),
3483
+ track: z26.boolean().default(true).describe("Record real prevention outcomes. Set false for eval/selftest probes so synthetic cases never inflate ROI.")
3423
3484
  };
3424
3485
  function tokenizeDiffForLiteral(diff) {
3425
3486
  const lines = diff.split("\n");
@@ -3612,7 +3673,9 @@ async function antiPatternsCheck(input, ctx) {
3612
3673
  }).slice(0, input.limit);
3613
3674
  const isHardBlockCatch = (w) => w.reasons.includes("sensor");
3614
3675
  const strongCatches = warnings.filter(isHardBlockCatch);
3615
- await recordPreventionHits(ctx.paths, strongCatches.map((w) => w.id), "anti-pattern");
3676
+ if (input.track !== false) {
3677
+ await recordPreventionHits(ctx.paths, strongCatches.map((w) => w.id), "anti-pattern");
3678
+ }
3616
3679
  return {
3617
3680
  scanned: negative.length,
3618
3681
  warnings
@@ -4306,6 +4369,7 @@ ${template}\`\`\`
4306
4369
  // src/prompts/bootstrap-repo.ts
4307
4370
  import { readFile as readFile8, readdir as readdir5 } from "fs/promises";
4308
4371
  import { existsSync as existsSync29 } from "fs";
4372
+ import path14 from "path";
4309
4373
  import {
4310
4374
  assessBootstrapState as assessBootstrapState2,
4311
4375
  loadCodeMap as loadCodeMap4,
@@ -4333,7 +4397,7 @@ async function currentAssessment(ctx) {
4333
4397
  return assessBootstrapState2({
4334
4398
  projectContextRaw,
4335
4399
  memories,
4336
- codeFiles: codeMap ? Object.keys(codeMap.files) : [],
4400
+ codeFiles: codeMap ? Object.keys(codeMap.files).filter((file) => existsSync29(path14.join(ctx.paths.root, file))) : [],
4337
4401
  existingModules
4338
4402
  });
4339
4403
  }
@@ -4393,13 +4457,14 @@ Main code areas detected: ${areas}
4393
4457
  import { z as z35 } from "zod";
4394
4458
  var PostTaskArgsSchema = {
4395
4459
  task_summary: z35.string().optional().describe("One sentence describing what you just did"),
4396
- files_touched: z35.array(z35.string()).optional().describe("Files you created or modified during the task")
4460
+ files_touched: z35.string().optional().describe("Files you created or modified during the task, as CSV or a JSON array string")
4397
4461
  };
4398
4462
  function postTaskPrompt(args, ctx) {
4399
4463
  const taskLine = args.task_summary ? `
4400
4464
  Task just completed: **${args.task_summary}**` : "";
4401
- const filesLine = args.files_touched && args.files_touched.length > 0 ? `
4402
- Files touched: ${args.files_touched.map((f) => `\`${f}\``).join(", ")}` : "";
4465
+ const filesTouched = parsePromptFilesTouched(args.files_touched);
4466
+ const filesLine = filesTouched.length > 0 ? `
4467
+ Files touched: ${filesTouched.map((f) => `\`${f}\``).join(", ")}` : "";
4403
4468
  const text = `You have just finished a task. Before closing this session, take 60 seconds to capture what you learned.
4404
4469
  ${taskLine}${filesLine}
4405
4470
 
@@ -4488,6 +4553,20 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
4488
4553
  ]
4489
4554
  };
4490
4555
  }
4556
+ function parsePromptFilesTouched(input) {
4557
+ const raw = input?.trim();
4558
+ if (!raw) return [];
4559
+ if (raw.startsWith("[")) {
4560
+ try {
4561
+ const parsed2 = JSON.parse(raw);
4562
+ if (Array.isArray(parsed2)) {
4563
+ return parsed2.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean);
4564
+ }
4565
+ } catch {
4566
+ }
4567
+ }
4568
+ return raw.split(",").map((value) => value.trim()).filter(Boolean);
4569
+ }
4491
4570
 
4492
4571
  // src/prompts/import-docs.ts
4493
4572
  import { z as z36 } from "zod";
@@ -4561,7 +4640,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
4561
4640
  // src/server.ts
4562
4641
  import { hasRecentBriefingMarker, loadConfigSync } from "@hivelore/core";
4563
4642
  var SERVER_NAME = "hivelore";
4564
- var SERVER_VERSION = "0.42.1";
4643
+ var SERVER_VERSION = "0.44.0";
4565
4644
  function jsonResult(data) {
4566
4645
  return {
4567
4646
  content: [
@@ -5500,11 +5579,22 @@ function printHaiveMcpVersion() {
5500
5579
  }
5501
5580
  async function runHaiveMcpStdio(options) {
5502
5581
  const { server, context } = createHaiveServer({ root: options.root, env: process.env });
5582
+ await writeMcpRuntimeMarker(context).catch(() => {
5583
+ });
5503
5584
  console.error(
5504
5585
  `[haive-mcp] starting server v${SERVER_VERSION} (project root: ${context.paths.root})`
5505
5586
  );
5506
5587
  await server.connect(new StdioServerTransport());
5507
5588
  }
5589
+ async function writeMcpRuntimeMarker(context) {
5590
+ await mkdir8(context.paths.runtimeDir, { recursive: true });
5591
+ await writeFile15(path15.join(context.paths.runtimeDir, "mcp-server.json"), JSON.stringify({
5592
+ version: SERVER_VERSION,
5593
+ pid: process.pid,
5594
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
5595
+ command: "hivelore mcp --stdio"
5596
+ }, null, 2), "utf8");
5597
+ }
5508
5598
 
5509
5599
  // src/index.ts
5510
5600
  var parsed = parseMcpCliArgs(process.argv);