@hivelore/mcp 0.39.2 → 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/server.js CHANGED
@@ -1122,7 +1122,7 @@ async function memApprove(input, ctx) {
1122
1122
  // src/tools/mem-tried.ts
1123
1123
  import { mkdir as mkdir3, writeFile as writeFile9 } from "fs/promises";
1124
1124
  import { existsSync as existsSync16 } from "fs";
1125
- import path6 from "path";
1125
+ import path7 from "path";
1126
1126
  import {
1127
1127
  buildFrontmatter as buildFrontmatter2,
1128
1128
  memoryFilePath as memoryFilePath2,
@@ -1134,21 +1134,105 @@ import { z as z16 } from "zod";
1134
1134
  // src/tools/propose-sensor.ts
1135
1135
  import { execSync } from "child_process";
1136
1136
  import { readFile as readFile3, writeFile as writeFile8 } from "fs/promises";
1137
- import { existsSync as existsSync15 } from "fs";
1138
- import path5 from "path";
1137
+ import { existsSync as existsSync15, rmSync, symlinkSync } from "fs";
1138
+ import os from "os";
1139
+ import path6 from "path";
1139
1140
  import {
1140
1141
  extractSensorExamples,
1141
1142
  extractTestFilePathsFromCommand,
1142
1143
  hasPendingTestMarker,
1143
1144
  judgeProposedSensor,
1144
1145
  loadMemoriesFromDir as loadMemoriesFromDir13,
1146
+ scrubbedCommandEnv,
1147
+ sensorPatternBrittleness,
1145
1148
  serializeMemory as serializeMemory7
1146
1149
  } from "@hivelore/core";
1147
1150
  import { z as z15 } from "zod";
1151
+
1152
+ // src/ast-sensors.ts
1153
+ import path5 from "path";
1154
+ var cachedEngine;
1155
+ async function loadAstEngine() {
1156
+ if (cachedEngine !== void 0) return cachedEngine;
1157
+ try {
1158
+ cachedEngine = await import("@ast-grep/napi");
1159
+ } catch {
1160
+ cachedEngine = null;
1161
+ }
1162
+ return cachedEngine;
1163
+ }
1164
+ async function astEngineAvailable() {
1165
+ return await loadAstEngine() !== null;
1166
+ }
1167
+ function astLangForPath(filePath) {
1168
+ const ext = path5.extname(filePath).toLowerCase();
1169
+ if (ext === ".ts" || ext === ".mts" || ext === ".cts") return "TypeScript";
1170
+ if (ext === ".tsx") return "Tsx";
1171
+ if (ext === ".js" || ext === ".jsx" || ext === ".mjs" || ext === ".cjs") return "JavaScript";
1172
+ return null;
1173
+ }
1174
+ function absentPresentInNode(node, absent) {
1175
+ try {
1176
+ if (node.find(absent)) return true;
1177
+ } catch {
1178
+ }
1179
+ const text = node.text();
1180
+ try {
1181
+ return new RegExp(absent).test(text);
1182
+ } catch {
1183
+ return text.includes(absent);
1184
+ }
1185
+ }
1186
+ async function runAstPattern(content, filePath, pattern, absent) {
1187
+ const engine = await loadAstEngine();
1188
+ if (!engine) return { status: "engine-missing", matches: [] };
1189
+ const langName = astLangForPath(filePath);
1190
+ if (!langName) return { status: "unsupported-language", matches: [] };
1191
+ const lang = engine.Lang[langName];
1192
+ let root;
1193
+ try {
1194
+ root = engine.parse(lang, content).root();
1195
+ } catch (err) {
1196
+ return { status: "parse-error", matches: [], detail: String(err).slice(0, 200) };
1197
+ }
1198
+ let nodes;
1199
+ try {
1200
+ nodes = root.findAll(pattern);
1201
+ } catch (err) {
1202
+ return { status: "invalid-pattern", matches: [], detail: String(err).slice(0, 200) };
1203
+ }
1204
+ const matches = [];
1205
+ for (const node of nodes) {
1206
+ if (absent && absentPresentInNode(node, absent)) continue;
1207
+ const range = node.range();
1208
+ matches.push({
1209
+ startLine: range.start.line + 1,
1210
+ endLine: range.end.line + 1,
1211
+ text: node.text().trim().slice(0, 200)
1212
+ });
1213
+ }
1214
+ return { status: "ok", matches };
1215
+ }
1216
+ async function runAstSensorOnContent(input) {
1217
+ const scan = await runAstPattern(input.content, input.filePath, input.pattern, input.absent);
1218
+ if (scan.status !== "ok" || !input.addedLines || input.addedLines.size === 0) return scan;
1219
+ const added = input.addedLines;
1220
+ return {
1221
+ status: "ok",
1222
+ matches: scan.matches.filter((m) => {
1223
+ for (let line = m.startLine; line <= m.endLine; line++) {
1224
+ if (added.has(line)) return true;
1225
+ }
1226
+ return false;
1227
+ })
1228
+ };
1229
+ }
1230
+
1231
+ // src/tools/propose-sensor.ts
1148
1232
  var ProposeSensorInputSchema = {
1149
1233
  memory_id: z15.string().min(1).describe("Id of the gotcha/attempt memory this sensor protects."),
1150
- kind: z15.enum(["regex", "shell", "test"]).default("regex").describe(
1151
- "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."
1234
+ kind: z15.enum(["regex", "ast", "shell", "test"]).default("regex").describe(
1235
+ "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."
1152
1236
  ),
1153
1237
  pattern: z15.string().optional().describe("kind=regex: regex matching the FAULTY usage (the risky call/token), e.g. 'stripe\\.paymentIntents\\.create'."),
1154
1238
  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."),
@@ -1162,6 +1246,9 @@ var ProposeSensorInputSchema = {
1162
1246
  incident: z15.string().optional().describe(
1163
1247
  "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."
1164
1248
  ),
1249
+ red_ref: z15.string().optional().describe(
1250
+ "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."
1251
+ ),
1165
1252
  flags: z15.string().optional().describe("Optional regex flags (e.g. 'i' for case-insensitive)."),
1166
1253
  paths: z15.array(z15.string()).default([]).describe("Override scope paths. Defaults to the memory's anchor paths.")
1167
1254
  };
@@ -1188,7 +1275,7 @@ async function readPresumedCorrectTargets(root, relPaths) {
1188
1275
  continue;
1189
1276
  } catch {
1190
1277
  }
1191
- const abs = path5.resolve(root, rel);
1278
+ const abs = path6.resolve(root, rel);
1192
1279
  if (!existsSync15(abs)) continue;
1193
1280
  try {
1194
1281
  targets.push({ path: rel, content: await readFile3(abs, "utf8") });
@@ -1203,7 +1290,9 @@ function runCommandForValidation(command, root, timeoutMs = 12e4) {
1203
1290
  cwd: root,
1204
1291
  timeout: timeoutMs,
1205
1292
  maxBuffer: 8 * 1024 * 1024,
1206
- stdio: ["ignore", "pipe", "pipe"]
1293
+ stdio: ["ignore", "pipe", "pipe"],
1294
+ // Same containment as the gate executor: an oracle gets a test-runner env, not credentials.
1295
+ env: { ...scrubbedCommandEnv(process.env), HIVELORE_SENSOR: "validation" }
1207
1296
  });
1208
1297
  return { status: "passed", detail: "exit 0" };
1209
1298
  } catch (err) {
@@ -1217,6 +1306,48 @@ ${e.stderr?.toString() ?? ""}`.split("\n").filter(Boolean).slice(-8).join("\n");
1217
1306
  return { status: "failed", detail: out || `exit ${e.status ?? "?"}` };
1218
1307
  }
1219
1308
  }
1309
+ function proveRedOnIncident(command, root, redRef, timeoutMs) {
1310
+ const worktree = path6.join(os.tmpdir(), `hivelore-red-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
1311
+ let added = false;
1312
+ try {
1313
+ try {
1314
+ execSync(`git worktree add --detach ${JSON.stringify(worktree)} ${JSON.stringify(redRef)}`, {
1315
+ cwd: root,
1316
+ stdio: ["ignore", "pipe", "pipe"],
1317
+ timeout: 6e4
1318
+ });
1319
+ added = true;
1320
+ } catch (err) {
1321
+ const e = err;
1322
+ return { proven: false, reason: "red-ref-invalid", detail: (e.stderr?.toString() ?? String(err)).slice(0, 300) };
1323
+ }
1324
+ const mainModules = path6.join(root, "node_modules");
1325
+ const wtModules = path6.join(worktree, "node_modules");
1326
+ if (existsSync15(mainModules) && !existsSync15(wtModules)) {
1327
+ try {
1328
+ symlinkSync(mainModules, wtModules, "dir");
1329
+ } catch {
1330
+ }
1331
+ }
1332
+ const run = runCommandForValidation(command, worktree, timeoutMs);
1333
+ if (run.status === "failed") return { proven: true, detail: run.detail };
1334
+ if (run.status === "passed") {
1335
+ return { proven: false, reason: "red-not-proven", detail: "oracle PASSED on the incident state \u2014 it does not catch the incident" };
1336
+ }
1337
+ return { proven: false, reason: "red-unrunnable", detail: run.detail };
1338
+ } finally {
1339
+ if (added) {
1340
+ try {
1341
+ execSync(`git worktree remove --force ${JSON.stringify(worktree)}`, { cwd: root, stdio: "ignore", timeout: 6e4 });
1342
+ } catch {
1343
+ try {
1344
+ rmSync(worktree, { recursive: true, force: true });
1345
+ } catch {
1346
+ }
1347
+ }
1348
+ }
1349
+ }
1350
+ }
1220
1351
  async function proposeSensor(input, ctx) {
1221
1352
  if (!existsSync15(ctx.paths.memoriesDir)) {
1222
1353
  throw new Error(`No .ai/memories at ${ctx.paths.root}. Run 'hivelore init' first.`);
@@ -1246,6 +1377,17 @@ async function proposeSensor(input, ctx) {
1246
1377
  self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1247
1378
  };
1248
1379
  }
1380
+ } else if (kind === "ast") {
1381
+ if (!input.pattern?.trim()) {
1382
+ return {
1383
+ accepted: false,
1384
+ memory_id: input.memory_id,
1385
+ severity: input.severity,
1386
+ reason: "invalid-pattern",
1387
+ guidance: "kind=ast requires a `pattern` (an ast-grep structural pattern).",
1388
+ self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1389
+ };
1390
+ }
1249
1391
  } else if (!input.command?.trim()) {
1250
1392
  return {
1251
1393
  accepted: false,
@@ -1262,12 +1404,109 @@ async function proposeSensor(input, ctx) {
1262
1404
  throw new Error(`No memory found with id ${input.memory_id}`);
1263
1405
  }
1264
1406
  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}.` : "";
1265
- if (kind !== "regex") {
1266
- const referencedTests = extractTestFilePathsFromCommand(input.command.trim()).filter((rel) => existsSync15(path5.resolve(ctx.paths.root, rel)));
1407
+ if (kind === "ast") {
1408
+ const pattern = input.pattern.trim();
1409
+ if (!await astEngineAvailable() && input.severity === "block") {
1410
+ return {
1411
+ accepted: false,
1412
+ memory_id: input.memory_id,
1413
+ severity: input.severity,
1414
+ reason: "ast-engine-missing",
1415
+ 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.",
1416
+ self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1417
+ };
1418
+ }
1419
+ const brittleAst = sensorPatternBrittleness(pattern);
1420
+ if (brittleAst && input.severity === "block") {
1421
+ return {
1422
+ accepted: false,
1423
+ memory_id: input.memory_id,
1424
+ severity: input.severity,
1425
+ reason: "brittle",
1426
+ guidance: `The pattern is brittle (${brittleAst}). Use a durable structural pattern, then re-propose.`,
1427
+ self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1428
+ };
1429
+ }
1430
+ const anchorPathsAst = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
1431
+ const currentTargetsAst = await readPresumedCorrectTargets(ctx.paths.root, anchorPathsAst);
1432
+ const firedOnAst = [];
1433
+ for (const target of currentTargetsAst) {
1434
+ const scan = await runAstSensorOnContent({ pattern, absent: input.absent, content: target.content, filePath: target.path });
1435
+ if (scan.status === "invalid-pattern") {
1436
+ return {
1437
+ accepted: false,
1438
+ memory_id: input.memory_id,
1439
+ severity: input.severity,
1440
+ reason: "invalid-pattern",
1441
+ guidance: `The ast-grep pattern is invalid: ${scan.detail ?? "unparseable"}. Fix it and re-propose.`,
1442
+ self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1443
+ };
1444
+ }
1445
+ if (scan.status === "ok" && scan.matches.length > 0) firedOnAst.push(target.path);
1446
+ }
1447
+ if (firedOnAst.length > 0 && input.severity === "block") {
1448
+ return {
1449
+ accepted: false,
1450
+ memory_id: input.memory_id,
1451
+ severity: input.severity,
1452
+ reason: "fires-on-current",
1453
+ 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.`,
1454
+ self_check: { silent_on_current: false, fires_on_bad: null, fired_on: firedOnAst }
1455
+ };
1456
+ }
1457
+ const badExamplesAst = [
1458
+ ...input.bad_example ? [input.bad_example] : [],
1459
+ ...extractSensorExamples(found.memory.body)
1460
+ ];
1461
+ let firesOnBadAst = null;
1462
+ if (badExamplesAst.length > 0 && await astEngineAvailable()) {
1463
+ const exampleLang = anchorPathsAst.find((p) => astLangForPath(p) !== null) ?? "example.tsx";
1464
+ firesOnBadAst = false;
1465
+ for (const example of badExamplesAst) {
1466
+ const scan = await runAstSensorOnContent({ pattern, absent: input.absent, content: example, filePath: exampleLang });
1467
+ if (scan.status === "ok" && scan.matches.length > 0) {
1468
+ firesOnBadAst = true;
1469
+ break;
1470
+ }
1471
+ }
1472
+ }
1473
+ if (firesOnBadAst === false && input.severity === "block") {
1474
+ return {
1475
+ accepted: false,
1476
+ memory_id: input.memory_id,
1477
+ severity: input.severity,
1478
+ reason: "missed-bad-example",
1479
+ guidance: "The pattern did not match the bad example structurally, so it won't catch the mistake. Adjust it, then re-propose.",
1480
+ self_check: { silent_on_current: firedOnAst.length === 0, fires_on_bad: false, fired_on: [] }
1481
+ };
1482
+ }
1483
+ const sensorAst = {
1484
+ kind: "ast",
1485
+ pattern,
1486
+ ...input.absent ? { absent: input.absent } : {},
1487
+ paths: anchorPathsAst,
1488
+ message: input.message?.trim() || deriveMessage(found.memory.body, pattern, input.absent),
1489
+ ...input.incident?.trim() ? { incident: input.incident.trim() } : {},
1490
+ severity: input.severity,
1491
+ autogen: false,
1492
+ last_fired: null
1493
+ };
1494
+ await writeFile8(found.filePath, serializeMemory7({ frontmatter: { ...found.memory.frontmatter, sensor: sensorAst }, body: found.memory.body }), "utf8");
1495
+ return {
1496
+ accepted: true,
1497
+ memory_id: input.memory_id,
1498
+ severity: input.severity,
1499
+ 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,
1500
+ self_check: { silent_on_current: firedOnAst.length === 0, fires_on_bad: firesOnBadAst, fired_on: firedOnAst },
1501
+ file_path: found.filePath
1502
+ };
1503
+ }
1504
+ if (kind === "shell" || kind === "test") {
1505
+ const referencedTests = extractTestFilePathsFromCommand(input.command.trim()).filter((rel) => existsSync15(path6.resolve(ctx.paths.root, rel)));
1267
1506
  const pendingTests = [];
1268
1507
  for (const rel of referencedTests) {
1269
1508
  try {
1270
- if (hasPendingTestMarker(await readFile3(path5.resolve(ctx.paths.root, rel), "utf8"))) pendingTests.push(rel);
1509
+ if (hasPendingTestMarker(await readFile3(path6.resolve(ctx.paths.root, rel), "utf8"))) pendingTests.push(rel);
1271
1510
  } catch {
1272
1511
  }
1273
1512
  }
@@ -1283,6 +1522,21 @@ async function proposeSensor(input, ctx) {
1283
1522
  }
1284
1523
  const verdictCmd = runCommandForValidation(input.command.trim(), ctx.paths.root, input.timeout_ms);
1285
1524
  const anchorPathsCmd = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
1525
+ let redProven = false;
1526
+ if (input.red_ref?.trim()) {
1527
+ const red = proveRedOnIncident(input.command.trim(), ctx.paths.root, input.red_ref.trim(), input.timeout_ms);
1528
+ if (!red.proven && input.severity === "block") {
1529
+ return {
1530
+ accepted: false,
1531
+ memory_id: input.memory_id,
1532
+ severity: input.severity,
1533
+ reason: red.reason ?? "red-not-proven",
1534
+ 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),
1535
+ self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: false, fired_on: [] }
1536
+ };
1537
+ }
1538
+ redProven = red.proven;
1539
+ }
1286
1540
  if (verdictCmd.status !== "passed" && input.severity === "block") {
1287
1541
  return {
1288
1542
  accepted: false,
@@ -1301,6 +1555,7 @@ ${verdictCmd.detail}`,
1301
1555
  paths: anchorPathsCmd,
1302
1556
  message: input.message?.trim() || deriveMessage(found.memory.body, input.command.trim(), void 0),
1303
1557
  ...input.incident?.trim() ? { incident: input.incident.trim() } : {},
1558
+ ...redProven ? { red_proven: true } : {},
1304
1559
  severity: input.severity,
1305
1560
  autogen: false,
1306
1561
  last_fired: null
@@ -1314,8 +1569,9 @@ ${verdictCmd.detail}`,
1314
1569
  accepted: true,
1315
1570
  memory_id: input.memory_id,
1316
1571
  severity: input.severity,
1317
- 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,
1318
- self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: null, fired_on: [] }
1572
+ 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,
1573
+ self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: redProven ? true : null, fired_on: [] },
1574
+ file_path: found.filePath
1319
1575
  };
