@hivelore/mcp 0.38.0 → 0.39.0

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/server.d.ts CHANGED
@@ -552,7 +552,7 @@ declare const MemTriedInputSchema: {
552
552
  what: z.ZodString;
553
553
  why_failed: z.ZodString;
554
554
  instead: z.ZodOptional<z.ZodString>;
555
- scope: z.ZodDefault<z.ZodEnum<["personal", "team", "module"]>>;
555
+ scope: z.ZodOptional<z.ZodEnum<["personal", "team", "module"]>>;
556
556
  module: z.ZodOptional<z.ZodString>;
557
557
  tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
558
558
  paths: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
@@ -619,16 +619,22 @@ interface MemTriedOutput {
619
619
  }
620
620
  declare function memTried(input: MemTriedInput, ctx: HaiveContext): Promise<MemTriedOutput>;
621
621
 
622
- /**
623
- * Detect the test framework + owning package dir (repo-relative) for an incident's anchor paths.
624
- * Walks up from each anchor path's directory to the repo root and uses the NEAREST enclosing manifest
625
- * (package.json / go.mod / a python signal); falls back to the repo root with a vitest default. This
626
- * is FS I/O, so it lives in the MCP layer (imported by the CLI too) rather than in pure core.
627
- */
628
622
  declare function detectTestFrameworkForPaths(root: string, anchorPaths: string[]): Promise<{
629
623
  framework: TestFramework;
630
624
  baseDir: string;
631
625
  }>;
626
+ interface AnchorFrameworkGroup {
627
+ framework: TestFramework;
628
+ baseDir: string;
629
+ /** The anchor paths owned by this package (subset of the lesson's anchors). */
630
+ anchors: string[];
631
+ }
632
+ /**
633
+ * Group a lesson's anchor paths by OWNING package: one entry per distinct enclosing manifest dir,
634
+ * in first-anchor order. A lesson that spans several packages gets one scaffold per package instead
635
+ * of "first anchor wins". Anchors with no enclosing manifest fall back to the repo root + vitest.
636
+ */
637
+ declare function detectTestFrameworksForAnchors(root: string, anchorPaths: string[]): Promise<AnchorFrameworkGroup[]>;
632
638
  declare const ScaffoldTestInputSchema: {
633
639
  memory_id: z.ZodString;
634
640
  framework: z.ZodOptional<z.ZodEnum<["vitest", "jest", "pytest", "gotest"]>>;
@@ -653,6 +659,19 @@ interface ScaffoldTestOutput {
653
659
  written?: boolean;
654
660
  already_exists?: boolean;
655
661
  notice?: string;
662
+ /**
663
+ * One entry per generated test. A lesson whose anchors span several packages scaffolds one test
664
+ * per OWNING package; they share a single propose_command (a memory carries one sensor) whose
665
+ * command chains every run command.
666
+ */
667
+ scaffolds?: Array<{
668
+ framework: TestFramework;
669
+ path: string;
670
+ run_command: string;
671
+ content: string;
672
+ written: boolean;
673
+ already_exists: boolean;
674
+ }>;
656
675
  }
657
676
  declare function scaffoldTest(input: ScaffoldTestInput, ctx: HaiveContext): Promise<ScaffoldTestOutput>;
658
677
 
@@ -761,4 +780,4 @@ declare function runHaiveMcpStdio(options: {
761
780
  root?: string;
762
781
  }): Promise<void>;
763
782
 
764
- export { type AntiPatternsCheckInput, type AntiPatternsCheckOutput, type BriefingOutput, type CodeMapInput, type CodeMapToolOutput, type CodeSearchInput, type CodeSearchOutput, ENFORCEMENT_PROFILE_TOOLS, EXPERIMENTAL_PROFILE_TOOLS, type GetBriefingInput, type GetRecapInput, type GetRecapOutput, MAINTENANCE_PROFILE_TOOLS, type MemConflictCandidatesInput, type MemDistillInput, type MemDistillOutput, type MemRelevantToInput, type MemRelevantToOutput, type MemResolveProjectInput, type MemSuggestTopicInput, type MemTimelineInput, type MemTriedOutput, type PreCommitCheckInput, type PreCommitCheckOutput, type ProposeSensorOutput, SERVER_NAME, SERVER_VERSION, type ScaffoldTestOutput, TOOL_PROFILES, type ToolProfile, antiPatternsCheck, codeMapTool, codeSearch, createHaiveServer, detectTestFrameworkForPaths, getAllowedToolsForProfile, getBriefing, getRecap, memConflictCandidates, memDistill, memRelevantTo, memResolveProject, memSuggestTopic, memTimeline, memTried, parseMcpCliArgs, preCommitCheck, printHaiveMcpVersion, proposeSensor, readPresumedCorrectTargets, runHaiveMcpStdio, scaffoldTest };
783
+ export { type AnchorFrameworkGroup, type AntiPatternsCheckInput, type AntiPatternsCheckOutput, type BriefingOutput, type CodeMapInput, type CodeMapToolOutput, type CodeSearchInput, type CodeSearchOutput, ENFORCEMENT_PROFILE_TOOLS, EXPERIMENTAL_PROFILE_TOOLS, type GetBriefingInput, type GetRecapInput, type GetRecapOutput, MAINTENANCE_PROFILE_TOOLS, type MemConflictCandidatesInput, type MemDistillInput, type MemDistillOutput, type MemRelevantToInput, type MemRelevantToOutput, type MemResolveProjectInput, type MemSuggestTopicInput, type MemTimelineInput, type MemTriedOutput, type PreCommitCheckInput, type PreCommitCheckOutput, type ProposeSensorOutput, SERVER_NAME, SERVER_VERSION, type ScaffoldTestOutput, TOOL_PROFILES, type ToolProfile, antiPatternsCheck, codeMapTool, codeSearch, createHaiveServer, detectTestFrameworkForPaths, detectTestFrameworksForAnchors, getAllowedToolsForProfile, getBriefing, getRecap, memConflictCandidates, memDistill, memRelevantTo, memResolveProject, memSuggestTopic, memTimeline, memTried, parseMcpCliArgs, preCommitCheck, printHaiveMcpVersion, proposeSensor, readPresumedCorrectTargets, runHaiveMcpStdio, scaffoldTest };
package/dist/server.js CHANGED
@@ -1259,6 +1259,7 @@ async function proposeSensor(input, ctx) {
1259
1259
  if (!found) {
1260
1260
  throw new Error(`No memory found with id ${input.memory_id}`);
1261
1261
  }
1262
+ 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}.` : "";
1262
1263
  if (kind !== "regex") {
1263
1264
  const verdictCmd = runCommandForValidation(input.command.trim(), ctx.paths.root, input.timeout_ms);
1264
1265
  const anchorPathsCmd = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
@@ -1293,7 +1294,7 @@ ${verdictCmd.detail}`,
1293
1294
  accepted: true,
1294
1295
  memory_id: input.memory_id,
1295
1296
  severity: input.severity,
1296
- 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}).`,
1297
+ 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}).`) + personalScopeNudge,
1297
1298
  self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: null, fired_on: [] }
