@hivelore/cli 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.
@@ -139,6 +139,8 @@ import { existsSync as existsSync15 } from "fs";
139
139
  import path5 from "path";
140
140
  import {
141
141
  extractSensorExamples,
142
+ extractTestFilePathsFromCommand,
143
+ hasPendingTestMarker,
142
144
  judgeProposedSensor,
143
145
  loadMemoriesFromDir as loadMemoriesFromDir13,
144
146
  serializeMemory as serializeMemory7
@@ -149,6 +151,7 @@ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile10 } from
149
151
  import path7 from "path";
150
152
  import { z as z17 } from "zod";
151
153
  import {
154
+ buildProposeCommand,
152
155
  loadMemoriesFromDir as loadMemoriesFromDir14,
153
156
  normalizeFramework,
154
157
  parseLessonFields,
@@ -1391,7 +1394,26 @@ async function proposeSensor(input, ctx) {
1391
1394
  if (!found) {
1392
1395
  throw new Error(`No memory found with id ${input.memory_id}`);
1393
1396
  }
1397
+ 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}.` : "";
1394
1398
  if (kind !== "regex") {
1399
+ const referencedTests = extractTestFilePathsFromCommand(input.command.trim()).filter((rel) => existsSync15(path5.resolve(ctx.paths.root, rel)));
1400
+ const pendingTests = [];
1401
+ for (const rel of referencedTests) {
1402
+ try {
1403
+ if (hasPendingTestMarker(await readFile3(path5.resolve(ctx.paths.root, rel), "utf8"))) pendingTests.push(rel);
1404
+ } catch {
1405
+ }
1406
+ }
1407
+ if (pendingTests.length > 0 && input.severity === "block") {
1408
+ return {
1409
+ accepted: false,
1410
+ memory_id: input.memory_id,
1411
+ severity: input.severity,
1412
+ reason: "oracle-pending",
1413
+ 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.`,
1414
+ self_check: { silent_on_current: false, fires_on_bad: null, fired_on: pendingTests }
1415
+ };
1416
+ }
1395
1417
  const verdictCmd = runCommandForValidation(input.command.trim(), ctx.paths.root, input.timeout_ms);
1396
1418
  const anchorPathsCmd = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
1397
1419
  if (verdictCmd.status !== "passed" && input.severity === "block") {
@@ -1425,7 +1447,7 @@ ${verdictCmd.detail}`,
1425
1447
  accepted: true,
1426
1448
  memory_id: input.memory_id,
1427
1449
  severity: input.severity,
1428
- 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}).`,
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,
1429
1451
  self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: null, fired_on: [] }
1430
1452
  };
1431
1453
  }
@@ -1474,6 +1496,7 @@ ${verdictCmd.detail}`,
1474
1496
  accepted: true,
1475
1497
  memory_id: input.memory_id,
1476
1498
  severity: input.severity,
1499
+ ...personalScopeNudge ? { guidance: personalScopeNudge.trim() } : {},
1477
1500
  self_check,
1478
1501
  file_path: found.filePath
1479
1502
  };
@@ -1482,7 +1505,9 @@ var MemTriedInputSchema = {
1482
1505
  what: z16.string().min(1).describe("Brief description of the approach that was tried"),
1483
1506
  why_failed: z16.string().min(1).describe("Why it failed or why it should NOT be used"),
1484
1507
  instead: z16.string().optional().describe("What to use or do instead (recommended alternative)"),
1485
- scope: z16.enum(["personal", "team", "module"]).default("personal").describe("Visibility scope"),
1508
+ scope: z16.enum(["personal", "team", "module"]).optional().describe(
1509
+ "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."
1510
+ ),
1486
1511
  module: z16.string().optional().describe("Module name (required when scope=module)"),
1487
1512
  tags: z16.array(z16.string()).default([]).describe("Tags for filtering"),
1488
1513
  paths: z16.array(z16.string()).default([]).describe("Anchor file paths this applies to"),
@@ -1505,11 +1530,15 @@ async function memTried(input, ctx) {
1505
1530
  if (!existsSync16(ctx.paths.haiveDir)) {
1506
1531
  throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'hivelore init' first.`);
