@hivelore/cli 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.
@@ -125,7 +125,7 @@ import {
125
125
  import { z as z14 } from "zod";
126
126
  import { mkdir as mkdir3, writeFile as writeFile9 } from "fs/promises";
127
127
  import { existsSync as existsSync16 } from "fs";
128
- import path6 from "path";
128
+ import path7 from "path";
129
129
  import {
130
130
  buildFrontmatter as buildFrontmatter2,
131
131
  memoryFilePath as memoryFilePath2,
@@ -135,20 +135,24 @@ import {
135
135
  import { z as z16 } from "zod";
136
136
  import { execSync } from "child_process";
137
137
  import { readFile as readFile3, writeFile as writeFile8 } from "fs/promises";
138
- import { existsSync as existsSync15 } from "fs";
139
- import path5 from "path";
138
+ import { existsSync as existsSync15, rmSync, symlinkSync } from "fs";
139
+ import os from "os";
140
+ import path6 from "path";
140
141
  import {
141
142
  extractSensorExamples,
142
143
  extractTestFilePathsFromCommand,
143
144
  hasPendingTestMarker,
144
145
  judgeProposedSensor,
145
146
  loadMemoriesFromDir as loadMemoriesFromDir13,
147
+ scrubbedCommandEnv,
148
+ sensorPatternBrittleness,
146
149
  serializeMemory as serializeMemory7
147
150
  } from "@hivelore/core";
148
151
  import { z as z15 } from "zod";
152
+ import path5 from "path";
149
153
  import { existsSync as existsSync17, statSync } from "fs";
150
154
  import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile10 } from "fs/promises";
151
- import path7 from "path";
155
+ import path8 from "path";
152
156
  import { z as z17 } from "zod";
153
157
  import {
154
158
  buildProposeCommand,
@@ -160,7 +164,7 @@ import {
160
164
  } from "@hivelore/core";
161
165
  import { existsSync as existsSync18 } from "fs";
162
166
  import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile11 } from "fs/promises";
163
- import path8 from "path";
167
+ import path9 from "path";
164
168
  import {
165
169
  draftsFromFindings,
166
170
  filterNewDrafts,
@@ -172,7 +176,7 @@ import {
172
176
  import { z as z18 } from "zod";
173
177
  import { writeFile as writeFile13, mkdir as mkdir7 } from "fs/promises";
174
178
  import { existsSync as existsSync20 } from "fs";
175
- import path10 from "path";
179
+ import path11 from "path";
176
180
  import {
177
181
  buildFrontmatter as buildFrontmatter3,
178
182
  loadMemoriesFromDir as loadMemoriesFromDir16,
@@ -188,11 +192,11 @@ import {
188
192
  } from "@hivelore/core";
189
193
  import { mkdir as mkdir6, writeFile as writeFile12, rm } from "fs/promises";
190
194
  import { existsSync as existsSync19 } from "fs";
191
- import path9 from "path";
195
+ import path10 from "path";
192
196
  import { execSync as execSync2 } from "child_process";
193
197
  import { readFile as readFile7, writeFile as writeFile14, readdir as readdir4 } from "fs/promises";
194
198
  import { existsSync as existsSync22 } from "fs";
195
- import path12 from "path";
199
+ import path13 from "path";
196
200
  import {
197
201
  allocateBudget,
198
202
  assessBootstrapState,
@@ -237,7 +241,7 @@ import {
237
241
  import { z as z20 } from "zod";
238
242
  import { readdir as readdir3, readFile as readFile6 } from "fs/promises";
239
243
  import { existsSync as existsSync21 } from "fs";
240
- import path11 from "path";
244
+ import path12 from "path";
241
245
  import {
242
246
  classifyMemoryPriority as coreClassifyPriority,
243
247
  isGlobPath,
@@ -1278,10 +1282,86 @@ async function memApprove(input, ctx) {
1278
1282
  file_path: found.filePath
1279
1283
  };
1280
1284
  }
1285
+ var cachedEngine;
1286
+ async function loadAstEngine() {
1287
+ if (cachedEngine !== void 0) return cachedEngine;
1288
+ try {
1289
+ cachedEngine = await import("@ast-grep/napi");
1290
+ } catch {
1291
+ cachedEngine = null;
1292
+ }
1293
+ return cachedEngine;
1294
+ }
1295
+ async function astEngineAvailable() {
1296
+ return await loadAstEngine() !== null;
1297
+ }
1298
+ function astLangForPath(filePath) {
1299
+ const ext = path5.extname(filePath).toLowerCase();
1300
+ if (ext === ".ts" || ext === ".mts" || ext === ".cts") return "TypeScript";
1301
+ if (ext === ".tsx") return "Tsx";
1302
+ if (ext === ".js" || ext === ".jsx" || ext === ".mjs" || ext === ".cjs") return "JavaScript";
1303
+ return null;
1304
+ }
1305
+ function absentPresentInNode(node, absent) {
1306
+ try {
1307
+ if (node.find(absent)) return true;
1308
+ } catch {
1309
+ }
1310
+ const text = node.text();
1311
+ try {
1312
+ return new RegExp(absent).test(text);
1313
+ } catch {
1314
+ return text.includes(absent);
1315
+ }
1316
+ }
1317
+ async function runAstPattern(content, filePath, pattern, absent) {
1318
+ const engine = await loadAstEngine();
1319
+ if (!engine) return { status: "engine-missing", matches: [] };
1320
+ const langName = astLangForPath(filePath);
1321
+ if (!langName) return { status: "unsupported-language", matches: [] };
1322
+ const lang = engine.Lang[langName];
1323
+ let root;
1324
+ try {
1325
+ root = engine.parse(lang, content).root();
1326
+ } catch (err) {
1327
+ return { status: "parse-error", matches: [], detail: String(err).slice(0, 200) };
1328
+ }
1329
+ let nodes;
1330
+ try {
1331
+ nodes = root.findAll(pattern);
1332
+ } catch (err) {
1333
+ return { status: "invalid-pattern", matches: [], detail: String(err).slice(0, 200) };
1334
+ }
1335
+ const matches = [];
1336
+ for (const node of nodes) {
1337
+ if (absent && absentPresentInNode(node, absent)) continue;
1338
+ const range = node.range();
1339
+ matches.push({
1340
+ startLine: range.start.line + 1,
1341
+ endLine: range.end.line + 1,
1342
+ text: node.text().trim().slice(0, 200)
1343
+ });
1344
+ }
1345
+ return { status: "ok", matches };
1346
+ }
1347
+ async function runAstSensorOnContent(input) {
1348
+ const scan = await runAstPattern(input.content, input.filePath, input.pattern, input.absent);
1349
+ if (scan.status !== "ok" || !input.addedLines || input.addedLines.size === 0) return scan;
1350
+ const added = input.addedLines;
1351
+ return {
1352
+ status: "ok",
1353
+ matches: scan.matches.filter((m) => {
1354
+ for (let line = m.startLine; line <= m.endLine; line++) {
1355
+ if (added.has(line)) return true;
1356
+ }
1357
+ return false;
1358
+ })
1359
+ };
1360
+ }
1281
1361
  var ProposeSensorInputSchema = {
1282
1362
  memory_id: z15.string().min(1).describe("Id of the gotcha/attempt memory this sensor protects."),
1283
- kind: z15.enum(["regex", "shell", "test"]).default("regex").describe(
1284
- "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."
1363
+ kind: z15.enum(["regex", "ast", "shell", "test"]).default("regex").describe(
1364
+ "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."
1285
1365
  ),
1286
1366
  pattern: z15.string().optional().describe("kind=regex: regex matching the FAULTY usage (the risky call/token), e.g. 'stripe\\.paymentIntents\\.create'."),
1287
1367
  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."),
@@ -1295,6 +1375,9 @@ var ProposeSensorInputSchema = {
1295
1375
  incident: z15.string().optional().describe(
1296
1376
  "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."
1297
1377
  ),
1378
+ red_ref: z15.string().optional().describe(
1379
+ "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."
1380
+ ),
1298
1381
  flags: z15.string().optional().describe("Optional regex flags (e.g. 'i' for case-insensitive)."),
1299
1382
  paths: z15.array(z15.string()).default([]).describe("Override scope paths. Defaults to the memory's anchor paths.")
1300
1383
  };
@@ -1321,7 +1404,7 @@ async function readPresumedCorrectTargets(root, relPaths) {
1321
1404
  continue;
1322
1405
  } catch {
1323
1406
  }
1324
- const abs = path5.resolve(root, rel);
1407
+ const abs = path6.resolve(root, rel);
1325
1408
  if (!existsSync15(abs)) continue;
1326
1409
  try {
1327
1410
  targets.push({ path: rel, content: await readFile3(abs, "utf8") });
@@ -1336,7 +1419,9 @@ function runCommandForValidation(command, root, timeoutMs = 12e4) {
1336
1419
  cwd: root,
1337
1420
  timeout: timeoutMs,
1338
1421
  maxBuffer: 8 * 1024 * 1024,
1339
- stdio: ["ignore", "pipe", "pipe"]
1422
+ stdio: ["ignore", "pipe", "pipe"],
1423
+ // Same containment as the gate executor: an oracle gets a test-runner env, not credentials.
1424
+ env: { ...scrubbedCommandEnv(process.env), HIVELORE_SENSOR: "validation" }
1340
1425
  });
1341
1426
  return { status: "passed", detail: "exit 0" };
1342
1427
  } catch (err) {
@@ -1350,6 +1435,48 @@ ${e.stderr?.toString() ?? ""}`.split("\n").filter(Boolean).slice(-8).join("\n");
1350
1435
  return { status: "failed", detail: out || `exit ${e.status ?? "?"}` };
1351
1436
  }
1352
1437
  }
1438
+ function proveRedOnIncident(command, root, redRef, timeoutMs) {
1439
+ const worktree = path6.join(os.tmpdir(), `hivelore-red-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
1440
+ let added = false;
1441
+ try {
1442
+ try {
1443
+ execSync(`git worktree add --detach ${JSON.stringify(worktree)} ${JSON.stringify(redRef)}`, {
1444
+ cwd: root,
1445
+ stdio: ["ignore", "pipe", "pipe"],
1446
+ timeout: 6e4
1447
+ });
1448
+ added = true;
1449
+ } catch (err) {
1450
+ const e = err;
1451
+ return { proven: false, reason: "red-ref-invalid", detail: (e.stderr?.toString() ?? String(err)).slice(0, 300) };
1452
+ }
1453
+ const mainModules = path6.join(root, "node_modules");
1454
+ const wtModules = path6.join(worktree, "node_modules");
1455
+ if (existsSync15(mainModules) && !existsSync15(wtModules)) {
1456
+ try {
1457
+ symlinkSync(mainModules, wtModules, "dir");
1458
+ } catch {
1459
+ }
1460
+ }
1461
+ const run = runCommandForValidation(command, worktree, timeoutMs);
1462
+ if (run.status === "failed") return { proven: true, detail: run.detail };
1463
+ if (run.status === "passed") {
1464
+ return { proven: false, reason: "red-not-proven", detail: "oracle PASSED on the incident state \u2014 it does not catch the incident" };
1465
+ }
1466
+ return { proven: false, reason: "red-unrunnable", detail: run.detail };
1467
+ } finally {
1468
+ if (added) {
1469
+ try {
1470
+ execSync(`git worktree remove --force ${JSON.stringify(worktree)}`, { cwd: root, stdio: "ignore", timeout: 6e4 });
1471
+ } catch {
1472
+ try {
1473
+ rmSync(worktree, { recursive: true, force: true });
1474
+ } catch {
1475
+ }
1476
+ }
1477
+ }
1478
+ }
1479
+ }
1353
1480
  async function proposeSensor(input, ctx) {
1354
1481
  if (!existsSync15(ctx.paths.memoriesDir)) {
1355
1482
  throw new Error(`No .ai/memories at ${ctx.paths.root}. Run 'hivelore init' first.`);
@@ -1379,6 +1506,17 @@ async function proposeSensor(input, ctx) {
1379
1506
  self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1380
1507
  };
1381
1508
  }
1509
+ } else if (kind === "ast") {
1510
+ if (!input.pattern?.trim()) {
1511
+ return {
1512
+ accepted: false,
1513
+ memory_id: input.memory_id,
1514
+ severity: input.severity,
1515
+ reason: "invalid-pattern",
1516
+ guidance: "kind=ast requires a `pattern` (an ast-grep structural pattern).",
1517
+ self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1518
+ };
1519
+ }
1382
1520
  } else if (!input.command?.trim()) {
1383
1521
  return {
1384
1522
  accepted: false,
@@ -1395,12 +1533,109 @@ async function proposeSensor(input, ctx) {
1395
1533
  throw new Error(`No memory found with id ${input.memory_id}`);
1396
1534
  }
1397
1535
  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}.` : "";
1398
- if (kind !== "regex") {
1399
- const referencedTests = extractTestFilePathsFromCommand(input.command.trim()).filter((rel) => existsSync15(path5.resolve(ctx.paths.root, rel)));
1536
+ if (kind === "ast") {
1537
+ const pattern = input.pattern.trim();
1538
+ if (!await astEngineAvailable() && input.severity === "block") {
1539
+ return {
1540
+ accepted: false,
1541
+ memory_id: input.memory_id,
1542
+ severity: input.severity,
1543
+ reason: "ast-engine-missing",
1544
+ 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.",
1545
+ self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1546
+ };
1547
+ }
1548
+ const brittleAst = sensorPatternBrittleness(pattern);
1549
+ if (brittleAst && input.severity === "block") {
1550
+ return {
1551
+ accepted: false,
1552
+ memory_id: input.memory_id,
1553
+ severity: input.severity,
1554
+ reason: "brittle",
1555
+ guidance: `The pattern is brittle (${brittleAst}). Use a durable structural pattern, then re-propose.`,
1556
+ self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1557
+ };
1558
+ }
1559
+ const anchorPathsAst = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
1560
+ const currentTargetsAst = await readPresumedCorrectTargets(ctx.paths.root, anchorPathsAst);
1561
+ const firedOnAst = [];
1562
+ for (const target of currentTargetsAst) {
1563
+ const scan = await runAstSensorOnContent({ pattern, absent: input.absent, content: target.content, filePath: target.path });
1564
+ if (scan.status === "invalid-pattern") {
1565
+ return {
1566
+ accepted: false,
1567
+ memory_id: input.memory_id,
1568
+ severity: input.severity,
1569
+ reason: "invalid-pattern",
1570
+ guidance: `The ast-grep pattern is invalid: ${scan.detail ?? "unparseable"}. Fix it and re-propose.`,
1571
+ self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
1572
+ };
1573
+ }
1574
+ if (scan.status === "ok" && scan.matches.length > 0) firedOnAst.push(target.path);
1575
+ }
1576
+ if (firedOnAst.length > 0 && input.severity === "block") {
1577
+ return {
1578
+ accepted: false,
1579
+ memory_id: input.memory_id,
1580
+ severity: input.severity,
1581
+ reason: "fires-on-current",
1582
+ 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.`,
1583
+ self_check: { silent_on_current: false, fires_on_bad: null, fired_on: firedOnAst }
1584
+ };
1585
+ }
1586
+ const badExamplesAst = [
1587
+ ...input.bad_example ? [input.bad_example] : [],
1588
+ ...extractSensorExamples(found.memory.body)
1589
+ ];
1590
+ let firesOnBadAst = null;
1591
+ if (badExamplesAst.length > 0 && await astEngineAvailable()) {
1592
+ const exampleLang = anchorPathsAst.find((p) => astLangForPath(p) !== null) ?? "example.tsx";
1593
+ firesOnBadAst = false;
1594
+ for (const example of badExamplesAst) {
1595
+ const scan = await runAstSensorOnContent({ pattern, absent: input.absent, content: example, filePath: exampleLang });
1596
+ if (scan.status === "ok" && scan.matches.length > 0) {
1597
+ firesOnBadAst = true;
1598
+ break;
1599
+ }
1600
+ }
1601
+ }
1602
+ if (firesOnBadAst === false && input.severity === "block") {
1603
+ return {
1604
+ accepted: false,
1605
+ memory_id: input.memory_id,
1606
+ severity: input.severity,
1607
+ reason: "missed-bad-example",
1608
+ guidance: "The pattern did not match the bad example structurally, so it won't catch the mistake. Adjust it, then re-propose.",
1609
+ self_check: { silent_on_current: firedOnAst.length === 0, fires_on_bad: false, fired_on: [] }
1610
+ };
1611
+ }
1612
+ const sensorAst = {
1613
+ kind: "ast",
1614
+ pattern,
1615
+ ...input.absent ? { absent: input.absent } : {},
1616
+ paths: anchorPathsAst,
1617
+ message: input.message?.trim() || deriveMessage(found.memory.body, pattern, input.absent),
1618
+ ...input.incident?.trim() ? { incident: input.incident.trim() } : {},
1619
+ severity: input.severity,
1620
+ autogen: false,
1621
+ last_fired: null
1622
+ };
1623
+ await writeFile8(found.filePath, serializeMemory7({ frontmatter: { ...found.memory.frontmatter, sensor: sensorAst }, body: found.memory.body }), "utf8");
1624
+ return {
1625
+ accepted: true,
1626
+ memory_id: input.memory_id,
1627
+ severity: input.severity,
1628
+ 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,
1629
+ self_check: { silent_on_current: firedOnAst.length === 0, fires_on_bad: firesOnBadAst, fired_on: firedOnAst },
1630
+ file_path: found.filePath
1631
+ };
1632
+ }
1633
+ if (kind === "shell" || kind === "test") {
1634
+ const referencedTests = extractTestFilePathsFromCommand(input.command.trim()).filter((rel) => existsSync15(path6.resolve(ctx.paths.root, rel)));
1400
1635
  const pendingTests = [];
1401
1636
  for (const rel of referencedTests) {
1402
1637
  try {
1403
- if (hasPendingTestMarker(await readFile3(path5.resolve(ctx.paths.root, rel), "utf8"))) pendingTests.push(rel);
1638
+ if (hasPendingTestMarker(await readFile3(path6.resolve(ctx.paths.root, rel), "utf8"))) pendingTests.push(rel);
1404
1639
  } catch {
1405
1640
  }
1406
1641
  }
@@ -1416,6 +1651,21 @@ async function proposeSensor(input, ctx) {
1416
1651
  }
1417
1652
  const verdictCmd = runCommandForValidation(input.command.trim(), ctx.paths.root, input.timeout_ms);
1418
1653
  const anchorPathsCmd = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
1654
+ let redProven = false;
1655
+ if (input.red_ref?.trim()) {
1656
+ const red = proveRedOnIncident(input.command.trim(), ctx.paths.root, input.red_ref.trim(), input.timeout_ms);
1657
+ if (!red.proven && input.severity === "block") {
1658
+ return {
1659
+ accepted: false,
1660
+ memory_id: input.memory_id,
1661
+ severity: input.severity,
1662
+ reason: red.reason ?? "red-not-proven",
1663
+ 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),
1664
+ self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: false, fired_on: [] }
1665
+ };
1666
+ }
1667
+ redProven = red.proven;
1668
+ }
1419
1669
  if (verdictCmd.status !== "passed" && input.severity === "block") {
1420
1670
  return {
1421
1671
  accepted: false,
@@ -1434,6 +1684,7 @@ ${verdictCmd.detail}`,
1434
1684
  paths: anchorPathsCmd,
1435
1685
  message: input.message?.trim() || deriveMessage(found.memory.body, input.command.trim(), void 0),
1436
1686
  ...input.incident?.trim() ? { incident: input.incident.trim() } : {},
1687
+ ...redProven ? { red_proven: true } : {},
1437
1688
  severity: input.severity,
1438
1689
  autogen: false,
1439
1690
  last_fired: null
@@ -1447,8 +1698,9 @@ ${verdictCmd.detail}`,
1447
1698
  accepted: true,
1448
1699
  memory_id: input.memory_id,
1449
1700
  severity: input.severity,
1450
- 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,
1451
- self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: null, fired_on: [] }
1701
+ 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,
1702
+ self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: redProven ? true : null, fired_on: [] },
1703
+ file_path: found.filePath
1452
1704
  };
1453
1705
  }
1454
1706
  const anchorPaths = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
@@ -1521,6 +1773,7 @@ var MemTriedInputSchema = {
1521
1773
  severity: z16.enum(["warn", "block"]).default("block").describe("block = deterministic gate refusal"),
1522
1774
  message: z16.string().optional().describe("Self-correction message shown when the sensor fires"),
1523
1775
  incident: z16.string().optional().describe("Provenance: the incident this sensor guards (e.g. 'prod #442') \u2014 surfaced when it fires and in the receipt"),
1776
+ 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)"),
1524
1777
  bad_example: z16.string().optional().describe("kind=regex: code snippet the sensor MUST fire on (validation)")
1525
1778
  }).optional().describe(
1526
1779
  "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."
@@ -1552,7 +1805,7 @@ async function memTried(input, ctx) {
1552
1805
  }
1553
1806
  const body = lines.join("\n") + "\n";
1554
1807
  const file = memoryFilePath2(ctx.paths, frontmatter.scope, frontmatter.id, frontmatter.module);
1555
- await mkdir3(path6.dirname(file), { recursive: true });
1808
+ await mkdir3(path7.dirname(file), { recursive: true });
1556
1809
  if (existsSync16(file)) {
1557
1810
  throw new Error(`Memory already exists at ${file}`);
1558
1811
  }
@@ -1569,6 +1822,7 @@ async function memTried(input, ctx) {
1569
1822
  severity: input.sensor.severity ?? "block",
1570
1823
  message: input.sensor.message,
1571
1824
  incident: input.sensor.incident,
1825
+ red_ref: input.sensor.red_ref,
1572
1826
  bad_example: input.sensor.bad_example,
1573
1827
  flags: void 0,
1574
1828
  paths: []
@@ -1608,17 +1862,17 @@ async function memTried(input, ctx) {
1608
1862
  }
1609
1863
  var PY_SIGNALS = ["pyproject.toml", "setup.py", "pytest.ini", "requirements.txt", "tox.ini"];
1610
1864
  async function detectForAnchor(root, rel) {
1611
- let dir = path7.resolve(root, rel);
1865
+ let dir = path8.resolve(root, rel);
1612
1866
  try {
1613
- if (!statSync(dir).isDirectory()) dir = path7.dirname(dir);
1867
+ if (!statSync(dir).isDirectory()) dir = path8.dirname(dir);
1614
1868
  } catch {
1615
- if (path7.extname(dir)) dir = path7.dirname(dir);
1869
+ if (path8.extname(dir)) dir = path8.dirname(dir);
1616
1870
  }
1617
1871
  while (dir.startsWith(root)) {
1618
- const pkgJson = path7.join(dir, "package.json");
1872
+ const pkgJson = path8.join(dir, "package.json");
1619
1873
  const hasPkg = existsSync17(pkgJson);
1620
- const goMod = existsSync17(path7.join(dir, "go.mod"));
1621
- const pySignal = PY_SIGNALS.some((s) => existsSync17(path7.join(dir, s)));
1874
+ const goMod = existsSync17(path8.join(dir, "go.mod"));
1875
+ const pySignal = PY_SIGNALS.some((s) => existsSync17(path8.join(dir, s)));
1622
1876
  if (hasPkg || goMod || pySignal) {
1623
1877
  let pkg = null;
1624
1878
  if (hasPkg) {
@@ -1628,10 +1882,10 @@ async function detectForAnchor(root, rel) {
1628
1882
  pkg = null;
1629
1883
  }
1630
1884
  }
1631
- const baseDir = path7.relative(root, dir).split(path7.sep).join("/");
1885
+ const baseDir = path8.relative(root, dir).split(path8.sep).join("/");
1632
1886
  return { framework: pickTestFramework(pkg, { goMod, pySignal }), baseDir };
1633
1887
  }
1634
- const parent = path7.dirname(dir);
1888
+ const parent = path8.dirname(dir);
1635
1889
  if (parent === dir || dir === root) break;
1636
1890
  dir = parent;
1637
1891
  }
@@ -1697,14 +1951,14 @@ async function scaffoldTest(input, ctx) {
1697
1951
  }
1698
1952
  const results = [];
1699
1953
  for (const scaffold of scaffolds) {
1700
- const abs = path7.isAbsolute(scaffold.relPath) ? scaffold.relPath : path7.resolve(ctx.paths.root, scaffold.relPath);
1954
+ const abs = path8.isAbsolute(scaffold.relPath) ? scaffold.relPath : path8.resolve(ctx.paths.root, scaffold.relPath);
1701
1955
  let written = false;
1702
1956
  let alreadyExists = false;
1703
1957
  if (input.write) {
1704
1958
  if (existsSync17(abs)) {
1705
1959
  alreadyExists = true;
1706
1960
  } else {
1707
- await mkdir4(path7.dirname(abs), { recursive: true });
1961
+ await mkdir4(path8.dirname(abs), { recursive: true });
1708
1962
  await writeFile10(abs, scaffold.content, "utf8");
1709
1963
  written = true;
1710
1964
  }
@@ -1755,7 +2009,7 @@ async function ingestFindings(input, ctx) {
1755
2009
  if (input.report && input.report.trim()) {
1756
2010
  raw = input.report;
1757
2011
  } else if (input.report_path) {
1758
- const file = path8.resolve(ctx.paths.root, input.report_path);
2012
+ const file = path9.resolve(ctx.paths.root, input.report_path);
1759
2013
  if (!existsSync18(file)) throw new Error(`Report file not found: ${file}`);
1760
2014
  raw = await readFile5(file, "utf8");
1761
2015
  } else {
@@ -1809,12 +2063,12 @@ async function writeDraft(ctx, draft) {
1809
2063
  draft.frontmatter.id,
1810
2064
  draft.frontmatter.module
1811
2065
  );
1812
- await mkdir5(path8.dirname(file), { recursive: true });
2066
+ await mkdir5(path9.dirname(file), { recursive: true });
1813
2067
  await writeFile11(file, serializeMemory9({ frontmatter: draft.frontmatter, body: draft.body }), "utf8");
1814
2068
  return file;
1815
2069
  }
1816
2070
  function pendingDistillPath(ctx) {
1817
- return path9.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
2071
+ return path10.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
1818
2072
  }
1819
2073
  var SessionTracker = class {
1820
2074
  events = [];
@@ -1931,7 +2185,7 @@ var SessionTracker = class {
1931
2185
  ...gitDiff ? { git_diff: gitDiff } : {},
1932
2186
  ...recapId ? { recap_id: recapId } : {}
1933
2187
  };
1934
- const cacheDir = path9.join(this.ctx.paths.haiveDir, ".cache");
2188
+ const cacheDir = path10.join(this.ctx.paths.haiveDir, ".cache");
1935
2189
  await mkdir6(cacheDir, { recursive: true });
1936
2190
  await writeFile12(
1937
2191
  pendingDistillPath(this.ctx),
@@ -2011,12 +2265,12 @@ async function memSessionEnd(input, ctx) {
2011
2265
  const body = buildBody(input);
2012
2266
  const topic = recapTopic(input.scope, input.module);
2013
2267
  const normalizedFiles = input.files_touched.map((p) => {
2014
- if (!p || !path10.isAbsolute(p)) return p;
2015
- const rel = path10.relative(ctx.paths.root, p);
2268
+ if (!p || !path11.isAbsolute(p)) return p;
2269
+ const rel = path11.relative(ctx.paths.root, p);
2016
2270
  return rel.startsWith("..") ? p : rel;
2017
2271
  });
2018
2272
  const invalidPaths = normalizedFiles.filter(
2019
- (p) => !existsSync20(path10.resolve(ctx.paths.root, p))
2273
+ (p) => !existsSync20(path11.resolve(ctx.paths.root, p))
2020
2274
  );
2021
2275
  if (invalidPaths.length > 0) {
2022
2276
  console.warn(`[haive] session end: anchor path(s) not found: ${invalidPaths.join(", ")}`);
@@ -2067,7 +2321,7 @@ async function memSessionEnd(input, ctx) {
2067
2321
  frontmatter.id,
2068
2322
  frontmatter.module
2069
2323
  );
2070
- await mkdir7(path10.dirname(file), { recursive: true });
2324
+ await mkdir7(path11.dirname(file), { recursive: true });
2071
2325
  await writeFile13(file, serializeMemory10({ frontmatter, body }), "utf8");
2072
2326
  await clearPendingDistill(ctx);
2073
2327
  return {
@@ -2211,7 +2465,7 @@ async function loadModuleContexts2(ctx, modules) {
2211
2465
  const out = [];
2212
2466
  for (const m of modules) {
2213
2467
  if (!available.has(m)) continue;
2214
- const file = path11.join(ctx.paths.modulesContextDir, m, "context.md");
2468
+ const file = path12.join(ctx.paths.modulesContextDir, m, "context.md");
2215
2469
  if (existsSync21(file)) {
2216
2470
  out.push({ name: m, content: await readFile6(file, "utf8") });
2217
2471
  }
@@ -2830,7 +3084,7 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
2830
3084
  };
2831
3085
  }
2832
3086
  async function detectRunCommands(root) {
2833
- const pkgPath = path12.join(root, "package.json");
3087
+ const pkgPath = path13.join(root, "package.json");
2834
3088
  if (!existsSync22(pkgPath)) return null;
2835
3089
  try {
2836
3090
  const pkg = JSON.parse(await readFile7(pkgPath, "utf8"));
@@ -2898,7 +3152,7 @@ function oneLine(value) {
2898
3152
  return value.replace(/\s+/g, " ").replace(/"/g, '\\"').trim().slice(0, 120);
2899
3153
  }
2900
3154
  function serverVersion() {
2901
- return true ? "0.39.1" : "dev";
3155
+ return true ? "0.42.1" : "dev";
2902
3156
  }
2903
3157
  var CodeMapInputSchema = {
2904
3158
  file: z21.string().optional().describe("Filter to files whose path contains this substring"),
@@ -4225,7 +4479,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
4225
4479
  };
4226
4480
  }
4227
4481
  var SERVER_NAME = "hivelore";
4228
- var SERVER_VERSION = "0.39.1";
4482
+ var SERVER_VERSION = "0.42.1";
4229
4483
  function jsonResult(data) {
4230
4484
  return {
4231
4485
  content: [
@@ -5171,6 +5425,10 @@ async function runHaiveMcpStdio(options) {
5171
5425
  }
5172
5426
 
5173
5427
  export {
5428
+ astEngineAvailable,
5429
+ astLangForPath,
5430
+ runAstPattern,
5431
+ runAstSensorOnContent,
5174
5432
  readPresumedCorrectTargets,
5175
5433
  proposeSensor,
5176
5434
  memTried,
@@ -5201,4 +5459,4 @@ export {
5201
5459
  printHaiveMcpVersion,
5202
5460
  runHaiveMcpStdio
5203
5461
  };
5204
- //# sourceMappingURL=chunk-YMIQAOFL.js.map
5462
+ //# sourceMappingURL=chunk-EJ7A4IKD.js.map