@hivelore/mcp 0.38.0 → 0.39.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 +128 -65
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +27 -8
- package/dist/server.js +136 -64
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1140,6 +1140,8 @@ import { existsSync as existsSync15 } from "fs";
|
|
|
1140
1140
|
import path5 from "path";
|
|
1141
1141
|
import {
|
|
1142
1142
|
extractSensorExamples,
|
|
1143
|
+
extractTestFilePathsFromCommand,
|
|
1144
|
+
hasPendingTestMarker,
|
|
1143
1145
|
judgeProposedSensor,
|
|
1144
1146
|
loadMemoriesFromDir as loadMemoriesFromDir13,
|
|
1145
1147
|
serializeMemory as serializeMemory7
|
|
@@ -1261,7 +1263,26 @@ async function proposeSensor(input, ctx) {
|
|
|
1261
1263
|
if (!found) {
|
|
1262
1264
|
throw new Error(`No memory found with id ${input.memory_id}`);
|
|
1263
1265
|
}
|
|
1266
|
+
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}.` : "";
|
|
1264
1267
|
if (kind !== "regex") {
|
|
1268
|
+
const referencedTests = extractTestFilePathsFromCommand(input.command.trim()).filter((rel) => existsSync15(path5.resolve(ctx.paths.root, rel)));
|
|
1269
|
+
const pendingTests = [];
|
|
1270
|
+
for (const rel of referencedTests) {
|
|
1271
|
+
try {
|
|
1272
|
+
if (hasPendingTestMarker(await readFile3(path5.resolve(ctx.paths.root, rel), "utf8"))) pendingTests.push(rel);
|
|
1273
|
+
} catch {
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (pendingTests.length > 0 && input.severity === "block") {
|
|
1277
|
+
return {
|
|
1278
|
+
accepted: false,
|
|
1279
|
+
memory_id: input.memory_id,
|
|
1280
|
+
severity: input.severity,
|
|
1281
|
+
reason: "oracle-pending",
|
|
1282
|
+
guidance: `The routed test is still a PENDING stub (${pendingTests.join(", ")}) \u2014 it passes on anything, so the sensor would enforce nothing while reporting protection. Write the assertion (RED on the incident, GREEN once fixed), run it, then re-propose.`,
|
|
1283
|
+
self_check: { silent_on_current: false, fires_on_bad: null, fired_on: pendingTests }
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1265
1286
|
const verdictCmd = runCommandForValidation(input.command.trim(), ctx.paths.root, input.timeout_ms);
|
|
1266
1287
|
const anchorPathsCmd = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
|
|
1267
1288
|
if (verdictCmd.status !== "passed" && input.severity === "block") {
|
|
@@ -1295,7 +1316,7 @@ ${verdictCmd.detail}`,
|
|
|
1295
1316
|
accepted: true,
|
|
1296
1317
|
memory_id: input.memory_id,
|
|
1297
1318
|
severity: input.severity,
|
|
1298
|
-
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})
|
|
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,
|
|
1299
1320
|
self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: null, fired_on: [] }
|
|
1300
1321
|
};
|
|
1301
1322
|
}
|
|
@@ -1344,6 +1365,7 @@ ${verdictCmd.detail}`,
|
|
|
1344
1365
|
accepted: true,
|
|
1345
1366
|
memory_id: input.memory_id,
|
|
1346
1367
|
severity: input.severity,
|
|
1368
|
+
...personalScopeNudge ? { guidance: personalScopeNudge.trim() } : {},
|
|
1347
1369
|
self_check,
|
|
1348
1370
|
file_path: found.filePath
|
|
1349
1371
|
};
|
|
@@ -1354,7 +1376,9 @@ var MemTriedInputSchema = {
|
|
|
1354
1376
|
what: z16.string().min(1).describe("Brief description of the approach that was tried"),
|
|
1355
1377
|
why_failed: z16.string().min(1).describe("Why it failed or why it should NOT be used"),
|
|
1356
1378
|
instead: z16.string().optional().describe("What to use or do instead (recommended alternative)"),
|
|
1357
|
-
scope: z16.enum(["personal", "team", "module"]).
|
|
1379
|
+
scope: z16.enum(["personal", "team", "module"]).optional().describe(
|
|
1380
|
+
"Visibility scope. Defaults to personal \u2014 EXCEPT when a one-shot `sensor` is attached: an enforced lesson is team truth (the sensor must travel to every machine and CI), so it defaults to team. Pass scope explicitly to override."
|
|
1381
|
+
),
|
|
1358
1382
|
module: z16.string().optional().describe("Module name (required when scope=module)"),
|
|
1359
1383
|
tags: z16.array(z16.string()).default([]).describe("Tags for filtering"),
|
|
1360
1384
|
paths: z16.array(z16.string()).default([]).describe("Anchor file paths this applies to"),
|
|
@@ -1377,11 +1401,15 @@ async function memTried(input, ctx) {
|
|
|
1377
1401
|
if (!existsSync16(ctx.paths.haiveDir)) {
|
|
1378
1402
|
throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'hivelore init' first.`);
|
|
1379
1403
|
}
|
|
1380
|
-
const
|
|
1404
|
+
const SLUG_STOPWORDS = /* @__PURE__ */ new Set(["and", "or", "the", "a", "an", "of", "to", "in", "for", "with", "on", "at", "by"]);
|
|
1405
|
+
const words = input.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 5);
|
|
1406
|
+
while (words.length > 2 && SLUG_STOPWORDS.has(words[words.length - 1])) words.pop();
|
|
1407
|
+
const slug = words.join("-");
|
|
1408
|
+
const scope = input.scope ?? (input.sensor ? "team" : "personal");
|
|
1381
1409
|
const baseFm = buildFrontmatter2({
|
|
1382
1410
|
type: "attempt",
|
|
1383
1411
|
slug,
|
|
1384
|
-
scope
|
|
1412
|
+
scope,
|
|
1385
1413
|
module: input.module,
|
|
1386
1414
|
tags: input.tags,
|
|
1387
1415
|
paths: input.paths,
|
|
@@ -1429,7 +1457,7 @@ async function memTried(input, ctx) {
|
|
|
1429
1457
|
...verdict.reason ? { reason: verdict.reason } : {},
|
|
1430
1458
|
...verdict.guidance ? { guidance: verdict.guidance } : {}
|
|
1431
1459
|
},
|
|
1432
|
-
hint: verdict.accepted ? "Loop closed: the attempt is saved AND enforced \u2014 the gate now refuses a repeat deterministically." : `Attempt saved, but the sensor was rejected (${verdict.reason}). Revise per the guidance and re-propose with propose_sensor.`
|
|
1460
|
+
hint: (verdict.accepted ? "Loop closed: the attempt is saved AND enforced \u2014 the gate now refuses a repeat deterministically." : `Attempt saved, but the sensor was rejected (${verdict.reason}). Revise per the guidance and re-propose with propose_sensor.`) + (input.scope === void 0 ? " Saved team-scoped (an enforced lesson must travel with the repo) \u2014 pass scope:'personal' to keep it private." : "")
|
|
1433
1461
|
};
|
|
1434
1462
|
}
|
|
1435
1463
|
const seed = input.paths.length > 0 ? suggestSensorSeed2(body, input.paths) : null;
|
|
@@ -1456,6 +1484,7 @@ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile10 } from
|
|
|
1456
1484
|
import path7 from "path";
|
|
1457
1485
|
import { z as z17 } from "zod";
|
|
1458
1486
|
import {
|
|
1487
|
+
buildProposeCommand,
|
|
1459
1488
|
loadMemoriesFromDir as loadMemoriesFromDir14,
|
|
1460
1489
|
normalizeFramework,
|
|
1461
1490
|
parseLessonFields,
|
|
@@ -1463,38 +1492,46 @@ import {
|
|
|
1463
1492
|
scaffoldPostIncidentTest
|
|
1464
1493
|
} from "@hivelore/core";
|
|
1465
1494
|
var PY_SIGNALS = ["pyproject.toml", "setup.py", "pytest.ini", "requirements.txt", "tox.ini"];
|
|
1466
|
-
async function
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
if (hasPkg
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
} catch {
|
|
1486
|
-
pkg = null;
|
|
1487
|
-
}
|
|
1495
|
+
async function detectForAnchor(root, rel) {
|
|
1496
|
+
let dir = path7.resolve(root, rel);
|
|
1497
|
+
try {
|
|
1498
|
+
if (!statSync(dir).isDirectory()) dir = path7.dirname(dir);
|
|
1499
|
+
} catch {
|
|
1500
|
+
if (path7.extname(dir)) dir = path7.dirname(dir);
|
|
1501
|
+
}
|
|
1502
|
+
while (dir.startsWith(root)) {
|
|
1503
|
+
const pkgJson = path7.join(dir, "package.json");
|
|
1504
|
+
const hasPkg = existsSync17(pkgJson);
|
|
1505
|
+
const goMod = existsSync17(path7.join(dir, "go.mod"));
|
|
1506
|
+
const pySignal = PY_SIGNALS.some((s) => existsSync17(path7.join(dir, s)));
|
|
1507
|
+
if (hasPkg || goMod || pySignal) {
|
|
1508
|
+
let pkg = null;
|
|
1509
|
+
if (hasPkg) {
|
|
1510
|
+
try {
|
|
1511
|
+
pkg = JSON.parse(await readFile4(pkgJson, "utf8"));
|
|
1512
|
+
} catch {
|
|
1513
|
+
pkg = null;
|
|
1488
1514
|
}
|
|
1489
|
-
const baseDir = path7.relative(root, dir).split(path7.sep).join("/");
|
|
1490
|
-
return { framework: pickTestFramework(pkg, { goMod, pySignal }), baseDir };
|
|
1491
1515
|
}
|
|
1492
|
-
const
|
|
1493
|
-
|
|
1494
|
-
dir = parent;
|
|
1516
|
+
const baseDir = path7.relative(root, dir).split(path7.sep).join("/");
|
|
1517
|
+
return { framework: pickTestFramework(pkg, { goMod, pySignal }), baseDir };
|
|
1495
1518
|
}
|
|
1519
|
+
const parent = path7.dirname(dir);
|
|
1520
|
+
if (parent === dir || dir === root) break;
|
|
1521
|
+
dir = parent;
|
|
1522
|
+
}
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
async function detectTestFrameworksForAnchors(root, anchorPaths) {
|
|
1526
|
+
const starts = anchorPaths.length > 0 ? anchorPaths : ["."];
|
|
1527
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1528
|
+
for (const rel of starts) {
|
|
1529
|
+
const found = await detectForAnchor(root, rel) ?? { framework: "vitest", baseDir: "" };
|
|
1530
|
+
const existing = groups.get(found.baseDir);
|
|
1531
|
+
if (existing) existing.anchors.push(rel);
|
|
1532
|
+
else groups.set(found.baseDir, { ...found, anchors: [rel] });
|
|
1496
1533
|
}
|
|
1497
|
-
return
|
|
1534
|
+
return [...groups.values()];
|
|
1498
1535
|
}
|
|
1499
1536
|
var ScaffoldTestInputSchema = {
|
|
1500
1537
|
memory_id: z17.string().min(1).describe("Id of the attempt/gotcha lesson to scaffold a post-incident test from."),
|
|
@@ -1509,43 +1546,69 @@ async function scaffoldTest(input, ctx) {
|
|
|
1509
1546
|
return { ok: false, error: `No memory found with id ${input.memory_id}`, memory_id: input.memory_id };
|
|
1510
1547
|
}
|
|
1511
1548
|
const anchorPaths = found.memory.frontmatter.anchor.paths ?? [];
|
|
1512
|
-
const
|
|
1513
|
-
const
|
|
1549
|
+
const allGroups = await detectTestFrameworksForAnchors(ctx.paths.root, anchorPaths);
|
|
1550
|
+
const groups = input.out_path ? allGroups.slice(0, 1) : allGroups;
|
|
1551
|
+
const frameworkFor = (detected) => input.framework ? normalizeFramework(input.framework) ?? detected : detected;
|
|
1514
1552
|
const fields = parseLessonFields(found.memory.body);
|
|
1515
|
-
const
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
{ framework, outPath: input.out_path, baseDir:
|
|
1553
|
+
const lesson = {
|
|
1554
|
+
memoryId: input.memory_id,
|
|
1555
|
+
title: fields.title || input.memory_id,
|
|
1556
|
+
whyFailed: fields.whyFailed,
|
|
1557
|
+
instead: fields.instead,
|
|
1558
|
+
incident: found.memory.frontmatter.sensor?.incident,
|
|
1559
|
+
paths: anchorPaths
|
|
1560
|
+
};
|
|
1561
|
+
let scaffolds = groups.map(
|
|
1562
|
+
(g) => scaffoldPostIncidentTest(lesson, { framework: frameworkFor(g.framework), outPath: input.out_path, baseDir: g.baseDir })
|
|
1525
1563
|
);
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1564
|
+
let proposeCommand = scaffolds[0].proposeCommand;
|
|
1565
|
+
if (scaffolds.length > 1) {
|
|
1566
|
+
proposeCommand = buildProposeCommand(lesson, scaffolds.map((s) => s.runCommand).join(" && "));
|
|
1567
|
+
scaffolds = groups.map(
|
|
1568
|
+
(g) => scaffoldPostIncidentTest(lesson, {
|
|
1569
|
+
framework: frameworkFor(g.framework),
|
|
1570
|
+
baseDir: g.baseDir,
|
|
1571
|
+
proposeCommandOverride: proposeCommand
|
|
1572
|
+
})
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
const results = [];
|
|
1576
|
+
for (const scaffold of scaffolds) {
|
|
1577
|
+
const abs = path7.isAbsolute(scaffold.relPath) ? scaffold.relPath : path7.resolve(ctx.paths.root, scaffold.relPath);
|
|
1578
|
+
let written = false;
|
|
1579
|
+
let alreadyExists = false;
|
|
1580
|
+
if (input.write) {
|
|
1581
|
+
if (existsSync17(abs)) {
|
|
1582
|
+
alreadyExists = true;
|
|
1583
|
+
} else {
|
|
1584
|
+
await mkdir4(path7.dirname(abs), { recursive: true });
|
|
1585
|
+
await writeFile10(abs, scaffold.content, "utf8");
|
|
1586
|
+
written = true;
|
|
1587
|
+
}
|
|
1536
1588
|
}
|
|
1589
|
+
results.push({
|
|
1590
|
+
framework: scaffold.framework,
|
|
1591
|
+
path: scaffold.relPath,
|
|
1592
|
+
run_command: scaffold.runCommand,
|
|
1593
|
+
content: scaffold.content,
|
|
1594
|
+
written,
|
|
1595
|
+
already_exists: alreadyExists
|
|
1596
|
+
});
|
|
1537
1597
|
}
|
|
1598
|
+
const first = results[0];
|
|
1599
|
+
const anyExisting = results.some((r) => r.already_exists);
|
|
1538
1600
|
return {
|
|
1539
1601
|
ok: true,
|
|
1540
1602
|
memory_id: input.memory_id,
|
|
1541
|
-
framework,
|
|
1542
|
-
path:
|
|
1543
|
-
run_command:
|
|
1544
|
-
propose_command:
|
|
1545
|
-
content:
|
|
1546
|
-
written,
|
|
1547
|
-
already_exists:
|
|
1548
|
-
|
|
1603
|
+
framework: first.framework,
|
|
1604
|
+
path: first.path,
|
|
1605
|
+
run_command: first.run_command,
|
|
1606
|
+
propose_command: proposeCommand,
|
|
1607
|
+
content: first.content,
|
|
1608
|
+
written: first.written,
|
|
1609
|
+
already_exists: first.already_exists,
|
|
1610
|
+
...results.length > 1 ? { scaffolds: results } : {},
|
|
1611
|
+
notice: (results.length > 1 ? `Lesson spans ${results.length} packages \u2014 one pending test per owning package; ONE propose_command arms them all (chained oracle). ` : "") + (anyExisting ? "Some file(s) already exist \u2014 not overwritten. Delete them or pass out_path to write elsewhere." : "PENDING test scaffolded. Fill in the assertion (RED on the incident, GREEN once fixed), run it, then arm it with propose_command \u2014 propose_sensor stays the sole validated writer.")
|
|
1549
1612
|
};
|
|
1550
1613
|
}
|
|
1551
1614
|
|
|
@@ -2812,7 +2875,7 @@ function oneLine(value) {
|
|
|
2812
2875
|
return value.replace(/\s+/g, " ").replace(/"/g, '\\"').trim().slice(0, 120);
|
|
2813
2876
|
}
|
|
2814
2877
|
function serverVersion() {
|
|
2815
|
-
return true ? "0.
|
|
2878
|
+
return true ? "0.39.1" : "dev";
|
|
2816
2879
|
}
|
|
2817
2880
|
|
|
2818
2881
|
// src/tools/code-map.ts
|
|
@@ -4240,7 +4303,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
4240
4303
|
// src/server.ts
|
|
4241
4304
|
import { hasRecentBriefingMarker, loadConfigSync } from "@hivelore/core";
|
|
4242
4305
|
var SERVER_NAME = "hivelore";
|
|
4243
|
-
var SERVER_VERSION = "0.
|
|
4306
|
+
var SERVER_VERSION = "0.39.1";
|
|
4244
4307
|
function jsonResult(data) {
|
|
4245
4308
|
return {
|
|
4246
4309
|
content: [
|