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