@hivelore/cli 0.39.2 → 0.43.2

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
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  antiPatternsCheck,
4
+ astEngineAvailable,
4
5
  codeMapTool,
5
6
  codeSearch,
6
7
  detectTestFrameworksForAnchors,
@@ -10,8 +11,9 @@ import {
10
11
  memTried,
11
12
  preCommitCheck,
12
13
  readPresumedCorrectTargets,
14
+ runAstSensorOnContent,
13
15
  runHaiveMcpStdio
14
- } from "./chunk-XA5FXG6E.js";
16
+ } from "./chunk-FXDGOBPT.js";
15
17
  import {
16
18
  registerMemoryPending
17
19
  } from "./chunk-OYJKHD22.js";
@@ -3756,7 +3758,7 @@ ${SEED_FOOTER(stack)}` });
3756
3758
 
3757
3759
  // src/commands/init.ts
3758
3760
  var execFileAsync = promisify2(execFile2);
3759
- var HAIVE_GITHUB_ACTION_REF = `v${"0.39.2"}`;
3761
+ var HAIVE_GITHUB_ACTION_REF = `v${"0.43.2"}`;
3760
3762
  var PROJECT_CONTEXT_TEMPLATE = `# Project context
3761
3763
 
3762
3764
  > Generated by \`hivelore init\`. Run \`hivelore init --bootstrap\` to auto-fill from your codebase,
@@ -4714,6 +4716,7 @@ import { existsSync as existsSync14 } from "fs";
4714
4716
  import path15 from "path";
4715
4717
  import "commander";
4716
4718
  import {
4719
+ appendProposedRetrievalCases,
4717
4720
  DEFAULT_AUTO_PROMOTE_RULE,
4718
4721
  assessSensorHealth,
4719
4722
  sensorPromotedAtMap,
@@ -4882,7 +4885,7 @@ function registerSync(program2) {
4882
4885
  promoted++;
4883
4886
  continue;
4884
4887
  }
4885
- if (autoApproveDelayHours !== null && fm.status === "proposed" && fm.scope === "team") {
4888
+ if (autoApproveDelayHours !== null && fm.status === "proposed" && fm.scope === "team" && !fm.tags.includes("auto-captured")) {
4886
4889
  const ageHours = (nowMs - new Date(fm.created_at).getTime()) / (1e3 * 60 * 60);
4887
4890
  if (ageHours >= autoApproveDelayHours) {
4888
4891
  if (!dryRun) {
@@ -4917,6 +4920,25 @@ function registerSync(program2) {
4917
4920
  const gateMissIds = await processGateMissWatch(root, paths, dryRun);
4918
4921
  if (gateMissIds.length > 0) {
4919
4922
  log(ui.yellow(`gate-miss: proposed ${gateMissIds.length} lesson(s): ${gateMissIds.join(", ")}`));
4923
+ if (!dryRun) {
4924
+ try {
4925
+ const freshlyLoaded = await loadMemoriesFromDir7(paths.memoriesDir);
4926
+ const cases = gateMissIds.flatMap((id) => {
4927
+ const loaded = freshlyLoaded.find((m) => m.memory.frontmatter.id === id);
4928
+ const heading = loaded?.memory.body.match(/^#\s+(.+)$/m)?.[1]?.trim();
4929
+ if (!heading) return [];
4930
+ return [{ name: `gate-miss:${id}`, task: heading, expect_ids: [id] }];
4931
+ });
4932
+ if (cases.length > 0) {
4933
+ const specFile = path15.join(root, ".ai", "eval", "spec.json");
4934
+ const raw = existsSync14(specFile) ? await readFile7(specFile, "utf8") : null;
4935
+ await mkdir8(path15.dirname(specFile), { recursive: true });
4936
+ await writeFile8(specFile, appendProposedRetrievalCases(raw, cases), "utf8");
4937
+ log(ui.dim(`gate-miss: ${cases.length} proposed golden eval case(s) \u2192 .ai/eval/spec.json (approve with \`hivelore eval --approve-cases\`)`));
4938
+ }
4939
+ } catch {
4940
+ }
4941
+ }
4920
4942
  }
4921
4943
  const draftMemories = (await loadMemoriesFromDir7(paths.memoriesDir)).filter(
4922
4944
  (m) => m.memory.frontmatter.status === "draft"
@@ -6946,6 +6968,7 @@ import { spawn as spawn2 } from "child_process";
6946
6968
  import path29 from "path";
6947
6969
  import { Option as Option2 } from "commander";
6948
6970
  import {
6971
+ distillFailureObservations,
6949
6972
  buildFrontmatter as buildFrontmatter5,
6950
6973
  findProjectRoot as findProjectRoot27,
6951
6974
  loadConfig as loadConfig6,
@@ -7128,6 +7151,65 @@ function runGit(cwd, args) {
7128
7151
  });
7129
7152
  });
7130
7153
  }
