@hiveai/cli 0.9.16 → 0.9.18
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 +1590 -931
- 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(([path50, changes]) => ({ path: path50, 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);
|
|
@@ -2432,7 +2462,7 @@ function registerInit(program2) {
|
|
|
2432
2462
|
console.log(ui.dim(` \u2713 Stack memory packs pre-seeded (${stacksToSeed.join(", ")})`));
|
|
2433
2463
|
}
|
|
2434
2464
|
console.log();
|
|
2435
|
-
if (!
|
|
2465
|
+
if (!wantBootstrap) {
|
|
2436
2466
|
console.log(ui.bold("One remaining step (optional but recommended):"));
|
|
2437
2467
|
console.log(" " + ui.bold("haive init --bootstrap") + ui.dim(" \u2190 fill project-context.md without AI"));
|
|
2438
2468
|
console.log(" " + ui.dim("Or in your AI client: invoke the MCP prompt ") + ui.bold("bootstrap_project"));
|
|
@@ -2447,7 +2477,7 @@ function registerInit(program2) {
|
|
|
2447
2477
|
console.log(ui.dim(" haive memory import README.md \u2014 from README / docs"));
|
|
2448
2478
|
} else {
|
|
2449
2479
|
console.log(ui.bold("Next steps:"));
|
|
2450
|
-
if (!
|
|
2480
|
+
if (!wantBootstrap) {
|
|
2451
2481
|
console.log(ui.dim(" 1. Fill project context (pick one):"));
|
|
2452
2482
|
console.log(" " + ui.bold("haive init --bootstrap") + ui.dim(" \u2190 instant, no AI needed"));
|
|
2453
2483
|
console.log(" or invoke the MCP prompt " + ui.bold("bootstrap_project") + ui.dim(" in your AI client"));
|
|
@@ -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.18";
|
|
6349
6519
|
function jsonResult(data) {
|
|
6350
6520
|
return {
|
|
6351
6521
|
content: [
|
|
@@ -7313,161 +7483,652 @@ function registerMcp(program2) {
|
|
|
7313
7483
|
}
|
|
7314
7484
|
|
|
7315
7485
|
// src/commands/sync.ts
|
|
7316
|
-
import { spawnSync as
|
|
7317
|
-
import { readFile as
|
|
7318
|
-
import { existsSync as
|
|
7319
|
-
import
|
|
7486
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
7487
|
+
import { readFile as readFile9, writeFile as writeFile15, mkdir as mkdir10 } from "fs/promises";
|
|
7488
|
+
import { existsSync as existsSync31 } from "fs";
|
|
7489
|
+
import path15 from "path";
|
|
7320
7490
|
import "commander";
|
|
7321
7491
|
import {
|
|
7322
7492
|
DEFAULT_AUTO_PROMOTE_RULE as DEFAULT_AUTO_PROMOTE_RULE2,
|
|
7323
7493
|
buildFrontmatter as buildFrontmatter6,
|
|
7324
|
-
findProjectRoot as
|
|
7325
|
-
getUsage as
|
|
7494
|
+
findProjectRoot as findProjectRoot12,
|
|
7495
|
+
getUsage as getUsage11,
|
|
7326
7496
|
isAutoPromoteEligible as isAutoPromoteEligible2,
|
|
7327
7497
|
isDecaying as isDecaying2,
|
|
7328
|
-
loadCodeMap as
|
|
7329
|
-
loadConfig as
|
|
7330
|
-
loadMemoriesFromDir as
|
|
7331
|
-
loadUsageIndex as
|
|
7498
|
+
loadCodeMap as loadCodeMap6,
|
|
7499
|
+
loadConfig as loadConfig5,
|
|
7500
|
+
loadMemoriesFromDir as loadMemoriesFromDir24,
|
|
7501
|
+
loadUsageIndex as loadUsageIndex13,
|
|
7332
7502
|
pullCrossRepoSources,
|
|
7333
|
-
resolveHaivePaths as
|
|
7503
|
+
resolveHaivePaths as resolveHaivePaths9,
|
|
7334
7504
|
resolveManifestFiles,
|
|
7335
|
-
serializeMemory as
|
|
7505
|
+
serializeMemory as serializeMemory12,
|
|
7336
7506
|
trackDependencies,
|
|
7337
7507
|
verifyAnchor as verifyAnchor2,
|
|
7338
7508
|
watchContracts
|
|
7339
7509
|
} from "@hiveai/core";
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7510
|
+
|
|
7511
|
+
// src/utils/autopilot.ts
|
|
7512
|
+
import { existsSync as existsSync30 } from "fs";
|
|
7513
|
+
import { readFile as readFile8, writeFile as writeFile14 } from "fs/promises";
|
|
7514
|
+
import path14 from "path";
|
|
7515
|
+
import {
|
|
7516
|
+
AUTOPILOT_DEFAULTS as AUTOPILOT_DEFAULTS2,
|
|
7517
|
+
buildCodeMap as buildCodeMap3,
|
|
7518
|
+
loadCodeMap as loadCodeMap5,
|
|
7519
|
+
loadConfig as loadConfig4,
|
|
7520
|
+
saveCodeMap as saveCodeMap3,
|
|
7521
|
+
saveConfig as saveConfig2
|
|
7522
|
+
} from "@hiveai/core";
|
|
7523
|
+
|
|
7524
|
+
// src/commands/memory-lint.ts
|
|
7525
|
+
import { existsSync as existsSync29 } from "fs";
|
|
7526
|
+
import { writeFile as writeFile13 } from "fs/promises";
|
|
7527
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
7528
|
+
import path13 from "path";
|
|
7529
|
+
import "commander";
|
|
7530
|
+
import {
|
|
7531
|
+
findProjectRoot as findProjectRoot11,
|
|
7532
|
+
getUsage as getUsage10,
|
|
7533
|
+
loadCodeMap as loadCodeMap4,
|
|
7534
|
+
loadMemoriesFromDir as loadMemoriesFromDir23,
|
|
7535
|
+
loadUsageIndex as loadUsageIndex12,
|
|
7536
|
+
resolveHaivePaths as resolveHaivePaths8,
|
|
7537
|
+
serializeMemory as serializeMemory11
|
|
7538
|
+
} from "@hiveai/core";
|
|
7539
|
+
async function lintMemoriesAsync(root, options = {}) {
|
|
7540
|
+
const paths = resolveHaivePaths8(root);
|
|
7541
|
+
const out = [];
|
|
7542
|
+
const fixes = [];
|
|
7543
|
+
if (!existsSync29(paths.memoriesDir)) return { findings: out, fixes };
|
|
7544
|
+
const loaded = await loadMemoriesFromDir23(paths.memoriesDir);
|
|
7545
|
+
const usage = await loadUsageIndex12(paths);
|
|
7546
|
+
const codeMap = await loadCodeMap4(paths);
|
|
7547
|
+
const trackedFiles = gitTrackedFiles(root);
|
|
7548
|
+
const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
|
|
7549
|
+
const actionableWords = /\b(always|never|prefer|use|run|avoid|because|instead|why|rationale|do not|must|should|require|required|requires|fix|fail|failed|fails|prevent|prevents|allow|allows|lets|ensure|ensures|catch|catches)\b/i;
|
|
7550
|
+
for (const { filePath, memory: memory2 } of loaded) {
|
|
7551
|
+
const fm = memory2.frontmatter;
|
|
7552
|
+
if (fm.type === "session_recap") continue;
|
|
7553
|
+
const body = memory2.body.trim();
|
|
7554
|
+
const naked = body.replace(/^#.*$/gm, "").replace(/```[\s\S]*?```/g, "").trim();
|
|
7555
|
+
if (naked.length < 40 && fm.status !== "rejected") {
|
|
7556
|
+
out.push({
|
|
7557
|
+
file: filePath,
|
|
7558
|
+
id: fm.id,
|
|
7559
|
+
severity: "warn",
|
|
7560
|
+
code: "SHORT_BODY",
|
|
7561
|
+
message: "Body looks very short (< ~40 chars of prose after headings). Prefer actionable detail."
|
|
7562
|
+
});
|
|
7358
7563
|
}
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
if (
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7564
|
+
if (["decision", "gotcha", "convention", "architecture", "attempt"].includes(fm.type) && fm.status !== "rejected" && !actionableWords.test(naked)) {
|
|
7565
|
+
out.push({
|
|
7566
|
+
file: filePath,
|
|
7567
|
+
id: fm.id,
|
|
7568
|
+
severity: "info",
|
|
7569
|
+
code: "LOW_ACTIONABILITY",
|
|
7570
|
+
message: "Record does not contain obvious action/rationale words. Add the concrete rule, why it exists, and what to do instead."
|
|
7571
|
+
});
|
|
7572
|
+
}
|
|
7573
|
+
const suggestedAnchors = suggestAnchors(root, { filePath, memory: memory2 }, codeMap, trackedFiles);
|
|
7574
|
+
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated") {
|
|
7575
|
+
out.push({
|
|
7576
|
+
file: filePath,
|
|
7577
|
+
id: fm.id,
|
|
7578
|
+
severity: "warn",
|
|
7579
|
+
code: "MISSING_ANCHOR",
|
|
7580
|
+
message: `${fm.type} is validated without anchor paths \u2014 add anchor.paths so haive sync can flag staleness.`,
|
|
7581
|
+
...suggestedAnchors.paths.length > 0 || suggestedAnchors.symbols.length > 0 ? { suggested_anchors: suggestedAnchors } : {}
|
|
7582
|
+
});
|
|
7583
|
+
}
|
|
7584
|
+
if (fm.status === "stale" && !fm.stale_reason) {
|
|
7585
|
+
out.push({
|
|
7586
|
+
file: filePath,
|
|
7587
|
+
id: fm.id,
|
|
7588
|
+
severity: "info",
|
|
7589
|
+
code: "STALE_NO_REASON",
|
|
7590
|
+
message: "Status is stale but stale_reason is empty \u2014 document why when possible."
|
|
7591
|
+
});
|
|
7592
|
+
}
|
|
7593
|
+
if (fm.type === "glossary" && naked.length > 6e3) {
|
|
7594
|
+
out.push({
|
|
7595
|
+
file: filePath,
|
|
7596
|
+
id: fm.id,
|
|
7597
|
+
severity: "info",
|
|
7598
|
+
code: "LONG_GLOSSARY",
|
|
7599
|
+
message: "Very long glossary \u2014 consider splitting concepts for tighter briefings."
|
|
7600
|
+
});
|
|
7601
|
+
}
|
|
7602
|
+
const hasMarkdownHeading = /^#{1,3}\s+\S/m.test(memory2.body.trim());
|
|
7603
|
+
if (!hasMarkdownHeading) {
|
|
7604
|
+
out.push({
|
|
7605
|
+
file: filePath,
|
|
7606
|
+
id: fm.id,
|
|
7607
|
+
severity: "warn",
|
|
7608
|
+
code: "NO_MD_HEADING",
|
|
7609
|
+
message: "No Markdown heading (#/##/###) \u2014 add one so humans and auditors can skim the memo quickly."
|
|
7610
|
+
});
|
|
7611
|
+
}
|
|
7612
|
+
const u = getUsage10(usage, fm.id);
|
|
7613
|
+
if (fm.status === "validated" && u.read_count === 0) {
|
|
7614
|
+
out.push({
|
|
7615
|
+
file: filePath,
|
|
7616
|
+
id: fm.id,
|
|
7617
|
+
severity: "info",
|
|
7618
|
+
code: "NEVER_READ",
|
|
7619
|
+
message: "Validated record has never been surfaced/read. Consider improving tags/anchors or archiving it if it is not useful."
|
|
7620
|
+
});
|
|
7621
|
+
}
|
|
7622
|
+
if (options.fix) {
|
|
7623
|
+
const actions = [];
|
|
7624
|
+
let nextBody = memory2.body;
|
|
7625
|
+
let nextFrontmatter = memory2.frontmatter;
|
|
7626
|
+
if (!hasMarkdownHeading) {
|
|
7627
|
+
nextBody = `# ${titleFromId(fm.id)}
|
|
7628
|
+
|
|
7629
|
+
${nextBody.trim()}`;
|
|
7630
|
+
actions.push("add missing Markdown heading");
|
|
7631
|
+
}
|
|
7632
|
+
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated" && suggestedAnchors.paths.length > 0) {
|
|
7633
|
+
nextFrontmatter = {
|
|
7634
|
+
...nextFrontmatter,
|
|
7635
|
+
anchor: {
|
|
7636
|
+
...nextFrontmatter.anchor,
|
|
7637
|
+
paths: [.../* @__PURE__ */ new Set([...nextFrontmatter.anchor.paths, ...suggestedAnchors.paths])],
|
|
7638
|
+
symbols: [
|
|
7639
|
+
.../* @__PURE__ */ new Set([...nextFrontmatter.anchor.symbols, ...suggestedAnchors.symbols])
|
|
7640
|
+
]
|
|
7641
|
+
},
|
|
7642
|
+
tags: nextFrontmatter.tags.filter((tag) => tag !== "needs_anchor")
|
|
7643
|
+
};
|
|
7644
|
+
actions.push("add suggested tracked anchor paths");
|
|
7645
|
+
if (suggestedAnchors.symbols.length > 0) {
|
|
7646
|
+
actions.push("add suggested anchor symbols");
|
|
7427
7647
|
}
|
|
7428
7648
|
}
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
maxRejections: DEFAULT_AUTO_PROMOTE_RULE2.maxRejections
|
|
7440
|
-
})) {
|
|
7649
|
+
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.anchor.symbols.length === 0 && suggestedAnchors.paths.length === 0 && fm.status === "validated" && !fm.tags.includes("needs_anchor")) {
|
|
7650
|
+
nextFrontmatter = {
|
|
7651
|
+
...nextFrontmatter,
|
|
7652
|
+
tags: [...nextFrontmatter.tags, "needs_anchor"]
|
|
7653
|
+
};
|
|
7654
|
+
actions.push("tag validated anchorless record with needs_anchor");
|
|
7655
|
+
}
|
|
7656
|
+
if (actions.length > 0) {
|
|
7657
|
+
fixes.push({ file: filePath, id: fm.id, actions, applied: Boolean(options.apply) });
|
|
7658
|
+
if (options.apply) {
|
|
7441
7659
|
await writeFile13(
|
|
7442
7660
|
filePath,
|
|
7443
|
-
serializeMemory11({ frontmatter:
|
|
7661
|
+
serializeMemory11({ frontmatter: nextFrontmatter, body: nextBody }),
|
|
7444
7662
|
"utf8"
|
|
7445
7663
|
);
|
|
7446
|
-
promoted++;
|
|
7447
|
-
continue;
|
|
7448
|
-
}
|
|
7449
|
-
if (autoApproveDelayHours !== null && fm.status === "proposed" && fm.scope === "team") {
|
|
7450
|
-
const ageHours = (nowMs - new Date(fm.created_at).getTime()) / (1e3 * 60 * 60);
|
|
7451
|
-
if (ageHours >= autoApproveDelayHours) {
|
|
7452
|
-
await writeFile13(
|
|
7453
|
-
filePath,
|
|
7454
|
-
serializeMemory11({
|
|
7455
|
-
frontmatter: {
|
|
7456
|
-
...fm,
|
|
7457
|
-
status: "validated",
|
|
7458
|
-
verified_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
7459
|
-
},
|
|
7460
|
-
body: memory2.body
|
|
7461
|
-
}),
|
|
7462
|
-
"utf8"
|
|
7463
|
-
);
|
|
7464
|
-
autoApproved++;
|
|
7465
|
-
}
|
|
7466
7664
|
}
|
|
7467
7665
|
}
|
|
7468
7666
|
}
|
|
7469
|
-
|
|
7470
|
-
|
|
7667
|
+
}
|
|
7668
|
+
for (const dup of nearDuplicatePairs(loaded)) {
|
|
7669
|
+
out.push({
|
|
7670
|
+
file: dup.file,
|
|
7671
|
+
id: dup.id,
|
|
7672
|
+
severity: "warn",
|
|
7673
|
+
code: "NEAR_DUPLICATE",
|
|
7674
|
+
message: `Body overlaps ~${Math.round(dup.score * 100)}% with ${dup.otherId}. Merge or deprecate one record to reduce briefing noise.`
|
|
7675
|
+
});
|
|
7676
|
+
}
|
|
7677
|
+
return { findings: out, fixes };
|
|
7678
|
+
}
|
|
7679
|
+
function titleFromId(id) {
|
|
7680
|
+
const withoutDate = id.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
7681
|
+
return withoutDate.split("-").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
7682
|
+
}
|
|
7683
|
+
function suggestAnchors(root, loaded, codeMap, trackedFiles) {
|
|
7684
|
+
const body = loaded.memory.body;
|
|
7685
|
+
const paths = /* @__PURE__ */ new Set();
|
|
7686
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
7687
|
+
for (const match of body.matchAll(/`([^`\n]+\.[A-Za-z0-9]+)`|(?:^|\s)([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)/gm)) {
|
|
7688
|
+
const candidate = (match[1] ?? match[2] ?? "").replace(/^\.?\//, "");
|
|
7689
|
+
if (!candidate || candidate.startsWith("http")) continue;
|
|
7690
|
+
if (existsSync29(path13.join(root, candidate)) && isSafeAnchorPath(candidate, trackedFiles)) {
|
|
7691
|
+
paths.add(candidate);
|
|
7692
|
+
}
|
|
7693
|
+
}
|
|
7694
|
+
if (codeMap) {
|
|
7695
|
+
const lowered = body.toLowerCase();
|
|
7696
|
+
for (const [file, entry] of Object.entries(codeMap.files)) {
|
|
7697
|
+
for (const exp of entry.exports) {
|
|
7698
|
+
if (!exp.name || exp.name.length < 4) continue;
|
|
7699
|
+
if (lowered.includes(exp.name.toLowerCase())) {
|
|
7700
|
+
if (isSafeAnchorPath(file, trackedFiles)) {
|
|
7701
|
+
paths.add(file);
|
|
7702
|
+
symbols.add(exp.name);
|
|
7703
|
+
}
|
|
7704
|
+
}
|
|
7705
|
+
if (paths.size >= 5 && symbols.size >= 5) break;
|
|
7706
|
+
}
|
|
7707
|
+
if (paths.size >= 5 && symbols.size >= 5) break;
|
|
7708
|
+
}
|
|
7709
|
+
}
|
|
7710
|
+
return {
|
|
7711
|
+
paths: [...paths].slice(0, 5),
|
|
7712
|
+
symbols: [...symbols].slice(0, 5)
|
|
7713
|
+
};
|
|
7714
|
+
}
|
|
7715
|
+
function gitTrackedFiles(root) {
|
|
7716
|
+
const result = spawnSync3("git", ["ls-files"], {
|
|
7717
|
+
cwd: root,
|
|
7718
|
+
encoding: "utf8",
|
|
7719
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
7720
|
+
});
|
|
7721
|
+
if (result.status !== 0) return null;
|
|
7722
|
+
const files = result.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
7723
|
+
return new Set(files);
|
|
7724
|
+
}
|
|
7725
|
+
function isSafeAnchorPath(file, trackedFiles) {
|
|
7726
|
+
const normalized = file.replace(/\\/g, "/").replace(/^\.?\//, "");
|
|
7727
|
+
if (normalized.startsWith(".ai/.cache/") || normalized.startsWith(".ai/.runtime/")) return false;
|
|
7728
|
+
if (normalized.includes("/node_modules/") || normalized.startsWith("node_modules/")) return false;
|
|
7729
|
+
if (normalized.includes("/dist/") || normalized.startsWith("dist/")) return false;
|
|
7730
|
+
if (trackedFiles && !trackedFiles.has(normalized)) return false;
|
|
7731
|
+
return true;
|
|
7732
|
+
}
|
|
7733
|
+
function nearDuplicatePairs(loaded) {
|
|
7734
|
+
const out = [];
|
|
7735
|
+
const candidates = loaded.filter(({ memory: memory2 }) => {
|
|
7736
|
+
const fm = memory2.frontmatter;
|
|
7737
|
+
return fm.type !== "session_recap" && fm.status !== "rejected" && fm.status !== "deprecated";
|
|
7738
|
+
});
|
|
7739
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
7740
|
+
for (let j = i + 1; j < candidates.length; j++) {
|
|
7741
|
+
const a = candidates[i];
|
|
7742
|
+
const b = candidates[j];
|
|
7743
|
+
if (a.memory.frontmatter.scope !== b.memory.frontmatter.scope) continue;
|
|
7744
|
+
if (a.memory.frontmatter.type !== b.memory.frontmatter.type) continue;
|
|
7745
|
+
const score = jaccard2(tokenSet(a.memory.body), tokenSet(b.memory.body));
|
|
7746
|
+
if (score >= 0.72) {
|
|
7747
|
+
out.push({
|
|
7748
|
+
id: a.memory.frontmatter.id,
|
|
7749
|
+
otherId: b.memory.frontmatter.id,
|
|
7750
|
+
file: a.filePath,
|
|
7751
|
+
score
|
|
7752
|
+
});
|
|
7753
|
+
}
|
|
7754
|
+
}
|
|
7755
|
+
}
|
|
7756
|
+
return out;
|
|
7757
|
+
}
|
|
7758
|
+
function tokenSet(body) {
|
|
7759
|
+
return new Set(
|
|
7760
|
+
(body.toLowerCase().match(/\b[a-z0-9]{4,}\b/g) ?? []).filter((word) => !["this", "that", "with", "from", "have"].includes(word))
|
|
7761
|
+
);
|
|
7762
|
+
}
|
|
7763
|
+
function jaccard2(a, b) {
|
|
7764
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
7765
|
+
let inter = 0;
|
|
7766
|
+
for (const item of a) if (b.has(item)) inter++;
|
|
7767
|
+
return inter / (a.size + b.size - inter);
|
|
7768
|
+
}
|
|
7769
|
+
function registerMemoryLint(parent) {
|
|
7770
|
+
parent.command("lint").description(
|
|
7771
|
+
"Heuristic corpus checks (anchors on key types, headings, verbosity). Static analysis only."
|
|
7772
|
+
).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) => {
|
|
7773
|
+
const root = findProjectRoot11(opts.dir);
|
|
7774
|
+
const apply = Boolean(opts.fix && opts.apply);
|
|
7775
|
+
const dryRun = Boolean(opts.fix && (opts.dryRun || !opts.apply));
|
|
7776
|
+
const report = await lintMemoriesAsync(root, { fix: Boolean(opts.fix), apply });
|
|
7777
|
+
const findings = report.findings;
|
|
7778
|
+
if (opts.json) {
|
|
7779
|
+
console.log(JSON.stringify({
|
|
7780
|
+
findings_count: findings.length,
|
|
7781
|
+
findings,
|
|
7782
|
+
fixes_count: report.fixes.length,
|
|
7783
|
+
fixes: report.fixes,
|
|
7784
|
+
fix_mode: opts.fix ? apply ? "apply" : "dry-run" : "off"
|
|
7785
|
+
}, null, 2));
|
|
7786
|
+
process.exitCode = findings.some((f) => f.severity === "error") ? 1 : 0;
|
|
7787
|
+
return;
|
|
7788
|
+
}
|
|
7789
|
+
if (findings.length === 0) {
|
|
7790
|
+
ui.success(`memory lint OK \u2014 ${root}`);
|
|
7791
|
+
return;
|
|
7792
|
+
}
|
|
7793
|
+
console.log(ui.bold(`memory lint (${findings.length} finding${findings.length === 1 ? "" : "s"})`) + `
|
|
7794
|
+
`);
|
|
7795
|
+
if (opts.fix) {
|
|
7796
|
+
const mode = apply ? "apply" : dryRun ? "dry-run" : "dry-run";
|
|
7797
|
+
const verb = apply ? "changed" : "would change";
|
|
7798
|
+
console.log(ui.bold(`fix ${mode}: ${report.fixes.length} file${report.fixes.length === 1 ? "" : "s"} ${verb}`));
|
|
7799
|
+
for (const fix of report.fixes) {
|
|
7800
|
+
console.log(` ${ui.dim(fix.id)} ${fix.actions.join("; ")}`);
|
|
7801
|
+
console.log(ui.dim(` \u2192 ${fix.file}`));
|
|
7802
|
+
}
|
|
7803
|
+
console.log();
|
|
7804
|
+
}
|
|
7805
|
+
const order = { error: 0, warn: 1, info: 2 };
|
|
7806
|
+
findings.sort((a, b) => order[a.severity] - order[b.severity] || a.id.localeCompare(b.id));
|
|
7807
|
+
for (const f of findings) {
|
|
7808
|
+
const color = f.severity === "error" ? ui.red : f.severity === "warn" ? ui.yellow : ui.dim;
|
|
7809
|
+
console.log(
|
|
7810
|
+
`${color(f.severity.padEnd(5))} ${ui.dim(f.code)} ${f.id}`
|
|
7811
|
+
);
|
|
7812
|
+
console.log(` ${f.message}`);
|
|
7813
|
+
if (f.suggested_anchors) {
|
|
7814
|
+
const pathHints = f.suggested_anchors.paths.length > 0 ? `paths: ${f.suggested_anchors.paths.join(", ")}` : "";
|
|
7815
|
+
const symbolHints = f.suggested_anchors.symbols.length > 0 ? `symbols: ${f.suggested_anchors.symbols.join(", ")}` : "";
|
|
7816
|
+
console.log(ui.dim(` suggested anchors: ${[pathHints, symbolHints].filter(Boolean).join(" \xB7 ")}`));
|
|
7817
|
+
}
|
|
7818
|
+
console.log(ui.dim(` \u2192 ${f.file}`));
|
|
7819
|
+
}
|
|
7820
|
+
process.exitCode = findings.some((x) => x.severity === "error") ? 1 : 0;
|
|
7821
|
+
});
|
|
7822
|
+
}
|
|
7823
|
+
|
|
7824
|
+
// src/utils/autopilot.ts
|
|
7825
|
+
async function applyAutopilotRepairs(root, paths, options = {}) {
|
|
7826
|
+
const repairs = [];
|
|
7827
|
+
const config = await loadConfig4(paths);
|
|
7828
|
+
if (options.applyConfig) {
|
|
7829
|
+
const changed = await ensureAutopilotConfig(paths, config);
|
|
7830
|
+
if (changed) {
|
|
7831
|
+
repairs.push({
|
|
7832
|
+
code: "autopilot-config",
|
|
7833
|
+
message: "Enabled autopilot defaults in .ai/haive.config.json."
|
|
7834
|
+
});
|
|
7835
|
+
}
|
|
7836
|
+
}
|
|
7837
|
+
const current = await loadConfig4(paths);
|
|
7838
|
+
const autoRepair = current.autoRepair ?? {};
|
|
7839
|
+
if (options.applyContext ?? autoRepair.context ?? current.autopilot) {
|
|
7840
|
+
const changed = await syncProjectContextVersion(root, paths);
|
|
7841
|
+
if (changed) {
|
|
7842
|
+
repairs.push({
|
|
7843
|
+
code: "project-context-version",
|
|
7844
|
+
message: "Updated .ai/project-context.md version metadata from package.json."
|
|
7845
|
+
});
|
|
7846
|
+
}
|
|
7847
|
+
}
|
|
7848
|
+
if (options.applyCorpus ?? autoRepair.corpus ?? current.autopilot) {
|
|
7849
|
+
const report = await lintMemoriesAsync(root, { fix: true, apply: true });
|
|
7850
|
+
const applied = report.fixes.filter((fix) => fix.applied);
|
|
7851
|
+
if (applied.length > 0) {
|
|
7852
|
+
repairs.push({
|
|
7853
|
+
code: "memory-lint-fix",
|
|
7854
|
+
message: `Applied ${applied.length} safe memory lint fix${applied.length === 1 ? "" : "es"}.`
|
|
7855
|
+
});
|
|
7856
|
+
}
|
|
7857
|
+
}
|
|
7858
|
+
if (options.applyCodeMap ?? autoRepair.codeMap ?? current.autopilot) {
|
|
7859
|
+
const refreshed = await refreshCodeMap(root, paths, Boolean(options.forceCodeMap));
|
|
7860
|
+
if (refreshed) {
|
|
7861
|
+
repairs.push({
|
|
7862
|
+
code: "code-map-refresh",
|
|
7863
|
+
message: "Refreshed .ai/code-map.json."
|
|
7864
|
+
});
|
|
7865
|
+
}
|
|
7866
|
+
}
|
|
7867
|
+
if (options.applyCodeSearch ?? autoRepair.codeSearch ?? current.autopilot) {
|
|
7868
|
+
const indexed = await refreshCodeSearchIndex(paths);
|
|
7869
|
+
if (indexed) {
|
|
7870
|
+
repairs.push({
|
|
7871
|
+
code: "code-search-index",
|
|
7872
|
+
message: "Refreshed code-search embeddings index."
|
|
7873
|
+
});
|
|
7874
|
+
}
|
|
7875
|
+
}
|
|
7876
|
+
return repairs;
|
|
7877
|
+
}
|
|
7878
|
+
async function ensureAutopilotConfig(paths, currentConfig) {
|
|
7879
|
+
const current = currentConfig ?? await loadConfig4(paths);
|
|
7880
|
+
const next = {
|
|
7881
|
+
...current,
|
|
7882
|
+
autopilot: true,
|
|
7883
|
+
defaultScope: "team",
|
|
7884
|
+
defaultStatus: "validated",
|
|
7885
|
+
autoApproveDelayHours: current.autoApproveDelayHours ?? AUTOPILOT_DEFAULTS2.autoApproveDelayHours,
|
|
7886
|
+
autoPromoteMinReads: current.autoPromoteMinReads ?? AUTOPILOT_DEFAULTS2.autoPromoteMinReads,
|
|
7887
|
+
autoSessionEnd: true,
|
|
7888
|
+
autoContext: true,
|
|
7889
|
+
autoRepair: {
|
|
7890
|
+
context: true,
|
|
7891
|
+
corpus: true,
|
|
7892
|
+
codeMap: true,
|
|
7893
|
+
codeSearch: current.autoRepair?.codeSearch ?? true
|
|
7894
|
+
},
|
|
7895
|
+
enforcement: {
|
|
7896
|
+
...AUTOPILOT_DEFAULTS2.enforcement,
|
|
7897
|
+
...current.enforcement,
|
|
7898
|
+
mode: "strict",
|
|
7899
|
+
requireBriefingFirst: true,
|
|
7900
|
+
requireSessionRecap: true,
|
|
7901
|
+
requireMemoryVerify: true,
|
|
7902
|
+
blockStaleDecisionChanges: true,
|
|
7903
|
+
requireDecisionCoverage: true,
|
|
7904
|
+
cleanupGeneratedArtifacts: true,
|
|
7905
|
+
toolProfile: current.enforcement?.toolProfile ?? "enforcement"
|
|
7906
|
+
}
|
|
7907
|
+
};
|
|
7908
|
+
if (JSON.stringify(current) === JSON.stringify(next)) return false;
|
|
7909
|
+
await saveConfig2(paths, next);
|
|
7910
|
+
return true;
|
|
7911
|
+
}
|
|
7912
|
+
async function syncProjectContextVersion(root, paths) {
|
|
7913
|
+
const status = await projectContextVersionStatus(root, paths);
|
|
7914
|
+
if (!status.canSync || !status.expectedVersion) return false;
|
|
7915
|
+
const original = await readFile8(paths.projectContext, "utf8");
|
|
7916
|
+
let updated = original.replace(
|
|
7917
|
+
/^# Project context — hAIve \(v[^)]+\)$/m,
|
|
7918
|
+
`# Project context \u2014 hAIve (v${status.expectedVersion})`
|
|
7919
|
+
).replace(
|
|
7920
|
+
/> \*\*Current version\*\*: [^—\n]+—/m,
|
|
7921
|
+
`> **Current version**: ${status.expectedVersion} \u2014`
|
|
7922
|
+
);
|
|
7923
|
+
if (updated === original && !original.includes("Current version")) {
|
|
7924
|
+
updated = original.replace(
|
|
7925
|
+
/^(> Repo-native context enforcement[^\n]*\n)/m,
|
|
7926
|
+
`$1> **Current version**: ${status.expectedVersion} \u2014 @hiveai/core, cli, mcp, embeddings are versioned together.
|
|
7927
|
+
`
|
|
7928
|
+
);
|
|
7929
|
+
}
|
|
7930
|
+
if (updated === original) return false;
|
|
7931
|
+
await writeFile14(paths.projectContext, updated, "utf8");
|
|
7932
|
+
return true;
|
|
7933
|
+
}
|
|
7934
|
+
async function projectContextVersionStatus(root, paths) {
|
|
7935
|
+
if (!existsSync30(paths.projectContext)) {
|
|
7936
|
+
return { mismatch: false, canSync: false };
|
|
7937
|
+
}
|
|
7938
|
+
const packagePath = path14.join(root, "package.json");
|
|
7939
|
+
if (!existsSync30(packagePath)) {
|
|
7940
|
+
return { mismatch: false, canSync: false };
|
|
7941
|
+
}
|
|
7942
|
+
const packageJson = JSON.parse(await readFile8(packagePath, "utf8"));
|
|
7943
|
+
const expectedVersion = packageJson.version;
|
|
7944
|
+
if (!expectedVersion) {
|
|
7945
|
+
return { mismatch: false, canSync: false };
|
|
7946
|
+
}
|
|
7947
|
+
const content = await readFile8(paths.projectContext, "utf8");
|
|
7948
|
+
const headingVersion = content.match(/^# Project context — hAIve \(v([^)]+)\)$/m)?.[1];
|
|
7949
|
+
const currentLineVersion = content.match(/^> \*\*Current version\*\*: ([^—\n]+)—/m)?.[1]?.trim();
|
|
7950
|
+
const currentVersion = currentLineVersion ?? headingVersion;
|
|
7951
|
+
return {
|
|
7952
|
+
expectedVersion,
|
|
7953
|
+
currentVersion,
|
|
7954
|
+
mismatch: currentVersion !== expectedVersion,
|
|
7955
|
+
canSync: true
|
|
7956
|
+
};
|
|
7957
|
+
}
|
|
7958
|
+
async function refreshCodeMap(root, paths, force) {
|
|
7959
|
+
if (!force) {
|
|
7960
|
+
const existing = await loadCodeMap5(paths);
|
|
7961
|
+
if (existing) return false;
|
|
7962
|
+
}
|
|
7963
|
+
const map = await buildCodeMap3(root, {
|
|
7964
|
+
excludeDirs: [
|
|
7965
|
+
"node_modules",
|
|
7966
|
+
"dist",
|
|
7967
|
+
"build",
|
|
7968
|
+
"out",
|
|
7969
|
+
".git",
|
|
7970
|
+
".next",
|
|
7971
|
+
".turbo",
|
|
7972
|
+
".vitest-cache",
|
|
7973
|
+
"coverage"
|
|
7974
|
+
]
|
|
7975
|
+
});
|
|
7976
|
+
await saveCodeMap3(paths, map);
|
|
7977
|
+
return true;
|
|
7978
|
+
}
|
|
7979
|
+
async function refreshCodeSearchIndex(paths) {
|
|
7980
|
+
try {
|
|
7981
|
+
const mod = await import("@hiveai/embeddings");
|
|
7982
|
+
const embedder = await mod.Embedder.create();
|
|
7983
|
+
const { report } = await mod.rebuildCodeIndex(paths, embedder);
|
|
7984
|
+
return report.added > 0 || report.updated > 0 || report.removed > 0;
|
|
7985
|
+
} catch {
|
|
7986
|
+
return false;
|
|
7987
|
+
}
|
|
7988
|
+
}
|
|
7989
|
+
|
|
7990
|
+
// src/commands/sync.ts
|
|
7991
|
+
var BRIDGE_START = "<!-- haive:memories-start -->";
|
|
7992
|
+
var BRIDGE_END = "<!-- haive:memories-end -->";
|
|
7993
|
+
function registerSync(program2) {
|
|
7994
|
+
program2.command("sync").description(
|
|
7995
|
+
"Refresh memory state after a git pull or merge.\n What it does:\n 1. Verifies anchor paths \u2014 marks stale if files/symbols moved or deleted\n 2. Re-validates previously stale memories that are now fresh\n 3. Auto-promotes proposed memories (by usage count or time delay in autopilot)\n 4. Auto-refreshes code-map if source files changed\n 5. Reports decay warnings for memories unused >90 days\n\n Install git hooks to run sync automatically: haive install-hooks\n\n Examples:\n haive sync\n haive sync --since main # also report memories changed since main\n haive sync --embed # also rebuild embeddings index\n"
|
|
7996
|
+
).option("-d, --dir <dir>", "project root").option("--quiet", "minimal output (suitable for git hooks)").option(
|
|
7997
|
+
"--since <ref>",
|
|
7998
|
+
"git ref/commit to compare against; report memories added/modified/removed since"
|
|
7999
|
+
).option("--no-verify", "skip the anchor verification step").option("--no-promote", "skip the auto-promotion step").option(
|
|
8000
|
+
"--inject-bridge",
|
|
8001
|
+
"inject top validated memories into CLAUDE.md (or --bridge-file) between <!-- haive:memories-start/end --> markers"
|
|
8002
|
+
).option("--bridge-file <path>", "bridge file to inject into (default: CLAUDE.md)").option("--bridge-max-memories <n>", "max memories to inject into bridge file", "5").option("--embed", "rebuild embeddings index after sync (requires @haive/embeddings)").option("--no-cross-repo", "skip cross-repo memory pull even if crossRepoSources is configured").option("--no-deps", "skip dependency version tracking").option("--no-contracts", "skip contract file diff checking").action(async (opts) => {
|
|
8003
|
+
const root = findProjectRoot12(opts.dir);
|
|
8004
|
+
const paths = resolveHaivePaths9(root);
|
|
8005
|
+
if (!existsSync31(paths.memoriesDir)) {
|
|
8006
|
+
if (!opts.quiet) ui.warn(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
8007
|
+
process.exitCode = 1;
|
|
8008
|
+
return;
|
|
8009
|
+
}
|
|
8010
|
+
const log = (msg) => {
|
|
8011
|
+
if (!opts.quiet) console.log(msg);
|
|
8012
|
+
};
|
|
8013
|
+
const config = await loadConfig5(paths);
|
|
8014
|
+
const autoApproveDelayHours = config.autoApproveDelayHours ?? null;
|
|
8015
|
+
const autoPromoteMinReads = config.autoPromoteMinReads ?? DEFAULT_AUTO_PROMOTE_RULE2.minReads;
|
|
8016
|
+
const autoRepair = config.autoRepair ?? {};
|
|
8017
|
+
let staleMarked = 0;
|
|
8018
|
+
let revalidated = 0;
|
|
8019
|
+
let promoted = 0;
|
|
8020
|
+
let autoApproved = 0;
|
|
8021
|
+
if (opts.verify !== false) {
|
|
8022
|
+
const memories = await loadMemoriesFromDir24(paths.memoriesDir);
|
|
8023
|
+
for (const { memory: memory2, filePath } of memories) {
|
|
8024
|
+
if (memory2.frontmatter.type === "session_recap") {
|
|
8025
|
+
if (memory2.frontmatter.status === "stale") {
|
|
8026
|
+
await writeFile15(
|
|
8027
|
+
filePath,
|
|
8028
|
+
serializeMemory12({
|
|
8029
|
+
frontmatter: {
|
|
8030
|
+
...memory2.frontmatter,
|
|
8031
|
+
status: "validated",
|
|
8032
|
+
stale_reason: null,
|
|
8033
|
+
verified_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8034
|
+
},
|
|
8035
|
+
body: memory2.body
|
|
8036
|
+
}),
|
|
8037
|
+
"utf8"
|
|
8038
|
+
);
|
|
8039
|
+
revalidated++;
|
|
8040
|
+
}
|
|
8041
|
+
continue;
|
|
8042
|
+
}
|
|
8043
|
+
const isAnchored = memory2.frontmatter.anchor.paths.length > 0 || memory2.frontmatter.anchor.symbols.length > 0;
|
|
8044
|
+
if (!isAnchored) continue;
|
|
8045
|
+
const result = await verifyAnchor2(memory2, { projectRoot: root });
|
|
8046
|
+
const verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8047
|
+
if (result.stale) {
|
|
8048
|
+
if (memory2.frontmatter.status !== "stale") {
|
|
8049
|
+
await writeFile15(
|
|
8050
|
+
filePath,
|
|
8051
|
+
serializeMemory12({
|
|
8052
|
+
frontmatter: {
|
|
8053
|
+
...memory2.frontmatter,
|
|
8054
|
+
status: "stale",
|
|
8055
|
+
verified_at: verifiedAt,
|
|
8056
|
+
stale_reason: result.reason
|
|
8057
|
+
},
|
|
8058
|
+
body: memory2.body
|
|
8059
|
+
}),
|
|
8060
|
+
"utf8"
|
|
8061
|
+
);
|
|
8062
|
+
staleMarked++;
|
|
8063
|
+
}
|
|
8064
|
+
} else if (memory2.frontmatter.status === "stale") {
|
|
8065
|
+
await writeFile15(
|
|
8066
|
+
filePath,
|
|
8067
|
+
serializeMemory12({
|
|
8068
|
+
frontmatter: {
|
|
8069
|
+
...memory2.frontmatter,
|
|
8070
|
+
status: "validated",
|
|
8071
|
+
verified_at: verifiedAt,
|
|
8072
|
+
stale_reason: null
|
|
8073
|
+
},
|
|
8074
|
+
body: memory2.body
|
|
8075
|
+
}),
|
|
8076
|
+
"utf8"
|
|
8077
|
+
);
|
|
8078
|
+
revalidated++;
|
|
8079
|
+
}
|
|
8080
|
+
}
|
|
8081
|
+
}
|
|
8082
|
+
if (opts.promote !== false) {
|
|
8083
|
+
const memories = await loadMemoriesFromDir24(paths.memoriesDir);
|
|
8084
|
+
const usage = await loadUsageIndex13(paths);
|
|
8085
|
+
const nowMs = Date.now();
|
|
8086
|
+
for (const { memory: memory2, filePath } of memories) {
|
|
8087
|
+
const fm = memory2.frontmatter;
|
|
8088
|
+
if (fm.type === "session_recap") continue;
|
|
8089
|
+
if (isAutoPromoteEligible2(fm, getUsage11(usage, fm.id), {
|
|
8090
|
+
minReads: autoPromoteMinReads,
|
|
8091
|
+
maxRejections: DEFAULT_AUTO_PROMOTE_RULE2.maxRejections
|
|
8092
|
+
})) {
|
|
8093
|
+
await writeFile15(
|
|
8094
|
+
filePath,
|
|
8095
|
+
serializeMemory12({ frontmatter: { ...fm, status: "validated" }, body: memory2.body }),
|
|
8096
|
+
"utf8"
|
|
8097
|
+
);
|
|
8098
|
+
promoted++;
|
|
8099
|
+
continue;
|
|
8100
|
+
}
|
|
8101
|
+
if (autoApproveDelayHours !== null && fm.status === "proposed" && fm.scope === "team") {
|
|
8102
|
+
const ageHours = (nowMs - new Date(fm.created_at).getTime()) / (1e3 * 60 * 60);
|
|
8103
|
+
if (ageHours >= autoApproveDelayHours) {
|
|
8104
|
+
await writeFile15(
|
|
8105
|
+
filePath,
|
|
8106
|
+
serializeMemory12({
|
|
8107
|
+
frontmatter: {
|
|
8108
|
+
...fm,
|
|
8109
|
+
status: "validated",
|
|
8110
|
+
verified_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8111
|
+
},
|
|
8112
|
+
body: memory2.body
|
|
8113
|
+
}),
|
|
8114
|
+
"utf8"
|
|
8115
|
+
);
|
|
8116
|
+
autoApproved++;
|
|
8117
|
+
}
|
|
8118
|
+
}
|
|
8119
|
+
}
|
|
8120
|
+
}
|
|
8121
|
+
if (config.autopilot || autoRepair.context || autoRepair.corpus) {
|
|
8122
|
+
const repairs = await applyAutopilotRepairs(root, paths, {
|
|
8123
|
+
applyContext: autoRepair.context ?? config.autopilot,
|
|
8124
|
+
applyCorpus: autoRepair.corpus ?? config.autopilot,
|
|
8125
|
+
applyCodeMap: false,
|
|
8126
|
+
applyCodeSearch: false
|
|
8127
|
+
});
|
|
8128
|
+
for (const repair of repairs) log(ui.dim(`autopilot: ${repair.message}`));
|
|
8129
|
+
}
|
|
8130
|
+
const sinceReport = opts.since ? collectSinceChanges(root, opts.since) : null;
|
|
8131
|
+
const draftMemories = (await loadMemoriesFromDir24(paths.memoriesDir)).filter(
|
|
7471
8132
|
(m) => m.memory.frontmatter.status === "draft"
|
|
7472
8133
|
);
|
|
7473
8134
|
const draftCount = draftMemories.length;
|
|
@@ -7483,7 +8144,7 @@ function registerSync(program2) {
|
|
|
7483
8144
|
);
|
|
7484
8145
|
}
|
|
7485
8146
|
if (opts.injectBridge) {
|
|
7486
|
-
const bridgeFile = opts.bridgeFile ?
|
|
8147
|
+
const bridgeFile = opts.bridgeFile ? path15.resolve(opts.bridgeFile) : path15.join(root, "CLAUDE.md");
|
|
7487
8148
|
const maxInject = Math.max(1, Number(opts.bridgeMaxMemories ?? 5));
|
|
7488
8149
|
await injectBridge(bridgeFile, paths.memoriesDir, maxInject, root, opts.quiet);
|
|
7489
8150
|
}
|
|
@@ -7502,12 +8163,12 @@ function registerSync(program2) {
|
|
|
7502
8163
|
}
|
|
7503
8164
|
}
|
|
7504
8165
|
if (!opts.quiet) {
|
|
7505
|
-
const allForDecay = await
|
|
7506
|
-
const usageForDecay = await
|
|
8166
|
+
const allForDecay = await loadMemoriesFromDir24(paths.memoriesDir);
|
|
8167
|
+
const usageForDecay = await loadUsageIndex13(paths);
|
|
7507
8168
|
const decaying = allForDecay.filter(({ memory: memory2 }) => {
|
|
7508
8169
|
const fm = memory2.frontmatter;
|
|
7509
8170
|
if (fm.status === "rejected" || fm.status === "deprecated" || fm.status === "stale") return false;
|
|
7510
|
-
const u =
|
|
8171
|
+
const u = getUsage11(usageForDecay, fm.id);
|
|
7511
8172
|
return isDecaying2(u, fm.created_at);
|
|
7512
8173
|
});
|
|
7513
8174
|
if (decaying.length > 0) {
|
|
@@ -7589,11 +8250,11 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7589
8250
|
paths: [result.file],
|
|
7590
8251
|
topic: `dep-bump-${slugParts}`
|
|
7591
8252
|
});
|
|
7592
|
-
const teamDir =
|
|
8253
|
+
const teamDir = path15.join(paths.memoriesDir, "team");
|
|
7593
8254
|
await mkdir10(teamDir, { recursive: true });
|
|
7594
|
-
await
|
|
7595
|
-
|
|
7596
|
-
|
|
8255
|
+
await writeFile15(
|
|
8256
|
+
path15.join(teamDir, `${fm.id}.md`),
|
|
8257
|
+
serializeMemory12({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
7597
8258
|
"utf8"
|
|
7598
8259
|
);
|
|
7599
8260
|
log(ui.yellow(` \u2192 memory created: ${fm.id}`));
|
|
@@ -7656,11 +8317,11 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7656
8317
|
paths: [diff.file],
|
|
7657
8318
|
topic: `contract-breaking-${diff.contract}`
|
|
7658
8319
|
});
|
|
7659
|
-
const teamDir =
|
|
8320
|
+
const teamDir = path15.join(paths.memoriesDir, "team");
|
|
7660
8321
|
await mkdir10(teamDir, { recursive: true });
|
|
7661
|
-
await
|
|
7662
|
-
|
|
7663
|
-
|
|
8322
|
+
await writeFile15(
|
|
8323
|
+
path15.join(teamDir, `${fm.id}.md`),
|
|
8324
|
+
serializeMemory12({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
7664
8325
|
"utf8"
|
|
7665
8326
|
);
|
|
7666
8327
|
log(ui.yellow(` \u2192 memory created: ${fm.id}`));
|
|
@@ -7670,10 +8331,19 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7670
8331
|
ui.warn(`contract watcher failed: ${String(err)}`);
|
|
7671
8332
|
}
|
|
7672
8333
|
}
|
|
7673
|
-
const existingMap = await
|
|
7674
|
-
if (existingMap) {
|
|
8334
|
+
const existingMap = await loadCodeMap6(paths);
|
|
8335
|
+
if (!existingMap && (config.autopilot || autoRepair.codeMap)) {
|
|
8336
|
+
try {
|
|
8337
|
+
const { buildCodeMap: buildCodeMap4, saveCodeMap: saveCodeMap4 } = await import("@hiveai/core");
|
|
8338
|
+
log(ui.dim("code-map: missing \u2014 building index\u2026"));
|
|
8339
|
+
const newMap = await buildCodeMap4(root);
|
|
8340
|
+
await saveCodeMap4(paths, newMap);
|
|
8341
|
+
log(ui.dim(`code-map: built (${Object.keys(newMap.files).length} files)`));
|
|
8342
|
+
} catch {
|
|
8343
|
+
}
|
|
8344
|
+
} else if (existingMap) {
|
|
7675
8345
|
const mapAge = new Date(existingMap.generated_at).getTime();
|
|
7676
|
-
const gitResult =
|
|
8346
|
+
const gitResult = spawnSync4(
|
|
7677
8347
|
"git",
|
|
7678
8348
|
[
|
|
7679
8349
|
"diff",
|
|
@@ -7698,22 +8368,32 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7698
8368
|
const changedSourceFiles = (gitResult.stdout ?? "").trim();
|
|
7699
8369
|
if (changedSourceFiles.length > 0) {
|
|
7700
8370
|
try {
|
|
7701
|
-
const { buildCodeMap:
|
|
8371
|
+
const { buildCodeMap: buildCodeMap4, saveCodeMap: saveCodeMap4 } = await import("@hiveai/core");
|
|
7702
8372
|
log(ui.dim("code-map: source files changed \u2014 refreshing index\u2026"));
|
|
7703
|
-
const newMap = await
|
|
7704
|
-
await
|
|
8373
|
+
const newMap = await buildCodeMap4(root);
|
|
8374
|
+
await saveCodeMap4(paths, newMap);
|
|
7705
8375
|
log(ui.dim(`code-map: refreshed (${Object.keys(newMap.files).length} files)`));
|
|
7706
8376
|
} catch {
|
|
7707
8377
|
}
|
|
7708
8378
|
}
|
|
7709
8379
|
}
|
|
7710
|
-
if (opts.embed) {
|
|
8380
|
+
if (opts.embed || autoRepair.codeSearch) {
|
|
7711
8381
|
try {
|
|
7712
|
-
const { Embedder, rebuildIndex } = await import("@hiveai/embeddings");
|
|
8382
|
+
const { Embedder, rebuildCodeIndex, rebuildIndex } = await import("@hiveai/embeddings");
|
|
7713
8383
|
log(ui.dim("embed: rebuilding index\u2026"));
|
|
7714
8384
|
const embedder = await Embedder.create();
|
|
7715
8385
|
const { report } = await rebuildIndex(paths, embedder);
|
|
7716
|
-
|
|
8386
|
+
const { report: codeReport } = await rebuildCodeIndex(paths, embedder);
|
|
8387
|
+
log(
|
|
8388
|
+
ui.dim(
|
|
8389
|
+
`embed: memory index rebuilt (${report.added} added, ${report.updated} updated, ${report.removed} removed)`
|
|
8390
|
+
)
|
|
8391
|
+
);
|
|
8392
|
+
log(
|
|
8393
|
+
ui.dim(
|
|
8394
|
+
`embed: code index rebuilt (${codeReport.total} symbols, ${codeReport.added} added, ${codeReport.updated} updated, ${codeReport.removed} removed)`
|
|
8395
|
+
)
|
|
8396
|
+
);
|
|
7717
8397
|
} catch {
|
|
7718
8398
|
ui.warn("--embed: @hiveai/embeddings not available or index build failed. Run `haive embeddings index` manually.");
|
|
7719
8399
|
}
|
|
@@ -7721,8 +8401,8 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7721
8401
|
});
|
|
7722
8402
|
}
|
|
7723
8403
|
async function injectBridge(bridgeFile, memoriesDir, maxMemories, root, quiet) {
|
|
7724
|
-
if (!
|
|
7725
|
-
const all = await
|
|
8404
|
+
if (!existsSync31(memoriesDir)) return;
|
|
8405
|
+
const all = await loadMemoriesFromDir24(memoriesDir);
|
|
7726
8406
|
const top = all.filter(({ memory: memory2 }) => {
|
|
7727
8407
|
const s = memory2.frontmatter.status;
|
|
7728
8408
|
if (memory2.frontmatter.type === "session_recap") return false;
|
|
@@ -7746,17 +8426,17 @@ ${m.memory.body.trim()}`;
|
|
|
7746
8426
|
` + block + `
|
|
7747
8427
|
|
|
7748
8428
|
${BRIDGE_END}`;
|
|
7749
|
-
const fileExists =
|
|
7750
|
-
let existing = fileExists ? await
|
|
8429
|
+
const fileExists = existsSync31(bridgeFile);
|
|
8430
|
+
let existing = fileExists ? await readFile9(bridgeFile, "utf8") : "";
|
|
7751
8431
|
existing = existing.replace(/\r\n/g, "\n");
|
|
7752
8432
|
const startIdx = existing.indexOf(BRIDGE_START);
|
|
7753
8433
|
const endIdx = existing.indexOf(BRIDGE_END);
|
|
7754
8434
|
if (startIdx !== -1 && endIdx === -1) {
|
|
7755
|
-
ui.warn(`${
|
|
8435
|
+
ui.warn(`${path15.relative(root, bridgeFile)}: found ${BRIDGE_START} without ${BRIDGE_END}. Fix the file manually before running --inject-bridge.`);
|
|
7756
8436
|
return;
|
|
7757
8437
|
}
|
|
7758
8438
|
if (startIdx === -1 && endIdx !== -1) {
|
|
7759
|
-
ui.warn(`${
|
|
8439
|
+
ui.warn(`${path15.relative(root, bridgeFile)}: found ${BRIDGE_END} without ${BRIDGE_START}. Fix the file manually before running --inject-bridge.`);
|
|
7760
8440
|
return;
|
|
7761
8441
|
}
|
|
7762
8442
|
let updated;
|
|
@@ -7764,19 +8444,19 @@ ${BRIDGE_END}`;
|
|
|
7764
8444
|
updated = existing.slice(0, startIdx) + injected + existing.slice(endIdx + BRIDGE_END.length);
|
|
7765
8445
|
} else {
|
|
7766
8446
|
if (!fileExists && !quiet) {
|
|
7767
|
-
ui.info(`Creating ${
|
|
8447
|
+
ui.info(`Creating ${path15.relative(root, bridgeFile)} with haive memory block.`);
|
|
7768
8448
|
}
|
|
7769
8449
|
updated = existing + (existing.endsWith("\n") ? "" : "\n") + "\n" + injected + "\n";
|
|
7770
8450
|
}
|
|
7771
|
-
await
|
|
8451
|
+
await writeFile15(bridgeFile, updated, "utf8");
|
|
7772
8452
|
if (!quiet) {
|
|
7773
8453
|
console.log(
|
|
7774
|
-
ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${
|
|
8454
|
+
ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${path15.relative(root, bridgeFile)}`)
|
|
7775
8455
|
);
|
|
7776
8456
|
}
|
|
7777
8457
|
}
|
|
7778
8458
|
function collectSinceChanges(root, ref) {
|
|
7779
|
-
const result =
|
|
8459
|
+
const result = spawnSync4(
|
|
7780
8460
|
"git",
|
|
7781
8461
|
["-C", root, "diff", "--name-status", "--diff-filter=AMD", `${ref}...HEAD`, "--", ".ai/memories"],
|
|
7782
8462
|
{ encoding: "utf8" }
|
|
@@ -7796,18 +8476,19 @@ function collectSinceChanges(root, ref) {
|
|
|
7796
8476
|
|
|
7797
8477
|
// src/commands/memory-add.ts
|
|
7798
8478
|
import { createHash as createHash2 } from "crypto";
|
|
7799
|
-
import { mkdir as mkdir11, readFile as
|
|
7800
|
-
import { existsSync as
|
|
7801
|
-
import
|
|
8479
|
+
import { mkdir as mkdir11, readFile as readFile10, writeFile as writeFile16 } from "fs/promises";
|
|
8480
|
+
import { existsSync as existsSync33 } from "fs";
|
|
8481
|
+
import path16 from "path";
|
|
7802
8482
|
import "commander";
|
|
7803
8483
|
import {
|
|
7804
8484
|
buildFrontmatter as buildFrontmatter7,
|
|
7805
|
-
findProjectRoot as
|
|
8485
|
+
findProjectRoot as findProjectRoot13,
|
|
7806
8486
|
inferModulesFromPaths as inferModulesFromPaths3,
|
|
7807
|
-
|
|
8487
|
+
loadConfig as loadConfig6,
|
|
8488
|
+
loadMemoriesFromDir as loadMemoriesFromDir25,
|
|
7808
8489
|
memoryFilePath as memoryFilePath6,
|
|
7809
|
-
resolveHaivePaths as
|
|
7810
|
-
serializeMemory as
|
|
8490
|
+
resolveHaivePaths as resolveHaivePaths10,
|
|
8491
|
+
serializeMemory as serializeMemory13
|
|
7811
8492
|
} from "@hiveai/core";
|
|
7812
8493
|
function registerMemoryAdd(memory2) {
|
|
7813
8494
|
memory2.command("add").description(
|
|
@@ -7833,21 +8514,22 @@ function registerMemoryAdd(memory2) {
|
|
|
7833
8514
|
haive memory add --type convention --slug flyway-no-modify --topic flyway \\\\
|
|
7834
8515
|
--scope team --body "Never modify existing migrations. Create V{n+1}__desc.sql."
|
|
7835
8516
|
`
|
|
7836
|
-
).requiredOption("--type <type>", "convention | decision | gotcha | architecture | glossary | attempt").requiredOption("--slug <slug>", "short kebab-case identifier used in the file name").option("--title <text>", "memory title \u2014 becomes the first heading of the body").option("--scope <scope>", "personal | team | module (default:
|
|
7837
|
-
const root =
|
|
7838
|
-
const paths =
|
|
7839
|
-
if (!
|
|
8517
|
+
).requiredOption("--type <type>", "convention | decision | gotcha | architecture | glossary | attempt").requiredOption("--slug <slug>", "short kebab-case identifier used in the file name").option("--title <text>", "memory title \u2014 becomes the first heading of the body").option("--scope <scope>", "personal | team | module (default: config default; team in autopilot)").option("--module <name>", "module name (required when scope=module)").option("--tags <csv>", "comma-separated tags for easier retrieval").option("--domain <domain>", "domain (e.g. transactions)").option("--author <author>", "author email or handle").option("--paths <csv>", "anchor to source files \u2014 used for staleness detection by haive sync").option("--symbols <csv>", "anchor to specific symbols (class/function names)").option("--commit <sha>", "anchor to a specific commit SHA").option("--body <text>", "memory body content (Markdown) \u2014 overrides --title default body").option("--body-file <path>", "read memory body from a Markdown file \u2014 for long content").option("--no-auto-tag", "disable automatic tag suggestions inferred from anchor paths").option("--topic <key>", "stable key for upsert: if a memory with this topic+scope already exists, update it in-place (revision_count++)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
8518
|
+
const root = findProjectRoot13(opts.dir);
|
|
8519
|
+
const paths = resolveHaivePaths10(root);
|
|
8520
|
+
if (!existsSync33(paths.haiveDir)) {
|
|
7840
8521
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
7841
8522
|
process.exitCode = 1;
|
|
7842
8523
|
return;
|
|
7843
8524
|
}
|
|
8525
|
+
const config = await loadConfig6(paths);
|
|
7844
8526
|
const userTags = parseCsv2(opts.tags);
|
|
7845
8527
|
const anchorPaths = parseCsv2(opts.paths);
|
|
7846
8528
|
const autoTagsEnabled = opts.autoTag !== false;
|
|
7847
8529
|
const inferredTags = autoTagsEnabled ? inferModulesFromPaths3(anchorPaths) : [];
|
|
7848
8530
|
const mergedTags = Array.from(/* @__PURE__ */ new Set([...userTags, ...inferredTags]));
|
|
7849
8531
|
if (anchorPaths.length > 0) {
|
|
7850
|
-
const missing = anchorPaths.filter((p) => !
|
|
8532
|
+
const missing = anchorPaths.filter((p) => !existsSync33(path16.resolve(root, p)));
|
|
7851
8533
|
if (missing.length > 0) {
|
|
7852
8534
|
ui.warn(`Anchor path${missing.length > 1 ? "s" : ""} not found in project:`);
|
|
7853
8535
|
for (const p of missing) ui.warn(` \u2717 ${p}`);
|
|
@@ -7859,12 +8541,12 @@ function registerMemoryAdd(memory2) {
|
|
|
7859
8541
|
const title = opts.title ?? opts.slug;
|
|
7860
8542
|
let body;
|
|
7861
8543
|
if (opts.bodyFile !== void 0) {
|
|
7862
|
-
if (!
|
|
8544
|
+
if (!existsSync33(opts.bodyFile)) {
|
|
7863
8545
|
ui.error(`--body-file not found: ${opts.bodyFile}`);
|
|
7864
8546
|
process.exitCode = 1;
|
|
7865
8547
|
return;
|
|
7866
8548
|
}
|
|
7867
|
-
const fileContent = await
|
|
8549
|
+
const fileContent = await readFile10(opts.bodyFile, "utf8");
|
|
7868
8550
|
body = opts.title ? `# ${opts.title}
|
|
7869
8551
|
|
|
7870
8552
|
${fileContent.trim()}
|
|
@@ -7879,10 +8561,10 @@ ${opts.body}` : opts.body;
|
|
|
7879
8561
|
TODO \u2014 write the memory body.
|
|
7880
8562
|
`;
|
|
7881
8563
|
}
|
|
7882
|
-
const scope = opts.scope ?? "personal";
|
|
7883
|
-
if (
|
|
8564
|
+
const scope = opts.scope ?? config.defaultScope ?? "personal";
|
|
8565
|
+
if (existsSync33(paths.memoriesDir)) {
|
|
7884
8566
|
const incomingHash = createHash2("sha256").update(body.trim()).digest("hex").slice(0, 12);
|
|
7885
|
-
const allForHash = await
|
|
8567
|
+
const allForHash = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
7886
8568
|
const hashDup = allForHash.find(
|
|
7887
8569
|
({ memory: memory3 }) => createHash2("sha256").update(memory3.body.trim()).digest("hex").slice(0, 12) === incomingHash && memory3.frontmatter.scope === scope
|
|
7888
8570
|
);
|
|
@@ -7893,8 +8575,8 @@ TODO \u2014 write the memory body.
|
|
|
7893
8575
|
return;
|
|
7894
8576
|
}
|
|
7895
8577
|
}
|
|
7896
|
-
if (opts.topic &&
|
|
7897
|
-
const existing = await
|
|
8578
|
+
if (opts.topic && existsSync33(paths.memoriesDir)) {
|
|
8579
|
+
const existing = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
7898
8580
|
const topicMatch = existing.find(
|
|
7899
8581
|
({ memory: memory3 }) => memory3.frontmatter.topic === opts.topic && memory3.frontmatter.scope === scope && (!opts.module || memory3.frontmatter.module === opts.module)
|
|
7900
8582
|
);
|
|
@@ -7911,8 +8593,8 @@ TODO \u2014 write the memory body.
|
|
|
7911
8593
|
symbols: parseCsv2(opts.symbols).length ? parseCsv2(opts.symbols) : fm.anchor.symbols
|
|
7912
8594
|
}
|
|
7913
8595
|
};
|
|
7914
|
-
await
|
|
7915
|
-
ui.success(`Updated (topic upsert) ${
|
|
8596
|
+
await writeFile16(topicMatch.filePath, serializeMemory13({ frontmatter: newFrontmatter, body }), "utf8");
|
|
8597
|
+
ui.success(`Updated (topic upsert) ${path16.relative(root, topicMatch.filePath)}`);
|
|
7916
8598
|
ui.info(`id=${fm.id} revision=${revisionCount}`);
|
|
7917
8599
|
return;
|
|
7918
8600
|
}
|
|
@@ -7928,17 +8610,18 @@ TODO \u2014 write the memory body.
|
|
|
7928
8610
|
paths: anchorPaths,
|
|
7929
8611
|
symbols: parseCsv2(opts.symbols),
|
|
7930
8612
|
commit: opts.commit,
|
|
7931
|
-
topic: opts.topic
|
|
8613
|
+
topic: opts.topic,
|
|
8614
|
+
status: config.defaultStatus === "validated" ? "validated" : void 0
|
|
7932
8615
|
});
|
|
7933
8616
|
const file = memoryFilePath6(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
7934
|
-
await mkdir11(
|
|
7935
|
-
if (
|
|
8617
|
+
await mkdir11(path16.dirname(file), { recursive: true });
|
|
8618
|
+
if (existsSync33(file)) {
|
|
7936
8619
|
ui.error(`Memory already exists at ${file}`);
|
|
7937
8620
|
process.exitCode = 1;
|
|
7938
8621
|
return;
|
|
7939
8622
|
}
|
|
7940
|
-
if (
|
|
7941
|
-
const existing = await
|
|
8623
|
+
if (existsSync33(paths.memoriesDir)) {
|
|
8624
|
+
const existing = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
7942
8625
|
const slugTokens = opts.slug.toLowerCase().split(/[-_\s]+/).filter(Boolean);
|
|
7943
8626
|
const similar = existing.filter(({ memory: memory3 }) => {
|
|
7944
8627
|
const id = memory3.frontmatter.id.toLowerCase();
|
|
@@ -7949,8 +8632,8 @@ TODO \u2014 write the memory body.
|
|
|
7949
8632
|
ui.warn("Consider updating one of these with `haive memory update` instead.");
|
|
7950
8633
|
}
|
|
7951
8634
|
}
|
|
7952
|
-
await
|
|
7953
|
-
ui.success(`Created ${
|
|
8635
|
+
await writeFile16(file, serializeMemory13({ frontmatter, body }), "utf8");
|
|
8636
|
+
ui.success(`Created ${path16.relative(root, file)}`);
|
|
7954
8637
|
ui.info(`id=${frontmatter.id} scope=${frontmatter.scope} status=${frontmatter.status}`);
|
|
7955
8638
|
if (inferredTags.length > 0) {
|
|
7956
8639
|
ui.info(`auto-tagged: ${inferredTags.join(", ")} (use --no-auto-tag to disable)`);
|
|
@@ -7961,7 +8644,9 @@ TODO \u2014 write the memory body.
|
|
|
7961
8644
|
Add file anchors: haive memory update ${frontmatter.id} --paths <file1,file2>`
|
|
7962
8645
|
);
|
|
7963
8646
|
}
|
|
7964
|
-
if (
|
|
8647
|
+
if (frontmatter.status === "validated") {
|
|
8648
|
+
console.log(ui.dim("\u2192 autopilot: memory is already validated and active"));
|
|
8649
|
+
} else if (scope === "personal") {
|
|
7965
8650
|
console.log(
|
|
7966
8651
|
ui.dim(
|
|
7967
8652
|
`\u2192 next: haive memory approve ${frontmatter.id} (activate) | haive memory promote ${frontmatter.id} (share with team)`
|
|
@@ -7980,14 +8665,14 @@ function parseCsv2(value) {
|
|
|
7980
8665
|
}
|
|
7981
8666
|
|
|
7982
8667
|
// src/commands/memory-list.ts
|
|
7983
|
-
import { existsSync as
|
|
7984
|
-
import
|
|
8668
|
+
import { existsSync as existsSync34 } from "fs";
|
|
8669
|
+
import path17 from "path";
|
|
7985
8670
|
import "commander";
|
|
7986
|
-
import { findProjectRoot as
|
|
8671
|
+
import { findProjectRoot as findProjectRoot14, resolveHaivePaths as resolveHaivePaths11 } from "@hiveai/core";
|
|
7987
8672
|
|
|
7988
8673
|
// src/utils/fs.ts
|
|
7989
8674
|
import {
|
|
7990
|
-
loadMemoriesFromDir as
|
|
8675
|
+
loadMemoriesFromDir as loadMemoriesFromDir26,
|
|
7991
8676
|
loadMemory,
|
|
7992
8677
|
listMarkdownFilesRecursive
|
|
7993
8678
|
} from "@hiveai/core";
|
|
@@ -7995,14 +8680,14 @@ import {
|
|
|
7995
8680
|
// src/commands/memory-list.ts
|
|
7996
8681
|
function registerMemoryList(memory2) {
|
|
7997
8682
|
memory2.command("list").description("List memories with optional filters").option("--scope <scope>", "personal | team | module").option("--type <type>", "filter by type").option("--tag <tag>", "filter by tag").option("--module <name>", "filter by module name").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected,deprecated)").option("--show-rejected", "include rejected memories (hidden by default)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
7998
|
-
const root =
|
|
7999
|
-
const paths =
|
|
8000
|
-
if (!
|
|
8683
|
+
const root = findProjectRoot14(opts.dir);
|
|
8684
|
+
const paths = resolveHaivePaths11(root);
|
|
8685
|
+
if (!existsSync34(paths.memoriesDir)) {
|
|
8001
8686
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
8002
8687
|
process.exitCode = 1;
|
|
8003
8688
|
return;
|
|
8004
8689
|
}
|
|
8005
|
-
const all = await
|
|
8690
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8006
8691
|
const statusFilter = opts.status ? opts.status.split(",").map((s) => s.trim()) : null;
|
|
8007
8692
|
const filtered = all.filter((m) => {
|
|
8008
8693
|
if (!matchesFilters(m, opts)) return false;
|
|
@@ -8029,7 +8714,7 @@ function registerMemoryList(memory2) {
|
|
|
8029
8714
|
console.log(
|
|
8030
8715
|
`${ui.bold(fm.id)} ${ui.dim(fm.scope)}/${ui.dim(fm.type)} ${statusBadge}${moduleStr}${tagStr}`
|
|
8031
8716
|
);
|
|
8032
|
-
console.log(` ${ui.dim(
|
|
8717
|
+
console.log(` ${ui.dim(path17.relative(root, filePath))}`);
|
|
8033
8718
|
}
|
|
8034
8719
|
console.log(ui.dim(`
|
|
8035
8720
|
${filtered.length} memor${filtered.length === 1 ? "y" : "ies"}`));
|
|
@@ -8064,26 +8749,26 @@ function matchesFilters(loaded, opts) {
|
|
|
8064
8749
|
}
|
|
8065
8750
|
|
|
8066
8751
|
// src/commands/memory-promote.ts
|
|
8067
|
-
import { mkdir as mkdir12, unlink as unlink2, writeFile as
|
|
8068
|
-
import { existsSync as
|
|
8069
|
-
import
|
|
8752
|
+
import { mkdir as mkdir12, unlink as unlink2, writeFile as writeFile17 } from "fs/promises";
|
|
8753
|
+
import { existsSync as existsSync35 } from "fs";
|
|
8754
|
+
import path18 from "path";
|
|
8070
8755
|
import "commander";
|
|
8071
8756
|
import {
|
|
8072
|
-
findProjectRoot as
|
|
8757
|
+
findProjectRoot as findProjectRoot15,
|
|
8073
8758
|
memoryFilePath as memoryFilePath7,
|
|
8074
|
-
resolveHaivePaths as
|
|
8075
|
-
serializeMemory as
|
|
8759
|
+
resolveHaivePaths as resolveHaivePaths12,
|
|
8760
|
+
serializeMemory as serializeMemory14
|
|
8076
8761
|
} from "@hiveai/core";
|
|
8077
8762
|
function registerMemoryPromote(memory2) {
|
|
8078
8763
|
memory2.command("promote <id>").description("Promote a personal memory to team scope (status -> proposed)").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
8079
|
-
const root =
|
|
8080
|
-
const paths =
|
|
8081
|
-
if (!
|
|
8764
|
+
const root = findProjectRoot15(opts.dir);
|
|
8765
|
+
const paths = resolveHaivePaths12(root);
|
|
8766
|
+
if (!existsSync35(paths.memoriesDir)) {
|
|
8082
8767
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
8083
8768
|
process.exitCode = 1;
|
|
8084
8769
|
return;
|
|
8085
8770
|
}
|
|
8086
|
-
const teamAndModule = await
|
|
8771
|
+
const teamAndModule = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8087
8772
|
const alreadyShared = teamAndModule.find(
|
|
8088
8773
|
(m) => m.memory.frontmatter.id === id && (m.memory.frontmatter.scope === "team" || m.memory.frontmatter.scope === "module")
|
|
8089
8774
|
);
|
|
@@ -8097,7 +8782,7 @@ function registerMemoryPromote(memory2) {
|
|
|
8097
8782
|
}
|
|
8098
8783
|
return;
|
|
8099
8784
|
}
|
|
8100
|
-
const all = await
|
|
8785
|
+
const all = await loadMemoriesFromDir26(paths.personalDir);
|
|
8101
8786
|
const found = all.find((m) => m.memory.frontmatter.id === id);
|
|
8102
8787
|
if (!found) {
|
|
8103
8788
|
ui.error(`No personal memory with id "${id}". (Promotion only applies to personal scope.)`);
|
|
@@ -8113,35 +8798,35 @@ function registerMemoryPromote(memory2) {
|
|
|
8113
8798
|
body: found.memory.body
|
|
8114
8799
|
};
|
|
8115
8800
|
const newPath = memoryFilePath7(paths, "team", updated.frontmatter.id);
|
|
8116
|
-
await mkdir12(
|
|
8117
|
-
await
|
|
8801
|
+
await mkdir12(path18.dirname(newPath), { recursive: true });
|
|
8802
|
+
await writeFile17(newPath, serializeMemory14(updated), "utf8");
|
|
8118
8803
|
await unlink2(found.filePath);
|
|
8119
8804
|
ui.success(`Promoted ${id} to team scope (status=proposed)`);
|
|
8120
|
-
ui.info(`Now at ${
|
|
8805
|
+
ui.info(`Now at ${path18.relative(root, newPath)}`);
|
|
8121
8806
|
console.log(ui.dim(`\u2192 next: haive memory approve ${id} (validate for team use)`));
|
|
8122
8807
|
});
|
|
8123
8808
|
}
|
|
8124
8809
|
|
|
8125
8810
|
// src/commands/memory-approve.ts
|
|
8126
|
-
import { existsSync as
|
|
8127
|
-
import { writeFile as
|
|
8128
|
-
import
|
|
8811
|
+
import { existsSync as existsSync36 } from "fs";
|
|
8812
|
+
import { writeFile as writeFile18 } from "fs/promises";
|
|
8813
|
+
import path19 from "path";
|
|
8129
8814
|
import "commander";
|
|
8130
8815
|
import {
|
|
8131
|
-
findProjectRoot as
|
|
8132
|
-
resolveHaivePaths as
|
|
8133
|
-
serializeMemory as
|
|
8816
|
+
findProjectRoot as findProjectRoot16,
|
|
8817
|
+
resolveHaivePaths as resolveHaivePaths13,
|
|
8818
|
+
serializeMemory as serializeMemory15
|
|
8134
8819
|
} from "@hiveai/core";
|
|
8135
8820
|
function registerMemoryApprove(memory2) {
|
|
8136
8821
|
memory2.command("approve [id]").description("Mark a memory as 'validated'. Use --all to bulk-approve all proposed/draft memories.").option("--all", "approve all proposed and draft memories at once").option("--pending", "approve all memories with status 'proposed'").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
8137
|
-
const root =
|
|
8138
|
-
const paths =
|
|
8139
|
-
if (!
|
|
8822
|
+
const root = findProjectRoot16(opts.dir);
|
|
8823
|
+
const paths = resolveHaivePaths13(root);
|
|
8824
|
+
if (!existsSync36(paths.memoriesDir)) {
|
|
8140
8825
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8141
8826
|
process.exitCode = 1;
|
|
8142
8827
|
return;
|
|
8143
8828
|
}
|
|
8144
|
-
const all = await
|
|
8829
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8145
8830
|
if (opts.all || opts.pending) {
|
|
8146
8831
|
const candidates = all.filter((m) => {
|
|
8147
8832
|
const s = m.memory.frontmatter.status;
|
|
@@ -8158,7 +8843,7 @@ function registerMemoryApprove(memory2) {
|
|
|
8158
8843
|
frontmatter: { ...found2.memory.frontmatter, status: "validated" },
|
|
8159
8844
|
body: found2.memory.body
|
|
8160
8845
|
};
|
|
8161
|
-
await
|
|
8846
|
+
await writeFile18(found2.filePath, serializeMemory15(next2), "utf8");
|
|
8162
8847
|
count++;
|
|
8163
8848
|
}
|
|
8164
8849
|
ui.success(`Approved ${count} memor${count === 1 ? "y" : "ies"} (status=validated)`);
|
|
@@ -8187,32 +8872,32 @@ function registerMemoryApprove(memory2) {
|
|
|
8187
8872
|
frontmatter: { ...found.memory.frontmatter, status: "validated" },
|
|
8188
8873
|
body: found.memory.body
|
|
8189
8874
|
};
|
|
8190
|
-
await
|
|
8875
|
+
await writeFile18(found.filePath, serializeMemory15(next), "utf8");
|
|
8191
8876
|
ui.success(`Approved ${id} (status=validated)`);
|
|
8192
|
-
ui.info(
|
|
8877
|
+
ui.info(path19.relative(root, found.filePath));
|
|
8193
8878
|
});
|
|
8194
8879
|
}
|
|
8195
8880
|
|
|
8196
8881
|
// src/commands/memory-update.ts
|
|
8197
|
-
import { writeFile as
|
|
8198
|
-
import { existsSync as
|
|
8199
|
-
import
|
|
8882
|
+
import { writeFile as writeFile19 } from "fs/promises";
|
|
8883
|
+
import { existsSync as existsSync37 } from "fs";
|
|
8884
|
+
import path20 from "path";
|
|
8200
8885
|
import "commander";
|
|
8201
8886
|
import {
|
|
8202
|
-
findProjectRoot as
|
|
8203
|
-
resolveHaivePaths as
|
|
8204
|
-
serializeMemory as
|
|
8887
|
+
findProjectRoot as findProjectRoot17,
|
|
8888
|
+
resolveHaivePaths as resolveHaivePaths14,
|
|
8889
|
+
serializeMemory as serializeMemory16
|
|
8205
8890
|
} from "@hiveai/core";
|
|
8206
8891
|
function registerMemoryUpdate(memory2) {
|
|
8207
8892
|
memory2.command("update <id>").description("Update body, tags, or anchor of an existing memory (preserves id and usage history)").option("--title <text>", "new title \u2014 replaces the first heading of the body").option("--body <text>", "new Markdown body \u2014 replaces the existing body").option("--tags <csv>", "new tags, comma-separated \u2014 fully replaces existing tags").option("--paths <csv>", "new anchor paths, comma-separated").option("--symbols <csv>", "new anchor symbols, comma-separated").option("--commit <sha>", "new anchor commit SHA").option("--domain <domain>", "new domain label").option("--author <author>", "new author handle or email").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
8208
|
-
const root =
|
|
8209
|
-
const paths =
|
|
8210
|
-
if (!
|
|
8893
|
+
const root = findProjectRoot17(opts.dir);
|
|
8894
|
+
const paths = resolveHaivePaths14(root);
|
|
8895
|
+
if (!existsSync37(paths.memoriesDir)) {
|
|
8211
8896
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
8212
8897
|
process.exitCode = 1;
|
|
8213
8898
|
return;
|
|
8214
8899
|
}
|
|
8215
|
-
const memories = await
|
|
8900
|
+
const memories = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8216
8901
|
const loaded = memories.find((m) => m.memory.frontmatter.id === id);
|
|
8217
8902
|
if (!loaded) {
|
|
8218
8903
|
ui.error(`No memory with id "${id}".`);
|
|
@@ -8254,12 +8939,12 @@ function registerMemoryUpdate(memory2) {
|
|
|
8254
8939
|
ui.warn("Nothing to update \u2014 provide at least one option.");
|
|
8255
8940
|
return;
|
|
8256
8941
|
}
|
|
8257
|
-
await
|
|
8942
|
+
await writeFile19(
|
|
8258
8943
|
loaded.filePath,
|
|
8259
|
-
|
|
8944
|
+
serializeMemory16({ frontmatter: newFrontmatter, body: newBody }),
|
|
8260
8945
|
"utf8"
|
|
8261
8946
|
);
|
|
8262
|
-
ui.success(`Updated ${
|
|
8947
|
+
ui.success(`Updated ${path20.relative(root, loaded.filePath)}`);
|
|
8263
8948
|
ui.info(`fields: ${updated.join(", ")}`);
|
|
8264
8949
|
});
|
|
8265
8950
|
}
|
|
@@ -8278,18 +8963,18 @@ function parseCsv3(value) {
|
|
|
8278
8963
|
}
|
|
8279
8964
|
|
|
8280
8965
|
// src/commands/memory-auto-promote.ts
|
|
8281
|
-
import { writeFile as
|
|
8282
|
-
import { existsSync as
|
|
8283
|
-
import
|
|
8966
|
+
import { writeFile as writeFile20 } from "fs/promises";
|
|
8967
|
+
import { existsSync as existsSync38 } from "fs";
|
|
8968
|
+
import path21 from "path";
|
|
8284
8969
|
import "commander";
|
|
8285
8970
|
import {
|
|
8286
8971
|
DEFAULT_AUTO_PROMOTE_RULE as DEFAULT_AUTO_PROMOTE_RULE3,
|
|
8287
|
-
findProjectRoot as
|
|
8288
|
-
getUsage as
|
|
8972
|
+
findProjectRoot as findProjectRoot18,
|
|
8973
|
+
getUsage as getUsage12,
|
|
8289
8974
|
isAutoPromoteEligible as isAutoPromoteEligible3,
|
|
8290
|
-
loadUsageIndex as
|
|
8291
|
-
resolveHaivePaths as
|
|
8292
|
-
serializeMemory as
|
|
8975
|
+
loadUsageIndex as loadUsageIndex14,
|
|
8976
|
+
resolveHaivePaths as resolveHaivePaths15,
|
|
8977
|
+
serializeMemory as serializeMemory17
|
|
8293
8978
|
} from "@hiveai/core";
|
|
8294
8979
|
function registerMemoryAutoPromote(memory2) {
|
|
8295
8980
|
memory2.command("auto-promote").description("Promote eligible 'proposed' memories to 'validated' based on usage").option("--min-reads <n>", "minimum read_count to qualify", String(DEFAULT_AUTO_PROMOTE_RULE3.minReads)).option(
|
|
@@ -8297,9 +8982,9 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
8297
8982
|
"memories with more rejections than this are skipped",
|
|
8298
8983
|
String(DEFAULT_AUTO_PROMOTE_RULE3.maxRejections)
|
|
8299
8984
|
).option("--apply", "actually write status=validated to disk (default: dry-run)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
8300
|
-
const root =
|
|
8301
|
-
const paths =
|
|
8302
|
-
if (!
|
|
8985
|
+
const root = findProjectRoot18(opts.dir);
|
|
8986
|
+
const paths = resolveHaivePaths15(root);
|
|
8987
|
+
if (!existsSync38(paths.memoriesDir)) {
|
|
8303
8988
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8304
8989
|
process.exitCode = 1;
|
|
8305
8990
|
return;
|
|
@@ -8308,10 +8993,10 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
8308
8993
|
minReads: Number(opts.minReads ?? DEFAULT_AUTO_PROMOTE_RULE3.minReads),
|
|
8309
8994
|
maxRejections: Number(opts.maxRejections ?? DEFAULT_AUTO_PROMOTE_RULE3.maxRejections)
|
|
8310
8995
|
};
|
|
8311
|
-
const memories = await
|
|
8312
|
-
const usage = await
|
|
8996
|
+
const memories = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8997
|
+
const usage = await loadUsageIndex14(paths);
|
|
8313
8998
|
const eligible = memories.filter(
|
|
8314
|
-
({ memory: memory3 }) => isAutoPromoteEligible3(memory3.frontmatter,
|
|
8999
|
+
({ memory: memory3 }) => isAutoPromoteEligible3(memory3.frontmatter, getUsage12(usage, memory3.frontmatter.id), rule)
|
|
8315
9000
|
);
|
|
8316
9001
|
if (eligible.length === 0) {
|
|
8317
9002
|
ui.info(
|
|
@@ -8321,17 +9006,17 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
8321
9006
|
}
|
|
8322
9007
|
let written = 0;
|
|
8323
9008
|
for (const { memory: mem, filePath } of eligible) {
|
|
8324
|
-
const u =
|
|
9009
|
+
const u = getUsage12(usage, mem.frontmatter.id);
|
|
8325
9010
|
console.log(
|
|
8326
9011
|
`${ui.bold(opts.apply ? "PROMOTE" : "would promote")} ${mem.frontmatter.id} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
8327
9012
|
);
|
|
8328
|
-
console.log(` ${ui.dim(
|
|
9013
|
+
console.log(` ${ui.dim(path21.relative(root, filePath))}`);
|
|
8329
9014
|
if (opts.apply) {
|
|
8330
9015
|
const next = {
|
|
8331
9016
|
frontmatter: { ...mem.frontmatter, status: "validated" },
|
|
8332
9017
|
body: mem.body
|
|
8333
9018
|
};
|
|
8334
|
-
await
|
|
9019
|
+
await writeFile20(filePath, serializeMemory17(next), "utf8");
|
|
8335
9020
|
written++;
|
|
8336
9021
|
}
|
|
8337
9022
|
}
|
|
@@ -8342,25 +9027,25 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
8342
9027
|
|
|
8343
9028
|
// src/commands/memory-edit.ts
|
|
8344
9029
|
import { spawn as spawn3 } from "child_process";
|
|
8345
|
-
import { existsSync as
|
|
8346
|
-
import { readFile as
|
|
8347
|
-
import
|
|
9030
|
+
import { existsSync as existsSync39 } from "fs";
|
|
9031
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
9032
|
+
import path23 from "path";
|
|
8348
9033
|
import "commander";
|
|
8349
9034
|
import {
|
|
8350
|
-
findProjectRoot as
|
|
9035
|
+
findProjectRoot as findProjectRoot19,
|
|
8351
9036
|
parseMemory,
|
|
8352
|
-
resolveHaivePaths as
|
|
9037
|
+
resolveHaivePaths as resolveHaivePaths16
|
|
8353
9038
|
} from "@hiveai/core";
|
|
8354
9039
|
function registerMemoryEdit(memory2) {
|
|
8355
9040
|
memory2.command("edit <id>").description("Open a memory in $EDITOR and re-validate when you save").option("-e, --editor <cmd>", "editor command (defaults to $EDITOR or 'vi')").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
8356
|
-
const root =
|
|
8357
|
-
const paths =
|
|
8358
|
-
if (!
|
|
9041
|
+
const root = findProjectRoot19(opts.dir);
|
|
9042
|
+
const paths = resolveHaivePaths16(root);
|
|
9043
|
+
if (!existsSync39(paths.memoriesDir)) {
|
|
8359
9044
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8360
9045
|
process.exitCode = 1;
|
|
8361
9046
|
return;
|
|
8362
9047
|
}
|
|
8363
|
-
const all = await
|
|
9048
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8364
9049
|
const found = all.find((m) => m.memory.frontmatter.id === id);
|
|
8365
9050
|
if (!found) {
|
|
8366
9051
|
ui.error(`No memory with id "${id}".`);
|
|
@@ -8368,13 +9053,13 @@ function registerMemoryEdit(memory2) {
|
|
|
8368
9053
|
return;
|
|
8369
9054
|
}
|
|
8370
9055
|
const editor = opts.editor ?? process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
8371
|
-
ui.info(`Opening ${
|
|
9056
|
+
ui.info(`Opening ${path23.relative(root, found.filePath)} with ${editor}\u2026`);
|
|
8372
9057
|
const code = await runEditor(editor, found.filePath);
|
|
8373
9058
|
if (code !== 0) {
|
|
8374
9059
|
ui.warn(`Editor exited with status ${code}.`);
|
|
8375
9060
|
}
|
|
8376
9061
|
try {
|
|
8377
|
-
const fresh = await
|
|
9062
|
+
const fresh = await readFile11(found.filePath, "utf8");
|
|
8378
9063
|
parseMemory(fresh);
|
|
8379
9064
|
ui.success("Memory still parses cleanly.");
|
|
8380
9065
|
} catch (err) {
|
|
@@ -8395,29 +9080,29 @@ function runEditor(editor, file) {
|
|
|
8395
9080
|
}
|
|
8396
9081
|
|
|
8397
9082
|
// src/commands/memory-for-files.ts
|
|
8398
|
-
import { existsSync as
|
|
8399
|
-
import
|
|
9083
|
+
import { existsSync as existsSync40 } from "fs";
|
|
9084
|
+
import path24 from "path";
|
|
8400
9085
|
import "commander";
|
|
8401
9086
|
import {
|
|
8402
9087
|
deriveConfidence as deriveConfidence9,
|
|
8403
|
-
findProjectRoot as
|
|
8404
|
-
getUsage as
|
|
9088
|
+
findProjectRoot as findProjectRoot20,
|
|
9089
|
+
getUsage as getUsage13,
|
|
8405
9090
|
inferModulesFromPaths as inferModulesFromPaths4,
|
|
8406
|
-
loadUsageIndex as
|
|
9091
|
+
loadUsageIndex as loadUsageIndex15,
|
|
8407
9092
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths5,
|
|
8408
|
-
resolveHaivePaths as
|
|
9093
|
+
resolveHaivePaths as resolveHaivePaths17
|
|
8409
9094
|
} from "@hiveai/core";
|
|
8410
9095
|
function registerMemoryForFiles(memory2) {
|
|
8411
9096
|
memory2.command("for-files <files...>").description("Show memories relevant to the given files (anchor overlap, module, domain)").option("-d, --dir <dir>", "project root").action(async (files, opts) => {
|
|
8412
|
-
const root =
|
|
8413
|
-
const paths =
|
|
8414
|
-
if (!
|
|
9097
|
+
const root = findProjectRoot20(opts.dir);
|
|
9098
|
+
const paths = resolveHaivePaths17(root);
|
|
9099
|
+
if (!existsSync40(paths.memoriesDir)) {
|
|
8415
9100
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8416
9101
|
process.exitCode = 1;
|
|
8417
9102
|
return;
|
|
8418
9103
|
}
|
|
8419
|
-
const all = await
|
|
8420
|
-
const usage = await
|
|
9104
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
9105
|
+
const usage = await loadUsageIndex15(paths);
|
|
8421
9106
|
const inferred = inferModulesFromPaths4(files);
|
|
8422
9107
|
const byAnchor = [];
|
|
8423
9108
|
const byModule = [];
|
|
@@ -8515,44 +9200,44 @@ function printGroup(root, label, loaded, usage) {
|
|
|
8515
9200
|
\u2014 ${label} \u2014`));
|
|
8516
9201
|
for (const { memory: mem, filePath } of loaded) {
|
|
8517
9202
|
const fm = mem.frontmatter;
|
|
8518
|
-
const u =
|
|
9203
|
+
const u = getUsage13(usage, fm.id);
|
|
8519
9204
|
const conf = deriveConfidence9(fm, u);
|
|
8520
9205
|
console.log(`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(conf)}`);
|
|
8521
|
-
console.log(` ${ui.dim(
|
|
9206
|
+
console.log(` ${ui.dim(path24.relative(root, filePath))}`);
|
|
8522
9207
|
}
|
|
8523
9208
|
}
|
|
8524
9209
|
|
|
8525
9210
|
// src/commands/memory-hot.ts
|
|
8526
|
-
import { existsSync as
|
|
8527
|
-
import
|
|
9211
|
+
import { existsSync as existsSync41 } from "fs";
|
|
9212
|
+
import path25 from "path";
|
|
8528
9213
|
import "commander";
|
|
8529
9214
|
import {
|
|
8530
|
-
findProjectRoot as
|
|
8531
|
-
getUsage as
|
|
8532
|
-
loadUsageIndex as
|
|
8533
|
-
resolveHaivePaths as
|
|
9215
|
+
findProjectRoot as findProjectRoot21,
|
|
9216
|
+
getUsage as getUsage14,
|
|
9217
|
+
loadUsageIndex as loadUsageIndex16,
|
|
9218
|
+
resolveHaivePaths as resolveHaivePaths18
|
|
8534
9219
|
} from "@hiveai/core";
|
|
8535
9220
|
function registerMemoryHot(memory2) {
|
|
8536
9221
|
memory2.command("hot").description("List memories actively used but not yet validated (good promotion candidates)").option("--threshold <n>", "minimum read_count to qualify", "3").option("--status <status>", "limit to one status (default: draft + proposed)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
8537
|
-
const root =
|
|
8538
|
-
const paths =
|
|
8539
|
-
if (!
|
|
9222
|
+
const root = findProjectRoot21(opts.dir);
|
|
9223
|
+
const paths = resolveHaivePaths18(root);
|
|
9224
|
+
if (!existsSync41(paths.memoriesDir)) {
|
|
8540
9225
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8541
9226
|
process.exitCode = 1;
|
|
8542
9227
|
return;
|
|
8543
9228
|
}
|
|
8544
9229
|
const threshold = Math.max(1, Number(opts.threshold ?? 3));
|
|
8545
|
-
const all = await
|
|
8546
|
-
const usage = await
|
|
9230
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
9231
|
+
const usage = await loadUsageIndex16(paths);
|
|
8547
9232
|
const candidates = all.filter(({ memory: mem }) => {
|
|
8548
9233
|
const fm = mem.frontmatter;
|
|
8549
9234
|
if (opts.status && fm.status !== opts.status) return false;
|
|
8550
9235
|
if (opts.status === void 0 && fm.status !== "draft" && fm.status !== "proposed") {
|
|
8551
9236
|
return false;
|
|
8552
9237
|
}
|
|
8553
|
-
return
|
|
9238
|
+
return getUsage14(usage, fm.id).read_count >= threshold;
|
|
8554
9239
|
}).sort(
|
|
8555
|
-
(a, b) =>
|
|
9240
|
+
(a, b) => getUsage14(usage, b.memory.frontmatter.id).read_count - getUsage14(usage, a.memory.frontmatter.id).read_count
|
|
8556
9241
|
);
|
|
8557
9242
|
if (candidates.length === 0) {
|
|
8558
9243
|
ui.info(`No hot memories (threshold=${threshold}).`);
|
|
@@ -8560,11 +9245,11 @@ function registerMemoryHot(memory2) {
|
|
|
8560
9245
|
}
|
|
8561
9246
|
for (const { memory: mem, filePath } of candidates) {
|
|
8562
9247
|
const fm = mem.frontmatter;
|
|
8563
|
-
const u =
|
|
9248
|
+
const u = getUsage14(usage, fm.id);
|
|
8564
9249
|
console.log(
|
|
8565
9250
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(fm.status)} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
8566
9251
|
);
|
|
8567
|
-
console.log(` ${ui.dim(
|
|
9252
|
+
console.log(` ${ui.dim(path25.relative(root, filePath))}`);
|
|
8568
9253
|
}
|
|
8569
9254
|
ui.info(
|
|
8570
9255
|
`${candidates.length} hot \u2014 promote drafts with \`haive memory promote <id>\`, then \`haive memory auto-promote --apply\`.`
|
|
@@ -8573,16 +9258,16 @@ function registerMemoryHot(memory2) {
|
|
|
8573
9258
|
}
|
|
8574
9259
|
|
|
8575
9260
|
// src/commands/memory-tried.ts
|
|
8576
|
-
import { mkdir as mkdir13, writeFile as
|
|
8577
|
-
import { existsSync as
|
|
8578
|
-
import
|
|
9261
|
+
import { mkdir as mkdir13, writeFile as writeFile21 } from "fs/promises";
|
|
9262
|
+
import { existsSync as existsSync43 } from "fs";
|
|
9263
|
+
import path26 from "path";
|
|
8579
9264
|
import "commander";
|
|
8580
9265
|
import {
|
|
8581
9266
|
buildFrontmatter as buildFrontmatter8,
|
|
8582
|
-
findProjectRoot as
|
|
9267
|
+
findProjectRoot as findProjectRoot22,
|
|
8583
9268
|
memoryFilePath as memoryFilePath8,
|
|
8584
|
-
resolveHaivePaths as
|
|
8585
|
-
serializeMemory as
|
|
9269
|
+
resolveHaivePaths as resolveHaivePaths19,
|
|
9270
|
+
serializeMemory as serializeMemory18
|
|
8586
9271
|
} from "@hiveai/core";
|
|
8587
9272
|
function registerMemoryTried(memory2) {
|
|
8588
9273
|
memory2.command("tried").description(
|
|
@@ -8601,9 +9286,9 @@ function registerMemoryTried(memory2) {
|
|
|
8601
9286
|
--paths packages/cli/src/index.ts
|
|
8602
9287
|
`
|
|
8603
9288
|
).requiredOption("--what <text>", "what approach was tried (short, descriptive title)").requiredOption("--why-failed <text>", "why it failed or should NOT be used (include the exact error if possible)").option("--instead <text>", "the correct approach to use instead").option("--scope <scope>", "personal | team | module (default: personal)", "personal").option("--module <name>", "module name (required when scope=module)").option("--tags <csv>", "comma-separated tags").option("--paths <csv>", "anchor paths, comma-separated").option("--author <author>", "author email or handle").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
8604
|
-
const root =
|
|
8605
|
-
const paths =
|
|
8606
|
-
if (!
|
|
9289
|
+
const root = findProjectRoot22(opts.dir);
|
|
9290
|
+
const paths = resolveHaivePaths19(root);
|
|
9291
|
+
if (!existsSync43(paths.haiveDir)) {
|
|
8607
9292
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
8608
9293
|
process.exitCode = 1;
|
|
8609
9294
|
return;
|
|
@@ -8626,14 +9311,14 @@ function registerMemoryTried(memory2) {
|
|
|
8626
9311
|
}
|
|
8627
9312
|
const body = lines.join("\n") + "\n";
|
|
8628
9313
|
const file = memoryFilePath8(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
8629
|
-
await mkdir13(
|
|
8630
|
-
if (
|
|
9314
|
+
await mkdir13(path26.dirname(file), { recursive: true });
|
|
9315
|
+
if (existsSync43(file)) {
|
|
8631
9316
|
ui.error(`Memory already exists at ${file}`);
|
|
8632
9317
|
process.exitCode = 1;
|
|
8633
9318
|
return;
|
|
8634
9319
|
}
|
|
8635
|
-
await
|
|
8636
|
-
ui.success(`Recorded: ${
|
|
9320
|
+
await writeFile21(file, serializeMemory18({ frontmatter, body }), "utf8");
|
|
9321
|
+
ui.success(`Recorded: ${path26.relative(root, file)}`);
|
|
8637
9322
|
ui.info(`id=${frontmatter.id} type=attempt status=validated (auto-approved)`);
|
|
8638
9323
|
});
|
|
8639
9324
|
}
|
|
@@ -8643,26 +9328,26 @@ function parseCsv4(value) {
|
|
|
8643
9328
|
}
|
|
8644
9329
|
|
|
8645
9330
|
// src/commands/memory-pending.ts
|
|
8646
|
-
import { existsSync as
|
|
8647
|
-
import
|
|
9331
|
+
import { existsSync as existsSync44 } from "fs";
|
|
9332
|
+
import path27 from "path";
|
|
8648
9333
|
import "commander";
|
|
8649
9334
|
import {
|
|
8650
|
-
findProjectRoot as
|
|
8651
|
-
getUsage as
|
|
8652
|
-
loadUsageIndex as
|
|
8653
|
-
resolveHaivePaths as
|
|
9335
|
+
findProjectRoot as findProjectRoot23,
|
|
9336
|
+
getUsage as getUsage15,
|
|
9337
|
+
loadUsageIndex as loadUsageIndex17,
|
|
9338
|
+
resolveHaivePaths as resolveHaivePaths20
|
|
8654
9339
|
} from "@hiveai/core";
|
|
8655
9340
|
function registerMemoryPending(memory2) {
|
|
8656
9341
|
memory2.command("pending").description("List 'proposed' memories awaiting review (sorted by reads desc)").option("--scope <scope>", "filter by scope (personal | team | module)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
8657
|
-
const root =
|
|
8658
|
-
const paths =
|
|
8659
|
-
if (!
|
|
9342
|
+
const root = findProjectRoot23(opts.dir);
|
|
9343
|
+
const paths = resolveHaivePaths20(root);
|
|
9344
|
+
if (!existsSync44(paths.memoriesDir)) {
|
|
8660
9345
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8661
9346
|
process.exitCode = 1;
|
|
8662
9347
|
return;
|
|
8663
9348
|
}
|
|
8664
|
-
const all = await
|
|
8665
|
-
const usage = await
|
|
9349
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
9350
|
+
const usage = await loadUsageIndex17(paths);
|
|
8666
9351
|
const proposed = all.filter(({ memory: mem }) => {
|
|
8667
9352
|
if (mem.frontmatter.status !== "proposed") return false;
|
|
8668
9353
|
if (opts.scope && mem.frontmatter.scope !== opts.scope) return false;
|
|
@@ -8673,42 +9358,42 @@ function registerMemoryPending(memory2) {
|
|
|
8673
9358
|
return;
|
|
8674
9359
|
}
|
|
8675
9360
|
proposed.sort(
|
|
8676
|
-
(a, b) =>
|
|
9361
|
+
(a, b) => getUsage15(usage, b.memory.frontmatter.id).read_count - getUsage15(usage, a.memory.frontmatter.id).read_count
|
|
8677
9362
|
);
|
|
8678
9363
|
const now = Date.now();
|
|
8679
9364
|
for (const { memory: mem, filePath } of proposed) {
|
|
8680
9365
|
const fm = mem.frontmatter;
|
|
8681
|
-
const u =
|
|
9366
|
+
const u = getUsage15(usage, fm.id);
|
|
8682
9367
|
const ageDays = Math.floor((now - new Date(fm.created_at).getTime()) / 864e5);
|
|
8683
9368
|
const ageStr = ageDays === 0 ? "today" : `${ageDays}d`;
|
|
8684
9369
|
console.log(
|
|
8685
9370
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.dim(`age=${ageStr} reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
8686
9371
|
);
|
|
8687
|
-
console.log(` ${ui.dim(
|
|
9372
|
+
console.log(` ${ui.dim(path27.relative(root, filePath))}`);
|
|
8688
9373
|
}
|
|
8689
9374
|
ui.info(`${proposed.length} pending`);
|
|
8690
9375
|
});
|
|
8691
9376
|
}
|
|
8692
9377
|
|
|
8693
9378
|
// src/commands/memory-query.ts
|
|
8694
|
-
import { existsSync as
|
|
8695
|
-
import
|
|
9379
|
+
import { existsSync as existsSync45 } from "fs";
|
|
9380
|
+
import path28 from "path";
|
|
8696
9381
|
import "commander";
|
|
8697
9382
|
import {
|
|
8698
9383
|
extractSnippet as extractSnippet2,
|
|
8699
|
-
findProjectRoot as
|
|
9384
|
+
findProjectRoot as findProjectRoot24,
|
|
8700
9385
|
literalMatchesAllTokens as literalMatchesAllTokens3,
|
|
8701
9386
|
literalMatchesAnyToken as literalMatchesAnyToken4,
|
|
8702
9387
|
pickSnippetNeedle as pickSnippetNeedle2,
|
|
8703
|
-
resolveHaivePaths as
|
|
9388
|
+
resolveHaivePaths as resolveHaivePaths21,
|
|
8704
9389
|
tokenizeQuery as tokenizeQuery6,
|
|
8705
9390
|
trackReads as trackReads4
|
|
8706
9391
|
} from "@hiveai/core";
|
|
8707
9392
|
function registerMemoryQuery(memory2) {
|
|
8708
9393
|
memory2.command("query <text>").alias("search").description("Search memories by id, tag, or substring (AND, OR fallback). Alias: search").option("-d, --dir <dir>", "project root").option("--limit <n>", "max results", "20").option("--scope <scope>", "personal | team | module").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected)").option("--show-rejected", "include rejected memories (hidden by default)").action(async (text, opts) => {
|
|
8709
|
-
const root =
|
|
8710
|
-
const paths =
|
|
8711
|
-
if (!
|
|
9394
|
+
const root = findProjectRoot24(opts.dir);
|
|
9395
|
+
const paths = resolveHaivePaths21(root);
|
|
9396
|
+
if (!existsSync45(paths.memoriesDir)) {
|
|
8712
9397
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
8713
9398
|
process.exitCode = 1;
|
|
8714
9399
|
return;
|
|
@@ -8719,7 +9404,7 @@ function registerMemoryQuery(memory2) {
|
|
|
8719
9404
|
return;
|
|
8720
9405
|
}
|
|
8721
9406
|
const statusFilter = opts.status ? opts.status.split(",").map((s) => s.trim()) : null;
|
|
8722
|
-
const all = await
|
|
9407
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8723
9408
|
const passesFilters2 = (mem) => {
|
|
8724
9409
|
const fm = mem.frontmatter;
|
|
8725
9410
|
if (opts.scope && fm.scope !== opts.scope) return false;
|
|
@@ -8749,7 +9434,7 @@ function registerMemoryQuery(memory2) {
|
|
|
8749
9434
|
const fm = mem.frontmatter;
|
|
8750
9435
|
const statusBadge = ui.statusBadge(fm.status);
|
|
8751
9436
|
console.log(`${ui.bold(fm.id)} ${ui.dim(fm.scope)} ${statusBadge}`);
|
|
8752
|
-
console.log(` ${ui.dim(
|
|
9437
|
+
console.log(` ${ui.dim(path28.relative(root, filePath))}`);
|
|
8753
9438
|
const snippet = extractSnippet2(mem.body, snippetNeedle);
|
|
8754
9439
|
if (snippet) console.log(` ${snippet}`);
|
|
8755
9440
|
}
|
|
@@ -8766,36 +9451,36 @@ ${top.length} of ${matches.length} match${matches.length === 1 ? "" : "es"}`)
|
|
|
8766
9451
|
}
|
|
8767
9452
|
|
|
8768
9453
|
// src/commands/memory-reject.ts
|
|
8769
|
-
import { writeFile as
|
|
8770
|
-
import { existsSync as
|
|
9454
|
+
import { writeFile as writeFile23 } from "fs/promises";
|
|
9455
|
+
import { existsSync as existsSync46 } from "fs";
|
|
8771
9456
|
import "commander";
|
|
8772
9457
|
import {
|
|
8773
|
-
findProjectRoot as
|
|
8774
|
-
loadUsageIndex as
|
|
9458
|
+
findProjectRoot as findProjectRoot25,
|
|
9459
|
+
loadUsageIndex as loadUsageIndex18,
|
|
8775
9460
|
recordRejection as recordRejection2,
|
|
8776
|
-
resolveHaivePaths as
|
|
9461
|
+
resolveHaivePaths as resolveHaivePaths22,
|
|
8777
9462
|
saveUsageIndex as saveUsageIndex3,
|
|
8778
|
-
serializeMemory as
|
|
9463
|
+
serializeMemory as serializeMemory19
|
|
8779
9464
|
} from "@hiveai/core";
|
|
8780
9465
|
function registerMemoryReject(memory2) {
|
|
8781
9466
|
memory2.command("reject <id>").description("Record a rejection (blocks auto-promotion and lowers confidence)").option("-r, --reason <reason>", "why this memory is being rejected").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
8782
|
-
const root =
|
|
8783
|
-
const paths =
|
|
8784
|
-
if (!
|
|
9467
|
+
const root = findProjectRoot25(opts.dir);
|
|
9468
|
+
const paths = resolveHaivePaths22(root);
|
|
9469
|
+
if (!existsSync46(paths.memoriesDir)) {
|
|
8785
9470
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8786
9471
|
process.exitCode = 1;
|
|
8787
9472
|
return;
|
|
8788
9473
|
}
|
|
8789
|
-
const memories = await
|
|
9474
|
+
const memories = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8790
9475
|
const loaded = memories.find((m) => m.memory.frontmatter.id === id);
|
|
8791
9476
|
if (!loaded) {
|
|
8792
9477
|
ui.error(`No memory with id "${id}".`);
|
|
8793
9478
|
process.exitCode = 1;
|
|
8794
9479
|
return;
|
|
8795
9480
|
}
|
|
8796
|
-
await
|
|
9481
|
+
await writeFile23(
|
|
8797
9482
|
loaded.filePath,
|
|
8798
|
-
|
|
9483
|
+
serializeMemory19({
|
|
8799
9484
|
frontmatter: {
|
|
8800
9485
|
...loaded.memory.frontmatter,
|
|
8801
9486
|
status: "rejected",
|
|
@@ -8805,7 +9490,7 @@ function registerMemoryReject(memory2) {
|
|
|
8805
9490
|
}),
|
|
8806
9491
|
"utf8"
|
|
8807
9492
|
);
|
|
8808
|
-
const idx = await
|
|
9493
|
+
const idx = await loadUsageIndex18(paths);
|
|
8809
9494
|
recordRejection2(idx, id, opts.reason ?? null);
|
|
8810
9495
|
await saveUsageIndex3(paths, idx);
|
|
8811
9496
|
const u = idx.by_id[id];
|
|
@@ -8817,34 +9502,34 @@ function registerMemoryReject(memory2) {
|
|
|
8817
9502
|
}
|
|
8818
9503
|
|
|
8819
9504
|
// src/commands/memory-rm.ts
|
|
8820
|
-
import { existsSync as
|
|
9505
|
+
import { existsSync as existsSync47 } from "fs";
|
|
8821
9506
|
import { unlink as unlink3 } from "fs/promises";
|
|
8822
|
-
import
|
|
9507
|
+
import path29 from "path";
|
|
8823
9508
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
8824
9509
|
import "commander";
|
|
8825
9510
|
import {
|
|
8826
|
-
findProjectRoot as
|
|
8827
|
-
loadUsageIndex as
|
|
8828
|
-
resolveHaivePaths as
|
|
9511
|
+
findProjectRoot as findProjectRoot26,
|
|
9512
|
+
loadUsageIndex as loadUsageIndex19,
|
|
9513
|
+
resolveHaivePaths as resolveHaivePaths23,
|
|
8829
9514
|
saveUsageIndex as saveUsageIndex4
|
|
8830
9515
|
} from "@hiveai/core";
|
|
8831
9516
|
function registerMemoryRm(memory2) {
|
|
8832
9517
|
memory2.command("rm <id>").description("Delete a memory file (and its usage entry by default)").option("-y, --yes", "skip the confirmation prompt").option("--keep-usage", "do not remove the usage.json entry").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
8833
|
-
const root =
|
|
8834
|
-
const paths =
|
|
8835
|
-
if (!
|
|
9518
|
+
const root = findProjectRoot26(opts.dir);
|
|
9519
|
+
const paths = resolveHaivePaths23(root);
|
|
9520
|
+
if (!existsSync47(paths.memoriesDir)) {
|
|
8836
9521
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8837
9522
|
process.exitCode = 1;
|
|
8838
9523
|
return;
|
|
8839
9524
|
}
|
|
8840
|
-
const all = await
|
|
9525
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8841
9526
|
const found = all.find((m) => m.memory.frontmatter.id === id);
|
|
8842
9527
|
if (!found) {
|
|
8843
9528
|
ui.error(`No memory with id "${id}".`);
|
|
8844
9529
|
process.exitCode = 1;
|
|
8845
9530
|
return;
|
|
8846
9531
|
}
|
|
8847
|
-
const rel =
|
|
9532
|
+
const rel = path29.relative(root, found.filePath);
|
|
8848
9533
|
if (!opts.yes) {
|
|
8849
9534
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
8850
9535
|
const answer = (await rl.question(`Delete ${rel}? [y/N] `)).trim().toLowerCase();
|
|
@@ -8857,7 +9542,7 @@ function registerMemoryRm(memory2) {
|
|
|
8857
9542
|
await unlink3(found.filePath);
|
|
8858
9543
|
ui.success(`Deleted ${rel}`);
|
|
8859
9544
|
if (!opts.keepUsage) {
|
|
8860
|
-
const idx = await
|
|
9545
|
+
const idx = await loadUsageIndex19(paths);
|
|
8861
9546
|
if (idx.by_id[id]) {
|
|
8862
9547
|
delete idx.by_id[id];
|
|
8863
9548
|
await saveUsageIndex4(paths, idx);
|
|
@@ -8868,27 +9553,27 @@ function registerMemoryRm(memory2) {
|
|
|
8868
9553
|
}
|
|
8869
9554
|
|
|
8870
9555
|
// src/commands/memory-show.ts
|
|
8871
|
-
import { existsSync as
|
|
8872
|
-
import { readFile as
|
|
8873
|
-
import
|
|
9556
|
+
import { existsSync as existsSync48 } from "fs";
|
|
9557
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
9558
|
+
import path30 from "path";
|
|
8874
9559
|
import "commander";
|
|
8875
9560
|
import {
|
|
8876
9561
|
deriveConfidence as deriveConfidence10,
|
|
8877
|
-
findProjectRoot as
|
|
8878
|
-
getUsage as
|
|
8879
|
-
loadUsageIndex as
|
|
8880
|
-
resolveHaivePaths as
|
|
9562
|
+
findProjectRoot as findProjectRoot27,
|
|
9563
|
+
getUsage as getUsage16,
|
|
9564
|
+
loadUsageIndex as loadUsageIndex20,
|
|
9565
|
+
resolveHaivePaths as resolveHaivePaths24
|
|
8881
9566
|
} from "@hiveai/core";
|
|
8882
9567
|
function registerMemoryShow(memory2) {
|
|
8883
9568
|
memory2.command("show <id>").description("Print a memory's frontmatter, body, and confidence/usage").option("--raw", "print the raw file contents instead of a summary").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
8884
|
-
const root =
|
|
8885
|
-
const paths =
|
|
8886
|
-
if (!
|
|
9569
|
+
const root = findProjectRoot27(opts.dir);
|
|
9570
|
+
const paths = resolveHaivePaths24(root);
|
|
9571
|
+
if (!existsSync48(paths.memoriesDir)) {
|
|
8887
9572
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8888
9573
|
process.exitCode = 1;
|
|
8889
9574
|
return;
|
|
8890
9575
|
}
|
|
8891
|
-
const all = await
|
|
9576
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8892
9577
|
const found = all.find((m) => m.memory.frontmatter.id === id);
|
|
8893
9578
|
if (!found) {
|
|
8894
9579
|
ui.error(`No memory with id "${id}".`);
|
|
@@ -8896,12 +9581,12 @@ function registerMemoryShow(memory2) {
|
|
|
8896
9581
|
return;
|
|
8897
9582
|
}
|
|
8898
9583
|
if (opts.raw) {
|
|
8899
|
-
console.log(await
|
|
9584
|
+
console.log(await readFile12(found.filePath, "utf8"));
|
|
8900
9585
|
return;
|
|
8901
9586
|
}
|
|
8902
9587
|
const fm = found.memory.frontmatter;
|
|
8903
|
-
const usage = await
|
|
8904
|
-
const u =
|
|
9588
|
+
const usage = await loadUsageIndex20(paths);
|
|
9589
|
+
const u = getUsage16(usage, fm.id);
|
|
8905
9590
|
const conf = deriveConfidence10(fm, u);
|
|
8906
9591
|
console.log(ui.bold(fm.id));
|
|
8907
9592
|
console.log(`${ui.dim("scope:")} ${fm.scope}${fm.module ? ` / ${fm.module}` : ""}`);
|
|
@@ -8912,7 +9597,7 @@ function registerMemoryShow(memory2) {
|
|
|
8912
9597
|
if (fm.verified_at) console.log(`${ui.dim("verified:")} ${fm.verified_at}`);
|
|
8913
9598
|
if (fm.stale_reason) console.log(`${ui.dim("stale:")} ${fm.stale_reason}`);
|
|
8914
9599
|
console.log(`${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`);
|
|
8915
|
-
console.log(`${ui.dim("file:")} ${
|
|
9600
|
+
console.log(`${ui.dim("file:")} ${path30.relative(root, found.filePath)}`);
|
|
8916
9601
|
if (fm.anchor.paths.length || fm.anchor.symbols.length) {
|
|
8917
9602
|
console.log(ui.dim("anchor:"));
|
|
8918
9603
|
if (fm.anchor.commit) console.log(` ${ui.dim("commit:")} ${fm.anchor.commit}`);
|
|
@@ -8927,38 +9612,38 @@ function registerMemoryShow(memory2) {
|
|
|
8927
9612
|
}
|
|
8928
9613
|
|
|
8929
9614
|
// src/commands/memory-stats.ts
|
|
8930
|
-
import { existsSync as
|
|
8931
|
-
import
|
|
9615
|
+
import { existsSync as existsSync49 } from "fs";
|
|
9616
|
+
import path31 from "path";
|
|
8932
9617
|
import "commander";
|
|
8933
9618
|
import {
|
|
8934
9619
|
deriveConfidence as deriveConfidence11,
|
|
8935
|
-
findProjectRoot as
|
|
8936
|
-
getUsage as
|
|
8937
|
-
loadUsageIndex as
|
|
8938
|
-
resolveHaivePaths as
|
|
9620
|
+
findProjectRoot as findProjectRoot28,
|
|
9621
|
+
getUsage as getUsage17,
|
|
9622
|
+
loadUsageIndex as loadUsageIndex21,
|
|
9623
|
+
resolveHaivePaths as resolveHaivePaths25
|
|
8939
9624
|
} from "@hiveai/core";
|
|
8940
9625
|
function registerMemoryStats(memory2) {
|
|
8941
9626
|
memory2.command("stats").description("Show usage stats and confidence levels per memory").option("--id <id>", "show stats for a single memory id").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
8942
|
-
const root =
|
|
8943
|
-
const paths =
|
|
8944
|
-
if (!
|
|
9627
|
+
const root = findProjectRoot28(opts.dir);
|
|
9628
|
+
const paths = resolveHaivePaths25(root);
|
|
9629
|
+
if (!existsSync49(paths.memoriesDir)) {
|
|
8945
9630
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
8946
9631
|
process.exitCode = 1;
|
|
8947
9632
|
return;
|
|
8948
9633
|
}
|
|
8949
|
-
const all = await
|
|
8950
|
-
const usage = await
|
|
9634
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
9635
|
+
const usage = await loadUsageIndex21(paths);
|
|
8951
9636
|
const target = opts.id ? all.filter((m) => m.memory.frontmatter.id === opts.id) : all;
|
|
8952
9637
|
if (target.length === 0) {
|
|
8953
9638
|
ui.info(opts.id ? `No memory with id "${opts.id}".` : "No memories.");
|
|
8954
9639
|
return;
|
|
8955
9640
|
}
|
|
8956
9641
|
target.sort(
|
|
8957
|
-
(a, b) =>
|
|
9642
|
+
(a, b) => getUsage17(usage, b.memory.frontmatter.id).read_count - getUsage17(usage, a.memory.frontmatter.id).read_count
|
|
8958
9643
|
);
|
|
8959
9644
|
for (const { memory: mem, filePath } of target) {
|
|
8960
9645
|
const fm = mem.frontmatter;
|
|
8961
|
-
const u =
|
|
9646
|
+
const u = getUsage17(usage, fm.id);
|
|
8962
9647
|
const conf = deriveConfidence11(fm, u);
|
|
8963
9648
|
console.log(
|
|
8964
9649
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(conf)}`
|
|
@@ -8966,34 +9651,34 @@ function registerMemoryStats(memory2) {
|
|
|
8966
9651
|
console.log(
|
|
8967
9652
|
` ${ui.dim("status:")} ${fm.status} ${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`
|
|
8968
9653
|
);
|
|
8969
|
-
console.log(` ${ui.dim(
|
|
9654
|
+
console.log(` ${ui.dim(path31.relative(root, filePath))}`);
|
|
8970
9655
|
}
|
|
8971
9656
|
});
|
|
8972
9657
|
}
|
|
8973
9658
|
|
|
8974
9659
|
// src/commands/memory-verify.ts
|
|
8975
|
-
import { writeFile as
|
|
8976
|
-
import { existsSync as
|
|
8977
|
-
import
|
|
9660
|
+
import { writeFile as writeFile24 } from "fs/promises";
|
|
9661
|
+
import { existsSync as existsSync50 } from "fs";
|
|
9662
|
+
import path33 from "path";
|
|
8978
9663
|
import "commander";
|
|
8979
9664
|
import {
|
|
8980
|
-
findProjectRoot as
|
|
8981
|
-
resolveHaivePaths as
|
|
8982
|
-
serializeMemory as
|
|
9665
|
+
findProjectRoot as findProjectRoot29,
|
|
9666
|
+
resolveHaivePaths as resolveHaivePaths26,
|
|
9667
|
+
serializeMemory as serializeMemory20,
|
|
8983
9668
|
verifyAnchor as verifyAnchor3
|
|
8984
9669
|
} from "@hiveai/core";
|
|
8985
9670
|
function registerMemoryVerify(memory2) {
|
|
8986
9671
|
memory2.command("verify").description(
|
|
8987
9672
|
"Check that memory anchor paths still exist in the current codebase.\n\n A memory is 'stale' when its anchored file or symbol was moved, deleted, or renamed.\n Stale memories are shown with a warning in get_briefing and should be updated or deleted.\n\n haive sync runs this automatically. Use this command for on-demand checks or in CI.\n\n CI recommendation: add 'haive memory verify' to your haive-sync.yml PR check job\n to catch stale memories before they reach main.\n\n Examples:\n haive memory verify # check all, report only\n haive memory verify --update # mark stale/fresh on disk\n haive memory verify --id 2026-04-28-gotcha-x # check one memory\n"
|
|
8988
9673
|
).option("--id <id>", "verify a single memory by id").option("--all", "verify every memory (default if --id is omitted)").option("--update", "write status=stale or status=validated back to disk").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
8989
|
-
const root =
|
|
8990
|
-
const paths =
|
|
8991
|
-
if (!
|
|
9674
|
+
const root = findProjectRoot29(opts.dir);
|
|
9675
|
+
const paths = resolveHaivePaths26(root);
|
|
9676
|
+
if (!existsSync50(paths.memoriesDir)) {
|
|
8992
9677
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
8993
9678
|
process.exitCode = 1;
|
|
8994
9679
|
return;
|
|
8995
9680
|
}
|
|
8996
|
-
const all = await
|
|
9681
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8997
9682
|
const targets = opts.id ? all.filter((m) => m.memory.frontmatter.id === opts.id) : all;
|
|
8998
9683
|
if (opts.id && targets.length === 0) {
|
|
8999
9684
|
ui.error(`No memory with id "${opts.id}".`);
|
|
@@ -9011,7 +9696,7 @@ function registerMemoryVerify(memory2) {
|
|
|
9011
9696
|
anchorlessIds.push(mem.frontmatter.id);
|
|
9012
9697
|
continue;
|
|
9013
9698
|
}
|
|
9014
|
-
const rel =
|
|
9699
|
+
const rel = path33.relative(root, filePath);
|
|
9015
9700
|
if (result.stale) {
|
|
9016
9701
|
staleCount++;
|
|
9017
9702
|
console.log(`${ui.bold("STALE")} ${mem.frontmatter.id}`);
|
|
@@ -9026,7 +9711,7 @@ function registerMemoryVerify(memory2) {
|
|
|
9026
9711
|
}
|
|
9027
9712
|
if (opts.update) {
|
|
9028
9713
|
const next = applyVerification2(mem, result);
|
|
9029
|
-
await
|
|
9714
|
+
await writeFile24(filePath, serializeMemory20(next), "utf8");
|
|
9030
9715
|
updated++;
|
|
9031
9716
|
}
|
|
9032
9717
|
}
|
|
@@ -9074,30 +9759,30 @@ function applyVerification2(mem, result) {
|
|
|
9074
9759
|
}
|
|
9075
9760
|
|
|
9076
9761
|
// src/commands/memory-import.ts
|
|
9077
|
-
import { readFile as
|
|
9078
|
-
import { existsSync as
|
|
9762
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
9763
|
+
import { existsSync as existsSync51 } from "fs";
|
|
9079
9764
|
import "commander";
|
|
9080
9765
|
import {
|
|
9081
|
-
findProjectRoot as
|
|
9082
|
-
resolveHaivePaths as
|
|
9766
|
+
findProjectRoot as findProjectRoot30,
|
|
9767
|
+
resolveHaivePaths as resolveHaivePaths27
|
|
9083
9768
|
} from "@hiveai/core";
|
|
9084
9769
|
function registerMemoryImport(memory2) {
|
|
9085
9770
|
memory2.command("import").description(
|
|
9086
9771
|
"Parse a Markdown file and suggest memories via the import_docs MCP prompt (prints a ready-to-use prompt invocation)"
|
|
9087
9772
|
).requiredOption("--from <file>", "Markdown/text file to import from").option("--scope <scope>", "personal | team (default: team)", "team").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9088
|
-
const root =
|
|
9089
|
-
const paths =
|
|
9090
|
-
if (!
|
|
9773
|
+
const root = findProjectRoot30(opts.dir);
|
|
9774
|
+
const paths = resolveHaivePaths27(root);
|
|
9775
|
+
if (!existsSync51(paths.haiveDir)) {
|
|
9091
9776
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
9092
9777
|
process.exitCode = 1;
|
|
9093
9778
|
return;
|
|
9094
9779
|
}
|
|
9095
|
-
if (!
|
|
9780
|
+
if (!existsSync51(opts.from)) {
|
|
9096
9781
|
ui.error(`File not found: ${opts.from}`);
|
|
9097
9782
|
process.exitCode = 1;
|
|
9098
9783
|
return;
|
|
9099
9784
|
}
|
|
9100
|
-
const content = await
|
|
9785
|
+
const content = await readFile13(opts.from, "utf8");
|
|
9101
9786
|
const scope = opts.scope ?? "team";
|
|
9102
9787
|
ui.info(`Preparing import from: ${opts.from} (scope=${scope})`);
|
|
9103
9788
|
ui.info(`Content length: ${content.length} chars`);
|
|
@@ -9125,15 +9810,15 @@ function registerMemoryImport(memory2) {
|
|
|
9125
9810
|
}
|
|
9126
9811
|
|
|
9127
9812
|
// src/commands/memory-import-changelog.ts
|
|
9128
|
-
import { existsSync as
|
|
9129
|
-
import { readFile as
|
|
9130
|
-
import
|
|
9813
|
+
import { existsSync as existsSync53 } from "fs";
|
|
9814
|
+
import { readFile as readFile14, mkdir as mkdir14, writeFile as writeFile25 } from "fs/promises";
|
|
9815
|
+
import path34 from "path";
|
|
9131
9816
|
import "commander";
|
|
9132
9817
|
import {
|
|
9133
9818
|
buildFrontmatter as buildFrontmatter9,
|
|
9134
|
-
findProjectRoot as
|
|
9135
|
-
resolveHaivePaths as
|
|
9136
|
-
serializeMemory as
|
|
9819
|
+
findProjectRoot as findProjectRoot31,
|
|
9820
|
+
resolveHaivePaths as resolveHaivePaths28,
|
|
9821
|
+
serializeMemory as serializeMemory21
|
|
9137
9822
|
} from "@hiveai/core";
|
|
9138
9823
|
function parseChangelog(content) {
|
|
9139
9824
|
const entries = [];
|
|
@@ -9197,15 +9882,15 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
9197
9882
|
"--versions <csv>",
|
|
9198
9883
|
"only import specific versions (comma-separated), or 'latest' for the most recent breaking version"
|
|
9199
9884
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9200
|
-
const root =
|
|
9201
|
-
const paths =
|
|
9202
|
-
const changelogPath =
|
|
9203
|
-
if (!
|
|
9885
|
+
const root = findProjectRoot31(opts.dir);
|
|
9886
|
+
const paths = resolveHaivePaths28(root);
|
|
9887
|
+
const changelogPath = path34.resolve(root, opts.fromChangelog);
|
|
9888
|
+
if (!existsSync53(changelogPath)) {
|
|
9204
9889
|
ui.error(`CHANGELOG not found: ${changelogPath}`);
|
|
9205
9890
|
process.exitCode = 1;
|
|
9206
9891
|
return;
|
|
9207
9892
|
}
|
|
9208
|
-
const content = await
|
|
9893
|
+
const content = await readFile14(changelogPath, "utf8");
|
|
9209
9894
|
let entries = parseChangelog(content);
|
|
9210
9895
|
if (entries.length === 0) {
|
|
9211
9896
|
ui.warn("No breaking changes, deprecations, or removals found in the CHANGELOG.");
|
|
@@ -9220,9 +9905,9 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
9220
9905
|
entries = entries.filter((e) => requested.includes(e.version));
|
|
9221
9906
|
}
|
|
9222
9907
|
}
|
|
9223
|
-
const pkgName = opts.package ??
|
|
9908
|
+
const pkgName = opts.package ?? path34.basename(path34.dirname(changelogPath));
|
|
9224
9909
|
const scope = opts.scope ?? "team";
|
|
9225
|
-
const teamDir =
|
|
9910
|
+
const teamDir = path34.join(paths.memoriesDir, scope);
|
|
9226
9911
|
await mkdir14(teamDir, { recursive: true });
|
|
9227
9912
|
let saved = 0;
|
|
9228
9913
|
for (const entry of entries) {
|
|
@@ -9245,7 +9930,7 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
9245
9930
|
lines.push("");
|
|
9246
9931
|
}
|
|
9247
9932
|
lines.push(
|
|
9248
|
-
`**Source:** \`${
|
|
9933
|
+
`**Source:** \`${path34.relative(root, changelogPath)}\`
|
|
9249
9934
|
**Action:** Update all usages of ${pkgName} if they rely on any of the above.`
|
|
9250
9935
|
);
|
|
9251
9936
|
const slug = `changelog-${pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase()}-v${entry.version.replace(/\./g, "-")}`;
|
|
@@ -9260,12 +9945,12 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
9260
9945
|
pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase(),
|
|
9261
9946
|
`v${entry.version}`
|
|
9262
9947
|
],
|
|
9263
|
-
paths: [
|
|
9948
|
+
paths: [path34.relative(root, changelogPath)],
|
|
9264
9949
|
topic: `changelog-${pkgName}-${entry.version}`
|
|
9265
9950
|
});
|
|
9266
|
-
await
|
|
9267
|
-
|
|
9268
|
-
|
|
9951
|
+
await writeFile25(
|
|
9952
|
+
path34.join(teamDir, `${fm.id}.md`),
|
|
9953
|
+
serializeMemory21({ frontmatter: fm, body: lines.join("\n") }),
|
|
9269
9954
|
"utf8"
|
|
9270
9955
|
);
|
|
9271
9956
|
console.log(ui.green(` \u2713 ${fm.id}`));
|
|
@@ -9287,17 +9972,17 @@ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${p
|
|
|
9287
9972
|
}
|
|
9288
9973
|
|
|
9289
9974
|
// src/commands/memory-digest.ts
|
|
9290
|
-
import { existsSync as
|
|
9291
|
-
import { writeFile as
|
|
9292
|
-
import
|
|
9975
|
+
import { existsSync as existsSync54 } from "fs";
|
|
9976
|
+
import { writeFile as writeFile26 } from "fs/promises";
|
|
9977
|
+
import path35 from "path";
|
|
9293
9978
|
import "commander";
|
|
9294
9979
|
import {
|
|
9295
9980
|
deriveConfidence as deriveConfidence12,
|
|
9296
|
-
findProjectRoot as
|
|
9297
|
-
getUsage as
|
|
9298
|
-
loadMemoriesFromDir as
|
|
9299
|
-
loadUsageIndex as
|
|
9300
|
-
resolveHaivePaths as
|
|
9981
|
+
findProjectRoot as findProjectRoot32,
|
|
9982
|
+
getUsage as getUsage18,
|
|
9983
|
+
loadMemoriesFromDir as loadMemoriesFromDir27,
|
|
9984
|
+
loadUsageIndex as loadUsageIndex23,
|
|
9985
|
+
resolveHaivePaths as resolveHaivePaths29
|
|
9301
9986
|
} from "@hiveai/core";
|
|
9302
9987
|
var CONFIDENCE_EMOJI = {
|
|
9303
9988
|
unverified: "\u2B1C",
|
|
@@ -9310,9 +9995,9 @@ function registerMemoryDigest(program2) {
|
|
|
9310
9995
|
program2.command("digest").description(
|
|
9311
9996
|
"Generate a Markdown review digest of recently added or updated memories.\n\n Groups memories by type, shows confidence, status, read count, and anchor info.\n Each memory has action checkboxes (approve / reject / keep as-is) for peer review.\n\n Use this to do a bulk weekly review of team memories, or share with teammates\n as a pull-request attachment so humans can validate what the AI captured.\n\n Examples:\n haive memory digest # last 7 days, team scope\n haive memory digest --days 30 --scope all # last 30 days, all scopes\n haive memory digest --out review.md # write to file\n"
|
|
9312
9997
|
).option("--days <n>", "look-back window in days (default: 7)", "7").option("--scope <scope>", "personal | team | module | all (default: team)", "team").option("--out <file>", "write digest to a file instead of stdout").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9313
|
-
const root =
|
|
9314
|
-
const paths =
|
|
9315
|
-
if (!
|
|
9998
|
+
const root = findProjectRoot32(opts.dir);
|
|
9999
|
+
const paths = resolveHaivePaths29(root);
|
|
10000
|
+
if (!existsSync54(paths.memoriesDir)) {
|
|
9316
10001
|
ui.error("No .ai/memories found. Run `haive init` first.");
|
|
9317
10002
|
process.exitCode = 1;
|
|
9318
10003
|
return;
|
|
@@ -9320,8 +10005,8 @@ function registerMemoryDigest(program2) {
|
|
|
9320
10005
|
const days = Math.max(1, Number(opts.days ?? 7));
|
|
9321
10006
|
const scopeFilter = opts.scope ?? "team";
|
|
9322
10007
|
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
9323
|
-
const all = await
|
|
9324
|
-
const usage = await
|
|
10008
|
+
const all = await loadMemoriesFromDir27(paths.memoriesDir);
|
|
10009
|
+
const usage = await loadUsageIndex23(paths);
|
|
9325
10010
|
const recent = all.filter(({ memory: mem }) => {
|
|
9326
10011
|
const fm = mem.frontmatter;
|
|
9327
10012
|
if (fm.type === "session_recap") return false;
|
|
@@ -9352,7 +10037,7 @@ function registerMemoryDigest(program2) {
|
|
|
9352
10037
|
lines.push(``);
|
|
9353
10038
|
for (const { memory: mem } of mems) {
|
|
9354
10039
|
const fm = mem.frontmatter;
|
|
9355
|
-
const u =
|
|
10040
|
+
const u = getUsage18(usage, fm.id);
|
|
9356
10041
|
const confidence = deriveConfidence12(fm, u);
|
|
9357
10042
|
const emoji = CONFIDENCE_EMOJI[confidence] ?? "\u2B1C";
|
|
9358
10043
|
const anchor = fm.anchor.paths.length > 0 ? `\`${fm.anchor.paths[0]}\`` + (fm.anchor.paths.length > 1 ? ` +${fm.anchor.paths.length - 1}` : "") : "_no anchor_";
|
|
@@ -9384,8 +10069,8 @@ function registerMemoryDigest(program2) {
|
|
|
9384
10069
|
);
|
|
9385
10070
|
const digest = lines.join("\n");
|
|
9386
10071
|
if (opts.out) {
|
|
9387
|
-
const outPath =
|
|
9388
|
-
await
|
|
10072
|
+
const outPath = path35.resolve(process.cwd(), opts.out);
|
|
10073
|
+
await writeFile26(outPath, digest, "utf8");
|
|
9389
10074
|
ui.success(`Digest written to ${opts.out} (${recent.length} memor${recent.length === 1 ? "y" : "ies"})`);
|
|
9390
10075
|
} else {
|
|
9391
10076
|
console.log(digest);
|
|
@@ -9394,22 +10079,22 @@ function registerMemoryDigest(program2) {
|
|
|
9394
10079
|
}
|
|
9395
10080
|
|
|
9396
10081
|
// src/commands/session-end.ts
|
|
9397
|
-
import { writeFile as
|
|
9398
|
-
import { existsSync as
|
|
9399
|
-
import
|
|
10082
|
+
import { writeFile as writeFile27, mkdir as mkdir15, readFile as readFile15, rm as rm2 } from "fs/promises";
|
|
10083
|
+
import { existsSync as existsSync55 } from "fs";
|
|
10084
|
+
import path36 from "path";
|
|
9400
10085
|
import "commander";
|
|
9401
10086
|
import {
|
|
9402
10087
|
buildFrontmatter as buildFrontmatter10,
|
|
9403
|
-
findProjectRoot as
|
|
9404
|
-
loadMemoriesFromDir as
|
|
10088
|
+
findProjectRoot as findProjectRoot33,
|
|
10089
|
+
loadMemoriesFromDir as loadMemoriesFromDir28,
|
|
9405
10090
|
memoryFilePath as memoryFilePath9,
|
|
9406
|
-
resolveHaivePaths as
|
|
9407
|
-
serializeMemory as
|
|
10091
|
+
resolveHaivePaths as resolveHaivePaths30,
|
|
10092
|
+
serializeMemory as serializeMemory23
|
|
9408
10093
|
} from "@hiveai/core";
|
|
9409
10094
|
async function buildAutoRecap(paths) {
|
|
9410
|
-
const obsFile =
|
|
9411
|
-
if (!
|
|
9412
|
-
const raw = await
|
|
10095
|
+
const obsFile = path36.join(paths.haiveDir, ".cache", "observations.jsonl");
|
|
10096
|
+
if (!existsSync55(obsFile)) return null;
|
|
10097
|
+
const raw = await readFile15(obsFile, "utf8").catch(() => "");
|
|
9413
10098
|
if (!raw.trim()) return null;
|
|
9414
10099
|
const lines = raw.split("\n").filter(Boolean);
|
|
9415
10100
|
const obs = [];
|
|
@@ -9487,9 +10172,9 @@ function registerSessionEnd(session2) {
|
|
|
9487
10172
|
--next "Add integration tests for webhook signature validation"
|
|
9488
10173
|
`
|
|
9489
10174
|
).option("--goal <text>", "what you were trying to accomplish (1\u20132 sentences)").option("--accomplished <text>", "what was actually done (bullet list recommended)").option("--discoveries <text>", "bugs, surprises, or inconsistencies found during this session").option("--files <csv>", "key files touched, comma-separated (used as anchor for staleness detection)").option("--next <text>", "what should happen next (for the next session or a teammate)").option("--scope <scope>", "personal | team | module (default: personal)", "personal").option("--module <name>", "module name (required when scope=module)").option("--auto", "synthesize the recap from .ai/.cache/observations.jsonl (used by Claude Code SessionEnd hook)").option("--quiet", "suppress non-error output (for hook use)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9490
|
-
const root =
|
|
9491
|
-
const paths =
|
|
9492
|
-
if (!
|
|
10175
|
+
const root = findProjectRoot33(opts.dir);
|
|
10176
|
+
const paths = resolveHaivePaths30(root);
|
|
10177
|
+
if (!existsSync55(paths.haiveDir)) {
|
|
9493
10178
|
if (opts.auto || opts.quiet) return;
|
|
9494
10179
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
9495
10180
|
process.exitCode = 1;
|
|
@@ -9521,19 +10206,19 @@ function registerSessionEnd(session2) {
|
|
|
9521
10206
|
});
|
|
9522
10207
|
const topic = recapTopic2(scope, opts.module);
|
|
9523
10208
|
const filesTouched = parseCsv5(resolvedFiles);
|
|
9524
|
-
const missingPaths = filesTouched.filter((p) => !
|
|
10209
|
+
const missingPaths = filesTouched.filter((p) => !existsSync55(path36.resolve(root, p)));
|
|
9525
10210
|
if (missingPaths.length > 0 && !opts.quiet) {
|
|
9526
10211
|
ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
|
|
9527
10212
|
for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
|
|
9528
10213
|
}
|
|
9529
10214
|
const cleanupObservations = async () => {
|
|
9530
10215
|
if (!opts.auto) return;
|
|
9531
|
-
const obsFile =
|
|
9532
|
-
if (
|
|
10216
|
+
const obsFile = path36.join(paths.haiveDir, ".cache", "observations.jsonl");
|
|
10217
|
+
if (existsSync55(obsFile)) await rm2(obsFile).catch(() => {
|
|
9533
10218
|
});
|
|
9534
10219
|
};
|
|
9535
|
-
if (
|
|
9536
|
-
const existing = await
|
|
10220
|
+
if (existsSync55(paths.memoriesDir)) {
|
|
10221
|
+
const existing = await loadMemoriesFromDir28(paths.memoriesDir);
|
|
9537
10222
|
const topicMatch = existing.find(
|
|
9538
10223
|
({ memory: memory2 }) => memory2.frontmatter.topic === topic && memory2.frontmatter.scope === scope && (!opts.module || memory2.frontmatter.module === opts.module)
|
|
9539
10224
|
);
|
|
@@ -9549,11 +10234,11 @@ function registerSessionEnd(session2) {
|
|
|
9549
10234
|
paths: filesTouched.length ? filesTouched : fm.anchor.paths
|
|
9550
10235
|
}
|
|
9551
10236
|
};
|
|
9552
|
-
await
|
|
10237
|
+
await writeFile27(topicMatch.filePath, serializeMemory23({ frontmatter: newFrontmatter, body }), "utf8");
|
|
9553
10238
|
await cleanupObservations();
|
|
9554
10239
|
if (!opts.quiet) {
|
|
9555
10240
|
ui.success(`Session recap updated (revision #${revisionCount})`);
|
|
9556
|
-
ui.info(`id=${fm.id} file=${
|
|
10241
|
+
ui.info(`id=${fm.id} file=${path36.relative(root, topicMatch.filePath)}`);
|
|
9557
10242
|
ui.info("Tip: `haive stats --export-report` generates a usage JSON suitable for dashboards.");
|
|
9558
10243
|
}
|
|
9559
10244
|
return;
|
|
@@ -9570,12 +10255,12 @@ function registerSessionEnd(session2) {
|
|
|
9570
10255
|
status: "validated"
|
|
9571
10256
|
});
|
|
9572
10257
|
const file = memoryFilePath9(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
9573
|
-
await mkdir15(
|
|
9574
|
-
await
|
|
10258
|
+
await mkdir15(path36.dirname(file), { recursive: true });
|
|
10259
|
+
await writeFile27(file, serializeMemory23({ frontmatter, body }), "utf8");
|
|
9575
10260
|
await cleanupObservations();
|
|
9576
10261
|
if (!opts.quiet) {
|
|
9577
10262
|
ui.success(`Session recap created`);
|
|
9578
|
-
ui.info(`id=${frontmatter.id} scope=${scope} file=${
|
|
10263
|
+
ui.info(`id=${frontmatter.id} scope=${scope} file=${path36.relative(root, file)}`);
|
|
9579
10264
|
ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
|
|
9580
10265
|
ui.info("Tip: export a local MCP usage rollup with `haive stats --export-report .ai/tool-usage-roi-report.json`.");
|
|
9581
10266
|
}
|
|
@@ -9587,15 +10272,15 @@ function parseCsv5(value) {
|
|
|
9587
10272
|
}
|
|
9588
10273
|
|
|
9589
10274
|
// src/commands/snapshot.ts
|
|
9590
|
-
import { existsSync as
|
|
10275
|
+
import { existsSync as existsSync56 } from "fs";
|
|
9591
10276
|
import { readdir as readdir4 } from "fs/promises";
|
|
9592
|
-
import
|
|
10277
|
+
import path37 from "path";
|
|
9593
10278
|
import "commander";
|
|
9594
10279
|
import {
|
|
9595
10280
|
diffContract,
|
|
9596
|
-
findProjectRoot as
|
|
9597
|
-
loadConfig as
|
|
9598
|
-
resolveHaivePaths as
|
|
10281
|
+
findProjectRoot as findProjectRoot34,
|
|
10282
|
+
loadConfig as loadConfig7,
|
|
10283
|
+
resolveHaivePaths as resolveHaivePaths31,
|
|
9599
10284
|
snapshotContract
|
|
9600
10285
|
} from "@hiveai/core";
|
|
9601
10286
|
function registerSnapshot(program2) {
|
|
@@ -9620,16 +10305,16 @@ function registerSnapshot(program2) {
|
|
|
9620
10305
|
"--format <format>",
|
|
9621
10306
|
"contract format: openapi | graphql | proto | typescript | json-schema (auto-detected if omitted)"
|
|
9622
10307
|
).option("--diff", "compare the contract against its stored snapshot").option("--list", "list all stored contract snapshots").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9623
|
-
const root =
|
|
9624
|
-
const paths =
|
|
9625
|
-
if (!
|
|
10308
|
+
const root = findProjectRoot34(opts.dir);
|
|
10309
|
+
const paths = resolveHaivePaths31(root);
|
|
10310
|
+
if (!existsSync56(paths.haiveDir)) {
|
|
9626
10311
|
ui.error("No .ai/ found. Run `haive init` first.");
|
|
9627
10312
|
process.exitCode = 1;
|
|
9628
10313
|
return;
|
|
9629
10314
|
}
|
|
9630
10315
|
if (opts.list) {
|
|
9631
|
-
const contractsDir =
|
|
9632
|
-
if (!
|
|
10316
|
+
const contractsDir = path37.join(paths.haiveDir, "contracts");
|
|
10317
|
+
if (!existsSync56(contractsDir)) {
|
|
9633
10318
|
console.log(ui.dim("No contract snapshots found."));
|
|
9634
10319
|
return;
|
|
9635
10320
|
}
|
|
@@ -9649,7 +10334,7 @@ function registerSnapshot(program2) {
|
|
|
9649
10334
|
}
|
|
9650
10335
|
if (opts.diff) {
|
|
9651
10336
|
if (!opts.name) {
|
|
9652
|
-
const config2 = await
|
|
10337
|
+
const config2 = await loadConfig7(paths);
|
|
9653
10338
|
const contracts = config2.contractFiles ?? [];
|
|
9654
10339
|
if (contracts.length === 0) {
|
|
9655
10340
|
ui.error("--diff requires --name, or configure contractFiles in haive.config.json");
|
|
@@ -9661,7 +10346,7 @@ function registerSnapshot(program2) {
|
|
|
9661
10346
|
}
|
|
9662
10347
|
return;
|
|
9663
10348
|
}
|
|
9664
|
-
const config = await
|
|
10349
|
+
const config = await loadConfig7(paths);
|
|
9665
10350
|
const configured = (config.contractFiles ?? []).find((c) => c.name === opts.name);
|
|
9666
10351
|
if (!configured && !opts.contract) {
|
|
9667
10352
|
ui.error(
|
|
@@ -9684,7 +10369,7 @@ function registerSnapshot(program2) {
|
|
|
9684
10369
|
return;
|
|
9685
10370
|
}
|
|
9686
10371
|
const contractPath = opts.contract;
|
|
9687
|
-
const name = opts.name ??
|
|
10372
|
+
const name = opts.name ?? path37.basename(contractPath, path37.extname(contractPath));
|
|
9688
10373
|
const format = opts.format ?? detectFormat(contractPath) ?? "openapi";
|
|
9689
10374
|
const contract = { name, path: contractPath, format };
|
|
9690
10375
|
try {
|
|
@@ -9739,8 +10424,8 @@ async function runDiff(root, haiveDir, contract) {
|
|
|
9739
10424
|
}
|
|
9740
10425
|
}
|
|
9741
10426
|
function detectFormat(filePath) {
|
|
9742
|
-
const ext =
|
|
9743
|
-
const base =
|
|
10427
|
+
const ext = path37.extname(filePath).toLowerCase();
|
|
10428
|
+
const base = path37.basename(filePath).toLowerCase();
|
|
9744
10429
|
if (ext === ".yaml" || ext === ".yml" || ext === ".json") {
|
|
9745
10430
|
if (base.includes("openapi") || base.includes("swagger")) return "openapi";
|
|
9746
10431
|
if (base.includes("schema") || base.includes("graphql")) return "graphql";
|
|
@@ -9753,18 +10438,18 @@ function detectFormat(filePath) {
|
|
|
9753
10438
|
}
|
|
9754
10439
|
|
|
9755
10440
|
// src/commands/hub.ts
|
|
9756
|
-
import { existsSync as
|
|
9757
|
-
import { mkdir as mkdir16, readFile as
|
|
9758
|
-
import
|
|
9759
|
-
import { spawnSync as
|
|
10441
|
+
import { existsSync as existsSync57 } from "fs";
|
|
10442
|
+
import { mkdir as mkdir16, readFile as readFile16, writeFile as writeFile28, copyFile } from "fs/promises";
|
|
10443
|
+
import path38 from "path";
|
|
10444
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
9760
10445
|
import "commander";
|
|
9761
10446
|
import {
|
|
9762
|
-
findProjectRoot as
|
|
9763
|
-
loadConfig as
|
|
9764
|
-
loadMemoriesFromDir as
|
|
9765
|
-
resolveHaivePaths as
|
|
9766
|
-
saveConfig as
|
|
9767
|
-
serializeMemory as
|
|
10447
|
+
findProjectRoot as findProjectRoot35,
|
|
10448
|
+
loadConfig as loadConfig8,
|
|
10449
|
+
loadMemoriesFromDir as loadMemoriesFromDir29,
|
|
10450
|
+
resolveHaivePaths as resolveHaivePaths32,
|
|
10451
|
+
saveConfig as saveConfig3,
|
|
10452
|
+
serializeMemory as serializeMemory24
|
|
9768
10453
|
} from "@hiveai/core";
|
|
9769
10454
|
function registerHub(program2) {
|
|
9770
10455
|
const hub = program2.command("hub").description(
|
|
@@ -9774,21 +10459,21 @@ function registerHub(program2) {
|
|
|
9774
10459
|
hub.command("init <hubPath>").description(
|
|
9775
10460
|
"Initialize a new team-knowledge hub repo at <hubPath>.\n\n Creates a git repo with a .ai/ directory structure ready for shared memories.\n\n Example:\n haive hub init ../team-hub\n haive hub init /srv/git/team-knowledge\n"
|
|
9776
10461
|
).action(async (hubPath) => {
|
|
9777
|
-
const absPath =
|
|
10462
|
+
const absPath = path38.resolve(hubPath);
|
|
9778
10463
|
await mkdir16(absPath, { recursive: true });
|
|
9779
|
-
const gitCheck =
|
|
10464
|
+
const gitCheck = spawnSync5("git", ["rev-parse", "--git-dir"], { cwd: absPath });
|
|
9780
10465
|
if (gitCheck.status !== 0) {
|
|
9781
|
-
const init =
|
|
10466
|
+
const init = spawnSync5("git", ["init"], { cwd: absPath, encoding: "utf8" });
|
|
9782
10467
|
if (init.status !== 0) {
|
|
9783
10468
|
ui.error(`git init failed: ${init.stderr}`);
|
|
9784
10469
|
process.exitCode = 1;
|
|
9785
10470
|
return;
|
|
9786
10471
|
}
|
|
9787
10472
|
}
|
|
9788
|
-
const sharedDir =
|
|
10473
|
+
const sharedDir = path38.join(absPath, ".ai", "memories", "shared");
|
|
9789
10474
|
await mkdir16(sharedDir, { recursive: true });
|
|
9790
|
-
await
|
|
9791
|
-
|
|
10475
|
+
await writeFile28(
|
|
10476
|
+
path38.join(absPath, ".ai", "README.md"),
|
|
9792
10477
|
`# hAIve Team Knowledge Hub
|
|
9793
10478
|
|
|
9794
10479
|
This repo is a shared knowledge hub for hAIve.
|
|
@@ -9809,13 +10494,13 @@ haive hub pull # import into a project
|
|
|
9809
10494
|
`,
|
|
9810
10495
|
"utf8"
|
|
9811
10496
|
);
|
|
9812
|
-
await
|
|
9813
|
-
|
|
10497
|
+
await writeFile28(
|
|
10498
|
+
path38.join(absPath, ".gitignore"),
|
|
9814
10499
|
".ai/.cache/\n.ai/memories/personal/\n",
|
|
9815
10500
|
"utf8"
|
|
9816
10501
|
);
|
|
9817
|
-
|
|
9818
|
-
|
|
10502
|
+
spawnSync5("git", ["add", "."], { cwd: absPath });
|
|
10503
|
+
spawnSync5("git", ["commit", "-m", "chore: initialize hAIve team-knowledge hub"], {
|
|
9819
10504
|
cwd: absPath,
|
|
9820
10505
|
encoding: "utf8"
|
|
9821
10506
|
});
|
|
@@ -9825,7 +10510,7 @@ haive hub pull # import into a project
|
|
|
9825
10510
|
`
|
|
9826
10511
|
Next steps:
|
|
9827
10512
|
1. Add hubPath to your project's .ai/haive.config.json:
|
|
9828
|
-
{ "hubPath": "${
|
|
10513
|
+
{ "hubPath": "${path38.relative(process.cwd(), absPath)}" }
|
|
9829
10514
|
2. Run \`haive hub push\` to publish your shared memories
|
|
9830
10515
|
3. Share ${absPath} with teammates (git remote, NFS, etc.)
|
|
9831
10516
|
`
|
|
@@ -9844,9 +10529,9 @@ Next steps:
|
|
|
9844
10529
|
haive hub push --commit --message "feat: add payment API contract memories"
|
|
9845
10530
|
`
|
|
9846
10531
|
).option("-d, --dir <dir>", "project root").option("--commit", "auto-commit to the hub repo after pushing").option("--message <msg>", "commit message for the hub (used with --commit)").action(async (opts) => {
|
|
9847
|
-
const root =
|
|
9848
|
-
const paths =
|
|
9849
|
-
const config = await
|
|
10532
|
+
const root = findProjectRoot35(opts.dir);
|
|
10533
|
+
const paths = resolveHaivePaths32(root);
|
|
10534
|
+
const config = await loadConfig8(paths);
|
|
9850
10535
|
if (!config.hubPath) {
|
|
9851
10536
|
ui.error(
|
|
9852
10537
|
'hubPath not configured in .ai/haive.config.json.\n Add: { "hubPath": "../team-hub" }\n Or run: haive hub init <path> first.'
|
|
@@ -9854,16 +10539,16 @@ Next steps:
|
|
|
9854
10539
|
process.exitCode = 1;
|
|
9855
10540
|
return;
|
|
9856
10541
|
}
|
|
9857
|
-
const hubRoot =
|
|
9858
|
-
if (!
|
|
10542
|
+
const hubRoot = path38.resolve(root, config.hubPath);
|
|
10543
|
+
if (!existsSync57(hubRoot)) {
|
|
9859
10544
|
ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
|
|
9860
10545
|
process.exitCode = 1;
|
|
9861
10546
|
return;
|
|
9862
10547
|
}
|
|
9863
|
-
const projectName =
|
|
9864
|
-
const destDir =
|
|
10548
|
+
const projectName = path38.basename(root);
|
|
10549
|
+
const destDir = path38.join(hubRoot, ".ai", "memories", "shared", projectName);
|
|
9865
10550
|
await mkdir16(destDir, { recursive: true });
|
|
9866
|
-
const all = await
|
|
10551
|
+
const all = await loadMemoriesFromDir29(paths.memoriesDir);
|
|
9867
10552
|
const shared = all.filter(
|
|
9868
10553
|
({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && // Don't push imported memories (avoid echo loops)
|
|
9869
10554
|
!memory2.frontmatter.tags.some((t) => t.startsWith("cross-repo:"))
|
|
@@ -9880,18 +10565,18 @@ Next steps:
|
|
|
9880
10565
|
for (const { memory: memory2 } of shared) {
|
|
9881
10566
|
const fm = memory2.frontmatter;
|
|
9882
10567
|
const fileName = `${fm.id}.md`;
|
|
9883
|
-
const destPath =
|
|
9884
|
-
await
|
|
10568
|
+
const destPath = path38.join(destDir, fileName);
|
|
10569
|
+
await writeFile28(destPath, serializeMemory24(memory2), "utf8");
|
|
9885
10570
|
pushed++;
|
|
9886
10571
|
}
|
|
9887
10572
|
console.log(ui.green(`\u2713 Pushed ${pushed} shared memor${pushed === 1 ? "y" : "ies"} to hub`));
|
|
9888
10573
|
console.log(ui.dim(` Location: ${destDir}`));
|
|
9889
10574
|
if (opts.commit) {
|
|
9890
10575
|
const message = opts.message ?? `haive: sync shared memories from ${projectName} (${pushed} memories)`;
|
|
9891
|
-
|
|
10576
|
+
spawnSync5("git", ["add", path38.join(".ai", "memories", "shared", projectName)], {
|
|
9892
10577
|
cwd: hubRoot
|
|
9893
10578
|
});
|
|
9894
|
-
const commit =
|
|
10579
|
+
const commit = spawnSync5("git", ["commit", "-m", message], {
|
|
9895
10580
|
cwd: hubRoot,
|
|
9896
10581
|
encoding: "utf8"
|
|
9897
10582
|
});
|
|
@@ -9913,9 +10598,9 @@ Next steps:
|
|
|
9913
10598
|
hub.command("pull").description(
|
|
9914
10599
|
"Pull shared memories from the hub into this project.\n\n Imports all memories from hub/.ai/memories/shared/ EXCEPT this project's own.\n Imported memories land in .ai/memories/shared/<source-project-name>/.\n\n Examples:\n haive hub pull\n"
|
|
9915
10600
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9916
|
-
const root =
|
|
9917
|
-
const paths =
|
|
9918
|
-
const config = await
|
|
10601
|
+
const root = findProjectRoot35(opts.dir);
|
|
10602
|
+
const paths = resolveHaivePaths32(root);
|
|
10603
|
+
const config = await loadConfig8(paths);
|
|
9919
10604
|
if (!config.hubPath) {
|
|
9920
10605
|
ui.error(
|
|
9921
10606
|
'hubPath not configured in .ai/haive.config.json.\n Add: { "hubPath": "../team-hub" }\n Or run: haive hub init <path> first.'
|
|
@@ -9923,13 +10608,13 @@ Next steps:
|
|
|
9923
10608
|
process.exitCode = 1;
|
|
9924
10609
|
return;
|
|
9925
10610
|
}
|
|
9926
|
-
const hubRoot =
|
|
9927
|
-
const hubSharedDir =
|
|
9928
|
-
if (!
|
|
10611
|
+
const hubRoot = path38.resolve(root, config.hubPath);
|
|
10612
|
+
const hubSharedDir = path38.join(hubRoot, ".ai", "memories", "shared");
|
|
10613
|
+
if (!existsSync57(hubSharedDir)) {
|
|
9929
10614
|
ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
|
|
9930
10615
|
return;
|
|
9931
10616
|
}
|
|
9932
|
-
const projectName =
|
|
10617
|
+
const projectName = path38.basename(root);
|
|
9933
10618
|
const { readdir: readdir6 } = await import("fs/promises");
|
|
9934
10619
|
const projectDirs = (await readdir6(hubSharedDir, { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== projectName).map((d) => d.name);
|
|
9935
10620
|
if (projectDirs.length === 0) {
|
|
@@ -9939,17 +10624,17 @@ Next steps:
|
|
|
9939
10624
|
let totalImported = 0;
|
|
9940
10625
|
let totalUpdated = 0;
|
|
9941
10626
|
for (const sourceName of projectDirs) {
|
|
9942
|
-
const sourceDir =
|
|
9943
|
-
const destDir =
|
|
10627
|
+
const sourceDir = path38.join(hubSharedDir, sourceName);
|
|
10628
|
+
const destDir = path38.join(paths.memoriesDir, "shared", sourceName);
|
|
9944
10629
|
await mkdir16(destDir, { recursive: true });
|
|
9945
10630
|
const sourceFiles = (await readdir6(sourceDir)).filter((f) => f.endsWith(".md"));
|
|
9946
10631
|
const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
|
|
9947
10632
|
const existingInDest = await loadDir(destDir);
|
|
9948
10633
|
const existingIds = new Set(existingInDest.map(({ memory: memory2 }) => memory2.frontmatter.id));
|
|
9949
10634
|
for (const file of sourceFiles) {
|
|
9950
|
-
const srcPath =
|
|
9951
|
-
const destPath =
|
|
9952
|
-
const fileContent = await
|
|
10635
|
+
const srcPath = path38.join(sourceDir, file);
|
|
10636
|
+
const destPath = path38.join(destDir, file);
|
|
10637
|
+
const fileContent = await readFile16(srcPath, "utf8");
|
|
9953
10638
|
const alreadyTagged = fileContent.includes(`cross-repo:${sourceName}`);
|
|
9954
10639
|
if (!alreadyTagged) {
|
|
9955
10640
|
await copyFile(srcPath, destPath);
|
|
@@ -9972,27 +10657,27 @@ Next steps:
|
|
|
9972
10657
|
);
|
|
9973
10658
|
});
|
|
9974
10659
|
hub.command("status").description("Show hub sync status.").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9975
|
-
const root =
|
|
9976
|
-
const paths =
|
|
9977
|
-
const config = await
|
|
10660
|
+
const root = findProjectRoot35(opts.dir);
|
|
10661
|
+
const paths = resolveHaivePaths32(root);
|
|
10662
|
+
const config = await loadConfig8(paths);
|
|
9978
10663
|
console.log(ui.bold("Hub status"));
|
|
9979
10664
|
console.log(
|
|
9980
10665
|
` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
|
|
9981
10666
|
);
|
|
9982
|
-
const sharedDir =
|
|
9983
|
-
if (
|
|
10667
|
+
const sharedDir = path38.join(paths.memoriesDir, "shared");
|
|
10668
|
+
if (existsSync57(sharedDir)) {
|
|
9984
10669
|
const { readdir: readdir6 } = await import("fs/promises");
|
|
9985
10670
|
const sources = (await readdir6(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
9986
10671
|
console.log(`
|
|
9987
10672
|
Imported from ${sources.length} source(s):`);
|
|
9988
10673
|
for (const src of sources) {
|
|
9989
|
-
const files = (await readdir6(
|
|
10674
|
+
const files = (await readdir6(path38.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
|
|
9990
10675
|
console.log(` ${src}: ${files.length} memor${files.length === 1 ? "y" : "ies"}`);
|
|
9991
10676
|
}
|
|
9992
10677
|
} else {
|
|
9993
10678
|
console.log(ui.dim(" No imported shared memories yet."));
|
|
9994
10679
|
}
|
|
9995
|
-
const all = await
|
|
10680
|
+
const all = await loadMemoriesFromDir29(paths.memoriesDir);
|
|
9996
10681
|
const outgoing = all.filter(
|
|
9997
10682
|
({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && !memory2.frontmatter.tags.some((t) => t.startsWith("cross-repo:"))
|
|
9998
10683
|
);
|
|
@@ -10001,25 +10686,25 @@ Next steps:
|
|
|
10001
10686
|
if (outgoing.length > 0) {
|
|
10002
10687
|
console.log(ui.dim(" Run `haive hub push` to publish them to the hub."));
|
|
10003
10688
|
}
|
|
10004
|
-
void
|
|
10005
|
-
void
|
|
10006
|
-
void
|
|
10689
|
+
void readFile16;
|
|
10690
|
+
void writeFile28;
|
|
10691
|
+
void saveConfig3;
|
|
10007
10692
|
});
|
|
10008
10693
|
}
|
|
10009
10694
|
|
|
10010
10695
|
// src/commands/stats.ts
|
|
10011
10696
|
import "commander";
|
|
10012
|
-
import { existsSync as
|
|
10013
|
-
import { mkdir as mkdir17, writeFile as
|
|
10014
|
-
import
|
|
10697
|
+
import { existsSync as existsSync58 } from "fs";
|
|
10698
|
+
import { mkdir as mkdir17, writeFile as writeFile29 } from "fs/promises";
|
|
10699
|
+
import path39 from "path";
|
|
10015
10700
|
import {
|
|
10016
10701
|
aggregateUsage,
|
|
10017
|
-
findProjectRoot as
|
|
10018
|
-
loadMemoriesFromDir as
|
|
10019
|
-
loadUsageIndex as
|
|
10702
|
+
findProjectRoot as findProjectRoot36,
|
|
10703
|
+
loadMemoriesFromDir as loadMemoriesFromDir30,
|
|
10704
|
+
loadUsageIndex as loadUsageIndex24,
|
|
10020
10705
|
parseSince,
|
|
10021
10706
|
readUsageEvents as readUsageEvents2,
|
|
10022
|
-
resolveHaivePaths as
|
|
10707
|
+
resolveHaivePaths as resolveHaivePaths33,
|
|
10023
10708
|
usageLogSize
|
|
10024
10709
|
} from "@hiveai/core";
|
|
10025
10710
|
function registerStats(program2) {
|
|
@@ -10028,8 +10713,8 @@ function registerStats(program2) {
|
|
|
10028
10713
|
"write a JSON rollup (tools + briefing counts + heuristic ROI hints). Parent dirs are created if needed.",
|
|
10029
10714
|
void 0
|
|
10030
10715
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10031
|
-
const root =
|
|
10032
|
-
const paths =
|
|
10716
|
+
const root = findProjectRoot36(opts.dir);
|
|
10717
|
+
const paths = resolveHaivePaths33(root);
|
|
10033
10718
|
if (opts.exportReport) {
|
|
10034
10719
|
await writeRoiReport(paths, root, opts.since ?? "30d", opts.exportReport);
|
|
10035
10720
|
return;
|
|
@@ -10083,12 +10768,12 @@ function registerStats(program2) {
|
|
|
10083
10768
|
});
|
|
10084
10769
|
}
|
|
10085
10770
|
async function writeRoiReport(paths, root, sinceRaw, outRelative) {
|
|
10086
|
-
const outAbs =
|
|
10771
|
+
const outAbs = path39.isAbsolute(outRelative) ? path39.resolve(outRelative) : path39.resolve(root, outRelative);
|
|
10087
10772
|
const size = await usageLogSize(paths);
|
|
10088
10773
|
let events = await readUsageEvents2(paths);
|
|
10089
10774
|
let memoryCount = { team: 0, personal: 0, total_skipped_session: 0 };
|
|
10090
|
-
if (
|
|
10091
|
-
const mems = await
|
|
10775
|
+
if (existsSync58(paths.memoriesDir)) {
|
|
10776
|
+
const mems = await loadMemoriesFromDir30(paths.memoriesDir);
|
|
10092
10777
|
for (const { memory: memory2 } of mems) {
|
|
10093
10778
|
const fm = memory2.frontmatter;
|
|
10094
10779
|
if (fm.type === "session_recap") memoryCount.total_skipped_session++;
|
|
@@ -10102,7 +10787,7 @@ async function writeRoiReport(paths, root, sinceRaw, outRelative) {
|
|
|
10102
10787
|
const briefingCalls = events.filter((e) => inWindow(e.at) && e.tool === "get_briefing").length;
|
|
10103
10788
|
let memoryHitsLeader = null;
|
|
10104
10789
|
try {
|
|
10105
|
-
const usageIdx = await
|
|
10790
|
+
const usageIdx = await loadUsageIndex24(paths);
|
|
10106
10791
|
const tops = Object.entries(usageIdx.by_id).map(([id, v]) => ({ id, read_count: v.read_count })).filter((x) => x.read_count > 0).sort((a, b) => b.read_count - a.read_count);
|
|
10107
10792
|
memoryHitsLeader = tops[0] ?? null;
|
|
10108
10793
|
} catch {
|
|
@@ -10118,7 +10803,7 @@ async function writeRoiReport(paths, root, sinceRaw, outRelative) {
|
|
|
10118
10803
|
ui.warn("Usage log missing or empty \u2014 report still written with partial data.");
|
|
10119
10804
|
events = [];
|
|
10120
10805
|
}
|
|
10121
|
-
await mkdir17(
|
|
10806
|
+
await mkdir17(path39.dirname(outAbs), { recursive: true });
|
|
10122
10807
|
const payload = {
|
|
10123
10808
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10124
10809
|
project_root: root,
|
|
@@ -10130,11 +10815,11 @@ async function writeRoiReport(paths, root, sinceRaw, outRelative) {
|
|
|
10130
10815
|
top_memory_reads: memoryHitsLeader,
|
|
10131
10816
|
roi_hints: roiHints
|
|
10132
10817
|
};
|
|
10133
|
-
await
|
|
10818
|
+
await writeFile29(outAbs, JSON.stringify(payload, null, 2), "utf8");
|
|
10134
10819
|
ui.success(`Wrote ROI / usage rollup \u2192 ${outAbs}`);
|
|
10135
10820
|
}
|
|
10136
10821
|
async function renderMemoryHits(paths, opts) {
|
|
10137
|
-
const index = await
|
|
10822
|
+
const index = await loadUsageIndex24(paths);
|
|
10138
10823
|
const since = parseSince(opts.since ?? "30d");
|
|
10139
10824
|
const sinceMs = since ? new Date(since).getTime() : null;
|
|
10140
10825
|
const entries = Object.entries(index.by_id).map(([id, usage]) => ({ id, ...usage })).filter((e) => e.read_count > 0).filter((e) => {
|
|
@@ -10182,13 +10867,13 @@ import { performance } from "perf_hooks";
|
|
|
10182
10867
|
import "commander";
|
|
10183
10868
|
import {
|
|
10184
10869
|
estimateTokens as estimateTokens3,
|
|
10185
|
-
findProjectRoot as
|
|
10186
|
-
resolveHaivePaths as
|
|
10870
|
+
findProjectRoot as findProjectRoot37,
|
|
10871
|
+
resolveHaivePaths as resolveHaivePaths34
|
|
10187
10872
|
} from "@hiveai/core";
|
|
10188
10873
|
function registerBench(program2) {
|
|
10189
10874
|
program2.command("bench").description("Self-test the local hAIve setup: runs core MCP tools against this project and reports latency + payload size.").option("-t, --task <task>", "task description for ranking-aware tools", "audit dependencies for security risks").option("--json", "emit JSON instead of a table", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10190
|
-
const root =
|
|
10191
|
-
const paths =
|
|
10875
|
+
const root = findProjectRoot37(opts.dir);
|
|
10876
|
+
const paths = resolveHaivePaths34(root);
|
|
10192
10877
|
const ctx = { paths };
|
|
10193
10878
|
const task = opts.task ?? "audit dependencies for security risks";
|
|
10194
10879
|
const scenarios = [
|
|
@@ -10307,11 +10992,11 @@ function summarize(name, t0, payload, notes) {
|
|
|
10307
10992
|
}
|
|
10308
10993
|
|
|
10309
10994
|
// src/commands/benchmark.ts
|
|
10310
|
-
import { existsSync as
|
|
10311
|
-
import { readdir as readdir5, readFile as
|
|
10312
|
-
import
|
|
10995
|
+
import { existsSync as existsSync59 } from "fs";
|
|
10996
|
+
import { readdir as readdir5, readFile as readFile17, writeFile as writeFile30 } from "fs/promises";
|
|
10997
|
+
import path40 from "path";
|
|
10313
10998
|
import "commander";
|
|
10314
|
-
import { estimateTokens as estimateTokens4, findProjectRoot as
|
|
10999
|
+
import { estimateTokens as estimateTokens4, findProjectRoot as findProjectRoot38 } from "@hiveai/core";
|
|
10315
11000
|
function registerBenchmark(program2) {
|
|
10316
11001
|
const benchmark = program2.command("benchmark").description("Official hAIve benchmark/demo utilities for measuring agent enforcement value.");
|
|
10317
11002
|
benchmark.command("report").description("Summarize BENCHMARK_AGENT_REPORT.md files from a paired hAIve/plain agent benchmark.").option("-d, --dir <dir>", "benchmark root", "benchmarks/agent-benchmark").option("--out <file>", "write a Markdown report").option("--json", "emit JSON", false).action(async (opts) => {
|
|
@@ -10324,9 +11009,9 @@ function registerBenchmark(program2) {
|
|
|
10324
11009
|
}
|
|
10325
11010
|
const markdown = renderMarkdown(root, summary, rows);
|
|
10326
11011
|
if (opts.out) {
|
|
10327
|
-
const outFile =
|
|
10328
|
-
await
|
|
10329
|
-
ui.success(`wrote ${
|
|
11012
|
+
const outFile = path40.isAbsolute(opts.out) ? opts.out : path40.join(root, opts.out);
|
|
11013
|
+
await writeFile30(outFile, markdown, "utf8");
|
|
11014
|
+
ui.success(`wrote ${path40.relative(process.cwd(), outFile)}`);
|
|
10330
11015
|
return;
|
|
10331
11016
|
}
|
|
10332
11017
|
console.log(markdown);
|
|
@@ -10350,20 +11035,20 @@ function registerBenchmark(program2) {
|
|
|
10350
11035
|
}
|
|
10351
11036
|
function resolveBenchmarkRoot(dir) {
|
|
10352
11037
|
const candidate = dir ?? "benchmarks/agent-benchmark";
|
|
10353
|
-
if (
|
|
10354
|
-
const projectRoot =
|
|
10355
|
-
return
|
|
11038
|
+
if (path40.isAbsolute(candidate)) return candidate;
|
|
11039
|
+
const projectRoot = findProjectRoot38(process.cwd());
|
|
11040
|
+
return path40.join(projectRoot, candidate);
|
|
10356
11041
|
}
|
|
10357
11042
|
async function collectRows(root) {
|
|
10358
|
-
if (!
|
|
11043
|
+
if (!existsSync59(root)) throw new Error(`Benchmark directory not found: ${root}`);
|
|
10359
11044
|
const entries = await readdir5(root, { withFileTypes: true });
|
|
10360
11045
|
const rows = [];
|
|
10361
11046
|
for (const entry of entries) {
|
|
10362
11047
|
if (!entry.isDirectory()) continue;
|
|
10363
|
-
const fixtureDir =
|
|
10364
|
-
const reportFile =
|
|
10365
|
-
if (!
|
|
10366
|
-
const report = await
|
|
11048
|
+
const fixtureDir = path40.join(root, entry.name);
|
|
11049
|
+
const reportFile = path40.join(fixtureDir, "BENCHMARK_AGENT_REPORT.md");
|
|
11050
|
+
if (!existsSync59(reportFile)) continue;
|
|
11051
|
+
const report = await readFile17(reportFile, "utf8");
|
|
10367
11052
|
rows.push(parseAgentReport(entry.name, report));
|
|
10368
11053
|
}
|
|
10369
11054
|
return rows.sort((a, b) => a.fixture.localeCompare(b.fixture));
|
|
@@ -10452,20 +11137,20 @@ function escapeRegExp(value) {
|
|
|
10452
11137
|
}
|
|
10453
11138
|
|
|
10454
11139
|
// src/commands/memory-suggest.ts
|
|
10455
|
-
import { mkdir as mkdir18, writeFile as
|
|
10456
|
-
import { existsSync as
|
|
10457
|
-
import
|
|
11140
|
+
import { mkdir as mkdir18, writeFile as writeFile31 } from "fs/promises";
|
|
11141
|
+
import { existsSync as existsSync60 } from "fs";
|
|
11142
|
+
import path41 from "path";
|
|
10458
11143
|
import "commander";
|
|
10459
11144
|
import {
|
|
10460
11145
|
aggregateUsage as aggregateUsage2,
|
|
10461
11146
|
buildFrontmatter as buildFrontmatter11,
|
|
10462
|
-
findProjectRoot as
|
|
10463
|
-
loadMemoriesFromDir as
|
|
11147
|
+
findProjectRoot as findProjectRoot39,
|
|
11148
|
+
loadMemoriesFromDir as loadMemoriesFromDir31,
|
|
10464
11149
|
memoryFilePath as memoryFilePath10,
|
|
10465
11150
|
parseSince as parseSince2,
|
|
10466
11151
|
readUsageEvents as readUsageEvents3,
|
|
10467
|
-
resolveHaivePaths as
|
|
10468
|
-
serializeMemory as
|
|
11152
|
+
resolveHaivePaths as resolveHaivePaths35,
|
|
11153
|
+
serializeMemory as serializeMemory25
|
|
10469
11154
|
} from "@hiveai/core";
|
|
10470
11155
|
var SEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
10471
11156
|
"mem_search",
|
|
@@ -10477,8 +11162,8 @@ function registerMemorySuggest(memory2) {
|
|
|
10477
11162
|
memory2.command("suggest").description(
|
|
10478
11163
|
"Suggest memories to create based on recurring search queries in the usage log.\n\n Use --auto-save to draft the top-N suggestions as draft memories. They land\n in personal scope by default with status=draft, ready for you to edit and promote."
|
|
10479
11164
|
).option("--since <window>", "ISO date or relative (e.g. '7d', '24h')", "30d").option("--min <count>", "minimum repeat count to surface a query", "2").option("--top-n <n>", "with --auto-save, draft this many top suggestions", "3").option("--scope <scope>", "with --auto-save, scope of drafted memories (personal | team)", "personal").option("--auto-save", "draft top-N suggestions as draft memories on disk", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10480
|
-
const root =
|
|
10481
|
-
const paths =
|
|
11165
|
+
const root = findProjectRoot39(opts.dir);
|
|
11166
|
+
const paths = resolveHaivePaths35(root);
|
|
10482
11167
|
const events = await readUsageEvents3(paths);
|
|
10483
11168
|
if (events.length === 0) {
|
|
10484
11169
|
if (opts.json) {
|
|
@@ -10524,7 +11209,7 @@ function registerMemorySuggest(memory2) {
|
|
|
10524
11209
|
}
|
|
10525
11210
|
const created = [];
|
|
10526
11211
|
const skipped = [];
|
|
10527
|
-
const existing =
|
|
11212
|
+
const existing = existsSync60(paths.memoriesDir) ? await loadMemoriesFromDir31(paths.memoriesDir) : [];
|
|
10528
11213
|
for (const s of top) {
|
|
10529
11214
|
const slug = slugify(s.query);
|
|
10530
11215
|
if (!slug) {
|
|
@@ -10547,13 +11232,13 @@ function registerMemorySuggest(memory2) {
|
|
|
10547
11232
|
fm.status = "draft";
|
|
10548
11233
|
const body = renderTemplate(s);
|
|
10549
11234
|
const file = memoryFilePath10(paths, fm.scope, fm.id, fm.module);
|
|
10550
|
-
await mkdir18(
|
|
10551
|
-
if (
|
|
10552
|
-
skipped.push({ query: s.query, reason: `file already exists at ${
|
|
11235
|
+
await mkdir18(path41.dirname(file), { recursive: true });
|
|
11236
|
+
if (existsSync60(file)) {
|
|
11237
|
+
skipped.push({ query: s.query, reason: `file already exists at ${path41.relative(root, file)}` });
|
|
10553
11238
|
continue;
|
|
10554
11239
|
}
|
|
10555
|
-
await
|
|
10556
|
-
created.push({ id: fm.id, file:
|
|
11240
|
+
await writeFile31(file, serializeMemory25({ frontmatter: fm, body }), "utf8");
|
|
11241
|
+
created.push({ id: fm.id, file: path41.relative(root, file), query: s.query });
|
|
10557
11242
|
}
|
|
10558
11243
|
if (opts.json) {
|
|
10559
11244
|
console.log(JSON.stringify({ created, skipped }, null, 2));
|
|
@@ -10646,26 +11331,26 @@ function truncate2(text, max) {
|
|
|
10646
11331
|
}
|
|
10647
11332
|
|
|
10648
11333
|
// src/commands/memory-archive.ts
|
|
10649
|
-
import { existsSync as
|
|
10650
|
-
import { writeFile as
|
|
10651
|
-
import
|
|
11334
|
+
import { existsSync as existsSync61 } from "fs";
|
|
11335
|
+
import { writeFile as writeFile33 } from "fs/promises";
|
|
11336
|
+
import path43 from "path";
|
|
10652
11337
|
import "commander";
|
|
10653
11338
|
import {
|
|
10654
|
-
findProjectRoot as
|
|
10655
|
-
getUsage as
|
|
10656
|
-
loadMemoriesFromDir as
|
|
10657
|
-
loadUsageIndex as
|
|
10658
|
-
resolveHaivePaths as
|
|
10659
|
-
serializeMemory as
|
|
11339
|
+
findProjectRoot as findProjectRoot40,
|
|
11340
|
+
getUsage as getUsage19,
|
|
11341
|
+
loadMemoriesFromDir as loadMemoriesFromDir32,
|
|
11342
|
+
loadUsageIndex as loadUsageIndex25,
|
|
11343
|
+
resolveHaivePaths as resolveHaivePaths36,
|
|
11344
|
+
serializeMemory as serializeMemory26
|
|
10660
11345
|
} from "@hiveai/core";
|
|
10661
11346
|
var MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
|
|
10662
11347
|
function registerMemoryArchive(memory2) {
|
|
10663
11348
|
memory2.command("archive").description(
|
|
10664
11349
|
"Archive obsolete memories: marks status='deprecated' for memories not read in N days\n whose anchored paths have all disappeared (or have no anchor at all).\n\n Defaults to a DRY RUN \u2014 pass --apply to actually rewrite files.\n Targets `attempt` memories by default since they age the fastest.\n\n Recover later with `haive memory edit <id>` to set status back to validated."
|
|
10665
11350
|
).option("--since <window>", "minimum age since last read (e.g. '180d', '6m')", "180d").option("--type <type>", "limit to a memory type (default 'attempt'). Pass 'all' to scan all types.", "attempt").option("--apply", "actually rewrite files (default: dry run)", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10666
|
-
const root =
|
|
10667
|
-
const paths =
|
|
10668
|
-
if (!
|
|
11351
|
+
const root = findProjectRoot40(opts.dir);
|
|
11352
|
+
const paths = resolveHaivePaths36(root);
|
|
11353
|
+
if (!existsSync61(paths.memoriesDir)) {
|
|
10669
11354
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
10670
11355
|
process.exitCode = 1;
|
|
10671
11356
|
return;
|
|
@@ -10677,8 +11362,8 @@ function registerMemoryArchive(memory2) {
|
|
|
10677
11362
|
return;
|
|
10678
11363
|
}
|
|
10679
11364
|
const cutoff = Date.now() - minDays * MS_PER_DAY2;
|
|
10680
|
-
const all = await
|
|
10681
|
-
const usage = await
|
|
11365
|
+
const all = await loadMemoriesFromDir32(paths.memoriesDir);
|
|
11366
|
+
const usage = await loadUsageIndex25(paths);
|
|
10682
11367
|
const typeFilter = opts.type === "all" ? null : opts.type ?? "attempt";
|
|
10683
11368
|
const candidates = [];
|
|
10684
11369
|
for (const { memory: mem, filePath } of all) {
|
|
@@ -10686,10 +11371,10 @@ function registerMemoryArchive(memory2) {
|
|
|
10686
11371
|
if (typeFilter && fm.type !== typeFilter) continue;
|
|
10687
11372
|
if (fm.status === "deprecated" || fm.status === "rejected") continue;
|
|
10688
11373
|
const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
|
|
10689
|
-
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !
|
|
11374
|
+
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync61(path43.join(paths.root, p)));
|
|
10690
11375
|
const isAnchorless = !hasAnyAnchor;
|
|
10691
11376
|
if (!isAnchorless && !allPathsGone) continue;
|
|
10692
|
-
const u =
|
|
11377
|
+
const u = getUsage19(usage, fm.id);
|
|
10693
11378
|
const lastSeen = u.last_read_at ?? fm.created_at;
|
|
10694
11379
|
if (Date.parse(lastSeen) >= cutoff) continue;
|
|
10695
11380
|
candidates.push({
|
|
@@ -10734,7 +11419,7 @@ function registerMemoryArchive(memory2) {
|
|
|
10734
11419
|
if (!found) continue;
|
|
10735
11420
|
const fm = { ...found.memory.frontmatter, status: "deprecated" };
|
|
10736
11421
|
try {
|
|
10737
|
-
await
|
|
11422
|
+
await writeFile33(c.filePath, serializeMemory26({ frontmatter: fm, body: found.memory.body }), "utf8");
|
|
10738
11423
|
archived++;
|
|
10739
11424
|
} catch (err) {
|
|
10740
11425
|
if (!opts.json) {
|
|
@@ -10760,31 +11445,32 @@ function parseDays(input) {
|
|
|
10760
11445
|
}
|
|
10761
11446
|
|
|
10762
11447
|
// src/commands/doctor.ts
|
|
10763
|
-
import { existsSync as
|
|
10764
|
-
import { readFile as
|
|
10765
|
-
import
|
|
11448
|
+
import { existsSync as existsSync63 } from "fs";
|
|
11449
|
+
import { readFile as readFile18, stat } from "fs/promises";
|
|
11450
|
+
import path44 from "path";
|
|
10766
11451
|
import { execFileSync, execSync as execSync3 } from "child_process";
|
|
10767
11452
|
import "commander";
|
|
10768
11453
|
import {
|
|
10769
11454
|
codeMapPath as codeMapPath2,
|
|
10770
|
-
findProjectRoot as
|
|
10771
|
-
getUsage as
|
|
10772
|
-
loadCodeMap as
|
|
10773
|
-
loadConfig as
|
|
10774
|
-
loadMemoriesFromDir as
|
|
10775
|
-
loadUsageIndex as
|
|
11455
|
+
findProjectRoot as findProjectRoot41,
|
|
11456
|
+
getUsage as getUsage20,
|
|
11457
|
+
loadCodeMap as loadCodeMap7,
|
|
11458
|
+
loadConfig as loadConfig9,
|
|
11459
|
+
loadMemoriesFromDir as loadMemoriesFromDir33,
|
|
11460
|
+
loadUsageIndex as loadUsageIndex26,
|
|
10776
11461
|
readUsageEvents as readUsageEvents4,
|
|
10777
|
-
resolveHaivePaths as
|
|
11462
|
+
resolveHaivePaths as resolveHaivePaths37
|
|
10778
11463
|
} from "@hiveai/core";
|
|
10779
11464
|
var MS_PER_DAY3 = 24 * 60 * 60 * 1e3;
|
|
10780
11465
|
function registerDoctor(program2) {
|
|
10781
11466
|
program2.command("doctor").description(
|
|
10782
11467
|
"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) => {
|
|
10784
|
-
const root =
|
|
10785
|
-
const paths =
|
|
11468
|
+
).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) => {
|
|
11469
|
+
const root = findProjectRoot41(opts.dir);
|
|
11470
|
+
const paths = resolveHaivePaths37(root);
|
|
10786
11471
|
const findings = [];
|
|
10787
|
-
|
|
11472
|
+
const repairs = [];
|
|
11473
|
+
if (!existsSync63(paths.haiveDir)) {
|
|
10788
11474
|
findings.push({
|
|
10789
11475
|
severity: "error",
|
|
10790
11476
|
code: "not-initialized",
|
|
@@ -10793,7 +11479,18 @@ function registerDoctor(program2) {
|
|
|
10793
11479
|
});
|
|
10794
11480
|
return emit(findings, opts);
|
|
10795
11481
|
}
|
|
10796
|
-
if (!
|
|
11482
|
+
if (opts.fix && !opts.dryRun) {
|
|
11483
|
+
repairs.push(
|
|
11484
|
+
...await applyAutopilotRepairs(root, paths, {
|
|
11485
|
+
applyConfig: true,
|
|
11486
|
+
applyContext: true,
|
|
11487
|
+
applyCorpus: true,
|
|
11488
|
+
applyCodeMap: true,
|
|
11489
|
+
applyCodeSearch: true
|
|
11490
|
+
})
|
|
11491
|
+
);
|
|
11492
|
+
}
|
|
11493
|
+
if (!existsSync63(paths.projectContext)) {
|
|
10797
11494
|
findings.push({
|
|
10798
11495
|
severity: "warn",
|
|
10799
11496
|
code: "no-project-context",
|
|
@@ -10801,8 +11498,8 @@ function registerDoctor(program2) {
|
|
|
10801
11498
|
fix: "haive init"
|
|
10802
11499
|
});
|
|
10803
11500
|
} else {
|
|
10804
|
-
const { readFile:
|
|
10805
|
-
const content = await
|
|
11501
|
+
const { readFile: readFile20 } = await import("fs/promises");
|
|
11502
|
+
const content = await readFile20(paths.projectContext, "utf8");
|
|
10806
11503
|
const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
|
|
10807
11504
|
if (isTemplate) {
|
|
10808
11505
|
findings.push({
|
|
@@ -10812,8 +11509,17 @@ function registerDoctor(program2) {
|
|
|
10812
11509
|
fix: "Invoke the bootstrap_project MCP prompt from your AI client."
|
|
10813
11510
|
});
|
|
10814
11511
|
}
|
|
11512
|
+
const versionStatus = await projectContextVersionStatus(root, paths);
|
|
11513
|
+
if (versionStatus.mismatch) {
|
|
11514
|
+
findings.push({
|
|
11515
|
+
severity: "warn",
|
|
11516
|
+
code: "project-context-version-mismatch",
|
|
11517
|
+
message: `.ai/project-context.md version metadata (${versionStatus.currentVersion ?? "missing"}) does not match package.json (${versionStatus.expectedVersion}).`,
|
|
11518
|
+
fix: "haive doctor --fix"
|
|
11519
|
+
});
|
|
11520
|
+
}
|
|
10815
11521
|
}
|
|
10816
|
-
const memories =
|
|
11522
|
+
const memories = existsSync63(paths.memoriesDir) ? await loadMemoriesFromDir33(paths.memoriesDir) : [];
|
|
10817
11523
|
const now = Date.now();
|
|
10818
11524
|
if (memories.length === 0) {
|
|
10819
11525
|
findings.push({
|
|
@@ -10822,7 +11528,7 @@ function registerDoctor(program2) {
|
|
|
10822
11528
|
message: "No memories yet. Capture knowledge as agents work via mem_save / mem_observe / mem_tried."
|
|
10823
11529
|
});
|
|
10824
11530
|
} else {
|
|
10825
|
-
const usage = await
|
|
11531
|
+
const usage = await loadUsageIndex26(paths);
|
|
10826
11532
|
const stale = memories.filter((m) => m.memory.frontmatter.status === "stale");
|
|
10827
11533
|
if (stale.length > 0) {
|
|
10828
11534
|
findings.push({
|
|
@@ -10854,7 +11560,7 @@ function registerDoctor(program2) {
|
|
|
10854
11560
|
}
|
|
10855
11561
|
const decayCandidates = memories.filter((m) => {
|
|
10856
11562
|
if (m.memory.frontmatter.status !== "validated") return false;
|
|
10857
|
-
const u =
|
|
11563
|
+
const u = getUsage20(usage, m.memory.frontmatter.id);
|
|
10858
11564
|
const last = u.last_read_at ?? m.memory.frontmatter.created_at;
|
|
10859
11565
|
return (now - Date.parse(last)) / MS_PER_DAY3 > 180;
|
|
10860
11566
|
});
|
|
@@ -10867,7 +11573,7 @@ function registerDoctor(program2) {
|
|
|
10867
11573
|
});
|
|
10868
11574
|
}
|
|
10869
11575
|
}
|
|
10870
|
-
const codeMap = await
|
|
11576
|
+
const codeMap = await loadCodeMap7(paths);
|
|
10871
11577
|
if (!codeMap) {
|
|
10872
11578
|
findings.push({
|
|
10873
11579
|
severity: "warn",
|
|
@@ -10922,14 +11628,14 @@ function registerDoctor(program2) {
|
|
|
10922
11628
|
});
|
|
10923
11629
|
}
|
|
10924
11630
|
}
|
|
10925
|
-
const config = await
|
|
11631
|
+
const config = await loadConfig9(paths);
|
|
10926
11632
|
if (config.enforcement?.requireBriefingFirst) {
|
|
10927
|
-
const claudeSettings =
|
|
11633
|
+
const claudeSettings = path44.join(root, ".claude", "settings.local.json");
|
|
10928
11634
|
let hasClaudeEnforcement = false;
|
|
10929
|
-
if (
|
|
11635
|
+
if (existsSync63(claudeSettings)) {
|
|
10930
11636
|
try {
|
|
10931
|
-
const { readFile:
|
|
10932
|
-
const raw = await
|
|
11637
|
+
const { readFile: readFile20 } = await import("fs/promises");
|
|
11638
|
+
const raw = await readFile20(claudeSettings, "utf8");
|
|
10933
11639
|
hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
|
|
10934
11640
|
} catch {
|
|
10935
11641
|
hasClaudeEnforcement = false;
|
|
@@ -10952,14 +11658,14 @@ function registerDoctor(program2) {
|
|
|
10952
11658
|
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
|
|
10953
11659
|
});
|
|
10954
11660
|
}
|
|
10955
|
-
findings.push(...await collectInstallFindings(root, "0.9.
|
|
11661
|
+
findings.push(...await collectInstallFindings(root, "0.9.18"));
|
|
10956
11662
|
try {
|
|
10957
11663
|
const legacyRaw = execSync3("haive-mcp --version", {
|
|
10958
11664
|
encoding: "utf8",
|
|
10959
11665
|
timeout: 3e3,
|
|
10960
11666
|
stdio: ["ignore", "pipe", "ignore"]
|
|
10961
11667
|
}).trim();
|
|
10962
|
-
const cliVersion = "0.9.
|
|
11668
|
+
const cliVersion = "0.9.18";
|
|
10963
11669
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
10964
11670
|
findings.push({
|
|
10965
11671
|
severity: "warn",
|
|
@@ -10972,37 +11678,123 @@ npm uninstall -g @hiveai/mcp`
|
|
|
10972
11678
|
}
|
|
10973
11679
|
} catch {
|
|
10974
11680
|
}
|
|
10975
|
-
|
|
11681
|
+
if (repairs.length > 0) {
|
|
11682
|
+
findings.push({
|
|
11683
|
+
severity: "info",
|
|
11684
|
+
code: "autopilot-repairs-applied",
|
|
11685
|
+
message: repairs.map((repair) => repair.message).join(" "),
|
|
11686
|
+
section: "Next actions"
|
|
11687
|
+
});
|
|
11688
|
+
}
|
|
11689
|
+
emit(findings, opts, repairs);
|
|
10976
11690
|
});
|
|
10977
11691
|
}
|
|
10978
|
-
function emit(findings, opts) {
|
|
11692
|
+
function emit(findings, opts, repairs = []) {
|
|
11693
|
+
const classified = findings.map((finding) => ({
|
|
11694
|
+
...finding,
|
|
11695
|
+
section: finding.section ?? sectionForFinding(finding)
|
|
11696
|
+
}));
|
|
11697
|
+
const scores = computeDoctorScores(classified);
|
|
10979
11698
|
if (opts.json) {
|
|
10980
|
-
console.log(JSON.stringify({
|
|
11699
|
+
console.log(JSON.stringify({
|
|
11700
|
+
scores,
|
|
11701
|
+
findings: classified,
|
|
11702
|
+
sections: groupBySection(classified),
|
|
11703
|
+
next_actions: nextActions(classified),
|
|
11704
|
+
repairs,
|
|
11705
|
+
fix_mode: opts.fix ? opts.dryRun ? "dry-run" : "apply" : "off"
|
|
11706
|
+
}, null, 2));
|
|
10981
11707
|
return;
|
|
10982
11708
|
}
|
|
10983
|
-
if (
|
|
10984
|
-
ui.success("hAIve doctor \u2014 no issues found.");
|
|
11709
|
+
if (classified.length === 0) {
|
|
11710
|
+
ui.success(repairs.length > 0 ? "hAIve doctor \u2014 autopilot repairs applied." : "hAIve doctor \u2014 no issues found.");
|
|
10985
11711
|
return;
|
|
10986
11712
|
}
|
|
10987
|
-
console.log(ui.bold(`hAIve doctor \u2014 ${
|
|
11713
|
+
console.log(ui.bold(`hAIve doctor \u2014 ${classified.length} finding${classified.length === 1 ? "" : "s"}`));
|
|
11714
|
+
console.log(
|
|
11715
|
+
ui.dim(
|
|
11716
|
+
` protection=${scores.protection_score} context=${scores.context_quality_score} corpus=${scores.corpus_quality_score}`
|
|
11717
|
+
)
|
|
11718
|
+
);
|
|
10988
11719
|
console.log();
|
|
10989
|
-
const
|
|
10990
|
-
|
|
10991
|
-
|
|
10992
|
-
|
|
10993
|
-
|
|
10994
|
-
|
|
10995
|
-
|
|
10996
|
-
|
|
11720
|
+
const sectionOrder = [
|
|
11721
|
+
"Protection",
|
|
11722
|
+
"Agent coverage",
|
|
11723
|
+
"Context quality",
|
|
11724
|
+
"Corpus health",
|
|
11725
|
+
"Index health",
|
|
11726
|
+
"Next actions"
|
|
11727
|
+
];
|
|
11728
|
+
const severityOrder = ["error", "warn", "info"];
|
|
11729
|
+
for (const section2 of sectionOrder) {
|
|
11730
|
+
const sectionFindings = classified.filter((f) => f.section === section2);
|
|
11731
|
+
if (sectionFindings.length === 0) continue;
|
|
11732
|
+
console.log(ui.bold(section2));
|
|
11733
|
+
for (const sev of severityOrder) {
|
|
11734
|
+
for (const f of sectionFindings.filter((x) => x.severity === sev)) {
|
|
11735
|
+
const icon = sev === "error" ? ui.red("\u2717") : sev === "warn" ? ui.yellow("\u26A0") : ui.dim("\u2139");
|
|
11736
|
+
console.log(`${icon} ${ui.bold(f.code)} ${f.message}`);
|
|
11737
|
+
if (opts.fix && f.fix) {
|
|
11738
|
+
for (const line of f.fix.split("\n")) {
|
|
11739
|
+
console.log(` ${ui.dim(opts.dryRun ? "would run:" : "$")} ${line}`);
|
|
11740
|
+
}
|
|
10997
11741
|
}
|
|
10998
11742
|
}
|
|
10999
11743
|
}
|
|
11744
|
+
console.log();
|
|
11000
11745
|
}
|
|
11001
|
-
if (
|
|
11746
|
+
if (repairs.length > 0) {
|
|
11747
|
+
console.log(ui.bold("Autopilot repairs applied"));
|
|
11748
|
+
for (const repair of repairs) console.log(` ${ui.dim("\u2713")} ${repair.message}`);
|
|
11002
11749
|
console.log();
|
|
11750
|
+
}
|
|
11751
|
+
const actions = nextActions(classified);
|
|
11752
|
+
if (actions.length > 0) {
|
|
11753
|
+
console.log(ui.bold("Next actions"));
|
|
11754
|
+
for (const action of actions.slice(0, 5)) console.log(` ${ui.dim("$")} ${action}`);
|
|
11755
|
+
} else if (!opts.fix && classified.some((f) => f.fix)) {
|
|
11003
11756
|
ui.info("Re-run with --fix to see suggested commands.");
|
|
11004
11757
|
}
|
|
11005
11758
|
}
|
|
11759
|
+
function sectionForFinding(finding) {
|
|
11760
|
+
if (finding.code.includes("haive") || finding.code.includes("integration") || finding.code.includes("claude") || finding.code.includes("autopilot")) return "Agent coverage";
|
|
11761
|
+
if (finding.code.includes("context") || finding.code.includes("briefing") || finding.code.includes("search")) return "Context quality";
|
|
11762
|
+
if (finding.code.includes("code-map") || finding.code.includes("index")) return "Index health";
|
|
11763
|
+
if (finding.code.includes("memory") || finding.code.includes("anchor") || finding.code.includes("pending") || finding.code.includes("decay")) return "Corpus health";
|
|
11764
|
+
if (finding.severity === "error") return "Protection";
|
|
11765
|
+
return "Next actions";
|
|
11766
|
+
}
|
|
11767
|
+
function computeDoctorScores(findings) {
|
|
11768
|
+
const scoreFor = (sections) => {
|
|
11769
|
+
const scoped = findings.filter((f) => sections.includes(f.section ?? sectionForFinding(f)));
|
|
11770
|
+
const penalty = scoped.reduce((sum, f) => {
|
|
11771
|
+
if (f.severity === "error") return sum + 35;
|
|
11772
|
+
if (f.severity === "warn") return sum + 15;
|
|
11773
|
+
return sum + 4;
|
|
11774
|
+
}, 0);
|
|
11775
|
+
return Math.max(0, 100 - penalty);
|
|
11776
|
+
};
|
|
11777
|
+
return {
|
|
11778
|
+
protection_score: scoreFor(["Protection", "Agent coverage"]),
|
|
11779
|
+
context_quality_score: scoreFor(["Context quality", "Index health"]),
|
|
11780
|
+
corpus_quality_score: scoreFor(["Corpus health"])
|
|
11781
|
+
};
|
|
11782
|
+
}
|
|
11783
|
+
function groupBySection(findings) {
|
|
11784
|
+
const out = {
|
|
11785
|
+
"Protection": [],
|
|
11786
|
+
"Agent coverage": [],
|
|
11787
|
+
"Context quality": [],
|
|
11788
|
+
"Corpus health": [],
|
|
11789
|
+
"Index health": [],
|
|
11790
|
+
"Next actions": []
|
|
11791
|
+
};
|
|
11792
|
+
for (const finding of findings) out[finding.section ?? sectionForFinding(finding)].push(finding);
|
|
11793
|
+
return out;
|
|
11794
|
+
}
|
|
11795
|
+
function nextActions(findings) {
|
|
11796
|
+
return [...new Set(findings.flatMap((finding) => finding.fix ? finding.fix.split("\n") : []))].filter(Boolean);
|
|
11797
|
+
}
|
|
11006
11798
|
function isSearchTool(name) {
|
|
11007
11799
|
return ["mem_search", "code_search", "mem_relevant_to", "get_briefing"].includes(name);
|
|
11008
11800
|
}
|
|
@@ -11047,9 +11839,9 @@ which -a haive`
|
|
|
11047
11839
|
".vscode/mcp.json"
|
|
11048
11840
|
];
|
|
11049
11841
|
for (const rel of integrationFiles) {
|
|
11050
|
-
const file =
|
|
11051
|
-
if (!
|
|
11052
|
-
const text = await
|
|
11842
|
+
const file = path44.join(root, rel);
|
|
11843
|
+
if (!existsSync63(file)) continue;
|
|
11844
|
+
const text = await readFile18(file, "utf8").catch(() => "");
|
|
11053
11845
|
for (const bin of extractAbsoluteHaiveBins(text)) {
|
|
11054
11846
|
const version = versionForBinary(bin);
|
|
11055
11847
|
if (!version) {
|
|
@@ -11105,22 +11897,22 @@ function extractAbsoluteHaiveBins(text) {
|
|
|
11105
11897
|
}
|
|
11106
11898
|
|
|
11107
11899
|
// src/commands/playback.ts
|
|
11108
|
-
import { existsSync as
|
|
11900
|
+
import { existsSync as existsSync64 } from "fs";
|
|
11109
11901
|
import "commander";
|
|
11110
11902
|
import {
|
|
11111
|
-
findProjectRoot as
|
|
11112
|
-
loadMemoriesFromDir as
|
|
11903
|
+
findProjectRoot as findProjectRoot42,
|
|
11904
|
+
loadMemoriesFromDir as loadMemoriesFromDir34,
|
|
11113
11905
|
parseSince as parseSince3,
|
|
11114
11906
|
readUsageEvents as readUsageEvents5,
|
|
11115
|
-
resolveHaivePaths as
|
|
11907
|
+
resolveHaivePaths as resolveHaivePaths38
|
|
11116
11908
|
} from "@hiveai/core";
|
|
11117
11909
|
var MS_PER_MINUTE = 6e4;
|
|
11118
11910
|
function registerPlayback(program2) {
|
|
11119
11911
|
program2.command("playback").description(
|
|
11120
11912
|
"Replay past sessions from the usage log. For each session, show:\n - tool calls (what kind, how many)\n - briefing tasks asked\n - memories that have been created since then (that the session didn't have)\n\n Useful to ask 'would today's haive have helped past me on this task?'"
|
|
11121
11913
|
).option("--since <window>", "limit to events in this window (e.g. '7d')", "30d").option("--session-gap <minutes>", "minutes of inactivity that splits a session", "30").option("--limit <n>", "show at most this many sessions (newest first)", "10").option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
11122
|
-
const root =
|
|
11123
|
-
const paths =
|
|
11914
|
+
const root = findProjectRoot42(opts.dir);
|
|
11915
|
+
const paths = resolveHaivePaths38(root);
|
|
11124
11916
|
const events = await readUsageEvents5(paths);
|
|
11125
11917
|
if (events.length === 0) {
|
|
11126
11918
|
if (opts.json) {
|
|
@@ -11135,7 +11927,7 @@ function registerPlayback(program2) {
|
|
|
11135
11927
|
const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
|
|
11136
11928
|
const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
|
|
11137
11929
|
const sessions = bucketSessions(filtered, gapMs);
|
|
11138
|
-
const all =
|
|
11930
|
+
const all = existsSync64(paths.memoriesDir) ? await loadMemoriesFromDir34(paths.memoriesDir) : [];
|
|
11139
11931
|
const memByCreatedAt = all.filter(({ memory: memory2 }) => memory2.frontmatter.type !== "session_recap").map(({ memory: memory2 }) => ({ id: memory2.frontmatter.id, at: Date.parse(memory2.frontmatter.created_at) })).sort((a, b) => a.at - b.at);
|
|
11140
11932
|
const enriched = sessions.map((s, i) => {
|
|
11141
11933
|
const startMs = Date.parse(s.start);
|
|
@@ -11225,8 +12017,8 @@ function truncate3(text, max) {
|
|
|
11225
12017
|
import { spawn as spawn4 } from "child_process";
|
|
11226
12018
|
import "commander";
|
|
11227
12019
|
import {
|
|
11228
|
-
findProjectRoot as
|
|
11229
|
-
resolveHaivePaths as
|
|
12020
|
+
findProjectRoot as findProjectRoot43,
|
|
12021
|
+
resolveHaivePaths as resolveHaivePaths39
|
|
11230
12022
|
} from "@hiveai/core";
|
|
11231
12023
|
function registerPrecommit(program2) {
|
|
11232
12024
|
program2.command("precommit").description(
|
|
@@ -11236,8 +12028,8 @@ function registerPrecommit(program2) {
|
|
|
11236
12028
|
"'any' | 'high-confidence' (default) | 'never' (report only)",
|
|
11237
12029
|
"high-confidence"
|
|
11238
12030
|
).option("--no-semantic", "disable semantic search in anti-patterns matching").option("--json", "emit JSON instead of human-readable output", false).option("--paths <paths...>", "explicit paths to check (skips git diff)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
11239
|
-
const root =
|
|
11240
|
-
const paths =
|
|
12031
|
+
const root = findProjectRoot43(opts.dir);
|
|
12032
|
+
const paths = resolveHaivePaths39(root);
|
|
11241
12033
|
const ctx = { paths };
|
|
11242
12034
|
let diff = "";
|
|
11243
12035
|
let touchedPaths = opts.paths ?? [];
|
|
@@ -11321,7 +12113,11 @@ function printWarnings(title, warnings, tone) {
|
|
|
11321
12113
|
console.log(` ${ui.dim(line)}`);
|
|
11322
12114
|
}
|
|
11323
12115
|
console.log(` ${ui.dim("reasons:")} ${w.reasons.join(", ")}`);
|
|
12116
|
+
if (w.affected_files && w.affected_files.length > 0) {
|
|
12117
|
+
console.log(` ${ui.dim("files:")} ${w.affected_files.slice(0, 4).join(", ")}`);
|
|
12118
|
+
}
|
|
11324
12119
|
if (w.rationale) console.log(` ${ui.dim("why shown:")} ${w.rationale}`);
|
|
12120
|
+
if (w.repair_command) console.log(` ${ui.dim("repair:")} ${w.repair_command}`);
|
|
11325
12121
|
}
|
|
11326
12122
|
console.log();
|
|
11327
12123
|
}
|
|
@@ -11345,12 +12141,12 @@ function runCommand3(cmd, args, cwd) {
|
|
|
11345
12141
|
}
|
|
11346
12142
|
|
|
11347
12143
|
// src/commands/welcome.ts
|
|
11348
|
-
import { existsSync as
|
|
12144
|
+
import { existsSync as existsSync65 } from "fs";
|
|
11349
12145
|
import "commander";
|
|
11350
12146
|
import {
|
|
11351
|
-
findProjectRoot as
|
|
11352
|
-
loadMemoriesFromDir as
|
|
11353
|
-
resolveHaivePaths as
|
|
12147
|
+
findProjectRoot as findProjectRoot44,
|
|
12148
|
+
loadMemoriesFromDir as loadMemoriesFromDir35,
|
|
12149
|
+
resolveHaivePaths as resolveHaivePaths40
|
|
11354
12150
|
} from "@hiveai/core";
|
|
11355
12151
|
var TYPE_RANK = {
|
|
11356
12152
|
decision: 0,
|
|
@@ -11364,14 +12160,14 @@ function registerWelcome(program2) {
|
|
|
11364
12160
|
program2.command("welcome").description(
|
|
11365
12161
|
"Onboarding checklist: ranks validated/proposed **team** memories by type.\nUse after `haive init` so new devs skim institutional knowledge quickly.\n\n haive welcome\n haive welcome --limit 15\n"
|
|
11366
12162
|
).option("--limit <n>", "maximum memories listed", "20").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
11367
|
-
const root =
|
|
11368
|
-
const paths =
|
|
11369
|
-
if (!
|
|
12163
|
+
const root = findProjectRoot44(opts.dir);
|
|
12164
|
+
const paths = resolveHaivePaths40(root);
|
|
12165
|
+
if (!existsSync65(paths.memoriesDir)) {
|
|
11370
12166
|
ui.error(`No memories at ${paths.memoriesDir}. Run 'haive init' first.`);
|
|
11371
12167
|
process.exitCode = 1;
|
|
11372
12168
|
return;
|
|
11373
12169
|
}
|
|
11374
|
-
const all = await
|
|
12170
|
+
const all = await loadMemoriesFromDir35(paths.memoriesDir);
|
|
11375
12171
|
const team = all.filter(
|
|
11376
12172
|
({ memory: memory2 }) => memory2.frontmatter.scope === "team" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && memory2.frontmatter.status !== "stale" && memory2.frontmatter.type !== "session_recap"
|
|
11377
12173
|
);
|
|
@@ -11406,173 +12202,6 @@ function registerWelcome(program2) {
|
|
|
11406
12202
|
});
|
|
11407
12203
|
}
|
|
11408
12204
|
|
|
11409
|
-
// src/commands/memory-lint.ts
|
|
11410
|
-
import { existsSync as existsSync64 } from "fs";
|
|
11411
|
-
import "commander";
|
|
11412
|
-
import {
|
|
11413
|
-
findProjectRoot as findProjectRoot44,
|
|
11414
|
-
getUsage as getUsage20,
|
|
11415
|
-
loadMemoriesFromDir as loadMemoriesFromDir35,
|
|
11416
|
-
loadUsageIndex as loadUsageIndex26,
|
|
11417
|
-
resolveHaivePaths as resolveHaivePaths40
|
|
11418
|
-
} from "@hiveai/core";
|
|
11419
|
-
async function lintMemoriesAsync(root) {
|
|
11420
|
-
const paths = resolveHaivePaths40(root);
|
|
11421
|
-
const out = [];
|
|
11422
|
-
if (!existsSync64(paths.memoriesDir)) return out;
|
|
11423
|
-
const loaded = await loadMemoriesFromDir35(paths.memoriesDir);
|
|
11424
|
-
const usage = await loadUsageIndex26(paths);
|
|
11425
|
-
const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
|
|
11426
|
-
const actionableWords = /\b(always|never|prefer|use|avoid|because|instead|why|rationale|do not|must|should)\b/i;
|
|
11427
|
-
for (const { filePath, memory: memory2 } of loaded) {
|
|
11428
|
-
const fm = memory2.frontmatter;
|
|
11429
|
-
if (fm.type === "session_recap") continue;
|
|
11430
|
-
const body = memory2.body.trim();
|
|
11431
|
-
const naked = body.replace(/^#.*$/gm, "").replace(/```[\s\S]*?```/g, "").trim();
|
|
11432
|
-
if (naked.length < 40 && fm.status !== "rejected") {
|
|
11433
|
-
out.push({
|
|
11434
|
-
file: filePath,
|
|
11435
|
-
id: fm.id,
|
|
11436
|
-
severity: "warn",
|
|
11437
|
-
code: "SHORT_BODY",
|
|
11438
|
-
message: "Body looks very short (< ~40 chars of prose after headings). Prefer actionable detail."
|
|
11439
|
-
});
|
|
11440
|
-
}
|
|
11441
|
-
if (["decision", "gotcha", "convention", "architecture", "attempt"].includes(fm.type) && fm.status !== "rejected" && !actionableWords.test(naked)) {
|
|
11442
|
-
out.push({
|
|
11443
|
-
file: filePath,
|
|
11444
|
-
id: fm.id,
|
|
11445
|
-
severity: "info",
|
|
11446
|
-
code: "LOW_ACTIONABILITY",
|
|
11447
|
-
message: "Record does not contain obvious action/rationale words. Add the concrete rule, why it exists, and what to do instead."
|
|
11448
|
-
});
|
|
11449
|
-
}
|
|
11450
|
-
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated") {
|
|
11451
|
-
out.push({
|
|
11452
|
-
file: filePath,
|
|
11453
|
-
id: fm.id,
|
|
11454
|
-
severity: "warn",
|
|
11455
|
-
code: "MISSING_ANCHOR",
|
|
11456
|
-
message: `${fm.type} is validated without anchor paths \u2014 add anchor.paths so haive sync can flag staleness.`
|
|
11457
|
-
});
|
|
11458
|
-
}
|
|
11459
|
-
if (fm.status === "stale" && !fm.stale_reason) {
|
|
11460
|
-
out.push({
|
|
11461
|
-
file: filePath,
|
|
11462
|
-
id: fm.id,
|
|
11463
|
-
severity: "info",
|
|
11464
|
-
code: "STALE_NO_REASON",
|
|
11465
|
-
message: "Status is stale but stale_reason is empty \u2014 document why when possible."
|
|
11466
|
-
});
|
|
11467
|
-
}
|
|
11468
|
-
if (fm.type === "glossary" && naked.length > 6e3) {
|
|
11469
|
-
out.push({
|
|
11470
|
-
file: filePath,
|
|
11471
|
-
id: fm.id,
|
|
11472
|
-
severity: "info",
|
|
11473
|
-
code: "LONG_GLOSSARY",
|
|
11474
|
-
message: "Very long glossary \u2014 consider splitting concepts for tighter briefings."
|
|
11475
|
-
});
|
|
11476
|
-
}
|
|
11477
|
-
const hasMarkdownHeading = /^#{1,3}\s+\S/m.test(memory2.body.trim());
|
|
11478
|
-
if (!hasMarkdownHeading) {
|
|
11479
|
-
out.push({
|
|
11480
|
-
file: filePath,
|
|
11481
|
-
id: fm.id,
|
|
11482
|
-
severity: "warn",
|
|
11483
|
-
code: "NO_MD_HEADING",
|
|
11484
|
-
message: "No Markdown heading (#/##/###) \u2014 add one so humans and auditors can skim the memo quickly."
|
|
11485
|
-
});
|
|
11486
|
-
}
|
|
11487
|
-
const u = getUsage20(usage, fm.id);
|
|
11488
|
-
if (fm.status === "validated" && u.read_count === 0) {
|
|
11489
|
-
out.push({
|
|
11490
|
-
file: filePath,
|
|
11491
|
-
id: fm.id,
|
|
11492
|
-
severity: "info",
|
|
11493
|
-
code: "NEVER_READ",
|
|
11494
|
-
message: "Validated record has never been surfaced/read. Consider improving tags/anchors or archiving it if it is not useful."
|
|
11495
|
-
});
|
|
11496
|
-
}
|
|
11497
|
-
}
|
|
11498
|
-
for (const dup of nearDuplicatePairs(loaded)) {
|
|
11499
|
-
out.push({
|
|
11500
|
-
file: dup.file,
|
|
11501
|
-
id: dup.id,
|
|
11502
|
-
severity: "warn",
|
|
11503
|
-
code: "NEAR_DUPLICATE",
|
|
11504
|
-
message: `Body overlaps ~${Math.round(dup.score * 100)}% with ${dup.otherId}. Merge or deprecate one record to reduce briefing noise.`
|
|
11505
|
-
});
|
|
11506
|
-
}
|
|
11507
|
-
return out;
|
|
11508
|
-
}
|
|
11509
|
-
function nearDuplicatePairs(loaded) {
|
|
11510
|
-
const out = [];
|
|
11511
|
-
const candidates = loaded.filter(({ memory: memory2 }) => {
|
|
11512
|
-
const fm = memory2.frontmatter;
|
|
11513
|
-
return fm.type !== "session_recap" && fm.status !== "rejected" && fm.status !== "deprecated";
|
|
11514
|
-
});
|
|
11515
|
-
for (let i = 0; i < candidates.length; i++) {
|
|
11516
|
-
for (let j = i + 1; j < candidates.length; j++) {
|
|
11517
|
-
const a = candidates[i];
|
|
11518
|
-
const b = candidates[j];
|
|
11519
|
-
if (a.memory.frontmatter.scope !== b.memory.frontmatter.scope) continue;
|
|
11520
|
-
if (a.memory.frontmatter.type !== b.memory.frontmatter.type) continue;
|
|
11521
|
-
const score = jaccard2(tokenSet(a.memory.body), tokenSet(b.memory.body));
|
|
11522
|
-
if (score >= 0.72) {
|
|
11523
|
-
out.push({
|
|
11524
|
-
id: a.memory.frontmatter.id,
|
|
11525
|
-
otherId: b.memory.frontmatter.id,
|
|
11526
|
-
file: a.filePath,
|
|
11527
|
-
score
|
|
11528
|
-
});
|
|
11529
|
-
}
|
|
11530
|
-
}
|
|
11531
|
-
}
|
|
11532
|
-
return out;
|
|
11533
|
-
}
|
|
11534
|
-
function tokenSet(body) {
|
|
11535
|
-
return new Set(
|
|
11536
|
-
(body.toLowerCase().match(/\b[a-z0-9]{4,}\b/g) ?? []).filter((word) => !["this", "that", "with", "from", "have"].includes(word))
|
|
11537
|
-
);
|
|
11538
|
-
}
|
|
11539
|
-
function jaccard2(a, b) {
|
|
11540
|
-
if (a.size === 0 || b.size === 0) return 0;
|
|
11541
|
-
let inter = 0;
|
|
11542
|
-
for (const item of a) if (b.has(item)) inter++;
|
|
11543
|
-
return inter / (a.size + b.size - inter);
|
|
11544
|
-
}
|
|
11545
|
-
function registerMemoryLint(parent) {
|
|
11546
|
-
parent.command("lint").description(
|
|
11547
|
-
"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) => {
|
|
11549
|
-
const root = findProjectRoot44(opts.dir);
|
|
11550
|
-
const findings = await lintMemoriesAsync(root);
|
|
11551
|
-
if (opts.json) {
|
|
11552
|
-
console.log(JSON.stringify({ findings_count: findings.length, findings }, null, 2));
|
|
11553
|
-
process.exitCode = findings.some((f) => f.severity === "error") ? 1 : 0;
|
|
11554
|
-
return;
|
|
11555
|
-
}
|
|
11556
|
-
if (findings.length === 0) {
|
|
11557
|
-
ui.success(`memory lint OK \u2014 ${root}`);
|
|
11558
|
-
return;
|
|
11559
|
-
}
|
|
11560
|
-
console.log(ui.bold(`memory lint (${findings.length} finding${findings.length === 1 ? "" : "s"})`) + `
|
|
11561
|
-
`);
|
|
11562
|
-
const order = { error: 0, warn: 1, info: 2 };
|
|
11563
|
-
findings.sort((a, b) => order[a.severity] - order[b.severity] || a.id.localeCompare(b.id));
|
|
11564
|
-
for (const f of findings) {
|
|
11565
|
-
const color = f.severity === "error" ? ui.red : f.severity === "warn" ? ui.yellow : ui.dim;
|
|
11566
|
-
console.log(
|
|
11567
|
-
`${color(f.severity.padEnd(5))} ${ui.dim(f.code)} ${f.id}`
|
|
11568
|
-
);
|
|
11569
|
-
console.log(` ${f.message}`);
|
|
11570
|
-
console.log(ui.dim(` \u2192 ${f.file}`));
|
|
11571
|
-
}
|
|
11572
|
-
process.exitCode = findings.some((x) => x.severity === "error") ? 1 : 0;
|
|
11573
|
-
});
|
|
11574
|
-
}
|
|
11575
|
-
|
|
11576
12205
|
// src/commands/memory-suggest-topic.ts
|
|
11577
12206
|
import "commander";
|
|
11578
12207
|
import { MemoryTypeSchema as MemoryTypeSchema2, suggestTopicKey as suggestTopicKey2 } from "@hiveai/core";
|
|
@@ -11592,21 +12221,21 @@ function registerMemorySuggestTopic(memory2) {
|
|
|
11592
12221
|
}
|
|
11593
12222
|
|
|
11594
12223
|
// src/commands/resolve-project.ts
|
|
11595
|
-
import
|
|
12224
|
+
import path45 from "path";
|
|
11596
12225
|
import "commander";
|
|
11597
12226
|
import { resolveProjectInfo as resolveProjectInfo2 } from "@hiveai/core";
|
|
11598
12227
|
function registerResolveProject(program2) {
|
|
11599
12228
|
program2.command("resolve-project").description(
|
|
11600
12229
|
"Print JSON for hAIve project root resolution (HAIVE_PROJECT_ROOT, markers, .ai layout)."
|
|
11601
12230
|
).option("-d, --dir <dir>", "working directory", process.cwd()).action((opts) => {
|
|
11602
|
-
const info = resolveProjectInfo2({ cwd:
|
|
12231
|
+
const info = resolveProjectInfo2({ cwd: path45.resolve(opts.dir) });
|
|
11603
12232
|
console.log(JSON.stringify({ ok: true, info }, null, 2));
|
|
11604
12233
|
});
|
|
11605
12234
|
}
|
|
11606
12235
|
|
|
11607
12236
|
// src/commands/runtime-journal.ts
|
|
11608
|
-
import { existsSync as
|
|
11609
|
-
import
|
|
12237
|
+
import { existsSync as existsSync66 } from "fs";
|
|
12238
|
+
import path46 from "path";
|
|
11610
12239
|
import "commander";
|
|
11611
12240
|
import {
|
|
11612
12241
|
appendRuntimeJournalEntry as appendRuntimeJournalEntry3,
|
|
@@ -11620,18 +12249,18 @@ function registerRuntime(program2) {
|
|
|
11620
12249
|
);
|
|
11621
12250
|
const journal = runtime.command("journal").description("Append or read the machine-local session journal (NDJSON)");
|
|
11622
12251
|
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 =
|
|
12252
|
+
const root = path46.resolve(opts.dir ?? process.cwd());
|
|
11624
12253
|
const paths = resolveHaivePaths41(findProjectRoot45(root));
|
|
11625
12254
|
const raw = opts.kind ?? "note";
|
|
11626
12255
|
const kind = ["note", "session_end", "mcp"].includes(raw) ? raw : "note";
|
|
11627
12256
|
await appendRuntimeJournalEntry3(paths, { kind, message });
|
|
11628
|
-
ui.success(`Appended to ${
|
|
12257
|
+
ui.success(`Appended to ${path46.relative(root, paths.runtimeDir)}/session-journal.ndjson`);
|
|
11629
12258
|
});
|
|
11630
12259
|
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 =
|
|
12260
|
+
const root = path46.resolve(opts.dir ?? process.cwd());
|
|
11632
12261
|
const paths = resolveHaivePaths41(findProjectRoot45(root));
|
|
11633
12262
|
const limit = Math.min(500, Math.max(1, parseInt(opts.limit, 10) || 30));
|
|
11634
|
-
if (!
|
|
12263
|
+
if (!existsSync66(paths.haiveDir)) {
|
|
11635
12264
|
ui.error("No .ai/ \u2014 run `haive init` first.");
|
|
11636
12265
|
process.exitCode = 1;
|
|
11637
12266
|
return;
|
|
@@ -11646,8 +12275,8 @@ function registerRuntime(program2) {
|
|
|
11646
12275
|
}
|
|
11647
12276
|
|
|
11648
12277
|
// src/commands/memory-timeline.ts
|
|
11649
|
-
import { existsSync as
|
|
11650
|
-
import
|
|
12278
|
+
import { existsSync as existsSync67 } from "fs";
|
|
12279
|
+
import path47 from "path";
|
|
11651
12280
|
import "commander";
|
|
11652
12281
|
import {
|
|
11653
12282
|
collectTimelineEntries as collectTimelineEntries2,
|
|
@@ -11663,15 +12292,15 @@ function registerMemoryTimeline(memory2) {
|
|
|
11663
12292
|
process.exitCode = 1;
|
|
11664
12293
|
return;
|
|
11665
12294
|
}
|
|
11666
|
-
const root =
|
|
12295
|
+
const root = path47.resolve(opts.dir ?? process.cwd());
|
|
11667
12296
|
const paths = resolveHaivePaths42(findProjectRoot46(root));
|
|
11668
|
-
if (!
|
|
12297
|
+
if (!existsSync67(paths.memoriesDir)) {
|
|
11669
12298
|
ui.error("No memories \u2014 run `haive init`.");
|
|
11670
12299
|
process.exitCode = 1;
|
|
11671
12300
|
return;
|
|
11672
12301
|
}
|
|
11673
12302
|
const limit = Math.min(100, Math.max(1, parseInt(opts.limit, 10) || 30));
|
|
11674
|
-
const all = await
|
|
12303
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
11675
12304
|
const { entries, notice } = collectTimelineEntries2(all, {
|
|
11676
12305
|
memoryId: opts.id,
|
|
11677
12306
|
topic: opts.topic,
|
|
@@ -11683,8 +12312,8 @@ function registerMemoryTimeline(memory2) {
|
|
|
11683
12312
|
}
|
|
11684
12313
|
|
|
11685
12314
|
// src/commands/memory-conflict-candidates.ts
|
|
11686
|
-
import { existsSync as
|
|
11687
|
-
import
|
|
12315
|
+
import { existsSync as existsSync68 } from "fs";
|
|
12316
|
+
import path48 from "path";
|
|
11688
12317
|
import "commander";
|
|
11689
12318
|
import {
|
|
11690
12319
|
findLexicalConflictPairs as findLexicalConflictPairs2,
|
|
@@ -11706,9 +12335,9 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
11706
12335
|
"decision,architecture,convention,gotcha (lexical scan)",
|
|
11707
12336
|
"decision,architecture"
|
|
11708
12337
|
).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 =
|
|
12338
|
+
const root = path48.resolve(opts.dir ?? process.cwd());
|
|
11710
12339
|
const paths = resolveHaivePaths43(findProjectRoot47(root));
|
|
11711
|
-
if (!
|
|
12340
|
+
if (!existsSync68(paths.memoriesDir)) {
|
|
11712
12341
|
ui.error("No memories \u2014 run `haive init`.");
|
|
11713
12342
|
process.exitCode = 1;
|
|
11714
12343
|
return;
|
|
@@ -11718,7 +12347,7 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
11718
12347
|
const maxPairs = Math.min(100, Math.max(1, parseInt(opts.maxPairs, 10) || 20));
|
|
11719
12348
|
const maxScan = Math.min(2e3, Math.max(1, parseInt(opts.maxScan, 10) || 500));
|
|
11720
12349
|
const maxTopicPairs = Math.min(100, Math.max(1, parseInt(opts.maxTopicPairs, 10) || 20));
|
|
11721
|
-
const all = await
|
|
12350
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
11722
12351
|
const lexical = findLexicalConflictPairs2(all, {
|
|
11723
12352
|
sinceDays,
|
|
11724
12353
|
types: parseTypes(opts.types),
|
|
@@ -11744,21 +12373,21 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
11744
12373
|
|
|
11745
12374
|
// src/commands/enforce.ts
|
|
11746
12375
|
import { execFileSync as execFileSync2, spawn as spawn5 } from "child_process";
|
|
11747
|
-
import { existsSync as
|
|
11748
|
-
import { chmod as chmod2, mkdir as mkdir19, readFile as
|
|
11749
|
-
import
|
|
12376
|
+
import { existsSync as existsSync69 } from "fs";
|
|
12377
|
+
import { chmod as chmod2, mkdir as mkdir19, readFile as readFile19, rm as rm3, writeFile as writeFile34 } from "fs/promises";
|
|
12378
|
+
import path49 from "path";
|
|
11750
12379
|
import "commander";
|
|
11751
12380
|
import {
|
|
11752
12381
|
findProjectRoot as findProjectRoot48,
|
|
11753
12382
|
hasRecentBriefingMarker,
|
|
11754
12383
|
isFreshIsoDate,
|
|
11755
|
-
loadConfig as
|
|
12384
|
+
loadConfig as loadConfig10,
|
|
11756
12385
|
loadMemoriesFromDir as loadMemoriesFromDir36,
|
|
11757
12386
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths6,
|
|
11758
12387
|
readRecentBriefingMarker,
|
|
11759
12388
|
resolveBriefingBudget as resolveBriefingBudget3,
|
|
11760
12389
|
resolveHaivePaths as resolveHaivePaths44,
|
|
11761
|
-
saveConfig as
|
|
12390
|
+
saveConfig as saveConfig4,
|
|
11762
12391
|
SESSION_RECAP_TTL_MS,
|
|
11763
12392
|
verifyAnchor as verifyAnchor4,
|
|
11764
12393
|
writeBriefingMarker as writeBriefingMarker2
|
|
@@ -11773,8 +12402,8 @@ function registerEnforce(program2) {
|
|
|
11773
12402
|
const root = findProjectRoot48(opts.dir);
|
|
11774
12403
|
const paths = resolveHaivePaths44(root);
|
|
11775
12404
|
await mkdir19(paths.haiveDir, { recursive: true });
|
|
11776
|
-
const current = await
|
|
11777
|
-
await
|
|
12405
|
+
const current = await loadConfig10(paths);
|
|
12406
|
+
await saveConfig4(paths, {
|
|
11778
12407
|
...current,
|
|
11779
12408
|
enforcement: {
|
|
11780
12409
|
...current.enforcement,
|
|
@@ -11796,7 +12425,7 @@ function registerEnforce(program2) {
|
|
|
11796
12425
|
if (opts.claude !== false) {
|
|
11797
12426
|
try {
|
|
11798
12427
|
const result = await installClaudeHooksAtPath(defaultClaudeSettingsPath("project", root));
|
|
11799
|
-
ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${
|
|
12428
|
+
ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${path49.relative(root, result.settingsPath)})`);
|
|
11800
12429
|
} catch (err) {
|
|
11801
12430
|
ui.warn(`Claude Code hooks not installed: ${err instanceof Error ? err.message : String(err)}`);
|
|
11802
12431
|
}
|
|
@@ -11804,26 +12433,26 @@ function registerEnforce(program2) {
|
|
|
11804
12433
|
ui.info("Agent-agnostic gates are now active at workflow level: MCP, git, CI, and optional client hooks.");
|
|
11805
12434
|
ui.info("Use `haive run -- <agent command>` for agents that do not expose blocking hooks.");
|
|
11806
12435
|
});
|
|
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) => {
|
|
12436
|
+
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
12437
|
const report = await buildEnforcementReport(opts.dir, "local");
|
|
11809
|
-
printReport(report, Boolean(opts.json));
|
|
12438
|
+
printReport(report, Boolean(opts.json), Boolean(opts.explain));
|
|
11810
12439
|
if (report.should_block) process.exitCode = 1;
|
|
11811
12440
|
});
|
|
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) => {
|
|
12441
|
+
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
12442
|
const report = await buildEnforcementReport(opts.dir, opts.stage ?? "local");
|
|
11814
|
-
printReport(report, Boolean(opts.json));
|
|
12443
|
+
printReport(report, Boolean(opts.json), Boolean(opts.explain));
|
|
11815
12444
|
if (report.should_block) process.exit(2);
|
|
11816
12445
|
});
|
|
11817
12446
|
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
12447
|
const root = findProjectRoot48(opts.dir);
|
|
11819
12448
|
const paths = resolveHaivePaths44(root);
|
|
11820
12449
|
const targets = [
|
|
11821
|
-
|
|
11822
|
-
|
|
12450
|
+
path49.join(paths.haiveDir, ".cache"),
|
|
12451
|
+
path49.join(paths.haiveDir, ".runtime")
|
|
11823
12452
|
];
|
|
11824
12453
|
for (const target of targets) {
|
|
11825
|
-
if (!
|
|
11826
|
-
const rel =
|
|
12454
|
+
if (!existsSync69(target)) continue;
|
|
12455
|
+
const rel = path49.relative(root, target);
|
|
11827
12456
|
if (opts.dryRun) ui.info(`would remove ${rel}`);
|
|
11828
12457
|
else {
|
|
11829
12458
|
await rm3(target, { recursive: true, force: true });
|
|
@@ -11831,9 +12460,9 @@ function registerEnforce(program2) {
|
|
|
11831
12460
|
}
|
|
11832
12461
|
}
|
|
11833
12462
|
});
|
|
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) => {
|
|
12463
|
+
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
12464
|
const report = await buildEnforcementReport(opts.dir, "ci");
|
|
11836
|
-
printReport(report, Boolean(opts.json));
|
|
12465
|
+
printReport(report, Boolean(opts.json), Boolean(opts.explain));
|
|
11837
12466
|
if (report.should_block) process.exit(2);
|
|
11838
12467
|
});
|
|
11839
12468
|
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) => {
|
|
@@ -11841,7 +12470,7 @@ function registerEnforce(program2) {
|
|
|
11841
12470
|
const root = resolveRoot(opts.dir, payload);
|
|
11842
12471
|
if (!root) return;
|
|
11843
12472
|
const paths = resolveHaivePaths44(root);
|
|
11844
|
-
if (!
|
|
12473
|
+
if (!existsSync69(paths.haiveDir)) return;
|
|
11845
12474
|
await mkdir19(paths.runtimeDir, { recursive: true });
|
|
11846
12475
|
const sessionId = opts.sessionId ?? payload.session_id;
|
|
11847
12476
|
const task = opts.task ?? payload.prompt ?? "Start an AI coding session in this hAIve-initialized project.";
|
|
@@ -11903,7 +12532,7 @@ ${briefing.project_context.content.slice(0, 1800)}`);
|
|
|
11903
12532
|
const root = resolveRoot(opts.dir, payload);
|
|
11904
12533
|
if (!root) return;
|
|
11905
12534
|
const paths = resolveHaivePaths44(root);
|
|
11906
|
-
if (!
|
|
12535
|
+
if (!existsSync69(paths.haiveDir)) return;
|
|
11907
12536
|
if (!isWriteLikeTool(payload)) return;
|
|
11908
12537
|
const ok = await hasRecentBriefingMarker(paths, payload.session_id);
|
|
11909
12538
|
if (ok) return;
|
|
@@ -11927,7 +12556,7 @@ ${briefing.project_context.content.slice(0, 1800)}`);
|
|
|
11927
12556
|
async function runWithEnforcement(command, args, opts) {
|
|
11928
12557
|
const root = findProjectRoot48(opts.dir);
|
|
11929
12558
|
const paths = resolveHaivePaths44(root);
|
|
11930
|
-
if (!
|
|
12559
|
+
if (!existsSync69(paths.haiveDir)) {
|
|
11931
12560
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
11932
12561
|
process.exit(1);
|
|
11933
12562
|
}
|
|
@@ -11946,7 +12575,7 @@ async function runWithEnforcement(command, args, opts) {
|
|
|
11946
12575
|
process.exit(2);
|
|
11947
12576
|
}
|
|
11948
12577
|
ui.info(`hAIve briefing marker created for wrapped agent session: ${sessionId}`);
|
|
11949
|
-
ui.info(`Briefing written to ${
|
|
12578
|
+
ui.info(`Briefing written to ${path49.relative(root, briefingFile)} and exported as HAIVE_BRIEFING_FILE`);
|
|
11950
12579
|
const child = spawn5(command, args, {
|
|
11951
12580
|
cwd: root,
|
|
11952
12581
|
stdio: "inherit",
|
|
@@ -11995,9 +12624,9 @@ async function writeWrapperBriefing(paths, sessionId, task) {
|
|
|
11995
12624
|
source: "haive-run",
|
|
11996
12625
|
memoryIds: briefing.memories.map((m) => m.id)
|
|
11997
12626
|
});
|
|
11998
|
-
const dir =
|
|
12627
|
+
const dir = path49.join(paths.runtimeDir, "enforcement", "briefings");
|
|
11999
12628
|
await mkdir19(dir, { recursive: true });
|
|
12000
|
-
const file =
|
|
12629
|
+
const file = path49.join(dir, `${sessionId}.md`);
|
|
12001
12630
|
const parts = [
|
|
12002
12631
|
"# hAIve Briefing",
|
|
12003
12632
|
"",
|
|
@@ -12015,18 +12644,18 @@ async function writeWrapperBriefing(paths, sessionId, task) {
|
|
|
12015
12644
|
if (briefing.setup_warnings.length > 0) {
|
|
12016
12645
|
parts.push("", "## Setup Warnings", ...briefing.setup_warnings.map((w) => `- ${w}`));
|
|
12017
12646
|
}
|
|
12018
|
-
await
|
|
12647
|
+
await writeFile34(file, parts.join("\n") + "\n", "utf8");
|
|
12019
12648
|
return file;
|
|
12020
12649
|
}
|
|
12021
12650
|
async function buildEnforcementReport(dir, stage, sessionId) {
|
|
12022
12651
|
const root = findProjectRoot48(dir);
|
|
12023
12652
|
const paths = resolveHaivePaths44(root);
|
|
12024
|
-
const initialized =
|
|
12025
|
-
const config = initialized ? await
|
|
12653
|
+
const initialized = existsSync69(paths.haiveDir);
|
|
12654
|
+
const config = initialized ? await loadConfig10(paths) : {};
|
|
12026
12655
|
const mode = config.enforcement?.mode ?? "strict";
|
|
12027
12656
|
const findings = [];
|
|
12028
12657
|
if (!initialized) {
|
|
12029
|
-
return {
|
|
12658
|
+
return withCategories({
|
|
12030
12659
|
root,
|
|
12031
12660
|
initialized,
|
|
12032
12661
|
mode,
|
|
@@ -12039,19 +12668,19 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
12039
12668
|
fix: "Run `haive init` or `haive enforce install`.",
|
|
12040
12669
|
impact: 100
|
|
12041
12670
|
}]
|
|
12042
|
-
};
|
|
12671
|
+
});
|
|
12043
12672
|
}
|
|
12044
12673
|
if (mode === "off") {
|
|
12045
|
-
return {
|
|
12674
|
+
return withCategories({
|
|
12046
12675
|
root,
|
|
12047
12676
|
initialized,
|
|
12048
12677
|
mode,
|
|
12049
12678
|
score: buildScore([], config.enforcement?.scoreThreshold),
|
|
12050
12679
|
should_block: false,
|
|
12051
12680
|
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
12052
|
-
};
|
|
12681
|
+
});
|
|
12053
12682
|
}
|
|
12054
|
-
findings.push(...await inspectIntegrationVersions(root, "0.9.
|
|
12683
|
+
findings.push(...await inspectIntegrationVersions(root, "0.9.18"));
|
|
12055
12684
|
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
12056
12685
|
const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
|
|
12057
12686
|
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
|
|
@@ -12101,17 +12730,27 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
12101
12730
|
});
|
|
12102
12731
|
}
|
|
12103
12732
|
const hasErrors = findings.some((f) => f.severity === "error");
|
|
12104
|
-
return {
|
|
12733
|
+
return withCategories({
|
|
12105
12734
|
root,
|
|
12106
12735
|
initialized,
|
|
12107
12736
|
mode,
|
|
12108
12737
|
score: buildScore(findings, config.enforcement?.scoreThreshold),
|
|
12109
12738
|
should_block: mode === "strict" && hasErrors,
|
|
12110
12739
|
findings
|
|
12740
|
+
});
|
|
12741
|
+
}
|
|
12742
|
+
function withCategories(report) {
|
|
12743
|
+
return {
|
|
12744
|
+
...report,
|
|
12745
|
+
categories: {
|
|
12746
|
+
blocking: report.findings.filter((f) => f.severity === "error"),
|
|
12747
|
+
review: report.findings.filter((f) => f.severity === "warn"),
|
|
12748
|
+
info: report.findings.filter((f) => f.severity === "info" || f.severity === "ok")
|
|
12749
|
+
}
|
|
12111
12750
|
};
|
|
12112
12751
|
}
|
|
12113
12752
|
async function hasRecentSessionRecap(paths) {
|
|
12114
|
-
if (!
|
|
12753
|
+
if (!existsSync69(paths.memoriesDir)) return false;
|
|
12115
12754
|
const all = await loadMemoriesFromDir36(paths.memoriesDir);
|
|
12116
12755
|
return all.some(({ memory: memory2 }) => {
|
|
12117
12756
|
const fm = memory2.frontmatter;
|
|
@@ -12120,7 +12759,7 @@ async function hasRecentSessionRecap(paths) {
|
|
|
12120
12759
|
});
|
|
12121
12760
|
}
|
|
12122
12761
|
async function verifyMemoryPolicy(paths, config) {
|
|
12123
|
-
if (!
|
|
12762
|
+
if (!existsSync69(paths.memoriesDir)) return [];
|
|
12124
12763
|
const all = await loadMemoriesFromDir36(paths.memoriesDir);
|
|
12125
12764
|
const findings = [];
|
|
12126
12765
|
const staleImportant = [];
|
|
@@ -12158,7 +12797,7 @@ async function verifyMemoryPolicy(paths, config) {
|
|
|
12158
12797
|
return findings;
|
|
12159
12798
|
}
|
|
12160
12799
|
async function verifyDecisionCoverage(paths, stage, sessionId) {
|
|
12161
|
-
if (!
|
|
12800
|
+
if (!existsSync69(paths.memoriesDir)) return [];
|
|
12162
12801
|
const changedFiles = await getChangedFiles(paths.root, stage);
|
|
12163
12802
|
if (changedFiles.length === 0) {
|
|
12164
12803
|
return [{ severity: "info", code: "decision-coverage-no-changes", message: "No changed files to match against policy memories." }];
|
|
@@ -12168,7 +12807,7 @@ async function verifyDecisionCoverage(paths, stage, sessionId) {
|
|
|
12168
12807
|
const relevant = all.map(({ memory: memory2 }) => memory2).filter((memory2) => {
|
|
12169
12808
|
const fm = memory2.frontmatter;
|
|
12170
12809
|
if (!policyTypes.has(fm.type)) return false;
|
|
12171
|
-
if (fm.status
|
|
12810
|
+
if (fm.status !== "validated") return false;
|
|
12172
12811
|
return memoryMatchesAnchorPaths6(memory2, changedFiles);
|
|
12173
12812
|
});
|
|
12174
12813
|
if (relevant.length === 0) {
|
|
@@ -12251,9 +12890,9 @@ async function inspectIntegrationVersions(root, expectedVersion) {
|
|
|
12251
12890
|
];
|
|
12252
12891
|
const findings = [];
|
|
12253
12892
|
for (const rel of files) {
|
|
12254
|
-
const file =
|
|
12255
|
-
if (!
|
|
12256
|
-
const text = await
|
|
12893
|
+
const file = path49.join(root, rel);
|
|
12894
|
+
if (!existsSync69(file)) continue;
|
|
12895
|
+
const text = await readFile19(file, "utf8").catch(() => "");
|
|
12257
12896
|
for (const bin of extractAbsoluteHaiveBins2(text)) {
|
|
12258
12897
|
const version = versionForBinary2(bin);
|
|
12259
12898
|
if (!version) {
|
|
@@ -12318,7 +12957,9 @@ async function getChangedFiles(root, stage) {
|
|
|
12318
12957
|
if (file) files.add(file);
|
|
12319
12958
|
}
|
|
12320
12959
|
}
|
|
12321
|
-
return [...files].filter(
|
|
12960
|
+
return [...files].filter(
|
|
12961
|
+
(file) => !file.startsWith(".ai/.runtime/") && !file.startsWith(".ai/.cache/") && !file.startsWith(".ai/.usage/") && file !== ".ai/.usage/tool-usage.jsonl"
|
|
12962
|
+
);
|
|
12322
12963
|
}
|
|
12323
12964
|
function buildScore(findings, threshold = 80) {
|
|
12324
12965
|
const checks = {
|
|
@@ -12339,8 +12980,8 @@ function buildScore(findings, threshold = 80) {
|
|
|
12339
12980
|
};
|
|
12340
12981
|
}
|
|
12341
12982
|
async function installGitEnforcement(root) {
|
|
12342
|
-
const hooksDir =
|
|
12343
|
-
if (!
|
|
12983
|
+
const hooksDir = path49.join(root, ".git", "hooks");
|
|
12984
|
+
if (!existsSync69(path49.join(root, ".git"))) {
|
|
12344
12985
|
ui.warn("No .git directory found; git enforcement hooks skipped.");
|
|
12345
12986
|
return;
|
|
12346
12987
|
}
|
|
@@ -12362,31 +13003,31 @@ haive enforce check --stage pre-push --dir . || exit $?
|
|
|
12362
13003
|
}
|
|
12363
13004
|
];
|
|
12364
13005
|
for (const hook of hooks) {
|
|
12365
|
-
const file =
|
|
12366
|
-
if (
|
|
12367
|
-
const current = await
|
|
13006
|
+
const file = path49.join(hooksDir, hook.name);
|
|
13007
|
+
if (existsSync69(file)) {
|
|
13008
|
+
const current = await readFile19(file, "utf8").catch(() => "");
|
|
12368
13009
|
if (current.includes(ENFORCE_HOOK_MARKER)) {
|
|
12369
|
-
await
|
|
13010
|
+
await writeFile34(file, hook.body, "utf8");
|
|
12370
13011
|
} else {
|
|
12371
|
-
await
|
|
13012
|
+
await writeFile34(file, `${current.trimEnd()}
|
|
12372
13013
|
|
|
12373
13014
|
${hook.body}`, "utf8");
|
|
12374
13015
|
}
|
|
12375
13016
|
} else {
|
|
12376
|
-
await
|
|
13017
|
+
await writeFile34(file, hook.body, "utf8");
|
|
12377
13018
|
}
|
|
12378
13019
|
await chmod2(file, 493);
|
|
12379
13020
|
}
|
|
12380
13021
|
ui.success("Installed blocking git enforcement hooks: pre-commit, pre-push");
|
|
12381
13022
|
}
|
|
12382
13023
|
async function installCiEnforcement(root) {
|
|
12383
|
-
const workflowPath =
|
|
12384
|
-
await mkdir19(
|
|
12385
|
-
if (
|
|
13024
|
+
const workflowPath = path49.join(root, ".github", "workflows", "haive-enforcement.yml");
|
|
13025
|
+
await mkdir19(path49.dirname(workflowPath), { recursive: true });
|
|
13026
|
+
if (existsSync69(workflowPath)) {
|
|
12386
13027
|
ui.info("GitHub Actions enforcement workflow already exists \u2014 skipped");
|
|
12387
13028
|
return;
|
|
12388
13029
|
}
|
|
12389
|
-
await
|
|
13030
|
+
await writeFile34(workflowPath, `name: haive-enforcement
|
|
12390
13031
|
|
|
12391
13032
|
on:
|
|
12392
13033
|
pull_request:
|
|
@@ -12410,9 +13051,9 @@ jobs:
|
|
|
12410
13051
|
- name: Enforce hAIve policy
|
|
12411
13052
|
run: haive enforce ci
|
|
12412
13053
|
`, "utf8");
|
|
12413
|
-
ui.success(`Created ${
|
|
13054
|
+
ui.success(`Created ${path49.relative(root, workflowPath)}`);
|
|
12414
13055
|
}
|
|
12415
|
-
function printReport(report, json) {
|
|
13056
|
+
function printReport(report, json, explain = false) {
|
|
12416
13057
|
if (json) {
|
|
12417
13058
|
console.log(JSON.stringify(report, null, 2));
|
|
12418
13059
|
return;
|
|
@@ -12420,14 +13061,32 @@ function printReport(report, json) {
|
|
|
12420
13061
|
console.log(ui.bold(`hAIve enforcement \u2014 ${report.mode}`));
|
|
12421
13062
|
console.log(ui.dim(` root: ${report.root}`));
|
|
12422
13063
|
console.log(ui.dim(` score: ${report.score.score}% / threshold ${report.score.threshold}%`));
|
|
12423
|
-
|
|
12424
|
-
|
|
12425
|
-
|
|
12426
|
-
|
|
13064
|
+
if (explain) {
|
|
13065
|
+
printFindingGroup("Blocking", report.categories.blocking, "error");
|
|
13066
|
+
printFindingGroup("Review", report.categories.review, "warn");
|
|
13067
|
+
printFindingGroup("Info", report.categories.info, "info");
|
|
13068
|
+
} else {
|
|
13069
|
+
for (const finding of report.findings) printFinding(finding);
|
|
12427
13070
|
}
|
|
12428
13071
|
if (report.should_block) ui.error("hAIve enforcement gate failed.");
|
|
12429
13072
|
else ui.success("hAIve enforcement gate passed.");
|
|
12430
13073
|
}
|
|
13074
|
+
function printFindingGroup(title, findings, tone) {
|
|
13075
|
+
if (findings.length === 0) return;
|
|
13076
|
+
console.log();
|
|
13077
|
+
const heading = tone === "error" ? ui.red(title) : tone === "warn" ? ui.yellow(title) : ui.bold(title);
|
|
13078
|
+
console.log(ui.bold(`${heading} (${findings.length})`));
|
|
13079
|
+
const scoreFinding = findings.find((f) => f.code === "enforcement-score-below-threshold");
|
|
13080
|
+
for (const finding of findings.filter((f) => f.code !== "enforcement-score-below-threshold")) {
|
|
13081
|
+
printFinding(finding, true);
|
|
13082
|
+
}
|
|
13083
|
+
if (scoreFinding) printFinding(scoreFinding, true);
|
|
13084
|
+
}
|
|
13085
|
+
function printFinding(finding, explain = false) {
|
|
13086
|
+
const marker = finding.severity === "error" ? ui.red("\u2717") : finding.severity === "warn" ? ui.yellow("\u26A0") : finding.severity === "ok" ? ui.green("\u2713") : ui.dim("\u2022");
|
|
13087
|
+
console.log(`${marker} ${finding.code}: ${finding.message}`);
|
|
13088
|
+
if (finding.fix) console.log(ui.dim(`${explain ? " repair: " : " fix: "}${finding.fix}`));
|
|
13089
|
+
}
|
|
12431
13090
|
async function readHookPayload() {
|
|
12432
13091
|
const raw = await readStdin2(MAX_STDIN_BYTES2);
|
|
12433
13092
|
if (!raw.trim()) return {};
|
|
@@ -12509,7 +13168,7 @@ function registerRun(program2) {
|
|
|
12509
13168
|
|
|
12510
13169
|
// src/index.ts
|
|
12511
13170
|
var program = new Command51();
|
|
12512
|
-
program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.
|
|
13171
|
+
program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.18").option("--advanced", "show maintenance and experimental commands in help");
|
|
12513
13172
|
registerInit(program);
|
|
12514
13173
|
registerWelcome(program);
|
|
12515
13174
|
registerResolveProject(program);
|