@hivelore/mcp 0.39.2 → 0.43.2
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 +375 -58
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +60 -5
- package/dist/server.js +379 -58
- package/dist/server.js.map +1 -1
- package/package.json +10 -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,
|
|
@@ -1134,25 +1134,146 @@ import {
|
|
|
1134
1134
|
import { z as z16 } from "zod";
|
|
1135
1135
|
|
|
1136
1136
|
// src/tools/propose-sensor.ts
|
|
1137
|
-
import {
|
|
1137
|
+
import { execFileSync } 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
|
+
var registeredDynamicLanguages = /* @__PURE__ */ new Set();
|
|
1158
|
+
async function loadDynamicLanguages(engine) {
|
|
1159
|
+
const registrations = {};
|
|
1160
|
+
const candidates = [
|
|
1161
|
+
["python", () => import("@ast-grep/lang-python")],
|
|
1162
|
+
["go", () => import("@ast-grep/lang-go")],
|
|
1163
|
+
["rust", () => import("@ast-grep/lang-rust")],
|
|
1164
|
+
["java", () => import("@ast-grep/lang-java")]
|
|
1165
|
+
];
|
|
1166
|
+
for (const [name, load] of candidates) {
|
|
1167
|
+
try {
|
|
1168
|
+
const mod = await load();
|
|
1169
|
+
registrations[name] = mod.default;
|
|
1170
|
+
registeredDynamicLanguages.add(name);
|
|
1171
|
+
} catch {
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
if (Object.keys(registrations).length > 0) {
|
|
1175
|
+
engine.registerDynamicLanguage(registrations);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
async function loadAstEngine() {
|
|
1179
|
+
if (cachedEngine !== void 0) return cachedEngine;
|
|
1180
|
+
try {
|
|
1181
|
+
cachedEngine = await import("@ast-grep/napi");
|
|
1182
|
+
await loadDynamicLanguages(cachedEngine);
|
|
1183
|
+
} catch {
|
|
1184
|
+
cachedEngine = null;
|
|
1185
|
+
}
|
|
1186
|
+
return cachedEngine;
|
|
1187
|
+
}
|
|
1188
|
+
async function astEngineAvailable() {
|
|
1189
|
+
return await loadAstEngine() !== null;
|
|
1190
|
+
}
|
|
1191
|
+
function astLangForPath(filePath, explicitLanguage) {
|
|
1192
|
+
if (explicitLanguage) {
|
|
1193
|
+
const normalized = explicitLanguage.toLowerCase();
|
|
1194
|
+
if (["typescript", "tsx", "javascript", "html", "css"].includes(normalized)) {
|
|
1195
|
+
return normalized === "typescript" ? "TypeScript" : normalized === "tsx" ? "Tsx" : normalized === "javascript" ? "JavaScript" : normalized[0].toUpperCase() + normalized.slice(1);
|
|
1196
|
+
}
|
|
1197
|
+
return registeredDynamicLanguages.has(normalized) ? normalized : null;
|
|
1198
|
+
}
|
|
1199
|
+
const ext = path5.extname(filePath).toLowerCase();
|
|
1200
|
+
if (ext === ".ts" || ext === ".mts" || ext === ".cts") return "TypeScript";
|
|
1201
|
+
if (ext === ".tsx") return "Tsx";
|
|
1202
|
+
if (ext === ".js" || ext === ".jsx" || ext === ".mjs" || ext === ".cjs") return "JavaScript";
|
|
1203
|
+
if (ext === ".py" && registeredDynamicLanguages.has("python")) return "python";
|
|
1204
|
+
if (ext === ".go" && registeredDynamicLanguages.has("go")) return "go";
|
|
1205
|
+
if (ext === ".rs" && registeredDynamicLanguages.has("rust")) return "rust";
|
|
1206
|
+
if (ext === ".java" && registeredDynamicLanguages.has("java")) return "java";
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
function absentPresentInNode(node, absent) {
|
|
1210
|
+
try {
|
|
1211
|
+
if (node.find(absent)) return true;
|
|
1212
|
+
} catch {
|
|
1213
|
+
}
|
|
1214
|
+
const text = node.text();
|
|
1215
|
+
try {
|
|
1216
|
+
return new RegExp(absent).test(text);
|
|
1217
|
+
} catch {
|
|
1218
|
+
return text.includes(absent);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
async function runAstPattern(content, filePath, pattern, absent, rule, language) {
|
|
1222
|
+
const engine = await loadAstEngine();
|
|
1223
|
+
if (!engine) return { status: "engine-missing", matches: [] };
|
|
1224
|
+
const langName = astLangForPath(filePath, language);
|
|
1225
|
+
if (!langName) return { status: "unsupported-language", matches: [] };
|
|
1226
|
+
const lang = langName in engine.Lang ? engine.Lang[langName] : langName;
|
|
1227
|
+
let root;
|
|
1228
|
+
try {
|
|
1229
|
+
root = engine.parse(lang, content).root();
|
|
1230
|
+
} catch (err) {
|
|
1231
|
+
return { status: "parse-error", matches: [], detail: String(err).slice(0, 200) };
|
|
1232
|
+
}
|
|
1233
|
+
let nodes;
|
|
1234
|
+
try {
|
|
1235
|
+
const matcher = rule ? { rule: pattern ? { all: [{ pattern }, rule] } : rule } : pattern;
|
|
1236
|
+
if (!matcher) return { status: "invalid-pattern", matches: [], detail: "missing pattern/rule" };
|
|
1237
|
+
nodes = root.findAll(matcher);
|
|
1238
|
+
} catch (err) {
|
|
1239
|
+
return { status: "invalid-pattern", matches: [], detail: String(err).slice(0, 200) };
|
|
1240
|
+
}
|
|
1241
|
+
const matches = [];
|
|
1242
|
+
for (const node of nodes) {
|
|
1243
|
+
if (absent && absentPresentInNode(node, absent)) continue;
|
|
1244
|
+
const range = node.range();
|
|
1245
|
+
matches.push({
|
|
1246
|
+
startLine: range.start.line + 1,
|
|
1247
|
+
endLine: range.end.line + 1,
|
|
1248
|
+
text: node.text().trim().slice(0, 200)
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
return { status: "ok", matches };
|
|
1252
|
+
}
|
|
1253
|
+
async function runAstSensorOnContent(input) {
|
|
1254
|
+
const scan = await runAstPattern(input.content, input.filePath, input.pattern, input.absent, input.rule, input.language);
|
|
1255
|
+
if (scan.status !== "ok" || !input.addedLines || input.addedLines.size === 0) return scan;
|
|
1256
|
+
const added = input.addedLines;
|
|
1257
|
+
return {
|
|
1258
|
+
status: "ok",
|
|
1259
|
+
matches: scan.matches.filter((m) => {
|
|
1260
|
+
for (let line = m.startLine; line <= m.endLine; line++) {
|
|
1261
|
+
if (added.has(line)) return true;
|
|
1262
|
+
}
|
|
1263
|
+
return false;
|
|
1264
|
+
})
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// src/tools/propose-sensor.ts
|
|
1150
1269
|
var ProposeSensorInputSchema = {
|
|
1151
1270
|
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."
|
|
1271
|
+
kind: z15.enum(["regex", "ast", "shell", "test"]).default("regex").describe(
|
|
1272
|
+
"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
1273
|
),
|
|
1155
|
-
pattern: z15.string().optional().describe("kind=regex: regex matching the
|
|
1274
|
+
pattern: z15.string().optional().describe("kind=regex: regex matching the faulty usage; kind=ast: optional structural pattern (may be combined with `rule`)."),
|
|
1275
|
+
rule: z15.record(z15.unknown()).optional().describe("kind=ast: full ast-grep Rule object (kind/inside/has/not/all/any/etc.). May be used alone or combined with pattern."),
|
|
1276
|
+
language: z15.string().optional().describe("kind=ast: explicit built-in/dynamic language name for non-standard file extensions."),
|
|
1156
1277
|
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."),
|
|
1157
1278
|
timeout_ms: z15.number().int().positive().optional().describe("kind=shell|test: max runtime before the executor kills the command (default 120000)."),
|
|
1158
1279
|
absent: z15.string().optional().describe(
|
|
@@ -1164,6 +1285,9 @@ var ProposeSensorInputSchema = {
|
|
|
1164
1285
|
incident: z15.string().optional().describe(
|
|
1165
1286
|
"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
1287
|
),
|
|
1288
|
+
red_ref: z15.string().optional().describe(
|
|
1289
|
+
"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."
|
|
1290
|
+
),
|
|
1167
1291
|
flags: z15.string().optional().describe("Optional regex flags (e.g. 'i' for case-insensitive)."),
|
|
1168
1292
|
paths: z15.array(z15.string()).default([]).describe("Override scope paths. Defaults to the memory's anchor paths.")
|
|
1169
1293
|
};
|
|
@@ -1180,7 +1304,7 @@ async function readPresumedCorrectTargets(root, relPaths) {
|
|
|
1180
1304
|
const targets = [];
|
|
1181
1305
|
for (const rel of relPaths) {
|
|
1182
1306
|
try {
|
|
1183
|
-
const content =
|
|
1307
|
+
const content = execFileSync("git", ["show", `HEAD:./${rel}`], {
|
|
1184
1308
|
cwd: root,
|
|
1185
1309
|
encoding: "utf8",
|
|
1186
1310
|
maxBuffer: 10 * 1024 * 1024,
|
|
@@ -1190,7 +1314,7 @@ async function readPresumedCorrectTargets(root, relPaths) {
|
|
|
1190
1314
|
continue;
|
|
1191
1315
|
} catch {
|
|
1192
1316
|
}
|
|
1193
|
-
const abs =
|
|
1317
|
+
const abs = path6.resolve(root, rel);
|
|
1194
1318
|
if (!existsSync15(abs)) continue;
|
|
1195
1319
|
try {
|
|
1196
1320
|
targets.push({ path: rel, content: await readFile3(abs, "utf8") });
|
|
@@ -1201,11 +1325,13 @@ async function readPresumedCorrectTargets(root, relPaths) {
|
|
|
1201
1325
|
}
|
|
1202
1326
|
function runCommandForValidation(command, root, timeoutMs = 12e4) {
|
|
1203
1327
|
try {
|
|
1204
|
-
|
|
1328
|
+
execFileSync("bash", ["-c", command], {
|
|
1205
1329
|
cwd: root,
|
|
1206
1330
|
timeout: timeoutMs,
|
|
1207
1331
|
maxBuffer: 8 * 1024 * 1024,
|
|
1208
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1332
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1333
|
+
// Same containment as the gate executor: an oracle gets a test-runner env, not credentials.
|
|
1334
|
+
env: { ...scrubbedCommandEnv(process.env), HIVELORE_SENSOR: "validation" }
|
|
1209
1335
|
});
|
|
1210
1336
|
return { status: "passed", detail: "exit 0" };
|
|
1211
1337
|
} catch (err) {
|
|
@@ -1219,6 +1345,48 @@ ${e.stderr?.toString() ?? ""}`.split("\n").filter(Boolean).slice(-8).join("\n");
|
|
|
1219
1345
|
return { status: "failed", detail: out || `exit ${e.status ?? "?"}` };
|
|
1220
1346
|
}
|
|
1221
1347
|
}
|
|
1348
|
+
function proveRedOnIncident(command, root, redRef, timeoutMs) {
|
|
1349
|
+
const worktree = path6.join(os.tmpdir(), `hivelore-red-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
1350
|
+
let added = false;
|
|
1351
|
+
try {
|
|
1352
|
+
try {
|
|
1353
|
+
execFileSync("git", ["worktree", "add", "--detach", worktree, redRef], {
|
|
1354
|
+
cwd: root,
|
|
1355
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1356
|
+
timeout: 6e4
|
|
1357
|
+
});
|
|
1358
|
+
added = true;
|
|
1359
|
+
} catch (err) {
|
|
1360
|
+
const e = err;
|
|
1361
|
+
return { proven: false, reason: "red-ref-invalid", detail: (e.stderr?.toString() ?? String(err)).slice(0, 300) };
|
|
1362
|
+
}
|
|
1363
|
+
const mainModules = path6.join(root, "node_modules");
|
|
1364
|
+
const wtModules = path6.join(worktree, "node_modules");
|
|
1365
|
+
if (existsSync15(mainModules) && !existsSync15(wtModules)) {
|
|
1366
|
+
try {
|
|
1367
|
+
symlinkSync(mainModules, wtModules, "dir");
|
|
1368
|
+
} catch {
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
const run = runCommandForValidation(command, worktree, timeoutMs);
|
|
1372
|
+
if (run.status === "failed") return { proven: true, detail: run.detail };
|
|
1373
|
+
if (run.status === "passed") {
|
|
1374
|
+
return { proven: false, reason: "red-not-proven", detail: "oracle PASSED on the incident state \u2014 it does not catch the incident" };
|
|
1375
|
+
}
|
|
1376
|
+
return { proven: false, reason: "red-unrunnable", detail: run.detail };
|
|
1377
|
+
} finally {
|
|
1378
|
+
if (added) {
|
|
1379
|
+
try {
|
|
1380
|
+
execFileSync("git", ["worktree", "remove", "--force", worktree], { cwd: root, stdio: "ignore", timeout: 6e4 });
|
|
1381
|
+
} catch {
|
|
1382
|
+
try {
|
|
1383
|
+
rmSync(worktree, { recursive: true, force: true });
|
|
1384
|
+
} catch {
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1222
1390
|
async function proposeSensor(input, ctx) {
|
|
1223
1391
|
if (!existsSync15(ctx.paths.memoriesDir)) {
|
|
1224
1392
|
throw new Error(`No .ai/memories at ${ctx.paths.root}. Run 'hivelore init' first.`);
|
|
@@ -1248,6 +1416,17 @@ async function proposeSensor(input, ctx) {
|
|
|
1248
1416
|
self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
|
|
1249
1417
|
};
|
|
1250
1418
|
}
|
|
1419
|
+
} else if (kind === "ast") {
|
|
1420
|
+
if (!input.pattern?.trim() && !input.rule) {
|
|
1421
|
+
return {
|
|
1422
|
+
accepted: false,
|
|
1423
|
+
memory_id: input.memory_id,
|
|
1424
|
+
severity: input.severity,
|
|
1425
|
+
reason: "invalid-pattern",
|
|
1426
|
+
guidance: "kind=ast requires a structural `pattern` or a full ast-grep `rule` object.",
|
|
1427
|
+
self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1251
1430
|
} else if (!input.command?.trim()) {
|
|
1252
1431
|
return {
|
|
1253
1432
|
accepted: false,
|
|
@@ -1264,12 +1443,111 @@ async function proposeSensor(input, ctx) {
|
|
|
1264
1443
|
throw new Error(`No memory found with id ${input.memory_id}`);
|
|
1265
1444
|
}
|
|
1266
1445
|
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
|
|
1446
|
+
if (kind === "ast") {
|
|
1447
|
+
const pattern = input.pattern?.trim();
|
|
1448
|
+
if (!await astEngineAvailable() && input.severity === "block") {
|
|
1449
|
+
return {
|
|
1450
|
+
accepted: false,
|
|
1451
|
+
memory_id: input.memory_id,
|
|
1452
|
+
severity: input.severity,
|
|
1453
|
+
reason: "ast-engine-missing",
|
|
1454
|
+
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.",
|
|
1455
|
+
self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
const brittleAst = pattern ? sensorPatternBrittleness(pattern) : null;
|
|
1459
|
+
if (brittleAst && input.severity === "block") {
|
|
1460
|
+
return {
|
|
1461
|
+
accepted: false,
|
|
1462
|
+
memory_id: input.memory_id,
|
|
1463
|
+
severity: input.severity,
|
|
1464
|
+
reason: "brittle",
|
|
1465
|
+
guidance: `The pattern is brittle (${brittleAst}). Use a durable structural pattern, then re-propose.`,
|
|
1466
|
+
self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
const anchorPathsAst = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
|
|
1470
|
+
const currentTargetsAst = await readPresumedCorrectTargets(ctx.paths.root, anchorPathsAst);
|
|
1471
|
+
const firedOnAst = [];
|
|
1472
|
+
for (const target of currentTargetsAst) {
|
|
1473
|
+
const scan = await runAstSensorOnContent({ pattern, rule: input.rule, language: input.language, absent: input.absent, content: target.content, filePath: target.path });
|
|
1474
|
+
if (scan.status === "invalid-pattern") {
|
|
1475
|
+
return {
|
|
1476
|
+
accepted: false,
|
|
1477
|
+
memory_id: input.memory_id,
|
|
1478
|
+
severity: input.severity,
|
|
1479
|
+
reason: "invalid-pattern",
|
|
1480
|
+
guidance: `The ast-grep pattern is invalid: ${scan.detail ?? "unparseable"}. Fix it and re-propose.`,
|
|
1481
|
+
self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
if (scan.status === "ok" && scan.matches.length > 0) firedOnAst.push(target.path);
|
|
1485
|
+
}
|
|
1486
|
+
if (firedOnAst.length > 0 && input.severity === "block") {
|
|
1487
|
+
return {
|
|
1488
|
+
accepted: false,
|
|
1489
|
+
memory_id: input.memory_id,
|
|
1490
|
+
severity: input.severity,
|
|
1491
|
+
reason: "fires-on-current",
|
|
1492
|
+
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.`,
|
|
1493
|
+
self_check: { silent_on_current: false, fires_on_bad: null, fired_on: firedOnAst }
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
const badExamplesAst = [
|
|
1497
|
+
...input.bad_example ? [input.bad_example] : [],
|
|
1498
|
+
...extractSensorExamples(found.memory.body)
|
|
1499
|
+
];
|
|
1500
|
+
let firesOnBadAst = null;
|
|
1501
|
+
if (badExamplesAst.length > 0 && await astEngineAvailable()) {
|
|
1502
|
+
const exampleLang = anchorPathsAst.find((p) => astLangForPath(p, input.language) !== null) ?? "example.tsx";
|
|
1503
|
+
firesOnBadAst = false;
|
|
1504
|
+
for (const example of badExamplesAst) {
|
|
1505
|
+
const scan = await runAstSensorOnContent({ pattern, rule: input.rule, language: input.language, absent: input.absent, content: example, filePath: exampleLang });
|
|
1506
|
+
if (scan.status === "ok" && scan.matches.length > 0) {
|
|
1507
|
+
firesOnBadAst = true;
|
|
1508
|
+
break;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
if (firesOnBadAst === false && input.severity === "block") {
|
|
1513
|
+
return {
|
|
1514
|
+
accepted: false,
|
|
1515
|
+
memory_id: input.memory_id,
|
|
1516
|
+
severity: input.severity,
|
|
1517
|
+
reason: "missed-bad-example",
|
|
1518
|
+
guidance: "The pattern did not match the bad example structurally, so it won't catch the mistake. Adjust it, then re-propose.",
|
|
1519
|
+
self_check: { silent_on_current: firedOnAst.length === 0, fires_on_bad: false, fired_on: [] }
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
const sensorAst = {
|
|
1523
|
+
kind: "ast",
|
|
1524
|
+
...pattern ? { pattern } : {},
|
|
1525
|
+
...input.rule ? { rule: input.rule } : {},
|
|
1526
|
+
...input.language ? { language: input.language } : {},
|
|
1527
|
+
...input.absent ? { absent: input.absent } : {},
|
|
1528
|
+
paths: anchorPathsAst,
|
|
1529
|
+
message: input.message?.trim() || deriveMessage(found.memory.body, pattern ?? "AST rule", input.absent),
|
|
1530
|
+
...input.incident?.trim() ? { incident: input.incident.trim() } : {},
|
|
1531
|
+
severity: input.severity,
|
|
1532
|
+
autogen: false,
|
|
1533
|
+
last_fired: null
|
|
1534
|
+
};
|
|
1535
|
+
await writeFile8(found.filePath, serializeMemory7({ frontmatter: { ...found.memory.frontmatter, sensor: sensorAst }, body: found.memory.body }), "utf8");
|
|
1536
|
+
return {
|
|
1537
|
+
accepted: true,
|
|
1538
|
+
memory_id: input.memory_id,
|
|
1539
|
+
severity: input.severity,
|
|
1540
|
+
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,
|
|
1541
|
+
self_check: { silent_on_current: firedOnAst.length === 0, fires_on_bad: firesOnBadAst, fired_on: firedOnAst },
|
|
1542
|
+
file_path: found.filePath
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
if (kind === "shell" || kind === "test") {
|
|
1546
|
+
const referencedTests = extractTestFilePathsFromCommand(input.command.trim()).filter((rel) => existsSync15(path6.resolve(ctx.paths.root, rel)));
|
|
1269
1547
|
const pendingTests = [];
|
|
1270
1548
|
for (const rel of referencedTests) {
|
|
1271
1549
|
try {
|
|
1272
|
-
if (hasPendingTestMarker(await readFile3(
|
|
1550
|
+
if (hasPendingTestMarker(await readFile3(path6.resolve(ctx.paths.root, rel), "utf8"))) pendingTests.push(rel);
|
|
1273
1551
|
} catch {
|
|
1274
1552
|
}
|
|
1275
1553
|
}
|
|
@@ -1285,6 +1563,21 @@ async function proposeSensor(input, ctx) {
|
|
|
1285
1563
|
}
|
|
1286
1564
|
const verdictCmd = runCommandForValidation(input.command.trim(), ctx.paths.root, input.timeout_ms);
|
|
1287
1565
|
const anchorPathsCmd = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
|
|
1566
|
+
let redProven = false;
|
|
1567
|
+
if (input.red_ref?.trim()) {
|
|
1568
|
+
const red = proveRedOnIncident(input.command.trim(), ctx.paths.root, input.red_ref.trim(), input.timeout_ms);
|
|
1569
|
+
if (!red.proven && input.severity === "block") {
|
|
1570
|
+
return {
|
|
1571
|
+
accepted: false,
|
|
1572
|
+
memory_id: input.memory_id,
|
|
1573
|
+
severity: input.severity,
|
|
1574
|
+
reason: red.reason ?? "red-not-proven",
|
|
1575
|
+
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),
|
|
1576
|
+
self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: false, fired_on: [] }
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
redProven = red.proven;
|
|
1580
|
+
}
|
|
1288
1581
|
if (verdictCmd.status !== "passed" && input.severity === "block") {
|
|
1289
1582
|
return {
|
|
1290
1583
|
accepted: false,
|
|
@@ -1296,6 +1589,16 @@ ${verdictCmd.detail}`,
|
|
|
1296
1589
|
self_check: { silent_on_current: false, fires_on_bad: null, fired_on: anchorPathsCmd }
|
|
1297
1590
|
};
|
|
1298
1591
|
}
|
|
1592
|
+
if (input.severity === "block" && !input.red_ref?.trim()) {
|
|
1593
|
+
return {
|
|
1594
|
+
accepted: false,
|
|
1595
|
+
memory_id: input.memory_id,
|
|
1596
|
+
severity: input.severity,
|
|
1597
|
+
reason: "red-required",
|
|
1598
|
+
guidance: "A blocking shell/test sensor requires `red_ref`: the oracle must PASS on the current tree and FAIL on the pre-fix incident state. Pass the incident commit/ref, or propose the sensor at warn severity until RED can be proven.",
|
|
1599
|
+
self_check: { silent_on_current: true, fires_on_bad: null, fired_on: [] }
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1299
1602
|
const sensorCmd = {
|
|
1300
1603
|
kind,
|
|
1301
1604
|
command: input.command.trim(),
|
|
@@ -1303,6 +1606,7 @@ ${verdictCmd.detail}`,
|
|
|
1303
1606
|
paths: anchorPathsCmd,
|
|
1304
1607
|
message: input.message?.trim() || deriveMessage(found.memory.body, input.command.trim(), void 0),
|
|
1305
1608
|
...input.incident?.trim() ? { incident: input.incident.trim() } : {},
|
|
1609
|
+
...redProven ? { red_proven: true } : {},
|
|
1306
1610
|
severity: input.severity,
|
|
1307
1611
|
autogen: false,
|
|
1308
1612
|
last_fired: null
|
|
@@ -1316,8 +1620,9 @@ ${verdictCmd.detail}`,
|
|
|
1316
1620
|
accepted: true,
|
|
1317
1621
|
memory_id: input.memory_id,
|
|
1318
1622
|
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: [] }
|
|
1623
|
+
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." : " This warn-only oracle is not RED-proven; pass red_ref before promoting it to block.") + (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,
|
|
1624
|
+
self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: redProven ? true : null, fired_on: [] },
|
|
1625
|
+
file_path: found.filePath
|
|
1321
1626
|
};
|
|
1322
1627
|
}
|
|
1323
1628
|
const anchorPaths = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
|
|
@@ -1384,14 +1689,17 @@ var MemTriedInputSchema = {
|
|
|
1384
1689
|
paths: z16.array(z16.string()).default([]).describe("Anchor file paths this applies to"),
|
|
1385
1690
|
author: z16.string().optional().describe("Author handle or email"),
|
|
1386
1691
|
sensor: z16.object({
|
|
1387
|
-
kind: z16.enum(["regex", "shell", "test"]).default("regex").describe("regex pattern, or a shell/test
|
|
1388
|
-
pattern: z16.string().optional().describe("kind=regex:
|
|
1692
|
+
kind: z16.enum(["regex", "ast", "shell", "test"]).default("regex").describe("regex/AST pattern, or a shell/test command"),
|
|
1693
|
+
pattern: z16.string().optional().describe("kind=regex|ast: pattern matching the faulty usage"),
|
|
1694
|
+
rule: z16.record(z16.unknown()).optional().describe("kind=ast: full ast-grep Rule object"),
|
|
1695
|
+
language: z16.string().optional().describe("kind=ast: explicit built-in/dynamic language"),
|
|
1389
1696
|
command: z16.string().optional().describe("kind=shell|test: command the gate runs when the diff touches the sensor's paths (non-zero exit = lesson fires)"),
|
|
1390
1697
|
timeout_ms: z16.number().int().positive().optional().describe("kind=shell|test: max runtime (default 120000)"),
|
|
1391
1698
|
absent: z16.string().optional().describe("kind=regex: regex marking CORRECT usage nearby \u2014 excludes it from firing"),
|
|
1392
1699
|
severity: z16.enum(["warn", "block"]).default("block").describe("block = deterministic gate refusal"),
|
|
1393
1700
|
message: z16.string().optional().describe("Self-correction message shown when the sensor fires"),
|
|
1394
1701
|
incident: z16.string().optional().describe("Provenance: the incident this sensor guards (e.g. 'prod #442') \u2014 surfaced when it fires and in the receipt"),
|
|
1702
|
+
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
1703
|
bad_example: z16.string().optional().describe("kind=regex: code snippet the sensor MUST fire on (validation)")
|
|
1396
1704
|
}).optional().describe(
|
|
1397
1705
|
"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 +1731,7 @@ async function memTried(input, ctx) {
|
|
|
1423
1731
|
}
|
|
1424
1732
|
const body = lines.join("\n") + "\n";
|
|
1425
1733
|
const file = memoryFilePath2(ctx.paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
1426
|
-
await mkdir3(
|
|
1734
|
+
await mkdir3(path7.dirname(file), { recursive: true });
|
|
1427
1735
|
if (existsSync16(file)) {
|
|
1428
1736
|
throw new Error(`Memory already exists at ${file}`);
|
|
1429
1737
|
}
|
|
@@ -1434,12 +1742,15 @@ async function memTried(input, ctx) {
|
|
|
1434
1742
|
memory_id: frontmatter.id,
|
|
1435
1743
|
kind: input.sensor.kind ?? "regex",
|
|
1436
1744
|
pattern: input.sensor.pattern,
|
|
1745
|
+
rule: input.sensor.rule,
|
|
1746
|
+
language: input.sensor.language,
|
|
1437
1747
|
command: input.sensor.command,
|
|
1438
1748
|
timeout_ms: input.sensor.timeout_ms,
|
|
1439
1749
|
absent: input.sensor.absent,
|
|
1440
1750
|
severity: input.sensor.severity ?? "block",
|
|
1441
1751
|
message: input.sensor.message,
|
|
1442
1752
|
incident: input.sensor.incident,
|
|
1753
|
+
red_ref: input.sensor.red_ref,
|
|
1443
1754
|
bad_example: input.sensor.bad_example,
|
|
1444
1755
|
flags: void 0,
|
|
1445
1756
|
paths: []
|
|
@@ -1481,7 +1792,7 @@ async function memTried(input, ctx) {
|
|
|
1481
1792
|
// src/tools/scaffold-test.ts
|
|
1482
1793
|
import { existsSync as existsSync17, statSync } from "fs";
|
|
1483
1794
|
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile10 } from "fs/promises";
|
|
1484
|
-
import
|
|
1795
|
+
import path8 from "path";
|
|
1485
1796
|
import { z as z17 } from "zod";
|
|
1486
1797
|
import {
|
|
1487
1798
|
buildProposeCommand,
|
|
@@ -1493,17 +1804,17 @@ import {
|
|
|
1493
1804
|
} from "@hivelore/core";
|
|
1494
1805
|
var PY_SIGNALS = ["pyproject.toml", "setup.py", "pytest.ini", "requirements.txt", "tox.ini"];
|
|
1495
1806
|
async function detectForAnchor(root, rel) {
|
|
1496
|
-
let dir =
|
|
1807
|
+
let dir = path8.resolve(root, rel);
|
|
1497
1808
|
try {
|
|
1498
|
-
if (!statSync(dir).isDirectory()) dir =
|
|
1809
|
+
if (!statSync(dir).isDirectory()) dir = path8.dirname(dir);
|
|
1499
1810
|
} catch {
|
|
1500
|
-
if (
|
|
1811
|
+
if (path8.extname(dir)) dir = path8.dirname(dir);
|
|
1501
1812
|
}
|
|
1502
1813
|
while (dir.startsWith(root)) {
|
|
1503
|
-
const pkgJson =
|
|
1814
|
+
const pkgJson = path8.join(dir, "package.json");
|
|
1504
1815
|
const hasPkg = existsSync17(pkgJson);
|
|
1505
|
-
const goMod = existsSync17(
|
|
1506
|
-
const pySignal = PY_SIGNALS.some((s) => existsSync17(
|
|
1816
|
+
const goMod = existsSync17(path8.join(dir, "go.mod"));
|
|
1817
|
+
const pySignal = PY_SIGNALS.some((s) => existsSync17(path8.join(dir, s)));
|
|
1507
1818
|
if (hasPkg || goMod || pySignal) {
|
|
1508
1819
|
let pkg = null;
|
|
1509
1820
|
if (hasPkg) {
|
|
@@ -1513,10 +1824,10 @@ async function detectForAnchor(root, rel) {
|
|
|
1513
1824
|
pkg = null;
|
|
1514
1825
|
}
|
|
1515
1826
|
}
|
|
1516
|
-
const baseDir =
|
|
1827
|
+
const baseDir = path8.relative(root, dir).split(path8.sep).join("/");
|
|
1517
1828
|
return { framework: pickTestFramework(pkg, { goMod, pySignal }), baseDir };
|
|
1518
1829
|
}
|
|
1519
|
-
const parent =
|
|
1830
|
+
const parent = path8.dirname(dir);
|
|
1520
1831
|
if (parent === dir || dir === root) break;
|
|
1521
1832
|
dir = parent;
|
|
1522
1833
|
}
|
|
@@ -1574,14 +1885,14 @@ async function scaffoldTest(input, ctx) {
|
|
|
1574
1885
|
}
|
|
1575
1886
|
const results = [];
|
|
1576
1887
|
for (const scaffold of scaffolds) {
|
|
1577
|
-
const abs =
|
|
1888
|
+
const abs = path8.isAbsolute(scaffold.relPath) ? scaffold.relPath : path8.resolve(ctx.paths.root, scaffold.relPath);
|
|
1578
1889
|
let written = false;
|
|
1579
1890
|
let alreadyExists = false;
|
|
1580
1891
|
if (input.write) {
|
|
1581
1892
|
if (existsSync17(abs)) {
|
|
1582
1893
|
alreadyExists = true;
|
|
1583
1894
|
} else {
|
|
1584
|
-
await mkdir4(
|
|
1895
|
+
await mkdir4(path8.dirname(abs), { recursive: true });
|
|
1585
1896
|
await writeFile10(abs, scaffold.content, "utf8");
|
|
1586
1897
|
written = true;
|
|
1587
1898
|
}
|
|
@@ -1615,7 +1926,7 @@ async function scaffoldTest(input, ctx) {
|
|
|
1615
1926
|
// src/tools/ingest-findings.ts
|
|
1616
1927
|
import { existsSync as existsSync18 } from "fs";
|
|
1617
1928
|
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile11 } from "fs/promises";
|
|
1618
|
-
import
|
|
1929
|
+
import path9 from "path";
|
|
1619
1930
|
import {
|
|
1620
1931
|
draftsFromFindings,
|
|
1621
1932
|
filterNewDrafts,
|
|
@@ -1646,7 +1957,7 @@ async function ingestFindings(input, ctx) {
|
|
|
1646
1957
|
if (input.report && input.report.trim()) {
|
|
1647
1958
|
raw = input.report;
|
|
1648
1959
|
} else if (input.report_path) {
|
|
1649
|
-
const file =
|
|
1960
|
+
const file = path9.resolve(ctx.paths.root, input.report_path);
|
|
1650
1961
|
if (!existsSync18(file)) throw new Error(`Report file not found: ${file}`);
|
|
1651
1962
|
raw = await readFile5(file, "utf8");
|
|
1652
1963
|
} else {
|
|
@@ -1700,7 +2011,7 @@ async function writeDraft(ctx, draft) {
|
|
|
1700
2011
|
draft.frontmatter.id,
|
|
1701
2012
|
draft.frontmatter.module
|
|
1702
2013
|
);
|
|
1703
|
-
await mkdir5(
|
|
2014
|
+
await mkdir5(path9.dirname(file), { recursive: true });
|
|
1704
2015
|
await writeFile11(file, serializeMemory9({ frontmatter: draft.frontmatter, body: draft.body }), "utf8");
|
|
1705
2016
|
return file;
|
|
1706
2017
|
}
|
|
@@ -1708,7 +2019,7 @@ async function writeDraft(ctx, draft) {
|
|
|
1708
2019
|
// src/tools/mem-session-end.ts
|
|
1709
2020
|
import { writeFile as writeFile13, mkdir as mkdir7 } from "fs/promises";
|
|
1710
2021
|
import { existsSync as existsSync20 } from "fs";
|
|
1711
|
-
import
|
|
2022
|
+
import path11 from "path";
|
|
1712
2023
|
import {
|
|
1713
2024
|
buildFrontmatter as buildFrontmatter3,
|
|
1714
2025
|
loadMemoriesFromDir as loadMemoriesFromDir16,
|
|
@@ -1726,10 +2037,10 @@ import {
|
|
|
1726
2037
|
} from "@hivelore/core";
|
|
1727
2038
|
import { mkdir as mkdir6, writeFile as writeFile12, rm } from "fs/promises";
|
|
1728
2039
|
import { existsSync as existsSync19 } from "fs";
|
|
1729
|
-
import
|
|
1730
|
-
import { execSync
|
|
2040
|
+
import path10 from "path";
|
|
2041
|
+
import { execSync } from "child_process";
|
|
1731
2042
|
function pendingDistillPath(ctx) {
|
|
1732
|
-
return
|
|
2043
|
+
return path10.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
|
|
1733
2044
|
}
|
|
1734
2045
|
var SessionTracker = class {
|
|
1735
2046
|
events = [];
|
|
@@ -1770,7 +2081,7 @@ var SessionTracker = class {
|
|
|
1770
2081
|
}
|
|
1771
2082
|
let gitDiff;
|
|
1772
2083
|
try {
|
|
1773
|
-
const raw =
|
|
2084
|
+
const raw = execSync("git diff HEAD", {
|
|
1774
2085
|
cwd: this.ctx.paths.root,
|
|
1775
2086
|
timeout: 5e3,
|
|
1776
2087
|
encoding: "utf8",
|
|
@@ -1802,7 +2113,7 @@ var SessionTracker = class {
|
|
|
1802
2113
|
const triedThreads = this.events.filter((e) => e.tool === "mem_tried").map((e) => e.summary ?? "").filter(Boolean);
|
|
1803
2114
|
let diffStat;
|
|
1804
2115
|
try {
|
|
1805
|
-
diffStat =
|
|
2116
|
+
diffStat = execSync("git diff --stat HEAD", {
|
|
1806
2117
|
cwd: this.ctx.paths.root,
|
|
1807
2118
|
timeout: 5e3,
|
|
1808
2119
|
encoding: "utf8",
|
|
@@ -1846,7 +2157,7 @@ var SessionTracker = class {
|
|
|
1846
2157
|
...gitDiff ? { git_diff: gitDiff } : {},
|
|
1847
2158
|
...recapId ? { recap_id: recapId } : {}
|
|
1848
2159
|
};
|
|
1849
|
-
const cacheDir =
|
|
2160
|
+
const cacheDir = path10.join(this.ctx.paths.haiveDir, ".cache");
|
|
1850
2161
|
await mkdir6(cacheDir, { recursive: true });
|
|
1851
2162
|
await writeFile12(
|
|
1852
2163
|
pendingDistillPath(this.ctx),
|
|
@@ -1928,12 +2239,12 @@ async function memSessionEnd(input, ctx) {
|
|
|
1928
2239
|
const body = buildBody(input);
|
|
1929
2240
|
const topic = recapTopic(input.scope, input.module);
|
|
1930
2241
|
const normalizedFiles = input.files_touched.map((p) => {
|
|
1931
|
-
if (!p || !
|
|
1932
|
-
const rel =
|
|
2242
|
+
if (!p || !path11.isAbsolute(p)) return p;
|
|
2243
|
+
const rel = path11.relative(ctx.paths.root, p);
|
|
1933
2244
|
return rel.startsWith("..") ? p : rel;
|
|
1934
2245
|
});
|
|
1935
2246
|
const invalidPaths = normalizedFiles.filter(
|
|
1936
|
-
(p) => !existsSync20(
|
|
2247
|
+
(p) => !existsSync20(path11.resolve(ctx.paths.root, p))
|
|
1937
2248
|
);
|
|
1938
2249
|
if (invalidPaths.length > 0) {
|
|
1939
2250
|
console.warn(`[haive] session end: anchor path(s) not found: ${invalidPaths.join(", ")}`);
|
|
@@ -1984,7 +2295,7 @@ async function memSessionEnd(input, ctx) {
|
|
|
1984
2295
|
frontmatter.id,
|
|
1985
2296
|
frontmatter.module
|
|
1986
2297
|
);
|
|
1987
|
-
await mkdir7(
|
|
2298
|
+
await mkdir7(path11.dirname(file), { recursive: true });
|
|
1988
2299
|
await writeFile13(file, serializeMemory10({ frontmatter, body }), "utf8");
|
|
1989
2300
|
await clearPendingDistill(ctx);
|
|
1990
2301
|
return {
|
|
@@ -1999,7 +2310,7 @@ async function memSessionEnd(input, ctx) {
|
|
|
1999
2310
|
// src/tools/get-briefing.ts
|
|
2000
2311
|
import { readFile as readFile7, writeFile as writeFile14, readdir as readdir4 } from "fs/promises";
|
|
2001
2312
|
import { existsSync as existsSync22 } from "fs";
|
|
2002
|
-
import
|
|
2313
|
+
import path13 from "path";
|
|
2003
2314
|
import {
|
|
2004
2315
|
allocateBudget,
|
|
2005
2316
|
assessBootstrapState,
|
|
@@ -2046,7 +2357,7 @@ import { z as z20 } from "zod";
|
|
|
2046
2357
|
// src/tools/briefing-helpers.ts
|
|
2047
2358
|
import { readdir as readdir3, readFile as readFile6 } from "fs/promises";
|
|
2048
2359
|
import { existsSync as existsSync21 } from "fs";
|
|
2049
|
-
import
|
|
2360
|
+
import path12 from "path";
|
|
2050
2361
|
import {
|
|
2051
2362
|
classifyMemoryPriority as coreClassifyPriority,
|
|
2052
2363
|
isGlobPath,
|
|
@@ -2186,7 +2497,7 @@ async function loadModuleContexts2(ctx, modules) {
|
|
|
2186
2497
|
const out = [];
|
|
2187
2498
|
for (const m of modules) {
|
|
2188
2499
|
if (!available.has(m)) continue;
|
|
2189
|
-
const file =
|
|
2500
|
+
const file = path12.join(ctx.paths.modulesContextDir, m, "context.md");
|
|
2190
2501
|
if (existsSync21(file)) {
|
|
2191
2502
|
out.push({ name: m, content: await readFile6(file, "utf8") });
|
|
2192
2503
|
}
|
|
@@ -2214,6 +2525,8 @@ var GetBriefingInputSchema = {
|
|
|
2214
2525
|
),
|
|
2215
2526
|
include_stale: z20.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
|
|
2216
2527
|
track: z20.boolean().default(true).describe("Increment read_count on returned memories"),
|
|
2528
|
+
memory_scopes: z20.array(z20.enum(["personal", "team", "module", "shared"])).optional().describe("Restrict the candidate corpus to selected scopes. Omit to include every scope."),
|
|
2529
|
+
deterministic: z20.boolean().optional().describe("Ignore machine-local usage/impact signals so repeated evaluations rank the shared corpus reproducibly."),
|
|
2217
2530
|
format: z20.enum(["full", "compact", "actions"]).default("full").describe(
|
|
2218
2531
|
"Output format: 'full' returns memory bodies (honors token budget via truncation); 'compact' returns a 1-line summary per memory (call mem_get for detail); 'actions' squeezes bodies to actionable bullet lines \u2014 fewer tokens vs full."
|
|
2219
2532
|
),
|
|
@@ -2242,9 +2555,11 @@ async function getBriefing(input, ctx) {
|
|
|
2242
2555
|
let searchMode = "literal";
|
|
2243
2556
|
let usage = { version: 1, updated_at: "", by_id: {} };
|
|
2244
2557
|
let byId = /* @__PURE__ */ new Map();
|
|
2558
|
+
const allowedScopes = input.memory_scopes ? new Set(input.memory_scopes) : null;
|
|
2559
|
+
const scopeAllowed = (loaded) => allowedScopes === null || allowedScopes.has(loaded.memory.frontmatter.scope);
|
|
2245
2560
|
let lastSession;
|
|
2246
2561
|
if (existsSync22(ctx.paths.memoriesDir)) {
|
|
2247
|
-
const allLoaded = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
|
|
2562
|
+
const allLoaded = (await loadMemoriesFromDir17(ctx.paths.memoriesDir)).filter(scopeAllowed);
|
|
2248
2563
|
const recaps = allLoaded.filter(({ memory }) => memory.frontmatter.type === "session_recap").sort(
|
|
2249
2564
|
(a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
|
|
2250
2565
|
);
|
|
@@ -2277,10 +2592,11 @@ async function getBriefing(input, ctx) {
|
|
|
2277
2592
|
if (memoryHasExcludedTag(memory.frontmatter, excludeTags)) return false;
|
|
2278
2593
|
return true;
|
|
2279
2594
|
});
|
|
2280
|
-
usage = await loadUsageIndex8(ctx.paths);
|
|
2595
|
+
usage = input.deterministic ? { version: 1, updated_at: "", by_id: {} } : await loadUsageIndex8(ctx.paths);
|
|
2281
2596
|
byId = new Map(allMemories.map((m) => [m.memory.frontmatter.id, m]));
|
|
2282
|
-
const
|
|
2283
|
-
|
|
2597
|
+
const semanticEnabled = input.semantic && input.deterministic !== true;
|
|
2598
|
+
const semanticHits = input.task && semanticEnabled ? await trySemanticHits(ctx, input.task, allMemories.length * 2) : null;
|
|
2599
|
+
if (input.task && semanticEnabled) {
|
|
2284
2600
|
searchMode = semanticHits ? "semantic" : "literal_fallback";
|
|
2285
2601
|
}
|
|
2286
2602
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -2616,7 +2932,7 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
2616
2932
|
}
|
|
2617
2933
|
}
|
|
2618
2934
|
if (existsSync22(ctx.paths.memoriesDir)) {
|
|
2619
|
-
const allMems = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
|
|
2935
|
+
const allMems = (await loadMemoriesFromDir17(ctx.paths.memoriesDir)).filter(scopeAllowed);
|
|
2620
2936
|
for (const { memory } of allMems) {
|
|
2621
2937
|
const fm = memory.frontmatter;
|
|
2622
2938
|
if (!fm.requires_human_approval) continue;
|
|
@@ -2673,7 +2989,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
2673
2989
|
pcRaw = await readFile7(ctx.paths.projectContext, "utf8");
|
|
2674
2990
|
} catch {
|
|
2675
2991
|
}
|
|
2676
|
-
const allForBootstrap = existsSync22(ctx.paths.memoriesDir) ? await loadMemoriesFromDir17(ctx.paths.memoriesDir) : [];
|
|
2992
|
+
const allForBootstrap = existsSync22(ctx.paths.memoriesDir) ? (await loadMemoriesFromDir17(ctx.paths.memoriesDir)).filter(scopeAllowed) : [];
|
|
2677
2993
|
const cmForBootstrap = await loadCodeMap(ctx.paths);
|
|
2678
2994
|
let existingModules = [];
|
|
2679
2995
|
try {
|
|
@@ -2684,7 +3000,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
2684
3000
|
const bootstrap = assessBootstrapState({
|
|
2685
3001
|
projectContextRaw: pcRaw,
|
|
2686
3002
|
memories: allForBootstrap,
|
|
2687
|
-
codeFiles: cmForBootstrap ? Object.keys(cmForBootstrap.files) : [],
|
|
3003
|
+
codeFiles: cmForBootstrap ? Object.keys(cmForBootstrap.files).filter((file) => existsSync22(path13.join(ctx.paths.root, file))) : [],
|
|
2688
3004
|
existingModules
|
|
2689
3005
|
});
|
|
2690
3006
|
if (bootstrap.state !== "ready" && bootstrap.metrics.mainAreas > 0) {
|
|
@@ -2807,7 +3123,7 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
|
|
|
2807
3123
|
};
|
|
2808
3124
|
}
|
|
2809
3125
|
async function detectRunCommands(root) {
|
|
2810
|
-
const pkgPath =
|
|
3126
|
+
const pkgPath = path13.join(root, "package.json");
|
|
2811
3127
|
if (!existsSync22(pkgPath)) return null;
|
|
2812
3128
|
try {
|
|
2813
3129
|
const pkg = JSON.parse(await readFile7(pkgPath, "utf8"));
|
|
@@ -2875,7 +3191,7 @@ function oneLine(value) {
|
|
|
2875
3191
|
return value.replace(/\s+/g, " ").replace(/"/g, '\\"').trim().slice(0, 120);
|
|
2876
3192
|
}
|
|
2877
3193
|
function serverVersion() {
|
|
2878
|
-
return true ? "0.
|
|
3194
|
+
return true ? "0.43.2" : "dev";
|
|
2879
3195
|
}
|
|
2880
3196
|
|
|
2881
3197
|
// src/tools/code-map.ts
|
|
@@ -4048,6 +4364,7 @@ ${template}\`\`\`
|
|
|
4048
4364
|
// src/prompts/bootstrap-repo.ts
|
|
4049
4365
|
import { readFile as readFile8, readdir as readdir5 } from "fs/promises";
|
|
4050
4366
|
import { existsSync as existsSync29 } from "fs";
|
|
4367
|
+
import path14 from "path";
|
|
4051
4368
|
import {
|
|
4052
4369
|
assessBootstrapState as assessBootstrapState2,
|
|
4053
4370
|
loadCodeMap as loadCodeMap4,
|
|
@@ -4075,7 +4392,7 @@ async function currentAssessment(ctx) {
|
|
|
4075
4392
|
return assessBootstrapState2({
|
|
4076
4393
|
projectContextRaw,
|
|
4077
4394
|
memories,
|
|
4078
|
-
codeFiles: codeMap ? Object.keys(codeMap.files) : [],
|
|
4395
|
+
codeFiles: codeMap ? Object.keys(codeMap.files).filter((file) => existsSync29(path14.join(ctx.paths.root, file))) : [],
|
|
4079
4396
|
existingModules
|
|
4080
4397
|
});
|
|
4081
4398
|
}
|
|
@@ -4303,7 +4620,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
4303
4620
|
// src/server.ts
|
|
4304
4621
|
import { hasRecentBriefingMarker, loadConfigSync } from "@hivelore/core";
|
|
4305
4622
|
var SERVER_NAME = "hivelore";
|
|
4306
|
-
var SERVER_VERSION = "0.
|
|
4623
|
+
var SERVER_VERSION = "0.43.2";
|
|
4307
4624
|
function jsonResult(data) {
|
|
4308
4625
|
return {
|
|
4309
4626
|
content: [
|