@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 +299 -41
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +40 -2
- package/dist/server.js +303 -41
- package/dist/server.js.map +1 -1
- package/package.json +6 -3
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
|
|
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
|
|
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 =
|
|
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
|
|
1266
|
-
const
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
1752
|
+
let dir = path8.resolve(root, rel);
|
|
1495
1753
|
try {
|
|
1496
|
-
if (!statSync(dir).isDirectory()) dir =
|
|
1754
|
+
if (!statSync(dir).isDirectory()) dir = path8.dirname(dir);
|
|
1497
1755
|
} catch {
|
|
1498
|
-
if (
|
|
1756
|
+
if (path8.extname(dir)) dir = path8.dirname(dir);
|
|
1499
1757
|
}
|
|
1500
1758
|
while (dir.startsWith(root)) {
|
|
1501
|
-
const pkgJson =
|
|
1759
|
+
const pkgJson = path8.join(dir, "package.json");
|
|
1502
1760
|
const hasPkg = existsSync17(pkgJson);
|
|
1503
|
-
const goMod = existsSync17(
|
|
1504
|
-
const pySignal = PY_SIGNALS.some((s) => existsSync17(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
1993
|
+
import path10 from "path";
|
|
1736
1994
|
import { execSync as execSync2 } from "child_process";
|
|
1737
1995
|
function pendingDistillPath(ctx) {
|
|
1738
|
-
return
|
|
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 =
|
|
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 || !
|
|
1938
|
-
const rel =
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
};
|