@hiveai/cli 0.9.16 → 0.9.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +460 -92
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -198,7 +198,7 @@ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
|
|
|
198
198
|
if (!f) continue;
|
|
199
199
|
counts.set(f, (counts.get(f) ?? 0) + 1);
|
|
200
200
|
}
|
|
201
|
-
let entries = [...counts.entries()].map(([
|
|
201
|
+
let entries = [...counts.entries()].map(([path49, changes]) => ({ path: path49, changes }));
|
|
202
202
|
const lowerPaths = filePaths.map((p) => p.toLowerCase());
|
|
203
203
|
if (lowerPaths.length > 0) {
|
|
204
204
|
entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
|
|
@@ -288,7 +288,7 @@ var TokenBudgetWriter = class {
|
|
|
288
288
|
function registerBriefing(program2) {
|
|
289
289
|
program2.command("briefing").description(
|
|
290
290
|
'Print the full project briefing: last session recap + project context + relevant memories.\n Equivalent to calling get_briefing via MCP. Run before starting any task.\n\n Examples:\n haive briefing\n haive briefing --task "add Stripe payment" --files src/payments/PaymentService.ts\n haive briefing --budget quick --task "tiny fix"\n'
|
|
291
|
-
).option("--task <text>", "what you are about to do \u2014 filters memories by relevance").option("--files <csv>", "comma-separated file paths being worked on (surfaces anchored memories)").option("--symbols <csv>", "symbol names to look up in the code-map (e.g. PaymentService,TenantFilter) \u2014 requires haive index code").option("--max-memories <n>", "cap on memories surfaced", "
|
|
291
|
+
).option("--task <text>", "what you are about to do \u2014 filters memories by relevance").option("--files <csv>", "comma-separated file paths being worked on (surfaces anchored memories)").option("--symbols <csv>", "symbol names to look up in the code-map (e.g. PaymentService,TenantFilter) \u2014 requires haive index code").option("--max-memories <n>", "cap on memories surfaced", "8").option("--max-tokens <n>", "approximate token budget for the entire briefing (truncates if exceeded)").option("--explain-source", "annotate each memory with [source: <relative-path> \xB7 anchors: <files>] for traceable citations").option("--radar", "force project radar (recent commits, open TODOs, hot files) even when memories are plentiful").option("--no-radar", "disable the project radar even when memories are scarce").option(
|
|
292
292
|
"--budget <preset>",
|
|
293
293
|
"align with MCP get_briefing budget_preset: quick | balanced | deep \u2014 sets cap + truncation budget (overrides --max-memories / replaces default open-ended output)",
|
|
294
294
|
void 0
|
|
@@ -325,7 +325,7 @@ function registerBriefing(program2) {
|
|
|
325
325
|
if (b === "quick" || b === "balanced" || b === "deep") budgetPreset = b;
|
|
326
326
|
else ui.warn(`Unknown --budget '${opts.budget}' \u2014 ignoring (use quick|balanced|deep).`);
|
|
327
327
|
}
|
|
328
|
-
let maxMemories = Math.max(1, Number(opts.maxMemories ??
|
|
328
|
+
let maxMemories = Math.max(1, Number(opts.maxMemories ?? 8));
|
|
329
329
|
let budgetTokensCap = opts.maxTokens ? Math.max(100, Number(opts.maxTokens)) : null;
|
|
330
330
|
if (budgetPreset !== null) {
|
|
331
331
|
const presetNums = resolveBriefingBudget(budgetPreset, {
|
|
@@ -470,7 +470,22 @@ function registerBriefing(program2) {
|
|
|
470
470
|
const usageIndex = await loadUsageIndex(paths).catch(() => null);
|
|
471
471
|
out(`${ui.bold("=== Relevant Memories ===")}
|
|
472
472
|
`);
|
|
473
|
-
|
|
473
|
+
const priorities = top.map(
|
|
474
|
+
(item) => classifyCliPriority(
|
|
475
|
+
item,
|
|
476
|
+
filePaths,
|
|
477
|
+
tokens,
|
|
478
|
+
Boolean(andTaskHits?.has(item.memory.frontmatter.id)),
|
|
479
|
+
Boolean(useOrFallback && tokens && literalMatchesAnyToken(item.memory, tokens))
|
|
480
|
+
)
|
|
481
|
+
);
|
|
482
|
+
const mustReadCount = priorities.filter((p) => p === "must_read").length;
|
|
483
|
+
const usefulCount = priorities.filter((p) => p === "useful").length;
|
|
484
|
+
const backgroundCount = priorities.filter((p) => p === "background").length;
|
|
485
|
+
const quality = mustReadCount > 0 || usefulCount > 0 ? backgroundCount > mustReadCount + usefulCount && backgroundCount > 2 ? "noisy" : "strong" : "thin";
|
|
486
|
+
out(ui.dim(`briefing_quality: ${quality} \xB7 must_read=${mustReadCount} useful=${usefulCount} background=${backgroundCount}`));
|
|
487
|
+
out("");
|
|
488
|
+
for (const [idx, item] of top.entries()) {
|
|
474
489
|
if (stopped()) break;
|
|
475
490
|
const fm = item.memory.frontmatter;
|
|
476
491
|
const badge = ui.statusBadge(fm.status);
|
|
@@ -479,8 +494,9 @@ function registerBriefing(program2) {
|
|
|
479
494
|
const originMarker = item.origin ? ` ${ui.yellow("[from " + item.origin + "]")}` : "";
|
|
480
495
|
const reads = usageIndex?.by_id[fm.id]?.read_count ?? 0;
|
|
481
496
|
const hitMarker = reads > 0 ? ` ${ui.dim("\xB7 " + reads + "\xD7 read")}` : "";
|
|
497
|
+
const priority = priorities[idx] ?? "background";
|
|
482
498
|
out(
|
|
483
|
-
`${ui.bold(fm.id)} ${ui.dim(fm.scope + "/" + fm.type)} ${badge}${draftMarker}${unverifiedMarker}${originMarker}${hitMarker}`
|
|
499
|
+
`${ui.bold(fm.id)} ${priorityBadge(priority)} ${ui.dim(fm.scope + "/" + fm.type)} ${badge}${draftMarker}${unverifiedMarker}${originMarker}${hitMarker}`
|
|
484
500
|
);
|
|
485
501
|
if (opts.explainSource) {
|
|
486
502
|
const relPath = path.relative(root, item.filePath);
|
|
@@ -551,6 +567,20 @@ ${ui.bold("=== Symbol Locations ===")}
|
|
|
551
567
|
}
|
|
552
568
|
});
|
|
553
569
|
}
|
|
570
|
+
function classifyCliPriority(item, filePaths, tokens, exactTaskHit, partialTaskHit) {
|
|
571
|
+
const fm = item.memory.frontmatter;
|
|
572
|
+
const anchored = filePaths.length > 0 && memoryMatchesAnchorPaths(item.memory, filePaths);
|
|
573
|
+
if (anchored || fm.type === "attempt" && exactTaskHit) return "must_read";
|
|
574
|
+
if (exactTaskHit || partialTaskHit || item.score >= 4 || tokens && fm.tags.some((tag) => tokens.includes(tag))) {
|
|
575
|
+
return "useful";
|
|
576
|
+
}
|
|
577
|
+
return "background";
|
|
578
|
+
}
|
|
579
|
+
function priorityBadge(priority) {
|
|
580
|
+
if (priority === "must_read") return ui.red("[must_read]");
|
|
581
|
+
if (priority === "useful") return ui.yellow("[useful]");
|
|
582
|
+
return ui.dim("[background]");
|
|
583
|
+
}
|
|
554
584
|
function parseCsv(value) {
|
|
555
585
|
if (!value) return [];
|
|
556
586
|
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -3054,6 +3084,7 @@ import {
|
|
|
3054
3084
|
extractActionsBriefBody as extractActionsBriefBody2,
|
|
3055
3085
|
getUsage as getUsage5,
|
|
3056
3086
|
inferModulesFromPaths as inferModulesFromPaths2,
|
|
3087
|
+
isGlobPath,
|
|
3057
3088
|
isAutoPromoteEligible,
|
|
3058
3089
|
isDecaying,
|
|
3059
3090
|
literalMatchesAllTokens as literalMatchesAllTokens22,
|
|
@@ -3352,7 +3383,6 @@ async function memSave(input, ctx) {
|
|
|
3352
3383
|
const { similarityWarning: simW, body_similar: bs } = bodySimilarWarnings(/* @__PURE__ */ new Set([fm.id]));
|
|
3353
3384
|
const newFrontmatter = {
|
|
3354
3385
|
...fm,
|
|
3355
|
-
body: input.body,
|
|
3356
3386
|
tags: input.tags.length ? input.tags : fm.tags,
|
|
3357
3387
|
revision_count: (fm.revision_count ?? 0) + 1,
|
|
3358
3388
|
anchor: {
|
|
@@ -3368,6 +3398,7 @@ async function memSave(input, ctx) {
|
|
|
3368
3398
|
);
|
|
3369
3399
|
const mergedTw = [
|
|
3370
3400
|
invalidPaths.length > 0 ? `Anchor path(s) not found in project: ${invalidPaths.join(", ")}. They will be marked stale by haive sync.` : null,
|
|
3401
|
+
criticalAnchorWarning(input.type, fm.status, newFrontmatter.anchor.paths, newFrontmatter.anchor.symbols),
|
|
3371
3402
|
simW ?? null
|
|
3372
3403
|
].filter(Boolean).join(" \u2014 ") || void 0;
|
|
3373
3404
|
return {
|
|
@@ -3423,6 +3454,7 @@ async function memSave(input, ctx) {
|
|
|
3423
3454
|
const { similarityWarning: simWarnNew, body_similar: bsNew } = bodySimilarWarnings();
|
|
3424
3455
|
const finalWarning = [
|
|
3425
3456
|
invalidPaths.length > 0 ? `Anchor path(s) not found in project: ${invalidPaths.join(", ")}. They will be marked stale by \`haive sync\`.` : null,
|
|
3457
|
+
criticalAnchorWarning(frontmatter.type, frontmatter.status, frontmatter.anchor.paths, frontmatter.anchor.symbols),
|
|
3426
3458
|
warning ?? null,
|
|
3427
3459
|
simWarnNew ?? null
|
|
3428
3460
|
].filter(Boolean).join(" \u2014 ") || void 0;
|
|
@@ -3437,6 +3469,12 @@ async function memSave(input, ctx) {
|
|
|
3437
3469
|
...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {}
|
|
3438
3470
|
};
|
|
3439
3471
|
}
|
|
3472
|
+
function criticalAnchorWarning(type, status, paths, symbols) {
|
|
3473
|
+
if (!["decision", "gotcha", "architecture"].includes(type)) return null;
|
|
3474
|
+
if (status !== "validated") return null;
|
|
3475
|
+
if (paths.length > 0 || symbols.length > 0) return null;
|
|
3476
|
+
return `${type} is validated without paths or symbols; add anchors so hAIve can detect drift.`;
|
|
3477
|
+
}
|
|
3440
3478
|
var MemSearchInputSchema = {
|
|
3441
3479
|
query: z5.string().describe("Substring matched against id, tags, and body"),
|
|
3442
3480
|
scope: z5.enum(["personal", "team", "module"]).optional().describe("Restrict results to a single scope"),
|
|
@@ -4458,6 +4496,7 @@ async function getBriefing(input, ctx) {
|
|
|
4458
4496
|
reasons: [reason],
|
|
4459
4497
|
match_quality: matchQuality ?? "partial",
|
|
4460
4498
|
...score !== void 0 ? { semantic_score: score } : {},
|
|
4499
|
+
priority: "background",
|
|
4461
4500
|
body: loaded.memory.body,
|
|
4462
4501
|
file_path: loaded.filePath
|
|
4463
4502
|
});
|
|
@@ -4473,6 +4512,13 @@ async function getBriefing(input, ctx) {
|
|
|
4473
4512
|
if (fm.tags.some((t) => inferred.includes(t))) addOrUpdate(loaded, "module", void 0, "partial");
|
|
4474
4513
|
}
|
|
4475
4514
|
}
|
|
4515
|
+
if (input.symbols.length > 0) {
|
|
4516
|
+
const wanted = new Set(input.symbols.map((s) => s.toLowerCase()));
|
|
4517
|
+
for (const loaded of allMemories) {
|
|
4518
|
+
const symbols = loaded.memory.frontmatter.anchor.symbols.map((s) => s.toLowerCase());
|
|
4519
|
+
if (symbols.some((s) => wanted.has(s))) addOrUpdate(loaded, "symbol", void 0, "exact");
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4476
4522
|
if (input.task) {
|
|
4477
4523
|
const tokens = tokenizeQuery22(input.task);
|
|
4478
4524
|
const andHits = allMemories.filter((m) => literalMatchesAllTokens22(m.memory, tokens));
|
|
@@ -4498,11 +4544,12 @@ async function getBriefing(input, ctx) {
|
|
|
4498
4544
|
}
|
|
4499
4545
|
}
|
|
4500
4546
|
const ranked = [...seen.values()].sort((a, b) => {
|
|
4547
|
+
const priorityScore = (m) => priorityRank(classifyMemoryPriority(m, byId.get(m.id), input.files, input.symbols));
|
|
4501
4548
|
const reasonScore = (m) => (m.type === "attempt" ? 3 : 0) + // attempt = negative knowledge, surface first to prevent repeating mistakes
|
|
4502
|
-
(m.reasons.includes("anchor") ? 4 : 0) + (m.reasons.includes("module") ? 2 : 0) + (m.reasons.includes("semantic") ? 2 : 0) + (m.reasons.includes("domain") ? 1 : 0);
|
|
4549
|
+
(m.reasons.includes("anchor") ? 4 : 0) + (m.reasons.includes("symbol") ? 4 : 0) + (m.reasons.includes("module") ? 2 : 0) + (m.reasons.includes("semantic") ? 2 : 0) + (m.reasons.includes("domain") ? 1 : 0);
|
|
4503
4550
|
const confidenceScore = (m) => m.confidence === "authoritative" ? 4 : m.confidence === "trusted" ? 3 : m.confidence === "low" ? 1 : m.confidence === "stale" ? -2 : 0;
|
|
4504
|
-
const sa = reasonScore(a) + confidenceScore(a) + (a.semantic_score ?? 0);
|
|
4505
|
-
const sb = reasonScore(b) + confidenceScore(b) + (b.semantic_score ?? 0);
|
|
4551
|
+
const sa = priorityScore(a) * 100 + reasonScore(a) + confidenceScore(a) + (a.semantic_score ?? 0);
|
|
4552
|
+
const sb = priorityScore(b) * 100 + reasonScore(b) + confidenceScore(b) + (b.semantic_score ?? 0);
|
|
4506
4553
|
return sb - sa;
|
|
4507
4554
|
});
|
|
4508
4555
|
for (const mem of ranked.slice(0, briefingMaxMemories)) {
|
|
@@ -4661,8 +4708,15 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
4661
4708
|
})) : trimmedMemories;
|
|
4662
4709
|
const outputMemories = formattedMemories.map((m) => ({
|
|
4663
4710
|
...m,
|
|
4711
|
+
priority: classifyMemoryPriority(m, byId.get(m.id), input.files, input.symbols),
|
|
4664
4712
|
why: explainWhySurfaced(m, byId.get(m.id), input.files, inferred)
|
|
4665
4713
|
}));
|
|
4714
|
+
const briefingQuality = classifyBriefingQuality(outputMemories, {
|
|
4715
|
+
isTemplateContext,
|
|
4716
|
+
autoContextGenerated,
|
|
4717
|
+
hasLastSession: Boolean(lastSession),
|
|
4718
|
+
searchMode
|
|
4719
|
+
});
|
|
4666
4720
|
let symbolLocations;
|
|
4667
4721
|
const symbolsToLookup = new Set(input.symbols);
|
|
4668
4722
|
for (const m of outputMemories) {
|
|
@@ -4803,6 +4857,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
4803
4857
|
} : null,
|
|
4804
4858
|
module_contexts: trimmedModules,
|
|
4805
4859
|
memories: outputMemories,
|
|
4860
|
+
briefing_quality: briefingQuality,
|
|
4806
4861
|
...symbolLocations ? { symbol_locations: symbolLocations } : {},
|
|
4807
4862
|
action_required: actionRequired,
|
|
4808
4863
|
decay_warnings: decayWarnings,
|
|
@@ -4828,6 +4883,53 @@ function compactSummary(body) {
|
|
|
4828
4883
|
}
|
|
4829
4884
|
return body.slice(0, 120);
|
|
4830
4885
|
}
|
|
4886
|
+
function classifyMemoryPriority(memory2, loaded, inputFiles, inputSymbols) {
|
|
4887
|
+
const fm = loaded?.memory.frontmatter;
|
|
4888
|
+
const directAnchor = Boolean(
|
|
4889
|
+
fm && inputFiles.length > 0 && fm.anchor.paths.some((p) => inputFiles.some((file) => pathsOverlap(p, file)))
|
|
4890
|
+
);
|
|
4891
|
+
const directSymbol = Boolean(
|
|
4892
|
+
fm && inputSymbols.length > 0 && fm.anchor.symbols.some(
|
|
4893
|
+
(sym) => inputSymbols.some((wanted) => wanted.toLowerCase() === sym.toLowerCase())
|
|
4894
|
+
)
|
|
4895
|
+
);
|
|
4896
|
+
const strongSemantic = (memory2.semantic_score ?? 0) >= 0.65;
|
|
4897
|
+
const usefulSemantic = (memory2.semantic_score ?? 0) >= 0.35;
|
|
4898
|
+
if (fm?.requires_human_approval || directAnchor || directSymbol || memory2.type === "attempt" && (memory2.match_quality === "exact" || strongSemantic)) {
|
|
4899
|
+
return "must_read";
|
|
4900
|
+
}
|
|
4901
|
+
if (memory2.reasons.includes("module") || memory2.reasons.includes("domain") || memory2.match_quality === "exact" || usefulSemantic) {
|
|
4902
|
+
return "useful";
|
|
4903
|
+
}
|
|
4904
|
+
return "background";
|
|
4905
|
+
}
|
|
4906
|
+
function priorityRank(priority) {
|
|
4907
|
+
return priority === "must_read" ? 3 : priority === "useful" ? 2 : 1;
|
|
4908
|
+
}
|
|
4909
|
+
function classifyBriefingQuality(memories, context) {
|
|
4910
|
+
const mustRead = memories.filter((m) => m.priority === "must_read").length;
|
|
4911
|
+
const useful = memories.filter((m) => m.priority === "useful").length;
|
|
4912
|
+
const background = memories.filter((m) => m.priority === "background").length;
|
|
4913
|
+
const weakSemantic = memories.filter(
|
|
4914
|
+
(m) => m.reasons.length === 1 && m.reasons.includes("semantic") && (m.semantic_score ?? 0) > 0 && (m.semantic_score ?? 0) < 0.35
|
|
4915
|
+
).length;
|
|
4916
|
+
const reasons = [];
|
|
4917
|
+
if (memories.length === 0) reasons.push("no memories matched the task or files");
|
|
4918
|
+
if (context.isTemplateContext && !context.autoContextGenerated) reasons.push("project context is still a template");
|
|
4919
|
+
if (!context.hasLastSession) reasons.push("no previous session recap");
|
|
4920
|
+
if (mustRead > 0) reasons.push(`${mustRead} must_read memor${mustRead === 1 ? "y" : "ies"} matched directly`);
|
|
4921
|
+
if (useful > 0) reasons.push(`${useful} useful memor${useful === 1 ? "y" : "ies"} matched`);
|
|
4922
|
+
if (background > useful + mustRead && background > 2) reasons.push(`${background} background memories dominate the result`);
|
|
4923
|
+
if (weakSemantic > 0) reasons.push(`${weakSemantic} weak semantic-only match${weakSemantic === 1 ? "" : "es"}`);
|
|
4924
|
+
if (context.searchMode === "literal_fallback") reasons.push("semantic index unavailable or empty; literal fallback used");
|
|
4925
|
+
if (memories.length === 0 || mustRead === 0 && useful === 0) {
|
|
4926
|
+
return { level: "thin", reasons };
|
|
4927
|
+
}
|
|
4928
|
+
if (background > useful + mustRead && background > 2) {
|
|
4929
|
+
return { level: "noisy", reasons };
|
|
4930
|
+
}
|
|
4931
|
+
return { level: "strong", reasons };
|
|
4932
|
+
}
|
|
4831
4933
|
function explainWhySurfaced(memory2, loaded, inputFiles, inferredModules) {
|
|
4832
4934
|
const why = [];
|
|
4833
4935
|
const fm = loaded?.memory.frontmatter;
|
|
@@ -4836,7 +4938,19 @@ function explainWhySurfaced(memory2, loaded, inputFiles, inferredModules) {
|
|
|
4836
4938
|
(p) => inputFiles.length === 0 || inputFiles.some((file) => pathsOverlap(p, file))
|
|
4837
4939
|
);
|
|
4838
4940
|
if (matching.length > 0) {
|
|
4839
|
-
|
|
4941
|
+
const exact = matching.filter(
|
|
4942
|
+
(p) => !isGlobPath(p) && inputFiles.some((file) => p === file || pathsOverlap(p, file))
|
|
4943
|
+
);
|
|
4944
|
+
const glob = matching.filter((p) => isGlobPath(p));
|
|
4945
|
+
if (exact.length > 0) {
|
|
4946
|
+
why.push(`Exact/file anchor match: ${exact.slice(0, 4).join(", ")}`);
|
|
4947
|
+
}
|
|
4948
|
+
if (glob.length > 0) {
|
|
4949
|
+
why.push(`Glob anchor match: ${glob.slice(0, 4).join(", ")}`);
|
|
4950
|
+
}
|
|
4951
|
+
if (exact.length === 0 && glob.length === 0) {
|
|
4952
|
+
why.push(`Anchored to touched path${matching.length === 1 ? "" : "s"}: ${matching.slice(0, 4).join(", ")}`);
|
|
4953
|
+
}
|
|
4840
4954
|
} else if (fm.anchor.paths.length > 0) {
|
|
4841
4955
|
why.push(`Pulled by related anchor: ${fm.anchor.paths.slice(0, 4).join(", ")}`);
|
|
4842
4956
|
}
|
|
@@ -4844,6 +4958,9 @@ function explainWhySurfaced(memory2, loaded, inputFiles, inferredModules) {
|
|
|
4844
4958
|
why.push(`Anchor symbol${fm.anchor.symbols.length === 1 ? "" : "s"}: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
|
|
4845
4959
|
}
|
|
4846
4960
|
}
|
|
4961
|
+
if (memory2.reasons.includes("symbol") && fm) {
|
|
4962
|
+
why.push(`Explicit symbol match: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
|
|
4963
|
+
}
|
|
4847
4964
|
if (memory2.reasons.includes("module")) {
|
|
4848
4965
|
const moduleHints = [
|
|
4849
4966
|
...memory2.module ? [memory2.module] : [],
|
|
@@ -5068,7 +5185,8 @@ async function memRelevantTo(input, ctx) {
|
|
|
5068
5185
|
const out = {
|
|
5069
5186
|
task: input.task,
|
|
5070
5187
|
search_mode: briefing.search_mode,
|
|
5071
|
-
memories: briefing.memories
|
|
5188
|
+
memories: briefing.memories,
|
|
5189
|
+
briefing_quality: briefing.briefing_quality
|
|
5072
5190
|
};
|
|
5073
5191
|
if (briefing.hints && briefing.hints.length > 0) out.hints = briefing.hints;
|
|
5074
5192
|
if (briefing.memories.length === 0) out.empty = true;
|
|
@@ -5750,7 +5868,7 @@ async function preCommitCheck(input, ctx) {
|
|
|
5750
5868
|
const filesTouching = new Set(relevantMatches.map((m) => m.id));
|
|
5751
5869
|
const staleHits = verifyResult.results.filter((r) => r.stale && filesTouching.has(r.id));
|
|
5752
5870
|
const blockOn = input.block_on;
|
|
5753
|
-
const classifiedWarnings = apResult.warnings.map(classifyWarning);
|
|
5871
|
+
const classifiedWarnings = apResult.warnings.map((warning) => classifyWarning(warning, input.paths));
|
|
5754
5872
|
const blockingWarnings = classifiedWarnings.filter((w) => w.level === "blocking");
|
|
5755
5873
|
const reviewWarnings = classifiedWarnings.filter((w) => w.level === "review");
|
|
5756
5874
|
const infoWarnings = classifiedWarnings.filter((w) => w.level === "info");
|
|
@@ -5785,12 +5903,26 @@ async function preCommitCheck(input, ctx) {
|
|
|
5785
5903
|
}))
|
|
5786
5904
|
};
|
|
5787
5905
|
}
|
|
5788
|
-
function classifyWarning(warning) {
|
|
5906
|
+
function classifyWarning(warning, paths) {
|
|
5907
|
+
const affectedFiles = paths.filter((p) => !p.startsWith(".ai/.usage/"));
|
|
5908
|
+
const repairCommand = repairCommandForWarning(warning, affectedFiles);
|
|
5909
|
+
const fileDowngrade = fileTypeDowngradeReason(warning, affectedFiles);
|
|
5910
|
+
if (fileDowngrade) {
|
|
5911
|
+
return {
|
|
5912
|
+
...warning,
|
|
5913
|
+
level: "info",
|
|
5914
|
+
rationale: fileDowngrade,
|
|
5915
|
+
affected_files: affectedFiles,
|
|
5916
|
+
repair_command: repairCommand
|
|
5917
|
+
};
|
|
5918
|
+
}
|
|
5789
5919
|
if (isBlockingWarning(warning)) {
|
|
5790
5920
|
return {
|
|
5791
5921
|
...warning,
|
|
5792
5922
|
level: "blocking",
|
|
5793
|
-
rationale: "authoritative/trusted memory plus strong semantic match to the diff (score >= 0.65)"
|
|
5923
|
+
rationale: "authoritative/trusted memory plus strong semantic match to the diff (score >= 0.65)",
|
|
5924
|
+
affected_files: affectedFiles,
|
|
5925
|
+
repair_command: repairCommand
|
|
5794
5926
|
};
|
|
5795
5927
|
}
|
|
5796
5928
|
const hasSemantic = warning.reasons.includes("semantic");
|
|
@@ -5800,13 +5932,17 @@ function classifyWarning(warning) {
|
|
|
5800
5932
|
return {
|
|
5801
5933
|
...warning,
|
|
5802
5934
|
level: "review",
|
|
5803
|
-
rationale: hasSemantic ? "semantic match is plausible but below blocking threshold" : "anchored high-confidence memory also matched diff tokens, but no strong semantic proof"
|
|
5935
|
+
rationale: hasSemantic ? "semantic match is plausible but below blocking threshold" : "anchored high-confidence memory also matched diff tokens, but no strong semantic proof",
|
|
5936
|
+
affected_files: affectedFiles,
|
|
5937
|
+
repair_command: repairCommand
|
|
5804
5938
|
};
|
|
5805
5939
|
}
|
|
5806
5940
|
return {
|
|
5807
5941
|
...warning,
|
|
5808
5942
|
level: "info",
|
|
5809
|
-
rationale: "weak signal only (literal/anchor/low semantic evidence); surfaced for audit, hidden in concise CLI output"
|
|
5943
|
+
rationale: "weak signal only (literal/anchor/low semantic evidence); surfaced for audit, hidden in concise CLI output",
|
|
5944
|
+
affected_files: affectedFiles,
|
|
5945
|
+
repair_command: repairCommand
|
|
5810
5946
|
};
|
|
5811
5947
|
}
|
|
5812
5948
|
function isBlockingWarning(warning) {
|
|
@@ -5814,6 +5950,40 @@ function isBlockingWarning(warning) {
|
|
|
5814
5950
|
if (!highConfidence) return false;
|
|
5815
5951
|
return warning.reasons.includes("semantic") && (warning.semantic_score ?? 0) >= 0.65;
|
|
5816
5952
|
}
|
|
5953
|
+
function fileTypeDowngradeReason(warning, paths) {
|
|
5954
|
+
if (paths.length === 0) return null;
|
|
5955
|
+
if (paths.every((p) => p.startsWith(".ai/.usage/") || p === ".ai/.usage/tool-usage.jsonl")) {
|
|
5956
|
+
return ".ai usage logs are local telemetry and never block commits.";
|
|
5957
|
+
}
|
|
5958
|
+
const docsOnly = paths.every(isDocLikePath);
|
|
5959
|
+
if (docsOnly && !hasStrongSemantic(warning)) {
|
|
5960
|
+
return "docs/changelog-only change; anti-pattern is downgraded unless semantic evidence is strong.";
|
|
5961
|
+
}
|
|
5962
|
+
const configOnly = paths.every(isPackageOrConfigPath);
|
|
5963
|
+
if (configOnly && looksRuntimeSpecific(warning) && !warning.reasons.includes("anchor") && !hasStrongSemantic(warning)) {
|
|
5964
|
+
return "package/config-only change; runtime-specific gotcha is not anchored or semantically strong.";
|
|
5965
|
+
}
|
|
5966
|
+
return null;
|
|
5967
|
+
}
|
|
5968
|
+
function hasStrongSemantic(warning) {
|
|
5969
|
+
return warning.reasons.includes("semantic") && (warning.semantic_score ?? 0) >= 0.65;
|
|
5970
|
+
}
|
|
5971
|
+
function isDocLikePath(file) {
|
|
5972
|
+
const lower = file.toLowerCase();
|
|
5973
|
+
return lower.endsWith(".md") || lower.includes("changelog") || lower.startsWith("docs/") || lower.startsWith(".github/") && lower.endsWith(".md");
|
|
5974
|
+
}
|
|
5975
|
+
function isPackageOrConfigPath(file) {
|
|
5976
|
+
const lower = file.toLowerCase();
|
|
5977
|
+
return lower.endsWith("package.json") || lower.endsWith("package-lock.json") || lower.endsWith("pnpm-lock.yaml") || lower.endsWith("yarn.lock") || lower.endsWith("bun.lockb") || lower.endsWith(".config.ts") || lower.endsWith(".config.js") || lower.endsWith(".json") || lower.endsWith(".yml") || lower.endsWith(".yaml") || lower.startsWith(".github/workflows/");
|
|
5978
|
+
}
|
|
5979
|
+
function looksRuntimeSpecific(warning) {
|
|
5980
|
+
const text = `${warning.body_preview} ${warning.id}`.toLowerCase();
|
|
5981
|
+
return /\b(runtime|controller|request|response|database|transaction|auth|cache|production|service|api|endpoint)\b/.test(text);
|
|
5982
|
+
}
|
|
5983
|
+
function repairCommandForWarning(warning, paths) {
|
|
5984
|
+
const firstPath = paths[0];
|
|
5985
|
+
return firstPath ? `haive briefing --files "${firstPath}" --task "review ${warning.id}"` : `haive memory show ${warning.id}`;
|
|
5986
|
+
}
|
|
5817
5987
|
var CONFIG_PATTERNS = [
|
|
5818
5988
|
".eslintrc",
|
|
5819
5989
|
"eslint.config",
|
|
@@ -6345,7 +6515,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
6345
6515
|
};
|
|
6346
6516
|
}
|
|
6347
6517
|
var SERVER_NAME = "haive";
|
|
6348
|
-
var SERVER_VERSION = "0.9.
|
|
6518
|
+
var SERVER_VERSION = "0.9.17";
|
|
6349
6519
|
function jsonResult(data) {
|
|
6350
6520
|
return {
|
|
6351
6521
|
content: [
|
|
@@ -10780,7 +10950,7 @@ var MS_PER_DAY3 = 24 * 60 * 60 * 1e3;
|
|
|
10780
10950
|
function registerDoctor(program2) {
|
|
10781
10951
|
program2.command("doctor").description(
|
|
10782
10952
|
"Analyze the local hAIve setup and emit actionable recommendations.\n\n Inspects: project-context status, memory health (stale/anchorless/decay/pending),\n code-map freshness, usage log signals (low-hit briefings, repeated empty searches).\n\n Read-only by default. Pass --fix to suggest commands you can copy-paste."
|
|
10783
|
-
).option("--json", "emit JSON instead of human-readable output", false).option("--fix", "include suggested fix commands in human output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10953
|
+
).option("--json", "emit JSON instead of human-readable output", false).option("--fix", "include suggested fix commands in human output", false).option("--dry-run", "with --fix, show delegated repairs without applying them", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10784
10954
|
const root = findProjectRoot40(opts.dir);
|
|
10785
10955
|
const paths = resolveHaivePaths36(root);
|
|
10786
10956
|
const findings = [];
|
|
@@ -10952,14 +11122,14 @@ function registerDoctor(program2) {
|
|
|
10952
11122
|
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
|
|
10953
11123
|
});
|
|
10954
11124
|
}
|
|
10955
|
-
findings.push(...await collectInstallFindings(root, "0.9.
|
|
11125
|
+
findings.push(...await collectInstallFindings(root, "0.9.17"));
|
|
10956
11126
|
try {
|
|
10957
11127
|
const legacyRaw = execSync3("haive-mcp --version", {
|
|
10958
11128
|
encoding: "utf8",
|
|
10959
11129
|
timeout: 3e3,
|
|
10960
11130
|
stdio: ["ignore", "pipe", "ignore"]
|
|
10961
11131
|
}).trim();
|
|
10962
|
-
const cliVersion = "0.9.
|
|
11132
|
+
const cliVersion = "0.9.17";
|
|
10963
11133
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
10964
11134
|
findings.push({
|
|
10965
11135
|
severity: "warn",
|
|
@@ -10976,33 +11146,105 @@ npm uninstall -g @hiveai/mcp`
|
|
|
10976
11146
|
});
|
|
10977
11147
|
}
|
|
10978
11148
|
function emit(findings, opts) {
|
|
11149
|
+
const classified = findings.map((finding) => ({
|
|
11150
|
+
...finding,
|
|
11151
|
+
section: finding.section ?? sectionForFinding(finding)
|
|
11152
|
+
}));
|
|
11153
|
+
const scores = computeDoctorScores(classified);
|
|
10979
11154
|
if (opts.json) {
|
|
10980
|
-
console.log(JSON.stringify({
|
|
11155
|
+
console.log(JSON.stringify({
|
|
11156
|
+
scores,
|
|
11157
|
+
findings: classified,
|
|
11158
|
+
sections: groupBySection(classified),
|
|
11159
|
+
next_actions: nextActions(classified),
|
|
11160
|
+
fix_mode: opts.fix ? opts.dryRun ? "dry-run" : "suggest" : "off"
|
|
11161
|
+
}, null, 2));
|
|
10981
11162
|
return;
|
|
10982
11163
|
}
|
|
10983
|
-
if (
|
|
11164
|
+
if (classified.length === 0) {
|
|
10984
11165
|
ui.success("hAIve doctor \u2014 no issues found.");
|
|
10985
11166
|
return;
|
|
10986
11167
|
}
|
|
10987
|
-
console.log(ui.bold(`hAIve doctor \u2014 ${
|
|
11168
|
+
console.log(ui.bold(`hAIve doctor \u2014 ${classified.length} finding${classified.length === 1 ? "" : "s"}`));
|
|
11169
|
+
console.log(
|
|
11170
|
+
ui.dim(
|
|
11171
|
+
` protection=${scores.protection_score} context=${scores.context_quality_score} corpus=${scores.corpus_quality_score}`
|
|
11172
|
+
)
|
|
11173
|
+
);
|
|
10988
11174
|
console.log();
|
|
10989
|
-
const
|
|
10990
|
-
|
|
10991
|
-
|
|
10992
|
-
|
|
10993
|
-
|
|
10994
|
-
|
|
10995
|
-
|
|
10996
|
-
|
|
11175
|
+
const sectionOrder = [
|
|
11176
|
+
"Protection",
|
|
11177
|
+
"Agent coverage",
|
|
11178
|
+
"Context quality",
|
|
11179
|
+
"Corpus health",
|
|
11180
|
+
"Index health",
|
|
11181
|
+
"Next actions"
|
|
11182
|
+
];
|
|
11183
|
+
const severityOrder = ["error", "warn", "info"];
|
|
11184
|
+
for (const section2 of sectionOrder) {
|
|
11185
|
+
const sectionFindings = classified.filter((f) => f.section === section2);
|
|
11186
|
+
if (sectionFindings.length === 0) continue;
|
|
11187
|
+
console.log(ui.bold(section2));
|
|
11188
|
+
for (const sev of severityOrder) {
|
|
11189
|
+
for (const f of sectionFindings.filter((x) => x.severity === sev)) {
|
|
11190
|
+
const icon = sev === "error" ? ui.red("\u2717") : sev === "warn" ? ui.yellow("\u26A0") : ui.dim("\u2139");
|
|
11191
|
+
console.log(`${icon} ${ui.bold(f.code)} ${f.message}`);
|
|
11192
|
+
if (opts.fix && f.fix) {
|
|
11193
|
+
for (const line of f.fix.split("\n")) {
|
|
11194
|
+
console.log(` ${ui.dim(opts.dryRun ? "would run:" : "$")} ${line}`);
|
|
11195
|
+
}
|
|
10997
11196
|
}
|
|
10998
11197
|
}
|
|
10999
11198
|
}
|
|
11000
|
-
}
|
|
11001
|
-
if (!opts.fix && findings.some((f) => f.fix)) {
|
|
11002
11199
|
console.log();
|
|
11200
|
+
}
|
|
11201
|
+
const actions = nextActions(classified);
|
|
11202
|
+
if (actions.length > 0) {
|
|
11203
|
+
console.log(ui.bold("Next actions"));
|
|
11204
|
+
for (const action of actions.slice(0, 5)) console.log(` ${ui.dim("$")} ${action}`);
|
|
11205
|
+
} else if (!opts.fix && classified.some((f) => f.fix)) {
|
|
11003
11206
|
ui.info("Re-run with --fix to see suggested commands.");
|
|
11004
11207
|
}
|
|
11005
11208
|
}
|
|
11209
|
+
function sectionForFinding(finding) {
|
|
11210
|
+
if (finding.code.includes("haive") || finding.code.includes("integration") || finding.code.includes("claude") || finding.code.includes("autopilot")) return "Agent coverage";
|
|
11211
|
+
if (finding.code.includes("context") || finding.code.includes("briefing") || finding.code.includes("search")) return "Context quality";
|
|
11212
|
+
if (finding.code.includes("code-map") || finding.code.includes("index")) return "Index health";
|
|
11213
|
+
if (finding.code.includes("memory") || finding.code.includes("anchor") || finding.code.includes("pending") || finding.code.includes("decay")) return "Corpus health";
|
|
11214
|
+
if (finding.severity === "error") return "Protection";
|
|
11215
|
+
return "Next actions";
|
|
11216
|
+
}
|
|
11217
|
+
function computeDoctorScores(findings) {
|
|
11218
|
+
const scoreFor = (sections) => {
|
|
11219
|
+
const scoped = findings.filter((f) => sections.includes(f.section ?? sectionForFinding(f)));
|
|
11220
|
+
const penalty = scoped.reduce((sum, f) => {
|
|
11221
|
+
if (f.severity === "error") return sum + 35;
|
|
11222
|
+
if (f.severity === "warn") return sum + 15;
|
|
11223
|
+
return sum + 4;
|
|
11224
|
+
}, 0);
|
|
11225
|
+
return Math.max(0, 100 - penalty);
|
|
11226
|
+
};
|
|
11227
|
+
return {
|
|
11228
|
+
protection_score: scoreFor(["Protection", "Agent coverage"]),
|
|
11229
|
+
context_quality_score: scoreFor(["Context quality", "Index health"]),
|
|
11230
|
+
corpus_quality_score: scoreFor(["Corpus health"])
|
|
11231
|
+
};
|
|
11232
|
+
}
|
|
11233
|
+
function groupBySection(findings) {
|
|
11234
|
+
const out = {
|
|
11235
|
+
"Protection": [],
|
|
11236
|
+
"Agent coverage": [],
|
|
11237
|
+
"Context quality": [],
|
|
11238
|
+
"Corpus health": [],
|
|
11239
|
+
"Index health": [],
|
|
11240
|
+
"Next actions": []
|
|
11241
|
+
};
|
|
11242
|
+
for (const finding of findings) out[finding.section ?? sectionForFinding(finding)].push(finding);
|
|
11243
|
+
return out;
|
|
11244
|
+
}
|
|
11245
|
+
function nextActions(findings) {
|
|
11246
|
+
return [...new Set(findings.flatMap((finding) => finding.fix ? finding.fix.split("\n") : []))].filter(Boolean);
|
|
11247
|
+
}
|
|
11006
11248
|
function isSearchTool(name) {
|
|
11007
11249
|
return ["mem_search", "code_search", "mem_relevant_to", "get_briefing"].includes(name);
|
|
11008
11250
|
}
|
|
@@ -11321,7 +11563,11 @@ function printWarnings(title, warnings, tone) {
|
|
|
11321
11563
|
console.log(` ${ui.dim(line)}`);
|
|
11322
11564
|
}
|
|
11323
11565
|
console.log(` ${ui.dim("reasons:")} ${w.reasons.join(", ")}`);
|
|
11566
|
+
if (w.affected_files && w.affected_files.length > 0) {
|
|
11567
|
+
console.log(` ${ui.dim("files:")} ${w.affected_files.slice(0, 4).join(", ")}`);
|
|
11568
|
+
}
|
|
11324
11569
|
if (w.rationale) console.log(` ${ui.dim("why shown:")} ${w.rationale}`);
|
|
11570
|
+
if (w.repair_command) console.log(` ${ui.dim("repair:")} ${w.repair_command}`);
|
|
11325
11571
|
}
|
|
11326
11572
|
console.log();
|
|
11327
11573
|
}
|
|
@@ -11408,20 +11654,26 @@ function registerWelcome(program2) {
|
|
|
11408
11654
|
|
|
11409
11655
|
// src/commands/memory-lint.ts
|
|
11410
11656
|
import { existsSync as existsSync64 } from "fs";
|
|
11657
|
+
import { writeFile as writeFile31 } from "fs/promises";
|
|
11658
|
+
import path43 from "path";
|
|
11411
11659
|
import "commander";
|
|
11412
11660
|
import {
|
|
11413
11661
|
findProjectRoot as findProjectRoot44,
|
|
11414
11662
|
getUsage as getUsage20,
|
|
11663
|
+
loadCodeMap as loadCodeMap6,
|
|
11415
11664
|
loadMemoriesFromDir as loadMemoriesFromDir35,
|
|
11416
11665
|
loadUsageIndex as loadUsageIndex26,
|
|
11417
|
-
resolveHaivePaths as resolveHaivePaths40
|
|
11666
|
+
resolveHaivePaths as resolveHaivePaths40,
|
|
11667
|
+
serializeMemory as serializeMemory26
|
|
11418
11668
|
} from "@hiveai/core";
|
|
11419
|
-
async function lintMemoriesAsync(root) {
|
|
11669
|
+
async function lintMemoriesAsync(root, options = {}) {
|
|
11420
11670
|
const paths = resolveHaivePaths40(root);
|
|
11421
11671
|
const out = [];
|
|
11422
|
-
|
|
11672
|
+
const fixes = [];
|
|
11673
|
+
if (!existsSync64(paths.memoriesDir)) return { findings: out, fixes };
|
|
11423
11674
|
const loaded = await loadMemoriesFromDir35(paths.memoriesDir);
|
|
11424
11675
|
const usage = await loadUsageIndex26(paths);
|
|
11676
|
+
const codeMap = await loadCodeMap6(paths);
|
|
11425
11677
|
const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
|
|
11426
11678
|
const actionableWords = /\b(always|never|prefer|use|avoid|because|instead|why|rationale|do not|must|should)\b/i;
|
|
11427
11679
|
for (const { filePath, memory: memory2 } of loaded) {
|
|
@@ -11447,13 +11699,15 @@ async function lintMemoriesAsync(root) {
|
|
|
11447
11699
|
message: "Record does not contain obvious action/rationale words. Add the concrete rule, why it exists, and what to do instead."
|
|
11448
11700
|
});
|
|
11449
11701
|
}
|
|
11702
|
+
const suggestedAnchors = suggestAnchors(root, { filePath, memory: memory2 }, codeMap);
|
|
11450
11703
|
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated") {
|
|
11451
11704
|
out.push({
|
|
11452
11705
|
file: filePath,
|
|
11453
11706
|
id: fm.id,
|
|
11454
11707
|
severity: "warn",
|
|
11455
11708
|
code: "MISSING_ANCHOR",
|
|
11456
|
-
message: `${fm.type} is validated without anchor paths \u2014 add anchor.paths so haive sync can flag staleness
|
|
11709
|
+
message: `${fm.type} is validated without anchor paths \u2014 add anchor.paths so haive sync can flag staleness.`,
|
|
11710
|
+
...suggestedAnchors.paths.length > 0 || suggestedAnchors.symbols.length > 0 ? { suggested_anchors: suggestedAnchors } : {}
|
|
11457
11711
|
});
|
|
11458
11712
|
}
|
|
11459
11713
|
if (fm.status === "stale" && !fm.stale_reason) {
|
|
@@ -11494,6 +11748,34 @@ async function lintMemoriesAsync(root) {
|
|
|
11494
11748
|
message: "Validated record has never been surfaced/read. Consider improving tags/anchors or archiving it if it is not useful."
|
|
11495
11749
|
});
|
|
11496
11750
|
}
|
|
11751
|
+
if (options.fix) {
|
|
11752
|
+
const actions = [];
|
|
11753
|
+
let nextBody = memory2.body;
|
|
11754
|
+
let nextFrontmatter = memory2.frontmatter;
|
|
11755
|
+
if (!hasMarkdownHeading) {
|
|
11756
|
+
nextBody = `# ${titleFromId(fm.id)}
|
|
11757
|
+
|
|
11758
|
+
${nextBody.trim()}`;
|
|
11759
|
+
actions.push("add missing Markdown heading");
|
|
11760
|
+
}
|
|
11761
|
+
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.anchor.symbols.length === 0 && fm.status === "validated" && !fm.tags.includes("needs_anchor")) {
|
|
11762
|
+
nextFrontmatter = {
|
|
11763
|
+
...nextFrontmatter,
|
|
11764
|
+
tags: [...nextFrontmatter.tags, "needs_anchor"]
|
|
11765
|
+
};
|
|
11766
|
+
actions.push("tag validated anchorless record with needs_anchor");
|
|
11767
|
+
}
|
|
11768
|
+
if (actions.length > 0) {
|
|
11769
|
+
fixes.push({ file: filePath, id: fm.id, actions, applied: Boolean(options.apply) });
|
|
11770
|
+
if (options.apply) {
|
|
11771
|
+
await writeFile31(
|
|
11772
|
+
filePath,
|
|
11773
|
+
serializeMemory26({ frontmatter: nextFrontmatter, body: nextBody }),
|
|
11774
|
+
"utf8"
|
|
11775
|
+
);
|
|
11776
|
+
}
|
|
11777
|
+
}
|
|
11778
|
+
}
|
|
11497
11779
|
}
|
|
11498
11780
|
for (const dup of nearDuplicatePairs(loaded)) {
|
|
11499
11781
|
out.push({
|
|
@@ -11504,7 +11786,39 @@ async function lintMemoriesAsync(root) {
|
|
|
11504
11786
|
message: `Body overlaps ~${Math.round(dup.score * 100)}% with ${dup.otherId}. Merge or deprecate one record to reduce briefing noise.`
|
|
11505
11787
|
});
|
|
11506
11788
|
}
|
|
11507
|
-
return out;
|
|
11789
|
+
return { findings: out, fixes };
|
|
11790
|
+
}
|
|
11791
|
+
function titleFromId(id) {
|
|
11792
|
+
const withoutDate = id.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
11793
|
+
return withoutDate.split("-").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
11794
|
+
}
|
|
11795
|
+
function suggestAnchors(root, loaded, codeMap) {
|
|
11796
|
+
const body = loaded.memory.body;
|
|
11797
|
+
const paths = /* @__PURE__ */ new Set();
|
|
11798
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
11799
|
+
for (const match of body.matchAll(/`([^`\n]+\.[A-Za-z0-9]+)`|(?:^|\s)([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)/gm)) {
|
|
11800
|
+
const candidate = (match[1] ?? match[2] ?? "").replace(/^\.?\//, "");
|
|
11801
|
+
if (!candidate || candidate.startsWith("http")) continue;
|
|
11802
|
+
if (existsSync64(path43.join(root, candidate))) paths.add(candidate);
|
|
11803
|
+
}
|
|
11804
|
+
if (codeMap) {
|
|
11805
|
+
const lowered = body.toLowerCase();
|
|
11806
|
+
for (const [file, entry] of Object.entries(codeMap.files)) {
|
|
11807
|
+
for (const exp of entry.exports) {
|
|
11808
|
+
if (!exp.name || exp.name.length < 4) continue;
|
|
11809
|
+
if (lowered.includes(exp.name.toLowerCase())) {
|
|
11810
|
+
paths.add(file);
|
|
11811
|
+
symbols.add(exp.name);
|
|
11812
|
+
}
|
|
11813
|
+
if (paths.size >= 5 && symbols.size >= 5) break;
|
|
11814
|
+
}
|
|
11815
|
+
if (paths.size >= 5 && symbols.size >= 5) break;
|
|
11816
|
+
}
|
|
11817
|
+
}
|
|
11818
|
+
return {
|
|
11819
|
+
paths: [...paths].slice(0, 5),
|
|
11820
|
+
symbols: [...symbols].slice(0, 5)
|
|
11821
|
+
};
|
|
11508
11822
|
}
|
|
11509
11823
|
function nearDuplicatePairs(loaded) {
|
|
11510
11824
|
const out = [];
|
|
@@ -11545,11 +11859,20 @@ function jaccard2(a, b) {
|
|
|
11545
11859
|
function registerMemoryLint(parent) {
|
|
11546
11860
|
parent.command("lint").description(
|
|
11547
11861
|
"Heuristic corpus checks (anchors on key types, headings, verbosity). Static analysis only."
|
|
11548
|
-
).option("--json", "emit findings as JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
11862
|
+
).option("--json", "emit findings as JSON", false).option("--fix", "prepare simple automatic fixes (use with --dry-run or --apply)", false).option("--dry-run", "with --fix, show files that would change without writing", false).option("--apply", "with --fix, write simple fixes to disk", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
11549
11863
|
const root = findProjectRoot44(opts.dir);
|
|
11550
|
-
const
|
|
11864
|
+
const apply = Boolean(opts.fix && opts.apply);
|
|
11865
|
+
const dryRun = Boolean(opts.fix && (opts.dryRun || !opts.apply));
|
|
11866
|
+
const report = await lintMemoriesAsync(root, { fix: Boolean(opts.fix), apply });
|
|
11867
|
+
const findings = report.findings;
|
|
11551
11868
|
if (opts.json) {
|
|
11552
|
-
console.log(JSON.stringify({
|
|
11869
|
+
console.log(JSON.stringify({
|
|
11870
|
+
findings_count: findings.length,
|
|
11871
|
+
findings,
|
|
11872
|
+
fixes_count: report.fixes.length,
|
|
11873
|
+
fixes: report.fixes,
|
|
11874
|
+
fix_mode: opts.fix ? apply ? "apply" : "dry-run" : "off"
|
|
11875
|
+
}, null, 2));
|
|
11553
11876
|
process.exitCode = findings.some((f) => f.severity === "error") ? 1 : 0;
|
|
11554
11877
|
return;
|
|
11555
11878
|
}
|
|
@@ -11559,6 +11882,16 @@ function registerMemoryLint(parent) {
|
|
|
11559
11882
|
}
|
|
11560
11883
|
console.log(ui.bold(`memory lint (${findings.length} finding${findings.length === 1 ? "" : "s"})`) + `
|
|
11561
11884
|
`);
|
|
11885
|
+
if (opts.fix) {
|
|
11886
|
+
const mode = apply ? "apply" : dryRun ? "dry-run" : "dry-run";
|
|
11887
|
+
const verb = apply ? "changed" : "would change";
|
|
11888
|
+
console.log(ui.bold(`fix ${mode}: ${report.fixes.length} file${report.fixes.length === 1 ? "" : "s"} ${verb}`));
|
|
11889
|
+
for (const fix of report.fixes) {
|
|
11890
|
+
console.log(` ${ui.dim(fix.id)} ${fix.actions.join("; ")}`);
|
|
11891
|
+
console.log(ui.dim(` \u2192 ${fix.file}`));
|
|
11892
|
+
}
|
|
11893
|
+
console.log();
|
|
11894
|
+
}
|
|
11562
11895
|
const order = { error: 0, warn: 1, info: 2 };
|
|
11563
11896
|
findings.sort((a, b) => order[a.severity] - order[b.severity] || a.id.localeCompare(b.id));
|
|
11564
11897
|
for (const f of findings) {
|
|
@@ -11567,6 +11900,11 @@ function registerMemoryLint(parent) {
|
|
|
11567
11900
|
`${color(f.severity.padEnd(5))} ${ui.dim(f.code)} ${f.id}`
|
|
11568
11901
|
);
|
|
11569
11902
|
console.log(` ${f.message}`);
|
|
11903
|
+
if (f.suggested_anchors) {
|
|
11904
|
+
const pathHints = f.suggested_anchors.paths.length > 0 ? `paths: ${f.suggested_anchors.paths.join(", ")}` : "";
|
|
11905
|
+
const symbolHints = f.suggested_anchors.symbols.length > 0 ? `symbols: ${f.suggested_anchors.symbols.join(", ")}` : "";
|
|
11906
|
+
console.log(ui.dim(` suggested anchors: ${[pathHints, symbolHints].filter(Boolean).join(" \xB7 ")}`));
|
|
11907
|
+
}
|
|
11570
11908
|
console.log(ui.dim(` \u2192 ${f.file}`));
|
|
11571
11909
|
}
|
|
11572
11910
|
process.exitCode = findings.some((x) => x.severity === "error") ? 1 : 0;
|
|
@@ -11592,21 +11930,21 @@ function registerMemorySuggestTopic(memory2) {
|
|
|
11592
11930
|
}
|
|
11593
11931
|
|
|
11594
11932
|
// src/commands/resolve-project.ts
|
|
11595
|
-
import
|
|
11933
|
+
import path44 from "path";
|
|
11596
11934
|
import "commander";
|
|
11597
11935
|
import { resolveProjectInfo as resolveProjectInfo2 } from "@hiveai/core";
|
|
11598
11936
|
function registerResolveProject(program2) {
|
|
11599
11937
|
program2.command("resolve-project").description(
|
|
11600
11938
|
"Print JSON for hAIve project root resolution (HAIVE_PROJECT_ROOT, markers, .ai layout)."
|
|
11601
11939
|
).option("-d, --dir <dir>", "working directory", process.cwd()).action((opts) => {
|
|
11602
|
-
const info = resolveProjectInfo2({ cwd:
|
|
11940
|
+
const info = resolveProjectInfo2({ cwd: path44.resolve(opts.dir) });
|
|
11603
11941
|
console.log(JSON.stringify({ ok: true, info }, null, 2));
|
|
11604
11942
|
});
|
|
11605
11943
|
}
|
|
11606
11944
|
|
|
11607
11945
|
// src/commands/runtime-journal.ts
|
|
11608
11946
|
import { existsSync as existsSync65 } from "fs";
|
|
11609
|
-
import
|
|
11947
|
+
import path45 from "path";
|
|
11610
11948
|
import "commander";
|
|
11611
11949
|
import {
|
|
11612
11950
|
appendRuntimeJournalEntry as appendRuntimeJournalEntry3,
|
|
@@ -11620,15 +11958,15 @@ function registerRuntime(program2) {
|
|
|
11620
11958
|
);
|
|
11621
11959
|
const journal = runtime.command("journal").description("Append or read the machine-local session journal (NDJSON)");
|
|
11622
11960
|
journal.command("append").description("Append one JSON line to .ai/.runtime/session-journal.ndjson").argument("<message>", "short text to log").option("-k, --kind <kind>", "note | session_end | mcp", "note").option("-d, --dir <dir>", "project root", process.cwd()).action(async (message, opts) => {
|
|
11623
|
-
const root =
|
|
11961
|
+
const root = path45.resolve(opts.dir ?? process.cwd());
|
|
11624
11962
|
const paths = resolveHaivePaths41(findProjectRoot45(root));
|
|
11625
11963
|
const raw = opts.kind ?? "note";
|
|
11626
11964
|
const kind = ["note", "session_end", "mcp"].includes(raw) ? raw : "note";
|
|
11627
11965
|
await appendRuntimeJournalEntry3(paths, { kind, message });
|
|
11628
|
-
ui.success(`Appended to ${
|
|
11966
|
+
ui.success(`Appended to ${path45.relative(root, paths.runtimeDir)}/session-journal.ndjson`);
|
|
11629
11967
|
});
|
|
11630
11968
|
journal.command("tail").description("Print the last N entries from the runtime session journal as JSON").option("-n, --limit <n>", "number of lines", "30").option("-d, --dir <dir>", "project root", process.cwd()).action(async (opts) => {
|
|
11631
|
-
const root =
|
|
11969
|
+
const root = path45.resolve(opts.dir ?? process.cwd());
|
|
11632
11970
|
const paths = resolveHaivePaths41(findProjectRoot45(root));
|
|
11633
11971
|
const limit = Math.min(500, Math.max(1, parseInt(opts.limit, 10) || 30));
|
|
11634
11972
|
if (!existsSync65(paths.haiveDir)) {
|
|
@@ -11647,7 +11985,7 @@ function registerRuntime(program2) {
|
|
|
11647
11985
|
|
|
11648
11986
|
// src/commands/memory-timeline.ts
|
|
11649
11987
|
import { existsSync as existsSync66 } from "fs";
|
|
11650
|
-
import
|
|
11988
|
+
import path46 from "path";
|
|
11651
11989
|
import "commander";
|
|
11652
11990
|
import {
|
|
11653
11991
|
collectTimelineEntries as collectTimelineEntries2,
|
|
@@ -11663,7 +12001,7 @@ function registerMemoryTimeline(memory2) {
|
|
|
11663
12001
|
process.exitCode = 1;
|
|
11664
12002
|
return;
|
|
11665
12003
|
}
|
|
11666
|
-
const root =
|
|
12004
|
+
const root = path46.resolve(opts.dir ?? process.cwd());
|
|
11667
12005
|
const paths = resolveHaivePaths42(findProjectRoot46(root));
|
|
11668
12006
|
if (!existsSync66(paths.memoriesDir)) {
|
|
11669
12007
|
ui.error("No memories \u2014 run `haive init`.");
|
|
@@ -11684,7 +12022,7 @@ function registerMemoryTimeline(memory2) {
|
|
|
11684
12022
|
|
|
11685
12023
|
// src/commands/memory-conflict-candidates.ts
|
|
11686
12024
|
import { existsSync as existsSync67 } from "fs";
|
|
11687
|
-
import
|
|
12025
|
+
import path47 from "path";
|
|
11688
12026
|
import "commander";
|
|
11689
12027
|
import {
|
|
11690
12028
|
findLexicalConflictPairs as findLexicalConflictPairs2,
|
|
@@ -11706,7 +12044,7 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
11706
12044
|
"decision,architecture,convention,gotcha (lexical scan)",
|
|
11707
12045
|
"decision,architecture"
|
|
11708
12046
|
).option("--min-jaccard <x>", "minimum Jaccard for lexical pairs", "0.45").option("--max-pairs <n>", "cap lexical pairs", "20").option("--max-scan <n>", "max memories scanned (lexical)", "500").option("--max-topic-pairs <n>", "cap topic/status pairs", "20").action(async (opts) => {
|
|
11709
|
-
const root =
|
|
12047
|
+
const root = path47.resolve(opts.dir ?? process.cwd());
|
|
11710
12048
|
const paths = resolveHaivePaths43(findProjectRoot47(root));
|
|
11711
12049
|
if (!existsSync67(paths.memoriesDir)) {
|
|
11712
12050
|
ui.error("No memories \u2014 run `haive init`.");
|
|
@@ -11745,8 +12083,8 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
11745
12083
|
// src/commands/enforce.ts
|
|
11746
12084
|
import { execFileSync as execFileSync2, spawn as spawn5 } from "child_process";
|
|
11747
12085
|
import { existsSync as existsSync68 } from "fs";
|
|
11748
|
-
import { chmod as chmod2, mkdir as mkdir19, readFile as readFile18, rm as rm3, writeFile as
|
|
11749
|
-
import
|
|
12086
|
+
import { chmod as chmod2, mkdir as mkdir19, readFile as readFile18, rm as rm3, writeFile as writeFile33 } from "fs/promises";
|
|
12087
|
+
import path48 from "path";
|
|
11750
12088
|
import "commander";
|
|
11751
12089
|
import {
|
|
11752
12090
|
findProjectRoot as findProjectRoot48,
|
|
@@ -11796,7 +12134,7 @@ function registerEnforce(program2) {
|
|
|
11796
12134
|
if (opts.claude !== false) {
|
|
11797
12135
|
try {
|
|
11798
12136
|
const result = await installClaudeHooksAtPath(defaultClaudeSettingsPath("project", root));
|
|
11799
|
-
ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${
|
|
12137
|
+
ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${path48.relative(root, result.settingsPath)})`);
|
|
11800
12138
|
} catch (err) {
|
|
11801
12139
|
ui.warn(`Claude Code hooks not installed: ${err instanceof Error ? err.message : String(err)}`);
|
|
11802
12140
|
}
|
|
@@ -11804,26 +12142,26 @@ function registerEnforce(program2) {
|
|
|
11804
12142
|
ui.info("Agent-agnostic gates are now active at workflow level: MCP, git, CI, and optional client hooks.");
|
|
11805
12143
|
ui.info("Use `haive run -- <agent command>` for agents that do not expose blocking hooks.");
|
|
11806
12144
|
});
|
|
11807
|
-
enforce.command("status").description("Show whether this project has agent-agnostic hAIve enforcement installed.").option("-d, --dir <dir>", "project root").option("--json", "emit JSON", false).action(async (opts) => {
|
|
12145
|
+
enforce.command("status").description("Show whether this project has agent-agnostic hAIve enforcement installed.").option("-d, --dir <dir>", "project root").option("--explain", "group findings by blocking/review/info and show repair commands", false).option("--json", "emit JSON", false).action(async (opts) => {
|
|
11808
12146
|
const report = await buildEnforcementReport(opts.dir, "local");
|
|
11809
|
-
printReport(report, Boolean(opts.json));
|
|
12147
|
+
printReport(report, Boolean(opts.json), Boolean(opts.explain));
|
|
11810
12148
|
if (report.should_block) process.exitCode = 1;
|
|
11811
12149
|
});
|
|
11812
|
-
enforce.command("check").description("Run the hAIve policy gate. Intended for pre-commit, pre-push, wrappers, and any agent client.").option("-d, --dir <dir>", "project root").option("--stage <stage>", "local | pre-commit | pre-push | ci", "local").option("--json", "emit JSON", false).action(async (opts) => {
|
|
12150
|
+
enforce.command("check").description("Run the hAIve policy gate. Intended for pre-commit, pre-push, wrappers, and any agent client.").option("-d, --dir <dir>", "project root").option("--stage <stage>", "local | pre-commit | pre-push | ci", "local").option("--explain", "group findings by blocking/review/info and show repair commands", false).option("--json", "emit JSON", false).action(async (opts) => {
|
|
11813
12151
|
const report = await buildEnforcementReport(opts.dir, opts.stage ?? "local");
|
|
11814
|
-
printReport(report, Boolean(opts.json));
|
|
12152
|
+
printReport(report, Boolean(opts.json), Boolean(opts.explain));
|
|
11815
12153
|
if (report.should_block) process.exit(2);
|
|
11816
12154
|
});
|
|
11817
12155
|
enforce.command("cleanup").description("Remove generated hAIve runtime/cache artifacts that should not appear in commits.").option("-d, --dir <dir>", "project root").option("--dry-run", "print what would be removed without deleting", false).action(async (opts) => {
|
|
11818
12156
|
const root = findProjectRoot48(opts.dir);
|
|
11819
12157
|
const paths = resolveHaivePaths44(root);
|
|
11820
12158
|
const targets = [
|
|
11821
|
-
|
|
11822
|
-
|
|
12159
|
+
path48.join(paths.haiveDir, ".cache"),
|
|
12160
|
+
path48.join(paths.haiveDir, ".runtime")
|
|
11823
12161
|
];
|
|
11824
12162
|
for (const target of targets) {
|
|
11825
12163
|
if (!existsSync68(target)) continue;
|
|
11826
|
-
const rel =
|
|
12164
|
+
const rel = path48.relative(root, target);
|
|
11827
12165
|
if (opts.dryRun) ui.info(`would remove ${rel}`);
|
|
11828
12166
|
else {
|
|
11829
12167
|
await rm3(target, { recursive: true, force: true });
|
|
@@ -11831,9 +12169,9 @@ function registerEnforce(program2) {
|
|
|
11831
12169
|
}
|
|
11832
12170
|
}
|
|
11833
12171
|
});
|
|
11834
|
-
enforce.command("ci").description("CI entrypoint: fail if the repository violates hAIve enforcement policy.").option("-d, --dir <dir>", "project root").option("--json", "emit JSON", false).action(async (opts) => {
|
|
12172
|
+
enforce.command("ci").description("CI entrypoint: fail if the repository violates hAIve enforcement policy.").option("-d, --dir <dir>", "project root").option("--explain", "group findings by blocking/review/info and show repair commands", false).option("--json", "emit JSON", false).action(async (opts) => {
|
|
11835
12173
|
const report = await buildEnforcementReport(opts.dir, "ci");
|
|
11836
|
-
printReport(report, Boolean(opts.json));
|
|
12174
|
+
printReport(report, Boolean(opts.json), Boolean(opts.explain));
|
|
11837
12175
|
if (report.should_block) process.exit(2);
|
|
11838
12176
|
});
|
|
11839
12177
|
enforce.command("session-start").description("Claude Code SessionStart hook: inject briefing and write a local briefing marker.").option("-d, --dir <dir>", "project root").option("--task <text>", "task text to rank memories").option("--source <name>", "marker source", "claude-session-start").option("--session-id <id>", "agent session id").action(async (opts) => {
|
|
@@ -11946,7 +12284,7 @@ async function runWithEnforcement(command, args, opts) {
|
|
|
11946
12284
|
process.exit(2);
|
|
11947
12285
|
}
|
|
11948
12286
|
ui.info(`hAIve briefing marker created for wrapped agent session: ${sessionId}`);
|
|
11949
|
-
ui.info(`Briefing written to ${
|
|
12287
|
+
ui.info(`Briefing written to ${path48.relative(root, briefingFile)} and exported as HAIVE_BRIEFING_FILE`);
|
|
11950
12288
|
const child = spawn5(command, args, {
|
|
11951
12289
|
cwd: root,
|
|
11952
12290
|
stdio: "inherit",
|
|
@@ -11995,9 +12333,9 @@ async function writeWrapperBriefing(paths, sessionId, task) {
|
|
|
11995
12333
|
source: "haive-run",
|
|
11996
12334
|
memoryIds: briefing.memories.map((m) => m.id)
|
|
11997
12335
|
});
|
|
11998
|
-
const dir =
|
|
12336
|
+
const dir = path48.join(paths.runtimeDir, "enforcement", "briefings");
|
|
11999
12337
|
await mkdir19(dir, { recursive: true });
|
|
12000
|
-
const file =
|
|
12338
|
+
const file = path48.join(dir, `${sessionId}.md`);
|
|
12001
12339
|
const parts = [
|
|
12002
12340
|
"# hAIve Briefing",
|
|
12003
12341
|
"",
|
|
@@ -12015,7 +12353,7 @@ async function writeWrapperBriefing(paths, sessionId, task) {
|
|
|
12015
12353
|
if (briefing.setup_warnings.length > 0) {
|
|
12016
12354
|
parts.push("", "## Setup Warnings", ...briefing.setup_warnings.map((w) => `- ${w}`));
|
|
12017
12355
|
}
|
|
12018
|
-
await
|
|
12356
|
+
await writeFile33(file, parts.join("\n") + "\n", "utf8");
|
|
12019
12357
|
return file;
|
|
12020
12358
|
}
|
|
12021
12359
|
async function buildEnforcementReport(dir, stage, sessionId) {
|
|
@@ -12026,7 +12364,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
12026
12364
|
const mode = config.enforcement?.mode ?? "strict";
|
|
12027
12365
|
const findings = [];
|
|
12028
12366
|
if (!initialized) {
|
|
12029
|
-
return {
|
|
12367
|
+
return withCategories({
|
|
12030
12368
|
root,
|
|
12031
12369
|
initialized,
|
|
12032
12370
|
mode,
|
|
@@ -12039,19 +12377,19 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
12039
12377
|
fix: "Run `haive init` or `haive enforce install`.",
|
|
12040
12378
|
impact: 100
|
|
12041
12379
|
}]
|
|
12042
|
-
};
|
|
12380
|
+
});
|
|
12043
12381
|
}
|
|
12044
12382
|
if (mode === "off") {
|
|
12045
|
-
return {
|
|
12383
|
+
return withCategories({
|
|
12046
12384
|
root,
|
|
12047
12385
|
initialized,
|
|
12048
12386
|
mode,
|
|
12049
12387
|
score: buildScore([], config.enforcement?.scoreThreshold),
|
|
12050
12388
|
should_block: false,
|
|
12051
12389
|
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
12052
|
-
};
|
|
12390
|
+
});
|
|
12053
12391
|
}
|
|
12054
|
-
findings.push(...await inspectIntegrationVersions(root, "0.9.
|
|
12392
|
+
findings.push(...await inspectIntegrationVersions(root, "0.9.17"));
|
|
12055
12393
|
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
12056
12394
|
const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
|
|
12057
12395
|
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
|
|
@@ -12101,13 +12439,23 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
12101
12439
|
});
|
|
12102
12440
|
}
|
|
12103
12441
|
const hasErrors = findings.some((f) => f.severity === "error");
|
|
12104
|
-
return {
|
|
12442
|
+
return withCategories({
|
|
12105
12443
|
root,
|
|
12106
12444
|
initialized,
|
|
12107
12445
|
mode,
|
|
12108
12446
|
score: buildScore(findings, config.enforcement?.scoreThreshold),
|
|
12109
12447
|
should_block: mode === "strict" && hasErrors,
|
|
12110
12448
|
findings
|
|
12449
|
+
});
|
|
12450
|
+
}
|
|
12451
|
+
function withCategories(report) {
|
|
12452
|
+
return {
|
|
12453
|
+
...report,
|
|
12454
|
+
categories: {
|
|
12455
|
+
blocking: report.findings.filter((f) => f.severity === "error"),
|
|
12456
|
+
review: report.findings.filter((f) => f.severity === "warn"),
|
|
12457
|
+
info: report.findings.filter((f) => f.severity === "info" || f.severity === "ok")
|
|
12458
|
+
}
|
|
12111
12459
|
};
|
|
12112
12460
|
}
|
|
12113
12461
|
async function hasRecentSessionRecap(paths) {
|
|
@@ -12168,7 +12516,7 @@ async function verifyDecisionCoverage(paths, stage, sessionId) {
|
|
|
12168
12516
|
const relevant = all.map(({ memory: memory2 }) => memory2).filter((memory2) => {
|
|
12169
12517
|
const fm = memory2.frontmatter;
|
|
12170
12518
|
if (!policyTypes.has(fm.type)) return false;
|
|
12171
|
-
if (fm.status
|
|
12519
|
+
if (fm.status !== "validated") return false;
|
|
12172
12520
|
return memoryMatchesAnchorPaths6(memory2, changedFiles);
|
|
12173
12521
|
});
|
|
12174
12522
|
if (relevant.length === 0) {
|
|
@@ -12251,7 +12599,7 @@ async function inspectIntegrationVersions(root, expectedVersion) {
|
|
|
12251
12599
|
];
|
|
12252
12600
|
const findings = [];
|
|
12253
12601
|
for (const rel of files) {
|
|
12254
|
-
const file =
|
|
12602
|
+
const file = path48.join(root, rel);
|
|
12255
12603
|
if (!existsSync68(file)) continue;
|
|
12256
12604
|
const text = await readFile18(file, "utf8").catch(() => "");
|
|
12257
12605
|
for (const bin of extractAbsoluteHaiveBins2(text)) {
|
|
@@ -12318,7 +12666,9 @@ async function getChangedFiles(root, stage) {
|
|
|
12318
12666
|
if (file) files.add(file);
|
|
12319
12667
|
}
|
|
12320
12668
|
}
|
|
12321
|
-
return [...files].filter(
|
|
12669
|
+
return [...files].filter(
|
|
12670
|
+
(file) => !file.startsWith(".ai/.runtime/") && !file.startsWith(".ai/.cache/") && !file.startsWith(".ai/.usage/") && file !== ".ai/.usage/tool-usage.jsonl"
|
|
12671
|
+
);
|
|
12322
12672
|
}
|
|
12323
12673
|
function buildScore(findings, threshold = 80) {
|
|
12324
12674
|
const checks = {
|
|
@@ -12339,8 +12689,8 @@ function buildScore(findings, threshold = 80) {
|
|
|
12339
12689
|
};
|
|
12340
12690
|
}
|
|
12341
12691
|
async function installGitEnforcement(root) {
|
|
12342
|
-
const hooksDir =
|
|
12343
|
-
if (!existsSync68(
|
|
12692
|
+
const hooksDir = path48.join(root, ".git", "hooks");
|
|
12693
|
+
if (!existsSync68(path48.join(root, ".git"))) {
|
|
12344
12694
|
ui.warn("No .git directory found; git enforcement hooks skipped.");
|
|
12345
12695
|
return;
|
|
12346
12696
|
}
|
|
@@ -12362,31 +12712,31 @@ haive enforce check --stage pre-push --dir . || exit $?
|
|
|
12362
12712
|
}
|
|
12363
12713
|
];
|
|
12364
12714
|
for (const hook of hooks) {
|
|
12365
|
-
const file =
|
|
12715
|
+
const file = path48.join(hooksDir, hook.name);
|
|
12366
12716
|
if (existsSync68(file)) {
|
|
12367
12717
|
const current = await readFile18(file, "utf8").catch(() => "");
|
|
12368
12718
|
if (current.includes(ENFORCE_HOOK_MARKER)) {
|
|
12369
|
-
await
|
|
12719
|
+
await writeFile33(file, hook.body, "utf8");
|
|
12370
12720
|
} else {
|
|
12371
|
-
await
|
|
12721
|
+
await writeFile33(file, `${current.trimEnd()}
|
|
12372
12722
|
|
|
12373
12723
|
${hook.body}`, "utf8");
|
|
12374
12724
|
}
|
|
12375
12725
|
} else {
|
|
12376
|
-
await
|
|
12726
|
+
await writeFile33(file, hook.body, "utf8");
|
|
12377
12727
|
}
|
|
12378
12728
|
await chmod2(file, 493);
|
|
12379
12729
|
}
|
|
12380
12730
|
ui.success("Installed blocking git enforcement hooks: pre-commit, pre-push");
|
|
12381
12731
|
}
|
|
12382
12732
|
async function installCiEnforcement(root) {
|
|
12383
|
-
const workflowPath =
|
|
12384
|
-
await mkdir19(
|
|
12733
|
+
const workflowPath = path48.join(root, ".github", "workflows", "haive-enforcement.yml");
|
|
12734
|
+
await mkdir19(path48.dirname(workflowPath), { recursive: true });
|
|
12385
12735
|
if (existsSync68(workflowPath)) {
|
|
12386
12736
|
ui.info("GitHub Actions enforcement workflow already exists \u2014 skipped");
|
|
12387
12737
|
return;
|
|
12388
12738
|
}
|
|
12389
|
-
await
|
|
12739
|
+
await writeFile33(workflowPath, `name: haive-enforcement
|
|
12390
12740
|
|
|
12391
12741
|
on:
|
|
12392
12742
|
pull_request:
|
|
@@ -12410,9 +12760,9 @@ jobs:
|
|
|
12410
12760
|
- name: Enforce hAIve policy
|
|
12411
12761
|
run: haive enforce ci
|
|
12412
12762
|
`, "utf8");
|
|
12413
|
-
ui.success(`Created ${
|
|
12763
|
+
ui.success(`Created ${path48.relative(root, workflowPath)}`);
|
|
12414
12764
|
}
|
|
12415
|
-
function printReport(report, json) {
|
|
12765
|
+
function printReport(report, json, explain = false) {
|
|
12416
12766
|
if (json) {
|
|
12417
12767
|
console.log(JSON.stringify(report, null, 2));
|
|
12418
12768
|
return;
|
|
@@ -12420,14 +12770,32 @@ function printReport(report, json) {
|
|
|
12420
12770
|
console.log(ui.bold(`hAIve enforcement \u2014 ${report.mode}`));
|
|
12421
12771
|
console.log(ui.dim(` root: ${report.root}`));
|
|
12422
12772
|
console.log(ui.dim(` score: ${report.score.score}% / threshold ${report.score.threshold}%`));
|
|
12423
|
-
|
|
12424
|
-
|
|
12425
|
-
|
|
12426
|
-
|
|
12773
|
+
if (explain) {
|
|
12774
|
+
printFindingGroup("Blocking", report.categories.blocking, "error");
|
|
12775
|
+
printFindingGroup("Review", report.categories.review, "warn");
|
|
12776
|
+
printFindingGroup("Info", report.categories.info, "info");
|
|
12777
|
+
} else {
|
|
12778
|
+
for (const finding of report.findings) printFinding(finding);
|
|
12427
12779
|
}
|
|
12428
12780
|
if (report.should_block) ui.error("hAIve enforcement gate failed.");
|
|
12429
12781
|
else ui.success("hAIve enforcement gate passed.");
|
|
12430
12782
|
}
|
|
12783
|
+
function printFindingGroup(title, findings, tone) {
|
|
12784
|
+
if (findings.length === 0) return;
|
|
12785
|
+
console.log();
|
|
12786
|
+
const heading = tone === "error" ? ui.red(title) : tone === "warn" ? ui.yellow(title) : ui.bold(title);
|
|
12787
|
+
console.log(ui.bold(`${heading} (${findings.length})`));
|
|
12788
|
+
const scoreFinding = findings.find((f) => f.code === "enforcement-score-below-threshold");
|
|
12789
|
+
for (const finding of findings.filter((f) => f.code !== "enforcement-score-below-threshold")) {
|
|
12790
|
+
printFinding(finding, true);
|
|
12791
|
+
}
|
|
12792
|
+
if (scoreFinding) printFinding(scoreFinding, true);
|
|
12793
|
+
}
|
|
12794
|
+
function printFinding(finding, explain = false) {
|
|
12795
|
+
const marker = finding.severity === "error" ? ui.red("\u2717") : finding.severity === "warn" ? ui.yellow("\u26A0") : finding.severity === "ok" ? ui.green("\u2713") : ui.dim("\u2022");
|
|
12796
|
+
console.log(`${marker} ${finding.code}: ${finding.message}`);
|
|
12797
|
+
if (finding.fix) console.log(ui.dim(`${explain ? " repair: " : " fix: "}${finding.fix}`));
|
|
12798
|
+
}
|
|
12431
12799
|
async function readHookPayload() {
|
|
12432
12800
|
const raw = await readStdin2(MAX_STDIN_BYTES2);
|
|
12433
12801
|
if (!raw.trim()) return {};
|
|
@@ -12509,7 +12877,7 @@ function registerRun(program2) {
|
|
|
12509
12877
|
|
|
12510
12878
|
// src/index.ts
|
|
12511
12879
|
var program = new Command51();
|
|
12512
|
-
program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.
|
|
12880
|
+
program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.17").option("--advanced", "show maintenance and experimental commands in help");
|
|
12513
12881
|
registerInit(program);
|
|
12514
12882
|
registerWelcome(program);
|
|
12515
12883
|
registerResolveProject(program);
|