@hiveai/cli 0.9.15 → 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/README.md +16 -1
- package/dist/index.js +573 -96
- 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: [
|
|
@@ -6356,7 +6526,7 @@ function jsonResult(data) {
|
|
|
6356
6526
|
]
|
|
6357
6527
|
};
|
|
6358
6528
|
}
|
|
6359
|
-
var ENFORCEMENT_PROFILE_TOOLS =
|
|
6529
|
+
var ENFORCEMENT_PROFILE_TOOLS = [
|
|
6360
6530
|
"get_briefing",
|
|
6361
6531
|
"mem_save",
|
|
6362
6532
|
"mem_tried",
|
|
@@ -6367,7 +6537,47 @@ var ENFORCEMENT_PROFILE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
6367
6537
|
"code_map",
|
|
6368
6538
|
"pre_commit_check",
|
|
6369
6539
|
"mem_session_end"
|
|
6370
|
-
]
|
|
6540
|
+
];
|
|
6541
|
+
var MAINTENANCE_PROFILE_TOOLS = [
|
|
6542
|
+
...ENFORCEMENT_PROFILE_TOOLS,
|
|
6543
|
+
"mem_suggest_topic",
|
|
6544
|
+
"mem_for_files",
|
|
6545
|
+
"mem_list",
|
|
6546
|
+
"get_project_context",
|
|
6547
|
+
"bootstrap_project_save",
|
|
6548
|
+
"mem_resolve_project",
|
|
6549
|
+
"mem_update",
|
|
6550
|
+
"mem_approve",
|
|
6551
|
+
"mem_reject",
|
|
6552
|
+
"mem_pending",
|
|
6553
|
+
"mem_delete",
|
|
6554
|
+
"mem_diff",
|
|
6555
|
+
"get_recap",
|
|
6556
|
+
"code_search",
|
|
6557
|
+
"anti_patterns_check",
|
|
6558
|
+
"mem_distill",
|
|
6559
|
+
"mem_timeline",
|
|
6560
|
+
"mem_conflict_candidates"
|
|
6561
|
+
];
|
|
6562
|
+
var EXPERIMENTAL_PROFILE_TOOLS = [
|
|
6563
|
+
...MAINTENANCE_PROFILE_TOOLS,
|
|
6564
|
+
"mem_observe",
|
|
6565
|
+
"why_this_file",
|
|
6566
|
+
"why_this_decision",
|
|
6567
|
+
"mem_conflicts_with",
|
|
6568
|
+
"pattern_detect",
|
|
6569
|
+
"runtime_journal_append",
|
|
6570
|
+
"runtime_journal_tail"
|
|
6571
|
+
];
|
|
6572
|
+
var TOOL_PROFILES = {
|
|
6573
|
+
enforcement: new Set(ENFORCEMENT_PROFILE_TOOLS),
|
|
6574
|
+
maintenance: new Set(MAINTENANCE_PROFILE_TOOLS),
|
|
6575
|
+
experimental: new Set(EXPERIMENTAL_PROFILE_TOOLS)
|
|
6576
|
+
};
|
|
6577
|
+
function getAllowedToolsForProfile(profile) {
|
|
6578
|
+
if (profile === "full") return TOOL_PROFILES.experimental;
|
|
6579
|
+
return TOOL_PROFILES[profile] ?? TOOL_PROFILES.enforcement;
|
|
6580
|
+
}
|
|
6371
6581
|
var BRIEFING_TOOLS = /* @__PURE__ */ new Set(["get_briefing", "mem_relevant_to"]);
|
|
6372
6582
|
var MUTATING_TOOLS = /* @__PURE__ */ new Set([
|
|
6373
6583
|
"mem_save",
|
|
@@ -6394,7 +6604,8 @@ function createHaiveServer(options = {}) {
|
|
|
6394
6604
|
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
6395
6605
|
{ capabilities: { tools: {}, prompts: {} } }
|
|
6396
6606
|
);
|
|
6397
|
-
const
|
|
6607
|
+
const allowedTools = getAllowedToolsForProfile(toolProfile);
|
|
6608
|
+
const shouldRegisterTool = (name) => allowedTools.has(name);
|
|
6398
6609
|
const registerTool = (name, description, schema, handler) => {
|
|
6399
6610
|
if (!shouldRegisterTool(name)) return;
|
|
6400
6611
|
const tool = server.tool.bind(server);
|
|
@@ -6418,7 +6629,11 @@ function createHaiveServer(options = {}) {
|
|
|
6418
6629
|
}
|
|
6419
6630
|
);
|
|
6420
6631
|
};
|
|
6421
|
-
const shouldRegisterPrompt = (name) =>
|
|
6632
|
+
const shouldRegisterPrompt = (name) => {
|
|
6633
|
+
if (name === "bootstrap_project" || name === "post_task") return true;
|
|
6634
|
+
if (name === "import_docs") return toolProfile !== "enforcement";
|
|
6635
|
+
return toolProfile === "experimental" || toolProfile === "full";
|
|
6636
|
+
};
|
|
6422
6637
|
registerTool(
|
|
6423
6638
|
"mem_save",
|
|
6424
6639
|
[
|
|
@@ -10735,7 +10950,7 @@ var MS_PER_DAY3 = 24 * 60 * 60 * 1e3;
|
|
|
10735
10950
|
function registerDoctor(program2) {
|
|
10736
10951
|
program2.command("doctor").description(
|
|
10737
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."
|
|
10738
|
-
).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) => {
|
|
10739
10954
|
const root = findProjectRoot40(opts.dir);
|
|
10740
10955
|
const paths = resolveHaivePaths36(root);
|
|
10741
10956
|
const findings = [];
|
|
@@ -10907,14 +11122,14 @@ function registerDoctor(program2) {
|
|
|
10907
11122
|
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
|
|
10908
11123
|
});
|
|
10909
11124
|
}
|
|
10910
|
-
findings.push(...await collectInstallFindings(root, "0.9.
|
|
11125
|
+
findings.push(...await collectInstallFindings(root, "0.9.17"));
|
|
10911
11126
|
try {
|
|
10912
11127
|
const legacyRaw = execSync3("haive-mcp --version", {
|
|
10913
11128
|
encoding: "utf8",
|
|
10914
11129
|
timeout: 3e3,
|
|
10915
11130
|
stdio: ["ignore", "pipe", "ignore"]
|
|
10916
11131
|
}).trim();
|
|
10917
|
-
const cliVersion = "0.9.
|
|
11132
|
+
const cliVersion = "0.9.17";
|
|
10918
11133
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
10919
11134
|
findings.push({
|
|
10920
11135
|
severity: "warn",
|
|
@@ -10931,33 +11146,105 @@ npm uninstall -g @hiveai/mcp`
|
|
|
10931
11146
|
});
|
|
10932
11147
|
}
|
|
10933
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);
|
|
10934
11154
|
if (opts.json) {
|
|
10935
|
-
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));
|
|
10936
11162
|
return;
|
|
10937
11163
|
}
|
|
10938
|
-
if (
|
|
11164
|
+
if (classified.length === 0) {
|
|
10939
11165
|
ui.success("hAIve doctor \u2014 no issues found.");
|
|
10940
11166
|
return;
|
|
10941
11167
|
}
|
|
10942
|
-
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
|
+
);
|
|
10943
11174
|
console.log();
|
|
10944
|
-
const
|
|
10945
|
-
|
|
10946
|
-
|
|
10947
|
-
|
|
10948
|
-
|
|
10949
|
-
|
|
10950
|
-
|
|
10951
|
-
|
|
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
|
+
}
|
|
10952
11196
|
}
|
|
10953
11197
|
}
|
|
10954
11198
|
}
|
|
10955
|
-
}
|
|
10956
|
-
if (!opts.fix && findings.some((f) => f.fix)) {
|
|
10957
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)) {
|
|
10958
11206
|
ui.info("Re-run with --fix to see suggested commands.");
|
|
10959
11207
|
}
|
|
10960
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
|
+
}
|
|
10961
11248
|
function isSearchTool(name) {
|
|
10962
11249
|
return ["mem_search", "code_search", "mem_relevant_to", "get_briefing"].includes(name);
|
|
10963
11250
|
}
|
|
@@ -11276,7 +11563,11 @@ function printWarnings(title, warnings, tone) {
|
|
|
11276
11563
|
console.log(` ${ui.dim(line)}`);
|
|
11277
11564
|
}
|
|
11278
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
|
+
}
|
|
11279
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}`);
|
|
11280
11571
|
}
|
|
11281
11572
|
console.log();
|
|
11282
11573
|
}
|
|
@@ -11363,20 +11654,26 @@ function registerWelcome(program2) {
|
|
|
11363
11654
|
|
|
11364
11655
|
// src/commands/memory-lint.ts
|
|
11365
11656
|
import { existsSync as existsSync64 } from "fs";
|
|
11657
|
+
import { writeFile as writeFile31 } from "fs/promises";
|
|
11658
|
+
import path43 from "path";
|
|
11366
11659
|
import "commander";
|
|
11367
11660
|
import {
|
|
11368
11661
|
findProjectRoot as findProjectRoot44,
|
|
11369
11662
|
getUsage as getUsage20,
|
|
11663
|
+
loadCodeMap as loadCodeMap6,
|
|
11370
11664
|
loadMemoriesFromDir as loadMemoriesFromDir35,
|
|
11371
11665
|
loadUsageIndex as loadUsageIndex26,
|
|
11372
|
-
resolveHaivePaths as resolveHaivePaths40
|
|
11666
|
+
resolveHaivePaths as resolveHaivePaths40,
|
|
11667
|
+
serializeMemory as serializeMemory26
|
|
11373
11668
|
} from "@hiveai/core";
|
|
11374
|
-
async function lintMemoriesAsync(root) {
|
|
11669
|
+
async function lintMemoriesAsync(root, options = {}) {
|
|
11375
11670
|
const paths = resolveHaivePaths40(root);
|
|
11376
11671
|
const out = [];
|
|
11377
|
-
|
|
11672
|
+
const fixes = [];
|
|
11673
|
+
if (!existsSync64(paths.memoriesDir)) return { findings: out, fixes };
|
|
11378
11674
|
const loaded = await loadMemoriesFromDir35(paths.memoriesDir);
|
|
11379
11675
|
const usage = await loadUsageIndex26(paths);
|
|
11676
|
+
const codeMap = await loadCodeMap6(paths);
|
|
11380
11677
|
const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
|
|
11381
11678
|
const actionableWords = /\b(always|never|prefer|use|avoid|because|instead|why|rationale|do not|must|should)\b/i;
|
|
11382
11679
|
for (const { filePath, memory: memory2 } of loaded) {
|
|
@@ -11402,13 +11699,15 @@ async function lintMemoriesAsync(root) {
|
|
|
11402
11699
|
message: "Record does not contain obvious action/rationale words. Add the concrete rule, why it exists, and what to do instead."
|
|
11403
11700
|
});
|
|
11404
11701
|
}
|
|
11702
|
+
const suggestedAnchors = suggestAnchors(root, { filePath, memory: memory2 }, codeMap);
|
|
11405
11703
|
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated") {
|
|
11406
11704
|
out.push({
|
|
11407
11705
|
file: filePath,
|
|
11408
11706
|
id: fm.id,
|
|
11409
11707
|
severity: "warn",
|
|
11410
11708
|
code: "MISSING_ANCHOR",
|
|
11411
|
-
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 } : {}
|
|
11412
11711
|
});
|
|
11413
11712
|
}
|
|
11414
11713
|
if (fm.status === "stale" && !fm.stale_reason) {
|
|
@@ -11449,6 +11748,34 @@ async function lintMemoriesAsync(root) {
|
|
|
11449
11748
|
message: "Validated record has never been surfaced/read. Consider improving tags/anchors or archiving it if it is not useful."
|
|
11450
11749
|
});
|
|
11451
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
|
+
}
|
|
11452
11779
|
}
|
|
11453
11780
|
for (const dup of nearDuplicatePairs(loaded)) {
|
|
11454
11781
|
out.push({
|
|
@@ -11459,7 +11786,39 @@ async function lintMemoriesAsync(root) {
|
|
|
11459
11786
|
message: `Body overlaps ~${Math.round(dup.score * 100)}% with ${dup.otherId}. Merge or deprecate one record to reduce briefing noise.`
|
|
11460
11787
|
});
|
|
11461
11788
|
}
|
|
11462
|
-
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
|
+
};
|
|
11463
11822
|
}
|
|
11464
11823
|
function nearDuplicatePairs(loaded) {
|
|
11465
11824
|
const out = [];
|
|
@@ -11500,11 +11859,20 @@ function jaccard2(a, b) {
|
|
|
11500
11859
|
function registerMemoryLint(parent) {
|
|
11501
11860
|
parent.command("lint").description(
|
|
11502
11861
|
"Heuristic corpus checks (anchors on key types, headings, verbosity). Static analysis only."
|
|
11503
|
-
).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) => {
|
|
11504
11863
|
const root = findProjectRoot44(opts.dir);
|
|
11505
|
-
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;
|
|
11506
11868
|
if (opts.json) {
|
|
11507
|
-
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));
|
|
11508
11876
|
process.exitCode = findings.some((f) => f.severity === "error") ? 1 : 0;
|
|
11509
11877
|
return;
|
|
11510
11878
|
}
|
|
@@ -11514,6 +11882,16 @@ function registerMemoryLint(parent) {
|
|
|
11514
11882
|
}
|
|
11515
11883
|
console.log(ui.bold(`memory lint (${findings.length} finding${findings.length === 1 ? "" : "s"})`) + `
|
|
11516
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
|
+
}
|
|
11517
11895
|
const order = { error: 0, warn: 1, info: 2 };
|
|
11518
11896
|
findings.sort((a, b) => order[a.severity] - order[b.severity] || a.id.localeCompare(b.id));
|
|
11519
11897
|
for (const f of findings) {
|
|
@@ -11522,6 +11900,11 @@ function registerMemoryLint(parent) {
|
|
|
11522
11900
|
`${color(f.severity.padEnd(5))} ${ui.dim(f.code)} ${f.id}`
|
|
11523
11901
|
);
|
|
11524
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
|
+
}
|
|
11525
11908
|
console.log(ui.dim(` \u2192 ${f.file}`));
|
|
11526
11909
|
}
|
|
11527
11910
|
process.exitCode = findings.some((x) => x.severity === "error") ? 1 : 0;
|
|
@@ -11547,21 +11930,21 @@ function registerMemorySuggestTopic(memory2) {
|
|
|
11547
11930
|
}
|
|
11548
11931
|
|
|
11549
11932
|
// src/commands/resolve-project.ts
|
|
11550
|
-
import
|
|
11933
|
+
import path44 from "path";
|
|
11551
11934
|
import "commander";
|
|
11552
11935
|
import { resolveProjectInfo as resolveProjectInfo2 } from "@hiveai/core";
|
|
11553
11936
|
function registerResolveProject(program2) {
|
|
11554
11937
|
program2.command("resolve-project").description(
|
|
11555
11938
|
"Print JSON for hAIve project root resolution (HAIVE_PROJECT_ROOT, markers, .ai layout)."
|
|
11556
11939
|
).option("-d, --dir <dir>", "working directory", process.cwd()).action((opts) => {
|
|
11557
|
-
const info = resolveProjectInfo2({ cwd:
|
|
11940
|
+
const info = resolveProjectInfo2({ cwd: path44.resolve(opts.dir) });
|
|
11558
11941
|
console.log(JSON.stringify({ ok: true, info }, null, 2));
|
|
11559
11942
|
});
|
|
11560
11943
|
}
|
|
11561
11944
|
|
|
11562
11945
|
// src/commands/runtime-journal.ts
|
|
11563
11946
|
import { existsSync as existsSync65 } from "fs";
|
|
11564
|
-
import
|
|
11947
|
+
import path45 from "path";
|
|
11565
11948
|
import "commander";
|
|
11566
11949
|
import {
|
|
11567
11950
|
appendRuntimeJournalEntry as appendRuntimeJournalEntry3,
|
|
@@ -11575,15 +11958,15 @@ function registerRuntime(program2) {
|
|
|
11575
11958
|
);
|
|
11576
11959
|
const journal = runtime.command("journal").description("Append or read the machine-local session journal (NDJSON)");
|
|
11577
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) => {
|
|
11578
|
-
const root =
|
|
11961
|
+
const root = path45.resolve(opts.dir ?? process.cwd());
|
|
11579
11962
|
const paths = resolveHaivePaths41(findProjectRoot45(root));
|
|
11580
11963
|
const raw = opts.kind ?? "note";
|
|
11581
11964
|
const kind = ["note", "session_end", "mcp"].includes(raw) ? raw : "note";
|
|
11582
11965
|
await appendRuntimeJournalEntry3(paths, { kind, message });
|
|
11583
|
-
ui.success(`Appended to ${
|
|
11966
|
+
ui.success(`Appended to ${path45.relative(root, paths.runtimeDir)}/session-journal.ndjson`);
|
|
11584
11967
|
});
|
|
11585
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) => {
|
|
11586
|
-
const root =
|
|
11969
|
+
const root = path45.resolve(opts.dir ?? process.cwd());
|
|
11587
11970
|
const paths = resolveHaivePaths41(findProjectRoot45(root));
|
|
11588
11971
|
const limit = Math.min(500, Math.max(1, parseInt(opts.limit, 10) || 30));
|
|
11589
11972
|
if (!existsSync65(paths.haiveDir)) {
|
|
@@ -11602,7 +11985,7 @@ function registerRuntime(program2) {
|
|
|
11602
11985
|
|
|
11603
11986
|
// src/commands/memory-timeline.ts
|
|
11604
11987
|
import { existsSync as existsSync66 } from "fs";
|
|
11605
|
-
import
|
|
11988
|
+
import path46 from "path";
|
|
11606
11989
|
import "commander";
|
|
11607
11990
|
import {
|
|
11608
11991
|
collectTimelineEntries as collectTimelineEntries2,
|
|
@@ -11618,7 +12001,7 @@ function registerMemoryTimeline(memory2) {
|
|
|
11618
12001
|
process.exitCode = 1;
|
|
11619
12002
|
return;
|
|
11620
12003
|
}
|
|
11621
|
-
const root =
|
|
12004
|
+
const root = path46.resolve(opts.dir ?? process.cwd());
|
|
11622
12005
|
const paths = resolveHaivePaths42(findProjectRoot46(root));
|
|
11623
12006
|
if (!existsSync66(paths.memoriesDir)) {
|
|
11624
12007
|
ui.error("No memories \u2014 run `haive init`.");
|
|
@@ -11639,7 +12022,7 @@ function registerMemoryTimeline(memory2) {
|
|
|
11639
12022
|
|
|
11640
12023
|
// src/commands/memory-conflict-candidates.ts
|
|
11641
12024
|
import { existsSync as existsSync67 } from "fs";
|
|
11642
|
-
import
|
|
12025
|
+
import path47 from "path";
|
|
11643
12026
|
import "commander";
|
|
11644
12027
|
import {
|
|
11645
12028
|
findLexicalConflictPairs as findLexicalConflictPairs2,
|
|
@@ -11661,7 +12044,7 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
11661
12044
|
"decision,architecture,convention,gotcha (lexical scan)",
|
|
11662
12045
|
"decision,architecture"
|
|
11663
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) => {
|
|
11664
|
-
const root =
|
|
12047
|
+
const root = path47.resolve(opts.dir ?? process.cwd());
|
|
11665
12048
|
const paths = resolveHaivePaths43(findProjectRoot47(root));
|
|
11666
12049
|
if (!existsSync67(paths.memoriesDir)) {
|
|
11667
12050
|
ui.error("No memories \u2014 run `haive init`.");
|
|
@@ -11700,8 +12083,8 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
11700
12083
|
// src/commands/enforce.ts
|
|
11701
12084
|
import { execFileSync as execFileSync2, spawn as spawn5 } from "child_process";
|
|
11702
12085
|
import { existsSync as existsSync68 } from "fs";
|
|
11703
|
-
import { chmod as chmod2, mkdir as mkdir19, readFile as readFile18, rm as rm3, writeFile as
|
|
11704
|
-
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";
|
|
11705
12088
|
import "commander";
|
|
11706
12089
|
import {
|
|
11707
12090
|
findProjectRoot as findProjectRoot48,
|
|
@@ -11751,7 +12134,7 @@ function registerEnforce(program2) {
|
|
|
11751
12134
|
if (opts.claude !== false) {
|
|
11752
12135
|
try {
|
|
11753
12136
|
const result = await installClaudeHooksAtPath(defaultClaudeSettingsPath("project", root));
|
|
11754
|
-
ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${
|
|
12137
|
+
ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${path48.relative(root, result.settingsPath)})`);
|
|
11755
12138
|
} catch (err) {
|
|
11756
12139
|
ui.warn(`Claude Code hooks not installed: ${err instanceof Error ? err.message : String(err)}`);
|
|
11757
12140
|
}
|
|
@@ -11759,26 +12142,26 @@ function registerEnforce(program2) {
|
|
|
11759
12142
|
ui.info("Agent-agnostic gates are now active at workflow level: MCP, git, CI, and optional client hooks.");
|
|
11760
12143
|
ui.info("Use `haive run -- <agent command>` for agents that do not expose blocking hooks.");
|
|
11761
12144
|
});
|
|
11762
|
-
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) => {
|
|
11763
12146
|
const report = await buildEnforcementReport(opts.dir, "local");
|
|
11764
|
-
printReport(report, Boolean(opts.json));
|
|
12147
|
+
printReport(report, Boolean(opts.json), Boolean(opts.explain));
|
|
11765
12148
|
if (report.should_block) process.exitCode = 1;
|
|
11766
12149
|
});
|
|
11767
|
-
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) => {
|
|
11768
12151
|
const report = await buildEnforcementReport(opts.dir, opts.stage ?? "local");
|
|
11769
|
-
printReport(report, Boolean(opts.json));
|
|
12152
|
+
printReport(report, Boolean(opts.json), Boolean(opts.explain));
|
|
11770
12153
|
if (report.should_block) process.exit(2);
|
|
11771
12154
|
});
|
|
11772
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) => {
|
|
11773
12156
|
const root = findProjectRoot48(opts.dir);
|
|
11774
12157
|
const paths = resolveHaivePaths44(root);
|
|
11775
12158
|
const targets = [
|
|
11776
|
-
|
|
11777
|
-
|
|
12159
|
+
path48.join(paths.haiveDir, ".cache"),
|
|
12160
|
+
path48.join(paths.haiveDir, ".runtime")
|
|
11778
12161
|
];
|
|
11779
12162
|
for (const target of targets) {
|
|
11780
12163
|
if (!existsSync68(target)) continue;
|
|
11781
|
-
const rel =
|
|
12164
|
+
const rel = path48.relative(root, target);
|
|
11782
12165
|
if (opts.dryRun) ui.info(`would remove ${rel}`);
|
|
11783
12166
|
else {
|
|
11784
12167
|
await rm3(target, { recursive: true, force: true });
|
|
@@ -11786,9 +12169,9 @@ function registerEnforce(program2) {
|
|
|
11786
12169
|
}
|
|
11787
12170
|
}
|
|
11788
12171
|
});
|
|
11789
|
-
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) => {
|
|
11790
12173
|
const report = await buildEnforcementReport(opts.dir, "ci");
|
|
11791
|
-
printReport(report, Boolean(opts.json));
|
|
12174
|
+
printReport(report, Boolean(opts.json), Boolean(opts.explain));
|
|
11792
12175
|
if (report.should_block) process.exit(2);
|
|
11793
12176
|
});
|
|
11794
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) => {
|
|
@@ -11901,7 +12284,7 @@ async function runWithEnforcement(command, args, opts) {
|
|
|
11901
12284
|
process.exit(2);
|
|
11902
12285
|
}
|
|
11903
12286
|
ui.info(`hAIve briefing marker created for wrapped agent session: ${sessionId}`);
|
|
11904
|
-
ui.info(`Briefing written to ${
|
|
12287
|
+
ui.info(`Briefing written to ${path48.relative(root, briefingFile)} and exported as HAIVE_BRIEFING_FILE`);
|
|
11905
12288
|
const child = spawn5(command, args, {
|
|
11906
12289
|
cwd: root,
|
|
11907
12290
|
stdio: "inherit",
|
|
@@ -11950,9 +12333,9 @@ async function writeWrapperBriefing(paths, sessionId, task) {
|
|
|
11950
12333
|
source: "haive-run",
|
|
11951
12334
|
memoryIds: briefing.memories.map((m) => m.id)
|
|
11952
12335
|
});
|
|
11953
|
-
const dir =
|
|
12336
|
+
const dir = path48.join(paths.runtimeDir, "enforcement", "briefings");
|
|
11954
12337
|
await mkdir19(dir, { recursive: true });
|
|
11955
|
-
const file =
|
|
12338
|
+
const file = path48.join(dir, `${sessionId}.md`);
|
|
11956
12339
|
const parts = [
|
|
11957
12340
|
"# hAIve Briefing",
|
|
11958
12341
|
"",
|
|
@@ -11970,7 +12353,7 @@ async function writeWrapperBriefing(paths, sessionId, task) {
|
|
|
11970
12353
|
if (briefing.setup_warnings.length > 0) {
|
|
11971
12354
|
parts.push("", "## Setup Warnings", ...briefing.setup_warnings.map((w) => `- ${w}`));
|
|
11972
12355
|
}
|
|
11973
|
-
await
|
|
12356
|
+
await writeFile33(file, parts.join("\n") + "\n", "utf8");
|
|
11974
12357
|
return file;
|
|
11975
12358
|
}
|
|
11976
12359
|
async function buildEnforcementReport(dir, stage, sessionId) {
|
|
@@ -11981,7 +12364,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
11981
12364
|
const mode = config.enforcement?.mode ?? "strict";
|
|
11982
12365
|
const findings = [];
|
|
11983
12366
|
if (!initialized) {
|
|
11984
|
-
return {
|
|
12367
|
+
return withCategories({
|
|
11985
12368
|
root,
|
|
11986
12369
|
initialized,
|
|
11987
12370
|
mode,
|
|
@@ -11994,19 +12377,19 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
11994
12377
|
fix: "Run `haive init` or `haive enforce install`.",
|
|
11995
12378
|
impact: 100
|
|
11996
12379
|
}]
|
|
11997
|
-
};
|
|
12380
|
+
});
|
|
11998
12381
|
}
|
|
11999
12382
|
if (mode === "off") {
|
|
12000
|
-
return {
|
|
12383
|
+
return withCategories({
|
|
12001
12384
|
root,
|
|
12002
12385
|
initialized,
|
|
12003
12386
|
mode,
|
|
12004
12387
|
score: buildScore([], config.enforcement?.scoreThreshold),
|
|
12005
12388
|
should_block: false,
|
|
12006
12389
|
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
12007
|
-
};
|
|
12390
|
+
});
|
|
12008
12391
|
}
|
|
12009
|
-
findings.push(...await inspectIntegrationVersions(root, "0.9.
|
|
12392
|
+
findings.push(...await inspectIntegrationVersions(root, "0.9.17"));
|
|
12010
12393
|
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
12011
12394
|
const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
|
|
12012
12395
|
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
|
|
@@ -12056,13 +12439,23 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
12056
12439
|
});
|
|
12057
12440
|
}
|
|
12058
12441
|
const hasErrors = findings.some((f) => f.severity === "error");
|
|
12059
|
-
return {
|
|
12442
|
+
return withCategories({
|
|
12060
12443
|
root,
|
|
12061
12444
|
initialized,
|
|
12062
12445
|
mode,
|
|
12063
12446
|
score: buildScore(findings, config.enforcement?.scoreThreshold),
|
|
12064
12447
|
should_block: mode === "strict" && hasErrors,
|
|
12065
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
|
+
}
|
|
12066
12459
|
};
|
|
12067
12460
|
}
|
|
12068
12461
|
async function hasRecentSessionRecap(paths) {
|
|
@@ -12123,7 +12516,7 @@ async function verifyDecisionCoverage(paths, stage, sessionId) {
|
|
|
12123
12516
|
const relevant = all.map(({ memory: memory2 }) => memory2).filter((memory2) => {
|
|
12124
12517
|
const fm = memory2.frontmatter;
|
|
12125
12518
|
if (!policyTypes.has(fm.type)) return false;
|
|
12126
|
-
if (fm.status
|
|
12519
|
+
if (fm.status !== "validated") return false;
|
|
12127
12520
|
return memoryMatchesAnchorPaths6(memory2, changedFiles);
|
|
12128
12521
|
});
|
|
12129
12522
|
if (relevant.length === 0) {
|
|
@@ -12206,7 +12599,7 @@ async function inspectIntegrationVersions(root, expectedVersion) {
|
|
|
12206
12599
|
];
|
|
12207
12600
|
const findings = [];
|
|
12208
12601
|
for (const rel of files) {
|
|
12209
|
-
const file =
|
|
12602
|
+
const file = path48.join(root, rel);
|
|
12210
12603
|
if (!existsSync68(file)) continue;
|
|
12211
12604
|
const text = await readFile18(file, "utf8").catch(() => "");
|
|
12212
12605
|
for (const bin of extractAbsoluteHaiveBins2(text)) {
|
|
@@ -12273,7 +12666,9 @@ async function getChangedFiles(root, stage) {
|
|
|
12273
12666
|
if (file) files.add(file);
|
|
12274
12667
|
}
|
|
12275
12668
|
}
|
|
12276
|
-
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
|
+
);
|
|
12277
12672
|
}
|
|
12278
12673
|
function buildScore(findings, threshold = 80) {
|
|
12279
12674
|
const checks = {
|
|
@@ -12294,8 +12689,8 @@ function buildScore(findings, threshold = 80) {
|
|
|
12294
12689
|
};
|
|
12295
12690
|
}
|
|
12296
12691
|
async function installGitEnforcement(root) {
|
|
12297
|
-
const hooksDir =
|
|
12298
|
-
if (!existsSync68(
|
|
12692
|
+
const hooksDir = path48.join(root, ".git", "hooks");
|
|
12693
|
+
if (!existsSync68(path48.join(root, ".git"))) {
|
|
12299
12694
|
ui.warn("No .git directory found; git enforcement hooks skipped.");
|
|
12300
12695
|
return;
|
|
12301
12696
|
}
|
|
@@ -12317,31 +12712,31 @@ haive enforce check --stage pre-push --dir . || exit $?
|
|
|
12317
12712
|
}
|
|
12318
12713
|
];
|
|
12319
12714
|
for (const hook of hooks) {
|
|
12320
|
-
const file =
|
|
12715
|
+
const file = path48.join(hooksDir, hook.name);
|
|
12321
12716
|
if (existsSync68(file)) {
|
|
12322
12717
|
const current = await readFile18(file, "utf8").catch(() => "");
|
|
12323
12718
|
if (current.includes(ENFORCE_HOOK_MARKER)) {
|
|
12324
|
-
await
|
|
12719
|
+
await writeFile33(file, hook.body, "utf8");
|
|
12325
12720
|
} else {
|
|
12326
|
-
await
|
|
12721
|
+
await writeFile33(file, `${current.trimEnd()}
|
|
12327
12722
|
|
|
12328
12723
|
${hook.body}`, "utf8");
|
|
12329
12724
|
}
|
|
12330
12725
|
} else {
|
|
12331
|
-
await
|
|
12726
|
+
await writeFile33(file, hook.body, "utf8");
|
|
12332
12727
|
}
|
|
12333
12728
|
await chmod2(file, 493);
|
|
12334
12729
|
}
|
|
12335
12730
|
ui.success("Installed blocking git enforcement hooks: pre-commit, pre-push");
|
|
12336
12731
|
}
|
|
12337
12732
|
async function installCiEnforcement(root) {
|
|
12338
|
-
const workflowPath =
|
|
12339
|
-
await mkdir19(
|
|
12733
|
+
const workflowPath = path48.join(root, ".github", "workflows", "haive-enforcement.yml");
|
|
12734
|
+
await mkdir19(path48.dirname(workflowPath), { recursive: true });
|
|
12340
12735
|
if (existsSync68(workflowPath)) {
|
|
12341
12736
|
ui.info("GitHub Actions enforcement workflow already exists \u2014 skipped");
|
|
12342
12737
|
return;
|
|
12343
12738
|
}
|
|
12344
|
-
await
|
|
12739
|
+
await writeFile33(workflowPath, `name: haive-enforcement
|
|
12345
12740
|
|
|
12346
12741
|
on:
|
|
12347
12742
|
pull_request:
|
|
@@ -12365,9 +12760,9 @@ jobs:
|
|
|
12365
12760
|
- name: Enforce hAIve policy
|
|
12366
12761
|
run: haive enforce ci
|
|
12367
12762
|
`, "utf8");
|
|
12368
|
-
ui.success(`Created ${
|
|
12763
|
+
ui.success(`Created ${path48.relative(root, workflowPath)}`);
|
|
12369
12764
|
}
|
|
12370
|
-
function printReport(report, json) {
|
|
12765
|
+
function printReport(report, json, explain = false) {
|
|
12371
12766
|
if (json) {
|
|
12372
12767
|
console.log(JSON.stringify(report, null, 2));
|
|
12373
12768
|
return;
|
|
@@ -12375,14 +12770,32 @@ function printReport(report, json) {
|
|
|
12375
12770
|
console.log(ui.bold(`hAIve enforcement \u2014 ${report.mode}`));
|
|
12376
12771
|
console.log(ui.dim(` root: ${report.root}`));
|
|
12377
12772
|
console.log(ui.dim(` score: ${report.score.score}% / threshold ${report.score.threshold}%`));
|
|
12378
|
-
|
|
12379
|
-
|
|
12380
|
-
|
|
12381
|
-
|
|
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);
|
|
12382
12779
|
}
|
|
12383
12780
|
if (report.should_block) ui.error("hAIve enforcement gate failed.");
|
|
12384
12781
|
else ui.success("hAIve enforcement gate passed.");
|
|
12385
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
|
+
}
|
|
12386
12799
|
async function readHookPayload() {
|
|
12387
12800
|
const raw = await readStdin2(MAX_STDIN_BYTES2);
|
|
12388
12801
|
if (!raw.trim()) return {};
|
|
@@ -12464,7 +12877,7 @@ function registerRun(program2) {
|
|
|
12464
12877
|
|
|
12465
12878
|
// src/index.ts
|
|
12466
12879
|
var program = new Command51();
|
|
12467
|
-
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");
|
|
12468
12881
|
registerInit(program);
|
|
12469
12882
|
registerWelcome(program);
|
|
12470
12883
|
registerResolveProject(program);
|
|
@@ -12517,6 +12930,32 @@ registerBenchmark(program);
|
|
|
12517
12930
|
registerDoctor(program);
|
|
12518
12931
|
registerPlayback(program);
|
|
12519
12932
|
registerPrecommit(program);
|
|
12933
|
+
var CORE_ROOT_COMMANDS = /* @__PURE__ */ new Set([
|
|
12934
|
+
"init",
|
|
12935
|
+
"doctor",
|
|
12936
|
+
"agent",
|
|
12937
|
+
"enforce",
|
|
12938
|
+
"run",
|
|
12939
|
+
"briefing",
|
|
12940
|
+
"sync",
|
|
12941
|
+
"mcp",
|
|
12942
|
+
"memory",
|
|
12943
|
+
"session",
|
|
12944
|
+
"precommit",
|
|
12945
|
+
"welcome"
|
|
12946
|
+
]);
|
|
12947
|
+
var CORE_MEMORY_COMMANDS = /* @__PURE__ */ new Set([
|
|
12948
|
+
"add",
|
|
12949
|
+
"list",
|
|
12950
|
+
"query",
|
|
12951
|
+
"show",
|
|
12952
|
+
"verify",
|
|
12953
|
+
"lint",
|
|
12954
|
+
"tried",
|
|
12955
|
+
"rm"
|
|
12956
|
+
]);
|
|
12957
|
+
var CORE_SESSION_COMMANDS = /* @__PURE__ */ new Set(["end"]);
|
|
12958
|
+
applySurfaceVisibility(program);
|
|
12520
12959
|
program.parseAsync(process.argv).catch((err) => {
|
|
12521
12960
|
if (isZodError(err)) {
|
|
12522
12961
|
for (const issue of err.issues) {
|
|
@@ -12528,6 +12967,44 @@ program.parseAsync(process.argv).catch((err) => {
|
|
|
12528
12967
|
}
|
|
12529
12968
|
process.exit(1);
|
|
12530
12969
|
});
|
|
12970
|
+
function applySurfaceVisibility(root) {
|
|
12971
|
+
const showAdvanced = process.argv.includes("--advanced") || process.env.HAIVE_SHOW_ADVANCED === "1";
|
|
12972
|
+
if (!showAdvanced) hideNonCoreCommands(root);
|
|
12973
|
+
root.addHelpText(
|
|
12974
|
+
"after",
|
|
12975
|
+
[
|
|
12976
|
+
"",
|
|
12977
|
+
"Default help shows the core hAIve harness: init, doctor, agent setup, briefing, enforcement,",
|
|
12978
|
+
"sync, session recaps, and high-signal memory commands.",
|
|
12979
|
+
"Run `haive --advanced --help` or set HAIVE_SHOW_ADVANCED=1 to show maintenance and experimental commands."
|
|
12980
|
+
].join("\n")
|
|
12981
|
+
);
|
|
12982
|
+
const memoryCommand = root.commands.find((cmd) => cmd.name() === "memory");
|
|
12983
|
+
memoryCommand?.addHelpText(
|
|
12984
|
+
"after",
|
|
12985
|
+
[
|
|
12986
|
+
"",
|
|
12987
|
+
"Default help shows the memory commands that support the core harness workflow.",
|
|
12988
|
+
"Run `haive --advanced memory --help` or set HAIVE_SHOW_ADVANCED=1 to show review, import, digest, timeline, and conflict tools."
|
|
12989
|
+
].join("\n")
|
|
12990
|
+
);
|
|
12991
|
+
}
|
|
12992
|
+
function hideNonCoreCommands(command) {
|
|
12993
|
+
for (const child of command.commands) {
|
|
12994
|
+
if (!isCoreCommand(command, child)) {
|
|
12995
|
+
child._hidden = true;
|
|
12996
|
+
}
|
|
12997
|
+
hideNonCoreCommands(child);
|
|
12998
|
+
}
|
|
12999
|
+
}
|
|
13000
|
+
function isCoreCommand(parent, child) {
|
|
13001
|
+
const parentName = parent.name();
|
|
13002
|
+
const childName = child.name();
|
|
13003
|
+
if (parentName === "haive") return CORE_ROOT_COMMANDS.has(childName);
|
|
13004
|
+
if (parentName === "memory") return CORE_MEMORY_COMMANDS.has(childName);
|
|
13005
|
+
if (parentName === "session") return CORE_SESSION_COMMANDS.has(childName);
|
|
13006
|
+
return true;
|
|
13007
|
+
}
|
|
12531
13008
|
function isZodError(err) {
|
|
12532
13009
|
return err !== null && typeof err === "object" && "issues" in err && Array.isArray(err.issues);
|
|
12533
13010
|
}
|