@hivelore/cli 0.34.1 → 0.35.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
@@ -10,7 +10,7 @@ import {
10
10
  preCommitCheck,
11
11
  readPresumedCorrectTargets,
12
12
  runHaiveMcpStdio
13
- } from "./chunk-X6UHROFL.js";
13
+ } from "./chunk-QQANGHJK.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.1"}`;
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,28 @@ import path15 from "path";
4714
4714
  import "commander";
4715
4715
  import {
4716
4716
  DEFAULT_AUTO_PROMOTE_RULE,
4717
+ assessSensorHealth,
4718
+ sensorPromotedAtMap,
4717
4719
  buildFrontmatter as buildFrontmatter3,
4720
+ existingGateMissShas,
4718
4721
  findProjectRoot as findProjectRoot9,
4719
4722
  getUsage as getUsage2,
4723
+ gatePassedShas,
4720
4724
  isAutoPromoteEligible,
4721
4725
  isDecaying,
4722
4726
  isStackPackSeed,
4723
4727
  loadCodeMap as loadCodeMap5,
4724
4728
  loadConfig as loadConfig3,
4725
4729
  loadMemoriesFromDir as loadMemoriesFromDir7,
4730
+ loadSensorLedger,
4726
4731
  loadUsageIndex as loadUsageIndex3,
4727
4732
  pullCrossRepoSources,
4733
+ planGitWatch,
4734
+ proposeGateMissDrafts,
4728
4735
  resolveHaivePaths as resolveHaivePaths8,
4729
4736
  resolveManifestFiles,
4730
4737
  serializeMemory as serializeMemory4,
4738
+ withQuarantineNote,
4731
4739
  trackDependencies,
4732
4740
  verifyAnchor,
4733
4741
  watchContracts
@@ -4765,6 +4773,26 @@ function registerSync(program2) {
4765
4773
  let revalidated = 0;
4766
4774
  let promoted = 0;
4767
4775
  let autoApproved = 0;
4776
+ let quarantined = 0;
4777
+ const quarantineCandidates = await loadMemoriesFromDir7(paths.memoriesDir);
4778
+ const sensorHealth = new Map(
4779
+ assessSensorHealth(await loadSensorLedger(paths), /* @__PURE__ */ new Date(), {
4780
+ promotedAt: sensorPromotedAtMap(quarantineCandidates.map((m) => m.memory.frontmatter))
4781
+ }).map((health) => [health.memory_id, health])
4782
+ );
4783
+ for (const { memory: memory2, filePath } of quarantineCandidates) {
4784
+ const sensor = memory2.frontmatter.sensor;
4785
+ const health = sensorHealth.get(memory2.frontmatter.id);
4786
+ if (!sensor || sensor.kind !== "shell" && sensor.kind !== "test" || sensor.severity !== "block" || !health?.quarantine_pending) continue;
4787
+ if (!dryRun) {
4788
+ const at = (/* @__PURE__ */ new Date()).toISOString();
4789
+ await writeFile8(filePath, serializeMemory4({
4790
+ frontmatter: { ...memory2.frontmatter, sensor: { ...sensor, severity: "warn" } },
4791
+ body: withQuarantineNote(memory2.body, at, health.flap_count)
4792
+ }), "utf8");
4793
+ }
4794
+ quarantined++;
4795
+ }
4768
4796
  if (opts.verify !== false) {
4769
4797
  const memories = await loadMemoriesFromDir7(paths.memoriesDir);
4770
4798
  for (const { memory: memory2, filePath } of memories) {
@@ -4885,13 +4913,17 @@ function registerSync(program2) {
4885
4913
  for (const repair of repairs) log(ui.dim(`autopilot: ${repair.message}`));
4886
4914
  }
4887
4915
  const sinceReport = opts.since ? collectSinceChanges(root, opts.since) : null;
4916
+ const gateMissIds = await processGateMissWatch(root, paths, dryRun);
4917
+ if (gateMissIds.length > 0) {
4918
+ log(ui.yellow(`gate-miss: proposed ${gateMissIds.length} lesson(s): ${gateMissIds.join(", ")}`));
4919
+ }
4888
4920
  const draftMemories = (await loadMemoriesFromDir7(paths.memoriesDir)).filter(
4889
4921
  (m) => m.memory.frontmatter.status === "draft"
4890
4922
  );
4891
4923
  const draftCount = draftMemories.length;
4892
4924
  const autoApprovedNote = autoApproved > 0 ? ` \xB7 ${autoApproved} auto-approved` : "";
4893
4925
  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}` : ""}`
4926
+ `${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
4927
  );
4896
4928
  if (!opts.quiet && draftCount > 0) {
4897
4929
  log(
@@ -5209,6 +5241,84 @@ Wait for **explicit confirmation** before acting.
5209
5241
  }
5210
5242
  });
5211
5243
  }