1320
1576
  }
1321
1577
  const anchorPaths = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
@@ -1390,6 +1646,7 @@ var MemTriedInputSchema = {
1390
1646
  severity: z16.enum(["warn", "block"]).default("block").describe("block = deterministic gate refusal"),
1391
1647
  message: z16.string().optional().describe("Self-correction message shown when the sensor fires"),
1392
1648
  incident: z16.string().optional().describe("Provenance: the incident this sensor guards (e.g. 'prod #442') \u2014 surfaced when it fires and in the receipt"),
1649
+ 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)"),
1393
1650
  bad_example: z16.string().optional().describe("kind=regex: code snippet the sensor MUST fire on (validation)")
1394
1651
  }).optional().describe(
1395
1652
  "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."
@@ -1421,7 +1678,7 @@ async function memTried(input, ctx) {
1421
1678
  }
1422
1679
  const body = lines.join("\n") + "\n";
1423
1680
  const file = memoryFilePath2(ctx.paths, frontmatter.scope, frontmatter.id, frontmatter.module);
1424
- await mkdir3(path6.dirname(file), { recursive: true });
1681
+ await mkdir3(path7.dirname(file), { recursive: true });
1425
1682
  if (existsSync16(file)) {
1426
1683
  throw new Error(`Memory already exists at ${file}`);
1427
1684
  }
@@ -1438,6 +1695,7 @@ async function memTried(input, ctx) {
1438
1695
  severity: input.sensor.severity ?? "block",
1439
1696
  message: input.sensor.message,
1440
1697
  incident: input.sensor.incident,
1698
+ red_ref: input.sensor.red_ref,
1441
1699
  bad_example: input.sensor.bad_example,
1442
1700
  flags: void 0,
1443
1701
  paths: []
@@ -1479,7 +1737,7 @@ async function memTried(input, ctx) {
1479
1737
  // src/tools/scaffold-test.ts
1480
1738
  import { existsSync as existsSync17, statSync } from "fs";
1481
1739
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile10 } from "fs/promises";
1482
- import path7 from "path";
1740
+ import path8 from "path";
1483
1741
  import { z as z17 } from "zod";
1484
1742
  import {
1485
1743
  buildProposeCommand,
@@ -1491,17 +1749,17 @@ import {
1491
1749
  } from "@hivelore/core";
1492
1750
  var PY_SIGNALS = ["pyproject.toml", "setup.py", "pytest.ini", "requirements.txt", "tox.ini"];
1493
1751
  async function detectForAnchor(root, rel) {
1494
- let dir = path7.resolve(root, rel);
1752
+ let dir = path8.resolve(root, rel);
1495
1753
  try {
1496
- if (!statSync(dir).isDirectory()) dir = path7.dirname(dir);
1754
+ if (!statSync(dir).isDirectory()) dir = path8.dirname(dir);
1497
1755
  } catch {
1498
- if (path7.extname(dir)) dir = path7.dirname(dir);
1756
+ if (path8.extname(dir)) dir = path8.dirname(dir);
1499
1757
  }
1500
1758
  while (dir.startsWith(root)) {
1501
- const pkgJson = path7.join(dir, "package.json");
1759
+ const pkgJson = path8.join(dir, "package.json");
1502
1760
  const hasPkg = existsSync17(pkgJson);
1503
- const goMod = existsSync17(path7.join(dir, "go.mod"));
1504
- const pySignal = PY_SIGNALS.some((s) => existsSync17(path7.join(dir, s)));
1761
+ const goMod = existsSync17(path8.join(dir, "go.mod"));
1762
+ const pySignal = PY_SIGNALS.some((s) => existsSync17(path8.join(dir, s)));
1505
1763
  if (hasPkg || goMod || pySignal) {
1506
1764
  let pkg = null;
1507
1765
  if (hasPkg) {
@@ -1511,10 +1769,10 @@ async function detectForAnchor(root, rel) {
1511
1769
  pkg = null;
1512
1770
  }
1513
1771
  }
1514
- const baseDir = path7.relative(root, dir).split(path7.sep).join("/");
1772
+ const baseDir = path8.relative(root, dir).split(path8.sep).join("/");
1515
1773
  return { framework: pickTestFramework(pkg, { goMod, pySignal }), baseDir };
1516
1774
  }
1517
- const parent = path7.dirname(dir);
1775
+ const parent = path8.dirname(dir);
1518
1776
  if (parent === dir || dir === root) break;
1519
1777
  dir = parent;
1520
1778
  }
@@ -1580,14 +1838,14 @@ async function scaffoldTest(input, ctx) {
1580
1838
  }
1581
1839
  const results = [];
1582
1840
  for (const scaffold of scaffolds) {
1583
- const abs = path7.isAbsolute(scaffold.relPath) ? scaffold.relPath : path7.resolve(ctx.paths.root, scaffold.relPath);
1841
+ const abs = path8.isAbsolute(scaffold.relPath) ? scaffold.relPath : path8.resolve(ctx.paths.root, scaffold.relPath);
1584
1842
  let written = false;
1585
1843
  let alreadyExists = false;
1586
1844
  if (input.write) {
1587
1845
  if (existsSync17(abs)) {
1588
1846
  alreadyExists = true;
1589
1847
  } else {
1590
- await mkdir4(path7.dirname(abs), { recursive: true });
1848
+ await mkdir4(path8.dirname(abs), { recursive: true });
1591
1849
  await writeFile10(abs, scaffold.content, "utf8");
1592
1850
  written = true;
1593
1851
  }
@@ -1621,7 +1879,7 @@ async function scaffoldTest(input, ctx) {
1621
1879
  // src/tools/ingest-findings.ts
1622
1880
  import { existsSync as existsSync18 } from "fs";
1623
1881
  import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile11 } from "fs/promises";
1624
- import path8 from "path";
1882
+ import path9 from "path";
1625
1883
  import {
1626
1884
  draftsFromFindings,
1627
1885
  filterNewDrafts,
@@ -1652,7 +1910,7 @@ async function ingestFindings(input, ctx) {
1652
1910
  if (input.report && input.report.trim()) {
1653
1911
  raw = input.report;
1654
1912
  } else if (input.report_path) {
1655
- const file = path8.resolve(ctx.paths.root, input.report_path);
1913
+ const file = path9.resolve(ctx.paths.root, input.report_path);
1656
1914
  if (!existsSync18(file)) throw new Error(`Report file not found: ${file}`);
1657
1915
  raw = await readFile5(file, "utf8");
1658
1916
  } else {
@@ -1706,7 +1964,7 @@ async function writeDraft(ctx, draft) {
1706
1964
  draft.frontmatter.id,
1707
1965
  draft.frontmatter.module
1708
1966
  );
1709
- await mkdir5(path8.dirname(file), { recursive: true });
1967
+ await mkdir5(path9.dirname(file), { recursive: true });
1710
1968
  await writeFile11(file, serializeMemory9({ frontmatter: draft.frontmatter, body: draft.body }), "utf8");
1711
1969
  return file;
1712
1970
  }
@@ -1714,7 +1972,7 @@ async function writeDraft(ctx, draft) {
1714
1972
  // src/tools/mem-session-end.ts
1715
1973
  import { writeFile as writeFile13, mkdir as mkdir7 } from "fs/promises";
1716
1974
  import { existsSync as existsSync20 } from "fs";
1717
- import path10 from "path";
1975
+ import path11 from "path";
1718
1976
  import {
1719
1977
  buildFrontmatter as buildFrontmatter3,
1720
1978
  loadMemoriesFromDir as loadMemoriesFromDir16,
@@ -1732,10 +1990,10 @@ import {
1732
1990
  } from "@hivelore/core";
1733
1991
  import { mkdir as mkdir6, writeFile as writeFile12, rm } from "fs/promises";
1734
1992
  import { existsSync as existsSync19 } from "fs";
1735
- import path9 from "path";
1993
+ import path10 from "path";
1736
1994
  import { execSync as execSync2 } from "child_process";
1737
1995
  function pendingDistillPath(ctx) {
1738
- return path9.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
1996
+ return path10.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
1739
1997
  }
1740
1998
  var SessionTracker = class {
1741
1999
  events = [];
@@ -1852,7 +2110,7 @@ var SessionTracker = class {
1852
2110
  ...gitDiff ? { git_diff: gitDiff } : {},
1853
2111
  ...recapId ? { recap_id: recapId } : {}
1854
2112
  };
1855
- const cacheDir = path9.join(this.ctx.paths.haiveDir, ".cache");
2113
+ const cacheDir = path10.join(this.ctx.paths.haiveDir, ".cache");
1856
2114
  await mkdir6(cacheDir, { recursive: true });
1857
2115
  await writeFile12(
1858
2116
  pendingDistillPath(this.ctx),
@@ -1934,12 +2192,12 @@ async function memSessionEnd(input, ctx) {
1934
2192
  const body = buildBody(input);
1935
2193
  const topic = recapTopic(input.scope, input.module);
1936
2194
  const normalizedFiles = input.files_touched.map((p) => {
1937
- if (!p || !path10.isAbsolute(p)) return p;
1938
- const rel = path10.relative(ctx.paths.root, p);
2195
+ if (!p || !path11.isAbsolute(p)) return p;
2196
+ const rel = path11.relative(ctx.paths.root, p);
1939
2197
  return rel.startsWith("..") ? p : rel;
1940
2198
  });
1941
2199
  const invalidPaths = normalizedFiles.filter(
1942
- (p) => !existsSync20(path10.resolve(ctx.paths.root, p))
2200
+ (p) => !existsSync20(path11.resolve(ctx.paths.root, p))
1943
2201
  );
1944
2202
  if (invalidPaths.length > 0) {
1945
2203
  console.warn(`[haive] session end: anchor path(s) not found: ${invalidPaths.join(", ")}`);
@@ -1990,7 +2248,7 @@ async function memSessionEnd(input, ctx) {
1990
2248
  frontmatter.id,
1991
2249
  frontmatter.module
1992
2250
  );
1993
- await mkdir7(path10.dirname(file), { recursive: true });
2251
+ await mkdir7(path11.dirname(file), { recursive: true });
1994
2252
  await writeFile13(file, serializeMemory10({ frontmatter, body }), "utf8");
1995
2253
  await clearPendingDistill(ctx);
1996
2254
  return {
@@ -2005,7 +2263,7 @@ async function memSessionEnd(input, ctx) {
2005
2263
  // src/tools/get-briefing.ts
2006
2264
  import { readFile as readFile7, writeFile as writeFile14, readdir as readdir4 } from "fs/promises";
2007
2265
  import { existsSync as existsSync22 } from "fs";
2008
- import path12 from "path";
2266
+ import path13 from "path";
2009
2267
  import {
2010
2268
  allocateBudget,
2011
2269
  assessBootstrapState,
@@ -2052,7 +2310,7 @@ import { z as z20 } from "zod";
2052
2310
  // src/tools/briefing-helpers.ts
2053
2311
  import { readdir as readdir3, readFile as readFile6 } from "fs/promises";
2054
2312
  import { existsSync as existsSync21 } from "fs";
2055
- import path11 from "path";
2313
+ import path12 from "path";
2056
2314
  import {
2057
2315
  classifyMemoryPriority as coreClassifyPriority,
2058
2316
  isGlobPath,
@@ -2192,7 +2450,7 @@ async function loadModuleContexts2(ctx, modules) {
2192
2450
  const out = [];
2193
2451
  for (const m of modules) {
2194
2452
  if (!available.has(m)) continue;
2195
- const file = path11.join(ctx.paths.modulesContextDir, m, "context.md");
2453
+ const file = path12.join(ctx.paths.modulesContextDir, m, "context.md");
2196
2454
  if (existsSync21(file)) {
2197
2455
  out.push({ name: m, content: await readFile6(file, "utf8") });
2198
2456
  }
@@ -2813,7 +3071,7 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
2813
3071
  };
2814
3072
  }
2815
3073
  async function detectRunCommands(root) {
2816
- const pkgPath = path12.join(root, "package.json");
3074
+ const pkgPath = path13.join(root, "package.json");
2817
3075
  if (!existsSync22(pkgPath)) return null;
2818
3076
  try {
2819
3077
  const pkg = JSON.parse(await readFile7(pkgPath, "utf8"));
@@ -2881,7 +3139,7 @@ function oneLine(value) {
2881
3139
  return value.replace(/\s+/g, " ").replace(/"/g, '\\"').trim().slice(0, 120);
2882
3140
  }
2883
3141
  function serverVersion() {
2884
- return true ? "0.39.2" : "dev";
3142
+ return true ? "0.42.1" : "dev";
2885
3143
  }
2886
3144
 
2887
3145
  // src/tools/code-map.ts
@@ -4309,7 +4567,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
4309
4567
  // src/server.ts
4310
4568
  import { hasRecentBriefingMarker, loadConfigSync } from "@hivelore/core";
4311
4569
  var SERVER_NAME = "hivelore";
4312
- var SERVER_VERSION = "0.39.2";
4570
+ var SERVER_VERSION = "0.42.1";
4313
4571
  function jsonResult(data) {
4314
4572
  return {
4315
4573
  content: [
@@ -5261,6 +5519,8 @@ export {
5261
5519
  SERVER_VERSION,
5262
5520
  TOOL_PROFILES,
5263
5521
  antiPatternsCheck,
5522
+ astEngineAvailable,
5523
+ astLangForPath,
5264
5524
  codeMapTool,
5265
5525
  codeSearch,
5266
5526
  createHaiveServer,
@@ -5281,6 +5541,8 @@ export {
5281
5541
  printHaiveMcpVersion,
5282
5542
  proposeSensor,
5283
5543
  readPresumedCorrectTargets,
5544
+ runAstPattern,
5545
+ runAstSensorOnContent,
5284
5546
  runHaiveMcpStdio,
5285
5547
  scaffoldTest
5286
5548
  };