7154
+ async function readObservationList(paths) {
7155
+ const obsFile = path29.join(paths.haiveDir, ".cache", "observations.jsonl");
7156
+ if (!existsSync32(obsFile)) return [];
7157
+ const raw = await readFile13(obsFile, "utf8").catch(() => "");
7158
+ const out = [];
7159
+ for (const line of raw.split("\n")) {
7160
+ if (!line.trim()) continue;
7161
+ try {
7162
+ out.push(JSON.parse(line));
7163
+ } catch {
7164
+ }
7165
+ }
7166
+ return out;
7167
+ }
7168
+ async function autoCaptureFailureLessons(paths, root, observations) {
7169
+ const failures = observations.filter((o) => o.failure_hint).map((o) => ({
7170
+ ts: o.ts,
7171
+ tool: o.tool,
7172
+ summary: o.summary,
7173
+ files: (o.files ?? []).map((f) => normalizeAnchorPath(root, f)).filter((f) => existsSync32(path29.resolve(root, f)))
7174
+ }));
7175
+ if (failures.length === 0) return { written: 0, ids: [] };
7176
+ const lessons = distillFailureObservations(failures, { max: 3 });
7177
+ if (lessons.length === 0) return { written: 0, ids: [] };
7178
+ const existing = existsSync32(paths.memoriesDir) ? await loadMemoriesFromDir10(paths.memoriesDir) : [];
7179
+ const normalizeTitle = (t) => t.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 120);
7180
+ const existingTitles = new Set(
7181
+ existing.filter(({ memory: memory2 }) => memory2.frontmatter.type === "attempt").map(({ memory: memory2 }) => normalizeTitle(memory2.body.match(/^#\s+(.+)$/m)?.[1] ?? ""))
7182
+ );
7183
+ let written = 0;
7184
+ const ids = [];
7185
+ for (const lesson of lessons) {
7186
+ if (existingTitles.has(normalizeTitle(lesson.what))) continue;
7187
+ const slug = lesson.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 6).join("-") || "auto-captured-failure";
7188
+ const baseFm = buildFrontmatter5({
7189
+ type: "attempt",
7190
+ slug,
7191
+ scope: "personal",
7192
+ tags: ["auto-captured"],
7193
+ paths: lesson.paths
7194
+ });
7195
+ const frontmatter = { ...baseFm, status: "proposed" };
7196
+ const file = memoryFilePath5(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
7197
+ if (existsSync32(file)) continue;
7198
+ const body = `# ${lesson.what}
7199
+
7200
+ **Why it failed / do NOT use:** ${lesson.why_failed}
7201
+
7202
+ _Auto-captured from session observations (${lesson.occurrences} occurrence${lesson.occurrences === 1 ? "" : "s"}). Review: refine and approve (\`hivelore memory approve ${frontmatter.id}\`), or reject it._
7203
+ `;
7204
+ await mkdir11(path29.dirname(file), { recursive: true });
7205
+ await writeFile17(file, serializeMemory12({ frontmatter, body }), "utf8").catch(() => {
7206
+ });
7207
+ existingTitles.add(normalizeTitle(lesson.what));
7208
+ written++;
7209
+ ids.push(frontmatter.id);
7210
+ }
7211
+ return { written, ids };
7212
+ }
7131
7213
  async function observationStart(paths) {
7132
7214
  const obsFile = path29.join(paths.haiveDir, ".cache", "observations.jsonl");
7133
7215
  if (!existsSync32(obsFile)) return null;
@@ -7195,11 +7277,18 @@ function registerSessionEnd(session2) {
7195
7277
  let accomplished = opts.accomplished ?? opts.summary;
7196
7278
  const caughtSince = opts.auto ? await observationStart(paths) : null;
7197
7279
  if (opts.auto) {
7280
+ const autoCaptured = await autoCaptureFailureLessons(paths, root, await readObservationList(paths)).catch(() => ({ written: 0, ids: [] }));
7198
7281
  const synth = await buildAutoRecap(paths);
7199
7282
  if (!synth) return;
7200
7283
  goal = goal ?? synth.goal;
7201
7284
  accomplished = accomplished ?? synth.accomplished;
7202
7285
  opts.discoveries = opts.discoveries ?? synth.discoveries;
7286
+ if (autoCaptured.written > 0) {
7287
+ const note = `${autoCaptured.written} failure(s) auto-captured as proposed lesson(s): ${autoCaptured.ids.join(", ")} \u2014 review with \`hivelore memory list --status proposed\` (approve, refine, or reject).`;
7288
+ opts.discoveries = opts.discoveries ? `${opts.discoveries}
7289
+ ${note}` : note;
7290
+ if (!opts.quiet) ui.info(note);
7291
+ }
7203
7292
  if (!resolvedFiles && synth.files.length) resolvedFiles = synth.files.join(",");
7204
7293
  }
7205
7294
  if (!goal || !accomplished) {
@@ -7668,7 +7757,9 @@ function registerBenchmark(program2) {
7668
7757
  " - Hivelore agents must run `hivelore briefing --files ... --task ...` first.",
7669
7758
  " - Plain agents must not read `.ai` or call Hivelore.",
7670
7759
  "5. Require every agent to write `BENCHMARK_AGENT_REPORT.md`.",
7760
+ " Its `## Outcome` section must include: Task completed, Tests passed, Policy violations, Duration seconds, Total tokens.",
7671
7761
  "6. Run `hivelore benchmark report --dir <benchmark-root> --out RESULTS.md`.",
7762
+ "7. Do not make comparative claims until evidence_grade=decision-ready (>=10 paired tasks with complete outcomes).",
7672
7763
  "",
7673
7764
  "Recommended metrics: pass rate, test iterations, files read, files changed, visible artifacts, decision quality, and token proxy."
7674
7765
  ].join("\n"));
@@ -7706,15 +7797,32 @@ function parseAgentReport(fixture, report) {
7706
7797
  terminal_failures: countMatches(section(report, "Terminal Errors"), /fail|error|not raised|exited with code 1/gi),
7707
7798
  decision_mentions: sectionBulletCount(report, "Key Decisions"),
7708
7799
  report_tokens_est: estimateTokens2(report),
7709
- haive_impact: /Hivelore Memory Impact[\s\S]*?\b(yes|directly|changed|shaped|confirmed)\b/i.test(report)
7800
+ haive_impact: /Hivelore Memory Impact[\s\S]*?\b(yes|directly|changed|shaped|confirmed)\b/i.test(report),
7801
+ task_completed: reportBoolean(report, "Task completed"),
7802
+ tests_passed: reportBoolean(report, "Tests passed"),
7803
+ policy_violations: reportNumber(report, "Policy violations"),
7804
+ duration_seconds: reportNumber(report, "Duration seconds"),
7805
+ total_tokens: reportNumber(report, "Total tokens")
7710
7806
  };
7711
7807
  }
7712
7808
  function summarizeRows(rows) {
7713
7809
  const byGroup = (group) => rows.filter((r) => r.group === group);
7810
+ const haiveRows = byGroup("haive");
7811
+ const plainRows = byGroup("plain");
7812
+ const taskName = (fixture) => fixture.replace(/-(haive|plain)$/, "");
7813
+ const plainTasks = new Set(plainRows.map((row) => taskName(row.fixture)));
7814
+ const pairedTasks = new Set(haiveRows.map((row) => taskName(row.fixture)).filter((name) => plainTasks.has(name))).size;
7815
+ const outcomeComplete = rows.length > 0 && rows.every(
7816
+ (row) => row.task_completed !== null && row.tests_passed !== null && row.policy_violations !== null && row.duration_seconds !== null && row.total_tokens !== null
7817
+ );
7818
+ const decisionReady = pairedTasks >= 10 && outcomeComplete;
7714
7819
  return {
7715
7820
  fixtures: rows.length,
7716
- haive: summarizeGroup(byGroup("haive")),
7717
- plain: summarizeGroup(byGroup("plain"))
7821
+ paired_tasks: pairedTasks,
7822
+ evidence_grade: decisionReady ? "decision-ready" : "insufficient",
7823
+ evidence_reason: decisionReady ? "At least 10 paired tasks with complete correctness, policy, duration, and token outcomes." : `Need >=10 paired tasks and complete Outcome fields; found ${pairedTasks} pair(s), outcome_complete=${outcomeComplete}.`,
7824
+ haive: summarizeGroup(haiveRows),
7825
+ plain: summarizeGroup(plainRows)
7718
7826
  };
7719
7827
  }
7720
7828
  function summarizeGroup(rows) {
@@ -7728,7 +7836,12 @@ function summarizeGroup(rows) {
7728
7836
  terminal_failures: sum("terminal_failures"),
7729
7837
  decision_mentions: sum("decision_mentions"),
7730
7838
  report_tokens_est: sum("report_tokens_est"),
7731
- haive_impact_count: rows.filter((r) => r.haive_impact).length
7839
+ haive_impact_count: rows.filter((r) => r.haive_impact).length,
7840
+ completed: rows.filter((r) => r.task_completed === true).length,
7841
+ tests_passed: rows.filter((r) => r.tests_passed === true).length,
7842
+ policy_violations: rows.reduce((total, row) => total + (row.policy_violations ?? 0), 0),
7843
+ duration_seconds: rows.reduce((total, row) => total + (row.duration_seconds ?? 0), 0),
7844
+ total_tokens: rows.reduce((total, row) => total + (row.total_tokens ?? 0), 0)
7732
7845
  };
7733
7846
  }
7734
7847
  function renderMarkdown(root, summary, rows) {
@@ -7739,6 +7852,8 @@ function renderMarkdown(root, summary, rows) {
7739
7852
  "",
7740
7853
  "## Summary",
7741
7854
  "",
7855
+ `Evidence grade: **${summary.evidence_grade}** \u2014 ${summary.evidence_reason}`,
7856
+ "",
7742
7857
  "| Group | Fixtures | Commands | Files read | Files modified | Test iterations | Terminal failures | Decision mentions | Report tokens (est, report only) | Hivelore impact |",
7743
7858
  "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
7744
7859
  groupLine("Hivelore", summary.haive),
@@ -7762,6 +7877,23 @@ function renderMarkdown(root, summary, rows) {
7762
7877
  ];
7763
7878
  return lines.join("\n");
7764
7879
  }
7880
+ function reportValue(report, label) {
7881
+ const match = new RegExp(`^[-*]\\s*${escapeRegExp(label)}\\s*:\\s*(.+)$`, "im").exec(report);
7882
+ return match?.[1]?.trim() ?? null;
7883
+ }
7884
+ function reportBoolean(report, label) {
7885
+ const value = reportValue(report, label);
7886
+ if (!value) return null;
7887
+ if (/^(yes|true|pass|passed|complete|completed)$/i.test(value)) return true;
7888
+ if (/^(no|false|fail|failed|incomplete)$/i.test(value)) return false;
7889
+ return null;
7890
+ }
7891
+ function reportNumber(report, label) {
7892
+ const value = reportValue(report, label);
7893
+ if (!value) return null;
7894
+ const parsed = Number(value.replace(/,/g, ""));
7895
+ return Number.isFinite(parsed) ? parsed : null;
7896
+ }
7765
7897
  function groupLine(label, group) {
7766
7898
  return `| ${label} | ${group.fixtures} | ${group.commands} | ${group.files_read} | ${group.files_modified} | ${group.test_iterations} | ${group.terminal_failures} | ${group.decision_mentions} | ${group.report_tokens_est} | ${group.haive_impact_count} |`;
7767
7899
  }
@@ -7798,7 +7930,9 @@ import {
7798
7930
  loadEvalHistory,
7799
7931
  loadPreventionEvents as loadPreventionEvents3,
7800
7932
  loadUsageIndex as loadUsageIndex13,
7933
+ approveProposedCases,
7801
7934
  overallScore,
7935
+ runTierContract,
7802
7936
  resolveHaivePaths as resolveHaivePaths29,
7803
7937
  scoreRetrievalCase,
7804
7938
  scoreSensorCase,
@@ -7807,7 +7941,7 @@ import {
7807
7941
  function registerEval(program2) {
7808
7942
  program2.command("eval").description(
7809
7943
  "Rigorous, repeatable quality eval: do the right memories surface (retrieval) and do the right sensors fire (catch-rate)? Emits a numeric 0\u2013100 score. Uses .ai/eval cases via --spec, or auto-synthesizes cases from anchored memories."
7810
- ).option("--spec <file>", "JSON eval spec ({ retrieval: [...], sensors: [...] })").option("--semantic-only", "self-eval probes by title alone (no anchor files) \u2014 harder retrieval", false).option("-k, --top <n>", "briefing top-k considered a hit", "8").option("--json", "emit JSON", false).option("--out <file>", "write a Markdown report").option("--fail-under <score>", "exit non-zero if the overall score is below this (0\u2013100) \u2014 for CI gates").option("--fail-under-catch-rate <pct>", "exit non-zero if sensor catch-rate is below this percentage").option("--fail-under-gate-precision <pct>", "exit non-zero if gate precision is below this percentage").option("--baseline", "save this run as the baseline (.ai/eval/baseline.json) for future --compare", false).option("--compare", "diff this run against the saved baseline and print the delta", false).option("--baseline-file <path>", "baseline file to read/write (default: .ai/eval/baseline.json)").option("--fail-on-regression", "with --compare, exit non-zero if the score dropped vs the baseline", false).option("--regression-gate", "CI-safe gate: compare against the baseline IF one exists (fail on regression), else no-op", false).option("--record", "append this run's score to .ai/.cache/eval-history.jsonl (trend the harness over time)", false).option("--trend", "print the recorded score trend (sparkline + latest/best/delta) and exit", false).option("--ref <ref>", "version/commit label stored with a --record run").option("-d, --dir <dir>", "project root").action(async (opts) => {
7944
+ ).option("--spec <file>", "JSON eval spec ({ retrieval: [...], sensors: [...] })").option("--semantic-only", "self-eval probes by title alone (no anchor files) \u2014 harder retrieval", false).option("-k, --top <n>", "briefing top-k considered a hit", "8").option("--json", "emit JSON", false).option("--out <file>", "write a Markdown report").option("--fail-under <score>", "exit non-zero if the overall score is below this (0\u2013100) \u2014 for CI gates").option("--fail-under-catch-rate <pct>", "exit non-zero if sensor catch-rate is below this percentage").option("--fail-under-gate-precision <pct>", "exit non-zero if gate precision is below this percentage").option("--baseline", "save this run as the baseline (.ai/eval/baseline.json) for future --compare", false).option("--compare", "diff this run against the saved baseline and print the delta", false).option("--baseline-file <path>", "baseline file to read/write (default: .ai/eval/baseline.json)").option("--fail-on-regression", "with --compare, exit non-zero if the score dropped vs the baseline", false).option("--regression-gate", "CI-safe gate: compare against the baseline IF one exists (fail on regression), else no-op", false).option("--approve-cases", "approve every proposed golden case (gate-miss labeled) into the scored retrieval set, then exit", false).option("--record", "append this run's score to .ai/.cache/eval-history.jsonl (trend the harness over time)", false).option("--trend", "print the recorded score trend (sparkline + latest/best/delta) and exit", false).option("--ref <ref>", "version/commit label stored with a --record run").option("-d, --dir <dir>", "project root").action(async (opts) => {
7811
7945
  const root = findProjectRoot31(opts.dir);
7812
7946
  const paths = resolveHaivePaths29(root);
7813
7947
  if (!existsSync35(paths.memoriesDir)) {
@@ -7815,6 +7949,21 @@ function registerEval(program2) {
7815
7949
  process.exitCode = 1;
7816
7950
  return;
7817
7951
  }
7952
+ if (opts.approveCases) {
7953
+ const specFile = path32.join(root, ".ai", "eval", "spec.json");
7954
+ if (!existsSync35(specFile)) {
7955
+ ui.info("No .ai/eval/spec.json \u2014 nothing to approve.");
7956
+ return;
7957
+ }
7958
+ const { raw, approved } = approveProposedCases(await readFile15(specFile, "utf8"));
7959
+ if (approved === 0) {
7960
+ ui.info("No proposed cases waiting for approval.");
7961
+ return;
7962
+ }
7963
+ await writeFile20(specFile, raw, "utf8");
7964
+ ui.success(`Approved ${approved} proposed golden case(s) into the scored retrieval set.`);
7965
+ return;
7966
+ }
7818
7967
  if (opts.trend) {
7819
7968
  const trend = computeEvalTrend(await loadEvalHistory(paths));
7820
7969
  if (opts.json) {
@@ -7835,6 +7984,17 @@ function registerEval(program2) {
7835
7984
  const ctx = { paths };
7836
7985
  const resolvedSpec = await resolveSpec(opts, root, paths.memoriesDir);
7837
7986
  const spec = resolvedSpec.spec;
7987
+ const tierChecks = runTierContract();
7988
+ const tierFailures = tierChecks.filter((c) => !c.pass);
7989
+ let proposedGoldenCount = 0;
7990
+ try {
7991
+ const specFile = path32.join(root, ".ai", "eval", "spec.json");
7992
+ if (!opts.spec && existsSync35(specFile)) {
7993
+ const parsed = JSON.parse(await readFile15(specFile, "utf8"));
7994
+ proposedGoldenCount = parsed.proposed_retrieval?.length ?? 0;
7995
+ }
7996
+ } catch {
7997
+ }
7838
7998
  if ((spec.retrieval?.length ?? 0) === 0 && (spec.sensors?.length ?? 0) === 0) {
7839
7999
  ui.warn("No eval cases (no anchored memories and no --spec). Nothing to score.");
7840
8000
  return;
@@ -7861,6 +8021,8 @@ function registerEval(program2) {
7861
8021
  const authoredRetrievalCases = retrievalAgg ? retrievalAgg.cases.slice(resolvedSpec.synthesized) : [];
7862
8022
  const authoredRetrievalAgg = authoredRetrievalCases.length > 0 ? aggregateRetrieval(authoredRetrievalCases) : null;
7863
8023
  const authoredScore = resolvedSpec.authored > 0 && resolvedSpec.synthesized > 0 && (authoredRetrievalAgg || sensorAgg) ? overallScore(authoredRetrievalAgg, sensorAgg) : null;
8024
+ const authoredReport = resolvedSpec.authored > 0 && (authoredRetrievalAgg || sensorAgg) ? buildReport(authoredRetrievalAgg, sensorAgg) : null;
8025
+ const gateReport = authoredReport ?? report;
7864
8026
  const [usage, preventionEvents, config] = await Promise.all([
7865
8027
  loadUsageIndex13(paths),
7866
8028
  loadPreventionEvents3(paths),
@@ -7889,6 +8051,7 @@ function registerEval(program2) {
7889
8051
  k,
7890
8052
  spec_source: resolvedSpec.source,
7891
8053
  report,
8054
+ ...authoredReport ? { authored_report: authoredReport } : {},
7892
8055
  gate_precision: gatePrecision
7893
8056
  };
7894
8057
  await mkdir13(path32.dirname(baselineFile), { recursive: true });
@@ -7908,7 +8071,7 @@ function registerEval(program2) {
7908
8071
  }
7909
8072
  } else {
7910
8073
  const snapshot = JSON.parse(await readFile15(baselineFile, "utf8"));
7911
- delta = compareEvalReports(snapshot.report, report);
8074
+ delta = compareEvalReports(snapshot.authored_report ?? snapshot.report, gateReport);
7912
8075
  if (snapshot.gate_precision) {
7913
8076
  gateDelta = compareGatePrecision(snapshot.gate_precision, gatePrecision);
7914
8077
  }
@@ -7925,11 +8088,15 @@ function registerEval(program2) {
7925
8088
  ...authoredScore !== null ? { authored_score: authoredScore } : {}
7926
8089
  },
7927
8090
  report,
8091
+ ...authoredReport ? { authored_report: authoredReport } : {},
8092
+ tier_contract: { checks: tierChecks, failures: tierFailures.length },
8093
+ proposed_golden_cases: proposedGoldenCount,
7928
8094
  gate_precision: gatePrecision,
7929
8095
  ...delta ? { delta } : {},
7930
8096
  ...gateDelta ? { gate_delta: gateDelta } : {}
7931
8097
  }, null, 2));
7932
- applyExitGates(opts, report, delta, gatePrecision, gateDelta);
8098
+ if (tierFailures.length > 0) process.exitCode = 1;
8099
+ applyExitGates(opts, gateReport, delta, gatePrecision, gateDelta);
7933
8100
  return;
7934
8101
  }
7935
8102
  if (resolvedSpec.authored === 0 && resolvedSpec.synthesized > 0) {
@@ -7943,6 +8110,17 @@ function registerEval(program2) {
7943
8110
  if (gateDelta) {
7944
8111
  console.log(renderGateDelta(gateDelta));
7945
8112
  }
8113
+ if (tierFailures.length > 0) {
8114
+ ui.error(`Ranking tier contract BROKEN \u2014 ${tierFailures.length} check(s) violate the designed tiers:`);
8115
+ for (const c of tierFailures) ui.error(` \u2717 ${c.name} (expected ${c.expected}, got ${c.actual})`);
8116
+ } else {
8117
+ ui.info(`Ranking tier contract: ${tierChecks.length}/${tierChecks.length} checks hold.`);
8118
+ }
8119
+ if (proposedGoldenCount > 0) {
8120
+ ui.warn(
8121
+ `${proposedGoldenCount} proposed golden case(s) (gate-miss labeled) await approval \u2014 review .ai/eval/spec.json, then \`hivelore eval --approve-cases\` to score them.`
8122
+ );
8123
+ }
7946
8124
  const md = renderMarkdown2(root, k, resolvedSpec, report, gatePrecision, authoredScore);
7947
8125
  if (opts.out) {
7948
8126
  const outFile = path32.isAbsolute(opts.out) ? opts.out : path32.join(root, opts.out);
@@ -7951,7 +8129,8 @@ function registerEval(program2) {
7951
8129
  } else {
7952
8130
  console.log(md);
7953
8131
  }
7954
- applyExitGates(opts, report, delta, gatePrecision, gateDelta);
8132
+ if (tierFailures.length > 0) process.exitCode = 1;
8133
+ applyExitGates(opts, gateReport, delta, gatePrecision, gateDelta);
7955
8134
  });
7956
8135
  }
7957
8136
  function parsePctThreshold(label, raw) {
@@ -8040,7 +8219,7 @@ async function resolveSpec(opts, root, memoriesDir) {
8040
8219
  if (existsSync35(defaultSpec)) {
8041
8220
  const raw = await readFile15(defaultSpec, "utf8");
8042
8221
  const explicit = JSON.parse(raw);
8043
- const memories2 = await loadMemoriesFromDir(memoriesDir);
8222
+ const memories2 = (await loadMemoriesFromDir(memoriesDir)).filter(({ memory: memory2 }) => memory2.frontmatter.scope !== "personal");
8044
8223
  const synthesized2 = synthesizeSelfEvalCases(memories2, { includeFiles: !opts.semanticOnly });
8045
8224
  return {
8046
8225
  spec: {
@@ -8052,7 +8231,7 @@ async function resolveSpec(opts, root, memoriesDir) {
8052
8231
  authored: countCases(explicit)
8053
8232
  };
8054
8233
  }
8055
- const memories = await loadMemoriesFromDir(memoriesDir);
8234
+ const memories = (await loadMemoriesFromDir(memoriesDir)).filter(({ memory: memory2 }) => memory2.frontmatter.scope !== "personal");
8056
8235
  const synthesized = synthesizeSelfEvalCases(memories, { includeFiles: !opts.semanticOnly });
8057
8236
  return {
8058
8237
  spec: { retrieval: synthesized },
@@ -8074,6 +8253,8 @@ async function runRetrieval(c, k, ctx) {
8074
8253
  semantic: true,
8075
8254
  include_stale: false,
8076
8255
  track: false,
8256
+ memory_scopes: ["team", "module"],
8257
+ deterministic: true,
8077
8258
  format: "compact",
8078
8259
  min_semantic_score: 0
8079
8260
  },
@@ -8894,6 +9075,45 @@ function registerDoctor(program2) {
8894
9075
  fix: "hivelore sensors list # then `hivelore sensors promote <id>` for a trusted, non-brittle sensor \u2014 or retire the noise"
8895
9076
  });
8896
9077
  }
9078
+ const commandSensorMemories = sensorMemories.filter((m) => {
9079
+ const kind = m.memory.frontmatter.sensor?.kind;
9080
+ return kind === "shell" || kind === "test";
9081
+ });
9082
+ if (commandSensorMemories.length > 0 && config.enforcement?.runCommandSensors !== true) {
9083
+ findings.push({
9084
+ severity: "error",
9085
+ code: "command-sensors-disabled",
9086
+ section: "Protection",
9087
+ message: `${commandSensorMemories.length} command/test sensor(s) exist but enforcement.runCommandSensors is not true \u2014 their behaviour protection is OFF.`,
9088
+ fix: "Set enforcement.runCommandSensors=true after reviewing the repo-authored commands."
9089
+ });
9090
+ }
9091
+ const unprovenBlocks = commandSensorMemories.filter((m) => {
9092
+ const sensor = m.memory.frontmatter.sensor;
9093
+ return sensor.severity === "block" && sensor.red_proven !== true;
9094
+ });
9095
+ if (unprovenBlocks.length > 0) {
9096
+ findings.push({
9097
+ severity: "error",
9098
+ code: "command-sensors-block-without-red",
9099
+ section: "Protection",
9100
+ message: `${unprovenBlocks.length} blocking command/test sensor(s) predate mandatory prove-RED and cannot substantiate their incident claim.`,
9101
+ fix: "Re-propose each sensor with --red-ref <pre-fix-commit>, or demote it to warn."
9102
+ });
9103
+ }
9104
+ const astSensorCount = sensorMemories.filter((m) => m.memory.frontmatter.sensor?.kind === "ast").length;
9105
+ if (astSensorCount > 0) {
9106
+ const { astEngineAvailable: astEngineAvailable2 } = await import("./server-WW6JHBYY.js");
9107
+ if (!await astEngineAvailable2()) {
9108
+ findings.push({
9109
+ severity: "warn",
9110
+ code: "ast-engine-missing",
9111
+ section: "Protection",
9112
+ message: `${astSensorCount} AST sensor(s) exist but the optional @ast-grep/napi engine is not installed \u2014 they are unrunnable on this machine (warn-only at the gate, protection OFF).`,
9113
+ fix: "npm i -g @ast-grep/napi # or add it to the repo devDependencies"
9114
+ });
9115
+ }
9116
+ }
8897
9117
  const firesOnCurrent = [];
8898
9118
  for (const m of sensorMemories) {
8899
9119
  const s = m.memory.frontmatter.sensor;
@@ -9053,7 +9273,7 @@ function registerDoctor(program2) {
9053
9273
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `hivelore init` without --manual)."
9054
9274
  });
9055
9275
  }
9056
- findings.push(...await collectInstallFindings(root, "0.39.2"));
9276
+ findings.push(...await collectInstallFindings(root, "0.43.2"));
9057
9277
  findings.push(...await collectToolchainFindings(root));
9058
9278
  try {
9059
9279
  const legacyRaw = execSync("haive-mcp --version", {
@@ -9061,7 +9281,7 @@ function registerDoctor(program2) {
9061
9281
  timeout: 3e3,
9062
9282
  stdio: ["ignore", "pipe", "ignore"]
9063
9283
  }).trim();
9064
- const cliVersion = "0.39.2";
9284
+ const cliVersion = "0.43.2";
9065
9285
  if (legacyRaw && legacyRaw !== cliVersion) {
9066
9286
  findings.push({
9067
9287
  severity: "warn",
@@ -9806,6 +10026,7 @@ import {
9806
10026
  assessSensorHealth as assessSensorHealth3,
9807
10027
  sensorPromotedAtMap as sensorPromotedAtMap3,
9808
10028
  assessBootstrapState,
10029
+ addedLineNumbersFromDiff,
9809
10030
  detectSensorWeakening,
9810
10031
  isSensorScannablePath,
9811
10032
  findProjectRoot as findProjectRoot37,
@@ -9959,6 +10180,7 @@ function defaultClaudeSettingsPath(scope, projectRoot) {
9959
10180
  // src/utils/command-sensors.ts
9960
10181
  import { execFile as execFile3 } from "child_process";
9961
10182
  import { promisify as promisify3 } from "util";
10183
+ import { scrubbedCommandEnv } from "@hivelore/core";
9962
10184
  var exec2 = promisify3(execFile3);
9963
10185
  var COMMAND_SENSOR_DEFAULT_TIMEOUT_MS = 12e4;
9964
10186
  var OUTPUT_TAIL_LINES = 15;
@@ -9982,7 +10204,9 @@ async function executeCommandSensor(spec, root) {
9982
10204
  cwd: root,
9983
10205
  timeout: timeoutMs,
9984
10206
  maxBuffer: 8 * 1024 * 1024,
9985
- env: { ...process.env, HIVELORE_SENSOR: spec.memory_id }
10207
+ // Scrubbed on purpose: a repo-authored oracle gets a test-runner environment, not the
10208
+ // caller's credentials (cloud keys, tokens). See scrubbedCommandEnv in core.
10209
+ env: { ...scrubbedCommandEnv(process.env), HIVELORE_SENSOR: spec.memory_id }
9986
10210
  });
9987
10211
  return {
9988
10212
  ...base,
@@ -10583,11 +10807,14 @@ async function checkFailureCapture(paths, config) {
10583
10807
  message: "No uncaptured hard failures from this session."
10584
10808
  }];
10585
10809
  }
10810
+ const autoDrafts = memories.filter(
10811
+ ({ memory: memory2 }) => memory2.frontmatter.status === "proposed" && memory2.frontmatter.tags.includes("auto-captured")
10812
+ );
10586
10813
  return [{
10587
10814
  severity: gate === "block" ? "error" : "info",
10588
10815
  code: "uncaptured-failures",
10589
- message: `${uncaptured.length} hard failure(s) this session were never captured as a lesson (mem_tried).`,
10590
- fix: "Call `mem_tried` (or `hivelore memory tried`) for each real failure so the next session doesn't repeat it. False positives (e.g. a grep that found nothing) can be ignored.",
10816
+ message: `${uncaptured.length} hard failure(s) this session were never captured as a lesson (mem_tried).` + (autoDrafts.length > 0 ? ` ${autoDrafts.length} auto-captured draft(s) are waiting for review: ${autoDrafts.slice(0, 3).map(({ memory: memory2 }) => memory2.frontmatter.id).join(", ")}${autoDrafts.length > 3 ? ", \u2026" : ""}.` : ""),
10817
+ fix: autoDrafts.length > 0 ? "Review the auto-captured drafts (`hivelore memory list --status proposed`) \u2014 approve, refine, or reject; call `mem_tried` only for failures the drafts missed." : "Call `mem_tried` (or `hivelore memory tried`) for each real failure so the next session doesn't repeat it. False positives (e.g. a grep that found nothing) can be ignored.",
10591
10818
  reason: "Harness ratchet: a mistake that isn't written down gets re-introduced. Set enforcement.failureCaptureGate to 'off' to disable, or 'block' to hard-fail.",
10592
10819
  affected_files: uncaptured.slice(0, 8).map((f) => `${f.tool}: ${f.summary}`.slice(0, 100)),
10593
10820
  ...gate === "block" ? { impact: 30 } : {}
@@ -10647,6 +10874,22 @@ async function runWithEnforcement(command, args, opts) {
10647
10874
  child.on("close", (code, signal) => {
10648
10875
  if (signal) process.exit(128);
10649
10876
  process.exitCode = code ?? 0;
10877
+ if ((code ?? 0) !== 0) {
10878
+ const obsFile = path40.join(paths.haiveDir, ".cache", "observations.jsonl");
10879
+ void mkdir16(path40.dirname(obsFile), { recursive: true }).then(() => writeFile25(obsFile, "", { flag: "a" })).then(() => writeFile25(
10880
+ obsFile,
10881
+ JSON.stringify({
10882
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
10883
+ session_id: sessionId,
10884
+ tool: "AgentRun",
10885
+ summary: `wrapped agent exited ${code}: ${[command, ...args].join(" ").slice(0, 180)}`,
10886
+ failure_hint: true
10887
+ }) + "\n",
10888
+ { flag: "a" }
10889
+ )).catch(() => {
10890
+ }).finally(() => resolve());
10891
+ return;
10892
+ }
10650
10893
  resolve();
10651
10894
  });
10652
10895
  });
@@ -10721,7 +10964,7 @@ async function checkBootstrapComplete(paths, config, productionCodeChanged) {
10721
10964
  }
10722
10965
  const memories = existsSync42(paths.memoriesDir) ? await loadMemoriesFromDir15(paths.memoriesDir) : [];
10723
10966
  const codeMap = await loadCodeMap7(paths);
10724
- const codeFiles = codeMap ? Object.keys(codeMap.files) : [];
10967
+ const codeFiles = codeMap ? Object.keys(codeMap.files).filter((file) => existsSync42(path40.join(paths.root, file))) : [];
10725
10968
  let existingModules = [];
10726
10969
  try {
10727
10970
  const entries = await readdir4(paths.modulesContextDir, { withFileTypes: true });
@@ -10785,7 +11028,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
10785
11028
  findings: [{ severity: "info", code: "enforcement-off", message: "Hivelore enforcement is disabled." }]
10786
11029
  });
10787
11030
  }
10788
- findings.push(...await inspectIntegrationVersions(root, "0.39.2"));
11031
+ findings.push(...await inspectIntegrationVersions(root, "0.43.2"));
10789
11032
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
10790
11033
  const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
10791
11034
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent Hivelore briefing marker exists." } : {
@@ -10819,7 +11062,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
10819
11062
  findings.push(...await verifyDecisionCoverage(paths, stage, sessionId));
10820
11063
  }
10821
11064
  if (stage === "pre-commit" || stage === "ci") {
10822
- findings.push(...await runPrecommitPolicy(paths, config.enforcement?.antiPatternGate ?? "anchored", stage));
11065
+ findings.push(...await runPrecommitPolicy(paths, config.enforcement?.antiPatternGate ?? "anchored", stage, config));
10823
11066
  } else if (stage === "local") {
10824
11067
  findings.push({
10825
11068
  severity: "info",
@@ -11026,16 +11269,16 @@ async function verifyDecisionCoverage(paths, stage, sessionId) {
11026
11269
  impact: Math.min(35, 10 + missing.length * 5)
11027
11270
  }];
11028
11271
  }
11029
- async function runPrecommitPolicy(paths, gate, stage) {
11272
+ async function runPrecommitPolicy(paths, gate, stage, config) {
11030
11273
  const snapshot = await getPolicyDiffSnapshot(paths.root, stage);
11031
11274
  const weakenings = detectSensorWeakening(snapshot.diff);
11032
11275
  const weakeningFindings = weakenings.length > 0 ? [{
11033
- severity: "warn",
11276
+ severity: config.enforcement?.sensorWeakeningGate === "block" ? "error" : "warn",
11034
11277
  code: "sensor-weakened",
11035
11278
  message: `This diff weakens the enforcement surface \u2014 ${weakenings.length} sensor change(s) need review: ` + weakenings.slice(0, 5).map((w) => `${w.memory_id} (${w.change}: ${w.detail})`).join(", ") + (weakenings.length > 5 ? ", \u2026" : "") + ".",
11036
11279
  fix: "If the demotion/removal is intentional, say so in the commit message; otherwise restore the sensor (`hivelore sensors list` shows the current state).",
11037
11280
  memory_ids: [...new Set(weakenings.map((w) => w.memory_id))].slice(0, 10),
11038
- impact: 8
11281
+ impact: config.enforcement?.sensorWeakeningGate === "block" ? 30 : 8
11039
11282
  }] : [];
11040
11283
  if (gate === "off") {
11041
11284
  return [
@@ -11122,6 +11365,17 @@ async function runPrecommitPolicy(paths, gate, stage) {
11122
11365
  ...weakeningFindings
11123
11366
  ];
11124
11367
  }
11368
+ async function stagedFileContent(root, rel) {
11369
+ try {
11370
+ return await runCommand2("git", ["show", `:${rel}`], root);
11371
+ } catch {
11372
+ try {
11373
+ return await readFile18(path40.resolve(root, rel), "utf8");
11374
+ } catch {
11375
+ return null;
11376
+ }
11377
+ }
11378
+ }
11125
11379
  async function runSensorGate(paths, diff, stage) {
11126
11380
  if (!diff || !existsSync42(paths.memoriesDir)) return [];
11127
11381
  try {
@@ -11174,7 +11428,78 @@ async function runSensorGate(paths, diff, stage) {
11174
11428
  });
11175
11429
  }
11176
11430
  }
11177
- const config = await loadConfig12(paths).catch(() => null);
11431
+ const astSensorMemories = scannable.filter((m) => m.frontmatter.sensor.kind === "ast");
11432
+ if (astSensorMemories.length > 0) {
11433
+ const addedByPath = addedLineNumbersFromDiff(diff);
11434
+ if (!await astEngineAvailable()) {
11435
+ findings.push({
11436
+ severity: "warn",
11437
+ code: "ast-sensor-unrunnable",
11438
+ message: `${astSensorMemories.length} AST sensor(s) could not run \u2014 the optional @ast-grep/napi engine is not installed. Their protection is OFF on this machine.`,
11439
+ fix: "Install the engine: `npm i -g @ast-grep/napi` (or add it to the repo devDependencies).",
11440
+ impact: 5
11441
+ });
11442
+ } else {
11443
+ for (const memory2 of astSensorMemories) {
11444
+ const sensor = memory2.frontmatter.sensor;
11445
+ if (!sensor.pattern && !sensor.rule) continue;
11446
+ const applicable = targets.filter((t) => sensorAppliesToPath2(sensor, memory2.frontmatter.anchor.paths, t.path));
11447
+ if (applicable.length === 0) continue;
11448
+ let fired = false;
11449
+ for (const target of applicable) {
11450
+ const added = addedByPath.get(target.path);
11451
+ if (!added || added.size === 0) continue;
11452
+ const content = await stagedFileContent(paths.root, target.path);
11453
+ if (content === null) continue;
11454
+ const scan = await runAstSensorOnContent({
11455
+ pattern: sensor.pattern,
11456
+ rule: sensor.rule,
11457
+ language: sensor.language,
11458
+ absent: sensor.absent,
11459
+ content,
11460
+ filePath: target.path,
11461
+ addedLines: added
11462
+ });
11463
+ if (scan.status !== "ok" || scan.matches.length === 0) continue;
11464
+ fired = true;
11465
+ if (seen.has(memory2.frontmatter.id)) break;
11466
+ seen.add(memory2.frontmatter.id);
11467
+ firedIds.add(memory2.frontmatter.id);
11468
+ const where = ` (${target.path}:${scan.matches[0].startLine})`;
11469
+ if (sensor.severity === "block") {
11470
+ findings.push({
11471
+ severity: "error",
11472
+ code: "sensor-block",
11473
+ message: `Block AST sensor fired \u2014 ${memory2.frontmatter.id}: ${sensor.message}${where}${incidentSuffix(sensor.incident)}
11474
+ matched: ${scan.matches[0].text}`,
11475
+ fix: "Remove the flagged construct, or run `hivelore sensors check` to inspect the match.",
11476
+ impact: 45,
11477
+ memory_ids: [memory2.frontmatter.id]
11478
+ });
11479
+ } else {
11480
+ findings.push({
11481
+ severity: "warn",
11482
+ code: "sensor-warn",
11483
+ message: `AST sensor flagged ${memory2.frontmatter.id}: ${sensor.message}${where}${incidentSuffix(sensor.incident)}`,
11484
+ fix: "Review the flagged construct; `hivelore sensors check` shows the matched code.",
11485
+ impact: 5,
11486
+ memory_ids: [memory2.frontmatter.id]
11487
+ });
11488
+ }
11489
+ break;
11490
+ }
11491
+ ledgerRows.push(evaluation({
11492
+ memory_id: memory2.frontmatter.id,
11493
+ kind: "ast",
11494
+ stage,
11495
+ head_sha: headSha,
11496
+ scope_hash: "",
11497
+ outcome: fired ? "fired" : "silent"
11498
+ }));
11499
+ }
11500
+ }
11501
+ }
11502
+ const config = await loadConfig12(paths).catch(() => ({}));
11178
11503
  if (config?.enforcement?.runCommandSensors === true) {
11179
11504
  const changedPaths = targets.map((t) => t.path).filter(Boolean);
11180
11505
  const specs = selectCommandSensors(scannable, changedPaths).filter((sp) => !seen.has(sp.memory_id));
@@ -11212,13 +11537,14 @@ async function runSensorGate(paths, diff, stage) {
11212
11537
  if (run.status === "passed") continue;
11213
11538
  seen.add(run.memory_id);
11214
11539
  if (run.status === "unrunnable") {
11540
+ const strictUnrunnable = config.enforcement?.commandSensorUnrunnable === "block";
11215
11541
  findings.push({
11216
- severity: "warn",
11542
+ severity: strictUnrunnable ? "error" : "warn",
11217
11543
  code: "command-sensor-unrunnable",
11218
11544
  message: `Command sensor ${run.memory_id} could not run (${run.unrunnable_reason}): \`${run.command}\`` + (run.output_tail ? `
11219
11545
  ${run.output_tail}` : ""),
11220
11546
  fix: "Fix the sensor's command (or its timeout_ms), or demote it: `hivelore sensors promote <id> --severity warn`.",
11221
- impact: 5
11547
+ impact: strictUnrunnable ? 35 : 5
11222
11548
  });
11223
11549
  continue;
11224
11550
  }
@@ -12303,6 +12629,7 @@ import {
12303
12629
  recordPreventionHits as recordPreventionHits2,
12304
12630
  resolveHaivePaths as resolveHaivePaths36,
12305
12631
  runSensors as runSensors2,
12632
+ addedLineNumbersFromDiff as addedLineNumbersFromDiff2,
12306
12633
  buildProposeCommand,
12307
12634
  scaffoldPostIncidentTest,
12308
12635
  selectCommandSensors as selectCommandSensors2,
@@ -12340,6 +12667,7 @@ function registerSensors(program2) {
12340
12667
  );
12341
12668
  if ("absent" in row && row.absent) console.log(` ${ui.dim("only when missing:")} ${row.absent}`);
12342
12669
  if (row.paths.length > 0) console.log(` ${ui.dim("paths:")} ${row.paths.join(", ")}`);
12670
+ if (row.red_proven) console.log(` ${ui.green("\u2713 RED-proven")}${row.incident ? ` \u2014 ${row.incident}` : ""}`);
12343
12671
  if (row.last_fired) console.log(` ${ui.dim("last fired:")} ${row.last_fired}`);
12344
12672
  if (brittle) console.log(` ${ui.yellow("\u26A0 brittle:")} ${brittle} \u2014 consider rewriting or retiring this sensor`);
12345
12673
  }
@@ -12353,6 +12681,57 @@ function registerSensors(program2) {
12353
12681
  const diff = opts.diffFile ? await readFile20(path42.resolve(root, opts.diffFile), "utf8") : await stagedDiff(root);
12354
12682
  const targets = scannableSensorTargets(diff);
12355
12683
  const hits = runSensors2(memories, targets);
12684
+ const astMemories = (await runnableSensorMemories(paths, false)).filter(
12685
+ (m) => m.frontmatter.sensor?.kind === "ast" && m.frontmatter.sensor.pattern
12686
+ );
12687
+ const astHits = [];
12688
+ let astUnrunnable = 0;
12689
+ if (astMemories.length > 0) {
12690
+ if (!await astEngineAvailable()) {
12691
+ astUnrunnable = astMemories.length;
12692
+ } else {
12693
+ const addedByPath = addedLineNumbersFromDiff2(diff);
12694
+ for (const memory2 of astMemories) {
12695
+ const sensor = memory2.frontmatter.sensor;
12696
+ for (const target of targets) {
12697
+ if (!sensorAppliesToPath3(sensor, memory2.frontmatter.anchor.paths, target.path)) continue;
12698
+ const added = addedByPath.get(target.path);
12699
+ if (!added || added.size === 0) continue;
12700
+ let content = null;
12701
+ try {
12702
+ content = (await exec5("git", ["show", `:${target.path}`], { cwd: root })).stdout;
12703
+ } catch {
12704
+ try {
12705
+ content = await readFile20(path42.resolve(root, target.path), "utf8");
12706
+ } catch {
12707
+ content = null;
12708
+ }
12709
+ }
12710
+ if (content === null) continue;
12711
+ const scan = await runAstSensorOnContent({
12712
+ pattern: sensor.pattern,
12713
+ rule: sensor.rule,
12714
+ language: sensor.language,
12715
+ absent: sensor.absent,
12716
+ content,
12717
+ filePath: target.path,
12718
+ addedLines: added
12719
+ });
12720
+ if (scan.status === "ok" && scan.matches.length > 0) {
12721
+ astHits.push({
12722
+ memory_id: memory2.frontmatter.id,
12723
+ sensor,
12724
+ file: target.path,
12725
+ matched_line: `${target.path}:${scan.matches[0].startLine} ${scan.matches[0].text}`,
12726
+ message: sensor.message,
12727
+ severity: sensor.severity
12728
+ });
12729
+ break;
12730
+ }
12731
+ }
12732
+ }
12733
+ }
12734
+ }
12356
12735
  const config = await loadConfig13(paths);
12357
12736
  const runCommands = opts.commands || config.enforcement?.runCommandSensors === true;
12358
12737
  const changedPaths = targets.map((t) => t.path).filter(Boolean);
@@ -12414,9 +12793,10 @@ function registerSensors(program2) {
12414
12793
  for (const spec of commandSpecs) commandSkipped.push(spec.memory_id);
12415
12794
  }
12416
12795
  await appendSensorEvaluations2(paths, ledgerRows);
12417
- const firedIds = [...new Set([...hits, ...commandHits].map((hit) => hit.memory_id))];
12796
+ const firedIds = [...new Set([...hits, ...astHits, ...commandHits].map((hit) => hit.memory_id))];
12418
12797
  const preventionDetails = Object.fromEntries([
12419
12798
  ...hits.map((hit) => [hit.memory_id, { kind: "regex", stage: "manual" }]),
12799
+ ...astHits.map((hit) => [hit.memory_id, { kind: "ast", stage: "manual" }]),
12420
12800
  ...commandHits.map((hit) => [hit.memory_id, {
12421
12801
  kind: commandSpecs.find((spec) => spec.memory_id === hit.memory_id)?.kind ?? "shell",
12422
12802
  stage: "manual",
@@ -12425,8 +12805,9 @@ function registerSensors(program2) {
12425
12805
  ]);
12426
12806
  await recordPreventionHits2(paths, firedIds, "sensor", /* @__PURE__ */ new Date(), preventionDetails);
12427
12807
  const output = {
12428
- scanned: memories.length,
12429
- hits: hits.map((hit) => ({
12808
+ scanned: memories.length + astMemories.length,
12809
+ ast_unrunnable: astUnrunnable,
12810
+ hits: [...hits, ...astHits].map((hit) => ({
12430
12811
  memory_id: hit.memory_id,
12431
12812
  file: hit.file,
12432
12813
  severity: hit.severity,
@@ -12440,9 +12821,12 @@ function registerSensors(program2) {
12440
12821
  if (opts.json) {
12441
12822
  console.log(JSON.stringify(output, null, 2));
12442
12823
  } else {
12443
- const total = hits.length + commandHits.length;
12444
- console.log(ui.bold(`Hivelore sensors check \u2014 ${total} hit(s), ${memories.length} regex + ${commandSpecs.length} command sensor(s)`));
12445
- for (const hit of hits) {
12824
+ const total = hits.length + astHits.length + commandHits.length;
12825
+ console.log(ui.bold(`Hivelore sensors check \u2014 ${total} hit(s), ${memories.length} regex + ${astMemories.length} ast + ${commandSpecs.length} command sensor(s)`));
12826
+ if (astUnrunnable > 0) {
12827
+ console.log(ui.yellow(` \u26A0 ${astUnrunnable} AST sensor(s) unrunnable \u2014 install @ast-grep/napi to activate them (never blocks).`));
12828
+ }
12829
+ for (const hit of [...hits, ...astHits]) {
12446
12830
  const marker = hit.severity === "block" ? ui.red("\u2717") : ui.yellow("\u26A0");
12447
12831
  console.log(` ${marker} ${hit.memory_id} ${ui.dim(`(${hit.severity})`)}`);
12448
12832
  if (hit.file) console.log(` ${ui.dim("file:")} ${hit.file}`);
@@ -12466,7 +12850,7 @@ function registerSensors(program2) {
12466
12850
  console.log(ui.dim(` ${commandSkipped.length} command sensor(s) not run \u2014 pass --commands or set enforcement.runCommandSensors.`));
12467
12851
  }
12468
12852
  }
12469
- if ([...hits, ...commandHits].some((hit) => hit.severity === "block")) process.exitCode = 1;
12853
+ if ([...hits, ...astHits, ...commandHits].some((hit) => hit.severity === "block")) process.exitCode = 1;
12470
12854
  });
12471
12855
  sensors.command("promote").description("Promote or demote an existing memory sensor severity").argument("<memory-id>", "memory id carrying the sensor").option("--severity <severity>", "block | warn", "block").option("--yes", "confirm promotion to block severity", false).option("--force", "promote even a brittle sensor (line-number/literal patterns) to block", false).option("-d, --dir <dir>", "project root").action(async (id, opts) => {
12472
12856
  const severity = opts.severity ?? "block";
@@ -12495,6 +12879,11 @@ function registerSensors(program2) {
12495
12879
  process.exitCode = 1;
12496
12880
  return;
12497
12881
  }
12882
+ if (severity === "block" && (sensor.kind === "shell" || sensor.kind === "test") && sensor.red_proven !== true) {
12883
+ ui.error("Refusing to promote an unproven command/test oracle to block. Re-propose it with --red-ref <pre-fix-commit>.");
12884
+ process.exitCode = 1;
12885
+ return;
12886
+ }
12498
12887
  const brittle = sensor.kind === "regex" && sensor.pattern ? sensorPatternBrittleness2(sensor.pattern) : null;
12499
12888
  if (severity === "block" && brittle && !opts.force) {
12500
12889
  ui.error(`Refusing to block on a brittle sensor (${brittle}). Rewrite the pattern, or pass --force.`);
@@ -12541,34 +12930,54 @@ function registerSensors(program2) {
12541
12930
  });
12542
12931
  sensors.command("propose").description(
12543
12932
  "Propose a discriminating sensor for a memory \u2014 you write the pattern, Hivelore validates it before\n trusting it to block. Mirrors the MCP `propose_sensor` tool (the agent-authored path).\n\n A `block` proposal is accepted ONLY if it is not brittle, stays SILENT on the current code,\n and FIRES on the bad example. Rejected proposals are not written \u2014 fix and re-run.\n\n Example:\n hivelore sensors propose <memory-id> \\\n --pattern 'stripe\\.paymentIntents\\.create' --absent 'idempotencyKey' \\\n --bad-example 'stripe.paymentIntents.create({ amount })'"
12544
- ).argument("<memory-id>", "memory id to attach the sensor to").option("--kind <kind>", "regex (default) | shell | test \u2014 command kinds route the team's own oracle to this lesson", "regex").option("--pattern <regex>", "kind=regex: regex matching the FAULTY usage").option("--command <cmd>", "kind=shell|test: command the gate runs when the diff touches the sensor's paths").option("--timeout <ms>", "kind=shell|test: max runtime in ms (default 120000)").option("--absent <regex>", "regex for the CORRECT-usage marker (makes it discriminate)").option("--bad-example <code>", "a snippet that SHOULD match (else examples are read from the lesson)").option("--severity <severity>", "block | warn", "block").option("--message <text>", "fix message shown when it fires").option("--incident <ref>", "provenance: the incident this sensor guards (e.g. 'prod #442') \u2014 shown when it fires and in the receipt").option("--flags <flags>", "regex flags (e.g. i)").option("--paths <csv>", "override scope paths (defaults to the memory anchors)").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
12545
- if (opts.kind === "shell" || opts.kind === "test") {
12546
- if (!opts.command?.trim()) {
12933
+ ).argument("<memory-id>", "memory id to attach the sensor to").option("--kind <kind>", "regex (default) | ast (structural \u2014 comments/strings can't false-positive) | shell | test (route the team's own oracle)", "regex").option("--pattern <regex>", "kind=regex: regex matching the FAULTY usage").option("--rule <json>", "kind=ast: full ast-grep Rule JSON (kind/inside/has/not/all/any)").option("--language <name>", "kind=ast: explicit language for non-standard extensions").option("--command <cmd>", "kind=shell|test: command the gate runs when the diff touches the sensor's paths").option("--timeout <ms>", "kind=shell|test: max runtime in ms (default 120000)").option("--absent <regex>", "regex for the CORRECT-usage marker (makes it discriminate)").option("--bad-example <code>", "a snippet that SHOULD match (else examples are read from the lesson)").option("--severity <severity>", "block | warn", "block").option("--message <text>", "fix message shown when it fires").option("--incident <ref>", "provenance: the incident this sensor guards (e.g. 'prod #442') \u2014 shown when it fires and in the receipt").option("--red-ref <ref>", "kind=shell|test: pre-fix commit/ref \u2014 validation replays it in a scratch worktree and requires the oracle to FAIL there (records red_proven)").option("--flags <flags>", "regex flags (e.g. i)").option("--paths <csv>", "override scope paths (defaults to the memory anchors)").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
12934
+ if (opts.kind === "shell" || opts.kind === "test" || opts.kind === "ast") {
12935
+ if ((opts.kind === "shell" || opts.kind === "test") && !opts.command?.trim()) {
12547
12936
  ui.error("--kind shell|test requires --command.");
12548
12937
  process.exitCode = 1;
12549
12938
  return;
12550
12939
  }
12940
+ if (opts.kind === "ast" && !opts.pattern?.trim() && !opts.rule?.trim()) {
12941
+ ui.error("--kind ast requires --pattern or --rule <json>.");
12942
+ process.exitCode = 1;
12943
+ return;
12944
+ }
12945
+ let astRule;
12946
+ if (opts.rule?.trim()) {
12947
+ try {
12948
+ const parsed = JSON.parse(opts.rule);
12949
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("rule must be a JSON object");
12950
+ astRule = parsed;
12951
+ } catch (err) {
12952
+ ui.error(`--rule must be valid ast-grep Rule JSON: ${err instanceof Error ? err.message : String(err)}`);
12953
+ process.exitCode = 1;
12954
+ return;
12955
+ }
12956
+ }
12551
12957
  const root2 = findProjectRoot39(opts.dir);
12552
- const { proposeSensor } = await import("./server-G6N6NJ64.js");
12958
+ const { proposeSensor } = await import("./server-WW6JHBYY.js");
12553
12959
  const out = await proposeSensor(
12554
12960
  {
12555
12961
  memory_id: id,
12556
12962
  kind: opts.kind,
12557
- pattern: void 0,
12558
- command: opts.command.trim(),
12963
+ pattern: opts.kind === "ast" ? opts.pattern?.trim() : void 0,
12964
+ rule: astRule,
12965
+ language: opts.kind === "ast" ? opts.language : void 0,
12966
+ command: opts.command?.trim(),
12559
12967
  timeout_ms: opts.timeout ? Math.max(1, Number(opts.timeout)) : void 0,
12560
- absent: void 0,
12561
- bad_example: void 0,
12968
+ absent: opts.kind === "ast" ? opts.absent : void 0,
12969
+ bad_example: opts.kind === "ast" ? opts.badExample : void 0,
12562
12970
  severity: opts.severity === "warn" ? "warn" : "block",
12563
12971
  message: opts.message,
12564
12972
  incident: opts.incident,
12973
+ red_ref: opts.redRef,
12565
12974
  flags: void 0,
12566
12975
  paths: opts.paths ? opts.paths.split(",").map((p) => p.trim()).filter(Boolean) : []
12567
12976
  },
12568
12977
  { paths: resolveHaivePaths36(root2) }
12569
12978
  );
12570
12979
  if (out.accepted) {
12571
- ui.success(`Command sensor accepted (${out.severity}) on ${id}`);
12980
+ ui.success(`${opts.kind === "ast" ? "AST" : "Command"} sensor accepted (${out.severity}) on ${id}`);
12572
12981
  ui.info(` ${out.guidance}`);
12573
12982
  } else {
12574
12983
  ui.error(`Rejected (${out.reason}).`);
@@ -12748,10 +13157,14 @@ async function sensorRows(paths) {
12748
13157
  kind: sensor.kind,
12749
13158
  severity: sensor.severity,
12750
13159
  pattern: sensor.pattern,
13160
+ rule: sensor.rule,
13161
+ language: sensor.language,
12751
13162
  absent: sensor.absent,
12752
13163
  command: sensor.command,
12753
13164
  paths: sensor.paths.length > 0 ? sensor.paths : memory2.frontmatter.anchor.paths,
12754
13165
  message: sensor.message,
13166
+ incident: sensor.incident,
13167
+ red_proven: sensor.red_proven === true,
12755
13168
  autogen: sensor.autogen,
12756
13169
  last_fired: sensor.last_fired,
12757
13170
  ...brittle ? { brittle } : {}
@@ -12806,22 +13219,25 @@ import { mkdir as mkdir18, readFile as readFile21, writeFile as writeFile28 } fr
12806
13219
  import path43 from "path";
12807
13220
  import "commander";
12808
13221
  import {
13222
+ appendProposedRetrievalCases as appendProposedRetrievalCases2,
12809
13223
  draftsFromFindings,
12810
13224
  filterNewDrafts,
12811
13225
  findProjectRoot as findProjectRoot40,
12812
13226
  loadMemoriesFromDir as loadMemoriesFromDir17,
12813
13227
  memoryFilePath as memoryFilePath7,
13228
+ extractReviewLearnings,
12814
13229
  parseFindings,
12815
13230
  resolveHaivePaths as resolveHaivePaths37,
13231
+ reviewLearningsToDrafts,
12816
13232
  serializeMemory as serializeMemory16
12817
13233
  } from "@hivelore/core";
12818
13234
  var SEVERITIES = ["info", "minor", "major", "critical", "blocker"];
12819
13235
  function registerIngest(program2) {
12820
13236
  program2.command("ingest").description(
12821
- "Ingest scanner findings (SonarQube / SARIF) as proposed, anchored memories with sensors.\n\n Closes the review\u2194memory loop: a real defect a scanner found becomes a `gotcha`/`convention`\n memory anchored to the file, pre-filled with a conservative `warn` sensor, so the next agent\n is steered away from it. Drafts are status=proposed; a human validates/promotes them.\n\n `sonar-api` fetches issues live over plain HTTPS from any SonarQube/SonarCloud instance \u2014\n no MCP or special setup required, just a URL + token you provide (or SONAR_HOST_URL /\n SONAR_TOKEN env). If you don't use it, file-based ingest works exactly the same.\n\n Example:\n hivelore ingest --from eslint eslint-report.json --min-severity major\n hivelore ingest --from npm-audit audit.json --scope team\n hivelore ingest --from sarif report.sarif --dry-run\n hivelore ingest --from sonar sonar-issues.json --scope team --min-severity major\n hivelore ingest --from sonar-api --sonar-component my_project --min-severity major\n\n Generate the input reports:\n eslint -f json -o eslint-report.json . # --from eslint\n npm audit --json > audit.json # --from npm-audit\n"
12822
- ).argument("[file]", "path to the findings report JSON (required for --from sarif|sonar|eslint|npm-audit)").requiredOption("--from <format>", "report format: sarif | sonar | sonar-api | eslint | npm-audit").option("--dry-run", "show what would be created without writing", false).option("--scope <scope>", "memory scope: personal | team | module", "team").option("--module <name>", "module name (required when scope=module)").option("--type <type>", "memory type: gotcha | convention", "gotcha").option("--min-severity <severity>", "ignore findings below this severity (info|minor|major|critical|blocker)").option("--include-stylistic", "also ingest auto-fixable stylistic rules (semi/quotes/prefer-const\u2026); off by default as low-value noise", false).option("--limit <n>", "cap the number of memories created").option("--author <author>", "author email or handle").option("--json", "emit JSON", false).option("--sonar-url <url>", "SonarQube base URL for --from sonar-api (or env SONAR_HOST_URL)").option("--sonar-token <token>", "SonarQube token for --from sonar-api (or env SONAR_TOKEN)").option("--sonar-component <key>", "SonarQube project/component key for --from sonar-api").option("--sonar-branch <branch>", "optional SonarQube branch for --from sonar-api").option("-d, --dir <dir>", "project root").action(async (file, opts) => {
13237
+ "Ingest scanner findings (SonarQube / SARIF) as proposed, anchored memories with sensors.\n\n Closes the review\u2194memory loop: a real defect a scanner found becomes a `gotcha`/`convention`\n memory anchored to the file, pre-filled with a conservative `warn` sensor, so the next agent\n is steered away from it. Drafts are status=proposed; a human validates/promotes them.\n\n `sonar-api` fetches issues live over plain HTTPS from any SonarQube/SonarCloud instance \u2014\n no MCP or special setup required, just a URL + token you provide (or SONAR_HOST_URL /\n SONAR_TOKEN env). If you don't use it, file-based ingest works exactly the same.\n\n Example:\n hivelore ingest --from eslint eslint-report.json --min-severity major\n hivelore ingest --from npm-audit audit.json --scope team\n hivelore ingest --from sarif report.sarif --dry-run\n hivelore ingest --from sonar sonar-issues.json --scope team --min-severity major\n hivelore ingest --from sonar-api --sonar-component my_project --min-severity major\n\n Generate the input reports:\n eslint -f json -o eslint-report.json . # --from eslint\n npm audit --json > audit.json # --from npm-audit\n\n Review learnings (the PR loop \u2014 a reviewer reply becomes a proposed memory):\n hivelore ingest --from github-pr 123 # fetches review threads via gh\n hivelore ingest --from github-pr comments.json --dry-run\n Kept: human replies that read as instructions (never/always/must/prefer\u2026) or carry\n the explicit marker (reply `/hivelore remember <rule>` on the thread).\n"
13238
+ ).argument("[file]", "findings report JSON \u2014 or, for --from github-pr, a PR number/URL (fetched via gh) or a recorded comments JSON").requiredOption("--from <format>", "report format: sarif | sonar | sonar-api | eslint | npm-audit | github-pr").option("--dry-run", "show what would be created without writing", false).option("--scope <scope>", "memory scope: personal | team | module", "team").option("--module <name>", "module name (required when scope=module)").option("--type <type>", "memory type: gotcha | convention", "gotcha").option("--min-severity <severity>", "ignore findings below this severity (info|minor|major|critical|blocker)").option("--include-stylistic", "also ingest auto-fixable stylistic rules (semi/quotes/prefer-const\u2026); off by default as low-value noise", false).option("--limit <n>", "cap the number of memories created").option("--author <author>", "author email or handle").option("--json", "emit JSON", false).option("--sonar-url <url>", "SonarQube base URL for --from sonar-api (or env SONAR_HOST_URL)").option("--sonar-token <token>", "SonarQube token for --from sonar-api (or env SONAR_TOKEN)").option("--sonar-component <key>", "SonarQube project/component key for --from sonar-api").option("--sonar-branch <branch>", "optional SonarQube branch for --from sonar-api").option("-d, --dir <dir>", "project root").action(async (file, opts) => {
12823
13239
  const format = opts.from;
12824
- const VALID_FORMATS = ["sarif", "sonar", "sonar-api", "eslint", "npm-audit"];
13240
+ const VALID_FORMATS = ["sarif", "sonar", "sonar-api", "eslint", "npm-audit", "github-pr"];
12825
13241
  if (!format || !VALID_FORMATS.includes(format)) {
12826
13242
  ui.error(`--from must be one of: ${VALID_FORMATS.join(", ")}`);
12827
13243
  process.exitCode = 1;
@@ -12846,7 +13262,25 @@ function registerIngest(program2) {
12846
13262
  }
12847
13263
  const parseFormat = format === "sonar-api" ? "sonar" : format;
12848
13264
  let raw;
12849
- if (format === "sonar-api") {
13265
+ if (format === "github-pr") {
13266
+ if (!file) {
13267
+ ui.error("--from github-pr needs a PR number/URL or a recorded comments JSON file.");
13268
+ process.exitCode = 1;
13269
+ return;
13270
+ }
13271
+ const asPath = path43.resolve(root, file);
13272
+ if (existsSync45(asPath)) {
13273
+ raw = await readFile21(asPath, "utf8");
13274
+ } else {
13275
+ const fetched = await fetchPrReviewComments(root, file);
13276
+ if (!fetched.ok) {
13277
+ ui.error(fetched.error);
13278
+ process.exitCode = 1;
13279
+ return;
13280
+ }
13281
+ raw = fetched.json;
13282
+ }
13283
+ } else if (format === "sonar-api") {
12850
13284
  const fetched = await fetchSonarIssues(opts);
12851
13285
  if (!fetched.ok) {
12852
13286
  ui.error(fetched.error);
@@ -12876,7 +13310,23 @@ function registerIngest(program2) {
12876
13310
  }
12877
13311
  let drafts;
12878
13312
  let findingsCount = 0;
12879
- try {
13313
+ if (format === "github-pr") {
13314
+ try {
13315
+ const payload = JSON.parse(raw);
13316
+ findingsCount = Array.isArray(payload) ? payload.length : 0;
13317
+ const learnings = extractReviewLearnings(payload);
13318
+ drafts = reviewLearningsToDrafts(learnings, {
13319
+ scope: opts.scope ?? "team",
13320
+ module: opts.module,
13321
+ author: opts.author,
13322
+ ...opts.limit ? { limit: Math.max(0, Number.parseInt(opts.limit, 10) || 0) } : {}
13323
+ });
13324
+ } catch (err) {
13325
+ ui.error(`Failed to parse the PR comments payload: ${err instanceof Error ? err.message : String(err)}`);
13326
+ process.exitCode = 1;
13327
+ return;
13328
+ }
13329
+ } else try {
12880
13330
  const findings = parseFindings(parseFormat, raw, { cwd: root });
12881
13331
  findingsCount = findings.length;
12882
13332
  drafts = draftsFromFindings(findings, {
@@ -12903,6 +13353,7 @@ function registerIngest(program2) {
12903
13353
  const created2 = [];
12904
13354
  if (!opts.dryRun) {
12905
13355
  for (const draft of fresh) created2.push(await writeDraft(paths, draft));
13356
+ if (format === "github-pr") await appendReviewGoldenCases(root, fresh);
12906
13357
  }
12907
13358
  console.log(
12908
13359
  JSON.stringify(
@@ -12955,6 +13406,7 @@ function registerIngest(program2) {
12955
13406
  await writeDraft(paths, draft);
12956
13407
  created++;
12957
13408
  }
13409
+ if (format === "github-pr") await appendReviewGoldenCases(root, fresh);
12958
13410
  ui.success(`Created ${created} proposed memory(ies) under ${path43.relative(root, paths.memoriesDir)}/`);
12959
13411
  ui.info("Review with `hivelore memory pending`; promote sensors with `hivelore sensors promote <id> --yes`.");
12960
13412
  });
@@ -12965,6 +13417,19 @@ async function writeDraft(paths, draft) {
12965
13417
  await writeFile28(file, serializeMemory16({ frontmatter: draft.frontmatter, body: draft.body }), "utf8");
12966
13418
  return file;
12967
13419
  }
13420
+ async function appendReviewGoldenCases(root, drafts) {
13421
+ if (drafts.length === 0) return;
13422
+ const specFile = path43.join(root, ".ai", "eval", "spec.json");
13423
+ const current = existsSync45(specFile) ? await readFile21(specFile, "utf8") : null;
13424
+ const cases = drafts.map((draft) => ({
13425
+ name: `review-learning:${draft.topic}`,
13426
+ task: draft.finding.message,
13427
+ ...draft.finding.path ? { files: [draft.finding.path] } : {},
13428
+ expect_ids: [draft.frontmatter.id]
13429
+ }));
13430
+ await mkdir18(path43.dirname(specFile), { recursive: true });
13431
+ await writeFile28(specFile, appendProposedRetrievalCases2(current, cases), "utf8");
13432
+ }
12968
13433
  async function fetchSonarIssues(opts) {
12969
13434
  const baseUrl = (opts.sonarUrl ?? process.env.SONAR_HOST_URL ?? "").trim().replace(/\/+$/, "");
12970
13435
  const token = (opts.sonarToken ?? process.env.SONAR_TOKEN ?? "").trim();
@@ -13001,6 +13466,42 @@ async function fetchSonarIssues(opts) {
13001
13466
  };
13002
13467
  }
13003
13468
  }
13469
+ async function fetchPrReviewComments(root, ref) {
13470
+ const numberMatch = ref.match(/^(\d+)$/) ?? ref.match(/\/pull\/(\d+)/);
13471
+ if (!numberMatch) {
13472
+ return { ok: false, error: `"${ref}" is neither a comments JSON file, a PR number, nor a PR URL.` };
13473
+ }
13474
+ const prNumber = numberMatch[1];
13475
+ const { execFile: execFile9 } = await import("child_process");
13476
+ const { promisify: promisify9 } = await import("util");
13477
+ const run = promisify9(execFile9);
13478
+ try {
13479
+ const fetchPages = async (endpoint) => {
13480
+ const { stdout } = await run(
13481
+ "gh",
13482
+ ["api", endpoint, "--paginate", "--slurp"],
13483
+ { cwd: root, maxBuffer: 16 * 1024 * 1024 }
13484
+ );
13485
+ const pages = JSON.parse(stdout);
13486
+ return Array.isArray(pages) ? pages.flatMap((page) => Array.isArray(page) ? page : []) : [];
13487
+ };
13488
+ const [reviewComments, issueComments] = await Promise.all([
13489
+ fetchPages(`repos/{owner}/{repo}/pulls/${prNumber}/comments`),
13490
+ fetchPages(`repos/{owner}/{repo}/issues/${prNumber}/comments`)
13491
+ ]);
13492
+ const normalizedIssueComments = issueComments.map((row) => ({
13493
+ ...row,
13494
+ pull_request_url: `https://api.github.com/repos/{owner}/{repo}/pulls/${prNumber}`
13495
+ }));
13496
+ return { ok: true, json: JSON.stringify([...reviewComments, ...normalizedIssueComments]) };
13497
+ } catch (err) {
13498
+ const e = err;
13499
+ return {
13500
+ ok: false,
13501
+ error: `Could not fetch PR #${prNumber} comments via gh: ${(e.stderr || e.message || String(err)).slice(0, 200)}. Install/authenticate the gh CLI, or pass a recorded comments JSON file instead.`
13502
+ };
13503
+ }
13504
+ }
13004
13505
 
13005
13506
  // src/commands/dashboard.ts
13006
13507
  import { existsSync as existsSync46 } from "fs";
@@ -13460,7 +13961,7 @@ function registerBridges(program2) {
13460
13961
 
13461
13962
  // src/index.ts
13462
13963
  var program = new Command48();
13463
- program.name("hivelore").description("Hivelore - the deterministic policy gate for agent-written code (rules live as repo-native team memory)").version("0.39.2").option("--advanced", "show maintenance and experimental commands in help").showSuggestionAfterError(true);
13964
+ program.name("hivelore").description("Hivelore - the deterministic policy gate for agent-written code (rules live as repo-native team memory)").version("0.43.2").option("--advanced", "show maintenance and experimental commands in help").showSuggestionAfterError(true);
13464
13965
  registerInit(program);
13465
13966
  registerResolveProject(program);
13466
13967
  registerEnforce(program);