1507
1532
  }
1508
- const slug = input.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 5).join("-");
1533
+ const SLUG_STOPWORDS = /* @__PURE__ */ new Set(["and", "or", "the", "a", "an", "of", "to", "in", "for", "with", "on", "at", "by"]);
1534
+ const words = input.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 5);
1535
+ while (words.length > 2 && SLUG_STOPWORDS.has(words[words.length - 1])) words.pop();
1536
+ const slug = words.join("-");
1537
+ const scope = input.scope ?? (input.sensor ? "team" : "personal");
1509
1538
  const baseFm = buildFrontmatter2({
1510
1539
  type: "attempt",
1511
1540
  slug,
1512
- scope: input.scope,
1541
+ scope,
1513
1542
  module: input.module,
1514
1543
  tags: input.tags,
1515
1544
  paths: input.paths,
@@ -1557,7 +1586,7 @@ async function memTried(input, ctx) {
1557
1586
  ...verdict.reason ? { reason: verdict.reason } : {},
1558
1587
  ...verdict.guidance ? { guidance: verdict.guidance } : {}
1559
1588
  },
1560
- 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.`
1589
+ 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." : "")
1561
1590
  };
1562
1591
  }
1563
1592
  const seed = input.paths.length > 0 ? suggestSensorSeed2(body, input.paths) : null;
@@ -1578,39 +1607,55 @@ async function memTried(input, ctx) {
1578
1607
  };
1579
1608
  }
1580
1609
  var PY_SIGNALS = ["pyproject.toml", "setup.py", "pytest.ini", "requirements.txt", "tox.ini"];
1581
- async function detectTestFrameworkForPaths(root, anchorPaths) {
1582
- const starts = anchorPaths.length > 0 ? anchorPaths : ["."];
1583
- for (const rel of starts) {
1584
- let dir = path7.resolve(root, rel);
1585
- try {
1586
- if (!statSync(dir).isDirectory()) dir = path7.dirname(dir);
1587
- } catch {
1588
- if (path7.extname(dir)) dir = path7.dirname(dir);
1589
- }
1590
- while (dir.startsWith(root)) {
1591
- const pkgJson = path7.join(dir, "package.json");
1592
- const hasPkg = existsSync17(pkgJson);
1593
- const goMod = existsSync17(path7.join(dir, "go.mod"));
1594
- const pySignal = PY_SIGNALS.some((s) => existsSync17(path7.join(dir, s)));
1595
- if (hasPkg || goMod || pySignal) {
1596
- let pkg = null;
1597
- if (hasPkg) {
1598
- try {
1599
- pkg = JSON.parse(await readFile4(pkgJson, "utf8"));
1600
- } catch {
1601
- pkg = null;
1602
- }
1610
+ async function detectForAnchor(root, rel) {
1611
+ let dir = path7.resolve(root, rel);
1612
+ try {
1613
+ if (!statSync(dir).isDirectory()) dir = path7.dirname(dir);
1614
+ } catch {
1615
+ if (path7.extname(dir)) dir = path7.dirname(dir);
1616
+ }
1617
+ while (dir.startsWith(root)) {
1618
+ const pkgJson = path7.join(dir, "package.json");
1619
+ const hasPkg = existsSync17(pkgJson);
1620
+ const goMod = existsSync17(path7.join(dir, "go.mod"));
1621
+ const pySignal = PY_SIGNALS.some((s) => existsSync17(path7.join(dir, s)));
1622
+ if (hasPkg || goMod || pySignal) {
1623
+ let pkg = null;
1624
+ if (hasPkg) {
1625
+ try {
1626
+ pkg = JSON.parse(await readFile4(pkgJson, "utf8"));
1627
+ } catch {
1628
+ pkg = null;
1603
1629
  }
1604
- const baseDir = path7.relative(root, dir).split(path7.sep).join("/");
1605
- return { framework: pickTestFramework(pkg, { goMod, pySignal }), baseDir };
1606
1630
  }
1607
- const parent = path7.dirname(dir);
1608
- if (parent === dir || dir === root) break;
1609
- dir = parent;
1631
+ const baseDir = path7.relative(root, dir).split(path7.sep).join("/");
1632
+ return { framework: pickTestFramework(pkg, { goMod, pySignal }), baseDir };
1610
1633
  }
1634
+ const parent = path7.dirname(dir);
1635
+ if (parent === dir || dir === root) break;
1636
+ dir = parent;
1637
+ }
1638
+ return null;
1639
+ }
1640
+ async function detectTestFrameworkForPaths(root, anchorPaths) {
1641
+ const starts = anchorPaths.length > 0 ? anchorPaths : ["."];
1642
+ for (const rel of starts) {
1643
+ const found = await detectForAnchor(root, rel);
1644
+ if (found) return found;
1611
1645
  }
1612
1646
  return { framework: "vitest", baseDir: "" };
1613
1647
  }
1648
+ async function detectTestFrameworksForAnchors(root, anchorPaths) {
1649
+ const starts = anchorPaths.length > 0 ? anchorPaths : ["."];
1650
+ const groups = /* @__PURE__ */ new Map();
1651
+ for (const rel of starts) {
1652
+ const found = await detectForAnchor(root, rel) ?? { framework: "vitest", baseDir: "" };
1653
+ const existing = groups.get(found.baseDir);
1654
+ if (existing) existing.anchors.push(rel);
1655
+ else groups.set(found.baseDir, { ...found, anchors: [rel] });
1656
+ }
1657
+ return [...groups.values()];
1658
+ }
1614
1659
  var ScaffoldTestInputSchema = {
1615
1660
  memory_id: z17.string().min(1).describe("Id of the attempt/gotcha lesson to scaffold a post-incident test from."),
1616
1661
  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."),
@@ -1624,43 +1669,69 @@ async function scaffoldTest(input, ctx) {
1624
1669
  return { ok: false, error: `No memory found with id ${input.memory_id}`, memory_id: input.memory_id };
1625
1670
  }
1626
1671
  const anchorPaths = found.memory.frontmatter.anchor.paths ?? [];
1627
- const detected = await detectTestFrameworkForPaths(ctx.paths.root, anchorPaths);
1628
- const framework = input.framework ? normalizeFramework(input.framework) ?? detected.framework : detected.framework;
1672
+ const allGroups = await detectTestFrameworksForAnchors(ctx.paths.root, anchorPaths);
1673
+ const groups = input.out_path ? allGroups.slice(0, 1) : allGroups;
1674
+ const frameworkFor = (detected) => input.framework ? normalizeFramework(input.framework) ?? detected : detected;
1629
1675
  const fields = parseLessonFields(found.memory.body);
1630
- const scaffold = scaffoldPostIncidentTest(
1631
- {
1632
- memoryId: input.memory_id,
1633
- title: fields.title || input.memory_id,
1634
- whyFailed: fields.whyFailed,
1635
- instead: fields.instead,
1636
- incident: found.memory.frontmatter.sensor?.incident,
1637
- paths: anchorPaths
1638
- },
1639
- { framework, outPath: input.out_path, baseDir: detected.baseDir }
1676
+ const lesson = {
1677
+ memoryId: input.memory_id,
1678
+ title: fields.title || input.memory_id,
1679
+ whyFailed: fields.whyFailed,
1680
+ instead: fields.instead,
1681
+ incident: found.memory.frontmatter.sensor?.incident,
1682
+ paths: anchorPaths
1683
+ };
1684
+ let scaffolds = groups.map(
1685
+ (g) => scaffoldPostIncidentTest(lesson, { framework: frameworkFor(g.framework), outPath: input.out_path, baseDir: g.baseDir })
1640
1686
  );
1641
- const abs = path7.isAbsolute(scaffold.relPath) ? scaffold.relPath : path7.resolve(ctx.paths.root, scaffold.relPath);
1642
- let written = false;
1643
- let alreadyExists = false;
1644
- if (input.write) {
1645
- if (existsSync17(abs)) {
1646
- alreadyExists = true;
1647
- } else {
1648
- await mkdir4(path7.dirname(abs), { recursive: true });
1649
- await writeFile10(abs, scaffold.content, "utf8");
1650
- written = true;
1687
+ let proposeCommand = scaffolds[0].proposeCommand;
1688
+ if (scaffolds.length > 1) {
1689
+ proposeCommand = buildProposeCommand(lesson, scaffolds.map((s) => s.runCommand).join(" && "));
1690
+ scaffolds = groups.map(
1691
+ (g) => scaffoldPostIncidentTest(lesson, {
1692
+ framework: frameworkFor(g.framework),
1693
+ baseDir: g.baseDir,
1694
+ proposeCommandOverride: proposeCommand
1695
+ })
1696
+ );
1697
+ }
1698
+ const results = [];
1699
+ for (const scaffold of scaffolds) {
1700
+ const abs = path7.isAbsolute(scaffold.relPath) ? scaffold.relPath : path7.resolve(ctx.paths.root, scaffold.relPath);
1701
+ let written = false;
1702
+ let alreadyExists = false;
1703
+ if (input.write) {
1704
+ if (existsSync17(abs)) {
1705
+ alreadyExists = true;
1706
+ } else {
1707
+ await mkdir4(path7.dirname(abs), { recursive: true });
1708
+ await writeFile10(abs, scaffold.content, "utf8");
1709
+ written = true;
1710
+ }
1651
1711
  }
1712
+ results.push({
1713
+ framework: scaffold.framework,
1714
+ path: scaffold.relPath,
1715
+ run_command: scaffold.runCommand,
1716
+ content: scaffold.content,
1717
+ written,
1718
+ already_exists: alreadyExists
1719
+ });
1652
1720
  }
1721
+ const first = results[0];
1722
+ const anyExisting = results.some((r) => r.already_exists);
1653
1723
  return {
1654
1724
  ok: true,
1655
1725
  memory_id: input.memory_id,
1656
- framework,
1657
- path: scaffold.relPath,
1658
- run_command: scaffold.runCommand,
1659
- propose_command: scaffold.proposeCommand,
1660
- content: scaffold.content,
1661
- written,
1662
- already_exists: alreadyExists,
1663
- 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."
1726
+ framework: first.framework,
1727
+ path: first.path,
1728
+ run_command: first.run_command,
1729
+ propose_command: proposeCommand,
1730
+ content: first.content,
1731
+ written: first.written,
1732
+ already_exists: first.already_exists,
1733
+ ...results.length > 1 ? { scaffolds: results } : {},
1734
+ 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.")
1664
1735
  };
1665
1736
  }
1666
1737
  var IngestFindingsInputSchema = {
@@ -2827,7 +2898,7 @@ function oneLine(value) {
2827
2898
  return value.replace(/\s+/g, " ").replace(/"/g, '\\"').trim().slice(0, 120);
2828
2899
  }
2829
2900
  function serverVersion() {
2830
- return true ? "0.38.0" : "dev";
2901
+ return true ? "0.39.1" : "dev";
2831
2902
  }
2832
2903
  var CodeMapInputSchema = {
2833
2904
  file: z21.string().optional().describe("Filter to files whose path contains this substring"),
@@ -4154,7 +4225,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
4154
4225
  };
4155
4226
  }
4156
4227
  var SERVER_NAME = "hivelore";
4157
- var SERVER_VERSION = "0.38.0";
4228
+ var SERVER_VERSION = "0.39.1";
4158
4229
  function jsonResult(data) {
4159
4230
  return {
4160
4231
  content: [
@@ -5104,6 +5175,7 @@ export {
5104
5175
  proposeSensor,
5105
5176
  memTried,
5106
5177
  detectTestFrameworkForPaths,
5178
+ detectTestFrameworksForAnchors,
5107
5179
  scaffoldTest,
5108
5180
  getBriefing,
5109
5181
  codeMapTool,
@@ -5129,4 +5201,4 @@ export {
5129
5201
  printHaiveMcpVersion,
5130
5202
  runHaiveMcpStdio
5131
5203
  };
5132
- //# sourceMappingURL=chunk-UOMGIXZN.js.map
5204
+ //# sourceMappingURL=chunk-YMIQAOFL.js.map