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