@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/{chunk-XA5FXG6E.js → chunk-FXDGOBPT.js} +376 -59
- package/dist/chunk-FXDGOBPT.js.map +1 -0
- package/dist/index.js +550 -49
- package/dist/index.js.map +1 -1
- package/dist/{server-G6N6NJ64.js → server-WW6JHBYY.js} +10 -2
- package/package.json +11 -4
- package/dist/chunk-XA5FXG6E.js.map +0 -1
- /package/dist/{server-G6N6NJ64.js.map → server-WW6JHBYY.js.map} +0 -0
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-
|
|
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.
|
|
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
|
-
|
|
7717
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) |
|
|
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-
|
|
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
|
-
|
|
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(
|
|
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]", "
|
|
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 === "
|
|
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
|
-
|
|
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.
|
|
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);
|