@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/{chunk-XA5FXG6E.js → chunk-EJ7A4IKD.js} +300 -42
- package/dist/chunk-EJ7A4IKD.js.map +1 -0
- package/dist/index.js +401 -32
- package/dist/index.js.map +1 -1
- package/dist/{server-G6N6NJ64.js → server-PZWIQUU7.js} +10 -2
- package/package.json +7 -4
- package/dist/chunk-XA5FXG6E.js.map +0 -1
- /package/dist/{server-G6N6NJ64.js.map → server-PZWIQUU7.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-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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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) |
|
|
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-
|
|
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
|
|
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(
|
|
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]", "
|
|
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 === "
|
|
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
|
-
|
|
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.
|
|
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);
|