@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 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"]).default("personal").describe("Visibility scope"),
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 slug = input.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 5).join("-");
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: input.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 detectTestFrameworkForPaths(root, anchorPaths) {
1467
- const starts = anchorPaths.length > 0 ? anchorPaths : ["."];
1468
- for (const rel of starts) {
1469
- let dir = path7.resolve(root, rel);
1470
- try {
1471
- if (!statSync(dir).isDirectory()) dir = path7.dirname(dir);
1472
- } catch {
1473
- if (path7.extname(dir)) dir = path7.dirname(dir);
1474
- }
1475
- while (dir.startsWith(root)) {
1476
- const pkgJson = path7.join(dir, "package.json");
1477
- const hasPkg = existsSync17(pkgJson);
1478
- const goMod = existsSync17(path7.join(dir, "go.mod"));
1479
- const pySignal = PY_SIGNALS.some((s) => existsSync17(path7.join(dir, s)));
1480
- if (hasPkg || goMod || pySignal) {
1481
- let pkg = null;
1482
- if (hasPkg) {
1483
- try {
1484
- pkg = JSON.parse(await readFile4(pkgJson, "utf8"));
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 parent = path7.dirname(dir);
1493
- if (parent === dir || dir === root) break;
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 { framework: "vitest", baseDir: "" };
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 detected = await detectTestFrameworkForPaths(ctx.paths.root, anchorPaths);
1513
- const framework = input.framework ? normalizeFramework(input.framework) ?? detected.framework : detected.framework;
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 scaffold = scaffoldPostIncidentTest(
1516
- {
1517
- memoryId: input.memory_id,
1518
- title: fields.title || input.memory_id,
1519
- whyFailed: fields.whyFailed,
1520
- instead: fields.instead,
1521
- incident: found.memory.frontmatter.sensor?.incident,
1522
- paths: anchorPaths
1523
- },
1524
- { framework, outPath: input.out_path, baseDir: detected.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
- const abs = path7.isAbsolute(scaffold.relPath) ? scaffold.relPath : path7.resolve(ctx.paths.root, scaffold.relPath);
1527
- let written = false;
1528
- let alreadyExists = false;
1529
- if (input.write) {
1530
- if (existsSync17(abs)) {
1531
- alreadyExists = true;
1532
- } else {
1533
- await mkdir4(path7.dirname(abs), { recursive: true });
1534
- await writeFile10(abs, scaffold.content, "utf8");
1535
- written = true;
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: scaffold.relPath,
1543
- run_command: scaffold.runCommand,
1544
- propose_command: scaffold.proposeCommand,
1545
- content: scaffold.content,
1546
- written,
1547
- already_exists: alreadyExists,
1548
- notice: alreadyExists ? "File already exists \u2014 not overwritten. Delete it 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."
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.38.0" : "dev";
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.38.0";
4306
+ var SERVER_VERSION = "0.39.1";
4244
4307
  function jsonResult(data) {
4245
4308
  return {
4246
4309
  content: [