@hivelore/cli 0.39.2 → 0.42.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
@@ -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-EJ7A4IKD.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.42.1"}`;
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) {
@@ -7798,7 +7887,9 @@ import {
7798
7887
  loadEvalHistory,
7799
7888
  loadPreventionEvents as loadPreventionEvents3,
7800
7889
  loadUsageIndex as loadUsageIndex13,
7890
+ approveProposedCases,
7801
7891
  overallScore,
7892
+ runTierContract,
7802
7893
  resolveHaivePaths as resolveHaivePaths29,
7803
7894
  scoreRetrievalCase,
7804
7895
  scoreSensorCase,
@@ -7807,7 +7898,7 @@ import {
7807
7898
  function registerEval(program2) {
7808
7899
  program2.command("eval").description(
7809
7900
  "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) => {
7901
+ ).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
7902
  const root = findProjectRoot31(opts.dir);
7812
7903
  const paths = resolveHaivePaths29(root);
7813
7904
  if (!existsSync35(paths.memoriesDir)) {
@@ -7815,6 +7906,21 @@ function registerEval(program2) {
7815
7906
  process.exitCode = 1;
7816
7907
  return;
7817
7908
  }
7909
+ if (opts.approveCases) {
7910
+ const specFile = path32.join(root, ".ai", "eval", "spec.json");
7911
+ if (!existsSync35(specFile)) {
7912
+ ui.info("No .ai/eval/spec.json \u2014 nothing to approve.");
7913
+ return;
7914
+ }
7915
+ const { raw, approved } = approveProposedCases(await readFile15(specFile, "utf8"));
7916
+ if (approved === 0) {
7917
+ ui.info("No proposed cases waiting for approval.");
7918
+ return;
7919
+ }
7920
+ await writeFile20(specFile, raw, "utf8");
7921
+ ui.success(`Approved ${approved} proposed golden case(s) into the scored retrieval set.`);
7922
+ return;
7923
+ }
7818
7924
  if (opts.trend) {
7819
7925
  const trend = computeEvalTrend(await loadEvalHistory(paths));
7820
7926
  if (opts.json) {
@@ -7835,6 +7941,17 @@ function registerEval(program2) {
7835
7941
  const ctx = { paths };
7836
7942
  const resolvedSpec = await resolveSpec(opts, root, paths.memoriesDir);
7837
7943
  const spec = resolvedSpec.spec;
7944
+ const tierChecks = runTierContract();
7945
+ const tierFailures = tierChecks.filter((c) => !c.pass);
7946
+ let proposedGoldenCount = 0;
7947
+ try {
7948
+ const specFile = path32.join(root, ".ai", "eval", "spec.json");
7949
+ if (!opts.spec && existsSync35(specFile)) {
7950
+ const parsed = JSON.parse(await readFile15(specFile, "utf8"));
7951
+ proposedGoldenCount = parsed.proposed_retrieval?.length ?? 0;
7952
+ }
7953
+ } catch {
7954
+ }
7838
7955
  if ((spec.retrieval?.length ?? 0) === 0 && (spec.sensors?.length ?? 0) === 0) {
7839
7956
  ui.warn("No eval cases (no anchored memories and no --spec). Nothing to score.");
7840
7957
  return;
@@ -7925,10 +8042,13 @@ function registerEval(program2) {
7925
8042
  ...authoredScore !== null ? { authored_score: authoredScore } : {}
7926
8043
  },
7927
8044
  report,
8045
+ tier_contract: { checks: tierChecks, failures: tierFailures.length },
8046
+ proposed_golden_cases: proposedGoldenCount,
7928
8047
  gate_precision: gatePrecision,
7929
8048
  ...delta ? { delta } : {},
7930
8049
  ...gateDelta ? { gate_delta: gateDelta } : {}
7931
8050
  }, null, 2));
8051
+ if (tierFailures.length > 0) process.exitCode = 1;
7932
8052
  applyExitGates(opts, report, delta, gatePrecision, gateDelta);
7933
8053
  return;
7934
8054
  }
@@ -7943,6 +8063,17 @@ function registerEval(program2) {
7943
8063
  if (gateDelta) {
7944
8064
  console.log(renderGateDelta(gateDelta));
7945
8065
  }
8066
+ if (tierFailures.length > 0) {
8067
+ ui.error(`Ranking tier contract BROKEN \u2014 ${tierFailures.length} check(s) violate the designed tiers:`);
8068
+ for (const c of tierFailures) ui.error(` \u2717 ${c.name} (expected ${c.expected}, got ${c.actual})`);
8069
+ } else {
8070
+ ui.info(`Ranking tier contract: ${tierChecks.length}/${tierChecks.length} checks hold.`);
8071
+ }
8072
+ if (proposedGoldenCount > 0) {
8073
+ ui.warn(
8074
+ `${proposedGoldenCount} proposed golden case(s) (gate-miss labeled) await approval \u2014 review .ai/eval/spec.json, then \`hivelore eval --approve-cases\` to score them.`
8075
+ );
8076
+ }
7946
8077
  const md = renderMarkdown2(root, k, resolvedSpec, report, gatePrecision, authoredScore);
7947
8078
  if (opts.out) {
7948
8079
  const outFile = path32.isAbsolute(opts.out) ? opts.out : path32.join(root, opts.out);
@@ -7951,6 +8082,7 @@ function registerEval(program2) {
7951
8082
  } else {
7952
8083
  console.log(md);
7953
8084
  }
8085
+ if (tierFailures.length > 0) process.exitCode = 1;
7954
8086
  applyExitGates(opts, report, delta, gatePrecision, gateDelta);
7955
8087
  });
7956
8088
  }
@@ -8894,6 +9026,19 @@ function registerDoctor(program2) {
8894
9026
  fix: "hivelore sensors list # then `hivelore sensors promote <id>` for a trusted, non-brittle sensor \u2014 or retire the noise"
8895
9027
  });
8896
9028
  }
9029
+ const astSensorCount = sensorMemories.filter((m) => m.memory.frontmatter.sensor?.kind === "ast").length;
9030
+ if (astSensorCount > 0) {
9031
+ const { astEngineAvailable: astEngineAvailable2 } = await import("./server-PZWIQUU7.js");
9032
+ if (!await astEngineAvailable2()) {
9033
+ findings.push({
9034
+ severity: "warn",
9035
+ code: "ast-engine-missing",
9036
+ section: "Protection",
9037
+ 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).`,
9038
+ fix: "npm i -g @ast-grep/napi # or add it to the repo devDependencies"
9039
+ });
9040
+ }
9041
+ }
8897
9042
  const firesOnCurrent = [];
8898
9043
  for (const m of sensorMemories) {
8899
9044
  const s = m.memory.frontmatter.sensor;
@@ -9053,7 +9198,7 @@ function registerDoctor(program2) {
9053
9198
  fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `hivelore init` without --manual)."
9054
9199
  });
9055
9200
  }
9056
- findings.push(...await collectInstallFindings(root, "0.39.2"));
9201
+ findings.push(...await collectInstallFindings(root, "0.42.1"));
9057
9202
  findings.push(...await collectToolchainFindings(root));
9058
9203
  try {
9059
9204
  const legacyRaw = execSync("haive-mcp --version", {
@@ -9061,7 +9206,7 @@ function registerDoctor(program2) {
9061
9206
  timeout: 3e3,
9062
9207
  stdio: ["ignore", "pipe", "ignore"]
9063
9208
  }).trim();
9064
- const cliVersion = "0.39.2";
9209
+ const cliVersion = "0.42.1";
9065
9210
  if (legacyRaw && legacyRaw !== cliVersion) {
9066
9211
  findings.push({
9067
9212
  severity: "warn",
@@ -9806,6 +9951,7 @@ import {
9806
9951
  assessSensorHealth as assessSensorHealth3,
9807
9952
  sensorPromotedAtMap as sensorPromotedAtMap3,
9808
9953
  assessBootstrapState,
9954
+ addedLineNumbersFromDiff,
9809
9955
  detectSensorWeakening,
9810
9956
  isSensorScannablePath,
9811
9957
  findProjectRoot as findProjectRoot37,
@@ -9959,6 +10105,7 @@ function defaultClaudeSettingsPath(scope, projectRoot) {
9959
10105
  // src/utils/command-sensors.ts
9960
10106
  import { execFile as execFile3 } from "child_process";
9961
10107
  import { promisify as promisify3 } from "util";
10108
+ import { scrubbedCommandEnv } from "@hivelore/core";
9962
10109
  var exec2 = promisify3(execFile3);
9963
10110
  var COMMAND_SENSOR_DEFAULT_TIMEOUT_MS = 12e4;
9964
10111
  var OUTPUT_TAIL_LINES = 15;
@@ -9982,7 +10129,9 @@ async function executeCommandSensor(spec, root) {
9982
10129
  cwd: root,
9983
10130
  timeout: timeoutMs,
9984
10131
  maxBuffer: 8 * 1024 * 1024,
9985
- env: { ...process.env, HIVELORE_SENSOR: spec.memory_id }
10132
+ // Scrubbed on purpose: a repo-authored oracle gets a test-runner environment, not the
10133
+ // caller's credentials (cloud keys, tokens). See scrubbedCommandEnv in core.
10134
+ env: { ...scrubbedCommandEnv(process.env), HIVELORE_SENSOR: spec.memory_id }
9986
10135
  });
9987
10136
  return {
9988
10137
  ...base,
@@ -10583,11 +10732,14 @@ async function checkFailureCapture(paths, config) {
10583
10732
  message: "No uncaptured hard failures from this session."
10584
10733
  }];
10585
10734
  }
10735
+ const autoDrafts = memories.filter(
10736
+ ({ memory: memory2 }) => memory2.frontmatter.status === "proposed" && memory2.frontmatter.tags.includes("auto-captured")
10737
+ );
10586
10738
  return [{
10587
10739
  severity: gate === "block" ? "error" : "info",
10588
10740
  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.",
10741
+ 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" : ""}.` : ""),
10742
+ 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
10743
  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
10744
  affected_files: uncaptured.slice(0, 8).map((f) => `${f.tool}: ${f.summary}`.slice(0, 100)),
10593
10745
  ...gate === "block" ? { impact: 30 } : {}
@@ -10647,6 +10799,22 @@ async function runWithEnforcement(command, args, opts) {
10647
10799
  child.on("close", (code, signal) => {
10648
10800
  if (signal) process.exit(128);
10649
10801
  process.exitCode = code ?? 0;
10802
+ if ((code ?? 0) !== 0) {
10803
+ const obsFile = path40.join(paths.haiveDir, ".cache", "observations.jsonl");
10804
+ void mkdir16(path40.dirname(obsFile), { recursive: true }).then(() => writeFile25(obsFile, "", { flag: "a" })).then(() => writeFile25(
10805
+ obsFile,
10806
+ JSON.stringify({
10807
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
10808
+ session_id: sessionId,
10809
+ tool: "AgentRun",
10810
+ summary: `wrapped agent exited ${code}: ${[command, ...args].join(" ").slice(0, 180)}`,
10811
+ failure_hint: true
10812
+ }) + "\n",
10813
+ { flag: "a" }
10814
+ )).catch(() => {
10815
+ }).finally(() => resolve());
10816
+ return;
10817
+ }
10650
10818
  resolve();
10651
10819
  });
10652
10820
  });
@@ -10785,7 +10953,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
10785
10953
  findings: [{ severity: "info", code: "enforcement-off", message: "Hivelore enforcement is disabled." }]
10786
10954
  });
10787
10955
  }
