@hivelore/cli 0.34.1 → 0.35.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/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  preCommitCheck,
11
11
  readPresumedCorrectTargets,
12
12
  runHaiveMcpStdio
13
- } from "./chunk-X6UHROFL.js";
13
+ } from "./chunk-ZR7UPTRR.js";
14
14
  import {
15
15
  registerMemoryPending
16
16
  } from "./chunk-OYJKHD22.js";
@@ -275,7 +275,7 @@ async function lintMemoriesAsync(root, options = {}) {
275
275
  const loaded = await loadMemoriesFromDir2(paths.memoriesDir);
276
276
  const usage = await loadUsageIndex(paths);
277
277
  const codeMap = await loadCodeMap(paths);
278
- const trackedFiles = gitTrackedFiles(root);
278
+ const trackedFiles2 = gitTrackedFiles(root);
279
279
  const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
280
280
  const actionableWords = /\b(always|never|prefer|use|run|avoid|because|instead|why|rationale|do not|must|should|require|required|requires|fix|fail|failed|fails|prevent|prevents|allow|allows|lets|ensure|ensures|catch|catches)\b/i;
281
281
  for (const { filePath, memory: memory2 } of loaded) {
@@ -323,7 +323,7 @@ async function lintMemoriesAsync(root, options = {}) {
323
323
  });
324
324
  }
325
325
  const isStackSeed = fm.tags.includes("stack-pack");
326
- const suggestedAnchors = isStackSeed ? { paths: [], symbols: [] } : suggestAnchors(root, { filePath, memory: memory2 }, codeMap, trackedFiles);
326
+ const suggestedAnchors = isStackSeed ? { paths: [], symbols: [] } : suggestAnchors(root, { filePath, memory: memory2 }, codeMap, trackedFiles2);
327
327
  if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated" && !isStackSeed) {