1298
1299
  };
1299
1300
  }
@@ -1342,6 +1343,7 @@ ${verdictCmd.detail}`,
1342
1343
  accepted: true,
1343
1344
  memory_id: input.memory_id,
1344
1345
  severity: input.severity,
1346
+ ...personalScopeNudge ? { guidance: personalScopeNudge.trim() } : {},
1345
1347
  self_check,
1346
1348
  file_path: found.filePath
1347
1349
  };
@@ -1352,7 +1354,9 @@ var MemTriedInputSchema = {
1352
1354
  what: z16.string().min(1).describe("Brief description of the approach that was tried"),
1353
1355
  why_failed: z16.string().min(1).describe("Why it failed or why it should NOT be used"),
1354
1356
  instead: z16.string().optional().describe("What to use or do instead (recommended alternative)"),
1355
- scope: z16.enum(["personal", "team", "module"]).default("personal").describe("Visibility scope"),
1357
+ scope: z16.enum(["personal", "team", "module"]).optional().describe(
1358
+ "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."
1359
+ ),
1356
1360
  module: z16.string().optional().describe("Module name (required when scope=module)"),
1357
1361
  tags: z16.array(z16.string()).default([]).describe("Tags for filtering"),
1358
1362
  paths: z16.array(z16.string()).default([]).describe("Anchor file paths this applies to"),
@@ -1376,10 +1380,11 @@ async function memTried(input, ctx) {
1376
1380
  throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'hivelore init' first.`);
