@hivelore/cli 0.34.1 → 0.35.0
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-ZR7UPTRR.js} +3 -3
- package/dist/index.js +429 -59
- package/dist/index.js.map +1 -1
- package/dist/{server-FQ5C43MV.js → server-2FUYM6KI.js} +2 -2
- package/package.json +4 -4
- /package/dist/{chunk-X6UHROFL.js.map → chunk-ZR7UPTRR.js.map} +0 -0
- /package/dist/{server-FQ5C43MV.js.map → server-2FUYM6KI.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-ZR7UPTRR.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.0"}`;
|
|
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,27 @@ import path15 from "path";
|
|
|
4714
4714
|
import "commander";
|
|
4715
4715
|
import {
|
|
4716
4716
|
DEFAULT_AUTO_PROMOTE_RULE,
|
|
4717
|
+
assessSensorHealth,
|
|
4717
4718
|
buildFrontmatter as buildFrontmatter3,
|
|
4719
|
+
existingGateMissShas,
|
|
4718
4720
|
findProjectRoot as findProjectRoot9,
|
|
4719
4721
|
getUsage as getUsage2,
|
|
4722
|
+
gatePassedShas,
|
|
4720
4723
|
isAutoPromoteEligible,
|
|
4721
4724
|
isDecaying,
|
|
4722
4725
|
isStackPackSeed,
|
|
4723
4726
|
loadCodeMap as loadCodeMap5,
|
|
4724
4727
|
loadConfig as loadConfig3,
|
|
4725
4728
|
loadMemoriesFromDir as loadMemoriesFromDir7,
|
|
4729
|
+
loadSensorLedger,
|
|
4726
4730
|
loadUsageIndex as loadUsageIndex3,
|
|
4727
4731
|
pullCrossRepoSources,
|
|
4732
|
+
planGitWatch,
|
|
4733
|
+
proposeGateMissDrafts,
|
|
4728
4734
|
resolveHaivePaths as resolveHaivePaths8,
|
|
4729
4735
|
resolveManifestFiles,
|
|
4730
4736
|
serializeMemory as serializeMemory4,
|
|
4737
|
+
withQuarantineNote,
|
|
4731
4738
|
trackDependencies,
|
|
4732
4739
|
verifyAnchor,
|
|
4733
4740
|
watchContracts
|
|
@@ -4765,6 +4772,24 @@ function registerSync(program2) {
|
|
|
4765
4772
|
let revalidated = 0;
|
|
4766
4773
|
let promoted = 0;
|
|
4767
4774
|
let autoApproved = 0;
|
|
4775
|
+
let quarantined = 0;
|
|
4776
|
+
const sensorHealth = new Map(
|
|
4777
|
+
assessSensorHealth(await loadSensorLedger(paths)).map((health) => [health.memory_id, health])
|
|
4778
|
+
);
|
|
4779
|
+
const quarantineCandidates = await loadMemoriesFromDir7(paths.memoriesDir);
|
|
4780
|
+
for (const { memory: memory2, filePath } of quarantineCandidates) {
|
|
4781
|
+
const sensor = memory2.frontmatter.sensor;
|
|
4782
|
+
const health = sensorHealth.get(memory2.frontmatter.id);
|
|
4783
|
+
if (!sensor || sensor.kind !== "shell" && sensor.kind !== "test" || sensor.severity !== "block" || !health?.quarantine_pending) continue;
|
|
4784
|
+
if (!dryRun) {
|
|
4785
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4786
|
+
await writeFile8(filePath, serializeMemory4({
|
|
4787
|
+
frontmatter: { ...memory2.frontmatter, sensor: { ...sensor, severity: "warn" } },
|
|
4788
|
+
body: withQuarantineNote(memory2.body, at, health.flap_count)
|
|
4789
|
+
}), "utf8");
|
|
4790
|
+
}
|
|
4791
|
+
quarantined++;
|
|
4792
|
+
}
|
|
4768
4793
|
if (opts.verify !== false) {
|
|
4769
4794
|
const memories = await loadMemoriesFromDir7(paths.memoriesDir);
|
|
4770
4795
|
for (const { memory: memory2, filePath } of memories) {
|
|
@@ -4885,13 +4910,17 @@ function registerSync(program2) {
|
|
|
4885
4910
|
for (const repair of repairs) log(ui.dim(`autopilot: ${repair.message}`));
|
|
4886
4911
|
}
|
|
4887
4912
|
const sinceReport = opts.since ? collectSinceChanges(root, opts.since) : null;
|
|
4913
|
+
const gateMissIds = await processGateMissWatch(root, paths, dryRun);
|
|
4914
|
+
if (gateMissIds.length > 0) {
|
|
4915
|
+
log(ui.yellow(`gate-miss: proposed ${gateMissIds.length} lesson(s): ${gateMissIds.join(", ")}`));
|
|
4916
|
+
}
|
|
4888
4917
|
const draftMemories = (await loadMemoriesFromDir7(paths.memoriesDir)).filter(
|
|
4889
4918
|
(m) => m.memory.frontmatter.status === "draft"
|
|
4890
4919
|
);
|
|
4891
4920
|
const draftCount = draftMemories.length;
|
|
4892
4921
|
const autoApprovedNote = autoApproved > 0 ? ` \xB7 ${autoApproved} auto-approved` : "";
|
|
4893
4922
|
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}` : ""}`
|
|
4923
|
+
`${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
4924
|
);
|
|
4896
4925
|
if (!opts.quiet && draftCount > 0) {
|
|
4897
4926
|
log(
|
|
@@ -5209,6 +5238,83 @@ Wait for **explicit confirmation** before acting.
|
|
|
5209
5238
|
}
|
|
5210
5239
|
});
|
|
5211
5240
|
}
|
|
5241
|
+
async function processGateMissWatch(root, paths, dryRun) {
|
|
5242
|
+
const headResult = spawnSync4("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" });
|
|
5243
|
+
if (headResult.status !== 0) return [];
|
|
5244
|
+
const head = headResult.stdout.trim();
|
|
5245
|
+
if (!head) return [];
|
|
5246
|
+
const stateFile = path15.join(paths.runtimeDir, "enforcement", "git-watch.json");
|
|
5247
|
+
let state = null;
|
|
5248
|
+
try {
|
|
5249
|
+
state = JSON.parse(await readFile7(stateFile, "utf8"));
|
|
5250
|
+
} catch {
|
|
5251
|
+
}
|
|
5252
|
+
const plan = planGitWatch(state, head);
|
|
5253
|
+
if (plan.action === "initialize") {
|
|
5254
|
+
if (!dryRun) {
|
|
5255
|
+
await mkdir8(path15.dirname(stateFile), { recursive: true });
|
|
5256
|
+
await writeFile8(stateFile, JSON.stringify(plan.next, null, 2) + "\n", "utf8");
|
|
5257
|
+
}
|
|
5258
|
+
return [];
|
|
5259
|
+
}
|
|
5260
|
+
if (plan.action === "idle") return [];
|
|
5261
|
+
const proposals = proposeGateMissDrafts(
|
|
5262
|
+
readGitCommitsRange(root, plan.range),
|
|
5263
|
+
existingGateMissShas(await loadMemoriesFromDir7(paths.memoriesDir)),
|
|
5264
|
+
gatePassedShas(await loadSensorLedger(paths))
|
|
5265
|
+
);
|
|
5266
|
+
const ids = [];
|
|
5267
|
+
for (const proposal of proposals) {
|
|
5268
|
+
const fm = {
|
|
5269
|
+
...buildFrontmatter3({
|
|
5270
|
+
type: "attempt",
|
|
5271
|
+
slug: proposal.slug,
|
|
5272
|
+
scope: "team",
|
|
5273
|
+
status: "proposed",
|
|
5274
|
+
tags: ["gate-miss", proposal.kind],
|
|
5275
|
+
paths: proposal.paths,
|
|
5276
|
+
topic: `gate-miss-${proposal.reverted_sha}`
|
|
5277
|
+
}),
|
|
5278
|
+
status: "proposed"
|
|
5279
|
+
};
|
|
5280
|
+
ids.push(fm.id);
|
|
5281
|
+
if (!dryRun) {
|
|
5282
|
+
await mkdir8(paths.teamDir, { recursive: true });
|
|
5283
|
+
await writeFile8(
|
|
5284
|
+
path15.join(paths.teamDir, `${fm.id}.md`),
|
|
5285
|
+
serializeMemory4({ frontmatter: fm, body: proposal.body }),
|
|
5286
|
+
"utf8"
|
|
5287
|
+
);
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
5290
|
+
if (!dryRun) {
|
|
5291
|
+
await mkdir8(path15.dirname(stateFile), { recursive: true });
|
|
5292
|
+
await writeFile8(stateFile, JSON.stringify(plan.next, null, 2) + "\n", "utf8");
|
|
5293
|
+
}
|
|
5294
|
+
return ids;
|
|
5295
|
+
}
|
|
5296
|
+
function readGitCommitsRange(root, range) {
|
|
5297
|
+
const result = spawnSync4(
|
|
5298
|
+
"git",
|
|
5299
|
+
["log", "--reverse", "--format=%x1e%H%x1f%s%x1f%b%x1f", "--name-only", range],
|
|
5300
|
+
{ cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }
|
|
5301
|
+
);
|
|
5302
|
+
if (result.status !== 0) return [];
|
|
5303
|
+
const commits = [];
|
|
5304
|
+
for (const block of result.stdout.split("").filter((part) => part.trim())) {
|
|
5305
|
+
const [shaRaw, subjectRaw, bodyRaw, filesRaw = ""] = block.split("");
|
|
5306
|
+
const sha = shaRaw?.trim();
|
|
5307
|
+
const subject = subjectRaw?.trim();
|
|
5308
|
+
if (!sha || !subject) continue;
|
|
5309
|
+
commits.push({
|
|
5310
|
+
sha,
|
|
5311
|
+
subject,
|
|
5312
|
+
body: bodyRaw?.trim() ?? "",
|
|
5313
|
+
files: filesRaw.split("\n").map((file) => file.trim()).filter(Boolean)
|
|
5314
|
+
});
|
|
5315
|
+
}
|
|
5316
|
+
return commits;
|
|
5317
|
+
}
|
|
5212
5318
|
function bridgeSummaryLine(body) {
|
|
5213
5319
|
const firstLine = body.split("\n").map((l) => l.replace(/^#+\s*/, "").trim()).find((l) => l.length > 0) ?? "";
|
|
5214
5320
|
const oneLine = firstLine.replace(/\s+/g, " ");
|
|
@@ -7207,16 +7313,34 @@ import { mkdir as mkdir12, writeFile as writeFile18 } from "fs/promises";
|
|
|
7207
7313
|
import path30 from "path";
|
|
7208
7314
|
import {
|
|
7209
7315
|
aggregateUsage,
|
|
7316
|
+
buildPreventionReceipt,
|
|
7317
|
+
renderPreventionReceipt,
|
|
7210
7318
|
findProjectRoot as findProjectRoot28,
|
|
7211
7319
|
loadMemoriesFromDir as loadMemoriesFromDir11,
|
|
7212
7320
|
loadUsageIndex as loadUsageIndex12,
|
|
7321
|
+
loadPreventionEvents as loadPreventionEvents2,
|
|
7213
7322
|
parseSince,
|
|
7214
7323
|
readUsageEvents,
|
|
7215
7324
|
resolveHaivePaths as resolveHaivePaths27,
|
|
7216
7325
|
usageLogSize
|
|
7217
7326
|
} from "@hivelore/core";
|
|
7218
7327
|
function registerStats(program2) {
|
|
7219
|
-
program2.command("stats").description("Show MCP tool-usage stats
|
|
7328
|
+
const stats = program2.command("stats").description("Show MCP tool-usage stats and prevention receipts.");
|
|
7329
|
+
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 () => {
|
|
7330
|
+
const opts = stats.opts();
|
|
7331
|
+
const root = findProjectRoot28(opts.dir);
|
|
7332
|
+
const paths = resolveHaivePaths27(root);
|
|
7333
|
+
const sinceRaw = stats.getOptionValueSource("since") === "default" ? "7d" : opts.since ?? "7d";
|
|
7334
|
+
const since = parseSince(sinceRaw) ?? new Date(Date.now() - 7 * 864e5);
|
|
7335
|
+
const [events, usage, memories] = await Promise.all([
|
|
7336
|
+
loadPreventionEvents2(paths),
|
|
7337
|
+
loadUsageIndex12(paths),
|
|
7338
|
+
existsSync33(paths.memoriesDir) ? loadMemoriesFromDir11(paths.memoriesDir) : Promise.resolve([])
|
|
7339
|
+
]);
|
|
7340
|
+
const receipt = buildPreventionReceipt(events, memories, usage, { since });
|
|
7341
|
+
console.log(opts.json ? JSON.stringify(receipt, null, 2) : renderPreventionReceipt(receipt));
|
|
7342
|
+
});
|
|
7343
|
+
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
7344
|
"--export-report <path>",
|
|
7221
7345
|
"write a JSON rollup (tools + briefing counts + heuristic ROI hints). Parent dirs are created if needed.",
|
|
7222
7346
|
void 0
|
|
@@ -7662,7 +7786,7 @@ import {
|
|
|
7662
7786
|
findProjectRoot as findProjectRoot31,
|
|
7663
7787
|
loadConfig as loadConfig7,
|
|
7664
7788
|
loadEvalHistory,
|
|
7665
|
-
loadPreventionEvents as
|
|
7789
|
+
loadPreventionEvents as loadPreventionEvents3,
|
|
7666
7790
|
loadUsageIndex as loadUsageIndex13,
|
|
7667
7791
|
overallScore,
|
|
7668
7792
|
resolveHaivePaths as resolveHaivePaths29,
|
|
@@ -7729,7 +7853,7 @@ function registerEval(program2) {
|
|
|
7729
7853
|
const authoredScore = resolvedSpec.authored > 0 && resolvedSpec.synthesized > 0 && (authoredRetrievalAgg || sensorAgg) ? overallScore(authoredRetrievalAgg, sensorAgg) : null;
|
|
7730
7854
|
const [usage, preventionEvents, config] = await Promise.all([
|
|
7731
7855
|
loadUsageIndex13(paths),
|
|
7732
|
-
|
|
7856
|
+
loadPreventionEvents3(paths),
|
|
7733
7857
|
loadConfig7(paths)
|
|
7734
7858
|
]);
|
|
7735
7859
|
const gatePrecision = computeGatePrecision(
|
|
@@ -8449,6 +8573,7 @@ import { execFileSync, execSync } from "child_process";
|
|
|
8449
8573
|
import "commander";
|
|
8450
8574
|
import {
|
|
8451
8575
|
codeMapPath as codeMapPath2,
|
|
8576
|
+
assessSensorHealth as assessSensorHealth2,
|
|
8452
8577
|
countSourceFilesOnDisk,
|
|
8453
8578
|
extractReferencedPaths,
|
|
8454
8579
|
sensorPatternBrittleness,
|
|
@@ -8459,6 +8584,7 @@ import {
|
|
|
8459
8584
|
loadCodeMap as loadCodeMap6,
|
|
8460
8585
|
loadConfig as loadConfig10,
|
|
8461
8586
|
loadMemoriesFromDirDetailed,
|
|
8587
|
+
loadSensorLedger as loadSensorLedger2,
|
|
8462
8588
|
loadUsageIndex as loadUsageIndex15,
|
|
8463
8589
|
readUsageEvents as readUsageEvents3,
|
|
8464
8590
|
resolveHaivePaths as resolveHaivePaths33
|
|
@@ -8654,6 +8780,18 @@ function registerDoctor(program2) {
|
|
|
8654
8780
|
});
|
|
8655
8781
|
}
|
|
8656
8782
|
const sensorMemories = memories.filter((m) => m.memory.frontmatter.sensor);
|
|
8783
|
+
const gateMissDrafts = memories.filter(
|
|
8784
|
+
(m) => m.memory.frontmatter.status === "proposed" && m.memory.frontmatter.tags.includes("gate-miss")
|
|
8785
|
+
);
|
|
8786
|
+
if (gateMissDrafts.length > 0) {
|
|
8787
|
+
findings.push({
|
|
8788
|
+
severity: "info",
|
|
8789
|
+
code: "gate-miss-drafts",
|
|
8790
|
+
section: "Corpus health",
|
|
8791
|
+
message: `${gateMissDrafts.length} proposed gate-miss lesson(s) await review: ${gateMissDrafts.slice(0, 5).map((m) => m.memory.frontmatter.id).join(", ")}.`,
|
|
8792
|
+
fix: "Review with `hivelore memory list --status proposed`."
|
|
8793
|
+
});
|
|
8794
|
+
}
|
|
8657
8795
|
const blockSensors = sensorMemories.filter(
|
|
8658
8796
|
(m) => m.memory.frontmatter.sensor?.severity === "block"
|
|
8659
8797
|
).length;
|
|
@@ -8694,6 +8832,26 @@ function registerDoctor(program2) {
|
|
|
8694
8832
|
fix: "Make the pattern discriminating (add an 'absent' companion), or demote: `hivelore sensors promote <id> --severity warn`"
|
|
8695
8833
|
});
|
|
8696
8834
|
}
|
|
8835
|
+
const sensorHealth = assessSensorHealth2(await loadSensorLedger2(paths));
|
|
8836
|
+
for (const health of sensorHealth.filter((h) => h.quarantine_pending)) {
|
|
8837
|
+
const last = health.flaps.at(-1);
|
|
8838
|
+
findings.push({
|
|
8839
|
+
severity: "warn",
|
|
8840
|
+
code: "sensor-flaky",
|
|
8841
|
+
section: "Protection",
|
|
8842
|
+
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}.`,
|
|
8843
|
+
fix: "Run `hivelore sync` to demote it, fix the oracle, then `hivelore sensors promote <id> --yes`."
|
|
8844
|
+
});
|
|
8845
|
+
}
|
|
8846
|
+
for (const health of sensorHealth.filter((h) => h.never_fired)) {
|
|
8847
|
+
findings.push({
|
|
8848
|
+
severity: "info",
|
|
8849
|
+
code: "sensor-never-fired",
|
|
8850
|
+
section: "Protection",
|
|
8851
|
+
message: `${health.memory_id} was evaluated ${health.evaluation_count} times across 30+ days and never fired \u2014 retirement candidate.`,
|
|
8852
|
+
fix: "Review the sensor manually; no automatic delete/retire command is provided."
|
|
8853
|
+
});
|
|
8854
|
+
}
|
|
8697
8855
|
const codeMap = await loadCodeMap6(paths);
|
|
8698
8856
|
if (!codeMap) {
|
|
8699
8857
|
findings.push({
|
|
@@ -8791,7 +8949,7 @@ function registerDoctor(program2) {
|
|
|
8791
8949
|
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `hivelore init` without --manual)."
|
|
8792
8950
|
});
|
|
8793
8951
|
}
|
|
8794
|
-
findings.push(...await collectInstallFindings(root, "0.
|
|
8952
|
+
findings.push(...await collectInstallFindings(root, "0.35.0"));
|
|
8795
8953
|
findings.push(...await collectToolchainFindings(root));
|
|
8796
8954
|
try {
|
|
8797
8955
|
const legacyRaw = execSync("haive-mcp --version", {
|
|
@@ -8799,7 +8957,7 @@ function registerDoctor(program2) {
|
|
|
8799
8957
|
timeout: 3e3,
|
|
8800
8958
|
stdio: ["ignore", "pipe", "ignore"]
|
|
8801
8959
|
}).trim();
|
|
8802
|
-
const cliVersion = "0.
|
|
8960
|
+
const cliVersion = "0.35.0";
|
|
8803
8961
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
8804
8962
|
findings.push({
|
|
8805
8963
|
severity: "warn",
|
|
@@ -9529,14 +9687,16 @@ function registerResolveProject(program2) {
|
|
|
9529
9687
|
}
|
|
9530
9688
|
|
|
9531
9689
|
// src/commands/enforce.ts
|
|
9532
|
-
import { execFile as
|
|
9690
|
+
import { execFile as execFile5, execFileSync as execFileSync2, spawn as spawn4 } from "child_process";
|
|
9533
9691
|
import { existsSync as existsSync41, statSync as statSync3 } from "fs";
|
|
9534
9692
|
import { chmod, mkdir as mkdir16, readFile as readFile18, readdir as readdir4, rm as rm2, writeFile as writeFile25 } from "fs/promises";
|
|
9535
9693
|
import path39 from "path";
|
|
9536
|
-
import { promisify as
|
|
9694
|
+
import { promisify as promisify5 } from "util";
|
|
9537
9695
|
import "commander";
|
|
9538
9696
|
import {
|
|
9539
9697
|
antiPatternGateParams as antiPatternGateParams2,
|
|
9698
|
+
appendSensorEvaluations,
|
|
9699
|
+
assessSensorHealth as assessSensorHealth3,
|
|
9540
9700
|
assessBootstrapState,
|
|
9541
9701
|
isSensorScannablePath,
|
|
9542
9702
|
findProjectRoot as findProjectRoot37,
|
|
@@ -9550,6 +9710,7 @@ import {
|
|
|
9550
9710
|
loadConfig as loadConfig12,
|
|
9551
9711
|
detectAgentContext,
|
|
9552
9712
|
loadMemoriesFromDir as loadMemoriesFromDir14,
|
|
9713
|
+
loadSensorLedger as loadSensorLedger3,
|
|
9553
9714
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
|
|
9554
9715
|
readRecentBriefingMarker,
|
|
9555
9716
|
recordPreventionHits,
|
|
@@ -9559,6 +9720,7 @@ import {
|
|
|
9559
9720
|
saveConfig as saveConfig3,
|
|
9560
9721
|
selectCommandSensors,
|
|
9561
9722
|
sensorTargetsFromDiff,
|
|
9723
|
+
sensorAppliesToPath as sensorAppliesToPath2,
|
|
9562
9724
|
SESSION_RECAP_TTL_MS,
|
|
9563
9725
|
verifyAnchor as verifyAnchor3,
|
|
9564
9726
|
writeBriefingMarker as writeBriefingMarker2
|
|
@@ -9761,8 +9923,52 @@ async function executeCommandSensors(specs, root) {
|
|
|
9761
9923
|
return runs;
|
|
9762
9924
|
}
|
|
9763
9925
|
|
|
9926
|
+
// src/utils/sensor-evaluations.ts
|
|
9927
|
+
import { execFile as execFile4 } from "child_process";
|
|
9928
|
+
import { promisify as promisify4 } from "util";
|
|
9929
|
+
import {
|
|
9930
|
+
computeScopeHash,
|
|
9931
|
+
sensorAppliesToPath
|
|
9932
|
+
} from "@hivelore/core";
|
|
9933
|
+
var exec3 = promisify4(execFile4);
|
|
9934
|
+
async function gitHeadSha(root) {
|
|
9935
|
+
try {
|
|
9936
|
+
const { stdout } = await exec3("git", ["rev-parse", "HEAD"], { cwd: root });
|
|
9937
|
+
return stdout.trim();
|
|
9938
|
+
} catch {
|
|
9939
|
+
return "";
|
|
9940
|
+
}
|
|
9941
|
+
}
|
|
9942
|
+
async function trackedFiles(root) {
|
|
9943
|
+
try {
|
|
9944
|
+
const { stdout } = await exec3("git", ["ls-files", "-co", "--exclude-standard"], { cwd: root });
|
|
9945
|
+
return stdout.split("\n").map((f) => f.trim()).filter(Boolean);
|
|
9946
|
+
} catch {
|
|
9947
|
+
return [];
|
|
9948
|
+
}
|
|
9949
|
+
}
|
|
9950
|
+
async function commandScopeHash(root, spec) {
|
|
9951
|
+
if (spec.paths.length === 0) return "";
|
|
9952
|
+
const sensor = { kind: spec.kind, paths: spec.paths };
|
|
9953
|
+
const files = (await trackedFiles(root)).filter((file) => sensorAppliesToPath(sensor, [], file));
|
|
9954
|
+
return computeScopeHash(root, files);
|
|
9955
|
+
}
|
|
9956
|
+
function evaluation(base, command) {
|
|
9957
|
+
return {
|
|
9958
|
+
at: base.at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
9959
|
+
memory_id: base.memory_id,
|
|
9960
|
+
kind: base.kind,
|
|
9961
|
+
stage: base.stage,
|
|
9962
|
+
head_sha: base.head_sha,
|
|
9963
|
+
scope_hash: base.scope_hash,
|
|
9964
|
+
outcome: base.outcome,
|
|
9965
|
+
...command?.exit_code !== null ? { exit_code: command?.exit_code } : {},
|
|
9966
|
+
...command ? { duration_ms: command.duration_ms } : {}
|
|
9967
|
+
};
|
|
9968
|
+
}
|
|
9969
|
+
|
|
9764
9970
|
// src/commands/enforce.ts
|
|
9765
|
-
var execFileAsync2 =
|
|
9971
|
+
var execFileAsync2 = promisify5(execFile5);
|
|
9766
9972
|
var MAX_STDIN_BYTES2 = 256 * 1024;
|
|
9767
9973
|
var ENFORCE_HOOK_MARKER = "# Hivelore enforcement hook";
|
|
9768
9974
|
function registerEnforce(program2) {
|
|
@@ -10451,7 +10657,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
10451
10657
|
findings: [{ severity: "info", code: "enforcement-off", message: "Hivelore enforcement is disabled." }]
|
|
10452
10658
|
});
|
|
10453
10659
|
}
|
|
10454
|
-
findings.push(...await inspectIntegrationVersions(root, "0.
|
|
10660
|
+
findings.push(...await inspectIntegrationVersions(root, "0.35.0"));
|
|
10455
10661
|
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
10456
10662
|
const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
|
|
10457
10663
|
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent Hivelore briefing marker exists." } : {
|
|
@@ -10531,7 +10737,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
10531
10737
|
}];
|
|
10532
10738
|
}
|
|
10533
10739
|
const hasErrors = effectiveFindings.some((f) => f.severity === "error");
|
|
10534
|
-
|
|
10740
|
+
const report = withCategories({
|
|
10535
10741
|
root,
|
|
10536
10742
|
initialized,
|
|
10537
10743
|
mode,
|
|
@@ -10540,6 +10746,18 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
10540
10746
|
should_block: mode === "strict" && hasErrors,
|
|
10541
10747
|
findings: effectiveFindings
|
|
10542
10748
|
});
|
|
10749
|
+
if (!report.should_block && (stage === "pre-commit" || stage === "ci")) {
|
|
10750
|
+
const headSha = await gitHeadSha(root);
|
|
10751
|
+
await appendSensorEvaluations(paths, [evaluation({
|
|
10752
|
+
memory_id: "__gate__",
|
|
10753
|
+
kind: "shell",
|
|
10754
|
+
stage,
|
|
10755
|
+
head_sha: headSha,
|
|
10756
|
+
scope_hash: "",
|
|
10757
|
+
outcome: "silent"
|
|
10758
|
+
})]);
|
|
10759
|
+
}
|
|
10760
|
+
return report;
|
|
10543
10761
|
}
|
|
10544
10762
|
function withCategories(report) {
|
|
10545
10763
|
return {
|
|
@@ -10695,7 +10913,7 @@ async function runPrecommitPolicy(paths, gate, stage) {
|
|
|
10695
10913
|
anchored_blocks,
|
|
10696
10914
|
semantic: true
|
|
10697
10915
|
}, { paths });
|
|
10698
|
-
const sensorFindings = await runSensorGate(paths, snapshot.diff);
|
|
10916
|
+
const sensorFindings = await runSensorGate(paths, snapshot.diff, stage);
|
|
10699
10917
|
const reviewWarnings = result.warnings.filter(
|
|
10700
10918
|
(w) => w.level === "review" && !w.reasons.includes("sensor")
|
|
10701
10919
|
);
|
|
@@ -10758,7 +10976,7 @@ async function runPrecommitPolicy(paths, gate, stage) {
|
|
|
10758
10976
|
...sensorFindings
|
|
10759
10977
|
];
|
|
10760
10978
|
}
|
|
10761
|
-
async function runSensorGate(paths, diff) {
|
|
10979
|
+
async function runSensorGate(paths, diff, stage) {
|
|
10762
10980
|
if (!diff || !existsSync41(paths.memoriesDir)) return [];
|
|
10763
10981
|
try {
|
|
10764
10982
|
const loaded = await loadMemoriesFromDir14(paths.memoriesDir);
|
|
@@ -10769,8 +10987,22 @@ async function runSensorGate(paths, diff) {
|
|
|
10769
10987
|
const findings = [];
|
|
10770
10988
|
const seen = /* @__PURE__ */ new Set();
|
|
10771
10989
|
const firedIds = /* @__PURE__ */ new Set();
|
|
10990
|
+
const ledgerRows = [];
|
|
10991
|
+
const headSha = await gitHeadSha(paths.root);
|
|
10772
10992
|
const regexSensorMemories = scannable.filter((m) => m.frontmatter.sensor.kind === "regex");
|
|
10773
10993
|
const hits = regexSensorMemories.length > 0 ? runSensors(regexSensorMemories, targets) : [];
|
|
10994
|
+
for (const memory2 of regexSensorMemories) {
|
|
10995
|
+
const sensor = memory2.frontmatter.sensor;
|
|
10996
|
+
if (!targets.some((target) => sensorAppliesToPath2(sensor, memory2.frontmatter.anchor.paths, target.path))) continue;
|
|
10997
|
+
ledgerRows.push(evaluation({
|
|
10998
|
+
memory_id: memory2.frontmatter.id,
|
|
10999
|
+
kind: "regex",
|
|
11000
|
+
stage,
|
|
11001
|
+
head_sha: headSha,
|
|
11002
|
+
scope_hash: "",
|
|
11003
|
+
outcome: hits.some((hit) => hit.memory_id === memory2.frontmatter.id) ? "fired" : "silent"
|
|
11004
|
+
}));
|
|
11005
|
+
}
|
|
10774
11006
|
for (const hit of hits) {
|
|
10775
11007
|
if (seen.has(hit.memory_id)) continue;
|
|
10776
11008
|
seen.add(hit.memory_id);
|
|
@@ -10782,7 +11014,8 @@ async function runSensorGate(paths, diff) {
|
|
|
10782
11014
|
code: "sensor-block",
|
|
10783
11015
|
message: `Block sensor fired \u2014 ${hit.memory_id}: ${hit.message}${where}`,
|
|
10784
11016
|
fix: "Remove the flagged pattern, or run `hivelore sensors check` to inspect the match.",
|
|
10785
|
-
impact: 45
|
|
11017
|
+
impact: 45,
|
|
11018
|
+
memory_ids: [hit.memory_id]
|
|
10786
11019
|
});
|
|
10787
11020
|
} else {
|
|
10788
11021
|
findings.push({
|
|
@@ -10790,7 +11023,8 @@ async function runSensorGate(paths, diff) {
|
|
|
10790
11023
|
code: "sensor-warn",
|
|
10791
11024
|
message: `Sensor flagged ${hit.memory_id}: ${hit.message}${where}`,
|
|
10792
11025
|
fix: "Review the flagged line; `hivelore sensors check` shows the matched code.",
|
|
10793
|
-
impact: 5
|
|
11026
|
+
impact: 5,
|
|
11027
|
+
memory_ids: [hit.memory_id]
|
|
10794
11028
|
});
|
|
10795
11029
|
}
|
|
10796
11030
|
}
|
|
@@ -10798,7 +11032,34 @@ async function runSensorGate(paths, diff) {
|
|
|
10798
11032
|
if (config?.enforcement?.runCommandSensors === true) {
|
|
10799
11033
|
const changedPaths = targets.map((t) => t.path).filter(Boolean);
|
|
10800
11034
|
const specs = selectCommandSensors(scannable, changedPaths).filter((sp) => !seen.has(sp.memory_id));
|
|
10801
|
-
|
|
11035
|
+
const runs = await executeCommandSensors(specs, paths.root);
|
|
11036
|
+
for (const run of runs) {
|
|
11037
|
+
const spec = specs.find((candidate) => candidate.memory_id === run.memory_id);
|
|
11038
|
+
ledgerRows.push(evaluation({
|
|
11039
|
+
memory_id: run.memory_id,
|
|
11040
|
+
kind: run.kind,
|
|
11041
|
+
stage,
|
|
11042
|
+
head_sha: headSha,
|
|
11043
|
+
scope_hash: await commandScopeHash(paths.root, spec),
|
|
11044
|
+
outcome: run.status === "failed" ? "fired" : run.status === "passed" ? "silent" : "unrunnable"
|
|
11045
|
+
}, { exit_code: run.exit_code, duration_ms: run.duration_ms }));
|
|
11046
|
+
}
|
|
11047
|
+
const prior = await loadSensorLedger3(paths);
|
|
11048
|
+
const health = new Map(assessSensorHealth3([...prior, ...ledgerRows]).map((h) => [h.memory_id, h]));
|
|
11049
|
+
for (const run of runs) {
|
|
11050
|
+
const sensorHealth = health.get(run.memory_id);
|
|
11051
|
+
const quarantined = sensorHealth?.quarantine_pending === true;
|
|
11052
|
+
if (quarantined && run.severity === "block") {
|
|
11053
|
+
const last = sensorHealth.flaps.at(-1);
|
|
11054
|
+
findings.push({
|
|
11055
|
+
severity: "warn",
|
|
11056
|
+
code: "sensor-flaky",
|
|
11057
|
+
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}.`,
|
|
11058
|
+
fix: "Run `hivelore sync`, fix the flaky oracle, then re-promote with `hivelore sensors promote <id> --yes`.",
|
|
11059
|
+
impact: 5,
|
|
11060
|
+
memory_ids: [run.memory_id]
|
|
11061
|
+
});
|
|
11062
|
+
}
|
|
10802
11063
|
if (run.status === "passed") continue;
|
|
10803
11064
|
seen.add(run.memory_id);
|
|
10804
11065
|
if (run.status === "unrunnable") {
|
|
@@ -10815,14 +11076,15 @@ ${run.output_tail}` : ""),
|
|
|
10815
11076
|
firedIds.add(run.memory_id);
|
|
10816
11077
|
const outputBlock = run.output_tail ? `
|
|
10817
11078
|
${run.output_tail}` : "";
|
|
10818
|
-
if (run.severity === "block") {
|
|
11079
|
+
if (run.severity === "block" && !quarantined) {
|
|
10819
11080
|
findings.push({
|
|
10820
11081
|
severity: "error",
|
|
10821
11082
|
code: "sensor-block",
|
|
10822
11083
|
message: `Block ${run.kind} sensor fired \u2014 ${run.memory_id}: ${run.message}
|
|
10823
11084
|
command: ${run.command} (exit ${run.exit_code}, ${run.duration_ms}ms)${outputBlock}`,
|
|
10824
11085
|
fix: "Fix the behaviour the command checks, or run `hivelore sensors check --commands` to inspect it.",
|
|
10825
|
-
impact: 45
|
|
11086
|
+
impact: 45,
|
|
11087
|
+
memory_ids: [run.memory_id]
|
|
10826
11088
|
});
|
|
10827
11089
|
} else {
|
|
10828
11090
|
findings.push({
|
|
@@ -10830,13 +11092,19 @@ command: ${run.command} (exit ${run.exit_code}, ${run.duration_ms}ms)${outputBlo
|
|
|
10830
11092
|
code: "sensor-warn",
|
|
10831
11093
|
message: `${run.kind} sensor flagged ${run.memory_id}: ${run.message} (exit ${run.exit_code})${outputBlock}`,
|
|
10832
11094
|
fix: "Review the failing command; `hivelore sensors check --commands` re-runs it.",
|
|
10833
|
-
impact: 5
|
|
11095
|
+
impact: 5,
|
|
11096
|
+
memory_ids: [run.memory_id]
|
|
10834
11097
|
});
|
|
10835
11098
|
}
|
|
10836
11099
|
}
|
|
10837
11100
|
}
|
|
11101
|
+
await appendSensorEvaluations(paths, ledgerRows);
|
|
10838
11102
|
if (firedIds.size > 0) {
|
|
10839
|
-
|
|
11103
|
+
const details = Object.fromEntries([...firedIds].map((id) => {
|
|
11104
|
+
const row = ledgerRows.find((entry) => entry.memory_id === id && entry.outcome === "fired");
|
|
11105
|
+
return [id, { kind: row?.kind ?? "regex", stage, ...row?.exit_code !== void 0 ? { exit_code: row.exit_code } : {} }];
|
|
11106
|
+
}));
|
|
11107
|
+
await recordPreventionHits(paths, [...firedIds], "sensor", /* @__PURE__ */ new Date(), details).catch(() => {
|
|
10840
11108
|
});
|
|
10841
11109
|
}
|
|
10842
11110
|
return findings;
|
|
@@ -11442,11 +11710,27 @@ ${hook.body}`, "utf8");
|
|
|
11442
11710
|
async function installCiEnforcement(root) {
|
|
11443
11711
|
const workflowPath = path39.join(root, ".github", "workflows", "haive-enforcement.yml");
|
|
11444
11712
|
await mkdir16(path39.dirname(workflowPath), { recursive: true });
|
|
11713
|
+
const workflow = renderCiEnforcementWorkflow();
|
|
11445
11714
|
if (existsSync41(workflowPath)) {
|
|
11446
|
-
|
|
11715
|
+
const existing = await readFile18(workflowPath, "utf8");
|
|
11716
|
+
const start = "# haive:enforcement-workflow:start";
|
|
11717
|
+
const end = "# haive:enforcement-workflow:end";
|
|
11718
|
+
const startAt = existing.indexOf(start);
|
|
11719
|
+
const endAt = existing.indexOf(end);
|
|
11720
|
+
if (startAt >= 0 && endAt > startAt) {
|
|
11721
|
+
await writeFile25(workflowPath, existing.slice(0, startAt) + workflow + existing.slice(endAt + end.length), "utf8");
|
|
11722
|
+
ui.success(`Updated ${path39.relative(root, workflowPath)} managed block`);
|
|
11723
|
+
} else {
|
|
11724
|
+
ui.info("GitHub Actions enforcement workflow already exists without Hivelore markers \u2014 preserved");
|
|
11725
|
+
}
|
|
11447
11726
|
return;
|
|
11448
11727
|
}
|
|
11449
|
-
await writeFile25(workflowPath,
|
|
11728
|
+
await writeFile25(workflowPath, workflow, "utf8");
|
|
11729
|
+
ui.success(`Created ${path39.relative(root, workflowPath)}`);
|
|
11730
|
+
}
|
|
11731
|
+
function renderCiEnforcementWorkflow() {
|
|
11732
|
+
return `# haive:enforcement-workflow:start
|
|
11733
|
+
name: haive-enforcement
|
|
11450
11734
|
|
|
11451
11735
|
on:
|
|
11452
11736
|
pull_request:
|
|
@@ -11458,6 +11742,7 @@ jobs:
|
|
|
11458
11742
|
runs-on: ubuntu-latest
|
|
11459
11743
|
permissions:
|
|
11460
11744
|
contents: read
|
|
11745
|
+
pull-requests: write
|
|
11461
11746
|
steps:
|
|
11462
11747
|
- uses: actions/checkout@v4
|
|
11463
11748
|
with:
|
|
@@ -11468,12 +11753,51 @@ jobs:
|
|
|
11468
11753
|
- name: Install Hivelore
|
|
11469
11754
|
run: npm install -g @hivelore/cli
|
|
11470
11755
|
- name: Enforce Hivelore policy
|
|
11756
|
+
id: gate
|
|
11471
11757
|
env:
|
|
11472
11758
|
HAIVE_BASE_SHA: \${{ github.event.pull_request.base.sha || github.event.before }}
|
|
11473
11759
|
HAIVE_HEAD_SHA: \${{ github.event.pull_request.head.sha || github.sha }}
|
|
11474
|
-
run:
|
|
11475
|
-
|
|
11476
|
-
|
|
11760
|
+
run: |
|
|
11761
|
+
set +e
|
|
11762
|
+
hivelore enforce ci --json > "$RUNNER_TEMP/haive-gate.json"
|
|
11763
|
+
echo "exit_code=$?" >> "$GITHUB_OUTPUT"
|
|
11764
|
+
exit 0
|
|
11765
|
+
- name: Upsert prevention receipt
|
|
11766
|
+
if: always() && github.event_name == 'pull_request'
|
|
11767
|
+
env:
|
|
11768
|
+
GH_TOKEN: \${{ github.token }}
|
|
11769
|
+
PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
11770
|
+
run: |
|
|
11771
|
+
if [ -z "\${GH_TOKEN:-}" ] || ! command -v gh >/dev/null 2>&1; then exit 0; fi
|
|
11772
|
+
receipt="$(hivelore stats receipt --since 7d --json 2>/dev/null)" || exit 0
|
|
11773
|
+
gate="$(cat "$RUNNER_TEMP/haive-gate.json" 2>/dev/null)" || gate='{"findings":[]}'
|
|
11774
|
+
body="$(jq -nr --arg marker '<!-- haive:prevention-receipt -->' --argjson receipt "$receipt" --argjson gate "$gate" '
|
|
11775
|
+
$marker + "
|
|
11776
|
+
## Hivelore prevention receipt
|
|
11777
|
+
|
|
11778
|
+
" +
|
|
11779
|
+
(([$gate.findings[]? | select(.code == "sensor-block" or .code == "sensor-warn") |
|
|
11780
|
+
"- **" + (.memory_ids[0] // "sensor") + "** \u2014 " + .message] | if length == 0 then
|
|
11781
|
+
"No documented sensor fired on this PR." else "### Fired on this PR
|
|
11782
|
+
" + join("
|
|
11783
|
+
") end)) +
|
|
11784
|
+
"
|
|
11785
|
+
|
|
11786
|
+
Weekly total: **" + ($receipt.total|tostring) + "** refused; previous window: **" +
|
|
11787
|
+
($receipt.previous_total|tostring) + "**."
|
|
11788
|
+
')" || exit 0
|
|
11789
|
+
comments="$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" --paginate 2>/dev/null)" || exit 0
|
|
11790
|
+
comment_id="$(printf '%s' "$comments" | jq -r '.[] | select(.body | contains("<!-- haive:prevention-receipt -->")) | .id' | head -1)"
|
|
11791
|
+
if [ -n "$comment_id" ]; then
|
|
11792
|
+
gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$comment_id" -f body="$body" >/dev/null 2>&1 || true
|
|
11793
|
+
else
|
|
11794
|
+
gh api --method POST "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" -f body="$body" >/dev/null 2>&1 || true
|
|
11795
|
+
fi
|
|
11796
|
+
- name: Fail when enforcement blocked
|
|
11797
|
+
if: steps.gate.outputs.exit_code != '0'
|
|
11798
|
+
run: exit \${{ steps.gate.outputs.exit_code }}
|
|
11799
|
+
# haive:enforcement-workflow:end
|
|
11800
|
+
`;
|
|
11477
11801
|
}
|
|
11478
11802
|
function printReport(report, json, explain = false) {
|
|
11479
11803
|
if (json) {
|
|
@@ -11658,9 +11982,9 @@ import { readFile as readFile19, writeFile as writeFile26 } from "fs/promises";
|
|
|
11658
11982
|
import path40 from "path";
|
|
11659
11983
|
import "commander";
|
|
11660
11984
|
import { findProjectRoot as findProjectRoot38 } from "@hivelore/core";
|
|
11661
|
-
import { execFile as
|
|
11662
|
-
import { promisify as
|
|
11663
|
-
var
|
|
11985
|
+
import { execFile as execFile6 } from "child_process";
|
|
11986
|
+
import { promisify as promisify6 } from "util";
|
|
11987
|
+
var exec4 = promisify6(execFile6);
|
|
11664
11988
|
var VERSION_FILES2 = [
|
|
11665
11989
|
"package.json",
|
|
11666
11990
|
"packages/core/package.json",
|
|
@@ -11737,24 +12061,24 @@ ${heading}
|
|
|
11737
12061
|
return;
|
|
11738
12062
|
}
|
|
11739
12063
|
}
|
|
11740
|
-
const dirty = (await
|
|
12064
|
+
const dirty = (await exec4("git", ["status", "--porcelain"], { cwd: root })).stdout.trim();
|
|
11741
12065
|
if (dirty.length > 0) {
|
|
11742
12066
|
ui.error("Working tree is not clean \u2014 commit the bump before tagging.");
|
|
11743
12067
|
process.exitCode = 1;
|
|
11744
12068
|
return;
|
|
11745
12069
|
}
|
|
11746
12070
|
const tag = `v${version}`;
|
|
11747
|
-
const existing = (await
|
|
12071
|
+
const existing = (await exec4("git", ["tag", "--list", tag], { cwd: root })).stdout.trim();
|
|
11748
12072
|
if (existing) {
|
|
11749
12073
|
ui.error(`Tag ${tag} already exists \u2014 bump the version first.`);
|
|
11750
12074
|
process.exitCode = 1;
|
|
11751
12075
|
return;
|
|
11752
12076
|
}
|
|
11753
|
-
await
|
|
12077
|
+
await exec4("git", ["tag", tag], { cwd: root });
|
|
11754
12078
|
ui.success(`Created ${tag} at HEAD.`);
|
|
11755
12079
|
if (opts.push !== false) {
|
|
11756
|
-
await
|
|
11757
|
-
await
|
|
12080
|
+
await exec4("git", ["push"], { cwd: root });
|
|
12081
|
+
await exec4("git", ["push", "origin", tag], { cwd: root });
|
|
11758
12082
|
ui.success(`Pushed branch and ${tag}.`);
|
|
11759
12083
|
ui.info("Next: `hivelore enforce finish --wait` (polls CI), then publish via `pnpm run publish:all` (human step).");
|
|
11760
12084
|
}
|
|
@@ -11774,18 +12098,21 @@ function registerRun(program2) {
|
|
|
11774
12098
|
}
|
|
11775
12099
|
|
|
11776
12100
|
// src/commands/sensors.ts
|
|
11777
|
-
import { execFile as
|
|
12101
|
+
import { execFile as execFile7 } from "child_process";
|
|
11778
12102
|
import { existsSync as existsSync43 } from "fs";
|
|
11779
12103
|
import { chmod as chmod2, mkdir as mkdir17, readFile as readFile20, writeFile as writeFile27 } from "fs/promises";
|
|
11780
12104
|
import path41 from "path";
|
|
11781
|
-
import { promisify as
|
|
12105
|
+
import { promisify as promisify7 } from "util";
|
|
11782
12106
|
import "commander";
|
|
11783
12107
|
import {
|
|
11784
12108
|
extractSensorExamples,
|
|
12109
|
+
appendSensorEvaluations as appendSensorEvaluations2,
|
|
12110
|
+
assessSensorHealth as assessSensorHealth4,
|
|
11785
12111
|
findProjectRoot as findProjectRoot39,
|
|
11786
12112
|
isRetiredMemory as isRetiredMemory3,
|
|
11787
12113
|
judgeProposedSensor,
|
|
11788
12114
|
loadConfig as loadConfig13,
|
|
12115
|
+
loadSensorLedger as loadSensorLedger4,
|
|
11789
12116
|
loadMemoriesFromDir as loadMemoriesFromDir15,
|
|
11790
12117
|
recordPreventionHits as recordPreventionHits2,
|
|
11791
12118
|
resolveHaivePaths as resolveHaivePaths36,
|
|
@@ -11793,10 +12120,12 @@ import {
|
|
|
11793
12120
|
selectCommandSensors as selectCommandSensors2,
|
|
11794
12121
|
sensorPatternBrittleness as sensorPatternBrittleness2,
|
|
11795
12122
|
sensorSelfCheck as sensorSelfCheck2,
|
|
12123
|
+
sensorAppliesToPath as sensorAppliesToPath3,
|
|
11796
12124
|
scannableSensorTargets,
|
|
11797
|
-
serializeMemory as serializeMemory15
|
|
12125
|
+
serializeMemory as serializeMemory15,
|
|
12126
|
+
withoutQuarantineNote
|
|
11798
12127
|
} from "@hivelore/core";
|
|
11799
|
-
var
|
|
12128
|
+
var exec5 = promisify7(execFile7);
|
|
11800
12129
|
function registerSensors(program2) {
|
|
11801
12130
|
const sensors = program2.command("sensors").description("Operate executable sensors derived from Hivelore memories");
|
|
11802
12131
|
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 +12172,44 @@ function registerSensors(program2) {
|
|
|
11843
12172
|
const commandHits = [];
|
|
11844
12173
|
const commandUnrunnable = [];
|
|
11845
12174
|
const commandSkipped = [];
|
|
12175
|
+
const ledgerRows = [];
|
|
12176
|
+
const headSha = await gitHeadSha(root);
|
|
12177
|
+
for (const memory2 of memories) {
|
|
12178
|
+
const sensor = memory2.frontmatter.sensor;
|
|
12179
|
+
if (!sensor || sensor.kind !== "regex") continue;
|
|
12180
|
+
const anchors = memory2.frontmatter.anchor.paths;
|
|
12181
|
+
const applicable = targets.some((target) => sensorAppliesToPath3(sensor, anchors, target.path));
|
|
12182
|
+
if (!applicable) continue;
|
|
12183
|
+
ledgerRows.push(evaluation({
|
|
12184
|
+
memory_id: memory2.frontmatter.id,
|
|
12185
|
+
kind: "regex",
|
|
12186
|
+
stage: "manual",
|
|
12187
|
+
head_sha: headSha,
|
|
12188
|
+
scope_hash: "",
|
|
12189
|
+
outcome: hits.some((hit) => hit.memory_id === memory2.frontmatter.id) ? "fired" : "silent"
|
|
12190
|
+
}));
|
|
12191
|
+
}
|
|
11846
12192
|
if (commandSpecs.length > 0 && runCommands) {
|
|
11847
|
-
|
|
12193
|
+
const runs = await executeCommandSensors(commandSpecs, root);
|
|
12194
|
+
for (const run of runs) {
|
|
12195
|
+
const spec = commandSpecs.find((candidate) => candidate.memory_id === run.memory_id);
|
|
12196
|
+
ledgerRows.push(evaluation({
|
|
12197
|
+
memory_id: run.memory_id,
|
|
12198
|
+
kind: run.kind,
|
|
12199
|
+
stage: "manual",
|
|
12200
|
+
head_sha: headSha,
|
|
12201
|
+
scope_hash: await commandScopeHash(root, spec),
|
|
12202
|
+
outcome: run.status === "failed" ? "fired" : run.status === "passed" ? "silent" : "unrunnable"
|
|
12203
|
+
}, { exit_code: run.exit_code, duration_ms: run.duration_ms }));
|
|
12204
|
+
}
|
|
12205
|
+
const prior = await loadSensorLedger4(paths);
|
|
12206
|
+
const health = new Map(assessSensorHealth4([...prior, ...ledgerRows]).map((h) => [h.memory_id, h]));
|
|
12207
|
+
for (const run of runs) {
|
|
12208
|
+
const quarantined = health.get(run.memory_id)?.quarantine_pending === true;
|
|
11848
12209
|
if (run.status === "failed") {
|
|
11849
12210
|
commandHits.push({
|
|
11850
12211
|
memory_id: run.memory_id,
|
|
11851
|
-
severity: run.severity,
|
|
12212
|
+
severity: quarantined ? "warn" : run.severity,
|
|
11852
12213
|
message: run.message,
|
|
11853
12214
|
matched_line: `command failed (exit ${run.exit_code}, ${run.duration_ms}ms): ${run.command}`,
|
|
11854
12215
|
...run.output_tail ? { output_tail: run.output_tail } : {}
|
|
@@ -11860,8 +12221,17 @@ function registerSensors(program2) {
|
|
|
11860
12221
|
} else if (commandSpecs.length > 0) {
|
|
11861
12222
|
for (const spec of commandSpecs) commandSkipped.push(spec.memory_id);
|
|
11862
12223
|
}
|
|
12224
|
+
await appendSensorEvaluations2(paths, ledgerRows);
|
|
11863
12225
|
const firedIds = [...new Set([...hits, ...commandHits].map((hit) => hit.memory_id))];
|
|
11864
|
-
|
|
12226
|
+
const preventionDetails = Object.fromEntries([
|
|
12227
|
+
...hits.map((hit) => [hit.memory_id, { kind: "regex", stage: "manual" }]),
|
|
12228
|
+
...commandHits.map((hit) => [hit.memory_id, {
|
|
12229
|
+
kind: commandSpecs.find((spec) => spec.memory_id === hit.memory_id)?.kind ?? "shell",
|
|
12230
|
+
stage: "manual",
|
|
12231
|
+
exit_code: Number(/exit (\d+)/.exec(hit.matched_line)?.[1] ?? 1)
|
|
12232
|
+
}])
|
|
12233
|
+
]);
|
|
12234
|
+
await recordPreventionHits2(paths, firedIds, "sensor", /* @__PURE__ */ new Date(), preventionDetails);
|
|
11865
12235
|
const output = {
|
|
11866
12236
|
scanned: memories.length,
|
|
11867
12237
|
hits: hits.map((hit) => ({
|
|
@@ -11968,7 +12338,7 @@ function registerSensors(program2) {
|
|
|
11968
12338
|
...found.memory.frontmatter,
|
|
11969
12339
|
sensor: { ...sensor, severity }
|
|
11970
12340
|
},
|
|
11971
|
-
body: found.memory.body
|
|
12341
|
+
body: severity === "block" ? withoutQuarantineNote(found.memory.body) : found.memory.body
|
|
11972
12342
|
};
|
|
11973
12343
|
await writeFile27(found.filePath, serializeMemory15(next), "utf8");
|
|
11974
12344
|
ui.success(`Updated ${id}: sensor severity=${severity}`);
|
|
@@ -11985,7 +12355,7 @@ function registerSensors(program2) {
|
|
|
11985
12355
|
return;
|
|
11986
12356
|
}
|
|
11987
12357
|
const root2 = findProjectRoot39(opts.dir);
|
|
11988
|
-
const { proposeSensor } = await import("./server-
|
|
12358
|
+
const { proposeSensor } = await import("./server-2FUYM6KI.js");
|
|
11989
12359
|
const out = await proposeSensor(
|
|
11990
12360
|
{
|
|
11991
12361
|
memory_id: id,
|
|
@@ -12132,7 +12502,7 @@ async function runnableSensorMemories(paths, regexOnly = true) {
|
|
|
12132
12502
|
}
|
|
12133
12503
|
async function stagedDiff(root) {
|
|
12134
12504
|
try {
|
|
12135
|
-
const { stdout } = await
|
|
12505
|
+
const { stdout } = await exec5("git", ["diff", "--cached"], { cwd: root });
|
|
12136
12506
|
return stdout;
|
|
12137
12507
|
} catch (err) {
|
|
12138
12508
|
throw new Error(`git diff --cached failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -12372,7 +12742,7 @@ import {
|
|
|
12372
12742
|
findProjectRoot as findProjectRoot41,
|
|
12373
12743
|
loadConfig as loadConfig14,
|
|
12374
12744
|
loadMemoriesFromDir as loadMemoriesFromDir17,
|
|
12375
|
-
loadPreventionEvents as
|
|
12745
|
+
loadPreventionEvents as loadPreventionEvents4,
|
|
12376
12746
|
loadUsageIndex as loadUsageIndex16,
|
|
12377
12747
|
resolveHaivePaths as resolveHaivePaths38
|
|
12378
12748
|
} from "@hivelore/core";
|
|
@@ -12389,7 +12759,7 @@ function registerDashboard(program2) {
|
|
|
12389
12759
|
}
|
|
12390
12760
|
const memories = existsSync45(paths.memoriesDir) ? await loadMemoriesFromDir17(paths.memoriesDir) : [];
|
|
12391
12761
|
const usage = await loadUsageIndex16(paths);
|
|
12392
|
-
const preventionEvents = await
|
|
12762
|
+
const preventionEvents = await loadPreventionEvents4(paths);
|
|
12393
12763
|
const config = await loadConfig14(paths);
|
|
12394
12764
|
const top = Math.max(1, Number.parseInt(opts.top ?? "10", 10) || 10);
|
|
12395
12765
|
const dormantDays = opts.dormantDays ? Number.parseInt(opts.dormantDays, 10) : void 0;
|
|
@@ -12511,14 +12881,14 @@ function warnNum(n) {
|
|
|
12511
12881
|
}
|
|
12512
12882
|
|
|
12513
12883
|
// src/commands/dev-link.ts
|
|
12514
|
-
import { execFile as
|
|
12884
|
+
import { execFile as execFile8 } from "child_process";
|
|
12515
12885
|
import { cp, readFile as readFile22 } from "fs/promises";
|
|
12516
12886
|
import { existsSync as existsSync46 } from "fs";
|
|
12517
12887
|
import path43 from "path";
|
|
12518
|
-
import { promisify as
|
|
12888
|
+
import { promisify as promisify8 } from "util";
|
|
12519
12889
|
import "commander";
|
|
12520
12890
|
import { findProjectRoot as findProjectRoot42 } from "@hivelore/core";
|
|
12521
|
-
var
|
|
12891
|
+
var exec6 = promisify8(execFile8);
|
|
12522
12892
|
function registerDevLink(program2) {
|
|
12523
12893
|
const dev = program2.commands.find((c) => c.name() === "dev") ?? program2.command("dev").description("Developer utilities for working on Hivelore itself.");
|
|
12524
12894
|
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 +12900,7 @@ function registerDevLink(program2) {
|
|
|
12530
12900
|
}
|
|
12531
12901
|
let globalModules;
|
|
12532
12902
|
try {
|
|
12533
|
-
globalModules = (await
|
|
12903
|
+
globalModules = (await exec6("npm", ["root", "-g"])).stdout.trim();
|
|
12534
12904
|
} catch {
|
|
12535
12905
|
globalModules = path43.join(path43.dirname(path43.dirname(process.execPath)), "lib", "node_modules");
|
|
12536
12906
|
}
|
|
@@ -12822,7 +13192,7 @@ function registerBridges(program2) {
|
|
|
12822
13192
|
|
|
12823
13193
|
// src/index.ts
|
|
12824
13194
|
var program = new Command48();
|
|
12825
|
-
program.name("hivelore").description("Hivelore - repo-native memory and context policy for coding-agent harnesses").version("0.
|
|
13195
|
+
program.name("hivelore").description("Hivelore - repo-native memory and context policy for coding-agent harnesses").version("0.35.0").option("--advanced", "show maintenance and experimental commands in help").showSuggestionAfterError(true);
|
|
12826
13196
|
registerInit(program);
|
|
12827
13197
|
registerResolveProject(program);
|
|
12828
13198
|
registerEnforce(program);
|