328
328
  out.push({
329
329
  file: filePath,
@@ -435,14 +435,14 @@ function titleFromId(id) {
435
435
  const withoutDate = id.replace(/^\d{4}-\d{2}-\d{2}-/, "");
436
436
  return withoutDate.split("-").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
437
437
  }
438
- function suggestAnchors(root, loaded, codeMap, trackedFiles) {
438
+ function suggestAnchors(root, loaded, codeMap, trackedFiles2) {
439
439
  const body = loaded.memory.body;
440
440
  const paths = /* @__PURE__ */ new Set();
441
441
  const symbols = /* @__PURE__ */ new Set();
442
442
  for (const match of body.matchAll(/`([^`\n]+\.[A-Za-z0-9]+)`|(?:^|\s)([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)/gm)) {
443
443
  const candidate = (match[1] ?? match[2] ?? "").replace(/^\.?\//, "");
444
444
  if (!candidate || candidate.startsWith("http")) continue;
445
- if (existsSync(path2.join(root, candidate)) && isSafeAnchorPath(candidate, trackedFiles)) {
445
+ if (existsSync(path2.join(root, candidate)) && isSafeAnchorPath(candidate, trackedFiles2)) {
446
446
  paths.add(candidate);
447
447
  }
448
448
  }
@@ -452,7 +452,7 @@ function suggestAnchors(root, loaded, codeMap, trackedFiles) {
452
452
  for (const exp of entry.exports) {
453
453
  if (!exp.name || exp.name.length < 4) continue;
454
454
  if (lowered.includes(exp.name.toLowerCase())) {
455
- if (isSafeAnchorPath(file, trackedFiles)) {
455
+ if (isSafeAnchorPath(file, trackedFiles2)) {
456
456
  paths.add(file);
457
457
  symbols.add(exp.name);
458
458
  }
@@ -477,12 +477,12 @@ function gitTrackedFiles(root) {
477
477
  const files = result.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
478
478
  return new Set(files);
479
479
  }
480
- function isSafeAnchorPath(file, trackedFiles) {
480
+ function isSafeAnchorPath(file, trackedFiles2) {
481
481
  const normalized = file.replace(/\\/g, "/").replace(/^\.?\//, "");
482
482
  if (normalized.startsWith(".ai/.cache/") || normalized.startsWith(".ai/.runtime/")) return false;
483
483
  if (normalized.includes("/node_modules/") || normalized.startsWith("node_modules/")) return false;
484
484
  if (normalized.includes("/dist/") || normalized.startsWith("dist/")) return false;
485
- if (trackedFiles && !trackedFiles.has(normalized)) return false;
485
+ if (trackedFiles2 && !trackedFiles2.has(normalized)) return false;
486
486
  return true;
487
487
  }
488
488
  function nearDuplicatePairs(loaded) {
@@ -3755,7 +3755,7 @@ ${SEED_FOOTER(stack)}` });
3755
3755
 
3756
3756
  // src/commands/init.ts
3757
3757
  var execFileAsync = promisify2(execFile2);
3758
- var HAIVE_GITHUB_ACTION_REF = `v${"0.34.1"}`;
3758
+ var HAIVE_GITHUB_ACTION_REF = `v${"0.35.0"}`;
3759
3759
  var PROJECT_CONTEXT_TEMPLATE = `# Project context
3760
3760
 
3761
3761
  > Generated by \`hivelore init\`. Run \`hivelore init --bootstrap\` to auto-fill from your codebase,
@@ -4714,20 +4714,27 @@ import path15 from "path";
4714
4714
  import "commander";
4715
4715
  import {
4716
4716
  DEFAULT_AUTO_PROMOTE_RULE,
4717
+ assessSensorHealth,
4717
4718
  buildFrontmatter as buildFrontmatter3,
4719
+ existingGateMissShas,
4718
4720
  findProjectRoot as findProjectRoot9,
4719
4721
  getUsage as getUsage2,
4722
+ gatePassedShas,
4720
4723
  isAutoPromoteEligible,
4721
4724
  isDecaying,
4722
4725
  isStackPackSeed,
4723
4726
  loadCodeMap as loadCodeMap5,
4724
4727
  loadConfig as loadConfig3,
4725
4728
  loadMemoriesFromDir as loadMemoriesFromDir7,
4729
+ loadSensorLedger,
4726
4730
  loadUsageIndex as loadUsageIndex3,
4727
4731
  pullCrossRepoSources,
4732
+ planGitWatch,
4733
+ proposeGateMissDrafts,
4728
4734
  resolveHaivePaths as resolveHaivePaths8,
4729
4735
  resolveManifestFiles,
4730
4736
  serializeMemory as serializeMemory4,
4737
+ withQuarantineNote,
4731
4738
  trackDependencies,
4732
4739
  verifyAnchor,
4733
4740
  watchContracts
@@ -4765,6 +4772,24 @@ function registerSync(program2) {
4765
4772
  let revalidated = 0;
4766
4773
  let promoted = 0;
4767
4774
  let autoApproved = 0;
4775
+ let quarantined = 0;
4776
+ const sensorHealth = new Map(
4777
+ assessSensorHealth(await loadSensorLedger(paths)).map((health) => [health.memory_id, health])
4778
+ );
4779
+ const quarantineCandidates = await loadMemoriesFromDir7(paths.memoriesDir);
4780
+ for (const { memory: memory2, filePath } of quarantineCandidates) {
4781
+ const sensor = memory2.frontmatter.sensor;
4782
+ const health = sensorHealth.get(memory2.frontmatter.id);
4783
+ if (!sensor || sensor.kind !== "shell" && sensor.kind !== "test" || sensor.severity !== "block" || !health?.quarantine_pending) continue;
4784
+ if (!dryRun) {
4785
+ const at = (/* @__PURE__ */ new Date()).toISOString();
4786
+ await writeFile8(filePath, serializeMemory4({
4787
+ frontmatter: { ...memory2.frontmatter, sensor: { ...sensor, severity: "warn" } },
4788
+ body: withQuarantineNote(memory2.body, at, health.flap_count)
4789
+ }), "utf8");
4790
+ }
4791
+ quarantined++;
4792
+ }
4768
4793
  if (opts.verify !== false) {
4769
4794
  const memories = await loadMemoriesFromDir7(paths.memoriesDir);
4770
4795
  for (const { memory: memory2, filePath } of memories) {
@@ -4885,13 +4910,17 @@ function registerSync(program2) {
4885
4910
  for (const repair of repairs) log(ui.dim(`autopilot: ${repair.message}`));
4886
4911
  }
4887
4912
  const sinceReport = opts.since ? collectSinceChanges(root, opts.since) : null;
4913
+ const gateMissIds = await processGateMissWatch(root, paths, dryRun);
4914
+ if (gateMissIds.length > 0) {
4915
+ log(ui.yellow(`gate-miss: proposed ${gateMissIds.length} lesson(s): ${gateMissIds.join(", ")}`));
4916
+ }
4888
4917
  const draftMemories = (await loadMemoriesFromDir7(paths.memoriesDir)).filter(
4889
4918
  (m) => m.memory.frontmatter.status === "draft"
4890
4919
  );
4891
4920
  const draftCount = draftMemories.length;
4892
4921
  const autoApprovedNote = autoApproved > 0 ? ` \xB7 ${autoApproved} auto-approved` : "";
4893
4922
  log(
4894
- `${ui.dim("sync:")} ${staleMarked} stale \xB7 ${revalidated} revalidated \xB7 ${promoted} promoted${autoApprovedNote}${sinceReport ? ` \xB7 ${sinceReport.added.length}+/${sinceReport.modified.length}~/${sinceReport.removed.length}- since ${opts.since}` : ""}`
4923
+ `${ui.dim("sync:")} ${staleMarked} stale \xB7 ${revalidated} revalidated \xB7 ${promoted} promoted \xB7 ${quarantined} quarantined${autoApprovedNote}${sinceReport ? ` \xB7 ${sinceReport.added.length}+/${sinceReport.modified.length}~/${sinceReport.removed.length}- since ${opts.since}` : ""}`
4895
4924
  );
4896
4925
  if (!opts.quiet && draftCount > 0) {
4897
4926
  log(
@@ -5209,6 +5238,83 @@ Wait for **explicit confirmation** before acting.
5209
5238
  }
5210
5239
  });
5211
5240
  }
5241
+ async function processGateMissWatch(root, paths, dryRun) {
5242
+ const headResult = spawnSync4("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" });
5243
+ if (headResult.status !== 0) return [];
5244
+ const head = headResult.stdout.trim();
5245
+ if (!head) return [];
5246
+ const stateFile = path15.join(paths.runtimeDir, "enforcement", "git-watch.json");
5247
+ let state = null;
5248
+ try {
5249
+ state = JSON.parse(await readFile7(stateFile, "utf8"));
5250
+ } catch {
5251
+ }
5252
+ const plan = planGitWatch(state, head);
5253
+ if (plan.action === "initialize") {
5254
+ if (!dryRun) {
5255
+ await mkdir8(path15.dirname(stateFile), { recursive: true });
5256
+ await writeFile8(stateFile, JSON.stringify(plan.next, null, 2) + "\n", "utf8");
5257
+ }
5258
+ return [];
5259
+ }
5260
+ if (plan.action === "idle") return [];
5261
+ const proposals = proposeGateMissDrafts(
5262
+ readGitCommitsRange(root, plan.range),
5263
+ existingGateMissShas(await loadMemoriesFromDir7(paths.memoriesDir)),
5264
+ gatePassedShas(await loadSensorLedger(paths))
5265
+ );
5266
+ const ids = [];
5267
+ for (const proposal of proposals) {
5268
+ const fm = {
5269
+ ...buildFrontmatter3({
5270
+ type: "attempt",
5271
+ slug: proposal.slug,
5272
+ scope: "team",
5273
+ status: "proposed",
5274
+ tags: ["gate-miss", proposal.kind],
5275
+ paths: proposal.paths,
5276
+ topic: `gate-miss-${proposal.reverted_sha}`
5277
+ }),
5278
+ status: "proposed"
5279
+ };
5280
+ ids.push(fm.id);
5281
+ if (!dryRun) {
5282
+ await mkdir8(paths.teamDir, { recursive: true });
5283
+ await writeFile8(
5284
+ path15.join(paths.teamDir, `${fm.id}.md`),
5285
+ serializeMemory4({ frontmatter: fm, body: proposal.body }),
5286
+ "utf8"
5287
+ );
5288
+ }
5289
+ }
5290
+ if (!dryRun) {
5291
+ await mkdir8(path15.dirname(stateFile), { recursive: true });
5292
+ await writeFile8(stateFile, JSON.stringify(plan.next, null, 2) + "\n", "utf8");
5293
+ }
5294
+ return ids;
5295
+ }
5296
+ function readGitCommitsRange(root, range) {
5297
+ const result = spawnSync4(
5298
+ "git",
5299
+ ["log", "--reverse", "--format=%x1e%H%x1f%s%x1f%b%x1f", "--name-only", range],
5300
+ { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }
5301
+ );
5302
+ if (result.status !== 0) return [];
5303
+ const commits = [];
5304
+ for (const block of result.stdout.split("").filter((part) => part.trim())) {
5305
+ const [shaRaw, subjectRaw, bodyRaw, filesRaw = ""] = block.split("");
5306
+ const sha = shaRaw?.trim();
5307
+ const subject = subjectRaw?.trim();
5308
+ if (!sha || !subject) continue;
5309
+ commits.push({
5310
+ sha,
5311
+ subject,
5312
+ body: bodyRaw?.trim() ?? "",
5313
+ files: filesRaw.split("\n").map((file) => file.trim()).filter(Boolean)
5314
+ });
5315
+ }
5316
+ return commits;
5317
+ }
5212
5318
  function bridgeSummaryLine(body) {
5213
5319
  const firstLine = body.split("\n").map((l) => l.replace(/^#+\s*/, "").trim()).find((l) => l.length > 0) ?? "";
5214
5320
  const oneLine = firstLine.replace(/\s+/g, " ");
@@ -7207,16 +7313,34 @@ import { mkdir as mkdir12, writeFile as writeFile18 } from "fs/promises";
7207
7313
  import path30 from "path";
7208
7314
  import {
7209
7315
  aggregateUsage,
7316
+ buildPreventionReceipt,
7317
+ renderPreventionReceipt,
7210
7318
  findProjectRoot as findProjectRoot28,
7211
7319
  loadMemoriesFromDir as loadMemoriesFromDir11,
7212
7320
  loadUsageIndex as loadUsageIndex12,
7321
+ loadPreventionEvents as loadPreventionEvents2,
7213
7322
  parseSince,
7214
7323
  readUsageEvents,
7215
7324
  resolveHaivePaths as resolveHaivePaths27,
7216
7325
  usageLogSize
7217
7326
  } from "@hivelore/core";
7218
7327
  function registerStats(program2) {
7219
- program2.command("stats").description("Show MCP tool-usage stats over a window (e.g. --since 7d).").option("--since <window>", "ISO date or relative (e.g. '7d', '24h', '30m')", "30d").option("--json", "emit JSON instead of human-readable output", false).option("--memory-hits", "show top-read memories (which mems are actually being used)", false).option(
7328
+ const stats = program2.command("stats").description("Show MCP tool-usage stats and prevention receipts.");
7329
+ stats.command("receipt").description("Show documented mistakes refused by the gate over a time window").addHelpText("after", "\nParent options also apply: --since <window> (default 7d here), --json, --dir <dir>.").action(async () => {
7330
+ const opts = stats.opts();
7331
+ const root = findProjectRoot28(opts.dir);
7332
+ const paths = resolveHaivePaths27(root);
7333
+ const sinceRaw = stats.getOptionValueSource("since") === "default" ? "7d" : opts.since ?? "7d";
7334
+ const since = parseSince(sinceRaw) ?? new Date(Date.now() - 7 * 864e5);
7335
+ const [events, usage, memories] = await Promise.all([
7336
+ loadPreventionEvents2(paths),
7337
+ loadUsageIndex12(paths),
7338
+ existsSync33(paths.memoriesDir) ? loadMemoriesFromDir11(paths.memoriesDir) : Promise.resolve([])
7339
+ ]);
7340
+ const receipt = buildPreventionReceipt(events, memories, usage, { since });
7341
+ console.log(opts.json ? JSON.stringify(receipt, null, 2) : renderPreventionReceipt(receipt));
7342
+ });
7343
+ stats.option("--since <window>", "ISO date or relative (e.g. '7d', '24h', '30m')", "30d").option("--json", "emit JSON instead of human-readable output", false).option("--memory-hits", "show top-read memories (which mems are actually being used)", false).option(
7220
7344
  "--export-report <path>",
7221
7345
  "write a JSON rollup (tools + briefing counts + heuristic ROI hints). Parent dirs are created if needed.",
7222
7346
  void 0
@@ -7662,7 +7786,7 @@ import {
7662
7786
  findProjectRoot as findProjectRoot31,
7663
7787
  loadConfig as loadConfig7,
7664
7788
  loadEvalHistory,
7665
- loadPreventionEvents as loadPreventionEvents2,
7789
+ loadPreventionEvents as loadPreventionEvents3,
7666
7790
  loadUsageIndex as loadUsageIndex13,
7667
7791
  overallScore,
7668
7792
  resolveHaivePaths as resolveHaivePaths29,
@@ -7729,7 +7853,7 @@ function registerEval(program2) {
7729
7853
  const authoredScore = resolvedSpec.authored > 0 && resolvedSpec.synthesized > 0 && (authoredRetrievalAgg || sensorAgg) ? overallScore(authoredRetrievalAgg, sensorAgg) : null;
7730
7854
  const [usage, preventionEvents, config] = await Promise.all([
7731
7855
  loadUsageIndex13(paths),
7732
- loadPreventionEvents2(paths),
7856
+ loadPreventionEvents3(paths),
7733
7857
  loadConfig7(paths)
7734
7858
  ]);
7735
7859
  const gatePrecision = computeGatePrecision(
@@ -8449,6 +8573,7 @@ import { execFileSync, execSync } from "child_process";
8449
8573
  import "commander";
8450
8574
  import {
8451
8575
  codeMapPath as codeMapPath2,
8576
+ assessSensorHealth as assessSensorHealth2,
8452
8577
  countSourceFilesOnDisk,
8453
8578
  extractReferencedPaths,
8454
8579
  sensorPatternBrittleness,
@@ -8459,6 +8584,7 @@ import {
8459
8584
  loadCodeMap as loadCodeMap6,
8460
8585
  loadConfig as loadConfig10,
8461
8586
  loadMemoriesFromDirDetailed,
8587
+ loadSensorLedger as loadSensorLedger2,
8462
8588
  loadUsageIndex as loadUsageIndex15,
8463
8589
  readUsageEvents as readUsageEvents3,
8464
8590
  resolveHaivePaths as resolveHaivePaths33
@@ -8654,6 +8780,18 @@ function registerDoctor(program2) {
8654
8780
  });
8655
8781
  }
8656
8782
  const sensorMemories = memories.filter((m) => m.memory.frontmatter.sensor);
8783
+ const gateMissDrafts = memories.filter(
8784
+ (m) => m.memory.frontmatter.status === "proposed" && m.memory.frontmatter.tags.includes("gate-miss")
8785
+ );
8786
+ if (gateMissDrafts.length > 0) {
8787
+ findings.push({
8788
+ severity: "info",
8789
+ code: "gate-miss-drafts",
8790
+ section: "Corpus health",
8791
+ message: `${gateMissDrafts.length} proposed gate-miss lesson(s) await review: ${gateMissDrafts.slice(0, 5).map((m) => m.memory.frontmatter.id).join(", ")}.`,
8792
+ fix: "Review with `hivelore memory list --status proposed`."
8793
+ });
8794
+ }
8657
8795
  const blockSensors = sensorMemories.filter(
8658
8796
  (m) => m.memory.frontmatter.sensor?.severity === "block"
8659
8797
  ).length;
@@ -8694,6 +8832,26 @@ function registerDoctor(program2) {
8694
8832
  fix: "Make the pattern discriminating (add an 'absent' companion), or demote: `hivelore sensors promote <id> --severity warn`"
8695
8833
  });
8696
8834
  }
8835
+ const sensorHealth = assessSensorHealth2(await loadSensorLedger2(paths));
8836
+ for (const health of sensorHealth.filter((h) => h.quarantine_pending)) {
8837
+ const last = health.flaps.at(-1);
8838
+ findings.push({
8839
+ severity: "warn",
8840
+ code: "sensor-flaky",
8841
+ section: "Protection",
8842
+ message: `${health.memory_id} flapped ${health.flap_count}\xD7 on identical inputs. Last contradiction: ${last.previous.at} ${last.previous.outcome} \u2192 ${last.current.at} ${last.current.outcome}.`,
8843
+ fix: "Run `hivelore sync` to demote it, fix the oracle, then `hivelore sensors promote <id> --yes`."
8844
+ });
8845
+ }
8846
+ for (const health of sensorHealth.filter((h) => h.never_fired)) {
8847
+ findings.push({
8848
+ severity: "info",
8849
+ code: "sensor-never-fired",
8850
+ section: "Protection",
8851
+ message: `${health.memory_id} was evaluated ${health.evaluation_count} times across 30+ days and never fired \u2014 retirement candidate.`,
8852
+ fix: "Review the sensor manually; no automatic delete/retire command is provided."
8853
+ });
8854
+ }
8697
8855
  const codeMap = await loadCodeMap6(paths);
8698
8856
  if (!codeMap) {
8699
8857
  findings.push({
@@ -8791,7 +8949,7 @@ function registerDoctor(program2) {
8791
8949
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `hivelore init` without --manual)."
8792
8950
  });
8793
8951
  }
8794
- findings.push(...await collectInstallFindings(root, "0.34.1"));
8952
+ findings.push(...await collectInstallFindings(root, "0.35.0"));
8795
8953
  findings.push(...await collectToolchainFindings(root));
8796
8954
  try {
8797
8955
  const legacyRaw = execSync("haive-mcp --version", {
@@ -8799,7 +8957,7 @@ function registerDoctor(program2) {
8799
8957
  timeout: 3e3,
8800
8958
  stdio: ["ignore", "pipe", "ignore"]
8801
8959
  }).trim();
8802
- const cliVersion = "0.34.1";
8960
+ const cliVersion = "0.35.0";
8803
8961
  if (legacyRaw && legacyRaw !== cliVersion) {
8804
8962
  findings.push({
8805
8963
  severity: "warn",
@@ -9529,14 +9687,16 @@ function registerResolveProject(program2) {
9529
9687
  }
9530
9688
 
9531
9689
  // src/commands/enforce.ts
9532
- import { execFile as execFile4, execFileSync as execFileSync2, spawn as spawn4 } from "child_process";
9690
+ import { execFile as execFile5, execFileSync as execFileSync2, spawn as spawn4 } from "child_process";
9533
9691
  import { existsSync as existsSync41, statSync as statSync3 } from "fs";
9534
9692
  import { chmod, mkdir as mkdir16, readFile as readFile18, readdir as readdir4, rm as rm2, writeFile as writeFile25 } from "fs/promises";
9535
9693
  import path39 from "path";
9536
- import { promisify as promisify4 } from "util";
9694
+ import { promisify as promisify5 } from "util";
9537
9695
  import "commander";
9538
9696
  import {
9539
9697
  antiPatternGateParams as antiPatternGateParams2,
9698
+ appendSensorEvaluations,
9699
+ assessSensorHealth as assessSensorHealth3,
9540
9700
  assessBootstrapState,
9541
9701
  isSensorScannablePath,
9542
9702
  findProjectRoot as findProjectRoot37,
@@ -9550,6 +9710,7 @@ import {
9550
9710
  loadConfig as loadConfig12,
9551
9711
  detectAgentContext,
9552
9712
  loadMemoriesFromDir as loadMemoriesFromDir14,
9713
+ loadSensorLedger as loadSensorLedger3,
9553
9714
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
9554
9715
  readRecentBriefingMarker,
9555
9716
  recordPreventionHits,
@@ -9559,6 +9720,7 @@ import {
9559
9720
  saveConfig as saveConfig3,
9560
9721
  selectCommandSensors,
9561
9722
  sensorTargetsFromDiff,
9723
+ sensorAppliesToPath as sensorAppliesToPath2,
9562
9724
  SESSION_RECAP_TTL_MS,
9563
9725
  verifyAnchor as verifyAnchor3,
9564
9726
  writeBriefingMarker as writeBriefingMarker2
@@ -9761,8 +9923,52 @@ async function executeCommandSensors(specs, root) {
9761
9923
  return runs;
9762
9924
  }
9763
9925
 
9926
+ // src/utils/sensor-evaluations.ts
9927
+ import { execFile as execFile4 } from "child_process";
9928
+ import { promisify as promisify4 } from "util";
9929
+ import {
9930
+ computeScopeHash,
9931
+ sensorAppliesToPath
9932
+ } from "@hivelore/core";
9933
+ var exec3 = promisify4(execFile4);
9934
+ async function gitHeadSha(root) {
9935
+ try {
9936
+ const { stdout } = await exec3("git", ["rev-parse", "HEAD"], { cwd: root });
9937
+ return stdout.trim();
9938
+ } catch {
9939
+ return "";
9940
+ }
9941
+ }
9942
+ async function trackedFiles(root) {
9943
+ try {
9944
+ const { stdout } = await exec3("git", ["ls-files", "-co", "--exclude-standard"], { cwd: root });
9945
+ return stdout.split("\n").map((f) => f.trim()).filter(Boolean);
9946
+ } catch {
9947
+ return [];
9948
+ }
9949
+ }
9950
+ async function commandScopeHash(root, spec) {
9951
+ if (spec.paths.length === 0) return "";
9952
+ const sensor = { kind: spec.kind, paths: spec.paths };
9953
+ const files = (await trackedFiles(root)).filter((file) => sensorAppliesToPath(sensor, [], file));
9954
+ return computeScopeHash(root, files);
9955
+ }
9956
+ function evaluation(base, command) {
9957
+ return {
9958
+ at: base.at ?? (/* @__PURE__ */ new Date()).toISOString(),
9959
+ memory_id: base.memory_id,
9960
+ kind: base.kind,
9961
+ stage: base.stage,
9962
+ head_sha: base.head_sha,
9963
+ scope_hash: base.scope_hash,
9964
+ outcome: base.outcome,
9965
+ ...command?.exit_code !== null ? { exit_code: command?.exit_code } : {},
9966
+ ...command ? { duration_ms: command.duration_ms } : {}
9967
+ };
9968
+ }
9969
+
9764
9970
  // src/commands/enforce.ts
9765
- var execFileAsync2 = promisify4(execFile4);
9971
+ var execFileAsync2 = promisify5(execFile5);
9766
9972
  var MAX_STDIN_BYTES2 = 256 * 1024;
9767
9973
  var ENFORCE_HOOK_MARKER = "# Hivelore enforcement hook";
9768
9974
  function registerEnforce(program2) {
@@ -10451,7 +10657,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
10451
10657
  findings: [{ severity: "info", code: "enforcement-off", message: "Hivelore enforcement is disabled." }]
10452
10658
  });
10453
10659
  }
10454
- findings.push(...await inspectIntegrationVersions(root, "0.34.1"));
10660
+ findings.push(...await inspectIntegrationVersions(root, "0.35.0"));
10455
10661
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
10456
10662
  const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
10457
10663
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent Hivelore briefing marker exists." } : {
@@ -10531,7 +10737,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
10531
10737
  }];
10532
10738
  }
10533
10739
  const hasErrors = effectiveFindings.some((f) => f.severity === "error");
10534
- return withCategories({
10740
+ const report = withCategories({
10535
10741
  root,
10536
10742
  initialized,
10537
10743
  mode,
@@ -10540,6 +10746,18 @@ async function buildEnforcementReport(dir, stage, sessionId) {
10540
10746
  should_block: mode === "strict" && hasErrors,
10541
10747
  findings: effectiveFindings
10542
10748
  });
10749
+ if (!report.should_block && (stage === "pre-commit" || stage === "ci")) {
10750
+ const headSha = await gitHeadSha(root);
10751
+ await appendSensorEvaluations(paths, [evaluation({
10752
+ memory_id: "__gate__",
10753
+ kind: "shell",
10754
+ stage,
10755
+ head_sha: headSha,
10756
+ scope_hash: "",
10757
+ outcome: "silent"
10758
+ })]);
10759
+ }
10760
+ return report;
10543
10761
  }
10544
10762
  function withCategories(report) {
10545
10763
  return {
@@ -10695,7 +10913,7 @@ async function runPrecommitPolicy(paths, gate, stage) {
10695
10913
  anchored_blocks,
10696
10914
  semantic: true
10697
10915
  }, { paths });
10698
- const sensorFindings = await runSensorGate(paths, snapshot.diff);
10916
+ const sensorFindings = await runSensorGate(paths, snapshot.diff, stage);
10699
10917
  const reviewWarnings = result.warnings.filter(
10700
10918
  (w) => w.level === "review" && !w.reasons.includes("sensor")
10701
10919
  );
@@ -10758,7 +10976,7 @@ async function runPrecommitPolicy(paths, gate, stage) {
10758
10976
  ...sensorFindings
10759
10977
  ];
10760
10978
  }
10761
- async function runSensorGate(paths, diff) {
10979
+ async function runSensorGate(paths, diff, stage) {
10762
10980
  if (!diff || !existsSync41(paths.memoriesDir)) return [];
10763
10981
  try {
10764
10982
  const loaded = await loadMemoriesFromDir14(paths.memoriesDir);
@@ -10769,8 +10987,22 @@ async function runSensorGate(paths, diff) {
10769
10987
  const findings = [];
10770
10988
  const seen = /* @__PURE__ */ new Set();
10771
10989
  const firedIds = /* @__PURE__ */ new Set();
10990
+ const ledgerRows = [];
10991
+ const headSha = await gitHeadSha(paths.root);
10772
10992
  const regexSensorMemories = scannable.filter((m) => m.frontmatter.sensor.kind === "regex");
10773
10993
  const hits = regexSensorMemories.length > 0 ? runSensors(regexSensorMemories, targets) : [];
10994
+ for (const memory2 of regexSensorMemories) {
10995
+ const sensor = memory2.frontmatter.sensor;
10996
+ if (!targets.some((target) => sensorAppliesToPath2(sensor, memory2.frontmatter.anchor.paths, target.path))) continue;
10997
+ ledgerRows.push(evaluation({
10998
+ memory_id: memory2.frontmatter.id,
10999
+ kind: "regex",
11000
+ stage,
11001
+ head_sha: headSha,
11002
+ scope_hash: "",
11003
+ outcome: hits.some((hit) => hit.memory_id === memory2.frontmatter.id) ? "fired" : "silent"
11004
+ }));
11005
+ }
10774
11006
  for (const hit of hits) {
10775
11007
  if (seen.has(hit.memory_id)) continue;
10776
11008
  seen.add(hit.memory_id);
@@ -10782,7 +11014,8 @@ async function runSensorGate(paths, diff) {
10782
11014
  code: "sensor-block",
10783
11015
  message: `Block sensor fired \u2014 ${hit.memory_id}: ${hit.message}${where}`,
10784
11016
  fix: "Remove the flagged pattern, or run `hivelore sensors check` to inspect the match.",
10785
- impact: 45
11017
+ impact: 45,
11018
+ memory_ids: [hit.memory_id]
10786
11019
  });
10787
11020
  } else {
10788
11021
  findings.push({
@@ -10790,7 +11023,8 @@ async function runSensorGate(paths, diff) {
10790
11023
  code: "sensor-warn",
10791
11024
  message: `Sensor flagged ${hit.memory_id}: ${hit.message}${where}`,
10792
11025
  fix: "Review the flagged line; `hivelore sensors check` shows the matched code.",
10793
- impact: 5
11026
+ impact: 5,
11027
+ memory_ids: [hit.memory_id]
10794
11028
  });
10795
11029
  }
10796
11030
  }
@@ -10798,7 +11032,34 @@ async function runSensorGate(paths, diff) {
10798
11032
  if (config?.enforcement?.runCommandSensors === true) {
10799
11033
  const changedPaths = targets.map((t) => t.path).filter(Boolean);
10800
11034
  const specs = selectCommandSensors(scannable, changedPaths).filter((sp) => !seen.has(sp.memory_id));
10801
- for (const run of await executeCommandSensors(specs, paths.root)) {
11035
+ const runs = await executeCommandSensors(specs, paths.root);
11036
+ for (const run of runs) {
11037
+ const spec = specs.find((candidate) => candidate.memory_id === run.memory_id);
11038
+ ledgerRows.push(evaluation({
11039
+ memory_id: run.memory_id,
11040
+ kind: run.kind,
11041
+ stage,
11042
+ head_sha: headSha,
11043
+ scope_hash: await commandScopeHash(paths.root, spec),
11044
+ outcome: run.status === "failed" ? "fired" : run.status === "passed" ? "silent" : "unrunnable"
11045
+ }, { exit_code: run.exit_code, duration_ms: run.duration_ms }));
11046
+ }
11047
+ const prior = await loadSensorLedger3(paths);
11048
+ const health = new Map(assessSensorHealth3([...prior, ...ledgerRows]).map((h) => [h.memory_id, h]));
11049
+ for (const run of runs) {
11050
+ const sensorHealth = health.get(run.memory_id);
11051
+ const quarantined = sensorHealth?.quarantine_pending === true;
11052
+ if (quarantined && run.severity === "block") {
11053
+ const last = sensorHealth.flaps.at(-1);
11054
+ findings.push({
11055
+ severity: "warn",
11056
+ code: "sensor-flaky",
11057
+ message: `Command sensor ${run.memory_id} flapped ${sensorHealth.flap_count}\xD7 on identical inputs; treated as warn pending sync quarantine. Last contradiction: ${last.previous.at} ${last.previous.outcome} \u2192 ${last.current.at} ${last.current.outcome}.`,
11058
+ fix: "Run `hivelore sync`, fix the flaky oracle, then re-promote with `hivelore sensors promote <id> --yes`.",
11059
+ impact: 5,
11060
+ memory_ids: [run.memory_id]
11061
+ });
11062
+ }
10802
11063
  if (run.status === "passed") continue;
10803
11064
  seen.add(run.memory_id);
10804
11065
  if (run.status === "unrunnable") {
@@ -10815,14 +11076,15 @@ ${run.output_tail}` : ""),
10815
11076
  firedIds.add(run.memory_id);
10816
11077
  const outputBlock = run.output_tail ? `
10817
11078
  ${run.output_tail}` : "";
10818
- if (run.severity === "block") {
11079
+ if (run.severity === "block" && !quarantined) {
10819
11080
  findings.push({
10820
11081
  severity: "error",
10821
11082
  code: "sensor-block",
10822
11083
  message: `Block ${run.kind} sensor fired \u2014 ${run.memory_id}: ${run.message}
10823
11084
  command: ${run.command} (exit ${run.exit_code}, ${run.duration_ms}ms)${outputBlock}`,
10824
11085
  fix: "Fix the behaviour the command checks, or run `hivelore sensors check --commands` to inspect it.",
10825
- impact: 45
11086
+ impact: 45,
11087
+ memory_ids: [run.memory_id]
10826
11088
  });
10827
11089
  } else {
10828
11090
  findings.push({
@@ -10830,13 +11092,19 @@ command: ${run.command} (exit ${run.exit_code}, ${run.duration_ms}ms)${outputBlo
10830
11092
  code: "sensor-warn",
10831
11093
  message: `${run.kind} sensor flagged ${run.memory_id}: ${run.message} (exit ${run.exit_code})${outputBlock}`,
10832
11094
  fix: "Review the failing command; `hivelore sensors check --commands` re-runs it.",
10833
- impact: 5
11095
+ impact: 5,
11096
+ memory_ids: [run.memory_id]
10834
11097
  });
10835
11098
  }
10836
11099
  }
10837
11100
  }
11101
+ await appendSensorEvaluations(paths, ledgerRows);
10838
11102
  if (firedIds.size > 0) {
10839
- await recordPreventionHits(paths, [...firedIds], "sensor").catch(() => {
11103
+ const details = Object.fromEntries([...firedIds].map((id) => {
11104
+ const row = ledgerRows.find((entry) => entry.memory_id === id && entry.outcome === "fired");
11105
+ return [id, { kind: row?.kind ?? "regex", stage, ...row?.exit_code !== void 0 ? { exit_code: row.exit_code } : {} }];
11106
+ }));
11107
+ await recordPreventionHits(paths, [...firedIds], "sensor", /* @__PURE__ */ new Date(), details).catch(() => {
10840
11108
  });
10841
11109
  }
10842
11110
  return findings;
@@ -11442,11 +11710,27 @@ ${hook.body}`, "utf8");
11442
11710
  async function installCiEnforcement(root) {
11443
11711
  const workflowPath = path39.join(root, ".github", "workflows", "haive-enforcement.yml");
11444
11712
  await mkdir16(path39.dirname(workflowPath), { recursive: true });
11713
+ const workflow = renderCiEnforcementWorkflow();
11445
11714
  if (existsSync41(workflowPath)) {
11446
- ui.info("GitHub Actions enforcement workflow already exists \u2014 skipped");
11715
+ const existing = await readFile18(workflowPath, "utf8");
11716
+ const start = "# haive:enforcement-workflow:start";
11717
+ const end = "# haive:enforcement-workflow:end";
11718
+ const startAt = existing.indexOf(start);
11719
+ const endAt = existing.indexOf(end);
11720
+ if (startAt >= 0 && endAt > startAt) {
11721
+ await writeFile25(workflowPath, existing.slice(0, startAt) + workflow + existing.slice(endAt + end.length), "utf8");
11722
+ ui.success(`Updated ${path39.relative(root, workflowPath)} managed block`);
11723
+ } else {
11724
+ ui.info("GitHub Actions enforcement workflow already exists without Hivelore markers \u2014 preserved");
11725
+ }
11447
11726
  return;
11448
11727
  }
11449
- await writeFile25(workflowPath, `name: haive-enforcement
11728
+ await writeFile25(workflowPath, workflow, "utf8");
11729
+ ui.success(`Created ${path39.relative(root, workflowPath)}`);
11730
+ }
11731
+ function renderCiEnforcementWorkflow() {
11732
+ return `# haive:enforcement-workflow:start
11733
+ name: haive-enforcement
11450
11734
 
11451
11735
  on:
11452
11736
  pull_request:
@@ -11458,6 +11742,7 @@ jobs:
11458
11742
  runs-on: ubuntu-latest
11459
11743
  permissions:
11460
11744
  contents: read
11745
+ pull-requests: write
11461
11746
  steps:
11462
11747
  - uses: actions/checkout@v4
11463
11748
  with:
@@ -11468,12 +11753,51 @@ jobs:
11468
11753
  - name: Install Hivelore
11469
11754
  run: npm install -g @hivelore/cli
11470
11755
  - name: Enforce Hivelore policy
11756
+ id: gate
11471
11757
  env:
11472
11758
  HAIVE_BASE_SHA: \${{ github.event.pull_request.base.sha || github.event.before }}
11473
11759
  HAIVE_HEAD_SHA: \${{ github.event.pull_request.head.sha || github.sha }}
11474
- run: hivelore enforce ci
11475
- `, "utf8");
11476
- ui.success(`Created ${path39.relative(root, workflowPath)}`);
11760
+ run: |
11761
+ set +e
11762
+ hivelore enforce ci --json > "$RUNNER_TEMP/haive-gate.json"
11763
+ echo "exit_code=$?" >> "$GITHUB_OUTPUT"
11764
+ exit 0
11765
+ - name: Upsert prevention receipt
11766
+ if: always() && github.event_name == 'pull_request'
11767
+ env:
11768
+ GH_TOKEN: \${{ github.token }}
11769
+ PR_NUMBER: \${{ github.event.pull_request.number }}
11770
+ run: |
11771
+ if [ -z "\${GH_TOKEN:-}" ] || ! command -v gh >/dev/null 2>&1; then exit 0; fi
11772
+ receipt="$(hivelore stats receipt --since 7d --json 2>/dev/null)" || exit 0
11773
+ gate="$(cat "$RUNNER_TEMP/haive-gate.json" 2>/dev/null)" || gate='{"findings":[]}'
11774
+ body="$(jq -nr --arg marker '<!-- haive:prevention-receipt -->' --argjson receipt "$receipt" --argjson gate "$gate" '
11775
+ $marker + "
11776
+ ## Hivelore prevention receipt
11777
+
11778
+ " +
11779
+ (([$gate.findings[]? | select(.code == "sensor-block" or .code == "sensor-warn") |
11780
+ "- **" + (.memory_ids[0] // "sensor") + "** \u2014 " + .message] | if length == 0 then
11781
+ "No documented sensor fired on this PR." else "### Fired on this PR
11782
+ " + join("
11783
+ ") end)) +
11784
+ "
11785
+
11786
+ Weekly total: **" + ($receipt.total|tostring) + "** refused; previous window: **" +
11787
+ ($receipt.previous_total|tostring) + "**."
11788
+ ')" || exit 0
11789
+ comments="$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" --paginate 2>/dev/null)" || exit 0
11790
+ comment_id="$(printf '%s' "$comments" | jq -r '.[] | select(.body | contains("<!-- haive:prevention-receipt -->")) | .id' | head -1)"
11791
+ if [ -n "$comment_id" ]; then
11792
+ gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$comment_id" -f body="$body" >/dev/null 2>&1 || true
11793
+ else
11794
+ gh api --method POST "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" -f body="$body" >/dev/null 2>&1 || true
11795
+ fi
11796
+ - name: Fail when enforcement blocked
11797
+ if: steps.gate.outputs.exit_code != '0'
11798
+ run: exit \${{ steps.gate.outputs.exit_code }}
11799
+ # haive:enforcement-workflow:end
11800
+ `;
11477
11801
  }
11478
11802
  function printReport(report, json, explain = false) {
11479
11803
  if (json) {
@@ -11658,9 +11982,9 @@ import { readFile as readFile19, writeFile as writeFile26 } from "fs/promises";
11658
11982
  import path40 from "path";
11659
11983
  import "commander";
11660
11984
  import { findProjectRoot as findProjectRoot38 } from "@hivelore/core";
11661
- import { execFile as execFile5 } from "child_process";
11662
- import { promisify as promisify5 } from "util";
11663
- var exec3 = promisify5(execFile5);
11985
+ import { execFile as execFile6 } from "child_process";
11986
+ import { promisify as promisify6 } from "util";
11987
+ var exec4 = promisify6(execFile6);
11664
11988
  var VERSION_FILES2 = [
11665
11989
  "package.json",
11666
11990
  "packages/core/package.json",
@@ -11737,24 +12061,24 @@ ${heading}
11737
12061
  return;
11738
12062
  }
11739
12063
  }
11740
- const dirty = (await exec3("git", ["status", "--porcelain"], { cwd: root })).stdout.trim();
12064
+ const dirty = (await exec4("git", ["status", "--porcelain"], { cwd: root })).stdout.trim();
11741
12065
  if (dirty.length > 0) {
11742
12066
  ui.error("Working tree is not clean \u2014 commit the bump before tagging.");
11743
12067
  process.exitCode = 1;
11744
12068
  return;
11745
12069
  }
11746
12070
  const tag = `v${version}`;
11747
- const existing = (await exec3("git", ["tag", "--list", tag], { cwd: root })).stdout.trim();
12071
+ const existing = (await exec4("git", ["tag", "--list", tag], { cwd: root })).stdout.trim();
11748
12072
  if (existing) {
11749
12073
  ui.error(`Tag ${tag} already exists \u2014 bump the version first.`);
11750
12074
  process.exitCode = 1;
11751
12075
  return;
11752
12076
  }
11753
- await exec3("git", ["tag", tag], { cwd: root });
12077
+ await exec4("git", ["tag", tag], { cwd: root });
11754
12078
  ui.success(`Created ${tag} at HEAD.`);
11755
12079
  if (opts.push !== false) {
11756
- await exec3("git", ["push"], { cwd: root });
11757
- await exec3("git", ["push", "origin", tag], { cwd: root });
12080
+ await exec4("git", ["push"], { cwd: root });
12081
+ await exec4("git", ["push", "origin", tag], { cwd: root });
11758
12082
  ui.success(`Pushed branch and ${tag}.`);
11759
12083
  ui.info("Next: `hivelore enforce finish --wait` (polls CI), then publish via `pnpm run publish:all` (human step).");
11760
12084
  }
@@ -11774,18 +12098,21 @@ function registerRun(program2) {
11774
12098
  }
11775
12099
 
11776
12100
  // src/commands/sensors.ts
11777
- import { execFile as execFile6 } from "child_process";
12101
+ import { execFile as execFile7 } from "child_process";
11778
12102
  import { existsSync as existsSync43 } from "fs";
11779
12103
  import { chmod as chmod2, mkdir as mkdir17, readFile as readFile20, writeFile as writeFile27 } from "fs/promises";
11780
12104
  import path41 from "path";
11781
- import { promisify as promisify6 } from "util";
12105
+ import { promisify as promisify7 } from "util";
11782
12106
  import "commander";
11783
12107
  import {
11784
12108
  extractSensorExamples,
12109
+ appendSensorEvaluations as appendSensorEvaluations2,
12110
+ assessSensorHealth as assessSensorHealth4,
11785
12111
  findProjectRoot as findProjectRoot39,
11786
12112
  isRetiredMemory as isRetiredMemory3,
11787
12113
  judgeProposedSensor,
11788
12114
  loadConfig as loadConfig13,
12115
+ loadSensorLedger as loadSensorLedger4,
11789
12116
  loadMemoriesFromDir as loadMemoriesFromDir15,
11790
12117
  recordPreventionHits as recordPreventionHits2,
11791
12118
  resolveHaivePaths as resolveHaivePaths36,
@@ -11793,10 +12120,12 @@ import {
11793
12120
  selectCommandSensors as selectCommandSensors2,
11794
12121
  sensorPatternBrittleness as sensorPatternBrittleness2,
11795
12122
  sensorSelfCheck as sensorSelfCheck2,
12123
+ sensorAppliesToPath as sensorAppliesToPath3,
11796
12124
  scannableSensorTargets,
11797
- serializeMemory as serializeMemory15
12125
+ serializeMemory as serializeMemory15,
12126
+ withoutQuarantineNote
11798
12127
  } from "@hivelore/core";
11799
- var exec4 = promisify6(execFile6);
12128
+ var exec5 = promisify7(execFile7);
11800
12129
  function registerSensors(program2) {
11801
12130
  const sensors = program2.command("sensors").description("Operate executable sensors derived from Hivelore memories");
11802
12131
  sensors.command("list").description("List memories carrying executable sensors").option("--json", "emit JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
@@ -11843,12 +12172,44 @@ function registerSensors(program2) {
11843
12172
  const commandHits = [];
11844
12173
  const commandUnrunnable = [];
11845
12174
  const commandSkipped = [];
12175
+ const ledgerRows = [];
12176
+ const headSha = await gitHeadSha(root);
12177
+ for (const memory2 of memories) {
12178
+ const sensor = memory2.frontmatter.sensor;
12179
+ if (!sensor || sensor.kind !== "regex") continue;
12180
+ const anchors = memory2.frontmatter.anchor.paths;
12181
+ const applicable = targets.some((target) => sensorAppliesToPath3(sensor, anchors, target.path));
12182
+ if (!applicable) continue;
12183
+ ledgerRows.push(evaluation({
12184
+ memory_id: memory2.frontmatter.id,
12185
+ kind: "regex",
12186
+ stage: "manual",
12187
+ head_sha: headSha,
12188
+ scope_hash: "",
12189
+ outcome: hits.some((hit) => hit.memory_id === memory2.frontmatter.id) ? "fired" : "silent"
12190
+ }));
12191
+ }
11846
12192
  if (commandSpecs.length > 0 && runCommands) {
11847
- for (const run of await executeCommandSensors(commandSpecs, root)) {
12193
+ const runs = await executeCommandSensors(commandSpecs, root);
12194
+ for (const run of runs) {
12195
+ const spec = commandSpecs.find((candidate) => candidate.memory_id === run.memory_id);
12196
+ ledgerRows.push(evaluation({
12197
+ memory_id: run.memory_id,
12198
+ kind: run.kind,
12199
+ stage: "manual",
12200
+ head_sha: headSha,
12201
+ scope_hash: await commandScopeHash(root, spec),
12202
+ outcome: run.status === "failed" ? "fired" : run.status === "passed" ? "silent" : "unrunnable"
12203
+ }, { exit_code: run.exit_code, duration_ms: run.duration_ms }));
12204
+ }
12205
+ const prior = await loadSensorLedger4(paths);
12206
+ const health = new Map(assessSensorHealth4([...prior, ...ledgerRows]).map((h) => [h.memory_id, h]));
12207
+ for (const run of runs) {
12208
+ const quarantined = health.get(run.memory_id)?.quarantine_pending === true;
11848
12209
  if (run.status === "failed") {
11849
12210
  commandHits.push({
11850
12211
  memory_id: run.memory_id,
11851
- severity: run.severity,
12212
+ severity: quarantined ? "warn" : run.severity,
11852
12213
  message: run.message,
11853
12214
  matched_line: `command failed (exit ${run.exit_code}, ${run.duration_ms}ms): ${run.command}`,
11854
12215
  ...run.output_tail ? { output_tail: run.output_tail } : {}
@@ -11860,8 +12221,17 @@ function registerSensors(program2) {
11860
12221
  } else if (commandSpecs.length > 0) {
11861
12222
  for (const spec of commandSpecs) commandSkipped.push(spec.memory_id);
11862
12223
  }
12224
+ await appendSensorEvaluations2(paths, ledgerRows);
11863
12225
  const firedIds = [...new Set([...hits, ...commandHits].map((hit) => hit.memory_id))];
11864
- await recordPreventionHits2(paths, firedIds, "sensor");
12226
+ const preventionDetails = Object.fromEntries([
12227
+ ...hits.map((hit) => [hit.memory_id, { kind: "regex", stage: "manual" }]),
12228
+ ...commandHits.map((hit) => [hit.memory_id, {
12229
+ kind: commandSpecs.find((spec) => spec.memory_id === hit.memory_id)?.kind ?? "shell",
12230
+ stage: "manual",
12231
+ exit_code: Number(/exit (\d+)/.exec(hit.matched_line)?.[1] ?? 1)
12232
+ }])
12233
+ ]);
12234
+ await recordPreventionHits2(paths, firedIds, "sensor", /* @__PURE__ */ new Date(), preventionDetails);
11865
12235
  const output = {
11866
12236
  scanned: memories.length,
11867
12237
  hits: hits.map((hit) => ({
@@ -11968,7 +12338,7 @@ function registerSensors(program2) {
11968
12338
  ...found.memory.frontmatter,
11969
12339
  sensor: { ...sensor, severity }
11970
12340
  },
11971
- body: found.memory.body
12341
+ body: severity === "block" ? withoutQuarantineNote(found.memory.body) : found.memory.body
11972
12342
  };
11973
12343
  await writeFile27(found.filePath, serializeMemory15(next), "utf8");
11974
12344
  ui.success(`Updated ${id}: sensor severity=${severity}`);
@@ -11985,7 +12355,7 @@ function registerSensors(program2) {
11985
12355
  return;
11986
12356
  }
11987
12357
  const root2 = findProjectRoot39(opts.dir);
11988
- const { proposeSensor } = await import("./server-FQ5C43MV.js");
12358
+ const { proposeSensor } = await import("./server-2FUYM6KI.js");
11989
12359
  const out = await proposeSensor(
11990
12360
  {
11991
12361
  memory_id: id,
@@ -12132,7 +12502,7 @@ async function runnableSensorMemories(paths, regexOnly = true) {
12132
12502
  }
12133
12503
  async function stagedDiff(root) {
12134
12504
  try {
12135
- const { stdout } = await exec4("git", ["diff", "--cached"], { cwd: root });
12505
+ const { stdout } = await exec5("git", ["diff", "--cached"], { cwd: root });
12136
12506
  return stdout;
12137
12507
  } catch (err) {
12138
12508
  throw new Error(`git diff --cached failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -12372,7 +12742,7 @@ import {
12372
12742
  findProjectRoot as findProjectRoot41,
12373
12743
  loadConfig as loadConfig14,
12374
12744
  loadMemoriesFromDir as loadMemoriesFromDir17,
12375
- loadPreventionEvents as loadPreventionEvents3,
12745
+ loadPreventionEvents as loadPreventionEvents4,
12376
12746
  loadUsageIndex as loadUsageIndex16,
12377
12747
  resolveHaivePaths as resolveHaivePaths38
12378
12748
  } from "@hivelore/core";
@@ -12389,7 +12759,7 @@ function registerDashboard(program2) {
12389
12759
  }
12390
12760
  const memories = existsSync45(paths.memoriesDir) ? await loadMemoriesFromDir17(paths.memoriesDir) : [];
12391
12761
  const usage = await loadUsageIndex16(paths);
12392
- const preventionEvents = await loadPreventionEvents3(paths);
12762
+ const preventionEvents = await loadPreventionEvents4(paths);
12393
12763
  const config = await loadConfig14(paths);
12394
12764
  const top = Math.max(1, Number.parseInt(opts.top ?? "10", 10) || 10);
12395
12765
  const dormantDays = opts.dormantDays ? Number.parseInt(opts.dormantDays, 10) : void 0;
@@ -12511,14 +12881,14 @@ function warnNum(n) {
12511
12881
  }
12512
12882
 
12513
12883
  // src/commands/dev-link.ts
12514
- import { execFile as execFile7 } from "child_process";
12884
+ import { execFile as execFile8 } from "child_process";
12515
12885
  import { cp, readFile as readFile22 } from "fs/promises";
12516
12886
  import { existsSync as existsSync46 } from "fs";
12517
12887
  import path43 from "path";
12518
- import { promisify as promisify7 } from "util";
12888
+ import { promisify as promisify8 } from "util";
12519
12889
  import "commander";
12520
12890
  import { findProjectRoot as findProjectRoot42 } from "@hivelore/core";
12521
- var exec5 = promisify7(execFile7);
12891
+ var exec6 = promisify8(execFile8);
12522
12892
  function registerDevLink(program2) {
12523
12893
  const dev = program2.commands.find((c) => c.name() === "dev") ?? program2.command("dev").description("Developer utilities for working on Hivelore itself.");
12524
12894
  dev.command("link").description("Hot-swap this repo's built dist into the global @hivelore (or legacy @hiveai) install so the global binary runs your local code.").option("-d, --dir <dir>", "repo root (default: discovered from cwd)").option("--json", "emit a machine-readable summary", false).action(async (opts) => {
@@ -12530,7 +12900,7 @@ function registerDevLink(program2) {
12530
12900
  }
12531
12901
  let globalModules;
12532
12902
  try {
12533
- globalModules = (await exec5("npm", ["root", "-g"])).stdout.trim();
12903
+ globalModules = (await exec6("npm", ["root", "-g"])).stdout.trim();
12534
12904
  } catch {
12535
12905
  globalModules = path43.join(path43.dirname(path43.dirname(process.execPath)), "lib", "node_modules");
12536
12906
  }
@@ -12822,7 +13192,7 @@ function registerBridges(program2) {
12822
13192
 
12823
13193
  // src/index.ts
12824
13194
  var program = new Command48();
12825
- program.name("hivelore").description("Hivelore - repo-native memory and context policy for coding-agent harnesses").version("0.34.1").option("--advanced", "show maintenance and experimental commands in help").showSuggestionAfterError(true);
13195
+ program.name("hivelore").description("Hivelore - repo-native memory and context policy for coding-agent harnesses").version("0.35.0").option("--advanced", "show maintenance and experimental commands in help").showSuggestionAfterError(true);
12826
13196
  registerInit(program);
12827
13197
  registerResolveProject(program);
12828
13198
  registerEnforce(program);