1377
1381
  }
1378
1382
  const slug = input.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 5).join("-");
1383
+ const scope = input.scope ?? (input.sensor ? "team" : "personal");
1379
1384
  const baseFm = buildFrontmatter2({
1380
1385
  type: "attempt",
1381
1386
  slug,
1382
- scope: input.scope,
1387
+ scope,
1383
1388
  module: input.module,
1384
1389
  tags: input.tags,
1385
1390
  paths: input.paths,
@@ -1427,7 +1432,7 @@ async function memTried(input, ctx) {
1427
1432
  ...verdict.reason ? { reason: verdict.reason } : {},
1428
1433
  ...verdict.guidance ? { guidance: verdict.guidance } : {}
1429
1434
  },
1430
- 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.`
1435
+ 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." : "")
1431
1436
  };
1432
1437
  }
1433
1438
  const seed = input.paths.length > 0 ? suggestSensorSeed2(body, input.paths) : null;
@@ -1454,6 +1459,7 @@ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile10 } from
1454
1459
  import path7 from "path";
1455
1460
  import { z as z17 } from "zod";
1456
1461
  import {
1462
+ buildProposeCommand,
1457
1463
  loadMemoriesFromDir as loadMemoriesFromDir14,
1458
1464
  normalizeFramework,
1459
1465
  parseLessonFields,
@@ -1461,39 +1467,55 @@ import {
1461
1467
  scaffoldPostIncidentTest
1462
1468
  } from "@hivelore/core";
1463
1469
  var PY_SIGNALS = ["pyproject.toml", "setup.py", "pytest.ini", "requirements.txt", "tox.ini"];
1464
- async function detectTestFrameworkForPaths(root, anchorPaths) {
1465
- const starts = anchorPaths.length > 0 ? anchorPaths : ["."];
1466
- for (const rel of starts) {
1467
- let dir = path7.resolve(root, rel);
1468
- try {
1469
- if (!statSync(dir).isDirectory()) dir = path7.dirname(dir);
1470
- } catch {
1471
- if (path7.extname(dir)) dir = path7.dirname(dir);
1472
- }
1473
- while (dir.startsWith(root)) {
1474
- const pkgJson = path7.join(dir, "package.json");
1475
- const hasPkg = existsSync17(pkgJson);
1476
- const goMod = existsSync17(path7.join(dir, "go.mod"));
1477
- const pySignal = PY_SIGNALS.some((s) => existsSync17(path7.join(dir, s)));
1478
- if (hasPkg || goMod || pySignal) {
1479
- let pkg = null;
1480
- if (hasPkg) {
1481
- try {
1482
- pkg = JSON.parse(await readFile4(pkgJson, "utf8"));
1483
- } catch {
1484
- pkg = null;
1485
- }
1470
+ async function detectForAnchor(root, rel) {
1471
+ let dir = path7.resolve(root, rel);
1472
+ try {
1473
+ if (!statSync(dir).isDirectory()) dir = path7.dirname(dir);
1474
+ } catch {
1475
+ if (path7.extname(dir)) dir = path7.dirname(dir);
1476
+ }
1477
+ while (dir.startsWith(root)) {
1478
+ const pkgJson = path7.join(dir, "package.json");
1479
+ const hasPkg = existsSync17(pkgJson);
1480
+ const goMod = existsSync17(path7.join(dir, "go.mod"));
1481
+ const pySignal = PY_SIGNALS.some((s) => existsSync17(path7.join(dir, s)));
1482
+ if (hasPkg || goMod || pySignal) {
1483
+ let pkg = null;
1484
+ if (hasPkg) {
1485
+ try {
1486
+ pkg = JSON.parse(await readFile4(pkgJson, "utf8"));
1487
+ } catch {
1488
+ pkg = null;
1486
1489
  }
1487
- const baseDir = path7.relative(root, dir).split(path7.sep).join("/");
1488
- return { framework: pickTestFramework(pkg, { goMod, pySignal }), baseDir };
1489
1490
  }
1490
- const parent = path7.dirname(dir);
1491
- if (parent === dir || dir === root) break;
1492
- dir = parent;
1491
+ const baseDir = path7.relative(root, dir).split(path7.sep).join("/");
1492
+ return { framework: pickTestFramework(pkg, { goMod, pySignal }), baseDir };
1493
1493
  }
1494
+ const parent = path7.dirname(dir);
1495
+ if (parent === dir || dir === root) break;
1496
+ dir = parent;
1497
+ }
1498
+ return null;
1499
+ }
1500
+ async function detectTestFrameworkForPaths(root, anchorPaths) {
1501
+ const starts = anchorPaths.length > 0 ? anchorPaths : ["."];
1502
+ for (const rel of starts) {
1503
+ const found = await detectForAnchor(root, rel);
1504
+ if (found) return found;
1494
1505
  }
1495
1506
  return { framework: "vitest", baseDir: "" };
1496
1507
  }
1508
+ async function detectTestFrameworksForAnchors(root, anchorPaths) {
1509
+ const starts = anchorPaths.length > 0 ? anchorPaths : ["."];
1510
+ const groups = /* @__PURE__ */ new Map();
1511
+ for (const rel of starts) {
1512
+ const found = await detectForAnchor(root, rel) ?? { framework: "vitest", baseDir: "" };
1513
+ const existing = groups.get(found.baseDir);
1514
+ if (existing) existing.anchors.push(rel);
1515
+ else groups.set(found.baseDir, { ...found, anchors: [rel] });
1516
+ }
1517
+ return [...groups.values()];
1518
+ }
1497
1519
  var ScaffoldTestInputSchema = {
1498
1520
  memory_id: z17.string().min(1).describe("Id of the attempt/gotcha lesson to scaffold a post-incident test from."),
1499
1521
  framework: z17.enum(["vitest", "jest", "pytest", "gotest"]).optional().describe("Test framework. Auto-detected from the package that owns the lesson's anchor paths when omitted."),
@@ -1507,43 +1529,69 @@ async function scaffoldTest(input, ctx) {
1507
1529
  return { ok: false, error: `No memory found with id ${input.memory_id}`, memory_id: input.memory_id };
1508
1530
  }
1509
1531
  const anchorPaths = found.memory.frontmatter.anchor.paths ?? [];
1510
- const detected = await detectTestFrameworkForPaths(ctx.paths.root, anchorPaths);
1511
- const framework = input.framework ? normalizeFramework(input.framework) ?? detected.framework : detected.framework;
1532
+ const allGroups = await detectTestFrameworksForAnchors(ctx.paths.root, anchorPaths);
1533
+ const groups = input.out_path ? allGroups.slice(0, 1) : allGroups;
1534
+ const frameworkFor = (detected) => input.framework ? normalizeFramework(input.framework) ?? detected : detected;
1512
1535
  const fields = parseLessonFields(found.memory.body);
1513
- const scaffold = scaffoldPostIncidentTest(
1514
- {
1515
- memoryId: input.memory_id,
1516
- title: fields.title || input.memory_id,
1517
- whyFailed: fields.whyFailed,
1518
- instead: fields.instead,
1519
- incident: found.memory.frontmatter.sensor?.incident,
1520
- paths: anchorPaths
1521
- },
1522
- { framework, outPath: input.out_path, baseDir: detected.baseDir }
1536
+ const lesson = {
1537
+ memoryId: input.memory_id,
1538
+ title: fields.title || input.memory_id,
1539
+ whyFailed: fields.whyFailed,
1540
+ instead: fields.instead,
1541
+ incident: found.memory.frontmatter.sensor?.incident,
1542
+ paths: anchorPaths
1543
+ };
1544
+ let scaffolds = groups.map(
1545
+ (g) => scaffoldPostIncidentTest(lesson, { framework: frameworkFor(g.framework), outPath: input.out_path, baseDir: g.baseDir })
1523
1546
  );
1524
- const abs = path7.isAbsolute(scaffold.relPath) ? scaffold.relPath : path7.resolve(ctx.paths.root, scaffold.relPath);
1525
- let written = false;
1526
- let alreadyExists = false;
1527
- if (input.write) {
1528
- if (existsSync17(abs)) {
1529
- alreadyExists = true;
1530
- } else {
1531
- await mkdir4(path7.dirname(abs), { recursive: true });
1532
- await writeFile10(abs, scaffold.content, "utf8");
1533
- written = true;
1547
+ let proposeCommand = scaffolds[0].proposeCommand;
1548
+ if (scaffolds.length > 1) {
1549
+ proposeCommand = buildProposeCommand(lesson, scaffolds.map((s) => s.runCommand).join(" && "));
1550
+ scaffolds = groups.map(
1551
+ (g) => scaffoldPostIncidentTest(lesson, {
1552
+ framework: frameworkFor(g.framework),
1553
+ baseDir: g.baseDir,
1554
+ proposeCommandOverride: proposeCommand
1555
+ })
1556
+ );
1557
+ }
1558
+ const results = [];
1559
+ for (const scaffold of scaffolds) {
1560
+ const abs = path7.isAbsolute(scaffold.relPath) ? scaffold.relPath : path7.resolve(ctx.paths.root, scaffold.relPath);
1561
+ let written = false;
1562
+ let alreadyExists = false;
1563
+ if (input.write) {
1564
+ if (existsSync17(abs)) {
1565
+ alreadyExists = true;
1566
+ } else {
1567
+ await mkdir4(path7.dirname(abs), { recursive: true });
1568
+ await writeFile10(abs, scaffold.content, "utf8");
1569
+ written = true;
1570
+ }
1534
1571
  }
1572
+ results.push({
1573
+ framework: scaffold.framework,
1574
+ path: scaffold.relPath,
1575
+ run_command: scaffold.runCommand,
1576
+ content: scaffold.content,
1577
+ written,
1578
+ already_exists: alreadyExists
1579
+ });
1535
1580
  }
1581
+ const first = results[0];
1582
+ const anyExisting = results.some((r) => r.already_exists);
1536
1583
  return {
1537
1584
  ok: true,
1538
1585
  memory_id: input.memory_id,
1539
- framework,
1540
- path: scaffold.relPath,
1541
- run_command: scaffold.runCommand,
1542
- propose_command: scaffold.proposeCommand,
1543
- content: scaffold.content,
1544
- written,
1545
- already_exists: alreadyExists,
1546
- 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."
1586
+ framework: first.framework,
1587
+ path: first.path,
1588
+ run_command: first.run_command,
1589
+ propose_command: proposeCommand,
1590
+ content: first.content,
1591
+ written: first.written,
1592
+ already_exists: first.already_exists,
1593
+ ...results.length > 1 ? { scaffolds: results } : {},
1594
+ 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.")
1547
1595
  };
1548
1596
  }
1549
1597
 
@@ -2810,7 +2858,7 @@ function oneLine(value) {
2810
2858
  return value.replace(/\s+/g, " ").replace(/"/g, '\\"').trim().slice(0, 120);
2811
2859
  }
2812
2860
  function serverVersion() {
2813
- return true ? "0.38.0" : "dev";
2861
+ return true ? "0.39.0" : "dev";
2814
2862
  }
2815
2863
 
2816
2864
  // src/tools/code-map.ts
@@ -4238,7 +4286,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
4238
4286
  // src/server.ts
4239
4287
  import { hasRecentBriefingMarker, loadConfigSync } from "@hivelore/core";
4240
4288
  var SERVER_NAME = "hivelore";
4241
- var SERVER_VERSION = "0.38.0";
4289
+ var SERVER_VERSION = "0.39.0";
4242
4290
  function jsonResult(data) {
4243
4291
  return {
4244
4292
  content: [
@@ -5194,6 +5242,7 @@ export {
5194
5242
  codeSearch,
5195
5243
  createHaiveServer,
5196
5244
  detectTestFrameworkForPaths,
5245
+ detectTestFrameworksForAnchors,
5197
5246
  getAllowedToolsForProfile,
5198
5247
  getBriefing,
5199
5248
  getRecap,