5244
+ async function processGateMissWatch(root, paths, dryRun) {
5245
+ const headResult = spawnSync4("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" });
5246
+ if (headResult.status !== 0) return [];
5247
+ const head = headResult.stdout.trim();
5248
+ if (!head) return [];
5249
+ const stateFile = path15.join(paths.runtimeDir, "enforcement", "git-watch.json");
5250
+ let state = null;
5251
+ try {
5252
+ state = JSON.parse(await readFile7(stateFile, "utf8"));
5253
+ } catch {
5254
+ }
5255
+ const plan = planGitWatch(state, head);
5256
+ if (plan.action === "initialize") {
5257
+ if (!dryRun) {
5258
+ await mkdir8(path15.dirname(stateFile), { recursive: true });
5259
+ await writeFile8(stateFile, JSON.stringify(plan.next, null, 2) + "\n", "utf8");
5260
+ }
5261
+ return [];
5262
+ }
5263
+ if (plan.action === "idle") return [];
5264
+ const proposals = proposeGateMissDrafts(
5265
+ readGitCommitsRange(root, plan.range),
5266
+ existingGateMissShas(await loadMemoriesFromDir7(paths.memoriesDir)),
5267
+ gatePassedShas(await loadSensorLedger(paths)),
5268
+ { pathExists: (rel) => existsSync14(path15.join(root, rel)) }
5269
+ );
5270
+ const ids = [];
5271
+ for (const proposal of proposals) {
5272
+ const fm = {
5273
+ ...buildFrontmatter3({
5274
+ type: "attempt",
5275
+ slug: proposal.slug,
5276
+ scope: "team",
5277
+ status: "proposed",
5278
+ tags: ["gate-miss", proposal.kind],
5279
+ paths: proposal.paths,
5280
+ topic: `gate-miss-${proposal.reverted_sha}`
5281
+ }),
5282
+ status: "proposed"
5283
+ };
5284
+ ids.push(fm.id);
5285
+ if (!dryRun) {
5286
+ await mkdir8(paths.teamDir, { recursive: true });
5287
+ await writeFile8(
5288
+ path15.join(paths.teamDir, `${fm.id}.md`),
5289
+ serializeMemory4({ frontmatter: fm, body: proposal.body }),
5290
+ "utf8"
5291
+ );
5292
+ }
5293
+ }
5294
+ if (!dryRun) {
5295
+ await mkdir8(path15.dirname(stateFile), { recursive: true });
5296
+ await writeFile8(stateFile, JSON.stringify(plan.next, null, 2) + "\n", "utf8");
5297
+ }
5298
+ return ids;
5299
+ }
5300
+ function readGitCommitsRange(root, range) {
5301
+ const result = spawnSync4(
5302
+ "git",
5303
+ ["log", "--reverse", "--format=%x1e%H%x1f%s%x1f%b%x1f", "--name-only", range],
5304
+ { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }
5305
+ );
5306
+ if (result.status !== 0) return [];
5307
+ const commits = [];
5308
+ for (const block of result.stdout.split("").filter((part) => part.trim())) {
5309
+ const [shaRaw, subjectRaw, bodyRaw, filesRaw = ""] = block.split("");
5310
+ const sha = shaRaw?.trim();
5311
+ const subject = subjectRaw?.trim();
5312
+ if (!sha || !subject) continue;
5313
+ commits.push({
5314
+ sha,
5315
+ subject,
5316
+ body: bodyRaw?.trim() ?? "",
5317
+ files: filesRaw.split("\n").map((file) => file.trim()).filter(Boolean)
5318
+ });
5319
+ }
5320
+ return commits;
5321
+ }
5212
5322
  function bridgeSummaryLine(body) {
5213
5323
  const firstLine = body.split("\n").map((l) => l.replace(/^#+\s*/, "").trim()).find((l) => l.length > 0) ?? "";
5214
5324
  const oneLine = firstLine.replace(/\s+/g, " ");
@@ -7207,16 +7317,34 @@ import { mkdir as mkdir12, writeFile as writeFile18 } from "fs/promises";
7207
7317
  import path30 from "path";
7208
7318
  import {
7209
7319
  aggregateUsage,
7320
+ buildPreventionReceipt,
7321
+ renderPreventionReceipt,
7210
7322
  findProjectRoot as findProjectRoot28,
7211
7323
  loadMemoriesFromDir as loadMemoriesFromDir11,
7212
7324
  loadUsageIndex as loadUsageIndex12,
7325
+ loadPreventionEvents as loadPreventionEvents2,
7213
7326
  parseSince,
7214
7327
  readUsageEvents,
7215
7328
  resolveHaivePaths as resolveHaivePaths27,
7216
7329
  usageLogSize
7217
7330
  } from "@hivelore/core";
7218
7331
  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(
7332
+ const stats = program2.command("stats").description("Show MCP tool-usage stats and prevention receipts.");
7333
+ 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 () => {
7334
+ const opts = stats.opts();
7335
+ const root = findProjectRoot28(opts.dir);
7336
+ const paths = resolveHaivePaths27(root);
7337
+ const sinceRaw = stats.getOptionValueSource("since") === "default" ? "7d" : opts.since ?? "7d";
7338
+ const since = parseSince(sinceRaw) ?? new Date(Date.now() - 7 * 864e5);
7339
+ const [events, usage, memories] = await Promise.all([
7340
+ loadPreventionEvents2(paths),
7341
+ loadUsageIndex12(paths),
7342
+ existsSync33(paths.memoriesDir) ? loadMemoriesFromDir11(paths.memoriesDir) : Promise.resolve([])
7343
+ ]);
7344
+ const receipt = buildPreventionReceipt(events, memories, usage, { since });
7345
+ console.log(opts.json ? JSON.stringify(receipt, null, 2) : renderPreventionReceipt(receipt));
7346
+ });
7347
+ 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
7348
  "--export-report <path>",
7221
7349
  "write a JSON rollup (tools + briefing counts + heuristic ROI hints). Parent dirs are created if needed.",
7222
7350
  void 0
@@ -7662,7 +7790,7 @@ import {
7662
7790
  findProjectRoot as findProjectRoot31,
7663
7791
  loadConfig as loadConfig7,
7664
7792
  loadEvalHistory,
7665
- loadPreventionEvents as loadPreventionEvents2,
7793
+ loadPreventionEvents as loadPreventionEvents3,
7666
7794
  loadUsageIndex as loadUsageIndex13,
7667
7795
  overallScore,
7668
7796
  resolveHaivePaths as resolveHaivePaths29,
@@ -7729,7 +7857,7 @@ function registerEval(program2) {
7729
7857
  const authoredScore = resolvedSpec.authored > 0 && resolvedSpec.synthesized > 0 && (authoredRetrievalAgg || sensorAgg) ? overallScore(authoredRetrievalAgg, sensorAgg) : null;
7730
7858
  const [usage, preventionEvents, config] = await Promise.all([
7731
7859
  loadUsageIndex13(paths),
7732
- loadPreventionEvents2(paths),
7860
+ loadPreventionEvents3(paths),
7733
7861
  loadConfig7(paths)
7734
7862
  ]);
7735
7863
  const gatePrecision = computeGatePrecision(
@@ -8449,6 +8577,8 @@ import { execFileSync, execSync } from "child_process";
8449
8577
  import "commander";
8450
8578
  import {
8451
8579
  codeMapPath as codeMapPath2,
8580
+ assessSensorHealth as assessSensorHealth2,
8581
+ sensorPromotedAtMap as sensorPromotedAtMap2,
8452
8582
  countSourceFilesOnDisk,
8453
8583
  extractReferencedPaths,
8454
8584
  sensorPatternBrittleness,
@@ -8459,6 +8589,7 @@ import {
8459
8589
  loadCodeMap as loadCodeMap6,
8460
8590
  loadConfig as loadConfig10,
8461
8591
  loadMemoriesFromDirDetailed,
8592
+ loadSensorLedger as loadSensorLedger2,
8462
8593
  loadUsageIndex as loadUsageIndex15,
8463
8594
  readUsageEvents as readUsageEvents3,
8464
8595
  resolveHaivePaths as resolveHaivePaths33
@@ -8654,6 +8785,18 @@ function registerDoctor(program2) {
8654
8785
  });
8655
8786
  }
8656
8787
  const sensorMemories = memories.filter((m) => m.memory.frontmatter.sensor);
8788
+ const gateMissDrafts = memories.filter(
8789
+ (m) => m.memory.frontmatter.status === "proposed" && m.memory.frontmatter.tags.includes("gate-miss")
8790
+ );
8791
+ if (gateMissDrafts.length > 0) {
8792
+ findings.push({
8793
+ severity: "info",
8794
+ code: "gate-miss-drafts",
8795
+ section: "Corpus health",
8796
+ message: `${gateMissDrafts.length} proposed gate-miss lesson(s) await review: ${gateMissDrafts.slice(0, 5).map((m) => m.memory.frontmatter.id).join(", ")}.`,
8797
+ fix: "Review with `hivelore memory list --status proposed`."
8798
+ });
8799
+ }
8657
8800
  const blockSensors = sensorMemories.filter(
8658
8801
  (m) => m.memory.frontmatter.sensor?.severity === "block"
8659
8802
  ).length;
@@ -8694,6 +8837,28 @@ function registerDoctor(program2) {
8694
8837
  fix: "Make the pattern discriminating (add an 'absent' companion), or demote: `hivelore sensors promote <id> --severity warn`"
8695
8838
  });
8696
8839
  }
8840
+ const sensorHealth = assessSensorHealth2(await loadSensorLedger2(paths), /* @__PURE__ */ new Date(), {
8841
+ promotedAt: sensorPromotedAtMap2(memories.map((m) => m.memory.frontmatter))
8842
+ });
8843
+ for (const health of sensorHealth.filter((h) => h.quarantine_pending)) {
8844
+ const last = health.flaps.at(-1);
8845
+ findings.push({
8846
+ severity: "warn",
8847
+ code: "sensor-flaky",
8848
+ section: "Protection",
8849
+ 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}.`,
8850
+ fix: "Run `hivelore sync` to demote it, fix the oracle, then `hivelore sensors promote <id> --yes`."
8851
+ });
8852
+ }
8853
+ for (const health of sensorHealth.filter((h) => h.never_fired)) {
8854
+ findings.push({
8855
+ severity: "info",
8856
+ code: "sensor-never-fired",
8857
+ section: "Protection",
8858
+ message: `${health.memory_id} was evaluated ${health.evaluation_count} times across 30+ days and never fired \u2014 retirement candidate.`,
8859
+ fix: "Review the sensor manually; no automatic delete/retire command is provided."
8860
+ });
8861
+ }
8697
8862
  const codeMap = await loadCodeMap6(paths);
8698
8863
  if (!codeMap) {
8699
8864
  findings.push({
@@ -8791,7 +8956,7 @@ function registerDoctor(program2) {
8791
8956
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `hivelore init` without --manual)."
8792
8957
  });
8793
8958
  }
8794
- findings.push(...await collectInstallFindings(root, "0.34.1"));
8959
+ findings.push(...await collectInstallFindings(root, "0.35.1"));
8795
8960
  findings.push(...await collectToolchainFindings(root));
8796
8961
  try {
8797
8962
  const legacyRaw = execSync("haive-mcp --version", {
@@ -8799,7 +8964,7 @@ function registerDoctor(program2) {
8799
8964
  timeout: 3e3,
8800
8965
  stdio: ["ignore", "pipe", "ignore"]
8801
8966
  }).trim();
8802
- const cliVersion = "0.34.1";
8967
+ const cliVersion = "0.35.1";
8803
8968
  if (legacyRaw && legacyRaw !== cliVersion) {
8804
8969
  findings.push({
8805
8970
  severity: "warn",
@@ -9529,14 +9694,17 @@ function registerResolveProject(program2) {
9529
9694
  }
9530
9695
 
9531
9696
  // src/commands/enforce.ts
9532
- import { execFile as execFile4, execFileSync as execFileSync2, spawn as spawn4 } from "child_process";
9697
+ import { execFile as execFile5, execFileSync as execFileSync2, spawn as spawn4 } from "child_process";
9533
9698
  import { existsSync as existsSync41, statSync as statSync3 } from "fs";
9534
9699
  import { chmod, mkdir as mkdir16, readFile as readFile18, readdir as readdir4, rm as rm2, writeFile as writeFile25 } from "fs/promises";
9535
9700
  import path39 from "path";
9536
- import { promisify as promisify4 } from "util";
9701
+ import { promisify as promisify5 } from "util";
9537
9702
  import "commander";
9538
9703
  import {
9539
9704
  antiPatternGateParams as antiPatternGateParams2,
9705
+ appendSensorEvaluations,
9706
+ assessSensorHealth as assessSensorHealth3,
9707
+ sensorPromotedAtMap as sensorPromotedAtMap3,
9540
9708
  assessBootstrapState,
9541
9709
  isSensorScannablePath,
9542
9710
  findProjectRoot as findProjectRoot37,
@@ -9550,6 +9718,7 @@ import {
9550
9718
  loadConfig as loadConfig12,
9551
9719
  detectAgentContext,
9552
9720
  loadMemoriesFromDir as loadMemoriesFromDir14,
9721
+ loadSensorLedger as loadSensorLedger3,
9553
9722
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
9554
9723
  readRecentBriefingMarker,
9555
9724
  recordPreventionHits,
@@ -9559,6 +9728,7 @@ import {
9559
9728
  saveConfig as saveConfig3,
9560
9729
  selectCommandSensors,
9561
9730
  sensorTargetsFromDiff,
9731
+ sensorAppliesToPath as sensorAppliesToPath2,
9562
9732
  SESSION_RECAP_TTL_MS,
9563
9733
  verifyAnchor as verifyAnchor3,
9564
9734
  writeBriefingMarker as writeBriefingMarker2
@@ -9761,8 +9931,52 @@ async function executeCommandSensors(specs, root) {
9761
9931
  return runs;
9762
9932
  }
9763
9933
 
9934
+ // src/utils/sensor-evaluations.ts
9935
+ import { execFile as execFile4 } from "child_process";
9936
+ import { promisify as promisify4 } from "util";
9937
+ import {
9938
+ computeScopeHash,
9939
+ sensorAppliesToPath
9940
+ } from "@hivelore/core";
9941
+ var exec3 = promisify4(execFile4);
9942
+ async function gitHeadSha(root) {
9943
+ try {
9944
+ const { stdout } = await exec3("git", ["rev-parse", "HEAD"], { cwd: root });
9945
+ return stdout.trim();
9946
+ } catch {
9947
+ return "";
9948
+ }
9949
+ }
9950
+ async function trackedFiles(root) {
9951
+ try {
9952
+ const { stdout } = await exec3("git", ["ls-files", "-co", "--exclude-standard"], { cwd: root });
9953
+ return stdout.split("\n").map((f) => f.trim()).filter(Boolean);
9954
+ } catch {
9955
+ return [];
9956
+ }
9957
+ }
9958
+ async function commandScopeHash(root, spec) {
9959
+ if (spec.paths.length === 0) return "";
9960
+ const sensor = { kind: spec.kind, paths: spec.paths };
9961
+ const files = (await trackedFiles(root)).filter((file) => sensorAppliesToPath(sensor, [], file));
9962
+ return computeScopeHash(root, files);
9963
+ }
9964
+ function evaluation(base, command) {
9965
+ return {
9966
+ at: base.at ?? (/* @__PURE__ */ new Date()).toISOString(),
9967
+ memory_id: base.memory_id,
9968
+ kind: base.kind,
9969
+ stage: base.stage,
9970
+ head_sha: base.head_sha,
9971
+ scope_hash: base.scope_hash,
9972
+ outcome: base.outcome,
9973
+ ...command?.exit_code !== null ? { exit_code: command?.exit_code } : {},
9974
+ ...command ? { duration_ms: command.duration_ms } : {}
9975
+ };
9976
+ }
9977
+
9764
9978
  // src/commands/enforce.ts
9765
- var execFileAsync2 = promisify4(execFile4);
9979
+ var execFileAsync2 = promisify5(execFile5);
9766
9980
  var MAX_STDIN_BYTES2 = 256 * 1024;
9767
9981
  var ENFORCE_HOOK_MARKER = "# Hivelore enforcement hook";
9768
9982
  function registerEnforce(program2) {
@@ -10451,7 +10665,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
10451
10665
  findings: [{ severity: "info", code: "enforcement-off", message: "Hivelore enforcement is disabled." }]
10452
10666
  });
10453
10667
  }
10454
- findings.push(...await inspectIntegrationVersions(root, "0.34.1"));
10668
+ findings.push(...await inspectIntegrationVersions(root, "0.35.1"));
10455
10669
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
10456
10670
  const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
10457
10671
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent Hivelore briefing marker exists." } : {
@@ -10531,7 +10745,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
10531
10745
  }];
10532
10746
  }
10533
10747
  const hasErrors = effectiveFindings.some((f) => f.severity === "error");
10534
- return withCategories({
10748
+ const report = withCategories({
10535
10749
  root,
10536
10750
  initialized,
10537
10751
  mode,
@@ -10540,6 +10754,18 @@ async function buildEnforcementReport(dir, stage, sessionId) {
10540
10754
  should_block: mode === "strict" && hasErrors,
10541
10755
  findings: effectiveFindings
10542
10756
  });
10757
+ if (!report.should_block && (stage === "pre-commit" || stage === "ci")) {
10758
+ const headSha = await gitHeadSha(root);
10759
+ await appendSensorEvaluations(paths, [evaluation({
10760
+ memory_id: "__gate__",
10761
+ kind: "shell",
10762
+ stage,
10763
+ head_sha: headSha,
10764
+ scope_hash: "",
10765
+ outcome: "silent"
10766
+ })]);
10767
+ }
10768
+ return report;
10543
10769
  }
10544
10770
  function withCategories(report) {
10545
10771
  return {
@@ -10695,7 +10921,7 @@ async function runPrecommitPolicy(paths, gate, stage) {
10695
10921
  anchored_blocks,
10696
10922
  semantic: true
10697
10923
  }, { paths });
10698
- const sensorFindings = await runSensorGate(paths, snapshot.diff);
10924
+ const sensorFindings = await runSensorGate(paths, snapshot.diff, stage);
10699
10925
  const reviewWarnings = result.warnings.filter(
10700
10926
  (w) => w.level === "review" && !w.reasons.includes("sensor")
10701
10927
  );
@@ -10758,7 +10984,7 @@ async function runPrecommitPolicy(paths, gate, stage) {
10758
10984
  ...sensorFindings
10759
10985
  ];
10760
10986
  }
10761
- async function runSensorGate(paths, diff) {
10987
+ async function runSensorGate(paths, diff, stage) {
10762
10988
  if (!diff || !existsSync41(paths.memoriesDir)) return [];
10763
10989
  try {
10764
10990
  const loaded = await loadMemoriesFromDir14(paths.memoriesDir);
@@ -10769,8 +10995,22 @@ async function runSensorGate(paths, diff) {
10769
10995
  const findings = [];
10770
10996
  const seen = /* @__PURE__ */ new Set();
10771
10997
  const firedIds = /* @__PURE__ */ new Set();
10998
+ const ledgerRows = [];
10999
+ const headSha = await gitHeadSha(paths.root);
10772
11000
  const regexSensorMemories = scannable.filter((m) => m.frontmatter.sensor.kind === "regex");
10773
11001
  const hits = regexSensorMemories.length > 0 ? runSensors(regexSensorMemories, targets) : [];
11002
+ for (const memory2 of regexSensorMemories) {
11003
+ const sensor = memory2.frontmatter.sensor;
11004
+ if (!targets.some((target) => sensorAppliesToPath2(sensor, memory2.frontmatter.anchor.paths, target.path))) continue;
11005
+ ledgerRows.push(evaluation({
11006
+ memory_id: memory2.frontmatter.id,
11007
+ kind: "regex",
11008
+ stage,
11009
+ head_sha: headSha,
11010
+ scope_hash: "",
11011
+ outcome: hits.some((hit) => hit.memory_id === memory2.frontmatter.id) ? "fired" : "silent"
11012
+ }));
11013
+ }
10774
11014
  for (const hit of hits) {
10775
11015
  if (seen.has(hit.memory_id)) continue;
10776
11016
  seen.add(hit.memory_id);
@@ -10782,7 +11022,8 @@ async function runSensorGate(paths, diff) {
10782
11022
  code: "sensor-block",
10783
11023
  message: `Block sensor fired \u2014 ${hit.memory_id}: ${hit.message}${where}`,
10784
11024
  fix: "Remove the flagged pattern, or run `hivelore sensors check` to inspect the match.",
10785
- impact: 45
11025
+ impact: 45,
11026
+ memory_ids: [hit.memory_id]
10786
11027
  });
10787
11028
  } else {
10788
11029
  findings.push({
@@ -10790,7 +11031,8 @@ async function runSensorGate(paths, diff) {
10790
11031
  code: "sensor-warn",
10791
11032
  message: `Sensor flagged ${hit.memory_id}: ${hit.message}${where}`,
10792
11033
  fix: "Review the flagged line; `hivelore sensors check` shows the matched code.",
10793
- impact: 5
11034
+ impact: 5,
11035
+ memory_ids: [hit.memory_id]
10794
11036
  });
10795
11037
  }
10796
11038
  }
@@ -10798,7 +11040,37 @@ async function runSensorGate(paths, diff) {
10798
11040
  if (config?.enforcement?.runCommandSensors === true) {
10799
11041
  const changedPaths = targets.map((t) => t.path).filter(Boolean);
10800
11042
  const specs = selectCommandSensors(scannable, changedPaths).filter((sp) => !seen.has(sp.memory_id));
10801
- for (const run of await executeCommandSensors(specs, paths.root)) {
11043
+ const runs = await executeCommandSensors(specs, paths.root);
11044
+ for (const run of runs) {
11045
+ const spec = specs.find((candidate) => candidate.memory_id === run.memory_id);
11046
+ ledgerRows.push(evaluation({
11047
+ memory_id: run.memory_id,
11048
+ kind: run.kind,
11049
+ stage,
11050
+ head_sha: headSha,
11051
+ scope_hash: await commandScopeHash(paths.root, spec),
11052
+ outcome: run.status === "failed" ? "fired" : run.status === "passed" ? "silent" : "unrunnable"
11053
+ }, { exit_code: run.exit_code, duration_ms: run.duration_ms }));
11054
+ }
11055
+ const prior = await loadSensorLedger3(paths);
11056
+ const promotedAt = sensorPromotedAtMap3(scannable.map((m) => m.frontmatter));
11057
+ const health = new Map(
11058
+ assessSensorHealth3([...prior, ...ledgerRows], /* @__PURE__ */ new Date(), { promotedAt }).map((h) => [h.memory_id, h])
11059
+ );
11060
+ for (const run of runs) {
11061
+ const sensorHealth = health.get(run.memory_id);
11062
+ const quarantined = sensorHealth?.quarantine_pending === true;
11063
+ if (quarantined && run.severity === "block") {
11064
+ const last = sensorHealth.flaps.at(-1);
11065
+ findings.push({
11066
+ severity: "warn",
11067
+ code: "sensor-flaky",
11068
+ 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}.`,
11069
+ fix: "Run `hivelore sync`, fix the flaky oracle, then re-promote with `hivelore sensors promote <id> --yes`.",
11070
+ impact: 5,
11071
+ memory_ids: [run.memory_id]
11072
+ });
11073
+ }
10802
11074
  if (run.status === "passed") continue;
10803
11075
  seen.add(run.memory_id);
10804
11076
  if (run.status === "unrunnable") {
@@ -10815,14 +11087,15 @@ ${run.output_tail}` : ""),
10815
11087
  firedIds.add(run.memory_id);
10816
11088
  const outputBlock = run.output_tail ? `
10817
11089
  ${run.output_tail}` : "";
10818
- if (run.severity === "block") {
11090
+ if (run.severity === "block" && !quarantined) {
10819
11091
  findings.push({
10820
11092
  severity: "error",
10821
11093
  code: "sensor-block",
10822
11094
  message: `Block ${run.kind} sensor fired \u2014 ${run.memory_id}: ${run.message}
10823
11095
  command: ${run.command} (exit ${run.exit_code}, ${run.duration_ms}ms)${outputBlock}`,
10824
11096
  fix: "Fix the behaviour the command checks, or run `hivelore sensors check --commands` to inspect it.",
10825
- impact: 45
11097
+ impact: 45,
11098
+ memory_ids: [run.memory_id]
10826
11099
  });
10827
11100
  } else {
10828
11101
  findings.push({
@@ -10830,13 +11103,19 @@ command: ${run.command} (exit ${run.exit_code}, ${run.duration_ms}ms)${outputBlo
10830
11103
  code: "sensor-warn",
10831
11104
  message: `${run.kind} sensor flagged ${run.memory_id}: ${run.message} (exit ${run.exit_code})${outputBlock}`,
10832
11105
  fix: "Review the failing command; `hivelore sensors check --commands` re-runs it.",
10833
- impact: 5
11106
+ impact: 5,
11107
+ memory_ids: [run.memory_id]
10834
11108
  });
10835
11109
  }
10836
11110
  }
10837
11111
  }
11112
+ await appendSensorEvaluations(paths, ledgerRows);
10838
11113
  if (firedIds.size > 0) {
10839
- await recordPreventionHits(paths, [...firedIds], "sensor").catch(() => {
11114
+ const details = Object.fromEntries([...firedIds].map((id) => {
11115
+ const row = ledgerRows.find((entry) => entry.memory_id === id && entry.outcome === "fired");
11116
+ return [id, { kind: row?.kind ?? "regex", stage, ...row?.exit_code !== void 0 ? { exit_code: row.exit_code } : {} }];
11117
+ }));
11118
+ await recordPreventionHits(paths, [...firedIds], "sensor", /* @__PURE__ */ new Date(), details).catch(() => {
10840
11119
  });
10841
11120
  }
10842
11121
  return findings;
@@ -11442,11 +11721,27 @@ ${hook.body}`, "utf8");
11442
11721
  async function installCiEnforcement(root) {
11443
11722
  const workflowPath = path39.join(root, ".github", "workflows", "haive-enforcement.yml");
11444
11723
  await mkdir16(path39.dirname(workflowPath), { recursive: true });
11724
+ const workflow = renderCiEnforcementWorkflow();
11445
11725
  if (existsSync41(workflowPath)) {
11446
- ui.info("GitHub Actions enforcement workflow already exists \u2014 skipped");
11726
+ const existing = await readFile18(workflowPath, "utf8");
11727
+ const start = "# haive:enforcement-workflow:start";
11728
+ const end = "# haive:enforcement-workflow:end";
11729
+ const startAt = existing.indexOf(start);
11730
+ const endAt = existing.indexOf(end);
11731
+ if (startAt >= 0 && endAt > startAt) {
11732
+ await writeFile25(workflowPath, existing.slice(0, startAt) + workflow + existing.slice(endAt + end.length), "utf8");
11733
+ ui.success(`Updated ${path39.relative(root, workflowPath)} managed block`);
11734
+ } else {
11735
+ ui.info("GitHub Actions enforcement workflow already exists without Hivelore markers \u2014 preserved");
11736
+ }
11447
11737
  return;
11448
11738
  }
11449
- await writeFile25(workflowPath, `name: haive-enforcement
11739
+ await writeFile25(workflowPath, workflow, "utf8");
11740
+ ui.success(`Created ${path39.relative(root, workflowPath)}`);
11741
+ }
11742
+ function renderCiEnforcementWorkflow() {
11743
+ return `# haive:enforcement-workflow:start
11744
+ name: haive-enforcement
11450
11745
 
11451
11746
  on:
11452
11747
  pull_request:
@@ -11458,6 +11753,7 @@ jobs:
11458
11753
  runs-on: ubuntu-latest
11459
11754
  permissions:
11460
11755
  contents: read
11756
+ pull-requests: write
11461
11757
  steps:
11462
11758
  - uses: actions/checkout@v4
11463
11759
  with:
@@ -11468,12 +11764,51 @@ jobs:
11468
11764
  - name: Install Hivelore
11469
11765
  run: npm install -g @hivelore/cli
11470
11766
  - name: Enforce Hivelore policy
11767
+ id: gate
11471
11768
  env:
11472
11769
  HAIVE_BASE_SHA: \${{ github.event.pull_request.base.sha || github.event.before }}
11473
11770
  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)}`);
11771
+ run: |
11772
+ set +e
11773
+ hivelore enforce ci --json > "$RUNNER_TEMP/haive-gate.json"
11774
+ echo "exit_code=$?" >> "$GITHUB_OUTPUT"
11775
+ exit 0
11776
+ - name: Upsert prevention receipt
11777
+ if: always() && github.event_name == 'pull_request'
11778
+ env:
11779
+ GH_TOKEN: \${{ github.token }}
11780
+ PR_NUMBER: \${{ github.event.pull_request.number }}
11781
+ run: |
11782
+ if [ -z "\${GH_TOKEN:-}" ] || ! command -v gh >/dev/null 2>&1; then exit 0; fi
11783
+ receipt="$(hivelore stats receipt --since 7d --json 2>/dev/null)" || exit 0
11784
+ gate="$(cat "$RUNNER_TEMP/haive-gate.json" 2>/dev/null)" || gate='{"findings":[]}'
11785
+ body="$(jq -nr --arg marker '<!-- haive:prevention-receipt -->' --argjson receipt "$receipt" --argjson gate "$gate" '
11786
+ $marker + "
11787
+ ## Hivelore prevention receipt
11788
+
11789
+ " +
11790
+ (([$gate.findings[]? | select(.code == "sensor-block" or .code == "sensor-warn") |
11791
+ "- **" + (.memory_ids[0] // "sensor") + "** \u2014 " + .message] | if length == 0 then
11792
+ "No documented sensor fired on this PR." else "### Fired on this PR
11793
+ " + join("
11794
+ ") end)) +
11795
+ "
11796
+
11797
+ Weekly total: **" + ($receipt.total|tostring) + "** refused; previous window: **" +
11798
+ ($receipt.previous_total|tostring) + "**."
11799
+ ')" || exit 0
11800
+ comments="$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" --paginate 2>/dev/null)" || exit 0
11801
+ comment_id="$(printf '%s' "$comments" | jq -r '.[] | select(.body | contains("<!-- haive:prevention-receipt -->")) | .id' | head -1)"
11802
+ if [ -n "$comment_id" ]; then
11803
+ gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$comment_id" -f body="$body" >/dev/null 2>&1 || true
11804
+ else
11805
+ gh api --method POST "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" -f body="$body" >/dev/null 2>&1 || true
11806
+ fi
11807
+ - name: Fail when enforcement blocked
11808
+ if: steps.gate.outputs.exit_code != '0'
11809
+ run: exit \${{ steps.gate.outputs.exit_code }}
11810
+ # haive:enforcement-workflow:end
11811
+ `;
11477
11812
  }
11478
11813
  function printReport(report, json, explain = false) {
11479
11814
  if (json) {
@@ -11658,9 +11993,9 @@ import { readFile as readFile19, writeFile as writeFile26 } from "fs/promises";
11658
11993
  import path40 from "path";
11659
11994
  import "commander";
11660
11995
  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);
11996
+ import { execFile as execFile6 } from "child_process";
11997
+ import { promisify as promisify6 } from "util";
11998
+ var exec4 = promisify6(execFile6);
11664
11999
  var VERSION_FILES2 = [
11665
12000
  "package.json",
11666
12001
  "packages/core/package.json",
@@ -11737,24 +12072,24 @@ ${heading}
11737
12072
  return;
11738
12073
  }
11739
12074
  }
11740
- const dirty = (await exec3("git", ["status", "--porcelain"], { cwd: root })).stdout.trim();
12075
+ const dirty = (await exec4("git", ["status", "--porcelain"], { cwd: root })).stdout.trim();
11741
12076
  if (dirty.length > 0) {
11742
12077
  ui.error("Working tree is not clean \u2014 commit the bump before tagging.");
11743
12078
  process.exitCode = 1;
11744
12079
  return;
11745
12080
  }
11746
12081
  const tag = `v${version}`;
11747
- const existing = (await exec3("git", ["tag", "--list", tag], { cwd: root })).stdout.trim();
12082
+ const existing = (await exec4("git", ["tag", "--list", tag], { cwd: root })).stdout.trim();
11748
12083
  if (existing) {
11749
12084
  ui.error(`Tag ${tag} already exists \u2014 bump the version first.`);
11750
12085
  process.exitCode = 1;
11751
12086
  return;
11752
12087
  }
11753
- await exec3("git", ["tag", tag], { cwd: root });
12088
+ await exec4("git", ["tag", tag], { cwd: root });
11754
12089
  ui.success(`Created ${tag} at HEAD.`);
11755
12090
  if (opts.push !== false) {
11756
- await exec3("git", ["push"], { cwd: root });
11757
- await exec3("git", ["push", "origin", tag], { cwd: root });
12091
+ await exec4("git", ["push"], { cwd: root });
12092
+ await exec4("git", ["push", "origin", tag], { cwd: root });
11758
12093
  ui.success(`Pushed branch and ${tag}.`);
11759
12094
  ui.info("Next: `hivelore enforce finish --wait` (polls CI), then publish via `pnpm run publish:all` (human step).");
11760
12095
  }
@@ -11774,18 +12109,22 @@ function registerRun(program2) {
11774
12109
  }
11775
12110
 
11776
12111
  // src/commands/sensors.ts
11777
- import { execFile as execFile6 } from "child_process";
12112
+ import { execFile as execFile7 } from "child_process";
11778
12113
  import { existsSync as existsSync43 } from "fs";
11779
12114
  import { chmod as chmod2, mkdir as mkdir17, readFile as readFile20, writeFile as writeFile27 } from "fs/promises";
11780
12115
  import path41 from "path";
11781
- import { promisify as promisify6 } from "util";
12116
+ import { promisify as promisify7 } from "util";
11782
12117
  import "commander";
11783
12118
  import {
11784
12119
  extractSensorExamples,
12120
+ appendSensorEvaluations as appendSensorEvaluations2,
12121
+ assessSensorHealth as assessSensorHealth4,
12122
+ sensorPromotedAtMap as sensorPromotedAtMap4,
11785
12123
  findProjectRoot as findProjectRoot39,
11786
12124
  isRetiredMemory as isRetiredMemory3,
11787
12125
  judgeProposedSensor,
11788
12126
  loadConfig as loadConfig13,
12127
+ loadSensorLedger as loadSensorLedger4,
11789
12128
  loadMemoriesFromDir as loadMemoriesFromDir15,
11790
12129
  recordPreventionHits as recordPreventionHits2,
11791
12130
  resolveHaivePaths as resolveHaivePaths36,
@@ -11793,10 +12132,12 @@ import {
11793
12132
  selectCommandSensors as selectCommandSensors2,
11794
12133
  sensorPatternBrittleness as sensorPatternBrittleness2,
11795
12134
  sensorSelfCheck as sensorSelfCheck2,
12135
+ sensorAppliesToPath as sensorAppliesToPath3,
11796
12136
  scannableSensorTargets,
11797
- serializeMemory as serializeMemory15
12137
+ serializeMemory as serializeMemory15,
12138
+ withoutQuarantineNote
11798
12139
  } from "@hivelore/core";
11799
- var exec4 = promisify6(execFile6);
12140
+ var exec5 = promisify7(execFile7);
11800
12141
  function registerSensors(program2) {
11801
12142
  const sensors = program2.command("sensors").description("Operate executable sensors derived from Hivelore memories");
11802
12143
  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 +12184,47 @@ function registerSensors(program2) {
11843
12184
  const commandHits = [];
11844
12185
  const commandUnrunnable = [];
11845
12186
  const commandSkipped = [];
12187
+ const ledgerRows = [];
12188
+ const headSha = await gitHeadSha(root);
12189
+ for (const memory2 of memories) {
12190
+ const sensor = memory2.frontmatter.sensor;
12191
+ if (!sensor || sensor.kind !== "regex") continue;
12192
+ const anchors = memory2.frontmatter.anchor.paths;
12193
+ const applicable = targets.some((target) => sensorAppliesToPath3(sensor, anchors, target.path));
12194
+ if (!applicable) continue;
12195
+ ledgerRows.push(evaluation({
12196
+ memory_id: memory2.frontmatter.id,
12197
+ kind: "regex",
12198
+ stage: "manual",
12199
+ head_sha: headSha,
12200
+ scope_hash: "",
12201
+ outcome: hits.some((hit) => hit.memory_id === memory2.frontmatter.id) ? "fired" : "silent"
12202
+ }));
12203
+ }
11846
12204
  if (commandSpecs.length > 0 && runCommands) {
11847
- for (const run of await executeCommandSensors(commandSpecs, root)) {
12205
+ const runs = await executeCommandSensors(commandSpecs, root);
12206
+ for (const run of runs) {
12207
+ const spec = commandSpecs.find((candidate) => candidate.memory_id === run.memory_id);
12208
+ ledgerRows.push(evaluation({
12209
+ memory_id: run.memory_id,
12210
+ kind: run.kind,
12211
+ stage: "manual",
12212
+ head_sha: headSha,
12213
+ scope_hash: await commandScopeHash(root, spec),
12214
+ outcome: run.status === "failed" ? "fired" : run.status === "passed" ? "silent" : "unrunnable"
12215
+ }, { exit_code: run.exit_code, duration_ms: run.duration_ms }));
12216
+ }
12217
+ const prior = await loadSensorLedger4(paths);
12218
+ const promotedAt = sensorPromotedAtMap4(allSensorMemories.map((m) => m.frontmatter));
12219
+ const health = new Map(
12220
+ assessSensorHealth4([...prior, ...ledgerRows], /* @__PURE__ */ new Date(), { promotedAt }).map((h) => [h.memory_id, h])
12221
+ );
12222
+ for (const run of runs) {
12223
+ const quarantined = health.get(run.memory_id)?.quarantine_pending === true;
11848
12224
  if (run.status === "failed") {
11849
12225
  commandHits.push({
11850
12226
  memory_id: run.memory_id,
11851
- severity: run.severity,
12227
+ severity: quarantined ? "warn" : run.severity,
11852
12228
  message: run.message,
11853
12229
  matched_line: `command failed (exit ${run.exit_code}, ${run.duration_ms}ms): ${run.command}`,
11854
12230
  ...run.output_tail ? { output_tail: run.output_tail } : {}
@@ -11860,8 +12236,17 @@ function registerSensors(program2) {
11860
12236
  } else if (commandSpecs.length > 0) {
11861
12237
  for (const spec of commandSpecs) commandSkipped.push(spec.memory_id);
11862
12238
  }
12239
+ await appendSensorEvaluations2(paths, ledgerRows);
11863
12240
  const firedIds = [...new Set([...hits, ...commandHits].map((hit) => hit.memory_id))];
11864
- await recordPreventionHits2(paths, firedIds, "sensor");
12241
+ const preventionDetails = Object.fromEntries([
12242
+ ...hits.map((hit) => [hit.memory_id, { kind: "regex", stage: "manual" }]),
12243
+ ...commandHits.map((hit) => [hit.memory_id, {
12244
+ kind: commandSpecs.find((spec) => spec.memory_id === hit.memory_id)?.kind ?? "shell",
12245
+ stage: "manual",
12246
+ exit_code: Number(/exit (\d+)/.exec(hit.matched_line)?.[1] ?? 1)
12247
+ }])
12248
+ ]);
12249
+ await recordPreventionHits2(paths, firedIds, "sensor", /* @__PURE__ */ new Date(), preventionDetails);
11865
12250
  const output = {
11866
12251
  scanned: memories.length,
11867
12252
  hits: hits.map((hit) => ({
@@ -11966,9 +12351,11 @@ function registerSensors(program2) {
11966
12351
  const next = {
11967
12352
  frontmatter: {
11968
12353
  ...found.memory.frontmatter,
11969
- sensor: { ...sensor, severity }
12354
+ // promoted_at makes health assessment ignore pre-promotion ledger rows: promoting is the
12355
+ // human's assertion the oracle was fixed, so old flaps must not re-quarantine it.
12356
+ sensor: severity === "block" ? { ...sensor, severity, promoted_at: (/* @__PURE__ */ new Date()).toISOString() } : { ...sensor, severity }
11970
12357
  },
11971
- body: found.memory.body
12358
+ body: severity === "block" ? withoutQuarantineNote(found.memory.body) : found.memory.body
11972
12359
  };
11973
12360
  await writeFile27(found.filePath, serializeMemory15(next), "utf8");
11974
12361
  ui.success(`Updated ${id}: sensor severity=${severity}`);
@@ -11985,7 +12372,7 @@ function registerSensors(program2) {
11985
12372
  return;
11986
12373
  }
11987
12374
  const root2 = findProjectRoot39(opts.dir);
11988
- const { proposeSensor } = await import("./server-FQ5C43MV.js");
12375
+ const { proposeSensor } = await import("./server-W5Q35K7Q.js");
11989
12376
  const out = await proposeSensor(
11990
12377
  {
11991
12378
  memory_id: id,
@@ -12132,7 +12519,7 @@ async function runnableSensorMemories(paths, regexOnly = true) {
12132
12519
  }
12133
12520
  async function stagedDiff(root) {
12134
12521
  try {
12135
- const { stdout } = await exec4("git", ["diff", "--cached"], { cwd: root });
12522
+ const { stdout } = await exec5("git", ["diff", "--cached"], { cwd: root });
12136
12523
  return stdout;
12137
12524
  } catch (err) {
12138
12525
  throw new Error(`git diff --cached failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -12372,7 +12759,7 @@ import {
12372
12759
  findProjectRoot as findProjectRoot41,
12373
12760
  loadConfig as loadConfig14,
12374
12761
  loadMemoriesFromDir as loadMemoriesFromDir17,
12375
- loadPreventionEvents as loadPreventionEvents3,
12762
+ loadPreventionEvents as loadPreventionEvents4,
12376
12763
  loadUsageIndex as loadUsageIndex16,
12377
12764
  resolveHaivePaths as resolveHaivePaths38
12378
12765
  } from "@hivelore/core";
@@ -12389,7 +12776,7 @@ function registerDashboard(program2) {
12389
12776
  }
12390
12777
  const memories = existsSync45(paths.memoriesDir) ? await loadMemoriesFromDir17(paths.memoriesDir) : [];
12391
12778
  const usage = await loadUsageIndex16(paths);
12392
- const preventionEvents = await loadPreventionEvents3(paths);
12779
+ const preventionEvents = await loadPreventionEvents4(paths);
12393
12780
  const config = await loadConfig14(paths);
12394
12781
  const top = Math.max(1, Number.parseInt(opts.top ?? "10", 10) || 10);
12395
12782
  const dormantDays = opts.dormantDays ? Number.parseInt(opts.dormantDays, 10) : void 0;
@@ -12511,14 +12898,14 @@ function warnNum(n) {
12511
12898
  }
12512
12899
 
12513
12900
  // src/commands/dev-link.ts
12514
- import { execFile as execFile7 } from "child_process";
12901
+ import { execFile as execFile8 } from "child_process";
12515
12902
  import { cp, readFile as readFile22 } from "fs/promises";
12516
12903
  import { existsSync as existsSync46 } from "fs";
12517
12904
  import path43 from "path";
12518
- import { promisify as promisify7 } from "util";
12905
+ import { promisify as promisify8 } from "util";
12519
12906
  import "commander";
12520
12907
  import { findProjectRoot as findProjectRoot42 } from "@hivelore/core";
12521
- var exec5 = promisify7(execFile7);
12908
+ var exec6 = promisify8(execFile8);
12522
12909
  function registerDevLink(program2) {
12523
12910
  const dev = program2.commands.find((c) => c.name() === "dev") ?? program2.command("dev").description("Developer utilities for working on Hivelore itself.");
12524
12911
  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 +12917,7 @@ function registerDevLink(program2) {
12530
12917
  }
12531
12918
  let globalModules;
12532
12919
  try {
12533
- globalModules = (await exec5("npm", ["root", "-g"])).stdout.trim();
12920
+ globalModules = (await exec6("npm", ["root", "-g"])).stdout.trim();
12534
12921
  } catch {
12535
12922
  globalModules = path43.join(path43.dirname(path43.dirname(process.execPath)), "lib", "node_modules");
12536
12923
  }
@@ -12822,7 +13209,7 @@ function registerBridges(program2) {
12822
13209
 
12823
13210
  // src/index.ts
12824
13211
  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);
13212
+ program.name("hivelore").description("Hivelore - repo-native memory and context policy for coding-agent harnesses").version("0.35.1").option("--advanced", "show maintenance and experimental commands in help").showSuggestionAfterError(true);
12826
13213
  registerInit(program);
12827
13214
  registerResolveProject(program);
12828
13215
  registerEnforce(program);