@hivelore/mcp 0.39.1 → 0.42.1

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
@@ -1124,7 +1124,7 @@ async function memApprove(input, ctx) {
1124
1124
  // src/tools/mem-tried.ts
1125
1125
  import { mkdir as mkdir3, writeFile as writeFile9 } from "fs/promises";
1126
1126
  import { existsSync as existsSync16 } from "fs";
1127
- import path6 from "path";
1127
+ import path7 from "path";
1128
1128
  import {
1129
1129
  buildFrontmatter as buildFrontmatter2,
1130
1130
  memoryFilePath as memoryFilePath2,
@@ -1136,21 +1136,105 @@ import { z as z16 } from "zod";
1136
1136
  // src/tools/propose-sensor.ts
1137
1137
  import { execSync } from "child_process";
1138
1138
  import { readFile as readFile3, writeFile as writeFile8 } from "fs/promises";
1139
- import { existsSync as existsSync15 } from "fs";
1140
- import path5 from "path";
1139
+ import { existsSync as existsSync15, rmSync, symlinkSync } from "fs";
1140
+ import os from "os";
1141
+ import path6 from "path";
1141
1142
  import {
1142
1143
  extractSensorExamples,
1143
1144
  extractTestFilePathsFromCommand,
1144
1145
  hasPendingTestMarker,
1145
1146
  judgeProposedSensor,
1146
1147
  loadMemoriesFromDir as loadMemoriesFromDir13,
1148
+ scrubbedCommandEnv,
1149
+ sensorPatternBrittleness,
1147
1150
  serializeMemory as serializeMemory7
1148
1151
  } from "@hivelore/core";
1149
1152
  import { z as z15 } from "zod";
1153
+
1154
+ // src/ast-sensors.ts
1155
+ import path5 from "path";
1156
+ var cachedEngine;
1157
+ async function loadAstEngine() {
1158
+ if (cachedEngine !== void 0) return cachedEngine;
1159
+ try {
1160
+ cachedEngine = await import("@ast-grep/napi");
1161
+ } catch {
1162
+ cachedEngine = null;
1163
+ }
1164
+ return cachedEngine;
1165
+ }
1166
+ async function astEngineAvailable() {
1167
+ return await loadAstEngine() !== null;
1168
+ }
1169
+ function astLangForPath(filePath) {
1170
+ const ext = path5.extname(filePath).toLowerCase();
1171
+ if (ext === ".ts" || ext === ".mts" || ext === ".cts") return "TypeScript";
1172
+ if (ext === ".tsx") return "Tsx";
1173
+ if (ext === ".js" || ext === ".jsx" || ext === ".mjs" || ext === ".cjs") return "JavaScript";
1174
+ return null;
1175
+ }
1176
+ function absentPresentInNode(node, absent) {
1177
+ try {
1178
+ if (node.find(absent)) return true;
1179
+ } catch {
1180
+ }
1181
+ const text = node.text();
1182
+ try {
1183
+ return new RegExp(absent).test(text);
1184
+ } catch {
1185
+ return text.includes(absent);
1186
+ }
1187
+ }
1188
+ async function runAstPattern(content, filePath, pattern, absent) {
1189
+ const engine = await loadAstEngine();
1190
+ if (!engine) return { status: "engine-missing", matches: [] };
1191
+ const langName = astLangForPath(filePath);
1192
+ if (!langName) return { status: "unsupported-language", matches: [] };
1193
+ const lang = engine.Lang[langName];
1194
+ let root;
1195
+ try {
1196
+ root = engine.parse(lang, content).root();
1197
+ } catch (err) {
1198
+ return { status: "parse-error", matches: [], detail: String(err).slice(0, 200) };
1199
+ }
1200
+ let nodes;
1201
+ try {
1202
+ nodes = root.findAll(pattern);
1203
+ } catch (err) {
1204
+ return { status: "invalid-pattern", matches: [], detail: String(err).slice(0, 200) };
1205
+ }
1206
+ const matches = [];
1207
+ for (const node of nodes) {
1208
+ if (absent && absentPresentInNode(node, absent)) continue;
1209
+ const range = node.range();
1210
+ matches.push({
1211
+ startLine: range.start.line + 1,
1212
+ endLine: range.end.line + 1,
1213
+ text: node.text().trim().slice(0, 200)
1214
+ });
1215
+ }
1216
+ return { status: "ok", matches };
1217
+ }
1218
+ async function runAstSensorOnContent(input) {
1219
+ const scan = await runAstPattern(input.content, input.filePath, input.pattern, input.absent);
1220
+ if (scan.status !== "ok" || !input.addedLines || input.addedLines.size === 0) return scan;
1221
+ const added = input.addedLines;
1222
+ return {
1223
+ status: "ok",
1224
+ matches: scan.matches.filter((m) => {
1225
+ for (let line = m.startLine; line <= m.endLine; line++) {
1226
+ if (added.has(line)) return true;
1227
+ }
1228
+ return false;
1229
+ })
1230
+ };
1231
+ }
1232
+
1233
+ // src/tools/propose-sensor.ts
1150
1234
  var ProposeSensorInputSchema = {
1151
1235
  memory_id: z15.string().min(1).describe("Id of the gotcha/attempt memory this sensor protects."),
1152
- kind: z15.enum(["regex", "shell", "test"]).default("regex").describe(
1153
- "regex = pattern matched on added diff lines (default). 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."
1236
+ kind: z15.enum(["regex", "ast", "shell", "test"]).default("regex").describe(
1237
+ "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."
1154
1238
  ),
1155
1239
  pattern: z15.string().optional().describe("kind=regex: regex matching the FAULTY usage (the risky call/token), e.g. 'stripe\\.paymentIntents\\.create'."),
1156
1240
  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."),
@@ -1164,6 +1248,9 @@ var ProposeSensorInputSchema = {
1164
1248
  incident: z15.string().optional().describe(
1165
1249
  "Provenance: the real incident this sensor guards \u2014 a ticket/prod ref ('prod #442', 'INC-1029', '2026-06 refund overcharge'). Turns 'a test failed' into 'this reproduces the incident the test exists to prevent'. Surfaced in the block message and the prevention receipt. Strongly recommended for command/test sensors routed from a post-incident test."
1166
1250
  ),
1251
+ red_ref: z15.string().optional().describe(
1252
+ "kind=shell|test: prove the oracle actually catches the incident. A git ref (commit/branch) of the PRE-FIX state; validation replays it in a scratch worktree and requires the command to FAIL there (RED) in addition to passing on the current tree (GREEN). On success the sensor records red_proven: true \u2014 'the test demonstrably catches the incident', shown in the prevention receipt."
1253
+ ),
1167
1254
  flags: z15.string().optional().describe("Optional regex flags (e.g. 'i' for case-insensitive)."),
1168
1255
  paths: z15.array(z15.string()).default([]).describe("Override scope paths. Defaults to the memory's anchor paths.")
1169
1256
  };
@@ -1190,7 +1277,7 @@ async function readPresumedCorrectTargets(root, relPaths) {
1190
1277
  continue;
1191
1278
  } catch {
1192
1279
  }
1193
- const abs = path5.resolve(root, rel);
1280
+ const abs = path6.resolve(root, rel);
1194
1281
  if (!existsSync15(abs)) continue;
1195
1282
  try {
1196
1283
  targets.push({ path: rel, content: await readFile3(abs, "utf8") });
@@ -1205,7 +1292,9 @@ function runCommandForValidation(command, root, timeoutMs = 12e4) {
1205
1292
  cwd: root,
1206
1293
  timeout: timeoutMs,
1207
1294
  maxBuffer: 8 * 1024 * 1024,
1208
- stdio: ["ignore", "pipe", "pipe"]
1295
+ stdio: ["ignore", "pipe", "pipe"],
1296
+ // Same containment as the gate executor: an oracle gets a test-runner env, not credentials.
1297
+ env: { ...scrubbedCommandEnv(process.env), HIVELORE_SENSOR: "validation" }
1209
1298
  });
1210
1299
  return { status: "passed", detail: "exit 0" };
1211
1300
  } catch (err) {
@@ -1219,6 +1308,48 @@ ${e.stderr?.toString() ?? ""}`.split("\n").filter(Boolean).slice(-8).join("\n");
1219
1308
  return { status: "failed", detail: out || `exit ${e.status ?? "?"}` };
1220
1309
  }
1221
1310
  }
1311
+ function proveRedOnIncident(command, root, redRef, timeoutMs) {
1312
+ const worktree = path6.join(os.tmpdir(), `hivelore-red-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
1313
+ let added = false;
1314
+ try {
1315
+ try {
1316
+ execSync(`git worktree add --detach ${JSON.stringify(worktree)} ${JSON.stringify(redRef)}`, {
1317
+ cwd: root,
1318
+ stdio: ["ignore", "pipe", "pipe"],
1319
+ timeout: 6e4
1320
+ });
1321
+ added = true;
1322
+ } catch (err) {
1323
+ const e = err;
1324
+ return { proven: false, reason: "red-ref-invalid", detail: (e.stderr?.toString() ?? String(err)).slice(0, 300) };
1325
+ }
1326
+ const mainModules = path6.join(root, "node_modules");
1327
+ const wtModules = path6.join(worktree, "node_modules");
1328
+ if (existsSync15(mainModules) && !existsSync15(wtModules)) {
1329
+ try {
1330
+ symlinkSync(mainModules, wtModules, "dir");
1331
+ } catch {
1332
+ }
1333
+ }
1334
+ const run = runCommandForValidation(command, worktree, timeoutMs);
1335
+ if (run.status === "failed") return { proven: true, detail: run.detail };
1336
+ if (run.status === "passed") {
1337
+ return { proven: false, reason: "red-not-proven", detail: "oracle PASSED on the incident state \u2014 it does not catch the incident" };
1338
+ }
1339
+ return { proven: false, reason: "red-unrunnable", detail: run.detail };
1340
+ } finally {
1341
+ if (added) {
1342
+ try {
1343
+ execSync(`git worktree remove --force ${JSON.stringify(worktree)}`, { cwd: root, stdio: "ignore", timeout: 6e4 });
1344
+ } catch {
1345
+ try {
1346
+ rmSync(worktree, { recursive: true, force: true });
1347
+ } catch {
1348
+ }
1349
+ }
1350
+ }
1351
+ }
1352
+ }
1222
1353
  async function proposeSensor(input, ctx) {
1223
1354
  if (!existsSync15(ctx.paths.memoriesDir)) {
1224
1355
  throw new Error(`No .ai/memories at ${ctx.paths.root}. Run 'hivelore init' first.`);
@@ -1248,6 +1379,17 @@ async function proposeSensor(input, ctx) {
1248
1379
  self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1249
1380
  };
1250
1381
  }
1382
+ } else if (kind === "ast") {
1383
+ if (!input.pattern?.trim()) {
1384
+ return {
1385
+ accepted: false,
1386
+ memory_id: input.memory_id,
1387
+ severity: input.severity,
1388
+ reason: "invalid-pattern",
1389
+ guidance: "kind=ast requires a `pattern` (an ast-grep structural pattern).",
1390
+ self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1391
+ };
1392
+ }
1251
1393
  } else if (!input.command?.trim()) {
1252
1394
  return {
1253
1395
  accepted: false,
@@ -1264,12 +1406,109 @@ async function proposeSensor(input, ctx) {
1264
1406
  throw new Error(`No memory found with id ${input.memory_id}`);
1265
1407
  }
1266
1408
  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}.` : "";
1267
- if (kind !== "regex") {
1268
- const referencedTests = extractTestFilePathsFromCommand(input.command.trim()).filter((rel) => existsSync15(path5.resolve(ctx.paths.root, rel)));
1409
+ if (kind === "ast") {
1410
+ const pattern = input.pattern.trim();
1411
+ if (!await astEngineAvailable() && input.severity === "block") {
1412
+ return {
1413
+ accepted: false,
1414
+ memory_id: input.memory_id,
1415
+ severity: input.severity,
1416
+ reason: "ast-engine-missing",
1417
+ guidance: "The optional AST engine is not installed, so this proposal cannot be validated \u2014 a block sensor is only trusted after proof. Install it (`npm i -g @ast-grep/napi`, or add it to the repo) and re-propose.",
1418
+ self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1419
+ };
1420
+ }
1421
+ const brittleAst = sensorPatternBrittleness(pattern);
1422
+ if (brittleAst && input.severity === "block") {
1423
+ return {
1424
+ accepted: false,
1425
+ memory_id: input.memory_id,
1426
+ severity: input.severity,
1427
+ reason: "brittle",
1428
+ guidance: `The pattern is brittle (${brittleAst}). Use a durable structural pattern, then re-propose.`,
1429
+ self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1430
+ };
1431
+ }
1432
+ const anchorPathsAst = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
1433
+ const currentTargetsAst = await readPresumedCorrectTargets(ctx.paths.root, anchorPathsAst);
1434
+ const firedOnAst = [];
1435
+ for (const target of currentTargetsAst) {
1436
+ const scan = await runAstSensorOnContent({ pattern, absent: input.absent, content: target.content, filePath: target.path });
1437
+ if (scan.status === "invalid-pattern") {
1438
+ return {
1439
+ accepted: false,
1440
+ memory_id: input.memory_id,
1441
+ severity: input.severity,
1442
+ reason: "invalid-pattern",
1443
+ guidance: `The ast-grep pattern is invalid: ${scan.detail ?? "unparseable"}. Fix it and re-propose.`,
1444
+ self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1445
+ };
1446
+ }
1447
+ if (scan.status === "ok" && scan.matches.length > 0) firedOnAst.push(target.path);
1448
+ }
1449
+ if (firedOnAst.length > 0 && input.severity === "block") {
1450
+ return {
1451
+ accepted: false,
1452
+ memory_id: input.memory_id,
1453
+ severity: input.severity,
1454
+ reason: "fires-on-current",
1455
+ guidance: `The pattern matches the CURRENT (correct) code in ${firedOnAst.join(", ")}. Add/tighten the 'absent' companion sub-pattern so correct usage is excluded, then re-propose.`,
1456
+ self_check: { silent_on_current: false, fires_on_bad: null, fired_on: firedOnAst }
1457
+ };
1458
+ }
1459
+ const badExamplesAst = [
1460
+ ...input.bad_example ? [input.bad_example] : [],
1461
+ ...extractSensorExamples(found.memory.body)
1462
+ ];
1463
+ let firesOnBadAst = null;
1464
+ if (badExamplesAst.length > 0 && await astEngineAvailable()) {
1465
+ const exampleLang = anchorPathsAst.find((p) => astLangForPath(p) !== null) ?? "example.tsx";
1466
+ firesOnBadAst = false;
1467
+ for (const example of badExamplesAst) {
1468
+ const scan = await runAstSensorOnContent({ pattern, absent: input.absent, content: example, filePath: exampleLang });
1469
+ if (scan.status === "ok" && scan.matches.length > 0) {
1470
+ firesOnBadAst = true;
1471
+ break;
1472
+ }
1473
+ }
1474
+ }
1475
+ if (firesOnBadAst === false && input.severity === "block") {
1476
+ return {
1477
+ accepted: false,
1478
+ memory_id: input.memory_id,
1479
+ severity: input.severity,
1480
+ reason: "missed-bad-example",
1481
+ guidance: "The pattern did not match the bad example structurally, so it won't catch the mistake. Adjust it, then re-propose.",
1482
+ self_check: { silent_on_current: firedOnAst.length === 0, fires_on_bad: false, fired_on: [] }
1483
+ };
1484
+ }
1485
+ const sensorAst = {
1486
+ kind: "ast",
1487
+ pattern,
1488
+ ...input.absent ? { absent: input.absent } : {},
1489
+ paths: anchorPathsAst,
1490
+ message: input.message?.trim() || deriveMessage(found.memory.body, pattern, input.absent),
1491
+ ...input.incident?.trim() ? { incident: input.incident.trim() } : {},
1492
+ severity: input.severity,
1493
+ autogen: false,
1494
+ last_fired: null
1495
+ };
1496
+ await writeFile8(found.filePath, serializeMemory7({ frontmatter: { ...found.memory.frontmatter, sensor: sensorAst }, body: found.memory.body }), "utf8");
1497
+ return {
1498
+ accepted: true,
1499
+ memory_id: input.memory_id,
1500
+ severity: input.severity,
1501
+ guidance: "Structural sensor accepted \u2014 it matches the AST, so comments/strings can never false-positive." + (await astEngineAvailable() ? "" : " Note: the AST engine is not installed here; the sensor is UNRUNNABLE (warn-only) until @ast-grep/napi is available.") + personalScopeNudge,
1502
+ self_check: { silent_on_current: firedOnAst.length === 0, fires_on_bad: firesOnBadAst, fired_on: firedOnAst },
1503
+ file_path: found.filePath
1504
+ };
1505
+ }
1506
+ if (kind === "shell" || kind === "test") {
1507
+ const referencedTests = extractTestFilePathsFromCommand(input.command.trim()).filter((rel) => existsSync15(path6.resolve(ctx.paths.root, rel)));
1269
1508
  const pendingTests = [];
1270
1509
  for (const rel of referencedTests) {
1271
1510
  try {
1272
- if (hasPendingTestMarker(await readFile3(path5.resolve(ctx.paths.root, rel), "utf8"))) pendingTests.push(rel);
1511
+ if (hasPendingTestMarker(await readFile3(path6.resolve(ctx.paths.root, rel), "utf8"))) pendingTests.push(rel);
1273
1512
  } catch {
1274
1513
  }
1275
1514
  }
@@ -1285,6 +1524,21 @@ async function proposeSensor(input, ctx) {
1285
1524
  }
1286
1525
  const verdictCmd = runCommandForValidation(input.command.trim(), ctx.paths.root, input.timeout_ms);
1287
1526
  const anchorPathsCmd = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
1527
+ let redProven = false;
1528
+ if (input.red_ref?.trim()) {
1529
+ const red = proveRedOnIncident(input.command.trim(), ctx.paths.root, input.red_ref.trim(), input.timeout_ms);
1530
+ if (!red.proven && input.severity === "block") {
1531
+ return {
1532
+ accepted: false,
1533
+ memory_id: input.memory_id,
1534
+ severity: input.severity,
1535
+ reason: red.reason ?? "red-not-proven",
1536
+ guidance: red.reason === "red-ref-invalid" ? `red_ref could not be checked out (${red.detail}). Pass a valid commit/ref of the pre-fix state.` : red.reason === "red-unrunnable" ? `The oracle could not RUN on the incident state (${red.detail}) \u2014 it proves nothing there. Fix the command or drop red_ref.` : "The oracle PASSED on the incident state, so it does not catch the incident it claims to guard. Strengthen the assertion until it goes RED on red_ref, then re-propose. Output: " + red.detail.slice(0, 300),
1537
+ self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: false, fired_on: [] }
1538
+ };
1539
+ }
1540
+ redProven = red.proven;
1541
+ }
1288
1542
  if (verdictCmd.status !== "passed" && input.severity === "block") {
1289
1543
  return {
1290
1544
  accepted: false,
@@ -1303,6 +1557,7 @@ ${verdictCmd.detail}`,
1303
1557
  paths: anchorPathsCmd,
1304
1558
  message: input.message?.trim() || deriveMessage(found.memory.body, input.command.trim(), void 0),
1305
1559
  ...input.incident?.trim() ? { incident: input.incident.trim() } : {},
1560
+ ...redProven ? { red_proven: true } : {},
1306
1561
  severity: input.severity,
1307
1562
  autogen: false,
1308
1563
  last_fired: null
@@ -1316,8 +1571,9 @@ ${verdictCmd.detail}`,
1316
1571
  accepted: true,
1317
1572
  memory_id: input.memory_id,
1318
1573
  severity: input.severity,
1319
- 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}).`) + (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,
1320
- self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: null, fired_on: [] }
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,
1575
+ self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: redProven ? true : null, fired_on: [] },
1576
+ file_path: found.filePath
1321
1577
  };
1322
1578
  }
