@hivelore/core 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
@@ -54,7 +54,14 @@ var SensorSchema = z.object({
54
54
  /** True when Hivelore generated this sensor automatically (vs. hand-authored). */
55
55
  autogen: z.boolean().default(false),
56
56
  /** ISO timestamp of the last time this sensor matched a diff. */
57
- last_fired: z.string().nullable().default(null)
57
+ last_fired: z.string().nullable().default(null),
58
+ /**
59
+ * ISO timestamp of the last manual `sensors promote` back to block. Health assessment ignores
60
+ * ledger evaluations older than this — without it, a promoted sensor whose oracle was FIXED is
61
+ * re-quarantined on the next commit for up to 30 days (the stale flaps are still in the window),
62
+ * making the promotion promised by the quarantine note a no-op.
63
+ */
64
+ promoted_at: z.string().optional()
58
65
  });
59
66
  var ActivationSchema = z.object({
60
67
  /** Case-insensitive substrings matched against the task text. */
@@ -891,7 +898,7 @@ async function appendPreventionEvent(paths, event) {
891
898
  await mkdir2(path6.dirname(file), { recursive: true });
892
899
  await appendFile(file, JSON.stringify(event) + "\n", "utf8");
893
900
  }
894
- async function recordPreventionHits(paths, firedIds, source, now = /* @__PURE__ */ new Date()) {
901
+ async function recordPreventionHits(paths, firedIds, source, now = /* @__PURE__ */ new Date(), details = {}) {
895
902
  const unique = [...new Set(firedIds)].filter(Boolean);
896
903
  if (unique.length === 0) return [];
897
904
  const usage = await loadUsageIndex(paths).catch(() => null);
@@ -905,11 +912,68 @@ async function recordPreventionHits(paths, firedIds, source, now = /* @__PURE__
905
912
  });
906
913
  const at = now.toISOString();
907
914
  for (const id of recordedIds) {
908
- await appendPreventionEvent(paths, { at, id, source }).catch(() => {
915
+ await appendPreventionEvent(paths, { at, id, source, ...details[id] }).catch(() => {
909
916
  });
910
917
  }
911
918
  return recordedIds;
912
919
  }
920
+ function buildPreventionReceipt(events, memories, usage, options) {
921
+ const now = options.now ?? /* @__PURE__ */ new Date();
922
+ const sinceMs = options.since.getTime();
923
+ const nowMs = now.getTime();
924
+ const windowMs = Math.max(1, nowMs - sinceMs);
925
+ const previousSince = sinceMs - windowMs;
926
+ const byId = new Map(memories.map((m) => [m.memory.frontmatter.id, m]));
927
+ const current = events.filter((e) => {
928
+ const at = Date.parse(e.at);
929
+ return Number.isFinite(at) && at >= sinceMs && at <= nowMs;
930
+ });
931
+ const previousTotal = events.filter((e) => {
932
+ const at = Date.parse(e.at);
933
+ return Number.isFinite(at) && at >= previousSince && at < sinceMs;
934
+ }).length;
935
+ const rows = current.map((event) => {
936
+ const loaded = byId.get(event.id);
937
+ const sensor = loaded?.memory.frontmatter.sensor;
938
+ return {
939
+ at: event.at,
940
+ id: event.id,
941
+ title: titleFromMemory(loaded) || event.id,
942
+ source: event.source,
943
+ kind: event.kind ?? sensor?.kind ?? (event.source === "sensor" ? "regex" : null),
944
+ stage: event.stage ?? null,
945
+ exit_code: event.exit_code ?? null,
946
+ message: sensor?.message ?? null
947
+ };
948
+ }).sort((a, b) => b.at.localeCompare(a.at));
949
+ const preventedCountTotal = Object.values(usage.by_id).reduce((sum, item) => sum + item.prevented_count, 0);
950
+ return {
951
+ generated_at: now.toISOString(),
952
+ since: options.since.toISOString(),
953
+ window_days: Math.max(1, Math.round(windowMs / MS_PER_DAY2)),
954
+ total: rows.length,
955
+ previous_total: previousTotal,
956
+ prevented_count_total: preventedCountTotal,
957
+ trend: computePreventionTrend(events, now),
958
+ events: rows
959
+ };
960
+ }
961
+ function renderPreventionReceipt(receipt) {
962
+ const lines = [
963
+ `Hivelore prevention receipt \u2014 last ${receipt.window_days} days`,
964
+ ` ${receipt.total} repeat mistake${receipt.total === 1 ? "" : "s"} refused before ${receipt.total === 1 ? "it" : "they"} reached review.`
965
+ ];
966
+ for (const row of receipt.events) {
967
+ const kind = row.kind ? `${row.kind} sensor` : row.source;
968
+ const exit = row.exit_code === null ? "" : `, exit ${row.exit_code}`;
969
+ const stage = row.stage ? ` \u2014 caught at ${row.stage}` : "";
970
+ lines.push(` \u2717\u2192\u2713 ${row.at.slice(0, 10)} ${row.id.padEnd(32)} (${kind}${exit}${stage})`);
971
+ }
972
+ lines.push(
973
+ ` Trend: ${receipt.total} this window vs ${receipt.previous_total} previous window (${receipt.total <= receipt.previous_total ? "recurrences declining" : "recurrences rising"}).`
974
+ );
975
+ return lines.join("\n");
976
+ }
913
977
  async function loadPreventionEvents(paths) {
914
978
  const file = preventionLogPath(paths);
915
979
  if (!existsSync4(file)) return [];
@@ -3782,10 +3846,10 @@ function sensorPatternBrittleness(pattern) {
3782
3846
  function normalizeProjectPath(value) {
3783
3847
  return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^[ab]\//, "").replace(/\/+$/g, "");
3784
3848
  }
3785
- function sensorAppliesToPath(sensor, anchorPaths, path19) {
3849
+ function sensorAppliesToPath(sensor, anchorPaths, path20) {
3786
3850
  const scopes = sensor.paths.length > 0 ? sensor.paths : anchorPaths;
3787
3851
  if (scopes.length === 0) return true;
3788
- const target = normalizeProjectPath(path19);
3852
+ const target = normalizeProjectPath(path20);
3789
3853
  return scopes.some((rawScope) => {
3790
3854
  const scope = normalizeProjectPath(rawScope);
3791
3855
  if (!scope) return false;
@@ -3978,6 +4042,154 @@ function addedLinesFromDiff(diff) {
3978
4042
  return diff.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).map((l) => l.slice(1)).join("\n");
3979
4043
  }
3980
4044
 
4045
+ // src/sensor-ledger.ts
4046
+ import { createHash as createHash2 } from "crypto";
4047
+ import { existsSync as existsSync15, readFileSync as readFileSync2 } from "fs";
4048
+ import { appendFile as appendFile4, mkdir as mkdir11, readFile as readFile14, rename, writeFile as writeFile9 } from "fs/promises";
4049
+ import path17 from "path";
4050
+ var MAX_LINES = 1e4;
4051
+ var RETAINED_LINES = 8e3;
4052
+ var DAY_MS = 864e5;
4053
+ function sensorLedgerPath(paths) {
4054
+ return path17.join(paths.runtimeDir, "enforcement", "sensor-ledger.ndjson");
4055
+ }
4056
+ function isEvaluation(value) {
4057
+ if (!value || typeof value !== "object") return false;
4058
+ const v = value;
4059
+ return typeof v.at === "string" && typeof v.memory_id === "string" && (v.kind === "regex" || v.kind === "shell" || v.kind === "test") && (v.stage === "pre-commit" || v.stage === "pre-push" || v.stage === "ci" || v.stage === "manual") && typeof v.head_sha === "string" && typeof v.scope_hash === "string" && (v.outcome === "fired" || v.outcome === "silent" || v.outcome === "unrunnable");
4060
+ }
4061
+ async function appendSensorEvaluations(paths, evaluations) {
4062
+ if (evaluations.length === 0) return;
4063
+ try {
4064
+ const file = sensorLedgerPath(paths);
4065
+ await mkdir11(path17.dirname(file), { recursive: true });
4066
+ await appendFile4(file, evaluations.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf8");
4067
+ const raw = await readFile14(file, "utf8");
4068
+ const lines = raw.split("\n").filter(Boolean);
4069
+ if (lines.length > MAX_LINES) {
4070
+ const temp = `${file}.${process.pid}.tmp`;
4071
+ await writeFile9(temp, lines.slice(-RETAINED_LINES).join("\n") + "\n", "utf8");
4072
+ await rename(temp, file);
4073
+ }
4074
+ } catch {
4075
+ }
4076
+ }
4077
+ async function loadSensorLedger(paths, opts = {}) {
4078
+ try {
4079
+ const file = sensorLedgerPath(paths);
4080
+ if (!existsSync15(file)) return [];
4081
+ const since = opts.since ? Date.parse(opts.since) : Number.NEGATIVE_INFINITY;
4082
+ const raw = await readFile14(file, "utf8");
4083
+ const out = [];
4084
+ for (const line of raw.split("\n")) {
4085
+ if (!line.trim()) continue;
4086
+ try {
4087
+ const parsed = JSON.parse(line);
4088
+ if (!isEvaluation(parsed)) continue;
4089
+ const at = Date.parse(parsed.at);
4090
+ if (!Number.isFinite(at) || at < since) continue;
4091
+ out.push(parsed);
4092
+ } catch {
4093
+ }
4094
+ }
4095
+ return out;
4096
+ } catch {
4097
+ return [];
4098
+ }
4099
+ }
4100
+ function computeScopeHash(root, scopedFiles) {
4101
+ try {
4102
+ const files = [...new Set(scopedFiles.map((f) => f.replace(/\\/g, "/")))].sort();
4103
+ if (files.length === 0) return "";
4104
+ const hash = createHash2("sha256");
4105
+ let included = 0;
4106
+ for (const rel of files) {
4107
+ const abs = path17.resolve(root, rel);
4108
+ if (!existsSync15(abs)) continue;
4109
+ try {
4110
+ hash.update(rel);
4111
+ hash.update("\0");
4112
+ hash.update(readFileSync2(abs));
4113
+ hash.update("\0");
4114
+ included++;
4115
+ } catch {
4116
+ }
4117
+ }
4118
+ return included === 0 ? "" : hash.digest("hex");
4119
+ } catch {
4120
+ return "";
4121
+ }
4122
+ }
4123
+ function assessSensorHealth(evaluations, now = /* @__PURE__ */ new Date(), opts = {}) {
4124
+ const cutoff = now.getTime() - 30 * DAY_MS;
4125
+ const byMemory = /* @__PURE__ */ new Map();
4126
+ for (const e of evaluations) {
4127
+ if (e.memory_id === "__gate__" || e.kind !== "shell" && e.kind !== "test") continue;
4128
+ const promotedAtIso = opts.promotedAt?.get(e.memory_id);
4129
+ if (promotedAtIso) {
4130
+ const promoted = Date.parse(promotedAtIso);
4131
+ if (Number.isFinite(promoted) && Date.parse(e.at) <= promoted) continue;
4132
+ }
4133
+ const list = byMemory.get(e.memory_id) ?? [];
4134
+ list.push(e);
4135
+ byMemory.set(e.memory_id, list);
4136
+ }
4137
+ const out = [];
4138
+ for (const [memoryId, all] of byMemory) {
4139
+ all.sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
4140
+ const recent = all.filter((e) => Date.parse(e.at) >= cutoff && Date.parse(e.at) <= now.getTime());
4141
+ const byHash = /* @__PURE__ */ new Map();
4142
+ for (const e of recent) {
4143
+ if (e.outcome === "unrunnable") continue;
4144
+ const list = byHash.get(e.scope_hash) ?? [];
4145
+ list.push(e);
4146
+ byHash.set(e.scope_hash, list);
4147
+ }
4148
+ const flaps = [];
4149
+ for (const [scopeHash, rows] of byHash) {
4150
+ for (let i = 1; i < rows.length; i++) {
4151
+ const previous = rows[i - 1];
4152
+ const current = rows[i];
4153
+ if (previous.outcome !== current.outcome) {
4154
+ flaps.push({ memory_id: memoryId, scope_hash: scopeHash, previous, current });
4155
+ }
4156
+ }
4157
+ }
4158
+ flaps.sort((a, b) => Date.parse(a.current.at) - Date.parse(b.current.at));
4159
+ const runnable = all.filter((e) => e.outcome !== "unrunnable");
4160
+ const span = runnable.length > 1 ? Date.parse(runnable[runnable.length - 1].at) - Date.parse(runnable[0].at) : 0;
4161
+ out.push({
4162
+ memory_id: memoryId,
4163
+ flap_count: flaps.length,
4164
+ flaps,
4165
+ quarantine_pending: flaps.length >= 2,
4166
+ never_fired: runnable.length >= 20 && span >= 30 * DAY_MS && runnable.every((e) => e.outcome === "silent"),
4167
+ evaluation_count: runnable.length
4168
+ });
4169
+ }
4170
+ return out.sort((a, b) => a.memory_id.localeCompare(b.memory_id));
4171
+ }
4172
+ function sensorPromotedAtMap(frontmatters) {
4173
+ const out = /* @__PURE__ */ new Map();
4174
+ for (const fm of frontmatters) {
4175
+ if (fm.sensor?.promoted_at) out.set(fm.id, fm.sensor.promoted_at);
4176
+ }
4177
+ return out;
4178
+ }
4179
+ function quarantineNote(at, flapCount) {
4180
+ return `> Quarantined ${at}: oracle flapped ${flapCount}\xD7 on identical inputs \u2014 demoted block\u2192warn. Fix the test, then re-promote with \`hivelore sensors promote <id>\`.`;
4181
+ }
4182
+ function withQuarantineNote(body, at, flapCount) {
4183
+ const without = body.split("\n").filter((line) => !line.startsWith("> Quarantined ")).join("\n").trimEnd();
4184
+ return `${without}
4185
+
4186
+ ${quarantineNote(at, flapCount)}
4187
+ `;
4188
+ }
4189
+ function withoutQuarantineNote(body) {
4190
+ return body.split("\n").filter((line) => !line.startsWith("> Quarantined ")).join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
4191
+ }
4192
+
3981
4193
  // src/sensor-suggest.ts
3982
4194
  var CODE_TOKEN_RE = /`([^`\n]{3,80})`|["']([A-Za-z0-9_.:-]{3,80})["']|\b([A-Za-z][A-Za-z0-9_.:-]{2,79})\b/g;
3983
4195
  var SENSOR_STOPWORDS = /* @__PURE__ */ new Set([
@@ -4425,8 +4637,8 @@ function normalizeFindingSeverity(raw) {
4425
4637
  return "info";
4426
4638
  }
4427
4639
  }
4428
- function findingKey(tool, ruleId, path19) {
4429
- return `${tool}:${ruleId}:${path19}`;
4640
+ function findingKey(tool, ruleId, path20) {
4641
+ return `${tool}:${ruleId}:${path20}`;
4430
4642
  }
4431
4643
  function coerceJson(input) {
4432
4644
  if (typeof input === "string") {
@@ -4460,8 +4672,8 @@ function parseSarif(input) {
4460
4672
  const physical = asRecord(location.physicalLocation);
4461
4673
  const artifact = asRecord(physical.artifactLocation);
4462
4674
  const region = asRecord(physical.region);
4463
- const path19 = typeof artifact.uri === "string" ? normalizeUri(artifact.uri) : "";
4464
- if (!path19) continue;
4675
+ const path20 = typeof artifact.uri === "string" ? normalizeUri(artifact.uri) : "";
4676
+ if (!path20) continue;
4465
4677
  const line = typeof region.startLine === "number" ? region.startLine : void 0;
4466
4678
  const snippet = typeof asRecord(region.snippet).text === "string" ? asRecord(region.snippet).text.trim() : void 0;
4467
4679
  findings.push({
@@ -4469,10 +4681,10 @@ function parseSarif(input) {
4469
4681
  ruleId,
4470
4682
  message: message.trim(),
4471
4683
  severity,
4472
- path: path19,
4684
+ path: path20,
4473
4685
  ...line !== void 0 ? { line } : {},
4474
4686
  ...snippet ? { snippet } : {},
4475
- key: findingKey(tool, ruleId, path19)
4687
+ key: findingKey(tool, ruleId, path20)
4476
4688
  });
4477
4689
  }
4478
4690
  }
@@ -4491,17 +4703,17 @@ function parseSonar(input) {
4491
4703
  (typeof issue.severity === "string" ? issue.severity : void 0) ?? impactSeverity
4492
4704
  );
4493
4705
  const component = typeof issue.component === "string" ? issue.component : "";
4494
- const path19 = componentToPath(component);
4495
- if (!path19) continue;
4706
+ const path20 = componentToPath(component);
4707
+ if (!path20) continue;
4496
4708
  const line = typeof issue.line === "number" ? issue.line : void 0;
4497
4709
  findings.push({
4498
4710
  tool: "sonar",
4499
4711
  ruleId,
4500
4712
  message,
4501
4713
  severity,
4502
- path: path19,
4714
+ path: path20,
4503
4715
  ...line !== void 0 ? { line } : {},
4504
- key: findingKey("sonar", ruleId, path19)
4716
+ key: findingKey("sonar", ruleId, path20)
4505
4717
  });
4506
4718
  }
4507
4719
  return findings;
@@ -4514,7 +4726,7 @@ function parseEslintJson(input, opts = {}) {
4514
4726
  const file = asRecord(fileRaw);
4515
4727
  const rawPath = typeof file.filePath === "string" ? file.filePath : "";
4516
4728
  if (!rawPath) continue;
4517
- const path19 = cwd && rawPath.startsWith(cwd) ? rawPath.slice(cwd.length) : rawPath;
4729
+ const path20 = cwd && rawPath.startsWith(cwd) ? rawPath.slice(cwd.length) : rawPath;
4518
4730
  for (const msgRaw of asArray(file.messages)) {
4519
4731
  const msg = asRecord(msgRaw);
4520
4732
  const ruleId = typeof msg.ruleId === "string" && msg.ruleId ? msg.ruleId : "parse-error";
@@ -4526,9 +4738,9 @@ function parseEslintJson(input, opts = {}) {
4526
4738
  ruleId,
4527
4739
  message,
4528
4740
  severity,
4529
- path: path19,
4741
+ path: path20,
4530
4742
  ...line !== void 0 ? { line } : {},
4531
- key: findingKey("eslint", ruleId, path19)
4743
+ key: findingKey("eslint", ruleId, path20)
4532
4744
  });
4533
4745
  }
4534
4746
  }
@@ -4946,7 +5158,7 @@ function tallyHotFiles(paths, source = "agent") {
4946
5158
  if (!norm) continue;
4947
5159
  counts.set(norm, (counts.get(norm) ?? 0) + 1);
4948
5160
  }
4949
- return [...counts.entries()].map(([path19, changes]) => ({ path: path19, changes, source })).sort((a, b) => b.changes - a.changes);
5161
+ return [...counts.entries()].map(([path20, changes]) => ({ path: path20, changes, source })).sort((a, b) => b.changes - a.changes);
4950
5162
  }
4951
5163
  function mergeHotFiles(a, b) {
4952
5164
  const merged = /* @__PURE__ */ new Map();
@@ -4966,21 +5178,21 @@ function mergeHotFiles(a, b) {
4966
5178
  }
4967
5179
 
4968
5180
  // src/eval-history.ts
4969
- import { appendFile as appendFile4, mkdir as mkdir11, readFile as readFile14 } from "fs/promises";
4970
- import { existsSync as existsSync15 } from "fs";
4971
- import path17 from "path";
5181
+ import { appendFile as appendFile5, mkdir as mkdir12, readFile as readFile15 } from "fs/promises";
5182
+ import { existsSync as existsSync16 } from "fs";
5183
+ import path18 from "path";
4972
5184
  function evalHistoryPath(paths) {
4973
- return path17.join(paths.haiveDir, ".cache", "eval-history.jsonl");
5185
+ return path18.join(paths.haiveDir, ".cache", "eval-history.jsonl");
4974
5186
  }
4975
5187
  async function appendEvalHistory(paths, entry) {
4976
5188
  const file = evalHistoryPath(paths);
4977
- await mkdir11(path17.dirname(file), { recursive: true });
4978
- await appendFile4(file, JSON.stringify(entry) + "\n", "utf8");
5189
+ await mkdir12(path18.dirname(file), { recursive: true });
5190
+ await appendFile5(file, JSON.stringify(entry) + "\n", "utf8");
4979
5191
  }
4980
5192
  async function loadEvalHistory(paths) {
4981
5193
  const file = evalHistoryPath(paths);
4982
- if (!existsSync15(file)) return [];
4983
- const raw = await readFile14(file, "utf8").catch(() => "");
5194
+ if (!existsSync16(file)) return [];
5195
+ const raw = await readFile15(file, "utf8").catch(() => "");
4984
5196
  const out = [];
4985
5197
  for (const line of raw.split("\n")) {
4986
5198
  const trimmed = line.trim();
@@ -5124,6 +5336,81 @@ function proposeSeedsFromCommits(commits, limit = 20) {
5124
5336
  return out;
5125
5337
  }
5126
5338
 
5339
+ // src/gate-miss.ts
5340
+ function planGitWatch(state, headSha) {
5341
+ const next = { last_scanned_sha: headSha };
5342
+ if (!state?.last_scanned_sha) return { action: "initialize", next };
5343
+ if (state.last_scanned_sha === headSha) return { action: "idle", next };
5344
+ return { action: "scan", range: `${state.last_scanned_sha}..${headSha}`, next };
5345
+ }
5346
+ var REVERTED_SHA_RE = /\bThis reverts commit ([0-9a-f]{7,40})\b/i;
5347
+ var BODY_REVERTED_SHA_RE = /^Reverted SHA:\s*([0-9a-f]{7,40})\s*$/im;
5348
+ function revertedShaFromCommit(commit) {
5349
+ return commit.reverted_sha ?? REVERTED_SHA_RE.exec(commit.body ?? "")?.[1] ?? null;
5350
+ }
5351
+ function existingGateMissShas(memories) {
5352
+ const shas = /* @__PURE__ */ new Set();
5353
+ for (const loaded of memories) {
5354
+ if (!loaded.memory.frontmatter.tags.includes("gate-miss")) continue;
5355
+ const sha = BODY_REVERTED_SHA_RE.exec(loaded.memory.body)?.[1];
5356
+ if (sha) shas.add(sha);
5357
+ }
5358
+ return shas;
5359
+ }
5360
+ function gatePassedShas(evaluations) {
5361
+ return new Set(
5362
+ evaluations.filter((e) => e.memory_id === "__gate__" && e.outcome === "silent" && e.head_sha).map((e) => e.head_sha)
5363
+ );
5364
+ }
5365
+ function shaMatches(set, sha) {
5366
+ for (const candidate of set) {
5367
+ if (candidate === sha || candidate.startsWith(sha) || sha.startsWith(candidate)) return true;
5368
+ }
5369
+ return false;
5370
+ }
5371
+ function proposeGateMissDrafts(commits, existingRevertedShas, passedShas, opts = {}) {
5372
+ const seeds = proposeSeedsFromCommits(commits, commits.length);
5373
+ const bySha = new Map(commits.map((commit) => [commit.sha, commit]));
5374
+ const seen = new Set(existingRevertedShas);
5375
+ const out = [];
5376
+ for (const seed of seeds) {
5377
+ const commit = bySha.get(seed.source_sha);
5378
+ if (!commit) continue;
5379
+ const failedSha = revertedShaFromCommit(commit) ?? commit.sha;
5380
+ if (shaMatches(seen, failedSha)) continue;
5381
+ seen.add(failedSha);
5382
+ const gatePassed = shaMatches(passedShas, failedSha);
5383
+ const paths = (commit.files ?? []).filter((p) => !p.startsWith(".ai/")).filter((p) => opts.pathExists?.(p) ?? true).slice(0, 8);
5384
+ const base = `# Gate miss: ${seed.what}
5385
+
5386
+ A git ${seed.kind} indicates that a change escaped the existing harness. This is a proposed lesson only; review the actual regression before validating it.
5387
+
5388
+ Reverted SHA: ${failedSha}
5389
+ Revert SHA: ${commit.sha}
5390
+ Subject: ${seed.what}
5391
+ ` + (paths.length > 0 ? `Top paths: ${paths.join(", ")}
5392
+ ` : "") + `
5393
+ **Why it failed / do NOT use:** ${seed.why_failed}
5394
+ `;
5395
+ const gateLine = gatePassed ? "\nThe gate PASSED this commit \u2014 a validated sensor here upgrades the harness.\n" : "";
5396
+ const candidate = suggestSensorSeed(seed.what, paths);
5397
+ const sensorHint = candidate ? `
5398
+ proposed_sensor_seed: ${JSON.stringify(candidate)}
5399
+ ` : "\nproposed_sensor_seed: inspect the revert diff, then author a deterministic candidate with `hivelore sensors propose <id>`.\n";
5400
+ out.push({
5401
+ slug: `gate-miss-${failedSha.slice(0, 12)}`,
5402
+ reverted_sha: failedSha,
5403
+ revert_sha: commit.sha,
5404
+ subject: seed.what,
5405
+ paths,
5406
+ kind: seed.kind,
5407
+ gate_passed: gatePassed,
5408
+ body: base + gateLine + sensorHint
5409
+ });
5410
+ }
5411
+ return out;
5412
+ }
5413
+
5127
5414
  // src/seed.ts
5128
5415
  var JS_DETECTORS = [
5129
5416
  ["nestjs", ["@nestjs/core"]],
@@ -5248,12 +5535,12 @@ ${trimmed}`;
5248
5535
  }
5249
5536
 
5250
5537
  // src/handoff.ts
5251
- import { writeFile as writeFile9, readFile as readFile15, stat as stat3 } from "fs/promises";
5252
- import { existsSync as existsSync16 } from "fs";
5253
- import path18 from "path";
5538
+ import { writeFile as writeFile10, readFile as readFile16, stat as stat3 } from "fs/promises";
5539
+ import { existsSync as existsSync17 } from "fs";
5540
+ import path19 from "path";
5254
5541
  var HANDOFF_FILENAME = "NEXT.md";
5255
5542
  function handoffFilePath(root) {
5256
- return path18.join(root, HANDOFF_FILENAME);
5543
+ return path19.join(root, HANDOFF_FILENAME);
5257
5544
  }
5258
5545
  function buildHandoffMarkdown(data) {
5259
5546
  const at = (data.at ?? /* @__PURE__ */ new Date()).toISOString();
@@ -5302,18 +5589,18 @@ function buildHandoffMarkdown(data) {
5302
5589
  }
5303
5590
  async function writeSessionHandoff(root, data) {
5304
5591
  const file = handoffFilePath(root);
5305
- await writeFile9(file, buildHandoffMarkdown(data), "utf8");
5592
+ await writeFile10(file, buildHandoffMarkdown(data), "utf8");
5306
5593
  return file;
5307
5594
  }
5308
5595
  async function readSessionHandoff(root) {
5309
5596
  const file = handoffFilePath(root);
5310
- if (!existsSync16(file)) return null;
5311
- const raw = await readFile15(file, "utf8").catch(() => "");
5597
+ if (!existsSync17(file)) return null;
5598
+ const raw = await readFile16(file, "utf8").catch(() => "");
5312
5599
  return raw.trim() ? raw : null;
5313
5600
  }
5314
5601
  async function handoffAgeMs(root, now = /* @__PURE__ */ new Date()) {
5315
5602
  const file = handoffFilePath(root);
5316
- if (!existsSync16(file)) return null;
5603
+ if (!existsSync17(file)) return null;
5317
5604
  try {
5318
5605
  const s = await stat3(file);
5319
5606
  return Math.max(0, now.getTime() - s.mtimeMs);
@@ -5429,10 +5716,12 @@ export {
5429
5716
  appendEvalHistory,
5430
5717
  appendPreventionEvent,
5431
5718
  appendRuntimeJournalEntry,
5719
+ appendSensorEvaluations,
5432
5720
  appendUsageEvent,
5433
5721
  applyConflictResolution,
5434
5722
  applyFeedbackAdjustment,
5435
5723
  assessBootstrapState,
5724
+ assessSensorHealth,
5436
5725
  bridgeMemorySummary,
5437
5726
  briefingMarkerPath,
5438
5727
  briefingMarkersDir,
@@ -5443,6 +5732,7 @@ export {
5443
5732
  buildDocFrequency,
5444
5733
  buildFrontmatter,
5445
5734
  buildHandoffMarkdown,
5735
+ buildPreventionReceipt,
5446
5736
  buildReport,
5447
5737
  bumpRead,
5448
5738
  classifyMemoryPriority,
@@ -5459,6 +5749,7 @@ export {
5459
5749
  computeImpact,
5460
5750
  computePreventionTrend,
5461
5751
  computeRecurrence,
5752
+ computeScopeHash,
5462
5753
  configPath,
5463
5754
  contractLockPath,
5464
5755
  countSourceFilesOnDisk,
@@ -5475,6 +5766,7 @@ export {
5475
5766
  estimateTokens,
5476
5767
  evalHistoryPath,
5477
5768
  evaluateSkillActivation,
5769
+ existingGateMissShas,
5478
5770
  extractActionsBriefBody,
5479
5771
  extractReferencedPaths,
5480
5772
  extractSensorExamples,
@@ -5488,6 +5780,7 @@ export {
5488
5780
  findingBody,
5489
5781
  findingToDraft,
5490
5782
  firstMemoryOneLine,
5783
+ gatePassedShas,
5491
5784
  generateBridges,
5492
5785
  getUsage,
5493
5786
  globToRegExp,
@@ -5525,6 +5818,7 @@ export {
5525
5818
  loadMemoriesFromDirDetailed,
5526
5819
  loadMemory,
5527
5820
  loadPreventionEvents,
5821
+ loadSensorLedger,
5528
5822
  loadUsageIndex,
5529
5823
  looksLikeGenericAdvice,
5530
5824
  meetsSeedQualityFloor,
@@ -5549,13 +5843,16 @@ export {
5549
5843
  pathsOverlap,
5550
5844
  pickSnippetNeedle,
5551
5845
  planConflictResolution,
5846
+ planGitWatch,
5552
5847
  prepareBridgeData,
5553
5848
  preventionLogPath,
5554
5849
  priorityRank,
5555
5850
  prioritySignals,
5556
5851
  projectContextRecentlyEmitted,
5852
+ proposeGateMissDrafts,
5557
5853
  proposeSeedsFromCommits,
5558
5854
  pullCrossRepoSources,
5855
+ quarantineNote,
5559
5856
  queryCodeMap,
5560
5857
  rankMemoriesLexical,
5561
5858
  readRecentBriefingMarker,
@@ -5571,11 +5868,13 @@ export {
5571
5868
  relPathFrom,
5572
5869
  renderBootstrapChecklist,
5573
5870
  renderCaughtForYou,
5871
+ renderPreventionReceipt,
5574
5872
  resolveBriefingBudget,
5575
5873
  resolveHaivePaths,
5576
5874
  resolveManifestFiles,
5577
5875
  resolveProjectInfo,
5578
5876
  retirementSignal,
5877
+ revertedShaFromCommit,
5579
5878
  runRegexSensor,
5580
5879
  runSensors,
5581
5880
  runtimeJournalPath,
@@ -5587,7 +5886,9 @@ export {
5587
5886
  scoreSensorCase,
5588
5887
  selectCommandSensors,
5589
5888
  sensorAppliesToPath,
5889
+ sensorLedgerPath,
5590
5890
  sensorPatternBrittleness,
5891
+ sensorPromotedAtMap,
5591
5892
  sensorSelfCheck,
5592
5893
  sensorTargetsFromDiff,
5593
5894
  serializeMemory,
@@ -5613,6 +5914,8 @@ export {
5613
5914
  usagePath,
5614
5915
  verifyAnchor,
5615
5916
  watchContracts,
5917
+ withQuarantineNote,
5918
+ withoutQuarantineNote,
5616
5919
  writeBriefingMarker,
5617
5920
  writeSessionHandoff
5618
5921
  };