10788
- findings.push(...await inspectIntegrationVersions(root, "0.39.2"));
10956
+ findings.push(...await inspectIntegrationVersions(root, "0.42.1"));
10789
10957
  if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
10790
10958
  const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
10791
10959
  findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent Hivelore briefing marker exists." } : {
@@ -11122,6 +11290,17 @@ async function runPrecommitPolicy(paths, gate, stage) {
11122
11290
  ...weakeningFindings
11123
11291
  ];
11124
11292
  }
11293
+ async function stagedFileContent(root, rel) {
11294
+ try {
11295
+ return await runCommand2("git", ["show", `:${rel}`], root);
11296
+ } catch {
11297
+ try {
11298
+ return await readFile18(path40.resolve(root, rel), "utf8");
11299
+ } catch {
11300
+ return null;
11301
+ }
11302
+ }
11303
+ }
11125
11304
  async function runSensorGate(paths, diff, stage) {
11126
11305
  if (!diff || !existsSync42(paths.memoriesDir)) return [];
11127
11306
  try {
@@ -11174,6 +11353,75 @@ async function runSensorGate(paths, diff, stage) {
11174
11353
  });
11175
11354
  }
11176
11355
  }
11356
+ const astSensorMemories = scannable.filter((m) => m.frontmatter.sensor.kind === "ast");
11357
+ if (astSensorMemories.length > 0) {
11358
+ const addedByPath = addedLineNumbersFromDiff(diff);
11359
+ if (!await astEngineAvailable()) {
11360
+ findings.push({
11361
+ severity: "warn",
11362
+ code: "ast-sensor-unrunnable",
11363
+ 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.`,
11364
+ fix: "Install the engine: `npm i -g @ast-grep/napi` (or add it to the repo devDependencies).",
11365
+ impact: 5
11366
+ });
11367
+ } else {
11368
+ for (const memory2 of astSensorMemories) {
11369
+ const sensor = memory2.frontmatter.sensor;
11370
+ if (!sensor.pattern) continue;
11371
+ const applicable = targets.filter((t) => sensorAppliesToPath2(sensor, memory2.frontmatter.anchor.paths, t.path));
11372
+ if (applicable.length === 0) continue;
11373
+ let fired = false;
11374
+ for (const target of applicable) {
11375
+ const added = addedByPath.get(target.path);
11376
+ if (!added || added.size === 0) continue;
11377
+ const content = await stagedFileContent(paths.root, target.path);
11378
+ if (content === null) continue;
11379
+ const scan = await runAstSensorOnContent({
11380
+ pattern: sensor.pattern,
11381
+ absent: sensor.absent,
11382
+ content,
11383
+ filePath: target.path,
11384
+ addedLines: added
11385
+ });
11386
+ if (scan.status !== "ok" || scan.matches.length === 0) continue;
11387
+ fired = true;
11388
+ if (seen.has(memory2.frontmatter.id)) break;
11389
+ seen.add(memory2.frontmatter.id);
11390
+ firedIds.add(memory2.frontmatter.id);
11391
+ const where = ` (${target.path}:${scan.matches[0].startLine})`;
11392
+ if (sensor.severity === "block") {
11393
+ findings.push({
11394
+ severity: "error",
11395
+ code: "sensor-block",
11396
+ message: `Block AST sensor fired \u2014 ${memory2.frontmatter.id}: ${sensor.message}${where}${incidentSuffix(sensor.incident)}
11397
+ matched: ${scan.matches[0].text}`,
11398
+ fix: "Remove the flagged construct, or run `hivelore sensors check` to inspect the match.",
11399
+ impact: 45,
11400
+ memory_ids: [memory2.frontmatter.id]
11401
+ });
11402
+ } else {
11403
+ findings.push({
11404
+ severity: "warn",
11405
+ code: "sensor-warn",
11406
+ message: `AST sensor flagged ${memory2.frontmatter.id}: ${sensor.message}${where}${incidentSuffix(sensor.incident)}`,
11407
+ fix: "Review the flagged construct; `hivelore sensors check` shows the matched code.",
11408
+ impact: 5,
11409
+ memory_ids: [memory2.frontmatter.id]
11410
+ });
11411
+ }
11412
+ break;
11413
+ }
11414
+ ledgerRows.push(evaluation({
11415
+ memory_id: memory2.frontmatter.id,
11416
+ kind: "ast",
11417
+ stage,
11418
+ head_sha: headSha,
11419
+ scope_hash: "",
11420
+ outcome: fired ? "fired" : "silent"
11421
+ }));
11422
+ }
11423
+ }
11424
+ }
11177
11425
  const config = await loadConfig12(paths).catch(() => null);
11178
11426
  if (config?.enforcement?.runCommandSensors === true) {
11179
11427
  const changedPaths = targets.map((t) => t.path).filter(Boolean);
@@ -12303,6 +12551,7 @@ import {
12303
12551
  recordPreventionHits as recordPreventionHits2,
12304
12552
  resolveHaivePaths as resolveHaivePaths36,
12305
12553
  runSensors as runSensors2,
12554
+ addedLineNumbersFromDiff as addedLineNumbersFromDiff2,
12306
12555
  buildProposeCommand,
12307
12556
  scaffoldPostIncidentTest,
12308
12557
  selectCommandSensors as selectCommandSensors2,
@@ -12353,6 +12602,55 @@ function registerSensors(program2) {
12353
12602
  const diff = opts.diffFile ? await readFile20(path42.resolve(root, opts.diffFile), "utf8") : await stagedDiff(root);
12354
12603
  const targets = scannableSensorTargets(diff);
12355
12604
  const hits = runSensors2(memories, targets);
12605
+ const astMemories = (await runnableSensorMemories(paths, false)).filter(
12606
+ (m) => m.frontmatter.sensor?.kind === "ast" && m.frontmatter.sensor.pattern
12607
+ );
12608
+ const astHits = [];
12609
+ let astUnrunnable = 0;
12610
+ if (astMemories.length > 0) {
12611
+ if (!await astEngineAvailable()) {
12612
+ astUnrunnable = astMemories.length;
12613
+ } else {
12614
+ const addedByPath = addedLineNumbersFromDiff2(diff);
12615
+ for (const memory2 of astMemories) {
12616
+ const sensor = memory2.frontmatter.sensor;
12617
+ for (const target of targets) {
12618
+ if (!sensorAppliesToPath3(sensor, memory2.frontmatter.anchor.paths, target.path)) continue;
12619
+ const added = addedByPath.get(target.path);
12620
+ if (!added || added.size === 0) continue;
12621
+ let content = null;
12622
+ try {
12623
+ content = (await exec5("git", ["show", `:${target.path}`], { cwd: root })).stdout;
12624
+ } catch {
12625
+ try {
12626
+ content = await readFile20(path42.resolve(root, target.path), "utf8");
12627
+ } catch {
12628
+ content = null;
12629
+ }
12630
+ }
12631
+ if (content === null) continue;
12632
+ const scan = await runAstSensorOnContent({
12633
+ pattern: sensor.pattern,
12634
+ absent: sensor.absent,
12635
+ content,
12636
+ filePath: target.path,
12637
+ addedLines: added
12638
+ });
12639
+ if (scan.status === "ok" && scan.matches.length > 0) {
12640
+ astHits.push({
12641
+ memory_id: memory2.frontmatter.id,
12642
+ sensor,
12643
+ file: target.path,
12644
+ matched_line: `${target.path}:${scan.matches[0].startLine} ${scan.matches[0].text}`,
12645
+ message: sensor.message,
12646
+ severity: sensor.severity
12647
+ });
12648
+ break;
12649
+ }
12650
+ }
12651
+ }
12652
+ }
12653
+ }
12356
12654
  const config = await loadConfig13(paths);
12357
12655
  const runCommands = opts.commands || config.enforcement?.runCommandSensors === true;
12358
12656
  const changedPaths = targets.map((t) => t.path).filter(Boolean);
@@ -12414,9 +12712,10 @@ function registerSensors(program2) {
12414
12712
  for (const spec of commandSpecs) commandSkipped.push(spec.memory_id);
12415
12713
  }
12416
12714
  await appendSensorEvaluations2(paths, ledgerRows);
12417
- const firedIds = [...new Set([...hits, ...commandHits].map((hit) => hit.memory_id))];
12715
+ const firedIds = [...new Set([...hits, ...astHits, ...commandHits].map((hit) => hit.memory_id))];
12418
12716
  const preventionDetails = Object.fromEntries([
12419
12717
  ...hits.map((hit) => [hit.memory_id, { kind: "regex", stage: "manual" }]),
12718
+ ...astHits.map((hit) => [hit.memory_id, { kind: "ast", stage: "manual" }]),
12420
12719
  ...commandHits.map((hit) => [hit.memory_id, {
12421
12720
  kind: commandSpecs.find((spec) => spec.memory_id === hit.memory_id)?.kind ?? "shell",
12422
12721
  stage: "manual",
@@ -12425,8 +12724,9 @@ function registerSensors(program2) {
12425
12724
  ]);
12426
12725
  await recordPreventionHits2(paths, firedIds, "sensor", /* @__PURE__ */ new Date(), preventionDetails);
12427
12726
  const output = {
12428
- scanned: memories.length,
12429
- hits: hits.map((hit) => ({
12727
+ scanned: memories.length + astMemories.length,
12728
+ ast_unrunnable: astUnrunnable,
12729
+ hits: [...hits, ...astHits].map((hit) => ({
12430
12730
  memory_id: hit.memory_id,
12431
12731
  file: hit.file,
12432
12732
  severity: hit.severity,
@@ -12440,9 +12740,12 @@ function registerSensors(program2) {
12440
12740
  if (opts.json) {
12441
12741
  console.log(JSON.stringify(output, null, 2));
12442
12742
  } 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) {
12743
+ const total = hits.length + astHits.length + commandHits.length;
12744
+ console.log(ui.bold(`Hivelore sensors check \u2014 ${total} hit(s), ${memories.length} regex + ${astMemories.length} ast + ${commandSpecs.length} command sensor(s)`));
12745
+ if (astUnrunnable > 0) {
12746
+ console.log(ui.yellow(` \u26A0 ${astUnrunnable} AST sensor(s) unrunnable \u2014 install @ast-grep/napi to activate them (never blocks).`));
12747
+ }
12748
+ for (const hit of [...hits, ...astHits]) {
12446
12749
  const marker = hit.severity === "block" ? ui.red("\u2717") : ui.yellow("\u26A0");
12447
12750
  console.log(` ${marker} ${hit.memory_id} ${ui.dim(`(${hit.severity})`)}`);
12448
12751
  if (hit.file) console.log(` ${ui.dim("file:")} ${hit.file}`);
@@ -12466,7 +12769,7 @@ function registerSensors(program2) {
12466
12769
  console.log(ui.dim(` ${commandSkipped.length} command sensor(s) not run \u2014 pass --commands or set enforcement.runCommandSensors.`));
12467
12770
  }
12468
12771
  }
12469
- if ([...hits, ...commandHits].some((hit) => hit.severity === "block")) process.exitCode = 1;
12772
+ if ([...hits, ...astHits, ...commandHits].some((hit) => hit.severity === "block")) process.exitCode = 1;
12470
12773
  });
12471
12774
  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
12775
  const severity = opts.severity ?? "block";
@@ -12541,34 +12844,40 @@ function registerSensors(program2) {
12541
12844
  });
12542
12845
  sensors.command("propose").description(
12543
12846
  "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()) {
12847
+ ).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("--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) => {
12848
+ if (opts.kind === "shell" || opts.kind === "test" || opts.kind === "ast") {
12849
+ if ((opts.kind === "shell" || opts.kind === "test") && !opts.command?.trim()) {
12547
12850
  ui.error("--kind shell|test requires --command.");
12548
12851
  process.exitCode = 1;
12549
12852
  return;
12550
12853
  }
12854
+ if (opts.kind === "ast" && !opts.pattern?.trim()) {
12855
+ ui.error("--kind ast requires --pattern (an ast-grep structural pattern).");
12856
+ process.exitCode = 1;
12857
+ return;
12858
+ }
12551
12859
  const root2 = findProjectRoot39(opts.dir);
12552
- const { proposeSensor } = await import("./server-G6N6NJ64.js");
12860
+ const { proposeSensor } = await import("./server-PZWIQUU7.js");
12553
12861
  const out = await proposeSensor(
12554
12862
  {
12555
12863
  memory_id: id,
12556
12864
  kind: opts.kind,
12557
- pattern: void 0,
12558
- command: opts.command.trim(),
12865
+ pattern: opts.kind === "ast" ? opts.pattern.trim() : void 0,
12866
+ command: opts.command?.trim(),
12559
12867
  timeout_ms: opts.timeout ? Math.max(1, Number(opts.timeout)) : void 0,
12560
- absent: void 0,
12561
- bad_example: void 0,
12868
+ absent: opts.kind === "ast" ? opts.absent : void 0,
12869
+ bad_example: opts.kind === "ast" ? opts.badExample : void 0,
12562
12870
  severity: opts.severity === "warn" ? "warn" : "block",
12563
12871
  message: opts.message,
12564
12872
  incident: opts.incident,
12873
+ red_ref: opts.redRef,
12565
12874
  flags: void 0,
12566
12875
  paths: opts.paths ? opts.paths.split(",").map((p) => p.trim()).filter(Boolean) : []
12567
12876
  },
12568
12877
  { paths: resolveHaivePaths36(root2) }
12569
12878
  );
12570
12879
  if (out.accepted) {
12571
- ui.success(`Command sensor accepted (${out.severity}) on ${id}`);
12880
+ ui.success(`${opts.kind === "ast" ? "AST" : "Command"} sensor accepted (${out.severity}) on ${id}`);
12572
12881
  ui.info(` ${out.guidance}`);
12573
12882
  } else {
12574
12883
  ui.error(`Rejected (${out.reason}).`);
@@ -12811,17 +13120,19 @@ import {
12811
13120
  findProjectRoot as findProjectRoot40,
12812
13121
  loadMemoriesFromDir as loadMemoriesFromDir17,
12813
13122
  memoryFilePath as memoryFilePath7,
13123
+ extractReviewLearnings,
12814
13124
  parseFindings,
12815
13125
  resolveHaivePaths as resolveHaivePaths37,
13126
+ reviewLearningsToDrafts,
12816
13127
  serializeMemory as serializeMemory16
12817
13128
  } from "@hivelore/core";
12818
13129
  var SEVERITIES = ["info", "minor", "major", "critical", "blocker"];
12819
13130
  function registerIngest(program2) {
12820
13131
  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) => {
13132
+ "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"
13133
+ ).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
13134
  const format = opts.from;
12824
- const VALID_FORMATS = ["sarif", "sonar", "sonar-api", "eslint", "npm-audit"];
13135
+ const VALID_FORMATS = ["sarif", "sonar", "sonar-api", "eslint", "npm-audit", "github-pr"];
12825
13136
  if (!format || !VALID_FORMATS.includes(format)) {
12826
13137
  ui.error(`--from must be one of: ${VALID_FORMATS.join(", ")}`);
12827
13138
  process.exitCode = 1;
@@ -12846,7 +13157,25 @@ function registerIngest(program2) {
12846
13157
  }
12847
13158
  const parseFormat = format === "sonar-api" ? "sonar" : format;
12848
13159
  let raw;
12849
- if (format === "sonar-api") {
13160
+ if (format === "github-pr") {
13161
+ if (!file) {
13162
+ ui.error("--from github-pr needs a PR number/URL or a recorded comments JSON file.");
13163
+ process.exitCode = 1;
13164
+ return;
13165
+ }
13166
+ const asPath = path43.resolve(root, file);
13167
+ if (existsSync45(asPath)) {
13168
+ raw = await readFile21(asPath, "utf8");
13169
+ } else {
13170
+ const fetched = await fetchPrReviewComments(root, file);
13171
+ if (!fetched.ok) {
13172
+ ui.error(fetched.error);
13173
+ process.exitCode = 1;
13174
+ return;
13175
+ }
13176
+ raw = fetched.json;
13177
+ }
13178
+ } else if (format === "sonar-api") {
12850
13179
  const fetched = await fetchSonarIssues(opts);
12851
13180
  if (!fetched.ok) {
12852
13181
  ui.error(fetched.error);
@@ -12876,7 +13205,23 @@ function registerIngest(program2) {
12876
13205
  }
12877
13206
  let drafts;
12878
13207
  let findingsCount = 0;
12879
- try {
13208
+ if (format === "github-pr") {
13209
+ try {
13210
+ const payload = JSON.parse(raw);
13211
+ findingsCount = Array.isArray(payload) ? payload.length : 0;
13212
+ const learnings = extractReviewLearnings(payload);
13213
+ drafts = reviewLearningsToDrafts(learnings, {
13214
+ scope: opts.scope ?? "team",
13215
+ module: opts.module,
13216
+ author: opts.author,
13217
+ ...opts.limit ? { limit: Math.max(0, Number.parseInt(opts.limit, 10) || 0) } : {}
13218
+ });
13219
+ } catch (err) {
13220
+ ui.error(`Failed to parse the PR comments payload: ${err instanceof Error ? err.message : String(err)}`);
13221
+ process.exitCode = 1;
13222
+ return;
13223
+ }
13224
+ } else try {
12880
13225
  const findings = parseFindings(parseFormat, raw, { cwd: root });
12881
13226
  findingsCount = findings.length;
12882
13227
  drafts = draftsFromFindings(findings, {
@@ -13001,6 +13346,30 @@ async function fetchSonarIssues(opts) {
13001
13346
  };
13002
13347
  }
13003
13348
  }
13349
+ async function fetchPrReviewComments(root, ref) {
13350
+ const numberMatch = ref.match(/^(\d+)$/) ?? ref.match(/\/pull\/(\d+)/);
13351
+ if (!numberMatch) {
13352
+ return { ok: false, error: `"${ref}" is neither a comments JSON file, a PR number, nor a PR URL.` };
13353
+ }
13354
+ const prNumber = numberMatch[1];
13355
+ const { execFile: execFile9 } = await import("child_process");
13356
+ const { promisify: promisify9 } = await import("util");
13357
+ const run = promisify9(execFile9);
13358
+ try {
13359
+ const { stdout } = await run(
13360
+ "gh",
13361
+ ["api", `repos/{owner}/{repo}/pulls/${prNumber}/comments`, "--paginate"],
13362
+ { cwd: root, maxBuffer: 16 * 1024 * 1024 }
13363
+ );
13364
+ return { ok: true, json: stdout };
13365
+ } catch (err) {
13366
+ const e = err;
13367
+ return {
13368
+ ok: false,
13369
+ error: `Could not fetch PR #${prNumber} review 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.`
13370
+ };
13371
+ }
13372
+ }
13004
13373
 
13005
13374
  // src/commands/dashboard.ts
13006
13375
  import { existsSync as existsSync46 } from "fs";
@@ -13460,7 +13829,7 @@ function registerBridges(program2) {
13460
13829
 
13461
13830
  // src/index.ts
13462
13831
  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);
13832
+ program.name("hivelore").description("Hivelore - the deterministic policy gate for agent-written code (rules live as repo-native team memory)").version("0.42.1").option("--advanced", "show maintenance and experimental commands in help").showSuggestionAfterError(true);
13464
13833
  registerInit(program);
13465
13834
  registerResolveProject(program);
13466
13835
  registerEnforce(program);