@hivelore/cli 0.34.1 → 0.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-X6UHROFL.js → chunk-QQANGHJK.js} +3 -3
- package/dist/index.js +447 -60
- package/dist/index.js.map +1 -1
- package/dist/{server-FQ5C43MV.js → server-W5Q35K7Q.js} +2 -2
- package/package.json +4 -4
- /package/dist/{chunk-X6UHROFL.js.map → chunk-QQANGHJK.js.map} +0 -0
- /package/dist/{server-FQ5C43MV.js.map → server-W5Q35K7Q.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
preCommitCheck,
|
|
11
11
|
readPresumedCorrectTargets,
|
|
12
12
|
runHaiveMcpStdio
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-QQANGHJK.js";
|
|
14
14
|
import {
|
|
15
15
|
registerMemoryPending
|
|
16
16
|
} from "./chunk-OYJKHD22.js";
|
|
@@ -275,7 +275,7 @@ async function lintMemoriesAsync(root, options = {}) {
|
|
|
275
275
|
const loaded = await loadMemoriesFromDir2(paths.memoriesDir);
|
|
276
276
|
const usage = await loadUsageIndex(paths);
|
|
277
277
|
const codeMap = await loadCodeMap(paths);
|
|
278
|
-
const
|
|
278
|
+
const trackedFiles2 = gitTrackedFiles(root);
|
|
279
279
|
const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
|
|
280
280
|
const actionableWords = /\b(always|never|prefer|use|run|avoid|because|instead|why|rationale|do not|must|should|require|required|requires|fix|fail|failed|fails|prevent|prevents|allow|allows|lets|ensure|ensures|catch|catches)\b/i;
|
|
281
281
|
for (const { filePath, memory: memory2 } of loaded) {
|
|
@@ -323,7 +323,7 @@ async function lintMemoriesAsync(root, options = {}) {
|
|
|
323
323
|
});
|
|
324
324
|
}
|
|
325
325
|
const isStackSeed = fm.tags.includes("stack-pack");
|
|
326
|
-
const suggestedAnchors = isStackSeed ? { paths: [], symbols: [] } : suggestAnchors(root, { filePath, memory: memory2 }, codeMap,
|
|
326
|
+
const suggestedAnchors = isStackSeed ? { paths: [], symbols: [] } : suggestAnchors(root, { filePath, memory: memory2 }, codeMap, trackedFiles2);
|
|
327
327
|
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated" && !isStackSeed) {
|
|
328
328
|
out.push({
|
|
329
329
|
file: filePath,
|
|
@@ -435,14 +435,14 @@ function titleFromId(id) {
|
|
|
435
435
|
const withoutDate = id.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
436
436
|
return withoutDate.split("-").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
437
437
|
}
|
|
438
|
-
function suggestAnchors(root, loaded, codeMap,
|
|
438
|
+
function suggestAnchors(root, loaded, codeMap, trackedFiles2) {
|
|
439
439
|
const body = loaded.memory.body;
|
|
440
440
|
const paths = /* @__PURE__ */ new Set();
|
|
441
441
|
const symbols = /* @__PURE__ */ new Set();
|
|
442
442
|
for (const match of body.matchAll(/`([^`\n]+\.[A-Za-z0-9]+)`|(?:^|\s)([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)/gm)) {
|
|
443
443
|
const candidate = (match[1] ?? match[2] ?? "").replace(/^\.?\//, "");
|
|
444
444
|
if (!candidate || candidate.startsWith("http")) continue;
|
|
445
|
-
if (existsSync(path2.join(root, candidate)) && isSafeAnchorPath(candidate,
|
|
445
|
+
if (existsSync(path2.join(root, candidate)) && isSafeAnchorPath(candidate, trackedFiles2)) {
|
|
446
446
|
paths.add(candidate);
|
|
447
447
|
}
|
|
448
448
|
}
|
|
@@ -452,7 +452,7 @@ function suggestAnchors(root, loaded, codeMap, trackedFiles) {
|
|
|
452
452
|
for (const exp of entry.exports) {
|
|
453
453
|
if (!exp.name || exp.name.length < 4) continue;
|
|
454
454
|
if (lowered.includes(exp.name.toLowerCase())) {
|
|
455
|
-
if (isSafeAnchorPath(file,
|
|
455
|
+
if (isSafeAnchorPath(file, trackedFiles2)) {
|
|
456
456
|
paths.add(file);
|
|
457
457
|
symbols.add(exp.name);
|
|
458
458
|
}
|
|
@@ -477,12 +477,12 @@ function gitTrackedFiles(root) {
|
|
|
477
477
|
const files = result.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
478
478
|
return new Set(files);
|
|
479
479
|
}
|
|
480
|
-
function isSafeAnchorPath(file,
|
|
480
|
+
function isSafeAnchorPath(file, trackedFiles2) {
|
|
481
481
|
const normalized = file.replace(/\\/g, "/").replace(/^\.?\//, "");
|
|
482
482
|
if (normalized.startsWith(".ai/.cache/") || normalized.startsWith(".ai/.runtime/")) return false;
|
|
483
483
|
if (normalized.includes("/node_modules/") || normalized.startsWith("node_modules/")) return false;
|
|
484
484
|
if (normalized.includes("/dist/") || normalized.startsWith("dist/")) return false;
|
|
485
|
-
if (
|
|
485
|
+
if (trackedFiles2 && !trackedFiles2.has(normalized)) return false;
|
|
486
486
|
return true;
|
|
487
487
|
}
|
|
488
488
|
function nearDuplicatePairs(loaded) {
|
|
@@ -3755,7 +3755,7 @@ ${SEED_FOOTER(stack)}` });
|
|
|
3755
3755
|
|
|
3756
3756
|
// src/commands/init.ts
|
|
3757
3757
|
var execFileAsync = promisify2(execFile2);
|
|
3758
|
-
var HAIVE_GITHUB_ACTION_REF = `v${"0.
|
|
3758
|
+
var HAIVE_GITHUB_ACTION_REF = `v${"0.35.1"}`;
|
|
3759
3759
|
var PROJECT_CONTEXT_TEMPLATE = `# Project context
|
|
3760
3760
|
|
|
3761
3761
|
> Generated by \`hivelore init\`. Run \`hivelore init --bootstrap\` to auto-fill from your codebase,
|
|
@@ -4714,20 +4714,28 @@ import path15 from "path";
|
|
|
4714
4714
|
import "commander";
|
|
4715
4715
|
import {
|
|
4716
4716
|
DEFAULT_AUTO_PROMOTE_RULE,
|
|
4717
|
+
assessSensorHealth,
|
|
4718
|
+
sensorPromotedAtMap,
|
|
4717
4719
|
buildFrontmatter as buildFrontmatter3,
|
|
4720
|
+
existingGateMissShas,
|
|
4718
4721
|
findProjectRoot as findProjectRoot9,
|
|
4719
4722
|
getUsage as getUsage2,
|
|
4723
|
+
gatePassedShas,
|
|
4720
4724
|
isAutoPromoteEligible,
|
|
4721
4725
|
isDecaying,
|
|
4722
4726
|
isStackPackSeed,
|
|
4723
4727
|
loadCodeMap as loadCodeMap5,
|
|
4724
4728
|
loadConfig as loadConfig3,
|
|
4725
4729
|
loadMemoriesFromDir as loadMemoriesFromDir7,
|
|
4730
|
+
loadSensorLedger,
|
|
4726
4731
|
loadUsageIndex as loadUsageIndex3,
|
|
4727
4732
|
pullCrossRepoSources,
|
|
4733
|
+
planGitWatch,
|
|
4734
|
+
proposeGateMissDrafts,
|
|
4728
4735
|
resolveHaivePaths as resolveHaivePaths8,
|
|
4729
4736
|
resolveManifestFiles,
|
|
4730
4737
|
serializeMemory as serializeMemory4,
|
|
4738
|
+
withQuarantineNote,
|
|
4731
4739
|
trackDependencies,
|
|
4732
4740
|
verifyAnchor,
|
|
4733
4741
|
watchContracts
|
|
@@ -4765,6 +4773,26 @@ function registerSync(program2) {
|
|
|
4765
4773
|
let revalidated = 0;
|
|
4766
4774
|
let promoted = 0;
|
|
4767
4775
|
let autoApproved = 0;
|
|
4776
|
+
let quarantined = 0;
|
|
4777
|
+
const quarantineCandidates = await loadMemoriesFromDir7(paths.memoriesDir);
|
|
4778
|
+
const sensorHealth = new Map(
|
|
4779
|
+
assessSensorHealth(await loadSensorLedger(paths), /* @__PURE__ */ new Date(), {
|
|
4780
|
+
promotedAt: sensorPromotedAtMap(quarantineCandidates.map((m) => m.memory.frontmatter))
|
|
4781
|
+
}).map((health) => [health.memory_id, health])
|
|
4782
|
+
);
|
|
4783
|
+
for (const { memory: memory2, filePath } of quarantineCandidates) {
|
|
4784
|
+
const sensor = memory2.frontmatter.sensor;
|
|
4785
|
+
const health = sensorHealth.get(memory2.frontmatter.id);
|
|
4786
|
+
if (!sensor || sensor.kind !== "shell" && sensor.kind !== "test" || sensor.severity !== "block" || !health?.quarantine_pending) continue;
|
|
4787
|
+
if (!dryRun) {
|
|
4788
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4789
|
+
await writeFile8(filePath, serializeMemory4({
|
|
4790
|
+
frontmatter: { ...memory2.frontmatter, sensor: { ...sensor, severity: "warn" } },
|
|
4791
|
+
body: withQuarantineNote(memory2.body, at, health.flap_count)
|
|
4792
|
+
}), "utf8");
|
|
4793
|
+
}
|
|
4794
|
+
quarantined++;
|
|
4795
|
+
}
|
|
4768
4796
|
if (opts.verify !== false) {
|
|
4769
4797
|
const memories = await loadMemoriesFromDir7(paths.memoriesDir);
|
|
4770
4798
|
for (const { memory: memory2, filePath } of memories) {
|
|
@@ -4885,13 +4913,17 @@ function registerSync(program2) {
|
|
|
4885
4913
|
for (const repair of repairs) log(ui.dim(`autopilot: ${repair.message}`));
|
|
4886
4914
|
}
|
|
4887
4915
|
const sinceReport = opts.since ? collectSinceChanges(root, opts.since) : null;
|
|
4916
|
+
const gateMissIds = await processGateMissWatch(root, paths, dryRun);
|
|
4917
|
+
if (gateMissIds.length > 0) {
|
|
4918
|
+
log(ui.yellow(`gate-miss: proposed ${gateMissIds.length} lesson(s): ${gateMissIds.join(", ")}`));
|
|
4919
|
+
}
|
|
4888
4920
|
const draftMemories = (await loadMemoriesFromDir7(paths.memoriesDir)).filter(
|
|
4889
4921
|
(m) => m.memory.frontmatter.status === "draft"
|
|
4890
4922
|
);
|
|
4891
4923
|
const draftCount = draftMemories.length;
|
|
4892
4924
|
const autoApprovedNote = autoApproved > 0 ? ` \xB7 ${autoApproved} auto-approved` : "";
|
|
4893
4925
|
log(
|
|
4894
|
-
`${ui.dim("sync:")} ${staleMarked} stale \xB7 ${revalidated} revalidated \xB7 ${promoted} promoted${autoApprovedNote}${sinceReport ? ` \xB7 ${sinceReport.added.length}+/${sinceReport.modified.length}~/${sinceReport.removed.length}- since ${opts.since}` : ""}`
|
|
4926
|
+
`${ui.dim("sync:")} ${staleMarked} stale \xB7 ${revalidated} revalidated \xB7 ${promoted} promoted \xB7 ${quarantined} quarantined${autoApprovedNote}${sinceReport ? ` \xB7 ${sinceReport.added.length}+/${sinceReport.modified.length}~/${sinceReport.removed.length}- since ${opts.since}` : ""}`
|
|
4895
4927
|
);
|
|
4896
4928
|
if (!opts.quiet && draftCount > 0) {
|
|
4897
4929
|
log(
|
|
@@ -5209,6 +5241,84 @@ Wait for **explicit confirmation** before acting.
|
|
|
5209
5241
|
}
|
|
5210
5242
|
});
|
|
5211
5243
|
}
|
|
5244
|
+
async function processGateMissWatch(root, paths, dryRun) {
|
|
5245
|
+
const headResult = spawnSync4("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" });
|
|
5246
|
+
if (headResult.status !== 0) return [];
|
|
5247
|
+
const head = headResult.stdout.trim();
|
|
5248
|
+
if (!head) return [];
|
|
5249
|
+
const stateFile = path15.join(paths.runtimeDir, "enforcement", "git-watch.json");
|
|
5250
|
+
let state = null;
|
|
5251
|
+
try {
|
|
5252
|
+
state = JSON.parse(await readFile7(stateFile, "utf8"));
|
|
5253
|
+
} catch {
|
|
5254
|
+
}
|
|
5255
|
+
const plan = planGitWatch(state, head);
|
|
5256
|
+
if (plan.action === "initialize") {
|
|
5257
|
+
if (!dryRun) {
|
|
5258
|
+
await mkdir8(path15.dirname(stateFile), { recursive: true });
|
|
5259
|
+
await writeFile8(stateFile, JSON.stringify(plan.next, null, 2) + "\n", "utf8");
|
|
5260
|
+
}
|
|
5261
|
+
return [];
|
|
5262
|
+
}
|
|
5263
|
+
if (plan.action === "idle") return [];
|
|
5264
|
+
const proposals = proposeGateMissDrafts(
|
|
5265
|
+
readGitCommitsRange(root, plan.range),
|
|
5266
|
+
existingGateMissShas(await loadMemoriesFromDir7(paths.memoriesDir)),
|
|
5267
|
+
gatePassedShas(await loadSensorLedger(paths)),
|
|
5268
|
+
{ pathExists: (rel) => existsSync14(path15.join(root, rel)) }
|
|
5269
|
+
);
|
|
5270
|
+
const ids = [];
|
|
5271
|
+
for (const proposal of proposals) {
|
|
5272
|
+
const fm = {
|
|
5273
|
+
...buildFrontmatter3({
|
|
5274
|
+
type: "attempt",
|
|
5275
|
+
slug: proposal.slug,
|
|
5276
|
+
scope: "team",
|
|
5277
|
+
status: "proposed",
|
|
5278
|
+
tags: ["gate-miss", proposal.kind],
|
|
5279
|
+
paths: proposal.paths,
|
|
5280
|
+
topic: `gate-miss-${proposal.reverted_sha}`
|
|
5281
|
+
}),
|
|
5282
|
+
status: "proposed"
|
|
5283
|
+
};
|
|
5284
|
+
ids.push(fm.id);
|
|
5285
|
+
if (!dryRun) {
|
|
5286
|
+
await mkdir8(paths.teamDir, { recursive: true });
|
|
5287
|
+
await writeFile8(
|
|
5288
|
+
path15.join(paths.teamDir, `${fm.id}.md`),
|
|
5289
|
+
serializeMemory4({ frontmatter: fm, body: proposal.body }),
|
|
5290
|
+
"utf8"
|
|
5291
|
+
);
|
|
5292
|
+
}
|
|
5293
|
+
}
|
|
5294
|
+
if (!dryRun) {
|
|
5295
|
+
await mkdir8(path15.dirname(stateFile), { recursive: true });
|
|
5296
|
+
await writeFile8(stateFile, JSON.stringify(plan.next, null, 2) + "\n", "utf8");
|
|
5297
|
+
}
|
|
5298
|
+
return ids;
|
|
5299
|
+
}
|
|
5300
|
+
function readGitCommitsRange(root, range) {
|
|
5301
|
+
const result = spawnSync4(
|
|
5302
|
+
"git",
|
|
5303
|
+
["log", "--reverse", "--format=%x1e%H%x1f%s%x1f%b%x1f", "--name-only", range],
|
|
5304
|
+
{ cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }
|
|
5305
|
+
);
|
|
5306
|
+
if (result.status !== 0) return [];
|
|
5307
|
+
const commits = [];
|
|
5308
|
+
for (const block of result.stdout.split("").filter((part) => part.trim())) {
|
|
5309
|
+
const [shaRaw, subjectRaw, bodyRaw, filesRaw = ""] = block.split("");
|
|
5310
|
+
const sha = shaRaw?.trim();
|
|
5311
|
+
const subject = subjectRaw?.trim();
|
|
5312
|
+
if (!sha || !subject) continue;
|
|
5313
|
+
commits.push({
|
|
5314
|
+
sha,
|
|
5315
|
+
subject,
|
|
5316
|
+
body: bodyRaw?.trim() ?? "",
|
|
5317
|
+
files: filesRaw.split("\n").map((file) => file.trim()).filter(Boolean)
|
|
5318
|
+
});
|
|
5319
|
+
}
|
|
5320
|
+
return commits;
|
|
5321
|
+
}
|
|
5212
5322
|
function bridgeSummaryLine(body) {
|
|
5213
5323
|
const firstLine = body.split("\n").map((l) => l.replace(/^#+\s*/, "").trim()).find((l) => l.length > 0) ?? "";
|
|
5214
5324
|
const oneLine = firstLine.replace(/\s+/g, " ");
|
|
@@ -7207,16 +7317,34 @@ import { mkdir as mkdir12, writeFile as writeFile18 } from "fs/promises";
|
|
|
7207
7317
|
import path30 from "path";
|
|
7208
7318
|
import {
|
|
7209
7319
|
aggregateUsage,
|
|
7320
|
+
buildPreventionReceipt,
|
|
7321
|
+
renderPreventionReceipt,
|
|
7210
7322
|
findProjectRoot as findProjectRoot28,
|
|
7211
7323
|
loadMemoriesFromDir as loadMemoriesFromDir11,
|
|
7212
7324
|
loadUsageIndex as loadUsageIndex12,
|
|
7325
|
+
loadPreventionEvents as loadPreventionEvents2,
|
|
7213
7326
|
parseSince,
|
|
7214
7327
|
readUsageEvents,
|
|
7215
7328
|
resolveHaivePaths as resolveHaivePaths27,
|
|
7216
7329
|
usageLogSize
|
|
7217
7330
|
} from "@hivelore/core";
|
|
7218
7331
|
function registerStats(program2) {
|
|
7219
|
-
program2.command("stats").description("Show MCP tool-usage stats
|
|
7332
|
+
const stats = program2.command("stats").description("Show MCP tool-usage stats and prevention receipts.");
|
|
7333
|
+
stats.command("receipt").description("Show documented mistakes refused by the gate over a time window").addHelpText("after", "\nParent options also apply: --since <window> (default 7d here), --json, --dir <dir>.").action(async () => {
|
|
7334
|
+
const opts = stats.opts();
|
|
7335
|
+
const root = findProjectRoot28(opts.dir);
|
|
7336
|
+
const paths = resolveHaivePaths27(root);
|
|
7337
|
+
const sinceRaw = stats.getOptionValueSource("since") === "default" ? "7d" : opts.since ?? "7d";
|
|
7338
|
+
const since = parseSince(sinceRaw) ?? new Date(Date.now() - 7 * 864e5);
|
|
7339
|
+
const [events, usage, memories] = await Promise.all([
|
|
7340
|
+
loadPreventionEvents2(paths),
|
|
7341
|
+
loadUsageIndex12(paths),
|
|
7342
|
+
existsSync33(paths.memoriesDir) ? loadMemoriesFromDir11(paths.memoriesDir) : Promise.resolve([])
|
|
7343
|
+
]);
|
|
7344
|
+
const receipt = buildPreventionReceipt(events, memories, usage, { since });
|
|
7345
|
+
console.log(opts.json ? JSON.stringify(receipt, null, 2) : renderPreventionReceipt(receipt));
|
|
7346
|
+
});
|
|
7347
|
+
stats.option("--since <window>", "ISO date or relative (e.g. '7d', '24h', '30m')", "30d").option("--json", "emit JSON instead of human-readable output", false).option("--memory-hits", "show top-read memories (which mems are actually being used)", false).option(
|
|
7220
7348
|
"--export-report <path>",
|
|
7221
7349
|
"write a JSON rollup (tools + briefing counts + heuristic ROI hints). Parent dirs are created if needed.",
|
|
7222
7350
|
void 0
|
|
@@ -7662,7 +7790,7 @@ import {
|
|
|
7662
7790
|
findProjectRoot as findProjectRoot31,
|
|
7663
7791
|
loadConfig as loadConfig7,
|
|
7664
7792
|
loadEvalHistory,
|
|
7665
|
-
loadPreventionEvents as
|
|
7793
|
+
loadPreventionEvents as loadPreventionEvents3,
|
|
7666
7794
|
loadUsageIndex as loadUsageIndex13,
|
|
7667
7795
|
overallScore,
|
|
7668
7796
|
resolveHaivePaths as resolveHaivePaths29,
|
|
@@ -7729,7 +7857,7 @@ function registerEval(program2) {
|
|
|
7729
7857
|
const authoredScore = resolvedSpec.authored > 0 && resolvedSpec.synthesized > 0 && (authoredRetrievalAgg || sensorAgg) ? overallScore(authoredRetrievalAgg, sensorAgg) : null;
|
|
7730
7858
|
const [usage, preventionEvents, config] = await Promise.all([
|
|
7731
7859
|
loadUsageIndex13(paths),
|
|
7732
|
-
|
|
7860
|
+
loadPreventionEvents3(paths),
|
|
7733
7861
|
loadConfig7(paths)
|
|
7734
7862
|
]);
|
|
7735
7863
|
const gatePrecision = computeGatePrecision(
|
|
@@ -8449,6 +8577,8 @@ import { execFileSync, execSync } from "child_process";
|
|
|
8449
8577
|
import "commander";
|
|
8450
8578
|
import {
|
|
8451
8579
|
codeMapPath as codeMapPath2,
|
|
8580
|
+
assessSensorHealth as assessSensorHealth2,
|
|
8581
|
+
sensorPromotedAtMap as sensorPromotedAtMap2,
|
|
8452
8582
|
countSourceFilesOnDisk,
|
|
8453
8583
|
extractReferencedPaths,
|
|
8454
8584
|
sensorPatternBrittleness,
|
|
@@ -8459,6 +8589,7 @@ import {
|
|
|
8459
8589
|
loadCodeMap as loadCodeMap6,
|
|
8460
8590
|
loadConfig as loadConfig10,
|
|
8461
8591
|
loadMemoriesFromDirDetailed,
|
|
8592
|
+
loadSensorLedger as loadSensorLedger2,
|
|
8462
8593
|
loadUsageIndex as loadUsageIndex15,
|
|
8463
8594
|
readUsageEvents as readUsageEvents3,
|
|
8464
8595
|
resolveHaivePaths as resolveHaivePaths33
|
|
@@ -8654,6 +8785,18 @@ function registerDoctor(program2) {
|
|
|
8654
8785
|
});
|
|
8655
8786
|
}
|
|
8656
8787
|
const sensorMemories = memories.filter((m) => m.memory.frontmatter.sensor);
|
|
8788
|
+
const gateMissDrafts = memories.filter(
|
|
8789
|
+
(m) => m.memory.frontmatter.status === "proposed" && m.memory.frontmatter.tags.includes("gate-miss")
|
|
8790
|
+
);
|
|
8791
|
+
if (gateMissDrafts.length > 0) {
|
|
8792
|
+
findings.push({
|
|
8793
|
+
severity: "info",
|
|
8794
|
+
code: "gate-miss-drafts",
|
|
8795
|
+
section: "Corpus health",
|
|
8796
|
+
message: `${gateMissDrafts.length} proposed gate-miss lesson(s) await review: ${gateMissDrafts.slice(0, 5).map((m) => m.memory.frontmatter.id).join(", ")}.`,
|
|
8797
|
+
fix: "Review with `hivelore memory list --status proposed`."
|
|
8798
|
+
});
|
|
8799
|
+
}
|
|
8657
8800
|
const blockSensors = sensorMemories.filter(
|
|
8658
8801
|
(m) => m.memory.frontmatter.sensor?.severity === "block"
|
|
8659
8802
|
).length;
|
|
@@ -8694,6 +8837,28 @@ function registerDoctor(program2) {
|
|
|
8694
8837
|
fix: "Make the pattern discriminating (add an 'absent' companion), or demote: `hivelore sensors promote <id> --severity warn`"
|
|
8695
8838
|
});
|
|
8696
8839
|
}
|
|
8840
|
+
const sensorHealth = assessSensorHealth2(await loadSensorLedger2(paths), /* @__PURE__ */ new Date(), {
|
|
8841
|
+
promotedAt: sensorPromotedAtMap2(memories.map((m) => m.memory.frontmatter))
|
|
8842
|
+
});
|
|
8843
|
+
for (const health of sensorHealth.filter((h) => h.quarantine_pending)) {
|
|
8844
|
+
const last = health.flaps.at(-1);
|
|
8845
|
+
findings.push({
|
|
8846
|
+
severity: "warn",
|
|
8847
|
+
code: "sensor-flaky",
|
|
8848
|
+
section: "Protection",
|
|
8849
|
+
message: `${health.memory_id} flapped ${health.flap_count}\xD7 on identical inputs. Last contradiction: ${last.previous.at} ${last.previous.outcome} \u2192 ${last.current.at} ${last.current.outcome}.`,
|
|
8850
|
+
fix: "Run `hivelore sync` to demote it, fix the oracle, then `hivelore sensors promote <id> --yes`."
|
|
8851
|
+
});
|
|
8852
|
+
}
|
|
8853
|
+
for (const health of sensorHealth.filter((h) => h.never_fired)) {
|
|
8854
|
+
findings.push({
|
|
8855
|
+
severity: "info",
|
|
8856
|
+
code: "sensor-never-fired",
|
|
8857
|
+
section: "Protection",
|
|
8858
|
+
message: `${health.memory_id} was evaluated ${health.evaluation_count} times across 30+ days and never fired \u2014 retirement candidate.`,
|
|
8859
|
+
fix: "Review the sensor manually; no automatic delete/retire command is provided."
|
|
8860
|
+
});
|
|
8861
|
+
}
|
|
8697
8862
|
const codeMap = await loadCodeMap6(paths);
|
|
8698
8863
|
if (!codeMap) {
|
|
8699
8864
|
findings.push({
|
|
@@ -8791,7 +8956,7 @@ function registerDoctor(program2) {
|
|
|
8791
8956
|
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `hivelore init` without --manual)."
|
|
8792
8957
|
});
|
|
8793
8958
|
}
|
|
8794
|
-
findings.push(...await collectInstallFindings(root, "0.
|
|
8959
|
+
findings.push(...await collectInstallFindings(root, "0.35.1"));
|
|
8795
8960
|
findings.push(...await collectToolchainFindings(root));
|
|
8796
8961
|
try {
|
|
8797
8962
|
const legacyRaw = execSync("haive-mcp --version", {
|
|
@@ -8799,7 +8964,7 @@ function registerDoctor(program2) {
|
|
|
8799
8964
|
timeout: 3e3,
|
|
8800
8965
|
stdio: ["ignore", "pipe", "ignore"]
|
|
8801
8966
|
}).trim();
|
|
8802
|
-
const cliVersion = "0.
|
|
8967
|
+
const cliVersion = "0.35.1";
|
|
8803
8968
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
8804
8969
|
findings.push({
|
|
8805
8970
|
severity: "warn",
|
|
@@ -9529,14 +9694,17 @@ function registerResolveProject(program2) {
|
|
|
9529
9694
|
}
|
|
9530
9695
|
|
|
9531
9696
|
// src/commands/enforce.ts
|
|
9532
|
-
import { execFile as
|
|
9697
|
+
import { execFile as execFile5, execFileSync as execFileSync2, spawn as spawn4 } from "child_process";
|
|
9533
9698
|
import { existsSync as existsSync41, statSync as statSync3 } from "fs";
|
|
9534
9699
|
import { chmod, mkdir as mkdir16, readFile as readFile18, readdir as readdir4, rm as rm2, writeFile as writeFile25 } from "fs/promises";
|
|
9535
9700
|
import path39 from "path";
|
|
9536
|
-
import { promisify as
|
|
9701
|
+
import { promisify as promisify5 } from "util";
|
|
9537
9702
|
import "commander";
|
|
9538
9703
|
import {
|
|
9539
9704
|
antiPatternGateParams as antiPatternGateParams2,
|
|
9705
|
+
appendSensorEvaluations,
|
|
9706
|
+
assessSensorHealth as assessSensorHealth3,
|
|
9707
|
+
sensorPromotedAtMap as sensorPromotedAtMap3,
|
|
9540
9708
|
assessBootstrapState,
|
|
9541
9709
|
isSensorScannablePath,
|
|
9542
9710
|
findProjectRoot as findProjectRoot37,
|
|
@@ -9550,6 +9718,7 @@ import {
|
|
|
9550
9718
|
loadConfig as loadConfig12,
|
|
9551
9719
|
detectAgentContext,
|
|
9552
9720
|
loadMemoriesFromDir as loadMemoriesFromDir14,
|
|
9721
|
+
loadSensorLedger as loadSensorLedger3,
|
|
9553
9722
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
|
|
9554
9723
|
readRecentBriefingMarker,
|
|
9555
9724
|
recordPreventionHits,
|
|
@@ -9559,6 +9728,7 @@ import {
|
|
|
9559
9728
|
saveConfig as saveConfig3,
|
|
9560
9729
|
selectCommandSensors,
|
|
9561
9730
|
sensorTargetsFromDiff,
|
|
9731
|
+
sensorAppliesToPath as sensorAppliesToPath2,
|
|
9562
9732
|
SESSION_RECAP_TTL_MS,
|
|
9563
9733
|
verifyAnchor as verifyAnchor3,
|
|
9564
9734
|
writeBriefingMarker as writeBriefingMarker2
|
|
@@ -9761,8 +9931,52 @@ async function executeCommandSensors(specs, root) {
|
|
|
9761
9931
|
return runs;
|
|
9762
9932
|
}
|
|
9763
9933
|
|
|
9934
|
+
// src/utils/sensor-evaluations.ts
|
|
9935
|
+
import { execFile as execFile4 } from "child_process";
|
|
9936
|
+
import { promisify as promisify4 } from "util";
|
|
9937
|
+
import {
|
|
9938
|
+
computeScopeHash,
|
|
9939
|
+
sensorAppliesToPath
|
|
9940
|
+
} from "@hivelore/core";
|
|
9941
|
+
var exec3 = promisify4(execFile4);
|
|
9942
|
+
async function gitHeadSha(root) {
|
|
9943
|
+
try {
|
|
9944
|
+
const { stdout } = await exec3("git", ["rev-parse", "HEAD"], { cwd: root });
|
|
9945
|
+
return stdout.trim();
|
|
9946
|
+
} catch {
|
|
9947
|
+
return "";
|
|
9948
|
+
}
|
|
9949
|
+
}
|
|
9950
|
+
async function trackedFiles(root) {
|
|
9951
|
+
try {
|
|
9952
|
+
const { stdout } = await exec3("git", ["ls-files", "-co", "--exclude-standard"], { cwd: root });
|
|
9953
|
+
return stdout.split("\n").map((f) => f.trim()).filter(Boolean);
|
|
9954
|
+
} catch {
|
|
9955
|
+
return [];
|
|
9956
|
+
}
|
|
9957
|
+
}
|
|
9958
|
+
async function commandScopeHash(root, spec) {
|
|
9959
|
+
if (spec.paths.length === 0) return "";
|
|
9960
|
+
const sensor = { kind: spec.kind, paths: spec.paths };
|
|
9961
|
+
const files = (await trackedFiles(root)).filter((file) => sensorAppliesToPath(sensor, [], file));
|
|
9962
|
+
return computeScopeHash(root, files);
|
|
9963
|
+
}
|
|
9964
|
+
function evaluation(base, command) {
|
|
9965
|
+
return {
|
|
9966
|
+
at: base.at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
9967
|
+
memory_id: base.memory_id,
|
|
9968
|
+
kind: base.kind,
|
|
9969
|
+
stage: base.stage,
|
|
9970
|
+
head_sha: base.head_sha,
|
|
9971
|
+
scope_hash: base.scope_hash,
|
|
9972
|
+
outcome: base.outcome,
|
|
9973
|
+
...command?.exit_code !== null ? { exit_code: command?.exit_code } : {},
|
|
9974
|
+
...command ? { duration_ms: command.duration_ms } : {}
|
|
9975
|
+
};
|
|
9976
|
+
}
|
|
9977
|
+
|
|
9764
9978
|
// src/commands/enforce.ts
|
|
9765
|
-
var execFileAsync2 =
|
|
9979
|
+
var execFileAsync2 = promisify5(execFile5);
|
|
9766
9980
|
var MAX_STDIN_BYTES2 = 256 * 1024;
|
|
9767
9981
|
var ENFORCE_HOOK_MARKER = "# Hivelore enforcement hook";
|
|
9768
9982
|
function registerEnforce(program2) {
|
|
@@ -10451,7 +10665,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
10451
10665
|
findings: [{ severity: "info", code: "enforcement-off", message: "Hivelore enforcement is disabled." }]
|
|
10452
10666
|
});
|
|
10453
10667
|
}
|
|
10454
|
-
findings.push(...await inspectIntegrationVersions(root, "0.
|
|
10668
|
+
findings.push(...await inspectIntegrationVersions(root, "0.35.1"));
|
|
10455
10669
|
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
10456
10670
|
const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
|
|
10457
10671
|
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent Hivelore briefing marker exists." } : {
|
|
@@ -10531,7 +10745,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
10531
10745
|
}];
|
|
10532
10746
|
}
|
|
10533
10747
|
const hasErrors = effectiveFindings.some((f) => f.severity === "error");
|
|
10534
|
-
|
|
10748
|
+
const report = withCategories({
|
|
10535
10749
|
root,
|
|
10536
10750
|
initialized,
|
|
10537
10751
|
mode,
|
|
@@ -10540,6 +10754,18 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
10540
10754
|
should_block: mode === "strict" && hasErrors,
|
|
10541
10755
|
findings: effectiveFindings
|
|
10542
10756
|
});
|
|
10757
|
+
if (!report.should_block && (stage === "pre-commit" || stage === "ci")) {
|
|
10758
|
+
const headSha = await gitHeadSha(root);
|
|
10759
|
+
await appendSensorEvaluations(paths, [evaluation({
|
|
10760
|
+
memory_id: "__gate__",
|
|
10761
|
+
kind: "shell",
|
|
10762
|
+
stage,
|
|
10763
|
+
head_sha: headSha,
|
|
10764
|
+
scope_hash: "",
|
|
10765
|
+
outcome: "silent"
|
|
10766
|
+
})]);
|
|
10767
|
+
}
|
|
10768
|
+
return report;
|
|
10543
10769
|
}
|
|
10544
10770
|
function withCategories(report) {
|
|
10545
10771
|
return {
|
|
@@ -10695,7 +10921,7 @@ async function runPrecommitPolicy(paths, gate, stage) {
|
|
|
10695
10921
|
anchored_blocks,
|
|
10696
10922
|
semantic: true
|
|
10697
10923
|
}, { paths });
|
|
10698
|
-
const sensorFindings = await runSensorGate(paths, snapshot.diff);
|
|
10924
|
+
const sensorFindings = await runSensorGate(paths, snapshot.diff, stage);
|
|
10699
10925
|
const reviewWarnings = result.warnings.filter(
|
|
10700
10926
|
(w) => w.level === "review" && !w.reasons.includes("sensor")
|
|
10701
10927
|
);
|
|
@@ -10758,7 +10984,7 @@ async function runPrecommitPolicy(paths, gate, stage) {
|
|
|
10758
10984
|
...sensorFindings
|
|
10759
10985
|
];
|
|
10760
10986
|
}
|
|
10761
|
-
async function runSensorGate(paths, diff) {
|
|
10987
|
+
async function runSensorGate(paths, diff, stage) {
|
|
10762
10988
|
if (!diff || !existsSync41(paths.memoriesDir)) return [];
|
|
10763
10989
|
try {
|
|
10764
10990
|
const loaded = await loadMemoriesFromDir14(paths.memoriesDir);
|
|
@@ -10769,8 +10995,22 @@ async function runSensorGate(paths, diff) {
|
|
|
10769
10995
|
const findings = [];
|
|
10770
10996
|
const seen = /* @__PURE__ */ new Set();
|
|
10771
10997
|
const firedIds = /* @__PURE__ */ new Set();
|
|
10998
|
+
const ledgerRows = [];
|
|
10999
|
+
const headSha = await gitHeadSha(paths.root);
|
|
10772
11000
|
const regexSensorMemories = scannable.filter((m) => m.frontmatter.sensor.kind === "regex");
|
|
10773
11001
|
const hits = regexSensorMemories.length > 0 ? runSensors(regexSensorMemories, targets) : [];
|
|
11002
|
+
for (const memory2 of regexSensorMemories) {
|
|
11003
|
+
const sensor = memory2.frontmatter.sensor;
|
|
11004
|
+
if (!targets.some((target) => sensorAppliesToPath2(sensor, memory2.frontmatter.anchor.paths, target.path))) continue;
|
|
11005
|
+
ledgerRows.push(evaluation({
|
|
11006
|
+
memory_id: memory2.frontmatter.id,
|
|
11007
|
+
kind: "regex",
|
|
11008
|
+
stage,
|
|
11009
|
+
head_sha: headSha,
|
|
11010
|
+
scope_hash: "",
|
|
11011
|
+
outcome: hits.some((hit) => hit.memory_id === memory2.frontmatter.id) ? "fired" : "silent"
|
|
11012
|
+
}));
|
|
11013
|
+
}
|
|
10774
11014
|
for (const hit of hits) {
|
|
10775
11015
|
if (seen.has(hit.memory_id)) continue;
|
|
10776
11016
|
seen.add(hit.memory_id);
|
|
@@ -10782,7 +11022,8 @@ async function runSensorGate(paths, diff) {
|
|
|
10782
11022
|
code: "sensor-block",
|
|
10783
11023
|
message: `Block sensor fired \u2014 ${hit.memory_id}: ${hit.message}${where}`,
|
|
10784
11024
|
fix: "Remove the flagged pattern, or run `hivelore sensors check` to inspect the match.",
|
|
10785
|
-
impact: 45
|
|
11025
|
+
impact: 45,
|
|
11026
|
+
memory_ids: [hit.memory_id]
|
|
10786
11027
|
});
|
|
10787
11028
|
} else {
|
|
10788
11029
|
findings.push({
|
|
@@ -10790,7 +11031,8 @@ async function runSensorGate(paths, diff) {
|
|
|
10790
11031
|
code: "sensor-warn",
|
|
10791
11032
|
message: `Sensor flagged ${hit.memory_id}: ${hit.message}${where}`,
|
|
10792
11033
|
fix: "Review the flagged line; `hivelore sensors check` shows the matched code.",
|
|
10793
|
-
impact: 5
|
|
11034
|
+
impact: 5,
|
|
11035
|
+
memory_ids: [hit.memory_id]
|
|
10794
11036
|
});
|
|
10795
11037
|
}
|
|
10796
11038
|
}
|
|
@@ -10798,7 +11040,37 @@ async function runSensorGate(paths, diff) {
|
|
|
10798
11040
|
if (config?.enforcement?.runCommandSensors === true) {
|
|
10799
11041
|
const changedPaths = targets.map((t) => t.path).filter(Boolean);
|
|
10800
11042
|
const specs = selectCommandSensors(scannable, changedPaths).filter((sp) => !seen.has(sp.memory_id));
|
|
10801
|
-
|
|
11043
|
+
const runs = await executeCommandSensors(specs, paths.root);
|
|
11044
|
+
for (const run of runs) {
|
|
11045
|
+
const spec = specs.find((candidate) => candidate.memory_id === run.memory_id);
|
|
11046
|
+
ledgerRows.push(evaluation({
|
|
11047
|
+
memory_id: run.memory_id,
|
|
11048
|
+
kind: run.kind,
|
|
11049
|
+
stage,
|
|
11050
|
+
head_sha: headSha,
|
|
11051
|
+
scope_hash: await commandScopeHash(paths.root, spec),
|
|
11052
|
+
outcome: run.status === "failed" ? "fired" : run.status === "passed" ? "silent" : "unrunnable"
|
|
11053
|
+
}, { exit_code: run.exit_code, duration_ms: run.duration_ms }));
|
|
11054
|
+
}
|
|
11055
|
+
const prior = await loadSensorLedger3(paths);
|
|
11056
|
+
const promotedAt = sensorPromotedAtMap3(scannable.map((m) => m.frontmatter));
|
|
11057
|
+
const health = new Map(
|
|
11058
|
+
assessSensorHealth3([...prior, ...ledgerRows], /* @__PURE__ */ new Date(), { promotedAt }).map((h) => [h.memory_id, h])
|
|
11059
|
+
);
|
|
11060
|
+
for (const run of runs) {
|
|
11061
|
+
const sensorHealth = health.get(run.memory_id);
|
|
11062
|
+
const quarantined = sensorHealth?.quarantine_pending === true;
|
|
11063
|
+
if (quarantined && run.severity === "block") {
|
|
11064
|
+
const last = sensorHealth.flaps.at(-1);
|
|
11065
|
+
findings.push({
|
|
11066
|
+
severity: "warn",
|
|
11067
|
+
code: "sensor-flaky",
|
|
11068
|
+
message: `Command sensor ${run.memory_id} flapped ${sensorHealth.flap_count}\xD7 on identical inputs; treated as warn pending sync quarantine. Last contradiction: ${last.previous.at} ${last.previous.outcome} \u2192 ${last.current.at} ${last.current.outcome}.`,
|
|
11069
|
+
fix: "Run `hivelore sync`, fix the flaky oracle, then re-promote with `hivelore sensors promote <id> --yes`.",
|
|
11070
|
+
impact: 5,
|
|
11071
|
+
memory_ids: [run.memory_id]
|
|
11072
|
+
});
|
|
11073
|
+
}
|
|
10802
11074
|
if (run.status === "passed") continue;
|
|
10803
11075
|
seen.add(run.memory_id);
|
|
10804
11076
|
if (run.status === "unrunnable") {
|
|
@@ -10815,14 +11087,15 @@ ${run.output_tail}` : ""),
|
|
|
10815
11087
|
firedIds.add(run.memory_id);
|
|
10816
11088
|
const outputBlock = run.output_tail ? `
|
|
10817
11089
|
${run.output_tail}` : "";
|
|
10818
|
-
if (run.severity === "block") {
|
|
11090
|
+
if (run.severity === "block" && !quarantined) {
|
|
10819
11091
|
findings.push({
|
|
10820
11092
|
severity: "error",
|
|
10821
11093
|
code: "sensor-block",
|
|
10822
11094
|
message: `Block ${run.kind} sensor fired \u2014 ${run.memory_id}: ${run.message}
|
|
10823
11095
|
command: ${run.command} (exit ${run.exit_code}, ${run.duration_ms}ms)${outputBlock}`,
|
|
10824
11096
|
fix: "Fix the behaviour the command checks, or run `hivelore sensors check --commands` to inspect it.",
|
|
10825
|
-
impact: 45
|
|
11097
|
+
impact: 45,
|
|
11098
|
+
memory_ids: [run.memory_id]
|
|
10826
11099
|
});
|
|
10827
11100
|
} else {
|
|
10828
11101
|
findings.push({
|
|
@@ -10830,13 +11103,19 @@ command: ${run.command} (exit ${run.exit_code}, ${run.duration_ms}ms)${outputBlo
|
|
|
10830
11103
|
code: "sensor-warn",
|
|
10831
11104
|
message: `${run.kind} sensor flagged ${run.memory_id}: ${run.message} (exit ${run.exit_code})${outputBlock}`,
|
|
10832
11105
|
fix: "Review the failing command; `hivelore sensors check --commands` re-runs it.",
|
|
10833
|
-
impact: 5
|
|
11106
|
+
impact: 5,
|
|
11107
|
+
memory_ids: [run.memory_id]
|
|
10834
11108
|
});
|
|
10835
11109
|
}
|
|
10836
11110
|
}
|
|
10837
11111
|
}
|
|
11112
|
+
await appendSensorEvaluations(paths, ledgerRows);
|
|
10838
11113
|
if (firedIds.size > 0) {
|
|
10839
|
-
|
|
11114
|
+
const details = Object.fromEntries([...firedIds].map((id) => {
|
|
11115
|
+
const row = ledgerRows.find((entry) => entry.memory_id === id && entry.outcome === "fired");
|
|
11116
|
+
return [id, { kind: row?.kind ?? "regex", stage, ...row?.exit_code !== void 0 ? { exit_code: row.exit_code } : {} }];
|
|
11117
|
+
}));
|
|
11118
|
+
await recordPreventionHits(paths, [...firedIds], "sensor", /* @__PURE__ */ new Date(), details).catch(() => {
|
|
10840
11119
|
});
|
|
10841
11120
|
}
|
|
10842
11121
|
return findings;
|
|
@@ -11442,11 +11721,27 @@ ${hook.body}`, "utf8");
|
|
|
11442
11721
|
async function installCiEnforcement(root) {
|
|
11443
11722
|
const workflowPath = path39.join(root, ".github", "workflows", "haive-enforcement.yml");
|
|
11444
11723
|
await mkdir16(path39.dirname(workflowPath), { recursive: true });
|
|
11724
|
+
const workflow = renderCiEnforcementWorkflow();
|
|
11445
11725
|
if (existsSync41(workflowPath)) {
|
|
11446
|
-
|
|
11726
|
+
const existing = await readFile18(workflowPath, "utf8");
|
|
11727
|
+
const start = "# haive:enforcement-workflow:start";
|
|
11728
|
+
const end = "# haive:enforcement-workflow:end";
|
|
11729
|
+
const startAt = existing.indexOf(start);
|
|
11730
|
+
const endAt = existing.indexOf(end);
|
|
11731
|
+
if (startAt >= 0 && endAt > startAt) {
|
|
11732
|
+
await writeFile25(workflowPath, existing.slice(0, startAt) + workflow + existing.slice(endAt + end.length), "utf8");
|
|
11733
|
+
ui.success(`Updated ${path39.relative(root, workflowPath)} managed block`);
|
|
11734
|
+
} else {
|
|
11735
|
+
ui.info("GitHub Actions enforcement workflow already exists without Hivelore markers \u2014 preserved");
|
|
11736
|
+
}
|
|
11447
11737
|
return;
|
|
11448
11738
|
}
|
|
11449
|
-
await writeFile25(workflowPath,
|
|
11739
|
+
await writeFile25(workflowPath, workflow, "utf8");
|
|
11740
|
+
ui.success(`Created ${path39.relative(root, workflowPath)}`);
|
|
11741
|
+
}
|
|
11742
|
+
function renderCiEnforcementWorkflow() {
|
|
11743
|
+
return `# haive:enforcement-workflow:start
|
|
11744
|
+
name: haive-enforcement
|
|
11450
11745
|
|
|
11451
11746
|
on:
|
|
11452
11747
|
pull_request:
|
|
@@ -11458,6 +11753,7 @@ jobs:
|
|
|
11458
11753
|
runs-on: ubuntu-latest
|
|
11459
11754
|
permissions:
|
|
11460
11755
|
contents: read
|
|
11756
|
+
pull-requests: write
|
|
11461
11757
|
steps:
|
|
11462
11758
|
- uses: actions/checkout@v4
|
|
11463
11759
|
with:
|
|
@@ -11468,12 +11764,51 @@ jobs:
|
|
|
11468
11764
|
- name: Install Hivelore
|
|
11469
11765
|
run: npm install -g @hivelore/cli
|
|
11470
11766
|
- name: Enforce Hivelore policy
|
|
11767
|
+
id: gate
|
|
11471
11768
|
env:
|
|
11472
11769
|
HAIVE_BASE_SHA: \${{ github.event.pull_request.base.sha || github.event.before }}
|
|
11473
11770
|
HAIVE_HEAD_SHA: \${{ github.event.pull_request.head.sha || github.sha }}
|
|
11474
|
-
run:
|
|
11475
|
-
|
|
11476
|
-
|
|
11771
|
+
run: |
|
|
11772
|
+
set +e
|
|
11773
|
+
hivelore enforce ci --json > "$RUNNER_TEMP/haive-gate.json"
|
|
11774
|
+
echo "exit_code=$?" >> "$GITHUB_OUTPUT"
|
|
11775
|
+
exit 0
|
|
11776
|
+
- name: Upsert prevention receipt
|
|
11777
|
+
if: always() && github.event_name == 'pull_request'
|
|
11778
|
+
env:
|
|
11779
|
+
GH_TOKEN: \${{ github.token }}
|
|
11780
|
+
PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
11781
|
+
run: |
|
|
11782
|
+
if [ -z "\${GH_TOKEN:-}" ] || ! command -v gh >/dev/null 2>&1; then exit 0; fi
|
|
11783
|
+
receipt="$(hivelore stats receipt --since 7d --json 2>/dev/null)" || exit 0
|
|
11784
|
+
gate="$(cat "$RUNNER_TEMP/haive-gate.json" 2>/dev/null)" || gate='{"findings":[]}'
|
|
11785
|
+
body="$(jq -nr --arg marker '<!-- haive:prevention-receipt -->' --argjson receipt "$receipt" --argjson gate "$gate" '
|
|
11786
|
+
$marker + "
|
|
11787
|
+
## Hivelore prevention receipt
|
|
11788
|
+
|
|
11789
|
+
" +
|
|
11790
|
+
(([$gate.findings[]? | select(.code == "sensor-block" or .code == "sensor-warn") |
|
|
11791
|
+
"- **" + (.memory_ids[0] // "sensor") + "** \u2014 " + .message] | if length == 0 then
|
|
11792
|
+
"No documented sensor fired on this PR." else "### Fired on this PR
|
|
11793
|
+
" + join("
|
|
11794
|
+
") end)) +
|
|
11795
|
+
"
|
|
11796
|
+
|
|
11797
|
+
Weekly total: **" + ($receipt.total|tostring) + "** refused; previous window: **" +
|
|
11798
|
+
($receipt.previous_total|tostring) + "**."
|
|
11799
|
+
')" || exit 0
|
|
11800
|
+
comments="$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" --paginate 2>/dev/null)" || exit 0
|
|
11801
|
+
comment_id="$(printf '%s' "$comments" | jq -r '.[] | select(.body | contains("<!-- haive:prevention-receipt -->")) | .id' | head -1)"
|
|
11802
|
+
if [ -n "$comment_id" ]; then
|
|
11803
|
+
gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$comment_id" -f body="$body" >/dev/null 2>&1 || true
|
|
11804
|
+
else
|
|
11805
|
+
gh api --method POST "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" -f body="$body" >/dev/null 2>&1 || true
|
|
11806
|
+
fi
|
|
11807
|
+
- name: Fail when enforcement blocked
|
|
11808
|
+
if: steps.gate.outputs.exit_code != '0'
|
|
11809
|
+
run: exit \${{ steps.gate.outputs.exit_code }}
|
|
11810
|
+
# haive:enforcement-workflow:end
|
|
11811
|
+
`;
|
|
11477
11812
|
}
|
|
11478
11813
|
function printReport(report, json, explain = false) {
|
|
11479
11814
|
if (json) {
|
|
@@ -11658,9 +11993,9 @@ import { readFile as readFile19, writeFile as writeFile26 } from "fs/promises";
|
|
|
11658
11993
|
import path40 from "path";
|
|
11659
11994
|
import "commander";
|
|
11660
11995
|
import { findProjectRoot as findProjectRoot38 } from "@hivelore/core";
|
|
11661
|
-
import { execFile as
|
|
11662
|
-
import { promisify as
|
|
11663
|
-
var
|
|
11996
|
+
import { execFile as execFile6 } from "child_process";
|
|
11997
|
+
import { promisify as promisify6 } from "util";
|
|
11998
|
+
var exec4 = promisify6(execFile6);
|
|
11664
11999
|
var VERSION_FILES2 = [
|
|
11665
12000
|
"package.json",
|
|
11666
12001
|
"packages/core/package.json",
|
|
@@ -11737,24 +12072,24 @@ ${heading}
|
|
|
11737
12072
|
return;
|
|
11738
12073
|
}
|
|
11739
12074
|
}
|
|
11740
|
-
const dirty = (await
|
|
12075
|
+
const dirty = (await exec4("git", ["status", "--porcelain"], { cwd: root })).stdout.trim();
|
|
11741
12076
|
if (dirty.length > 0) {
|
|
11742
12077
|
ui.error("Working tree is not clean \u2014 commit the bump before tagging.");
|
|
11743
12078
|
process.exitCode = 1;
|
|
11744
12079
|
return;
|
|
11745
12080
|
}
|
|
11746
12081
|
const tag = `v${version}`;
|
|
11747
|
-
const existing = (await
|
|
12082
|
+
const existing = (await exec4("git", ["tag", "--list", tag], { cwd: root })).stdout.trim();
|
|
11748
12083
|
if (existing) {
|
|
11749
12084
|
ui.error(`Tag ${tag} already exists \u2014 bump the version first.`);
|
|
11750
12085
|
process.exitCode = 1;
|
|
11751
12086
|
return;
|
|
11752
12087
|
}
|
|
11753
|
-
await
|
|
12088
|
+
await exec4("git", ["tag", tag], { cwd: root });
|
|
11754
12089
|
ui.success(`Created ${tag} at HEAD.`);
|
|
11755
12090
|
if (opts.push !== false) {
|
|
11756
|
-
await
|
|
11757
|
-
await
|
|
12091
|
+
await exec4("git", ["push"], { cwd: root });
|
|
12092
|
+
await exec4("git", ["push", "origin", tag], { cwd: root });
|
|
11758
12093
|
ui.success(`Pushed branch and ${tag}.`);
|
|
11759
12094
|
ui.info("Next: `hivelore enforce finish --wait` (polls CI), then publish via `pnpm run publish:all` (human step).");
|
|
11760
12095
|
}
|
|
@@ -11774,18 +12109,22 @@ function registerRun(program2) {
|
|
|
11774
12109
|
}
|
|
11775
12110
|
|
|
11776
12111
|
// src/commands/sensors.ts
|
|
11777
|
-
import { execFile as
|
|
12112
|
+
import { execFile as execFile7 } from "child_process";
|
|
11778
12113
|
import { existsSync as existsSync43 } from "fs";
|
|
11779
12114
|
import { chmod as chmod2, mkdir as mkdir17, readFile as readFile20, writeFile as writeFile27 } from "fs/promises";
|
|
11780
12115
|
import path41 from "path";
|
|
11781
|
-
import { promisify as
|
|
12116
|
+
import { promisify as promisify7 } from "util";
|
|
11782
12117
|
import "commander";
|
|
11783
12118
|
import {
|
|
11784
12119
|
extractSensorExamples,
|
|
12120
|
+
appendSensorEvaluations as appendSensorEvaluations2,
|
|
12121
|
+
assessSensorHealth as assessSensorHealth4,
|
|
12122
|
+
sensorPromotedAtMap as sensorPromotedAtMap4,
|
|
11785
12123
|
findProjectRoot as findProjectRoot39,
|
|
11786
12124
|
isRetiredMemory as isRetiredMemory3,
|
|
11787
12125
|
judgeProposedSensor,
|
|
11788
12126
|
loadConfig as loadConfig13,
|
|
12127
|
+
loadSensorLedger as loadSensorLedger4,
|
|
11789
12128
|
loadMemoriesFromDir as loadMemoriesFromDir15,
|
|
11790
12129
|
recordPreventionHits as recordPreventionHits2,
|
|
11791
12130
|
resolveHaivePaths as resolveHaivePaths36,
|
|
@@ -11793,10 +12132,12 @@ import {
|
|
|
11793
12132
|
selectCommandSensors as selectCommandSensors2,
|
|
11794
12133
|
sensorPatternBrittleness as sensorPatternBrittleness2,
|
|
11795
12134
|
sensorSelfCheck as sensorSelfCheck2,
|
|
12135
|
+
sensorAppliesToPath as sensorAppliesToPath3,
|
|
11796
12136
|
scannableSensorTargets,
|
|
11797
|
-
serializeMemory as serializeMemory15
|
|
12137
|
+
serializeMemory as serializeMemory15,
|
|
12138
|
+
withoutQuarantineNote
|
|
11798
12139
|
} from "@hivelore/core";
|
|
11799
|
-
var
|
|
12140
|
+
var exec5 = promisify7(execFile7);
|
|
11800
12141
|
function registerSensors(program2) {
|
|
11801
12142
|
const sensors = program2.command("sensors").description("Operate executable sensors derived from Hivelore memories");
|
|
11802
12143
|
sensors.command("list").description("List memories carrying executable sensors").option("--json", "emit JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
@@ -11843,12 +12184,47 @@ function registerSensors(program2) {
|
|
|
11843
12184
|
const commandHits = [];
|
|
11844
12185
|
const commandUnrunnable = [];
|
|
11845
12186
|
const commandSkipped = [];
|
|
12187
|
+
const ledgerRows = [];
|
|
12188
|
+
const headSha = await gitHeadSha(root);
|
|
12189
|
+
for (const memory2 of memories) {
|
|
12190
|
+
const sensor = memory2.frontmatter.sensor;
|
|
12191
|
+
if (!sensor || sensor.kind !== "regex") continue;
|
|
12192
|
+
const anchors = memory2.frontmatter.anchor.paths;
|
|
12193
|
+
const applicable = targets.some((target) => sensorAppliesToPath3(sensor, anchors, target.path));
|
|
12194
|
+
if (!applicable) continue;
|
|
12195
|
+
ledgerRows.push(evaluation({
|
|
12196
|
+
memory_id: memory2.frontmatter.id,
|
|
12197
|
+
kind: "regex",
|
|
12198
|
+
stage: "manual",
|
|
12199
|
+
head_sha: headSha,
|
|
12200
|
+
scope_hash: "",
|
|
12201
|
+
outcome: hits.some((hit) => hit.memory_id === memory2.frontmatter.id) ? "fired" : "silent"
|
|
12202
|
+
}));
|
|
12203
|
+
}
|
|
11846
12204
|
if (commandSpecs.length > 0 && runCommands) {
|
|
11847
|
-
|
|
12205
|
+
const runs = await executeCommandSensors(commandSpecs, root);
|
|
12206
|
+
for (const run of runs) {
|
|
12207
|
+
const spec = commandSpecs.find((candidate) => candidate.memory_id === run.memory_id);
|
|
12208
|
+
ledgerRows.push(evaluation({
|
|
12209
|
+
memory_id: run.memory_id,
|
|
12210
|
+
kind: run.kind,
|
|
12211
|
+
stage: "manual",
|
|
12212
|
+
head_sha: headSha,
|
|
12213
|
+
scope_hash: await commandScopeHash(root, spec),
|
|
12214
|
+
outcome: run.status === "failed" ? "fired" : run.status === "passed" ? "silent" : "unrunnable"
|
|
12215
|
+
}, { exit_code: run.exit_code, duration_ms: run.duration_ms }));
|
|
12216
|
+
}
|
|
12217
|
+
const prior = await loadSensorLedger4(paths);
|
|
12218
|
+
const promotedAt = sensorPromotedAtMap4(allSensorMemories.map((m) => m.frontmatter));
|
|
12219
|
+
const health = new Map(
|
|
12220
|
+
assessSensorHealth4([...prior, ...ledgerRows], /* @__PURE__ */ new Date(), { promotedAt }).map((h) => [h.memory_id, h])
|
|
12221
|
+
);
|
|
12222
|
+
for (const run of runs) {
|
|
12223
|
+
const quarantined = health.get(run.memory_id)?.quarantine_pending === true;
|
|
11848
12224
|
if (run.status === "failed") {
|
|
11849
12225
|
commandHits.push({
|
|
11850
12226
|
memory_id: run.memory_id,
|
|
11851
|
-
severity: run.severity,
|
|
12227
|
+
severity: quarantined ? "warn" : run.severity,
|
|
11852
12228
|
message: run.message,
|
|
11853
12229
|
matched_line: `command failed (exit ${run.exit_code}, ${run.duration_ms}ms): ${run.command}`,
|
|
11854
12230
|
...run.output_tail ? { output_tail: run.output_tail } : {}
|
|
@@ -11860,8 +12236,17 @@ function registerSensors(program2) {
|
|
|
11860
12236
|
} else if (commandSpecs.length > 0) {
|
|
11861
12237
|
for (const spec of commandSpecs) commandSkipped.push(spec.memory_id);
|
|
11862
12238
|
}
|
|
12239
|
+
await appendSensorEvaluations2(paths, ledgerRows);
|
|
11863
12240
|
const firedIds = [...new Set([...hits, ...commandHits].map((hit) => hit.memory_id))];
|
|
11864
|
-
|
|
12241
|
+
const preventionDetails = Object.fromEntries([
|
|
12242
|
+
...hits.map((hit) => [hit.memory_id, { kind: "regex", stage: "manual" }]),
|
|
12243
|
+
...commandHits.map((hit) => [hit.memory_id, {
|
|
12244
|
+
kind: commandSpecs.find((spec) => spec.memory_id === hit.memory_id)?.kind ?? "shell",
|
|
12245
|
+
stage: "manual",
|
|
12246
|
+
exit_code: Number(/exit (\d+)/.exec(hit.matched_line)?.[1] ?? 1)
|
|
12247
|
+
}])
|
|
12248
|
+
]);
|
|
12249
|
+
await recordPreventionHits2(paths, firedIds, "sensor", /* @__PURE__ */ new Date(), preventionDetails);
|
|
11865
12250
|
const output = {
|
|
11866
12251
|
scanned: memories.length,
|
|
11867
12252
|
hits: hits.map((hit) => ({
|
|
@@ -11966,9 +12351,11 @@ function registerSensors(program2) {
|
|
|
11966
12351
|
const next = {
|
|
11967
12352
|
frontmatter: {
|
|
11968
12353
|
...found.memory.frontmatter,
|
|
11969
|
-
|
|
12354
|
+
// promoted_at makes health assessment ignore pre-promotion ledger rows: promoting is the
|
|
12355
|
+
// human's assertion the oracle was fixed, so old flaps must not re-quarantine it.
|
|
12356
|
+
sensor: severity === "block" ? { ...sensor, severity, promoted_at: (/* @__PURE__ */ new Date()).toISOString() } : { ...sensor, severity }
|
|
11970
12357
|
},
|
|
11971
|
-
body: found.memory.body
|
|
12358
|
+
body: severity === "block" ? withoutQuarantineNote(found.memory.body) : found.memory.body
|
|
11972
12359
|
};
|
|
11973
12360
|
await writeFile27(found.filePath, serializeMemory15(next), "utf8");
|
|
11974
12361
|
ui.success(`Updated ${id}: sensor severity=${severity}`);
|
|
@@ -11985,7 +12372,7 @@ function registerSensors(program2) {
|
|
|
11985
12372
|
return;
|
|
11986
12373
|
}
|
|
11987
12374
|
const root2 = findProjectRoot39(opts.dir);
|
|
11988
|
-
const { proposeSensor } = await import("./server-
|
|
12375
|
+
const { proposeSensor } = await import("./server-W5Q35K7Q.js");
|
|
11989
12376
|
const out = await proposeSensor(
|
|
11990
12377
|
{
|
|
11991
12378
|
memory_id: id,
|
|
@@ -12132,7 +12519,7 @@ async function runnableSensorMemories(paths, regexOnly = true) {
|
|
|
12132
12519
|
}
|
|
12133
12520
|
async function stagedDiff(root) {
|
|
12134
12521
|
try {
|
|
12135
|
-
const { stdout } = await
|
|
12522
|
+
const { stdout } = await exec5("git", ["diff", "--cached"], { cwd: root });
|
|
12136
12523
|
return stdout;
|
|
12137
12524
|
} catch (err) {
|
|
12138
12525
|
throw new Error(`git diff --cached failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -12372,7 +12759,7 @@ import {
|
|
|
12372
12759
|
findProjectRoot as findProjectRoot41,
|
|
12373
12760
|
loadConfig as loadConfig14,
|
|
12374
12761
|
loadMemoriesFromDir as loadMemoriesFromDir17,
|
|
12375
|
-
loadPreventionEvents as
|
|
12762
|
+
loadPreventionEvents as loadPreventionEvents4,
|
|
12376
12763
|
loadUsageIndex as loadUsageIndex16,
|
|
12377
12764
|
resolveHaivePaths as resolveHaivePaths38
|
|
12378
12765
|
} from "@hivelore/core";
|
|
@@ -12389,7 +12776,7 @@ function registerDashboard(program2) {
|
|
|
12389
12776
|
}
|
|
12390
12777
|
const memories = existsSync45(paths.memoriesDir) ? await loadMemoriesFromDir17(paths.memoriesDir) : [];
|
|
12391
12778
|
const usage = await loadUsageIndex16(paths);
|
|
12392
|
-
const preventionEvents = await
|
|
12779
|
+
const preventionEvents = await loadPreventionEvents4(paths);
|
|
12393
12780
|
const config = await loadConfig14(paths);
|
|
12394
12781
|
const top = Math.max(1, Number.parseInt(opts.top ?? "10", 10) || 10);
|
|
12395
12782
|
const dormantDays = opts.dormantDays ? Number.parseInt(opts.dormantDays, 10) : void 0;
|
|
@@ -12511,14 +12898,14 @@ function warnNum(n) {
|
|
|
12511
12898
|
}
|
|
12512
12899
|
|
|
12513
12900
|
// src/commands/dev-link.ts
|
|
12514
|
-
import { execFile as
|
|
12901
|
+
import { execFile as execFile8 } from "child_process";
|
|
12515
12902
|
import { cp, readFile as readFile22 } from "fs/promises";
|
|
12516
12903
|
import { existsSync as existsSync46 } from "fs";
|
|
12517
12904
|
import path43 from "path";
|
|
12518
|
-
import { promisify as
|
|
12905
|
+
import { promisify as promisify8 } from "util";
|
|
12519
12906
|
import "commander";
|
|
12520
12907
|
import { findProjectRoot as findProjectRoot42 } from "@hivelore/core";
|
|
12521
|
-
var
|
|
12908
|
+
var exec6 = promisify8(execFile8);
|
|
12522
12909
|
function registerDevLink(program2) {
|
|
12523
12910
|
const dev = program2.commands.find((c) => c.name() === "dev") ?? program2.command("dev").description("Developer utilities for working on Hivelore itself.");
|
|
12524
12911
|
dev.command("link").description("Hot-swap this repo's built dist into the global @hivelore (or legacy @hiveai) install so the global binary runs your local code.").option("-d, --dir <dir>", "repo root (default: discovered from cwd)").option("--json", "emit a machine-readable summary", false).action(async (opts) => {
|
|
@@ -12530,7 +12917,7 @@ function registerDevLink(program2) {
|
|
|
12530
12917
|
}
|
|
12531
12918
|
let globalModules;
|
|
12532
12919
|
try {
|
|
12533
|
-
globalModules = (await
|
|
12920
|
+
globalModules = (await exec6("npm", ["root", "-g"])).stdout.trim();
|
|
12534
12921
|
} catch {
|
|
12535
12922
|
globalModules = path43.join(path43.dirname(path43.dirname(process.execPath)), "lib", "node_modules");
|
|
12536
12923
|
}
|
|
@@ -12822,7 +13209,7 @@ function registerBridges(program2) {
|
|
|
12822
13209
|
|
|
12823
13210
|
// src/index.ts
|
|
12824
13211
|
var program = new Command48();
|
|
12825
|
-
program.name("hivelore").description("Hivelore - repo-native memory and context policy for coding-agent harnesses").version("0.
|
|
13212
|
+
program.name("hivelore").description("Hivelore - repo-native memory and context policy for coding-agent harnesses").version("0.35.1").option("--advanced", "show maintenance and experimental commands in help").showSuggestionAfterError(true);
|
|
12826
13213
|
registerInit(program);
|
|
12827
13214
|
registerResolveProject(program);
|
|
12828
13215
|
registerEnforce(program);
|