1323
1579
  const anchorPaths = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
@@ -1392,6 +1648,7 @@ var MemTriedInputSchema = {
1392
1648
  severity: z16.enum(["warn", "block"]).default("block").describe("block = deterministic gate refusal"),
1393
1649
  message: z16.string().optional().describe("Self-correction message shown when the sensor fires"),
1394
1650
  incident: z16.string().optional().describe("Provenance: the incident this sensor guards (e.g. 'prod #442') \u2014 surfaced when it fires and in the receipt"),
1651
+ red_ref: z16.string().optional().describe("kind=shell|test: pre-fix commit/ref \u2014 the oracle must FAIL on it (proves the test catches the incident; records red_proven)"),
1395
1652
  bad_example: z16.string().optional().describe("kind=regex: code snippet the sensor MUST fire on (validation)")
1396
1653
  }).optional().describe(
1397
1654
  "ONE-SHOT loop close: validate and attach a sensor in the same call (equivalent to a follow-up propose_sensor). Validated against HEAD \u2014 silent on current code, fires on the bad example. If rejected, the attempt is still saved and the verdict tells you how to revise."
@@ -1423,7 +1680,7 @@ async function memTried(input, ctx) {
1423
1680
  }
1424
1681
  const body = lines.join("\n") + "\n";
1425
1682
  const file = memoryFilePath2(ctx.paths, frontmatter.scope, frontmatter.id, frontmatter.module);
1426
- await mkdir3(path6.dirname(file), { recursive: true });
1683
+ await mkdir3(path7.dirname(file), { recursive: true });
1427
1684
  if (existsSync16(file)) {
1428
1685
  throw new Error(`Memory already exists at ${file}`);
1429
1686
  }
@@ -1440,6 +1697,7 @@ async function memTried(input, ctx) {
1440
1697
  severity: input.sensor.severity ?? "block",
1441
1698
  message: input.sensor.message,
1442
1699
  incident: input.sensor.incident,
1700
+ red_ref: input.sensor.red_ref,
1443
1701
  bad_example: input.sensor.bad_example,
1444
1702
  flags: void 0,
1445
1703
  paths: []
@@ -1481,7 +1739,7 @@ async function memTried(input, ctx) {
1481
1739
  // src/tools/scaffold-test.ts
1482
1740
  import { existsSync as existsSync17, statSync } from "fs";
1483
1741
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile10 } from "fs/promises";
1484
- import path7 from "path";
1742
+ import path8 from "path";
1485
1743
  import { z as z17 } from "zod";
1486
1744
  import {
1487
1745
  buildProposeCommand,
@@ -1493,17 +1751,17 @@ import {
1493
1751
  } from "@hivelore/core";
1494
1752
  var PY_SIGNALS = ["pyproject.toml", "setup.py", "pytest.ini", "requirements.txt", "tox.ini"];
1495
1753
  async function detectForAnchor(root, rel) {
1496
- let dir = path7.resolve(root, rel);
1754
+ let dir = path8.resolve(root, rel);
1497
1755
  try {
1498
- if (!statSync(dir).isDirectory()) dir = path7.dirname(dir);
1756
+ if (!statSync(dir).isDirectory()) dir = path8.dirname(dir);
1499
1757
  } catch {
1500
- if (path7.extname(dir)) dir = path7.dirname(dir);
1758
+ if (path8.extname(dir)) dir = path8.dirname(dir);
1501
1759
  }
1502
1760
  while (dir.startsWith(root)) {
1503
- const pkgJson = path7.join(dir, "package.json");
1761
+ const pkgJson = path8.join(dir, "package.json");
1504
1762
  const hasPkg = existsSync17(pkgJson);
1505
- const goMod = existsSync17(path7.join(dir, "go.mod"));
1506
- const pySignal = PY_SIGNALS.some((s) => existsSync17(path7.join(dir, s)));
1763
+ const goMod = existsSync17(path8.join(dir, "go.mod"));
1764
+ const pySignal = PY_SIGNALS.some((s) => existsSync17(path8.join(dir, s)));
1507
1765
  if (hasPkg || goMod || pySignal) {
1508
1766
  let pkg = null;
1509
1767
  if (hasPkg) {
@@ -1513,10 +1771,10 @@ async function detectForAnchor(root, rel) {
1513
1771
  pkg = null;
1514
1772
  }
1515
1773
  }
1516
- const baseDir = path7.relative(root, dir).split(path7.sep).join("/");
1774
+ const baseDir = path8.relative(root, dir).split(path8.sep).join("/");
1517
1775
  return { framework: pickTestFramework(pkg, { goMod, pySignal }), baseDir };
1518
1776
  }
1519
- const parent = path7.dirname(dir);
1777
+ const parent = path8.dirname(dir);
1520
1778
  if (parent === dir || dir === root) break;
1521
1779
  dir = parent;
1522
1780
  }
@@ -1574,14 +1832,14 @@ async function scaffoldTest(input, ctx) {
1574
1832
  }
1575
1833
  const results = [];
1576
1834
  for (const scaffold of scaffolds) {
1577
- const abs = path7.isAbsolute(scaffold.relPath) ? scaffold.relPath : path7.resolve(ctx.paths.root, scaffold.relPath);
1835
+ const abs = path8.isAbsolute(scaffold.relPath) ? scaffold.relPath : path8.resolve(ctx.paths.root, scaffold.relPath);
1578
1836
  let written = false;
1579
1837
  let alreadyExists = false;
1580
1838
  if (input.write) {
1581
1839
  if (existsSync17(abs)) {
1582
1840
  alreadyExists = true;
1583
1841
  } else {
1584
- await mkdir4(path7.dirname(abs), { recursive: true });
1842
+ await mkdir4(path8.dirname(abs), { recursive: true });
1585
1843
  await writeFile10(abs, scaffold.content, "utf8");
1586
1844
  written = true;
1587
1845
  }
@@ -1615,7 +1873,7 @@ async function scaffoldTest(input, ctx) {
1615
1873
  // src/tools/ingest-findings.ts
1616
1874
  import { existsSync as existsSync18 } from "fs";
1617
1875
  import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile11 } from "fs/promises";
1618
- import path8 from "path";
1876
+ import path9 from "path";
1619
1877
  import {
1620
1878
  draftsFromFindings,
1621
1879
  filterNewDrafts,
@@ -1646,7 +1904,7 @@ async function ingestFindings(input, ctx) {
1646
1904
  if (input.report && input.report.trim()) {
1647
1905
  raw = input.report;
1648
1906
  } else if (input.report_path) {
1649
- const file = path8.resolve(ctx.paths.root, input.report_path);
1907
+ const file = path9.resolve(ctx.paths.root, input.report_path);
1650
1908
  if (!existsSync18(file)) throw new Error(`Report file not found: ${file}`);
1651
1909
  raw = await readFile5(file, "utf8");
1652
1910
  } else {
@@ -1700,7 +1958,7 @@ async function writeDraft(ctx, draft) {
1700
1958
  draft.frontmatter.id,
1701
1959
  draft.frontmatter.module
1702
1960
  );
1703
- await mkdir5(path8.dirname(file), { recursive: true });
1961
+ await mkdir5(path9.dirname(file), { recursive: true });
1704
1962
  await writeFile11(file, serializeMemory9({ frontmatter: draft.frontmatter, body: draft.body }), "utf8");
1705
1963
  return file;
1706
1964
  }
@@ -1708,7 +1966,7 @@ async function writeDraft(ctx, draft) {
1708
1966
  // src/tools/mem-session-end.ts
1709
1967
  import { writeFile as writeFile13, mkdir as mkdir7 } from "fs/promises";
1710
1968
  import { existsSync as existsSync20 } from "fs";
1711
- import path10 from "path";
1969
+ import path11 from "path";
1712
1970
  import {
1713
1971
  buildFrontmatter as buildFrontmatter3,
1714
1972
  loadMemoriesFromDir as loadMemoriesFromDir16,
@@ -1726,10 +1984,10 @@ import {
1726
1984
  } from "@hivelore/core";
1727
1985
  import { mkdir as mkdir6, writeFile as writeFile12, rm } from "fs/promises";
1728
1986
  import { existsSync as existsSync19 } from "fs";
1729
- import path9 from "path";
1987
+ import path10 from "path";
1730
1988
  import { execSync as execSync2 } from "child_process";
1731
1989
  function pendingDistillPath(ctx) {
1732
- return path9.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
1990
+ return path10.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
1733
1991
  }
1734
1992
  var SessionTracker = class {
1735
1993
  events = [];
@@ -1846,7 +2104,7 @@ var SessionTracker = class {
1846
2104
  ...gitDiff ? { git_diff: gitDiff } : {},
1847
2105
  ...recapId ? { recap_id: recapId } : {}
1848
2106
  };
1849
- const cacheDir = path9.join(this.ctx.paths.haiveDir, ".cache");
2107
+ const cacheDir = path10.join(this.ctx.paths.haiveDir, ".cache");
1850
2108
  await mkdir6(cacheDir, { recursive: true });
1851
2109
  await writeFile12(
1852
2110
  pendingDistillPath(this.ctx),
@@ -1928,12 +2186,12 @@ async function memSessionEnd(input, ctx) {
1928
2186
  const body = buildBody(input);
1929
2187
  const topic = recapTopic(input.scope, input.module);
1930
2188
  const normalizedFiles = input.files_touched.map((p) => {
1931
- if (!p || !path10.isAbsolute(p)) return p;
1932
- const rel = path10.relative(ctx.paths.root, p);
2189
+ if (!p || !path11.isAbsolute(p)) return p;
2190
+ const rel = path11.relative(ctx.paths.root, p);
1933
2191
  return rel.startsWith("..") ? p : rel;
1934
2192
  });
1935
2193
  const invalidPaths = normalizedFiles.filter(
1936
- (p) => !existsSync20(path10.resolve(ctx.paths.root, p))
2194
+ (p) => !existsSync20(path11.resolve(ctx.paths.root, p))
1937
2195
  );
1938
2196
  if (invalidPaths.length > 0) {
1939
2197
  console.warn(`[haive] session end: anchor path(s) not found: ${invalidPaths.join(", ")}`);
@@ -1984,7 +2242,7 @@ async function memSessionEnd(input, ctx) {
1984
2242
  frontmatter.id,
1985
2243
  frontmatter.module
1986
2244
  );
1987
- await mkdir7(path10.dirname(file), { recursive: true });
2245
+ await mkdir7(path11.dirname(file), { recursive: true });
1988
2246
  await writeFile13(file, serializeMemory10({ frontmatter, body }), "utf8");
1989
2247
  await clearPendingDistill(ctx);
1990
2248
  return {
@@ -1999,7 +2257,7 @@ async function memSessionEnd(input, ctx) {
1999
2257
  // src/tools/get-briefing.ts
2000
2258
  import { readFile as readFile7, writeFile as writeFile14, readdir as readdir4 } from "fs/promises";
2001
2259
  import { existsSync as existsSync22 } from "fs";
2002
- import path12 from "path";
2260
+ import path13 from "path";
2003
2261
  import {
2004
2262
  allocateBudget,
2005
2263
  assessBootstrapState,
@@ -2046,7 +2304,7 @@ import { z as z20 } from "zod";
2046
2304
  // src/tools/briefing-helpers.ts
2047
2305
  import { readdir as readdir3, readFile as readFile6 } from "fs/promises";
2048
2306
  import { existsSync as existsSync21 } from "fs";
2049
- import path11 from "path";
2307
+ import path12 from "path";
2050
2308
  import {
2051
2309
  classifyMemoryPriority as coreClassifyPriority,
2052
2310
  isGlobPath,
@@ -2186,7 +2444,7 @@ async function loadModuleContexts2(ctx, modules) {
2186
2444
  const out = [];
2187
2445
  for (const m of modules) {
2188
2446
  if (!available.has(m)) continue;
2189
- const file = path11.join(ctx.paths.modulesContextDir, m, "context.md");
2447
+ const file = path12.join(ctx.paths.modulesContextDir, m, "context.md");
2190
2448
  if (existsSync21(file)) {
2191
2449
  out.push({ name: m, content: await readFile6(file, "utf8") });
2192
2450
  }
@@ -2807,7 +3065,7 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
2807
3065
  };
2808
3066
  }
2809
3067
  async function detectRunCommands(root) {
2810
- const pkgPath = path12.join(root, "package.json");
3068
+ const pkgPath = path13.join(root, "package.json");
2811
3069
  if (!existsSync22(pkgPath)) return null;
2812
3070
  try {
2813
3071
  const pkg = JSON.parse(await readFile7(pkgPath, "utf8"));
@@ -2875,7 +3133,7 @@ function oneLine(value) {
2875
3133
  return value.replace(/\s+/g, " ").replace(/"/g, '\\"').trim().slice(0, 120);
2876
3134
  }
2877
3135
  function serverVersion() {
2878
- return true ? "0.39.1" : "dev";
3136
+ return true ? "0.42.1" : "dev";
2879
3137
  }
2880
3138
 
2881
3139
  // src/tools/code-map.ts
@@ -4303,7 +4561,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
4303
4561
  // src/server.ts
4304
4562
  import { hasRecentBriefingMarker, loadConfigSync } from "@hivelore/core";
4305
4563
  var SERVER_NAME = "hivelore";
4306
- var SERVER_VERSION = "0.39.1";
4564
+ var SERVER_VERSION = "0.42.1";
4307
4565
  function jsonResult(data) {
4308
4566
  return {
4309
4567
  content: [