@hiveai/cli 0.9.17 → 0.9.19
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 +1482 -1113
- package/dist/index.js.map +1 -1
- package/package.json +4 -11
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)));
|
|
@@ -2462,7 +2462,7 @@ function registerInit(program2) {
|
|
|
2462
2462
|
console.log(ui.dim(` \u2713 Stack memory packs pre-seeded (${stacksToSeed.join(", ")})`));
|
|
2463
2463
|
}
|
|
2464
2464
|
console.log();
|
|
2465
|
-
if (!
|
|
2465
|
+
if (!wantBootstrap) {
|
|
2466
2466
|
console.log(ui.bold("One remaining step (optional but recommended):"));
|
|
2467
2467
|
console.log(" " + ui.bold("haive init --bootstrap") + ui.dim(" \u2190 fill project-context.md without AI"));
|
|
2468
2468
|
console.log(" " + ui.dim("Or in your AI client: invoke the MCP prompt ") + ui.bold("bootstrap_project"));
|
|
@@ -2477,7 +2477,7 @@ function registerInit(program2) {
|
|
|
2477
2477
|
console.log(ui.dim(" haive memory import README.md \u2014 from README / docs"));
|
|
2478
2478
|
} else {
|
|
2479
2479
|
console.log(ui.bold("Next steps:"));
|
|
2480
|
-
if (!
|
|
2480
|
+
if (!wantBootstrap) {
|
|
2481
2481
|
console.log(ui.dim(" 1. Fill project context (pick one):"));
|
|
2482
2482
|
console.log(" " + ui.bold("haive init --bootstrap") + ui.dim(" \u2190 instant, no AI needed"));
|
|
2483
2483
|
console.log(" or invoke the MCP prompt " + ui.bold("bootstrap_project") + ui.dim(" in your AI client"));
|
|
@@ -6515,7 +6515,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
6515
6515
|
};
|
|
6516
6516
|
}
|
|
6517
6517
|
var SERVER_NAME = "haive";
|
|
6518
|
-
var SERVER_VERSION = "0.9.
|
|
6518
|
+
var SERVER_VERSION = "0.9.19";
|
|
6519
6519
|
function jsonResult(data) {
|
|
6520
6520
|
return {
|
|
6521
6521
|
content: [
|
|
@@ -6535,6 +6535,7 @@ var ENFORCEMENT_PROFILE_TOOLS = [
|
|
|
6535
6535
|
"mem_verify",
|
|
6536
6536
|
"mem_relevant_to",
|
|
6537
6537
|
"code_map",
|
|
6538
|
+
"code_search",
|
|
6538
6539
|
"pre_commit_check",
|
|
6539
6540
|
"mem_session_end"
|
|
6540
6541
|
];
|
|
@@ -7483,177 +7484,668 @@ function registerMcp(program2) {
|
|
|
7483
7484
|
}
|
|
7484
7485
|
|
|
7485
7486
|
// src/commands/sync.ts
|
|
7486
|
-
import { spawnSync as
|
|
7487
|
-
import { readFile as
|
|
7488
|
-
import { existsSync as
|
|
7489
|
-
import
|
|
7487
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
7488
|
+
import { readFile as readFile9, writeFile as writeFile15, mkdir as mkdir10 } from "fs/promises";
|
|
7489
|
+
import { existsSync as existsSync31 } from "fs";
|
|
7490
|
+
import path15 from "path";
|
|
7490
7491
|
import "commander";
|
|
7491
7492
|
import {
|
|
7492
7493
|
DEFAULT_AUTO_PROMOTE_RULE as DEFAULT_AUTO_PROMOTE_RULE2,
|
|
7493
7494
|
buildFrontmatter as buildFrontmatter6,
|
|
7494
|
-
findProjectRoot as
|
|
7495
|
-
getUsage as
|
|
7495
|
+
findProjectRoot as findProjectRoot12,
|
|
7496
|
+
getUsage as getUsage11,
|
|
7496
7497
|
isAutoPromoteEligible as isAutoPromoteEligible2,
|
|
7497
7498
|
isDecaying as isDecaying2,
|
|
7498
|
-
loadCodeMap as
|
|
7499
|
-
loadConfig as
|
|
7500
|
-
loadMemoriesFromDir as
|
|
7501
|
-
loadUsageIndex as
|
|
7499
|
+
loadCodeMap as loadCodeMap6,
|
|
7500
|
+
loadConfig as loadConfig5,
|
|
7501
|
+
loadMemoriesFromDir as loadMemoriesFromDir24,
|
|
7502
|
+
loadUsageIndex as loadUsageIndex13,
|
|
7502
7503
|
pullCrossRepoSources,
|
|
7503
|
-
resolveHaivePaths as
|
|
7504
|
+
resolveHaivePaths as resolveHaivePaths9,
|
|
7504
7505
|
resolveManifestFiles,
|
|
7505
|
-
serializeMemory as
|
|
7506
|
+
serializeMemory as serializeMemory12,
|
|
7506
7507
|
trackDependencies,
|
|
7507
7508
|
verifyAnchor as verifyAnchor2,
|
|
7508
7509
|
watchContracts
|
|
7509
7510
|
} from "@hiveai/core";
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7511
|
+
|
|
7512
|
+
// src/utils/autopilot.ts
|
|
7513
|
+
import { existsSync as existsSync30 } from "fs";
|
|
7514
|
+
import { readFile as readFile8, writeFile as writeFile14 } from "fs/promises";
|
|
7515
|
+
import path14 from "path";
|
|
7516
|
+
import {
|
|
7517
|
+
AUTOPILOT_DEFAULTS as AUTOPILOT_DEFAULTS2,
|
|
7518
|
+
buildCodeMap as buildCodeMap3,
|
|
7519
|
+
loadCodeMap as loadCodeMap5,
|
|
7520
|
+
loadConfig as loadConfig4,
|
|
7521
|
+
saveCodeMap as saveCodeMap3,
|
|
7522
|
+
saveConfig as saveConfig2
|
|
7523
|
+
} from "@hiveai/core";
|
|
7524
|
+
|
|
7525
|
+
// src/commands/memory-lint.ts
|
|
7526
|
+
import { existsSync as existsSync29 } from "fs";
|
|
7527
|
+
import { writeFile as writeFile13 } from "fs/promises";
|
|
7528
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
7529
|
+
import path13 from "path";
|
|
7530
|
+
import "commander";
|
|
7531
|
+
import {
|
|
7532
|
+
findProjectRoot as findProjectRoot11,
|
|
7533
|
+
getUsage as getUsage10,
|
|
7534
|
+
loadCodeMap as loadCodeMap4,
|
|
7535
|
+
loadMemoriesFromDir as loadMemoriesFromDir23,
|
|
7536
|
+
loadUsageIndex as loadUsageIndex12,
|
|
7537
|
+
resolveHaivePaths as resolveHaivePaths8,
|
|
7538
|
+
serializeMemory as serializeMemory11
|
|
7539
|
+
} from "@hiveai/core";
|
|
7540
|
+
async function lintMemoriesAsync(root, options = {}) {
|
|
7541
|
+
const paths = resolveHaivePaths8(root);
|
|
7542
|
+
const out = [];
|
|
7543
|
+
const fixes = [];
|
|
7544
|
+
if (!existsSync29(paths.memoriesDir)) return { findings: out, fixes };
|
|
7545
|
+
const loaded = await loadMemoriesFromDir23(paths.memoriesDir);
|
|
7546
|
+
const usage = await loadUsageIndex12(paths);
|
|
7547
|
+
const codeMap = await loadCodeMap4(paths);
|
|
7548
|
+
const trackedFiles = gitTrackedFiles(root);
|
|
7549
|
+
const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
|
|
7550
|
+
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;
|
|
7551
|
+
for (const { filePath, memory: memory2 } of loaded) {
|
|
7552
|
+
const fm = memory2.frontmatter;
|
|
7553
|
+
if (fm.type === "session_recap") continue;
|
|
7554
|
+
const body = memory2.body.trim();
|
|
7555
|
+
const naked = body.replace(/^#.*$/gm, "").replace(/```[\s\S]*?```/g, "").trim();
|
|
7556
|
+
if (naked.length < 40 && fm.status !== "rejected") {
|
|
7557
|
+
out.push({
|
|
7558
|
+
file: filePath,
|
|
7559
|
+
id: fm.id,
|
|
7560
|
+
severity: "warn",
|
|
7561
|
+
code: "SHORT_BODY",
|
|
7562
|
+
message: "Body looks very short (< ~40 chars of prose after headings). Prefer actionable detail."
|
|
7563
|
+
});
|
|
7528
7564
|
}
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
if (
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7565
|
+
if (["decision", "gotcha", "convention", "architecture", "attempt"].includes(fm.type) && fm.status !== "rejected" && !actionableWords.test(naked)) {
|
|
7566
|
+
out.push({
|
|
7567
|
+
file: filePath,
|
|
7568
|
+
id: fm.id,
|
|
7569
|
+
severity: "info",
|
|
7570
|
+
code: "LOW_ACTIONABILITY",
|
|
7571
|
+
message: "Record does not contain obvious action/rationale words. Add the concrete rule, why it exists, and what to do instead."
|
|
7572
|
+
});
|
|
7573
|
+
}
|
|
7574
|
+
const suggestedAnchors = suggestAnchors(root, { filePath, memory: memory2 }, codeMap, trackedFiles);
|
|
7575
|
+
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated") {
|
|
7576
|
+
out.push({
|
|
7577
|
+
file: filePath,
|
|
7578
|
+
id: fm.id,
|
|
7579
|
+
severity: "warn",
|
|
7580
|
+
code: "MISSING_ANCHOR",
|
|
7581
|
+
message: `${fm.type} is validated without anchor paths \u2014 add anchor.paths so haive sync can flag staleness.`,
|
|
7582
|
+
...suggestedAnchors.paths.length > 0 || suggestedAnchors.symbols.length > 0 ? { suggested_anchors: suggestedAnchors } : {}
|
|
7583
|
+
});
|
|
7584
|
+
}
|
|
7585
|
+
if (fm.status === "stale" && !fm.stale_reason) {
|
|
7586
|
+
out.push({
|
|
7587
|
+
file: filePath,
|
|
7588
|
+
id: fm.id,
|
|
7589
|
+
severity: "info",
|
|
7590
|
+
code: "STALE_NO_REASON",
|
|
7591
|
+
message: "Status is stale but stale_reason is empty \u2014 document why when possible."
|
|
7592
|
+
});
|
|
7593
|
+
}
|
|
7594
|
+
if (fm.type === "glossary" && naked.length > 6e3) {
|
|
7595
|
+
out.push({
|
|
7596
|
+
file: filePath,
|
|
7597
|
+
id: fm.id,
|
|
7598
|
+
severity: "info",
|
|
7599
|
+
code: "LONG_GLOSSARY",
|
|
7600
|
+
message: "Very long glossary \u2014 consider splitting concepts for tighter briefings."
|
|
7601
|
+
});
|
|
7602
|
+
}
|
|
7603
|
+
const hasMarkdownHeading = /^#{1,3}\s+\S/m.test(memory2.body.trim());
|
|
7604
|
+
if (!hasMarkdownHeading) {
|
|
7605
|
+
out.push({
|
|
7606
|
+
file: filePath,
|
|
7607
|
+
id: fm.id,
|
|
7608
|
+
severity: "warn",
|
|
7609
|
+
code: "NO_MD_HEADING",
|
|
7610
|
+
message: "No Markdown heading (#/##/###) \u2014 add one so humans and auditors can skim the memo quickly."
|
|
7611
|
+
});
|
|
7612
|
+
}
|
|
7613
|
+
const u = getUsage10(usage, fm.id);
|
|
7614
|
+
if (fm.status === "validated" && u.read_count === 0) {
|
|
7615
|
+
out.push({
|
|
7616
|
+
file: filePath,
|
|
7617
|
+
id: fm.id,
|
|
7618
|
+
severity: "info",
|
|
7619
|
+
code: "NEVER_READ",
|
|
7620
|
+
message: "Validated record has never been surfaced/read. Consider improving tags/anchors or archiving it if it is not useful."
|
|
7621
|
+
});
|
|
7622
|
+
}
|
|
7623
|
+
if (options.fix) {
|
|
7624
|
+
const actions = [];
|
|
7625
|
+
let nextBody = memory2.body;
|
|
7626
|
+
let nextFrontmatter = memory2.frontmatter;
|
|
7627
|
+
if (!hasMarkdownHeading) {
|
|
7628
|
+
nextBody = `# ${titleFromId(fm.id)}
|
|
7629
|
+
|
|
7630
|
+
${nextBody.trim()}`;
|
|
7631
|
+
actions.push("add missing Markdown heading");
|
|
7632
|
+
}
|
|
7633
|
+
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated" && suggestedAnchors.paths.length > 0) {
|
|
7634
|
+
nextFrontmatter = {
|
|
7635
|
+
...nextFrontmatter,
|
|
7636
|
+
anchor: {
|
|
7637
|
+
...nextFrontmatter.anchor,
|
|
7638
|
+
paths: [.../* @__PURE__ */ new Set([...nextFrontmatter.anchor.paths, ...suggestedAnchors.paths])],
|
|
7639
|
+
symbols: [
|
|
7640
|
+
.../* @__PURE__ */ new Set([...nextFrontmatter.anchor.symbols, ...suggestedAnchors.symbols])
|
|
7641
|
+
]
|
|
7642
|
+
},
|
|
7643
|
+
tags: nextFrontmatter.tags.filter((tag) => tag !== "needs_anchor")
|
|
7644
|
+
};
|
|
7645
|
+
actions.push("add suggested tracked anchor paths");
|
|
7646
|
+
if (suggestedAnchors.symbols.length > 0) {
|
|
7647
|
+
actions.push("add suggested anchor symbols");
|
|
7597
7648
|
}
|
|
7598
7649
|
}
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
maxRejections: DEFAULT_AUTO_PROMOTE_RULE2.maxRejections
|
|
7610
|
-
})) {
|
|
7650
|
+
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")) {
|
|
7651
|
+
nextFrontmatter = {
|
|
7652
|
+
...nextFrontmatter,
|
|
7653
|
+
tags: [...nextFrontmatter.tags, "needs_anchor"]
|
|
7654
|
+
};
|
|
7655
|
+
actions.push("tag validated anchorless record with needs_anchor");
|
|
7656
|
+
}
|
|
7657
|
+
if (actions.length > 0) {
|
|
7658
|
+
fixes.push({ file: filePath, id: fm.id, actions, applied: Boolean(options.apply) });
|
|
7659
|
+
if (options.apply) {
|
|
7611
7660
|
await writeFile13(
|
|
7612
7661
|
filePath,
|
|
7613
|
-
serializeMemory11({ frontmatter:
|
|
7662
|
+
serializeMemory11({ frontmatter: nextFrontmatter, body: nextBody }),
|
|
7614
7663
|
"utf8"
|
|
7615
7664
|
);
|
|
7616
|
-
promoted++;
|
|
7617
|
-
continue;
|
|
7618
|
-
}
|
|
7619
|
-
if (autoApproveDelayHours !== null && fm.status === "proposed" && fm.scope === "team") {
|
|
7620
|
-
const ageHours = (nowMs - new Date(fm.created_at).getTime()) / (1e3 * 60 * 60);
|
|
7621
|
-
if (ageHours >= autoApproveDelayHours) {
|
|
7622
|
-
await writeFile13(
|
|
7623
|
-
filePath,
|
|
7624
|
-
serializeMemory11({
|
|
7625
|
-
frontmatter: {
|
|
7626
|
-
...fm,
|
|
7627
|
-
status: "validated",
|
|
7628
|
-
verified_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
7629
|
-
},
|
|
7630
|
-
body: memory2.body
|
|
7631
|
-
}),
|
|
7632
|
-
"utf8"
|
|
7633
|
-
);
|
|
7634
|
-
autoApproved++;
|
|
7635
|
-
}
|
|
7636
7665
|
}
|
|
7637
7666
|
}
|
|
7638
7667
|
}
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
);
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7668
|
+
}
|
|
7669
|
+
for (const dup of nearDuplicatePairs(loaded)) {
|
|
7670
|
+
out.push({
|
|
7671
|
+
file: dup.file,
|
|
7672
|
+
id: dup.id,
|
|
7673
|
+
severity: "warn",
|
|
7674
|
+
code: "NEAR_DUPLICATE",
|
|
7675
|
+
message: `Body overlaps ~${Math.round(dup.score * 100)}% with ${dup.otherId}. Merge or deprecate one record to reduce briefing noise.`
|
|
7676
|
+
});
|
|
7677
|
+
}
|
|
7678
|
+
return { findings: out, fixes };
|
|
7679
|
+
}
|
|
7680
|
+
function titleFromId(id) {
|
|
7681
|
+
const withoutDate = id.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
7682
|
+
return withoutDate.split("-").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
7683
|
+
}
|
|
7684
|
+
function suggestAnchors(root, loaded, codeMap, trackedFiles) {
|
|
7685
|
+
const body = loaded.memory.body;
|
|
7686
|
+
const paths = /* @__PURE__ */ new Set();
|
|
7687
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
7688
|
+
for (const match of body.matchAll(/`([^`\n]+\.[A-Za-z0-9]+)`|(?:^|\s)([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)/gm)) {
|
|
7689
|
+
const candidate = (match[1] ?? match[2] ?? "").replace(/^\.?\//, "");
|
|
7690
|
+
if (!candidate || candidate.startsWith("http")) continue;
|
|
7691
|
+
if (existsSync29(path13.join(root, candidate)) && isSafeAnchorPath(candidate, trackedFiles)) {
|
|
7692
|
+
paths.add(candidate);
|
|
7693
|
+
}
|
|
7694
|
+
}
|
|
7695
|
+
if (codeMap) {
|
|
7696
|
+
const lowered = body.toLowerCase();
|
|
7697
|
+
for (const [file, entry] of Object.entries(codeMap.files)) {
|
|
7698
|
+
for (const exp of entry.exports) {
|
|
7699
|
+
if (!exp.name || exp.name.length < 4) continue;
|
|
7700
|
+
if (lowered.includes(exp.name.toLowerCase())) {
|
|
7701
|
+
if (isSafeAnchorPath(file, trackedFiles)) {
|
|
7702
|
+
paths.add(file);
|
|
7703
|
+
symbols.add(exp.name);
|
|
7704
|
+
}
|
|
7705
|
+
}
|
|
7706
|
+
if (paths.size >= 5 && symbols.size >= 5) break;
|
|
7707
|
+
}
|
|
7708
|
+
if (paths.size >= 5 && symbols.size >= 5) break;
|
|
7709
|
+
}
|
|
7710
|
+
}
|
|
7711
|
+
return {
|
|
7712
|
+
paths: [...paths].slice(0, 5),
|
|
7713
|
+
symbols: [...symbols].slice(0, 5)
|
|
7714
|
+
};
|
|
7715
|
+
}
|
|
7716
|
+
function gitTrackedFiles(root) {
|
|
7717
|
+
const result = spawnSync3("git", ["ls-files"], {
|
|
7718
|
+
cwd: root,
|
|
7719
|
+
encoding: "utf8",
|
|
7720
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
7721
|
+
});
|
|
7722
|
+
if (result.status !== 0) return null;
|
|
7723
|
+
const files = result.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
7724
|
+
return new Set(files);
|
|
7725
|
+
}
|
|
7726
|
+
function isSafeAnchorPath(file, trackedFiles) {
|
|
7727
|
+
const normalized = file.replace(/\\/g, "/").replace(/^\.?\//, "");
|
|
7728
|
+
if (normalized.startsWith(".ai/.cache/") || normalized.startsWith(".ai/.runtime/")) return false;
|
|
7729
|
+
if (normalized.includes("/node_modules/") || normalized.startsWith("node_modules/")) return false;
|
|
7730
|
+
if (normalized.includes("/dist/") || normalized.startsWith("dist/")) return false;
|
|
7731
|
+
if (trackedFiles && !trackedFiles.has(normalized)) return false;
|
|
7732
|
+
return true;
|
|
7733
|
+
}
|
|
7734
|
+
function nearDuplicatePairs(loaded) {
|
|
7735
|
+
const out = [];
|
|
7736
|
+
const candidates = loaded.filter(({ memory: memory2 }) => {
|
|
7737
|
+
const fm = memory2.frontmatter;
|
|
7738
|
+
return fm.type !== "session_recap" && fm.status !== "rejected" && fm.status !== "deprecated";
|
|
7739
|
+
});
|
|
7740
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
7741
|
+
for (let j = i + 1; j < candidates.length; j++) {
|
|
7742
|
+
const a = candidates[i];
|
|
7743
|
+
const b = candidates[j];
|
|
7744
|
+
if (a.memory.frontmatter.scope !== b.memory.frontmatter.scope) continue;
|
|
7745
|
+
if (a.memory.frontmatter.type !== b.memory.frontmatter.type) continue;
|
|
7746
|
+
const score = jaccard2(tokenSet(a.memory.body), tokenSet(b.memory.body));
|
|
7747
|
+
if (score >= 0.72) {
|
|
7748
|
+
out.push({
|
|
7749
|
+
id: a.memory.frontmatter.id,
|
|
7750
|
+
otherId: b.memory.frontmatter.id,
|
|
7751
|
+
file: a.filePath,
|
|
7752
|
+
score
|
|
7753
|
+
});
|
|
7754
|
+
}
|
|
7755
|
+
}
|
|
7756
|
+
}
|
|
7757
|
+
return out;
|
|
7758
|
+
}
|
|
7759
|
+
function tokenSet(body) {
|
|
7760
|
+
return new Set(
|
|
7761
|
+
(body.toLowerCase().match(/\b[a-z0-9]{4,}\b/g) ?? []).filter((word) => !["this", "that", "with", "from", "have"].includes(word))
|
|
7762
|
+
);
|
|
7763
|
+
}
|
|
7764
|
+
function jaccard2(a, b) {
|
|
7765
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
7766
|
+
let inter = 0;
|
|
7767
|
+
for (const item of a) if (b.has(item)) inter++;
|
|
7768
|
+
return inter / (a.size + b.size - inter);
|
|
7769
|
+
}
|
|
7770
|
+
function registerMemoryLint(parent) {
|
|
7771
|
+
parent.command("lint").description(
|
|
7772
|
+
"Heuristic corpus checks (anchors on key types, headings, verbosity). Static analysis only."
|
|
7773
|
+
).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) => {
|
|
7774
|
+
const root = findProjectRoot11(opts.dir);
|
|
7775
|
+
const apply = Boolean(opts.fix && opts.apply);
|
|
7776
|
+
const dryRun = Boolean(opts.fix && (opts.dryRun || !opts.apply));
|
|
7777
|
+
const report = await lintMemoriesAsync(root, { fix: Boolean(opts.fix), apply });
|
|
7778
|
+
const findings = report.findings;
|
|
7779
|
+
if (opts.json) {
|
|
7780
|
+
console.log(JSON.stringify({
|
|
7781
|
+
findings_count: findings.length,
|
|
7782
|
+
findings,
|
|
7783
|
+
fixes_count: report.fixes.length,
|
|
7784
|
+
fixes: report.fixes,
|
|
7785
|
+
fix_mode: opts.fix ? apply ? "apply" : "dry-run" : "off"
|
|
7786
|
+
}, null, 2));
|
|
7787
|
+
process.exitCode = findings.some((f) => f.severity === "error") ? 1 : 0;
|
|
7788
|
+
return;
|
|
7789
|
+
}
|
|
7790
|
+
if (findings.length === 0) {
|
|
7791
|
+
ui.success(`memory lint OK \u2014 ${root}`);
|
|
7792
|
+
return;
|
|
7793
|
+
}
|
|
7794
|
+
console.log(ui.bold(`memory lint (${findings.length} finding${findings.length === 1 ? "" : "s"})`) + `
|
|
7795
|
+
`);
|
|
7796
|
+
if (opts.fix) {
|
|
7797
|
+
const mode = apply ? "apply" : dryRun ? "dry-run" : "dry-run";
|
|
7798
|
+
const verb = apply ? "changed" : "would change";
|
|
7799
|
+
console.log(ui.bold(`fix ${mode}: ${report.fixes.length} file${report.fixes.length === 1 ? "" : "s"} ${verb}`));
|
|
7800
|
+
for (const fix of report.fixes) {
|
|
7801
|
+
console.log(` ${ui.dim(fix.id)} ${fix.actions.join("; ")}`);
|
|
7802
|
+
console.log(ui.dim(` \u2192 ${fix.file}`));
|
|
7803
|
+
}
|
|
7804
|
+
console.log();
|
|
7805
|
+
}
|
|
7806
|
+
const order = { error: 0, warn: 1, info: 2 };
|
|
7807
|
+
findings.sort((a, b) => order[a.severity] - order[b.severity] || a.id.localeCompare(b.id));
|
|
7808
|
+
for (const f of findings) {
|
|
7809
|
+
const color = f.severity === "error" ? ui.red : f.severity === "warn" ? ui.yellow : ui.dim;
|
|
7810
|
+
console.log(
|
|
7811
|
+
`${color(f.severity.padEnd(5))} ${ui.dim(f.code)} ${f.id}`
|
|
7812
|
+
);
|
|
7813
|
+
console.log(` ${f.message}`);
|
|
7814
|
+
if (f.suggested_anchors) {
|
|
7815
|
+
const pathHints = f.suggested_anchors.paths.length > 0 ? `paths: ${f.suggested_anchors.paths.join(", ")}` : "";
|
|
7816
|
+
const symbolHints = f.suggested_anchors.symbols.length > 0 ? `symbols: ${f.suggested_anchors.symbols.join(", ")}` : "";
|
|
7817
|
+
console.log(ui.dim(` suggested anchors: ${[pathHints, symbolHints].filter(Boolean).join(" \xB7 ")}`));
|
|
7818
|
+
}
|
|
7819
|
+
console.log(ui.dim(` \u2192 ${f.file}`));
|
|
7820
|
+
}
|
|
7821
|
+
process.exitCode = findings.some((x) => x.severity === "error") ? 1 : 0;
|
|
7822
|
+
});
|
|
7823
|
+
}
|
|
7824
|
+
|
|
7825
|
+
// src/utils/autopilot.ts
|
|
7826
|
+
async function applyAutopilotRepairs(root, paths, options = {}) {
|
|
7827
|
+
const repairs = [];
|
|
7828
|
+
const config = await loadConfig4(paths);
|
|
7829
|
+
if (options.applyConfig) {
|
|
7830
|
+
const changed = await ensureAutopilotConfig(paths, config);
|
|
7831
|
+
if (changed) {
|
|
7832
|
+
repairs.push({
|
|
7833
|
+
code: "autopilot-config",
|
|
7834
|
+
message: "Enabled autopilot defaults in .ai/haive.config.json."
|
|
7835
|
+
});
|
|
7836
|
+
}
|
|
7837
|
+
}
|
|
7838
|
+
const current = await loadConfig4(paths);
|
|
7839
|
+
const autoRepair = current.autoRepair ?? {};
|
|
7840
|
+
if (options.applyContext ?? autoRepair.context ?? current.autopilot) {
|
|
7841
|
+
const changed = await syncProjectContextVersion(root, paths);
|
|
7842
|
+
if (changed) {
|
|
7843
|
+
repairs.push({
|
|
7844
|
+
code: "project-context-version",
|
|
7845
|
+
message: "Updated .ai/project-context.md version metadata from package.json."
|
|
7846
|
+
});
|
|
7847
|
+
}
|
|
7848
|
+
}
|
|
7849
|
+
if (options.applyCorpus ?? autoRepair.corpus ?? current.autopilot) {
|
|
7850
|
+
const report = await lintMemoriesAsync(root, { fix: true, apply: true });
|
|
7851
|
+
const applied = report.fixes.filter((fix) => fix.applied);
|
|
7852
|
+
if (applied.length > 0) {
|
|
7853
|
+
repairs.push({
|
|
7854
|
+
code: "memory-lint-fix",
|
|
7855
|
+
message: `Applied ${applied.length} safe memory lint fix${applied.length === 1 ? "" : "es"}.`
|
|
7856
|
+
});
|
|
7857
|
+
}
|
|
7858
|
+
}
|
|
7859
|
+
if (options.applyCodeMap ?? autoRepair.codeMap ?? current.autopilot) {
|
|
7860
|
+
const refreshed = await refreshCodeMap(root, paths, Boolean(options.forceCodeMap));
|
|
7861
|
+
if (refreshed) {
|
|
7862
|
+
repairs.push({
|
|
7863
|
+
code: "code-map-refresh",
|
|
7864
|
+
message: "Refreshed .ai/code-map.json."
|
|
7865
|
+
});
|
|
7866
|
+
}
|
|
7867
|
+
}
|
|
7868
|
+
if (options.applyCodeSearch ?? autoRepair.codeSearch ?? current.autopilot) {
|
|
7869
|
+
const indexed = await refreshCodeSearchIndex(paths);
|
|
7870
|
+
if (indexed) {
|
|
7871
|
+
repairs.push({
|
|
7872
|
+
code: "code-search-index",
|
|
7873
|
+
message: "Refreshed code-search embeddings index."
|
|
7874
|
+
});
|
|
7875
|
+
}
|
|
7876
|
+
}
|
|
7877
|
+
return repairs;
|
|
7878
|
+
}
|
|
7879
|
+
async function ensureAutopilotConfig(paths, currentConfig) {
|
|
7880
|
+
const current = currentConfig ?? await loadConfig4(paths);
|
|
7881
|
+
const next = {
|
|
7882
|
+
...current,
|
|
7883
|
+
autopilot: true,
|
|
7884
|
+
defaultScope: "team",
|
|
7885
|
+
defaultStatus: "validated",
|
|
7886
|
+
autoApproveDelayHours: current.autoApproveDelayHours ?? AUTOPILOT_DEFAULTS2.autoApproveDelayHours,
|
|
7887
|
+
autoPromoteMinReads: current.autoPromoteMinReads ?? AUTOPILOT_DEFAULTS2.autoPromoteMinReads,
|
|
7888
|
+
autoSessionEnd: true,
|
|
7889
|
+
autoContext: true,
|
|
7890
|
+
autoRepair: {
|
|
7891
|
+
context: true,
|
|
7892
|
+
corpus: true,
|
|
7893
|
+
codeMap: true,
|
|
7894
|
+
codeSearch: current.autoRepair?.codeSearch ?? true
|
|
7895
|
+
},
|
|
7896
|
+
enforcement: {
|
|
7897
|
+
...AUTOPILOT_DEFAULTS2.enforcement,
|
|
7898
|
+
...current.enforcement,
|
|
7899
|
+
mode: "strict",
|
|
7900
|
+
requireBriefingFirst: true,
|
|
7901
|
+
requireSessionRecap: true,
|
|
7902
|
+
requireMemoryVerify: true,
|
|
7903
|
+
blockStaleDecisionChanges: true,
|
|
7904
|
+
requireDecisionCoverage: true,
|
|
7905
|
+
cleanupGeneratedArtifacts: true,
|
|
7906
|
+
toolProfile: current.enforcement?.toolProfile ?? "enforcement"
|
|
7907
|
+
}
|
|
7908
|
+
};
|
|
7909
|
+
if (JSON.stringify(current) === JSON.stringify(next)) return false;
|
|
7910
|
+
await saveConfig2(paths, next);
|
|
7911
|
+
return true;
|
|
7912
|
+
}
|
|
7913
|
+
async function syncProjectContextVersion(root, paths) {
|
|
7914
|
+
const status = await projectContextVersionStatus(root, paths);
|
|
7915
|
+
if (!status.canSync || !status.expectedVersion) return false;
|
|
7916
|
+
const original = await readFile8(paths.projectContext, "utf8");
|
|
7917
|
+
let updated = original.replace(
|
|
7918
|
+
/^# Project context — hAIve \(v[^)]+\)$/m,
|
|
7919
|
+
`# Project context \u2014 hAIve (v${status.expectedVersion})`
|
|
7920
|
+
).replace(
|
|
7921
|
+
/> \*\*Current version\*\*: [^—\n]+—/m,
|
|
7922
|
+
`> **Current version**: ${status.expectedVersion} \u2014`
|
|
7923
|
+
);
|
|
7924
|
+
if (updated === original && !original.includes("Current version")) {
|
|
7925
|
+
updated = original.replace(
|
|
7926
|
+
/^(> Repo-native context enforcement[^\n]*\n)/m,
|
|
7927
|
+
`$1> **Current version**: ${status.expectedVersion} \u2014 @hiveai/core, cli, mcp, embeddings are versioned together.
|
|
7928
|
+
`
|
|
7929
|
+
);
|
|
7930
|
+
}
|
|
7931
|
+
if (updated === original) return false;
|
|
7932
|
+
await writeFile14(paths.projectContext, updated, "utf8");
|
|
7933
|
+
return true;
|
|
7934
|
+
}
|
|
7935
|
+
async function projectContextVersionStatus(root, paths) {
|
|
7936
|
+
if (!existsSync30(paths.projectContext)) {
|
|
7937
|
+
return { mismatch: false, canSync: false };
|
|
7938
|
+
}
|
|
7939
|
+
const packagePath = path14.join(root, "package.json");
|
|
7940
|
+
if (!existsSync30(packagePath)) {
|
|
7941
|
+
return { mismatch: false, canSync: false };
|
|
7942
|
+
}
|
|
7943
|
+
const packageJson = JSON.parse(await readFile8(packagePath, "utf8"));
|
|
7944
|
+
const expectedVersion = packageJson.version;
|
|
7945
|
+
if (!expectedVersion) {
|
|
7946
|
+
return { mismatch: false, canSync: false };
|
|
7947
|
+
}
|
|
7948
|
+
const content = await readFile8(paths.projectContext, "utf8");
|
|
7949
|
+
const headingVersion = content.match(/^# Project context — hAIve \(v([^)]+)\)$/m)?.[1];
|
|
7950
|
+
const currentLineVersion = content.match(/^> \*\*Current version\*\*: ([^—\n]+)—/m)?.[1]?.trim();
|
|
7951
|
+
const currentVersion = currentLineVersion ?? headingVersion;
|
|
7952
|
+
return {
|
|
7953
|
+
expectedVersion,
|
|
7954
|
+
currentVersion,
|
|
7955
|
+
mismatch: currentVersion !== expectedVersion,
|
|
7956
|
+
canSync: true
|
|
7957
|
+
};
|
|
7958
|
+
}
|
|
7959
|
+
async function refreshCodeMap(root, paths, force) {
|
|
7960
|
+
if (!force) {
|
|
7961
|
+
const existing = await loadCodeMap5(paths);
|
|
7962
|
+
if (existing) return false;
|
|
7963
|
+
}
|
|
7964
|
+
const map = await buildCodeMap3(root, {
|
|
7965
|
+
excludeDirs: [
|
|
7966
|
+
"node_modules",
|
|
7967
|
+
"dist",
|
|
7968
|
+
"build",
|
|
7969
|
+
"out",
|
|
7970
|
+
".git",
|
|
7971
|
+
".next",
|
|
7972
|
+
".turbo",
|
|
7973
|
+
".vitest-cache",
|
|
7974
|
+
"coverage"
|
|
7975
|
+
]
|
|
7976
|
+
});
|
|
7977
|
+
await saveCodeMap3(paths, map);
|
|
7978
|
+
return true;
|
|
7979
|
+
}
|
|
7980
|
+
async function refreshCodeSearchIndex(paths) {
|
|
7981
|
+
try {
|
|
7982
|
+
const mod = await import("@hiveai/embeddings");
|
|
7983
|
+
const embedder = await mod.Embedder.create();
|
|
7984
|
+
const { report } = await mod.rebuildCodeIndex(paths, embedder);
|
|
7985
|
+
return report.added > 0 || report.updated > 0 || report.removed > 0;
|
|
7986
|
+
} catch {
|
|
7987
|
+
return false;
|
|
7988
|
+
}
|
|
7989
|
+
}
|
|
7990
|
+
|
|
7991
|
+
// src/commands/sync.ts
|
|
7992
|
+
var BRIDGE_START = "<!-- haive:memories-start -->";
|
|
7993
|
+
var BRIDGE_END = "<!-- haive:memories-end -->";
|
|
7994
|
+
function registerSync(program2) {
|
|
7995
|
+
program2.command("sync").description(
|
|
7996
|
+
"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"
|
|
7997
|
+
).option("-d, --dir <dir>", "project root").option("--quiet", "minimal output (suitable for git hooks)").option(
|
|
7998
|
+
"--since <ref>",
|
|
7999
|
+
"git ref/commit to compare against; report memories added/modified/removed since"
|
|
8000
|
+
).option("--no-verify", "skip the anchor verification step").option("--no-promote", "skip the auto-promotion step").option(
|
|
8001
|
+
"--inject-bridge",
|
|
8002
|
+
"inject top validated memories into CLAUDE.md (or --bridge-file) between <!-- haive:memories-start/end --> markers"
|
|
8003
|
+
).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) => {
|
|
8004
|
+
const root = findProjectRoot12(opts.dir);
|
|
8005
|
+
const paths = resolveHaivePaths9(root);
|
|
8006
|
+
if (!existsSync31(paths.memoriesDir)) {
|
|
8007
|
+
if (!opts.quiet) ui.warn(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
8008
|
+
process.exitCode = 1;
|
|
8009
|
+
return;
|
|
8010
|
+
}
|
|
8011
|
+
const log = (msg) => {
|
|
8012
|
+
if (!opts.quiet) console.log(msg);
|
|
8013
|
+
};
|
|
8014
|
+
const config = await loadConfig5(paths);
|
|
8015
|
+
const autoApproveDelayHours = config.autoApproveDelayHours ?? null;
|
|
8016
|
+
const autoPromoteMinReads = config.autoPromoteMinReads ?? DEFAULT_AUTO_PROMOTE_RULE2.minReads;
|
|
8017
|
+
const autoRepair = config.autoRepair ?? {};
|
|
8018
|
+
let staleMarked = 0;
|
|
8019
|
+
let revalidated = 0;
|
|
8020
|
+
let promoted = 0;
|
|
8021
|
+
let autoApproved = 0;
|
|
8022
|
+
if (opts.verify !== false) {
|
|
8023
|
+
const memories = await loadMemoriesFromDir24(paths.memoriesDir);
|
|
8024
|
+
for (const { memory: memory2, filePath } of memories) {
|
|
8025
|
+
if (memory2.frontmatter.type === "session_recap") {
|
|
8026
|
+
if (memory2.frontmatter.status === "stale") {
|
|
8027
|
+
await writeFile15(
|
|
8028
|
+
filePath,
|
|
8029
|
+
serializeMemory12({
|
|
8030
|
+
frontmatter: {
|
|
8031
|
+
...memory2.frontmatter,
|
|
8032
|
+
status: "validated",
|
|
8033
|
+
stale_reason: null,
|
|
8034
|
+
verified_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8035
|
+
},
|
|
8036
|
+
body: memory2.body
|
|
8037
|
+
}),
|
|
8038
|
+
"utf8"
|
|
8039
|
+
);
|
|
8040
|
+
revalidated++;
|
|
8041
|
+
}
|
|
8042
|
+
continue;
|
|
8043
|
+
}
|
|
8044
|
+
const isAnchored = memory2.frontmatter.anchor.paths.length > 0 || memory2.frontmatter.anchor.symbols.length > 0;
|
|
8045
|
+
if (!isAnchored) continue;
|
|
8046
|
+
const result = await verifyAnchor2(memory2, { projectRoot: root });
|
|
8047
|
+
const verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8048
|
+
if (result.stale) {
|
|
8049
|
+
if (memory2.frontmatter.status !== "stale") {
|
|
8050
|
+
await writeFile15(
|
|
8051
|
+
filePath,
|
|
8052
|
+
serializeMemory12({
|
|
8053
|
+
frontmatter: {
|
|
8054
|
+
...memory2.frontmatter,
|
|
8055
|
+
status: "stale",
|
|
8056
|
+
verified_at: verifiedAt,
|
|
8057
|
+
stale_reason: result.reason
|
|
8058
|
+
},
|
|
8059
|
+
body: memory2.body
|
|
8060
|
+
}),
|
|
8061
|
+
"utf8"
|
|
8062
|
+
);
|
|
8063
|
+
staleMarked++;
|
|
8064
|
+
}
|
|
8065
|
+
} else if (memory2.frontmatter.status === "stale") {
|
|
8066
|
+
await writeFile15(
|
|
8067
|
+
filePath,
|
|
8068
|
+
serializeMemory12({
|
|
8069
|
+
frontmatter: {
|
|
8070
|
+
...memory2.frontmatter,
|
|
8071
|
+
status: "validated",
|
|
8072
|
+
verified_at: verifiedAt,
|
|
8073
|
+
stale_reason: null
|
|
8074
|
+
},
|
|
8075
|
+
body: memory2.body
|
|
8076
|
+
}),
|
|
8077
|
+
"utf8"
|
|
8078
|
+
);
|
|
8079
|
+
revalidated++;
|
|
8080
|
+
}
|
|
8081
|
+
}
|
|
8082
|
+
}
|
|
8083
|
+
if (opts.promote !== false) {
|
|
8084
|
+
const memories = await loadMemoriesFromDir24(paths.memoriesDir);
|
|
8085
|
+
const usage = await loadUsageIndex13(paths);
|
|
8086
|
+
const nowMs = Date.now();
|
|
8087
|
+
for (const { memory: memory2, filePath } of memories) {
|
|
8088
|
+
const fm = memory2.frontmatter;
|
|
8089
|
+
if (fm.type === "session_recap") continue;
|
|
8090
|
+
if (isAutoPromoteEligible2(fm, getUsage11(usage, fm.id), {
|
|
8091
|
+
minReads: autoPromoteMinReads,
|
|
8092
|
+
maxRejections: DEFAULT_AUTO_PROMOTE_RULE2.maxRejections
|
|
8093
|
+
})) {
|
|
8094
|
+
await writeFile15(
|
|
8095
|
+
filePath,
|
|
8096
|
+
serializeMemory12({ frontmatter: { ...fm, status: "validated" }, body: memory2.body }),
|
|
8097
|
+
"utf8"
|
|
8098
|
+
);
|
|
8099
|
+
promoted++;
|
|
8100
|
+
continue;
|
|
8101
|
+
}
|
|
8102
|
+
if (autoApproveDelayHours !== null && fm.status === "proposed" && fm.scope === "team") {
|
|
8103
|
+
const ageHours = (nowMs - new Date(fm.created_at).getTime()) / (1e3 * 60 * 60);
|
|
8104
|
+
if (ageHours >= autoApproveDelayHours) {
|
|
8105
|
+
await writeFile15(
|
|
8106
|
+
filePath,
|
|
8107
|
+
serializeMemory12({
|
|
8108
|
+
frontmatter: {
|
|
8109
|
+
...fm,
|
|
8110
|
+
status: "validated",
|
|
8111
|
+
verified_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8112
|
+
},
|
|
8113
|
+
body: memory2.body
|
|
8114
|
+
}),
|
|
8115
|
+
"utf8"
|
|
8116
|
+
);
|
|
8117
|
+
autoApproved++;
|
|
8118
|
+
}
|
|
8119
|
+
}
|
|
8120
|
+
}
|
|
8121
|
+
}
|
|
8122
|
+
if (config.autopilot || autoRepair.context || autoRepair.corpus) {
|
|
8123
|
+
const repairs = await applyAutopilotRepairs(root, paths, {
|
|
8124
|
+
applyContext: autoRepair.context ?? config.autopilot,
|
|
8125
|
+
applyCorpus: autoRepair.corpus ?? config.autopilot,
|
|
8126
|
+
applyCodeMap: false,
|
|
8127
|
+
applyCodeSearch: false
|
|
8128
|
+
});
|
|
8129
|
+
for (const repair of repairs) log(ui.dim(`autopilot: ${repair.message}`));
|
|
8130
|
+
}
|
|
8131
|
+
const sinceReport = opts.since ? collectSinceChanges(root, opts.since) : null;
|
|
8132
|
+
const draftMemories = (await loadMemoriesFromDir24(paths.memoriesDir)).filter(
|
|
8133
|
+
(m) => m.memory.frontmatter.status === "draft"
|
|
8134
|
+
);
|
|
8135
|
+
const draftCount = draftMemories.length;
|
|
8136
|
+
const autoApprovedNote = autoApproved > 0 ? ` \xB7 ${autoApproved} auto-approved` : "";
|
|
8137
|
+
log(
|
|
8138
|
+
`${ui.dim("sync:")} ${staleMarked} stale \xB7 ${revalidated} revalidated \xB7 ${promoted} promoted${autoApprovedNote}${sinceReport ? ` \xB7 ${sinceReport.added.length}+/${sinceReport.modified.length}~/${sinceReport.removed.length}- since ${opts.since}` : ""}`
|
|
8139
|
+
);
|
|
8140
|
+
if (!opts.quiet && draftCount > 0) {
|
|
8141
|
+
log(
|
|
8142
|
+
ui.dim(
|
|
8143
|
+
`\u2139 ${draftCount} memor${draftCount === 1 ? "y" : "ies"} in draft \u2014 run \`haive memory approve <id>\` to activate or \`haive memory list --status draft\` to review`
|
|
8144
|
+
)
|
|
7653
8145
|
);
|
|
7654
8146
|
}
|
|
7655
8147
|
if (opts.injectBridge) {
|
|
7656
|
-
const bridgeFile = opts.bridgeFile ?
|
|
8148
|
+
const bridgeFile = opts.bridgeFile ? path15.resolve(opts.bridgeFile) : path15.join(root, "CLAUDE.md");
|
|
7657
8149
|
const maxInject = Math.max(1, Number(opts.bridgeMaxMemories ?? 5));
|
|
7658
8150
|
await injectBridge(bridgeFile, paths.memoriesDir, maxInject, root, opts.quiet);
|
|
7659
8151
|
}
|
|
@@ -7672,12 +8164,12 @@ function registerSync(program2) {
|
|
|
7672
8164
|
}
|
|
7673
8165
|
}
|
|
7674
8166
|
if (!opts.quiet) {
|
|
7675
|
-
const allForDecay = await
|
|
7676
|
-
const usageForDecay = await
|
|
8167
|
+
const allForDecay = await loadMemoriesFromDir24(paths.memoriesDir);
|
|
8168
|
+
const usageForDecay = await loadUsageIndex13(paths);
|
|
7677
8169
|
const decaying = allForDecay.filter(({ memory: memory2 }) => {
|
|
7678
8170
|
const fm = memory2.frontmatter;
|
|
7679
8171
|
if (fm.status === "rejected" || fm.status === "deprecated" || fm.status === "stale") return false;
|
|
7680
|
-
const u =
|
|
8172
|
+
const u = getUsage11(usageForDecay, fm.id);
|
|
7681
8173
|
return isDecaying2(u, fm.created_at);
|
|
7682
8174
|
});
|
|
7683
8175
|
if (decaying.length > 0) {
|
|
@@ -7759,11 +8251,11 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7759
8251
|
paths: [result.file],
|
|
7760
8252
|
topic: `dep-bump-${slugParts}`
|
|
7761
8253
|
});
|
|
7762
|
-
const teamDir =
|
|
8254
|
+
const teamDir = path15.join(paths.memoriesDir, "team");
|
|
7763
8255
|
await mkdir10(teamDir, { recursive: true });
|
|
7764
|
-
await
|
|
7765
|
-
|
|
7766
|
-
|
|
8256
|
+
await writeFile15(
|
|
8257
|
+
path15.join(teamDir, `${fm.id}.md`),
|
|
8258
|
+
serializeMemory12({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
7767
8259
|
"utf8"
|
|
7768
8260
|
);
|
|
7769
8261
|
log(ui.yellow(` \u2192 memory created: ${fm.id}`));
|
|
@@ -7826,11 +8318,11 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7826
8318
|
paths: [diff.file],
|
|
7827
8319
|
topic: `contract-breaking-${diff.contract}`
|
|
7828
8320
|
});
|
|
7829
|
-
const teamDir =
|
|
8321
|
+
const teamDir = path15.join(paths.memoriesDir, "team");
|
|
7830
8322
|
await mkdir10(teamDir, { recursive: true });
|
|
7831
|
-
await
|
|
7832
|
-
|
|
7833
|
-
|
|
8323
|
+
await writeFile15(
|
|
8324
|
+
path15.join(teamDir, `${fm.id}.md`),
|
|
8325
|
+
serializeMemory12({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
7834
8326
|
"utf8"
|
|
7835
8327
|
);
|
|
7836
8328
|
log(ui.yellow(` \u2192 memory created: ${fm.id}`));
|
|
@@ -7840,10 +8332,19 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7840
8332
|
ui.warn(`contract watcher failed: ${String(err)}`);
|
|
7841
8333
|
}
|
|
7842
8334
|
}
|
|
7843
|
-
const existingMap = await
|
|
7844
|
-
if (existingMap) {
|
|
8335
|
+
const existingMap = await loadCodeMap6(paths);
|
|
8336
|
+
if (!existingMap && (config.autopilot || autoRepair.codeMap)) {
|
|
8337
|
+
try {
|
|
8338
|
+
const { buildCodeMap: buildCodeMap4, saveCodeMap: saveCodeMap4 } = await import("@hiveai/core");
|
|
8339
|
+
log(ui.dim("code-map: missing \u2014 building index\u2026"));
|
|
8340
|
+
const newMap = await buildCodeMap4(root);
|
|
8341
|
+
await saveCodeMap4(paths, newMap);
|
|
8342
|
+
log(ui.dim(`code-map: built (${Object.keys(newMap.files).length} files)`));
|
|
8343
|
+
} catch {
|
|
8344
|
+
}
|
|
8345
|
+
} else if (existingMap) {
|
|
7845
8346
|
const mapAge = new Date(existingMap.generated_at).getTime();
|
|
7846
|
-
const gitResult =
|
|
8347
|
+
const gitResult = spawnSync4(
|
|
7847
8348
|
"git",
|
|
7848
8349
|
[
|
|
7849
8350
|
"diff",
|
|
@@ -7868,22 +8369,32 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7868
8369
|
const changedSourceFiles = (gitResult.stdout ?? "").trim();
|
|
7869
8370
|
if (changedSourceFiles.length > 0) {
|
|
7870
8371
|
try {
|
|
7871
|
-
const { buildCodeMap:
|
|
8372
|
+
const { buildCodeMap: buildCodeMap4, saveCodeMap: saveCodeMap4 } = await import("@hiveai/core");
|
|
7872
8373
|
log(ui.dim("code-map: source files changed \u2014 refreshing index\u2026"));
|
|
7873
|
-
const newMap = await
|
|
7874
|
-
await
|
|
8374
|
+
const newMap = await buildCodeMap4(root);
|
|
8375
|
+
await saveCodeMap4(paths, newMap);
|
|
7875
8376
|
log(ui.dim(`code-map: refreshed (${Object.keys(newMap.files).length} files)`));
|
|
7876
8377
|
} catch {
|
|
7877
8378
|
}
|
|
7878
8379
|
}
|
|
7879
8380
|
}
|
|
7880
|
-
if (opts.embed) {
|
|
8381
|
+
if (opts.embed || autoRepair.codeSearch) {
|
|
7881
8382
|
try {
|
|
7882
|
-
const { Embedder, rebuildIndex } = await import("@hiveai/embeddings");
|
|
8383
|
+
const { Embedder, rebuildCodeIndex, rebuildIndex } = await import("@hiveai/embeddings");
|
|
7883
8384
|
log(ui.dim("embed: rebuilding index\u2026"));
|
|
7884
8385
|
const embedder = await Embedder.create();
|
|
7885
8386
|
const { report } = await rebuildIndex(paths, embedder);
|
|
7886
|
-
|
|
8387
|
+
const { report: codeReport } = await rebuildCodeIndex(paths, embedder);
|
|
8388
|
+
log(
|
|
8389
|
+
ui.dim(
|
|
8390
|
+
`embed: memory index rebuilt (${report.added} added, ${report.updated} updated, ${report.removed} removed)`
|
|
8391
|
+
)
|
|
8392
|
+
);
|
|
8393
|
+
log(
|
|
8394
|
+
ui.dim(
|
|
8395
|
+
`embed: code index rebuilt (${codeReport.total} symbols, ${codeReport.added} added, ${codeReport.updated} updated, ${codeReport.removed} removed)`
|
|
8396
|
+
)
|
|
8397
|
+
);
|
|
7887
8398
|
} catch {
|
|
7888
8399
|
ui.warn("--embed: @hiveai/embeddings not available or index build failed. Run `haive embeddings index` manually.");
|
|
7889
8400
|
}
|
|
@@ -7891,8 +8402,8 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7891
8402
|
});
|
|
7892
8403
|
}
|
|
7893
8404
|
async function injectBridge(bridgeFile, memoriesDir, maxMemories, root, quiet) {
|
|
7894
|
-
if (!
|
|
7895
|
-
const all = await
|
|
8405
|
+
if (!existsSync31(memoriesDir)) return;
|
|
8406
|
+
const all = await loadMemoriesFromDir24(memoriesDir);
|
|
7896
8407
|
const top = all.filter(({ memory: memory2 }) => {
|
|
7897
8408
|
const s = memory2.frontmatter.status;
|
|
7898
8409
|
if (memory2.frontmatter.type === "session_recap") return false;
|
|
@@ -7916,17 +8427,17 @@ ${m.memory.body.trim()}`;
|
|
|
7916
8427
|
` + block + `
|
|
7917
8428
|
|
|
7918
8429
|
${BRIDGE_END}`;
|
|
7919
|
-
const fileExists =
|
|
7920
|
-
let existing = fileExists ? await
|
|
8430
|
+
const fileExists = existsSync31(bridgeFile);
|
|
8431
|
+
let existing = fileExists ? await readFile9(bridgeFile, "utf8") : "";
|
|
7921
8432
|
existing = existing.replace(/\r\n/g, "\n");
|
|
7922
8433
|
const startIdx = existing.indexOf(BRIDGE_START);
|
|
7923
8434
|
const endIdx = existing.indexOf(BRIDGE_END);
|
|
7924
8435
|
if (startIdx !== -1 && endIdx === -1) {
|
|
7925
|
-
ui.warn(`${
|
|
8436
|
+
ui.warn(`${path15.relative(root, bridgeFile)}: found ${BRIDGE_START} without ${BRIDGE_END}. Fix the file manually before running --inject-bridge.`);
|
|
7926
8437
|
return;
|
|
7927
8438
|
}
|
|
7928
8439
|
if (startIdx === -1 && endIdx !== -1) {
|
|
7929
|
-
ui.warn(`${
|
|
8440
|
+
ui.warn(`${path15.relative(root, bridgeFile)}: found ${BRIDGE_END} without ${BRIDGE_START}. Fix the file manually before running --inject-bridge.`);
|
|
7930
8441
|
return;
|
|
7931
8442
|
}
|
|
7932
8443
|
let updated;
|
|
@@ -7934,19 +8445,19 @@ ${BRIDGE_END}`;
|
|
|
7934
8445
|
updated = existing.slice(0, startIdx) + injected + existing.slice(endIdx + BRIDGE_END.length);
|
|
7935
8446
|
} else {
|
|
7936
8447
|
if (!fileExists && !quiet) {
|
|
7937
|
-
ui.info(`Creating ${
|
|
8448
|
+
ui.info(`Creating ${path15.relative(root, bridgeFile)} with haive memory block.`);
|
|
7938
8449
|
}
|
|
7939
8450
|
updated = existing + (existing.endsWith("\n") ? "" : "\n") + "\n" + injected + "\n";
|
|
7940
8451
|
}
|
|
7941
|
-
await
|
|
8452
|
+
await writeFile15(bridgeFile, updated, "utf8");
|
|
7942
8453
|
if (!quiet) {
|
|
7943
8454
|
console.log(
|
|
7944
|
-
ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${
|
|
8455
|
+
ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${path15.relative(root, bridgeFile)}`)
|
|
7945
8456
|
);
|
|
7946
8457
|
}
|
|
7947
8458
|
}
|
|
7948
8459
|
function collectSinceChanges(root, ref) {
|
|
7949
|
-
const result =
|
|
8460
|
+
const result = spawnSync4(
|
|
7950
8461
|
"git",
|
|
7951
8462
|
["-C", root, "diff", "--name-status", "--diff-filter=AMD", `${ref}...HEAD`, "--", ".ai/memories"],
|
|
7952
8463
|
{ encoding: "utf8" }
|
|
@@ -7966,18 +8477,19 @@ function collectSinceChanges(root, ref) {
|
|
|
7966
8477
|
|
|
7967
8478
|
// src/commands/memory-add.ts
|
|
7968
8479
|
import { createHash as createHash2 } from "crypto";
|
|
7969
|
-
import { mkdir as mkdir11, readFile as
|
|
7970
|
-
import { existsSync as
|
|
7971
|
-
import
|
|
8480
|
+
import { mkdir as mkdir11, readFile as readFile10, writeFile as writeFile16 } from "fs/promises";
|
|
8481
|
+
import { existsSync as existsSync33 } from "fs";
|
|
8482
|
+
import path16 from "path";
|
|
7972
8483
|
import "commander";
|
|
7973
8484
|
import {
|
|
7974
8485
|
buildFrontmatter as buildFrontmatter7,
|
|
7975
|
-
findProjectRoot as
|
|
8486
|
+
findProjectRoot as findProjectRoot13,
|
|
7976
8487
|
inferModulesFromPaths as inferModulesFromPaths3,
|
|
7977
|
-
|
|
8488
|
+
loadConfig as loadConfig6,
|
|
8489
|
+
loadMemoriesFromDir as loadMemoriesFromDir25,
|
|
7978
8490
|
memoryFilePath as memoryFilePath6,
|
|
7979
|
-
resolveHaivePaths as
|
|
7980
|
-
serializeMemory as
|
|
8491
|
+
resolveHaivePaths as resolveHaivePaths10,
|
|
8492
|
+
serializeMemory as serializeMemory13
|
|
7981
8493
|
} from "@hiveai/core";
|
|
7982
8494
|
function registerMemoryAdd(memory2) {
|
|
7983
8495
|
memory2.command("add").description(
|
|
@@ -8003,21 +8515,22 @@ function registerMemoryAdd(memory2) {
|
|
|
8003
8515
|
haive memory add --type convention --slug flyway-no-modify --topic flyway \\\\
|
|
8004
8516
|
--scope team --body "Never modify existing migrations. Create V{n+1}__desc.sql."
|
|
8005
8517
|
`
|
|
8006
|
-
).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:
|
|
8007
|
-
const root =
|
|
8008
|
-
const paths =
|
|
8009
|
-
if (!
|
|
8518
|
+
).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) => {
|
|
8519
|
+
const root = findProjectRoot13(opts.dir);
|
|
8520
|
+
const paths = resolveHaivePaths10(root);
|
|
8521
|
+
if (!existsSync33(paths.haiveDir)) {
|
|
8010
8522
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
8011
8523
|
process.exitCode = 1;
|
|
8012
8524
|
return;
|
|
8013
8525
|
}
|
|
8526
|
+
const config = await loadConfig6(paths);
|
|
8014
8527
|
const userTags = parseCsv2(opts.tags);
|
|
8015
8528
|
const anchorPaths = parseCsv2(opts.paths);
|
|
8016
8529
|
const autoTagsEnabled = opts.autoTag !== false;
|
|
8017
8530
|
const inferredTags = autoTagsEnabled ? inferModulesFromPaths3(anchorPaths) : [];
|
|
8018
8531
|
const mergedTags = Array.from(/* @__PURE__ */ new Set([...userTags, ...inferredTags]));
|
|
8019
8532
|
if (anchorPaths.length > 0) {
|
|
8020
|
-
const missing = anchorPaths.filter((p) => !
|
|
8533
|
+
const missing = anchorPaths.filter((p) => !existsSync33(path16.resolve(root, p)));
|
|
8021
8534
|
if (missing.length > 0) {
|
|
8022
8535
|
ui.warn(`Anchor path${missing.length > 1 ? "s" : ""} not found in project:`);
|
|
8023
8536
|
for (const p of missing) ui.warn(` \u2717 ${p}`);
|
|
@@ -8029,12 +8542,12 @@ function registerMemoryAdd(memory2) {
|
|
|
8029
8542
|
const title = opts.title ?? opts.slug;
|
|
8030
8543
|
let body;
|
|
8031
8544
|
if (opts.bodyFile !== void 0) {
|
|
8032
|
-
if (!
|
|
8545
|
+
if (!existsSync33(opts.bodyFile)) {
|
|
8033
8546
|
ui.error(`--body-file not found: ${opts.bodyFile}`);
|
|
8034
8547
|
process.exitCode = 1;
|
|
8035
8548
|
return;
|
|
8036
8549
|
}
|
|
8037
|
-
const fileContent = await
|
|
8550
|
+
const fileContent = await readFile10(opts.bodyFile, "utf8");
|
|
8038
8551
|
body = opts.title ? `# ${opts.title}
|
|
8039
8552
|
|
|
8040
8553
|
${fileContent.trim()}
|
|
@@ -8049,10 +8562,10 @@ ${opts.body}` : opts.body;
|
|
|
8049
8562
|
TODO \u2014 write the memory body.
|
|
8050
8563
|
`;
|
|
8051
8564
|
}
|
|
8052
|
-
const scope = opts.scope ?? "personal";
|
|
8053
|
-
if (
|
|
8565
|
+
const scope = opts.scope ?? config.defaultScope ?? "personal";
|
|
8566
|
+
if (existsSync33(paths.memoriesDir)) {
|
|
8054
8567
|
const incomingHash = createHash2("sha256").update(body.trim()).digest("hex").slice(0, 12);
|
|
8055
|
-
const allForHash = await
|
|
8568
|
+
const allForHash = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
8056
8569
|
const hashDup = allForHash.find(
|
|
8057
8570
|
({ memory: memory3 }) => createHash2("sha256").update(memory3.body.trim()).digest("hex").slice(0, 12) === incomingHash && memory3.frontmatter.scope === scope
|
|
8058
8571
|
);
|
|
@@ -8063,8 +8576,8 @@ TODO \u2014 write the memory body.
|
|
|
8063
8576
|
return;
|
|
8064
8577
|
}
|
|
8065
8578
|
}
|
|
8066
|
-
if (opts.topic &&
|
|
8067
|
-
const existing = await
|
|
8579
|
+
if (opts.topic && existsSync33(paths.memoriesDir)) {
|
|
8580
|
+
const existing = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
8068
8581
|
const topicMatch = existing.find(
|
|
8069
8582
|
({ memory: memory3 }) => memory3.frontmatter.topic === opts.topic && memory3.frontmatter.scope === scope && (!opts.module || memory3.frontmatter.module === opts.module)
|
|
8070
8583
|
);
|
|
@@ -8081,8 +8594,8 @@ TODO \u2014 write the memory body.
|
|
|
8081
8594
|
symbols: parseCsv2(opts.symbols).length ? parseCsv2(opts.symbols) : fm.anchor.symbols
|
|
8082
8595
|
}
|
|
8083
8596
|
};
|
|
8084
|
-
await
|
|
8085
|
-
ui.success(`Updated (topic upsert) ${
|
|
8597
|
+
await writeFile16(topicMatch.filePath, serializeMemory13({ frontmatter: newFrontmatter, body }), "utf8");
|
|
8598
|
+
ui.success(`Updated (topic upsert) ${path16.relative(root, topicMatch.filePath)}`);
|
|
8086
8599
|
ui.info(`id=${fm.id} revision=${revisionCount}`);
|
|
8087
8600
|
return;
|
|
8088
8601
|
}
|
|
@@ -8098,17 +8611,18 @@ TODO \u2014 write the memory body.
|
|
|
8098
8611
|
paths: anchorPaths,
|
|
8099
8612
|
symbols: parseCsv2(opts.symbols),
|
|
8100
8613
|
commit: opts.commit,
|
|
8101
|
-
topic: opts.topic
|
|
8614
|
+
topic: opts.topic,
|
|
8615
|
+
status: config.defaultStatus === "validated" ? "validated" : void 0
|
|
8102
8616
|
});
|
|
8103
8617
|
const file = memoryFilePath6(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
8104
|
-
await mkdir11(
|
|
8105
|
-
if (
|
|
8618
|
+
await mkdir11(path16.dirname(file), { recursive: true });
|
|
8619
|
+
if (existsSync33(file)) {
|
|
8106
8620
|
ui.error(`Memory already exists at ${file}`);
|
|
8107
8621
|
process.exitCode = 1;
|
|
8108
8622
|
return;
|
|
8109
8623
|
}
|
|
8110
|
-
if (
|
|
8111
|
-
const existing = await
|
|
8624
|
+
if (existsSync33(paths.memoriesDir)) {
|
|
8625
|
+
const existing = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
8112
8626
|
const slugTokens = opts.slug.toLowerCase().split(/[-_\s]+/).filter(Boolean);
|
|
8113
8627
|
const similar = existing.filter(({ memory: memory3 }) => {
|
|
8114
8628
|
const id = memory3.frontmatter.id.toLowerCase();
|
|
@@ -8119,8 +8633,8 @@ TODO \u2014 write the memory body.
|
|
|
8119
8633
|
ui.warn("Consider updating one of these with `haive memory update` instead.");
|
|
8120
8634
|
}
|
|
8121
8635
|
}
|
|
8122
|
-
await
|
|
8123
|
-
ui.success(`Created ${
|
|
8636
|
+
await writeFile16(file, serializeMemory13({ frontmatter, body }), "utf8");
|
|
8637
|
+
ui.success(`Created ${path16.relative(root, file)}`);
|
|
8124
8638
|
ui.info(`id=${frontmatter.id} scope=${frontmatter.scope} status=${frontmatter.status}`);
|
|
8125
8639
|
if (inferredTags.length > 0) {
|
|
8126
8640
|
ui.info(`auto-tagged: ${inferredTags.join(", ")} (use --no-auto-tag to disable)`);
|
|
@@ -8131,7 +8645,9 @@ TODO \u2014 write the memory body.
|
|
|
8131
8645
|
Add file anchors: haive memory update ${frontmatter.id} --paths <file1,file2>`
|
|
8132
8646
|
);
|
|
8133
8647
|
}
|
|
8134
|
-
if (
|
|
8648
|
+
if (frontmatter.status === "validated") {
|
|
8649
|
+
console.log(ui.dim("\u2192 autopilot: memory is already validated and active"));
|
|
8650
|
+
} else if (scope === "personal") {
|
|
8135
8651
|
console.log(
|
|
8136
8652
|
ui.dim(
|
|
8137
8653
|
`\u2192 next: haive memory approve ${frontmatter.id} (activate) | haive memory promote ${frontmatter.id} (share with team)`
|
|
@@ -8150,14 +8666,14 @@ function parseCsv2(value) {
|
|
|
8150
8666
|
}
|
|
8151
8667
|
|
|
8152
8668
|
// src/commands/memory-list.ts
|
|
8153
|
-
import { existsSync as
|
|
8154
|
-
import
|
|
8669
|
+
import { existsSync as existsSync34 } from "fs";
|
|
8670
|
+
import path17 from "path";
|
|
8155
8671
|
import "commander";
|
|
8156
|
-
import { findProjectRoot as
|
|
8672
|
+
import { findProjectRoot as findProjectRoot14, resolveHaivePaths as resolveHaivePaths11 } from "@hiveai/core";
|
|
8157
8673
|
|
|
8158
8674
|
// src/utils/fs.ts
|
|
8159
8675
|
import {
|
|
8160
|
-
loadMemoriesFromDir as
|
|
8676
|
+
loadMemoriesFromDir as loadMemoriesFromDir26,
|
|
8161
8677
|
loadMemory,
|
|
8162
8678
|
listMarkdownFilesRecursive
|
|
8163
8679
|
} from "@hiveai/core";
|
|
@@ -8165,14 +8681,14 @@ import {
|
|
|
8165
8681
|
// src/commands/memory-list.ts
|
|
8166
8682
|
function registerMemoryList(memory2) {
|
|
8167
8683
|
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) => {
|
|
8168
|
-
const root =
|
|
8169
|
-
const paths =
|
|
8170
|
-
if (!
|
|
8684
|
+
const root = findProjectRoot14(opts.dir);
|
|
8685
|
+
const paths = resolveHaivePaths11(root);
|
|
8686
|
+
if (!existsSync34(paths.memoriesDir)) {
|
|
8171
8687
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
8172
8688
|
process.exitCode = 1;
|
|
8173
8689
|
return;
|
|
8174
8690
|
}
|
|
8175
|
-
const all = await
|
|
8691
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8176
8692
|
const statusFilter = opts.status ? opts.status.split(",").map((s) => s.trim()) : null;
|
|
8177
8693
|
const filtered = all.filter((m) => {
|
|
8178
8694
|
if (!matchesFilters(m, opts)) return false;
|
|
@@ -8199,7 +8715,7 @@ function registerMemoryList(memory2) {
|
|
|
8199
8715
|
console.log(
|
|
8200
8716
|
`${ui.bold(fm.id)} ${ui.dim(fm.scope)}/${ui.dim(fm.type)} ${statusBadge}${moduleStr}${tagStr}`
|
|
8201
8717
|
);
|
|
8202
|
-
console.log(` ${ui.dim(
|
|
8718
|
+
console.log(` ${ui.dim(path17.relative(root, filePath))}`);
|
|
8203
8719
|
}
|
|
8204
8720
|
console.log(ui.dim(`
|
|
8205
8721
|
${filtered.length} memor${filtered.length === 1 ? "y" : "ies"}`));
|
|
@@ -8234,26 +8750,26 @@ function matchesFilters(loaded, opts) {
|
|
|
8234
8750
|
}
|
|
8235
8751
|
|
|
8236
8752
|
// src/commands/memory-promote.ts
|
|
8237
|
-
import { mkdir as mkdir12, unlink as unlink2, writeFile as
|
|
8238
|
-
import { existsSync as
|
|
8239
|
-
import
|
|
8753
|
+
import { mkdir as mkdir12, unlink as unlink2, writeFile as writeFile17 } from "fs/promises";
|
|
8754
|
+
import { existsSync as existsSync35 } from "fs";
|
|
8755
|
+
import path18 from "path";
|
|
8240
8756
|
import "commander";
|
|
8241
8757
|
import {
|
|
8242
|
-
findProjectRoot as
|
|
8758
|
+
findProjectRoot as findProjectRoot15,
|
|
8243
8759
|
memoryFilePath as memoryFilePath7,
|
|
8244
|
-
resolveHaivePaths as
|
|
8245
|
-
serializeMemory as
|
|
8760
|
+
resolveHaivePaths as resolveHaivePaths12,
|
|
8761
|
+
serializeMemory as serializeMemory14
|
|
8246
8762
|
} from "@hiveai/core";
|
|
8247
8763
|
function registerMemoryPromote(memory2) {
|
|
8248
8764
|
memory2.command("promote <id>").description("Promote a personal memory to team scope (status -> proposed)").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
8249
|
-
const root =
|
|
8250
|
-
const paths =
|
|
8251
|
-
if (!
|
|
8765
|
+
const root = findProjectRoot15(opts.dir);
|
|
8766
|
+
const paths = resolveHaivePaths12(root);
|
|
8767
|
+
if (!existsSync35(paths.memoriesDir)) {
|
|
8252
8768
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
8253
8769
|
process.exitCode = 1;
|
|
8254
8770
|
return;
|
|
8255
8771
|
}
|
|
8256
|
-
const teamAndModule = await
|
|
8772
|
+
const teamAndModule = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8257
8773
|
const alreadyShared = teamAndModule.find(
|
|
8258
8774
|
(m) => m.memory.frontmatter.id === id && (m.memory.frontmatter.scope === "team" || m.memory.frontmatter.scope === "module")
|
|
8259
8775
|
);
|
|
@@ -8267,7 +8783,7 @@ function registerMemoryPromote(memory2) {
|
|
|
8267
8783
|
}
|
|
8268
8784
|
return;
|
|
8269
8785
|
}
|
|
8270
|
-
const all = await
|
|
8786
|
+
const all = await loadMemoriesFromDir26(paths.personalDir);
|
|
8271
8787
|
const found = all.find((m) => m.memory.frontmatter.id === id);
|
|
8272
8788
|
if (!found) {
|
|
8273
8789
|
ui.error(`No personal memory with id "${id}". (Promotion only applies to personal scope.)`);
|
|
@@ -8283,35 +8799,35 @@ function registerMemoryPromote(memory2) {
|
|
|
8283
8799
|
body: found.memory.body
|
|
8284
8800
|
};
|
|
8285
8801
|
const newPath = memoryFilePath7(paths, "team", updated.frontmatter.id);
|
|
8286
|
-
await mkdir12(
|
|
8287
|
-
await
|
|
8802
|
+
await mkdir12(path18.dirname(newPath), { recursive: true });
|
|
8803
|
+
await writeFile17(newPath, serializeMemory14(updated), "utf8");
|
|
8288
8804
|
await unlink2(found.filePath);
|
|
8289
8805
|
ui.success(`Promoted ${id} to team scope (status=proposed)`);
|
|
8290
|
-
ui.info(`Now at ${
|
|
8806
|
+
ui.info(`Now at ${path18.relative(root, newPath)}`);
|
|
8291
8807
|
console.log(ui.dim(`\u2192 next: haive memory approve ${id} (validate for team use)`));
|
|
8292
8808
|
});
|
|
8293
8809
|
}
|
|
8294
8810
|
|
|
8295
8811
|
// src/commands/memory-approve.ts
|
|
8296
|
-
import { existsSync as
|
|
8297
|
-
import { writeFile as
|
|
8298
|
-
import
|
|
8812
|
+
import { existsSync as existsSync36 } from "fs";
|
|
8813
|
+
import { writeFile as writeFile18 } from "fs/promises";
|
|
8814
|
+
import path19 from "path";
|
|
8299
8815
|
import "commander";
|
|
8300
8816
|
import {
|
|
8301
|
-
findProjectRoot as
|
|
8302
|
-
resolveHaivePaths as
|
|
8303
|
-
serializeMemory as
|
|
8817
|
+
findProjectRoot as findProjectRoot16,
|
|
8818
|
+
resolveHaivePaths as resolveHaivePaths13,
|
|
8819
|
+
serializeMemory as serializeMemory15
|
|
8304
8820
|
} from "@hiveai/core";
|
|
8305
8821
|
function registerMemoryApprove(memory2) {
|
|
8306
8822
|
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) => {
|
|
8307
|
-
const root =
|
|
8308
|
-
const paths =
|
|
8309
|
-
if (!
|
|
8823
|
+
const root = findProjectRoot16(opts.dir);
|
|
8824
|
+
const paths = resolveHaivePaths13(root);
|
|
8825
|
+
if (!existsSync36(paths.memoriesDir)) {
|
|
8310
8826
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8311
8827
|
process.exitCode = 1;
|
|
8312
8828
|
return;
|
|
8313
8829
|
}
|
|
8314
|
-
const all = await
|
|
8830
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8315
8831
|
if (opts.all || opts.pending) {
|
|
8316
8832
|
const candidates = all.filter((m) => {
|
|
8317
8833
|
const s = m.memory.frontmatter.status;
|
|
@@ -8328,7 +8844,7 @@ function registerMemoryApprove(memory2) {
|
|
|
8328
8844
|
frontmatter: { ...found2.memory.frontmatter, status: "validated" },
|
|
8329
8845
|
body: found2.memory.body
|
|
8330
8846
|
};
|
|
8331
|
-
await
|
|
8847
|
+
await writeFile18(found2.filePath, serializeMemory15(next2), "utf8");
|
|
8332
8848
|
count++;
|
|
8333
8849
|
}
|
|
8334
8850
|
ui.success(`Approved ${count} memor${count === 1 ? "y" : "ies"} (status=validated)`);
|
|
@@ -8357,32 +8873,32 @@ function registerMemoryApprove(memory2) {
|
|
|
8357
8873
|
frontmatter: { ...found.memory.frontmatter, status: "validated" },
|
|
8358
8874
|
body: found.memory.body
|
|
8359
8875
|
};
|
|
8360
|
-
await
|
|
8876
|
+
await writeFile18(found.filePath, serializeMemory15(next), "utf8");
|
|
8361
8877
|
ui.success(`Approved ${id} (status=validated)`);
|
|
8362
|
-
ui.info(
|
|
8878
|
+
ui.info(path19.relative(root, found.filePath));
|
|
8363
8879
|
});
|
|
8364
8880
|
}
|
|
8365
8881
|
|
|
8366
8882
|
// src/commands/memory-update.ts
|
|
8367
|
-
import { writeFile as
|
|
8368
|
-
import { existsSync as
|
|
8369
|
-
import
|
|
8883
|
+
import { writeFile as writeFile19 } from "fs/promises";
|
|
8884
|
+
import { existsSync as existsSync37 } from "fs";
|
|
8885
|
+
import path20 from "path";
|
|
8370
8886
|
import "commander";
|
|
8371
8887
|
import {
|
|
8372
|
-
findProjectRoot as
|
|
8373
|
-
resolveHaivePaths as
|
|
8374
|
-
serializeMemory as
|
|
8888
|
+
findProjectRoot as findProjectRoot17,
|
|
8889
|
+
resolveHaivePaths as resolveHaivePaths14,
|
|
8890
|
+
serializeMemory as serializeMemory16
|
|
8375
8891
|
} from "@hiveai/core";
|
|
8376
8892
|
function registerMemoryUpdate(memory2) {
|
|
8377
8893
|
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) => {
|
|
8378
|
-
const root =
|
|
8379
|
-
const paths =
|
|
8380
|
-
if (!
|
|
8894
|
+
const root = findProjectRoot17(opts.dir);
|
|
8895
|
+
const paths = resolveHaivePaths14(root);
|
|
8896
|
+
if (!existsSync37(paths.memoriesDir)) {
|
|
8381
8897
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
8382
8898
|
process.exitCode = 1;
|
|
8383
8899
|
return;
|
|
8384
8900
|
}
|
|
8385
|
-
const memories = await
|
|
8901
|
+
const memories = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8386
8902
|
const loaded = memories.find((m) => m.memory.frontmatter.id === id);
|
|
8387
8903
|
if (!loaded) {
|
|
8388
8904
|
ui.error(`No memory with id "${id}".`);
|
|
@@ -8424,12 +8940,12 @@ function registerMemoryUpdate(memory2) {
|
|
|
8424
8940
|
ui.warn("Nothing to update \u2014 provide at least one option.");
|
|
8425
8941
|
return;
|
|
8426
8942
|
}
|
|
8427
|
-
await
|
|
8943
|
+
await writeFile19(
|
|
8428
8944
|
loaded.filePath,
|
|
8429
|
-
|
|
8945
|
+
serializeMemory16({ frontmatter: newFrontmatter, body: newBody }),
|
|
8430
8946
|
"utf8"
|
|
8431
8947
|
);
|
|
8432
|
-
ui.success(`Updated ${
|
|
8948
|
+
ui.success(`Updated ${path20.relative(root, loaded.filePath)}`);
|
|
8433
8949
|
ui.info(`fields: ${updated.join(", ")}`);
|
|
8434
8950
|
});
|
|
8435
8951
|
}
|
|
@@ -8448,18 +8964,18 @@ function parseCsv3(value) {
|
|
|
8448
8964
|
}
|
|
8449
8965
|
|
|
8450
8966
|
// src/commands/memory-auto-promote.ts
|
|
8451
|
-
import { writeFile as
|
|
8452
|
-
import { existsSync as
|
|
8453
|
-
import
|
|
8967
|
+
import { writeFile as writeFile20 } from "fs/promises";
|
|
8968
|
+
import { existsSync as existsSync38 } from "fs";
|
|
8969
|
+
import path21 from "path";
|
|
8454
8970
|
import "commander";
|
|
8455
8971
|
import {
|
|
8456
8972
|
DEFAULT_AUTO_PROMOTE_RULE as DEFAULT_AUTO_PROMOTE_RULE3,
|
|
8457
|
-
findProjectRoot as
|
|
8458
|
-
getUsage as
|
|
8973
|
+
findProjectRoot as findProjectRoot18,
|
|
8974
|
+
getUsage as getUsage12,
|
|
8459
8975
|
isAutoPromoteEligible as isAutoPromoteEligible3,
|
|
8460
|
-
loadUsageIndex as
|
|
8461
|
-
resolveHaivePaths as
|
|
8462
|
-
serializeMemory as
|
|
8976
|
+
loadUsageIndex as loadUsageIndex14,
|
|
8977
|
+
resolveHaivePaths as resolveHaivePaths15,
|
|
8978
|
+
serializeMemory as serializeMemory17
|
|
8463
8979
|
} from "@hiveai/core";
|
|
8464
8980
|
function registerMemoryAutoPromote(memory2) {
|
|
8465
8981
|
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(
|
|
@@ -8467,9 +8983,9 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
8467
8983
|
"memories with more rejections than this are skipped",
|
|
8468
8984
|
String(DEFAULT_AUTO_PROMOTE_RULE3.maxRejections)
|
|
8469
8985
|
).option("--apply", "actually write status=validated to disk (default: dry-run)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
8470
|
-
const root =
|
|
8471
|
-
const paths =
|
|
8472
|
-
if (!
|
|
8986
|
+
const root = findProjectRoot18(opts.dir);
|
|
8987
|
+
const paths = resolveHaivePaths15(root);
|
|
8988
|
+
if (!existsSync38(paths.memoriesDir)) {
|
|
8473
8989
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8474
8990
|
process.exitCode = 1;
|
|
8475
8991
|
return;
|
|
@@ -8478,10 +8994,10 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
8478
8994
|
minReads: Number(opts.minReads ?? DEFAULT_AUTO_PROMOTE_RULE3.minReads),
|
|
8479
8995
|
maxRejections: Number(opts.maxRejections ?? DEFAULT_AUTO_PROMOTE_RULE3.maxRejections)
|
|
8480
8996
|
};
|
|
8481
|
-
const memories = await
|
|
8482
|
-
const usage = await
|
|
8997
|
+
const memories = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8998
|
+
const usage = await loadUsageIndex14(paths);
|
|
8483
8999
|
const eligible = memories.filter(
|
|
8484
|
-
({ memory: memory3 }) => isAutoPromoteEligible3(memory3.frontmatter,
|
|
9000
|
+
({ memory: memory3 }) => isAutoPromoteEligible3(memory3.frontmatter, getUsage12(usage, memory3.frontmatter.id), rule)
|
|
8485
9001
|
);
|
|
8486
9002
|
if (eligible.length === 0) {
|
|
8487
9003
|
ui.info(
|
|
@@ -8491,17 +9007,17 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
8491
9007
|
}
|
|
8492
9008
|
let written = 0;
|
|
8493
9009
|
for (const { memory: mem, filePath } of eligible) {
|
|
8494
|
-
const u =
|
|
9010
|
+
const u = getUsage12(usage, mem.frontmatter.id);
|
|
8495
9011
|
console.log(
|
|
8496
9012
|
`${ui.bold(opts.apply ? "PROMOTE" : "would promote")} ${mem.frontmatter.id} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
8497
9013
|
);
|
|
8498
|
-
console.log(` ${ui.dim(
|
|
9014
|
+
console.log(` ${ui.dim(path21.relative(root, filePath))}`);
|
|
8499
9015
|
if (opts.apply) {
|
|
8500
9016
|
const next = {
|
|
8501
9017
|
frontmatter: { ...mem.frontmatter, status: "validated" },
|
|
8502
9018
|
body: mem.body
|
|
8503
9019
|
};
|
|
8504
|
-
await
|
|
9020
|
+
await writeFile20(filePath, serializeMemory17(next), "utf8");
|
|
8505
9021
|
written++;
|
|
8506
9022
|
}
|
|
8507
9023
|
}
|
|
@@ -8512,25 +9028,25 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
8512
9028
|
|
|
8513
9029
|
// src/commands/memory-edit.ts
|
|
8514
9030
|
import { spawn as spawn3 } from "child_process";
|
|
8515
|
-
import { existsSync as
|
|
8516
|
-
import { readFile as
|
|
8517
|
-
import
|
|
9031
|
+
import { existsSync as existsSync39 } from "fs";
|
|
9032
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
9033
|
+
import path23 from "path";
|
|
8518
9034
|
import "commander";
|
|
8519
9035
|
import {
|
|
8520
|
-
findProjectRoot as
|
|
9036
|
+
findProjectRoot as findProjectRoot19,
|
|
8521
9037
|
parseMemory,
|
|
8522
|
-
resolveHaivePaths as
|
|
9038
|
+
resolveHaivePaths as resolveHaivePaths16
|
|
8523
9039
|
} from "@hiveai/core";
|
|
8524
9040
|
function registerMemoryEdit(memory2) {
|
|
8525
9041
|
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) => {
|
|
8526
|
-
const root =
|
|
8527
|
-
const paths =
|
|
8528
|
-
if (!
|
|
9042
|
+
const root = findProjectRoot19(opts.dir);
|
|
9043
|
+
const paths = resolveHaivePaths16(root);
|
|
9044
|
+
if (!existsSync39(paths.memoriesDir)) {
|
|
8529
9045
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8530
9046
|
process.exitCode = 1;
|
|
8531
9047
|
return;
|
|
8532
9048
|
}
|
|
8533
|
-
const all = await
|
|
9049
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8534
9050
|
const found = all.find((m) => m.memory.frontmatter.id === id);
|
|
8535
9051
|
if (!found) {
|
|
8536
9052
|
ui.error(`No memory with id "${id}".`);
|
|
@@ -8538,13 +9054,13 @@ function registerMemoryEdit(memory2) {
|
|
|
8538
9054
|
return;
|
|
8539
9055
|
}
|
|
8540
9056
|
const editor = opts.editor ?? process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
8541
|
-
ui.info(`Opening ${
|
|
9057
|
+
ui.info(`Opening ${path23.relative(root, found.filePath)} with ${editor}\u2026`);
|
|
8542
9058
|
const code = await runEditor(editor, found.filePath);
|
|
8543
9059
|
if (code !== 0) {
|
|
8544
9060
|
ui.warn(`Editor exited with status ${code}.`);
|
|
8545
9061
|
}
|
|
8546
9062
|
try {
|
|
8547
|
-
const fresh = await
|
|
9063
|
+
const fresh = await readFile11(found.filePath, "utf8");
|
|
8548
9064
|
parseMemory(fresh);
|
|
8549
9065
|
ui.success("Memory still parses cleanly.");
|
|
8550
9066
|
} catch (err) {
|
|
@@ -8565,29 +9081,29 @@ function runEditor(editor, file) {
|
|
|
8565
9081
|
}
|
|
8566
9082
|
|
|
8567
9083
|
// src/commands/memory-for-files.ts
|
|
8568
|
-
import { existsSync as
|
|
8569
|
-
import
|
|
9084
|
+
import { existsSync as existsSync40 } from "fs";
|
|
9085
|
+
import path24 from "path";
|
|
8570
9086
|
import "commander";
|
|
8571
9087
|
import {
|
|
8572
9088
|
deriveConfidence as deriveConfidence9,
|
|
8573
|
-
findProjectRoot as
|
|
8574
|
-
getUsage as
|
|
9089
|
+
findProjectRoot as findProjectRoot20,
|
|
9090
|
+
getUsage as getUsage13,
|
|
8575
9091
|
inferModulesFromPaths as inferModulesFromPaths4,
|
|
8576
|
-
loadUsageIndex as
|
|
9092
|
+
loadUsageIndex as loadUsageIndex15,
|
|
8577
9093
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths5,
|
|
8578
|
-
resolveHaivePaths as
|
|
9094
|
+
resolveHaivePaths as resolveHaivePaths17
|
|
8579
9095
|
} from "@hiveai/core";
|
|
8580
9096
|
function registerMemoryForFiles(memory2) {
|
|
8581
9097
|
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) => {
|
|
8582
|
-
const root =
|
|
8583
|
-
const paths =
|
|
8584
|
-
if (!
|
|
9098
|
+
const root = findProjectRoot20(opts.dir);
|
|
9099
|
+
const paths = resolveHaivePaths17(root);
|
|
9100
|
+
if (!existsSync40(paths.memoriesDir)) {
|
|
8585
9101
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8586
9102
|
process.exitCode = 1;
|
|
8587
9103
|
return;
|
|
8588
9104
|
}
|
|
8589
|
-
const all = await
|
|
8590
|
-
const usage = await
|
|
9105
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
9106
|
+
const usage = await loadUsageIndex15(paths);
|
|
8591
9107
|
const inferred = inferModulesFromPaths4(files);
|
|
8592
9108
|
const byAnchor = [];
|
|
8593
9109
|
const byModule = [];
|
|
@@ -8685,44 +9201,44 @@ function printGroup(root, label, loaded, usage) {
|
|
|
8685
9201
|
\u2014 ${label} \u2014`));
|
|
8686
9202
|
for (const { memory: mem, filePath } of loaded) {
|
|
8687
9203
|
const fm = mem.frontmatter;
|
|
8688
|
-
const u =
|
|
9204
|
+
const u = getUsage13(usage, fm.id);
|
|
8689
9205
|
const conf = deriveConfidence9(fm, u);
|
|
8690
9206
|
console.log(`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(conf)}`);
|
|
8691
|
-
console.log(` ${ui.dim(
|
|
9207
|
+
console.log(` ${ui.dim(path24.relative(root, filePath))}`);
|
|
8692
9208
|
}
|
|
8693
9209
|
}
|
|
8694
9210
|
|
|
8695
9211
|
// src/commands/memory-hot.ts
|
|
8696
|
-
import { existsSync as
|
|
8697
|
-
import
|
|
9212
|
+
import { existsSync as existsSync41 } from "fs";
|
|
9213
|
+
import path25 from "path";
|
|
8698
9214
|
import "commander";
|
|
8699
9215
|
import {
|
|
8700
|
-
findProjectRoot as
|
|
8701
|
-
getUsage as
|
|
8702
|
-
loadUsageIndex as
|
|
8703
|
-
resolveHaivePaths as
|
|
9216
|
+
findProjectRoot as findProjectRoot21,
|
|
9217
|
+
getUsage as getUsage14,
|
|
9218
|
+
loadUsageIndex as loadUsageIndex16,
|
|
9219
|
+
resolveHaivePaths as resolveHaivePaths18
|
|
8704
9220
|
} from "@hiveai/core";
|
|
8705
9221
|
function registerMemoryHot(memory2) {
|
|
8706
9222
|
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) => {
|
|
8707
|
-
const root =
|
|
8708
|
-
const paths =
|
|
8709
|
-
if (!
|
|
9223
|
+
const root = findProjectRoot21(opts.dir);
|
|
9224
|
+
const paths = resolveHaivePaths18(root);
|
|
9225
|
+
if (!existsSync41(paths.memoriesDir)) {
|
|
8710
9226
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8711
9227
|
process.exitCode = 1;
|
|
8712
9228
|
return;
|
|
8713
9229
|
}
|
|
8714
9230
|
const threshold = Math.max(1, Number(opts.threshold ?? 3));
|
|
8715
|
-
const all = await
|
|
8716
|
-
const usage = await
|
|
9231
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
9232
|
+
const usage = await loadUsageIndex16(paths);
|
|
8717
9233
|
const candidates = all.filter(({ memory: mem }) => {
|
|
8718
9234
|
const fm = mem.frontmatter;
|
|
8719
9235
|
if (opts.status && fm.status !== opts.status) return false;
|
|
8720
9236
|
if (opts.status === void 0 && fm.status !== "draft" && fm.status !== "proposed") {
|
|
8721
9237
|
return false;
|
|
8722
9238
|
}
|
|
8723
|
-
return
|
|
9239
|
+
return getUsage14(usage, fm.id).read_count >= threshold;
|
|
8724
9240
|
}).sort(
|
|
8725
|
-
(a, b) =>
|
|
9241
|
+
(a, b) => getUsage14(usage, b.memory.frontmatter.id).read_count - getUsage14(usage, a.memory.frontmatter.id).read_count
|
|
8726
9242
|
);
|
|
8727
9243
|
if (candidates.length === 0) {
|
|
8728
9244
|
ui.info(`No hot memories (threshold=${threshold}).`);
|
|
@@ -8730,11 +9246,11 @@ function registerMemoryHot(memory2) {
|
|
|
8730
9246
|
}
|
|
8731
9247
|
for (const { memory: mem, filePath } of candidates) {
|
|
8732
9248
|
const fm = mem.frontmatter;
|
|
8733
|
-
const u =
|
|
9249
|
+
const u = getUsage14(usage, fm.id);
|
|
8734
9250
|
console.log(
|
|
8735
9251
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(fm.status)} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
8736
9252
|
);
|
|
8737
|
-
console.log(` ${ui.dim(
|
|
9253
|
+
console.log(` ${ui.dim(path25.relative(root, filePath))}`);
|
|
8738
9254
|
}
|
|
8739
9255
|
ui.info(
|
|
8740
9256
|
`${candidates.length} hot \u2014 promote drafts with \`haive memory promote <id>\`, then \`haive memory auto-promote --apply\`.`
|
|
@@ -8743,16 +9259,16 @@ function registerMemoryHot(memory2) {
|
|
|
8743
9259
|
}
|
|
8744
9260
|
|
|
8745
9261
|
// src/commands/memory-tried.ts
|
|
8746
|
-
import { mkdir as mkdir13, writeFile as
|
|
8747
|
-
import { existsSync as
|
|
8748
|
-
import
|
|
9262
|
+
import { mkdir as mkdir13, writeFile as writeFile21 } from "fs/promises";
|
|
9263
|
+
import { existsSync as existsSync43 } from "fs";
|
|
9264
|
+
import path26 from "path";
|
|
8749
9265
|
import "commander";
|
|
8750
9266
|
import {
|
|
8751
9267
|
buildFrontmatter as buildFrontmatter8,
|
|
8752
|
-
findProjectRoot as
|
|
9268
|
+
findProjectRoot as findProjectRoot22,
|
|
8753
9269
|
memoryFilePath as memoryFilePath8,
|
|
8754
|
-
resolveHaivePaths as
|
|
8755
|
-
serializeMemory as
|
|
9270
|
+
resolveHaivePaths as resolveHaivePaths19,
|
|
9271
|
+
serializeMemory as serializeMemory18
|
|
8756
9272
|
} from "@hiveai/core";
|
|
8757
9273
|
function registerMemoryTried(memory2) {
|
|
8758
9274
|
memory2.command("tried").description(
|
|
@@ -8771,9 +9287,9 @@ function registerMemoryTried(memory2) {
|
|
|
8771
9287
|
--paths packages/cli/src/index.ts
|
|
8772
9288
|
`
|
|
8773
9289
|
).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) => {
|
|
8774
|
-
const root =
|
|
8775
|
-
const paths =
|
|
8776
|
-
if (!
|
|
9290
|
+
const root = findProjectRoot22(opts.dir);
|
|
9291
|
+
const paths = resolveHaivePaths19(root);
|
|
9292
|
+
if (!existsSync43(paths.haiveDir)) {
|
|
8777
9293
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
8778
9294
|
process.exitCode = 1;
|
|
8779
9295
|
return;
|
|
@@ -8796,14 +9312,14 @@ function registerMemoryTried(memory2) {
|
|
|
8796
9312
|
}
|
|
8797
9313
|
const body = lines.join("\n") + "\n";
|
|
8798
9314
|
const file = memoryFilePath8(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
8799
|
-
await mkdir13(
|
|
8800
|
-
if (
|
|
9315
|
+
await mkdir13(path26.dirname(file), { recursive: true });
|
|
9316
|
+
if (existsSync43(file)) {
|
|
8801
9317
|
ui.error(`Memory already exists at ${file}`);
|
|
8802
9318
|
process.exitCode = 1;
|
|
8803
9319
|
return;
|
|
8804
9320
|
}
|
|
8805
|
-
await
|
|
8806
|
-
ui.success(`Recorded: ${
|
|
9321
|
+
await writeFile21(file, serializeMemory18({ frontmatter, body }), "utf8");
|
|
9322
|
+
ui.success(`Recorded: ${path26.relative(root, file)}`);
|
|
8807
9323
|
ui.info(`id=${frontmatter.id} type=attempt status=validated (auto-approved)`);
|
|
8808
9324
|
});
|
|
8809
9325
|
}
|
|
@@ -8813,26 +9329,26 @@ function parseCsv4(value) {
|
|
|
8813
9329
|
}
|
|
8814
9330
|
|
|
8815
9331
|
// src/commands/memory-pending.ts
|
|
8816
|
-
import { existsSync as
|
|
8817
|
-
import
|
|
9332
|
+
import { existsSync as existsSync44 } from "fs";
|
|
9333
|
+
import path27 from "path";
|
|
8818
9334
|
import "commander";
|
|
8819
9335
|
import {
|
|
8820
|
-
findProjectRoot as
|
|
8821
|
-
getUsage as
|
|
8822
|
-
loadUsageIndex as
|
|
8823
|
-
resolveHaivePaths as
|
|
9336
|
+
findProjectRoot as findProjectRoot23,
|
|
9337
|
+
getUsage as getUsage15,
|
|
9338
|
+
loadUsageIndex as loadUsageIndex17,
|
|
9339
|
+
resolveHaivePaths as resolveHaivePaths20
|
|
8824
9340
|
} from "@hiveai/core";
|
|
8825
9341
|
function registerMemoryPending(memory2) {
|
|
8826
9342
|
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) => {
|
|
8827
|
-
const root =
|
|
8828
|
-
const paths =
|
|
8829
|
-
if (!
|
|
9343
|
+
const root = findProjectRoot23(opts.dir);
|
|
9344
|
+
const paths = resolveHaivePaths20(root);
|
|
9345
|
+
if (!existsSync44(paths.memoriesDir)) {
|
|
8830
9346
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8831
9347
|
process.exitCode = 1;
|
|
8832
9348
|
return;
|
|
8833
9349
|
}
|
|
8834
|
-
const all = await
|
|
8835
|
-
const usage = await
|
|
9350
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
9351
|
+
const usage = await loadUsageIndex17(paths);
|
|
8836
9352
|
const proposed = all.filter(({ memory: mem }) => {
|
|
8837
9353
|
if (mem.frontmatter.status !== "proposed") return false;
|
|
8838
9354
|
if (opts.scope && mem.frontmatter.scope !== opts.scope) return false;
|
|
@@ -8843,42 +9359,42 @@ function registerMemoryPending(memory2) {
|
|
|
8843
9359
|
return;
|
|
8844
9360
|
}
|
|
8845
9361
|
proposed.sort(
|
|
8846
|
-
(a, b) =>
|
|
9362
|
+
(a, b) => getUsage15(usage, b.memory.frontmatter.id).read_count - getUsage15(usage, a.memory.frontmatter.id).read_count
|
|
8847
9363
|
);
|
|
8848
9364
|
const now = Date.now();
|
|
8849
9365
|
for (const { memory: mem, filePath } of proposed) {
|
|
8850
9366
|
const fm = mem.frontmatter;
|
|
8851
|
-
const u =
|
|
9367
|
+
const u = getUsage15(usage, fm.id);
|
|
8852
9368
|
const ageDays = Math.floor((now - new Date(fm.created_at).getTime()) / 864e5);
|
|
8853
9369
|
const ageStr = ageDays === 0 ? "today" : `${ageDays}d`;
|
|
8854
9370
|
console.log(
|
|
8855
9371
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.dim(`age=${ageStr} reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
8856
9372
|
);
|
|
8857
|
-
console.log(` ${ui.dim(
|
|
9373
|
+
console.log(` ${ui.dim(path27.relative(root, filePath))}`);
|
|
8858
9374
|
}
|
|
8859
9375
|
ui.info(`${proposed.length} pending`);
|
|
8860
9376
|
});
|
|
8861
9377
|
}
|
|
8862
9378
|
|
|
8863
9379
|
// src/commands/memory-query.ts
|
|
8864
|
-
import { existsSync as
|
|
8865
|
-
import
|
|
9380
|
+
import { existsSync as existsSync45 } from "fs";
|
|
9381
|
+
import path28 from "path";
|
|
8866
9382
|
import "commander";
|
|
8867
9383
|
import {
|
|
8868
9384
|
extractSnippet as extractSnippet2,
|
|
8869
|
-
findProjectRoot as
|
|
9385
|
+
findProjectRoot as findProjectRoot24,
|
|
8870
9386
|
literalMatchesAllTokens as literalMatchesAllTokens3,
|
|
8871
9387
|
literalMatchesAnyToken as literalMatchesAnyToken4,
|
|
8872
9388
|
pickSnippetNeedle as pickSnippetNeedle2,
|
|
8873
|
-
resolveHaivePaths as
|
|
9389
|
+
resolveHaivePaths as resolveHaivePaths21,
|
|
8874
9390
|
tokenizeQuery as tokenizeQuery6,
|
|
8875
9391
|
trackReads as trackReads4
|
|
8876
9392
|
} from "@hiveai/core";
|
|
8877
9393
|
function registerMemoryQuery(memory2) {
|
|
8878
9394
|
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) => {
|
|
8879
|
-
const root =
|
|
8880
|
-
const paths =
|
|
8881
|
-
if (!
|
|
9395
|
+
const root = findProjectRoot24(opts.dir);
|
|
9396
|
+
const paths = resolveHaivePaths21(root);
|
|
9397
|
+
if (!existsSync45(paths.memoriesDir)) {
|
|
8882
9398
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
8883
9399
|
process.exitCode = 1;
|
|
8884
9400
|
return;
|
|
@@ -8889,7 +9405,7 @@ function registerMemoryQuery(memory2) {
|
|
|
8889
9405
|
return;
|
|
8890
9406
|
}
|
|
8891
9407
|
const statusFilter = opts.status ? opts.status.split(",").map((s) => s.trim()) : null;
|
|
8892
|
-
const all = await
|
|
9408
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8893
9409
|
const passesFilters2 = (mem) => {
|
|
8894
9410
|
const fm = mem.frontmatter;
|
|
8895
9411
|
if (opts.scope && fm.scope !== opts.scope) return false;
|
|
@@ -8919,7 +9435,7 @@ function registerMemoryQuery(memory2) {
|
|
|
8919
9435
|
const fm = mem.frontmatter;
|
|
8920
9436
|
const statusBadge = ui.statusBadge(fm.status);
|
|
8921
9437
|
console.log(`${ui.bold(fm.id)} ${ui.dim(fm.scope)} ${statusBadge}`);
|
|
8922
|
-
console.log(` ${ui.dim(
|
|
9438
|
+
console.log(` ${ui.dim(path28.relative(root, filePath))}`);
|
|
8923
9439
|
const snippet = extractSnippet2(mem.body, snippetNeedle);
|
|
8924
9440
|
if (snippet) console.log(` ${snippet}`);
|
|
8925
9441
|
}
|
|
@@ -8936,36 +9452,36 @@ ${top.length} of ${matches.length} match${matches.length === 1 ? "" : "es"}`)
|
|
|
8936
9452
|
}
|
|
8937
9453
|
|
|
8938
9454
|
// src/commands/memory-reject.ts
|
|
8939
|
-
import { writeFile as
|
|
8940
|
-
import { existsSync as
|
|
9455
|
+
import { writeFile as writeFile23 } from "fs/promises";
|
|
9456
|
+
import { existsSync as existsSync46 } from "fs";
|
|
8941
9457
|
import "commander";
|
|
8942
9458
|
import {
|
|
8943
|
-
findProjectRoot as
|
|
8944
|
-
loadUsageIndex as
|
|
9459
|
+
findProjectRoot as findProjectRoot25,
|
|
9460
|
+
loadUsageIndex as loadUsageIndex18,
|
|
8945
9461
|
recordRejection as recordRejection2,
|
|
8946
|
-
resolveHaivePaths as
|
|
9462
|
+
resolveHaivePaths as resolveHaivePaths22,
|
|
8947
9463
|
saveUsageIndex as saveUsageIndex3,
|
|
8948
|
-
serializeMemory as
|
|
9464
|
+
serializeMemory as serializeMemory19
|
|
8949
9465
|
} from "@hiveai/core";
|
|
8950
9466
|
function registerMemoryReject(memory2) {
|
|
8951
9467
|
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) => {
|
|
8952
|
-
const root =
|
|
8953
|
-
const paths =
|
|
8954
|
-
if (!
|
|
9468
|
+
const root = findProjectRoot25(opts.dir);
|
|
9469
|
+
const paths = resolveHaivePaths22(root);
|
|
9470
|
+
if (!existsSync46(paths.memoriesDir)) {
|
|
8955
9471
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8956
9472
|
process.exitCode = 1;
|
|
8957
9473
|
return;
|
|
8958
9474
|
}
|
|
8959
|
-
const memories = await
|
|
9475
|
+
const memories = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8960
9476
|
const loaded = memories.find((m) => m.memory.frontmatter.id === id);
|
|
8961
9477
|
if (!loaded) {
|
|
8962
9478
|
ui.error(`No memory with id "${id}".`);
|
|
8963
9479
|
process.exitCode = 1;
|
|
8964
9480
|
return;
|
|
8965
9481
|
}
|
|
8966
|
-
await
|
|
9482
|
+
await writeFile23(
|
|
8967
9483
|
loaded.filePath,
|
|
8968
|
-
|
|
9484
|
+
serializeMemory19({
|
|
8969
9485
|
frontmatter: {
|
|
8970
9486
|
...loaded.memory.frontmatter,
|
|
8971
9487
|
status: "rejected",
|
|
@@ -8975,7 +9491,7 @@ function registerMemoryReject(memory2) {
|
|
|
8975
9491
|
}),
|
|
8976
9492
|
"utf8"
|
|
8977
9493
|
);
|
|
8978
|
-
const idx = await
|
|
9494
|
+
const idx = await loadUsageIndex18(paths);
|
|
8979
9495
|
recordRejection2(idx, id, opts.reason ?? null);
|
|
8980
9496
|
await saveUsageIndex3(paths, idx);
|
|
8981
9497
|
const u = idx.by_id[id];
|
|
@@ -8987,34 +9503,34 @@ function registerMemoryReject(memory2) {
|
|
|
8987
9503
|
}
|
|
8988
9504
|
|
|
8989
9505
|
// src/commands/memory-rm.ts
|
|
8990
|
-
import { existsSync as
|
|
9506
|
+
import { existsSync as existsSync47 } from "fs";
|
|
8991
9507
|
import { unlink as unlink3 } from "fs/promises";
|
|
8992
|
-
import
|
|
9508
|
+
import path29 from "path";
|
|
8993
9509
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
8994
9510
|
import "commander";
|
|
8995
9511
|
import {
|
|
8996
|
-
findProjectRoot as
|
|
8997
|
-
loadUsageIndex as
|
|
8998
|
-
resolveHaivePaths as
|
|
9512
|
+
findProjectRoot as findProjectRoot26,
|
|
9513
|
+
loadUsageIndex as loadUsageIndex19,
|
|
9514
|
+
resolveHaivePaths as resolveHaivePaths23,
|
|
8999
9515
|
saveUsageIndex as saveUsageIndex4
|
|
9000
9516
|
} from "@hiveai/core";
|
|
9001
9517
|
function registerMemoryRm(memory2) {
|
|
9002
9518
|
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) => {
|
|
9003
|
-
const root =
|
|
9004
|
-
const paths =
|
|
9005
|
-
if (!
|
|
9519
|
+
const root = findProjectRoot26(opts.dir);
|
|
9520
|
+
const paths = resolveHaivePaths23(root);
|
|
9521
|
+
if (!existsSync47(paths.memoriesDir)) {
|
|
9006
9522
|
ui.error(`No .ai/memories at ${root}.`);
|
|
9007
9523
|
process.exitCode = 1;
|
|
9008
9524
|
return;
|
|
9009
9525
|
}
|
|
9010
|
-
const all = await
|
|
9526
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
9011
9527
|
const found = all.find((m) => m.memory.frontmatter.id === id);
|
|
9012
9528
|
if (!found) {
|
|
9013
9529
|
ui.error(`No memory with id "${id}".`);
|
|
9014
9530
|
process.exitCode = 1;
|
|
9015
9531
|
return;
|
|
9016
9532
|
}
|
|
9017
|
-
const rel =
|
|
9533
|
+
const rel = path29.relative(root, found.filePath);
|
|
9018
9534
|
if (!opts.yes) {
|
|
9019
9535
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
9020
9536
|
const answer = (await rl.question(`Delete ${rel}? [y/N] `)).trim().toLowerCase();
|
|
@@ -9027,7 +9543,7 @@ function registerMemoryRm(memory2) {
|
|
|
9027
9543
|
await unlink3(found.filePath);
|
|
9028
9544
|
ui.success(`Deleted ${rel}`);
|
|
9029
9545
|
if (!opts.keepUsage) {
|
|
9030
|
-
const idx = await
|
|
9546
|
+
const idx = await loadUsageIndex19(paths);
|
|
9031
9547
|
if (idx.by_id[id]) {
|
|
9032
9548
|
delete idx.by_id[id];
|
|
9033
9549
|
await saveUsageIndex4(paths, idx);
|
|
@@ -9038,27 +9554,27 @@ function registerMemoryRm(memory2) {
|
|
|
9038
9554
|
}
|
|
9039
9555
|
|
|
9040
9556
|
// src/commands/memory-show.ts
|
|
9041
|
-
import { existsSync as
|
|
9042
|
-
import { readFile as
|
|
9043
|
-
import
|
|
9557
|
+
import { existsSync as existsSync48 } from "fs";
|
|
9558
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
9559
|
+
import path30 from "path";
|
|
9044
9560
|
import "commander";
|
|
9045
9561
|
import {
|
|
9046
9562
|
deriveConfidence as deriveConfidence10,
|
|
9047
|
-
findProjectRoot as
|
|
9048
|
-
getUsage as
|
|
9049
|
-
loadUsageIndex as
|
|
9050
|
-
resolveHaivePaths as
|
|
9563
|
+
findProjectRoot as findProjectRoot27,
|
|
9564
|
+
getUsage as getUsage16,
|
|
9565
|
+
loadUsageIndex as loadUsageIndex20,
|
|
9566
|
+
resolveHaivePaths as resolveHaivePaths24
|
|
9051
9567
|
} from "@hiveai/core";
|
|
9052
9568
|
function registerMemoryShow(memory2) {
|
|
9053
9569
|
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) => {
|
|
9054
|
-
const root =
|
|
9055
|
-
const paths =
|
|
9056
|
-
if (!
|
|
9570
|
+
const root = findProjectRoot27(opts.dir);
|
|
9571
|
+
const paths = resolveHaivePaths24(root);
|
|
9572
|
+
if (!existsSync48(paths.memoriesDir)) {
|
|
9057
9573
|
ui.error(`No .ai/memories at ${root}.`);
|
|
9058
9574
|
process.exitCode = 1;
|
|
9059
9575
|
return;
|
|
9060
9576
|
}
|
|
9061
|
-
const all = await
|
|
9577
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
9062
9578
|
const found = all.find((m) => m.memory.frontmatter.id === id);
|
|
9063
9579
|
if (!found) {
|
|
9064
9580
|
ui.error(`No memory with id "${id}".`);
|
|
@@ -9066,12 +9582,12 @@ function registerMemoryShow(memory2) {
|
|
|
9066
9582
|
return;
|
|
9067
9583
|
}
|
|
9068
9584
|
if (opts.raw) {
|
|
9069
|
-
console.log(await
|
|
9585
|
+
console.log(await readFile12(found.filePath, "utf8"));
|
|
9070
9586
|
return;
|
|
9071
9587
|
}
|
|
9072
9588
|
const fm = found.memory.frontmatter;
|
|
9073
|
-
const usage = await
|
|
9074
|
-
const u =
|
|
9589
|
+
const usage = await loadUsageIndex20(paths);
|
|
9590
|
+
const u = getUsage16(usage, fm.id);
|
|
9075
9591
|
const conf = deriveConfidence10(fm, u);
|
|
9076
9592
|
console.log(ui.bold(fm.id));
|
|
9077
9593
|
console.log(`${ui.dim("scope:")} ${fm.scope}${fm.module ? ` / ${fm.module}` : ""}`);
|
|
@@ -9082,7 +9598,7 @@ function registerMemoryShow(memory2) {
|
|
|
9082
9598
|
if (fm.verified_at) console.log(`${ui.dim("verified:")} ${fm.verified_at}`);
|
|
9083
9599
|
if (fm.stale_reason) console.log(`${ui.dim("stale:")} ${fm.stale_reason}`);
|
|
9084
9600
|
console.log(`${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`);
|
|
9085
|
-
console.log(`${ui.dim("file:")} ${
|
|
9601
|
+
console.log(`${ui.dim("file:")} ${path30.relative(root, found.filePath)}`);
|
|
9086
9602
|
if (fm.anchor.paths.length || fm.anchor.symbols.length) {
|
|
9087
9603
|
console.log(ui.dim("anchor:"));
|
|
9088
9604
|
if (fm.anchor.commit) console.log(` ${ui.dim("commit:")} ${fm.anchor.commit}`);
|
|
@@ -9097,38 +9613,38 @@ function registerMemoryShow(memory2) {
|
|
|
9097
9613
|
}
|
|
9098
9614
|
|
|
9099
9615
|
// src/commands/memory-stats.ts
|
|
9100
|
-
import { existsSync as
|
|
9101
|
-
import
|
|
9616
|
+
import { existsSync as existsSync49 } from "fs";
|
|
9617
|
+
import path31 from "path";
|
|
9102
9618
|
import "commander";
|
|
9103
9619
|
import {
|
|
9104
9620
|
deriveConfidence as deriveConfidence11,
|
|
9105
|
-
findProjectRoot as
|
|
9106
|
-
getUsage as
|
|
9107
|
-
loadUsageIndex as
|
|
9108
|
-
resolveHaivePaths as
|
|
9621
|
+
findProjectRoot as findProjectRoot28,
|
|
9622
|
+
getUsage as getUsage17,
|
|
9623
|
+
loadUsageIndex as loadUsageIndex21,
|
|
9624
|
+
resolveHaivePaths as resolveHaivePaths25
|
|
9109
9625
|
} from "@hiveai/core";
|
|
9110
9626
|
function registerMemoryStats(memory2) {
|
|
9111
9627
|
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) => {
|
|
9112
|
-
const root =
|
|
9113
|
-
const paths =
|
|
9114
|
-
if (!
|
|
9628
|
+
const root = findProjectRoot28(opts.dir);
|
|
9629
|
+
const paths = resolveHaivePaths25(root);
|
|
9630
|
+
if (!existsSync49(paths.memoriesDir)) {
|
|
9115
9631
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
9116
9632
|
process.exitCode = 1;
|
|
9117
9633
|
return;
|
|
9118
9634
|
}
|
|
9119
|
-
const all = await
|
|
9120
|
-
const usage = await
|
|
9635
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
9636
|
+
const usage = await loadUsageIndex21(paths);
|
|
9121
9637
|
const target = opts.id ? all.filter((m) => m.memory.frontmatter.id === opts.id) : all;
|
|
9122
9638
|
if (target.length === 0) {
|
|
9123
9639
|
ui.info(opts.id ? `No memory with id "${opts.id}".` : "No memories.");
|
|
9124
9640
|
return;
|
|
9125
9641
|
}
|
|
9126
9642
|
target.sort(
|
|
9127
|
-
(a, b) =>
|
|
9643
|
+
(a, b) => getUsage17(usage, b.memory.frontmatter.id).read_count - getUsage17(usage, a.memory.frontmatter.id).read_count
|
|
9128
9644
|
);
|
|
9129
9645
|
for (const { memory: mem, filePath } of target) {
|
|
9130
9646
|
const fm = mem.frontmatter;
|
|
9131
|
-
const u =
|
|
9647
|
+
const u = getUsage17(usage, fm.id);
|
|
9132
9648
|
const conf = deriveConfidence11(fm, u);
|
|
9133
9649
|
console.log(
|
|
9134
9650
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(conf)}`
|
|
@@ -9136,34 +9652,34 @@ function registerMemoryStats(memory2) {
|
|
|
9136
9652
|
console.log(
|
|
9137
9653
|
` ${ui.dim("status:")} ${fm.status} ${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`
|
|
9138
9654
|
);
|
|
9139
|
-
console.log(` ${ui.dim(
|
|
9655
|
+
console.log(` ${ui.dim(path31.relative(root, filePath))}`);
|
|
9140
9656
|
}
|
|
9141
9657
|
});
|
|
9142
9658
|
}
|
|
9143
9659
|
|
|
9144
9660
|
// src/commands/memory-verify.ts
|
|
9145
|
-
import { writeFile as
|
|
9146
|
-
import { existsSync as
|
|
9147
|
-
import
|
|
9661
|
+
import { writeFile as writeFile24 } from "fs/promises";
|
|
9662
|
+
import { existsSync as existsSync50 } from "fs";
|
|
9663
|
+
import path33 from "path";
|
|
9148
9664
|
import "commander";
|
|
9149
9665
|
import {
|
|
9150
|
-
findProjectRoot as
|
|
9151
|
-
resolveHaivePaths as
|
|
9152
|
-
serializeMemory as
|
|
9666
|
+
findProjectRoot as findProjectRoot29,
|
|
9667
|
+
resolveHaivePaths as resolveHaivePaths26,
|
|
9668
|
+
serializeMemory as serializeMemory20,
|
|
9153
9669
|
verifyAnchor as verifyAnchor3
|
|
9154
9670
|
} from "@hiveai/core";
|
|
9155
9671
|
function registerMemoryVerify(memory2) {
|
|
9156
9672
|
memory2.command("verify").description(
|
|
9157
9673
|
"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"
|
|
9158
9674
|
).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) => {
|
|
9159
|
-
const root =
|
|
9160
|
-
const paths =
|
|
9161
|
-
if (!
|
|
9675
|
+
const root = findProjectRoot29(opts.dir);
|
|
9676
|
+
const paths = resolveHaivePaths26(root);
|
|
9677
|
+
if (!existsSync50(paths.memoriesDir)) {
|
|
9162
9678
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
9163
9679
|
process.exitCode = 1;
|
|
9164
9680
|
return;
|
|
9165
9681
|
}
|
|
9166
|
-
const all = await
|
|
9682
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
9167
9683
|
const targets = opts.id ? all.filter((m) => m.memory.frontmatter.id === opts.id) : all;
|
|
9168
9684
|
if (opts.id && targets.length === 0) {
|
|
9169
9685
|
ui.error(`No memory with id "${opts.id}".`);
|
|
@@ -9181,7 +9697,7 @@ function registerMemoryVerify(memory2) {
|
|
|
9181
9697
|
anchorlessIds.push(mem.frontmatter.id);
|
|
9182
9698
|
continue;
|
|
9183
9699
|
}
|
|
9184
|
-
const rel =
|
|
9700
|
+
const rel = path33.relative(root, filePath);
|
|
9185
9701
|
if (result.stale) {
|
|
9186
9702
|
staleCount++;
|
|
9187
9703
|
console.log(`${ui.bold("STALE")} ${mem.frontmatter.id}`);
|
|
@@ -9196,7 +9712,7 @@ function registerMemoryVerify(memory2) {
|
|
|
9196
9712
|
}
|
|
9197
9713
|
if (opts.update) {
|
|
9198
9714
|
const next = applyVerification2(mem, result);
|
|
9199
|
-
await
|
|
9715
|
+
await writeFile24(filePath, serializeMemory20(next), "utf8");
|
|
9200
9716
|
updated++;
|
|
9201
9717
|
}
|
|
9202
9718
|
}
|
|
@@ -9244,30 +9760,30 @@ function applyVerification2(mem, result) {
|
|
|
9244
9760
|
}
|
|
9245
9761
|
|
|
9246
9762
|
// src/commands/memory-import.ts
|
|
9247
|
-
import { readFile as
|
|
9248
|
-
import { existsSync as
|
|
9763
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
9764
|
+
import { existsSync as existsSync51 } from "fs";
|
|
9249
9765
|
import "commander";
|
|
9250
9766
|
import {
|
|
9251
|
-
findProjectRoot as
|
|
9252
|
-
resolveHaivePaths as
|
|
9767
|
+
findProjectRoot as findProjectRoot30,
|
|
9768
|
+
resolveHaivePaths as resolveHaivePaths27
|
|
9253
9769
|
} from "@hiveai/core";
|
|
9254
9770
|
function registerMemoryImport(memory2) {
|
|
9255
9771
|
memory2.command("import").description(
|
|
9256
9772
|
"Parse a Markdown file and suggest memories via the import_docs MCP prompt (prints a ready-to-use prompt invocation)"
|
|
9257
9773
|
).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) => {
|
|
9258
|
-
const root =
|
|
9259
|
-
const paths =
|
|
9260
|
-
if (!
|
|
9774
|
+
const root = findProjectRoot30(opts.dir);
|
|
9775
|
+
const paths = resolveHaivePaths27(root);
|
|
9776
|
+
if (!existsSync51(paths.haiveDir)) {
|
|
9261
9777
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
9262
9778
|
process.exitCode = 1;
|
|
9263
9779
|
return;
|
|
9264
9780
|
}
|
|
9265
|
-
if (!
|
|
9781
|
+
if (!existsSync51(opts.from)) {
|
|
9266
9782
|
ui.error(`File not found: ${opts.from}`);
|
|
9267
9783
|
process.exitCode = 1;
|
|
9268
9784
|
return;
|
|
9269
9785
|
}
|
|
9270
|
-
const content = await
|
|
9786
|
+
const content = await readFile13(opts.from, "utf8");
|
|
9271
9787
|
const scope = opts.scope ?? "team";
|
|
9272
9788
|
ui.info(`Preparing import from: ${opts.from} (scope=${scope})`);
|
|
9273
9789
|
ui.info(`Content length: ${content.length} chars`);
|
|
@@ -9295,15 +9811,15 @@ function registerMemoryImport(memory2) {
|
|
|
9295
9811
|
}
|
|
9296
9812
|
|
|
9297
9813
|
// src/commands/memory-import-changelog.ts
|
|
9298
|
-
import { existsSync as
|
|
9299
|
-
import { readFile as
|
|
9300
|
-
import
|
|
9814
|
+
import { existsSync as existsSync53 } from "fs";
|
|
9815
|
+
import { readFile as readFile14, mkdir as mkdir14, writeFile as writeFile25 } from "fs/promises";
|
|
9816
|
+
import path34 from "path";
|
|
9301
9817
|
import "commander";
|
|
9302
9818
|
import {
|
|
9303
9819
|
buildFrontmatter as buildFrontmatter9,
|
|
9304
|
-
findProjectRoot as
|
|
9305
|
-
resolveHaivePaths as
|
|
9306
|
-
serializeMemory as
|
|
9820
|
+
findProjectRoot as findProjectRoot31,
|
|
9821
|
+
resolveHaivePaths as resolveHaivePaths28,
|
|
9822
|
+
serializeMemory as serializeMemory21
|
|
9307
9823
|
} from "@hiveai/core";
|
|
9308
9824
|
function parseChangelog(content) {
|
|
9309
9825
|
const entries = [];
|
|
@@ -9367,15 +9883,15 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
9367
9883
|
"--versions <csv>",
|
|
9368
9884
|
"only import specific versions (comma-separated), or 'latest' for the most recent breaking version"
|
|
9369
9885
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9370
|
-
const root =
|
|
9371
|
-
const paths =
|
|
9372
|
-
const changelogPath =
|
|
9373
|
-
if (!
|
|
9886
|
+
const root = findProjectRoot31(opts.dir);
|
|
9887
|
+
const paths = resolveHaivePaths28(root);
|
|
9888
|
+
const changelogPath = path34.resolve(root, opts.fromChangelog);
|
|
9889
|
+
if (!existsSync53(changelogPath)) {
|
|
9374
9890
|
ui.error(`CHANGELOG not found: ${changelogPath}`);
|
|
9375
9891
|
process.exitCode = 1;
|
|
9376
9892
|
return;
|
|
9377
9893
|
}
|
|
9378
|
-
const content = await
|
|
9894
|
+
const content = await readFile14(changelogPath, "utf8");
|
|
9379
9895
|
let entries = parseChangelog(content);
|
|
9380
9896
|
if (entries.length === 0) {
|
|
9381
9897
|
ui.warn("No breaking changes, deprecations, or removals found in the CHANGELOG.");
|
|
@@ -9390,9 +9906,9 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
9390
9906
|
entries = entries.filter((e) => requested.includes(e.version));
|
|
9391
9907
|
}
|
|
9392
9908
|
}
|
|
9393
|
-
const pkgName = opts.package ??
|
|
9909
|
+
const pkgName = opts.package ?? path34.basename(path34.dirname(changelogPath));
|
|
9394
9910
|
const scope = opts.scope ?? "team";
|
|
9395
|
-
const teamDir =
|
|
9911
|
+
const teamDir = path34.join(paths.memoriesDir, scope);
|
|
9396
9912
|
await mkdir14(teamDir, { recursive: true });
|
|
9397
9913
|
let saved = 0;
|
|
9398
9914
|
for (const entry of entries) {
|
|
@@ -9415,7 +9931,7 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
9415
9931
|
lines.push("");
|
|
9416
9932
|
}
|
|
9417
9933
|
lines.push(
|
|
9418
|
-
`**Source:** \`${
|
|
9934
|
+
`**Source:** \`${path34.relative(root, changelogPath)}\`
|
|
9419
9935
|
**Action:** Update all usages of ${pkgName} if they rely on any of the above.`
|
|
9420
9936
|
);
|
|
9421
9937
|
const slug = `changelog-${pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase()}-v${entry.version.replace(/\./g, "-")}`;
|
|
@@ -9430,12 +9946,12 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
9430
9946
|
pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase(),
|
|
9431
9947
|
`v${entry.version}`
|
|
9432
9948
|
],
|
|
9433
|
-
paths: [
|
|
9949
|
+
paths: [path34.relative(root, changelogPath)],
|
|
9434
9950
|
topic: `changelog-${pkgName}-${entry.version}`
|
|
9435
9951
|
});
|
|
9436
|
-
await
|
|
9437
|
-
|
|
9438
|
-
|
|
9952
|
+
await writeFile25(
|
|
9953
|
+
path34.join(teamDir, `${fm.id}.md`),
|
|
9954
|
+
serializeMemory21({ frontmatter: fm, body: lines.join("\n") }),
|
|
9439
9955
|
"utf8"
|
|
9440
9956
|
);
|
|
9441
9957
|
console.log(ui.green(` \u2713 ${fm.id}`));
|
|
@@ -9457,17 +9973,17 @@ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${p
|
|
|
9457
9973
|
}
|
|
9458
9974
|
|
|
9459
9975
|
// src/commands/memory-digest.ts
|
|
9460
|
-
import { existsSync as
|
|
9461
|
-
import { writeFile as
|
|
9462
|
-
import
|
|
9976
|
+
import { existsSync as existsSync54 } from "fs";
|
|
9977
|
+
import { writeFile as writeFile26 } from "fs/promises";
|
|
9978
|
+
import path35 from "path";
|
|
9463
9979
|
import "commander";
|
|
9464
9980
|
import {
|
|
9465
9981
|
deriveConfidence as deriveConfidence12,
|
|
9466
|
-
findProjectRoot as
|
|
9467
|
-
getUsage as
|
|
9468
|
-
loadMemoriesFromDir as
|
|
9469
|
-
loadUsageIndex as
|
|
9470
|
-
resolveHaivePaths as
|
|
9982
|
+
findProjectRoot as findProjectRoot32,
|
|
9983
|
+
getUsage as getUsage18,
|
|
9984
|
+
loadMemoriesFromDir as loadMemoriesFromDir27,
|
|
9985
|
+
loadUsageIndex as loadUsageIndex23,
|
|
9986
|
+
resolveHaivePaths as resolveHaivePaths29
|
|
9471
9987
|
} from "@hiveai/core";
|
|
9472
9988
|
var CONFIDENCE_EMOJI = {
|
|
9473
9989
|
unverified: "\u2B1C",
|
|
@@ -9480,9 +9996,9 @@ function registerMemoryDigest(program2) {
|
|
|
9480
9996
|
program2.command("digest").description(
|
|
9481
9997
|
"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"
|
|
9482
9998
|
).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) => {
|
|
9483
|
-
const root =
|
|
9484
|
-
const paths =
|
|
9485
|
-
if (!
|
|
9999
|
+
const root = findProjectRoot32(opts.dir);
|
|
10000
|
+
const paths = resolveHaivePaths29(root);
|
|
10001
|
+
if (!existsSync54(paths.memoriesDir)) {
|
|
9486
10002
|
ui.error("No .ai/memories found. Run `haive init` first.");
|
|
9487
10003
|
process.exitCode = 1;
|
|
9488
10004
|
return;
|
|
@@ -9490,8 +10006,8 @@ function registerMemoryDigest(program2) {
|
|
|
9490
10006
|
const days = Math.max(1, Number(opts.days ?? 7));
|
|
9491
10007
|
const scopeFilter = opts.scope ?? "team";
|
|
9492
10008
|
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
9493
|
-
const all = await
|
|
9494
|
-
const usage = await
|
|
10009
|
+
const all = await loadMemoriesFromDir27(paths.memoriesDir);
|
|
10010
|
+
const usage = await loadUsageIndex23(paths);
|
|
9495
10011
|
const recent = all.filter(({ memory: mem }) => {
|
|
9496
10012
|
const fm = mem.frontmatter;
|
|
9497
10013
|
if (fm.type === "session_recap") return false;
|
|
@@ -9522,7 +10038,7 @@ function registerMemoryDigest(program2) {
|
|
|
9522
10038
|
lines.push(``);
|
|
9523
10039
|
for (const { memory: mem } of mems) {
|
|
9524
10040
|
const fm = mem.frontmatter;
|
|
9525
|
-
const u =
|
|
10041
|
+
const u = getUsage18(usage, fm.id);
|
|
9526
10042
|
const confidence = deriveConfidence12(fm, u);
|
|
9527
10043
|
const emoji = CONFIDENCE_EMOJI[confidence] ?? "\u2B1C";
|
|
9528
10044
|
const anchor = fm.anchor.paths.length > 0 ? `\`${fm.anchor.paths[0]}\`` + (fm.anchor.paths.length > 1 ? ` +${fm.anchor.paths.length - 1}` : "") : "_no anchor_";
|
|
@@ -9554,8 +10070,8 @@ function registerMemoryDigest(program2) {
|
|
|
9554
10070
|
);
|
|
9555
10071
|
const digest = lines.join("\n");
|
|
9556
10072
|
if (opts.out) {
|
|
9557
|
-
const outPath =
|
|
9558
|
-
await
|
|
10073
|
+
const outPath = path35.resolve(process.cwd(), opts.out);
|
|
10074
|
+
await writeFile26(outPath, digest, "utf8");
|
|
9559
10075
|
ui.success(`Digest written to ${opts.out} (${recent.length} memor${recent.length === 1 ? "y" : "ies"})`);
|
|
9560
10076
|
} else {
|
|
9561
10077
|
console.log(digest);
|
|
@@ -9564,22 +10080,22 @@ function registerMemoryDigest(program2) {
|
|
|
9564
10080
|
}
|
|
9565
10081
|
|
|
9566
10082
|
// src/commands/session-end.ts
|
|
9567
|
-
import { writeFile as
|
|
9568
|
-
import { existsSync as
|
|
9569
|
-
import
|
|
10083
|
+
import { writeFile as writeFile27, mkdir as mkdir15, readFile as readFile15, rm as rm2 } from "fs/promises";
|
|
10084
|
+
import { existsSync as existsSync55 } from "fs";
|
|
10085
|
+
import path36 from "path";
|
|
9570
10086
|
import "commander";
|
|
9571
10087
|
import {
|
|
9572
10088
|
buildFrontmatter as buildFrontmatter10,
|
|
9573
|
-
findProjectRoot as
|
|
9574
|
-
loadMemoriesFromDir as
|
|
10089
|
+
findProjectRoot as findProjectRoot33,
|
|
10090
|
+
loadMemoriesFromDir as loadMemoriesFromDir28,
|
|
9575
10091
|
memoryFilePath as memoryFilePath9,
|
|
9576
|
-
resolveHaivePaths as
|
|
9577
|
-
serializeMemory as
|
|
10092
|
+
resolveHaivePaths as resolveHaivePaths30,
|
|
10093
|
+
serializeMemory as serializeMemory23
|
|
9578
10094
|
} from "@hiveai/core";
|
|
9579
10095
|
async function buildAutoRecap(paths) {
|
|
9580
|
-
const obsFile =
|
|
9581
|
-
if (!
|
|
9582
|
-
const raw = await
|
|
10096
|
+
const obsFile = path36.join(paths.haiveDir, ".cache", "observations.jsonl");
|
|
10097
|
+
if (!existsSync55(obsFile)) return null;
|
|
10098
|
+
const raw = await readFile15(obsFile, "utf8").catch(() => "");
|
|
9583
10099
|
if (!raw.trim()) return null;
|
|
9584
10100
|
const lines = raw.split("\n").filter(Boolean);
|
|
9585
10101
|
const obs = [];
|
|
@@ -9657,9 +10173,9 @@ function registerSessionEnd(session2) {
|
|
|
9657
10173
|
--next "Add integration tests for webhook signature validation"
|
|
9658
10174
|
`
|
|
9659
10175
|
).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) => {
|
|
9660
|
-
const root =
|
|
9661
|
-
const paths =
|
|
9662
|
-
if (!
|
|
10176
|
+
const root = findProjectRoot33(opts.dir);
|
|
10177
|
+
const paths = resolveHaivePaths30(root);
|
|
10178
|
+
if (!existsSync55(paths.haiveDir)) {
|
|
9663
10179
|
if (opts.auto || opts.quiet) return;
|
|
9664
10180
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
9665
10181
|
process.exitCode = 1;
|
|
@@ -9691,19 +10207,19 @@ function registerSessionEnd(session2) {
|
|
|
9691
10207
|
});
|
|
9692
10208
|
const topic = recapTopic2(scope, opts.module);
|
|
9693
10209
|
const filesTouched = parseCsv5(resolvedFiles);
|
|
9694
|
-
const missingPaths = filesTouched.filter((p) => !
|
|
10210
|
+
const missingPaths = filesTouched.filter((p) => !existsSync55(path36.resolve(root, p)));
|
|
9695
10211
|
if (missingPaths.length > 0 && !opts.quiet) {
|
|
9696
10212
|
ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
|
|
9697
10213
|
for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
|
|
9698
10214
|
}
|
|
9699
10215
|
const cleanupObservations = async () => {
|
|
9700
10216
|
if (!opts.auto) return;
|
|
9701
|
-
const obsFile =
|
|
9702
|
-
if (
|
|
10217
|
+
const obsFile = path36.join(paths.haiveDir, ".cache", "observations.jsonl");
|
|
10218
|
+
if (existsSync55(obsFile)) await rm2(obsFile).catch(() => {
|
|
9703
10219
|
});
|
|
9704
10220
|
};
|
|
9705
|
-
if (
|
|
9706
|
-
const existing = await
|
|
10221
|
+
if (existsSync55(paths.memoriesDir)) {
|
|
10222
|
+
const existing = await loadMemoriesFromDir28(paths.memoriesDir);
|
|
9707
10223
|
const topicMatch = existing.find(
|
|
9708
10224
|
({ memory: memory2 }) => memory2.frontmatter.topic === topic && memory2.frontmatter.scope === scope && (!opts.module || memory2.frontmatter.module === opts.module)
|
|
9709
10225
|
);
|
|
@@ -9719,11 +10235,11 @@ function registerSessionEnd(session2) {
|
|
|
9719
10235
|
paths: filesTouched.length ? filesTouched : fm.anchor.paths
|
|
9720
10236
|
}
|
|
9721
10237
|
};
|
|
9722
|
-
await
|
|
10238
|
+
await writeFile27(topicMatch.filePath, serializeMemory23({ frontmatter: newFrontmatter, body }), "utf8");
|
|
9723
10239
|
await cleanupObservations();
|
|
9724
10240
|
if (!opts.quiet) {
|
|
9725
10241
|
ui.success(`Session recap updated (revision #${revisionCount})`);
|
|
9726
|
-
ui.info(`id=${fm.id} file=${
|
|
10242
|
+
ui.info(`id=${fm.id} file=${path36.relative(root, topicMatch.filePath)}`);
|
|
9727
10243
|
ui.info("Tip: `haive stats --export-report` generates a usage JSON suitable for dashboards.");
|
|
9728
10244
|
}
|
|
9729
10245
|
return;
|
|
@@ -9740,12 +10256,12 @@ function registerSessionEnd(session2) {
|
|
|
9740
10256
|
status: "validated"
|
|
9741
10257
|
});
|
|
9742
10258
|
const file = memoryFilePath9(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
9743
|
-
await mkdir15(
|
|
9744
|
-
await
|
|
10259
|
+
await mkdir15(path36.dirname(file), { recursive: true });
|
|
10260
|
+
await writeFile27(file, serializeMemory23({ frontmatter, body }), "utf8");
|
|
9745
10261
|
await cleanupObservations();
|
|
9746
10262
|
if (!opts.quiet) {
|
|
9747
10263
|
ui.success(`Session recap created`);
|
|
9748
|
-
ui.info(`id=${frontmatter.id} scope=${scope} file=${
|
|
10264
|
+
ui.info(`id=${frontmatter.id} scope=${scope} file=${path36.relative(root, file)}`);
|
|
9749
10265
|
ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
|
|
9750
10266
|
ui.info("Tip: export a local MCP usage rollup with `haive stats --export-report .ai/tool-usage-roi-report.json`.");
|
|
9751
10267
|
}
|
|
@@ -9757,15 +10273,15 @@ function parseCsv5(value) {
|
|
|
9757
10273
|
}
|
|
9758
10274
|
|
|
9759
10275
|
// src/commands/snapshot.ts
|
|
9760
|
-
import { existsSync as
|
|
10276
|
+
import { existsSync as existsSync56 } from "fs";
|
|
9761
10277
|
import { readdir as readdir4 } from "fs/promises";
|
|
9762
|
-
import
|
|
10278
|
+
import path37 from "path";
|
|
9763
10279
|
import "commander";
|
|
9764
10280
|
import {
|
|
9765
10281
|
diffContract,
|
|
9766
|
-
findProjectRoot as
|
|
9767
|
-
loadConfig as
|
|
9768
|
-
resolveHaivePaths as
|
|
10282
|
+
findProjectRoot as findProjectRoot34,
|
|
10283
|
+
loadConfig as loadConfig7,
|
|
10284
|
+
resolveHaivePaths as resolveHaivePaths31,
|
|
9769
10285
|
snapshotContract
|
|
9770
10286
|
} from "@hiveai/core";
|
|
9771
10287
|
function registerSnapshot(program2) {
|
|
@@ -9790,16 +10306,16 @@ function registerSnapshot(program2) {
|
|
|
9790
10306
|
"--format <format>",
|
|
9791
10307
|
"contract format: openapi | graphql | proto | typescript | json-schema (auto-detected if omitted)"
|
|
9792
10308
|
).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) => {
|
|
9793
|
-
const root =
|
|
9794
|
-
const paths =
|
|
9795
|
-
if (!
|
|
10309
|
+
const root = findProjectRoot34(opts.dir);
|
|
10310
|
+
const paths = resolveHaivePaths31(root);
|
|
10311
|
+
if (!existsSync56(paths.haiveDir)) {
|
|
9796
10312
|
ui.error("No .ai/ found. Run `haive init` first.");
|
|
9797
10313
|
process.exitCode = 1;
|
|
9798
10314
|
return;
|
|
9799
10315
|
}
|
|
9800
10316
|
if (opts.list) {
|
|
9801
|
-
const contractsDir =
|
|
9802
|
-
if (!
|
|
10317
|
+
const contractsDir = path37.join(paths.haiveDir, "contracts");
|
|
10318
|
+
if (!existsSync56(contractsDir)) {
|
|
9803
10319
|
console.log(ui.dim("No contract snapshots found."));
|
|
9804
10320
|
return;
|
|
9805
10321
|
}
|
|
@@ -9819,7 +10335,7 @@ function registerSnapshot(program2) {
|
|
|
9819
10335
|
}
|
|
9820
10336
|
if (opts.diff) {
|
|
9821
10337
|
if (!opts.name) {
|
|
9822
|
-
const config2 = await
|
|
10338
|
+
const config2 = await loadConfig7(paths);
|
|
9823
10339
|
const contracts = config2.contractFiles ?? [];
|
|
9824
10340
|
if (contracts.length === 0) {
|
|
9825
10341
|
ui.error("--diff requires --name, or configure contractFiles in haive.config.json");
|
|
@@ -9831,7 +10347,7 @@ function registerSnapshot(program2) {
|
|
|
9831
10347
|
}
|
|
9832
10348
|
return;
|
|
9833
10349
|
}
|
|
9834
|
-
const config = await
|
|
10350
|
+
const config = await loadConfig7(paths);
|
|
9835
10351
|
const configured = (config.contractFiles ?? []).find((c) => c.name === opts.name);
|
|
9836
10352
|
if (!configured && !opts.contract) {
|
|
9837
10353
|
ui.error(
|
|
@@ -9854,7 +10370,7 @@ function registerSnapshot(program2) {
|
|
|
9854
10370
|
return;
|
|
9855
10371
|
}
|
|
9856
10372
|
const contractPath = opts.contract;
|
|
9857
|
-
const name = opts.name ??
|
|
10373
|
+
const name = opts.name ?? path37.basename(contractPath, path37.extname(contractPath));
|
|
9858
10374
|
const format = opts.format ?? detectFormat(contractPath) ?? "openapi";
|
|
9859
10375
|
const contract = { name, path: contractPath, format };
|
|
9860
10376
|
try {
|
|
@@ -9909,8 +10425,8 @@ async function runDiff(root, haiveDir, contract) {
|
|
|
9909
10425
|
}
|
|
9910
10426
|
}
|
|
9911
10427
|
function detectFormat(filePath) {
|
|
9912
|
-
const ext =
|
|
9913
|
-
const base =
|
|
10428
|
+
const ext = path37.extname(filePath).toLowerCase();
|
|
10429
|
+
const base = path37.basename(filePath).toLowerCase();
|
|
9914
10430
|
if (ext === ".yaml" || ext === ".yml" || ext === ".json") {
|
|
9915
10431
|
if (base.includes("openapi") || base.includes("swagger")) return "openapi";
|
|
9916
10432
|
if (base.includes("schema") || base.includes("graphql")) return "graphql";
|
|
@@ -9923,18 +10439,18 @@ function detectFormat(filePath) {
|
|
|
9923
10439
|
}
|
|
9924
10440
|
|
|
9925
10441
|
// src/commands/hub.ts
|
|
9926
|
-
import { existsSync as
|
|
9927
|
-
import { mkdir as mkdir16, readFile as
|
|
9928
|
-
import
|
|
9929
|
-
import { spawnSync as
|
|
10442
|
+
import { existsSync as existsSync57 } from "fs";
|
|
10443
|
+
import { mkdir as mkdir16, readFile as readFile16, writeFile as writeFile28, copyFile } from "fs/promises";
|
|
10444
|
+
import path38 from "path";
|
|
10445
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
9930
10446
|
import "commander";
|
|
9931
10447
|
import {
|
|
9932
|
-
findProjectRoot as
|
|
9933
|
-
loadConfig as
|
|
9934
|
-
loadMemoriesFromDir as
|
|
9935
|
-
resolveHaivePaths as
|
|
9936
|
-
saveConfig as
|
|
9937
|
-
serializeMemory as
|
|
10448
|
+
findProjectRoot as findProjectRoot35,
|
|
10449
|
+
loadConfig as loadConfig8,
|
|
10450
|
+
loadMemoriesFromDir as loadMemoriesFromDir29,
|
|
10451
|
+
resolveHaivePaths as resolveHaivePaths32,
|
|
10452
|
+
saveConfig as saveConfig3,
|
|
10453
|
+
serializeMemory as serializeMemory24
|
|
9938
10454
|
} from "@hiveai/core";
|
|
9939
10455
|
function registerHub(program2) {
|
|
9940
10456
|
const hub = program2.command("hub").description(
|
|
@@ -9944,21 +10460,21 @@ function registerHub(program2) {
|
|
|
9944
10460
|
hub.command("init <hubPath>").description(
|
|
9945
10461
|
"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"
|
|
9946
10462
|
).action(async (hubPath) => {
|
|
9947
|
-
const absPath =
|
|
10463
|
+
const absPath = path38.resolve(hubPath);
|
|
9948
10464
|
await mkdir16(absPath, { recursive: true });
|
|
9949
|
-
const gitCheck =
|
|
10465
|
+
const gitCheck = spawnSync5("git", ["rev-parse", "--git-dir"], { cwd: absPath });
|
|
9950
10466
|
if (gitCheck.status !== 0) {
|
|
9951
|
-
const init =
|
|
10467
|
+
const init = spawnSync5("git", ["init"], { cwd: absPath, encoding: "utf8" });
|
|
9952
10468
|
if (init.status !== 0) {
|
|
9953
10469
|
ui.error(`git init failed: ${init.stderr}`);
|
|
9954
10470
|
process.exitCode = 1;
|
|
9955
10471
|
return;
|
|
9956
10472
|
}
|
|
9957
10473
|
}
|
|
9958
|
-
const sharedDir =
|
|
10474
|
+
const sharedDir = path38.join(absPath, ".ai", "memories", "shared");
|
|
9959
10475
|
await mkdir16(sharedDir, { recursive: true });
|
|
9960
|
-
await
|
|
9961
|
-
|
|
10476
|
+
await writeFile28(
|
|
10477
|
+
path38.join(absPath, ".ai", "README.md"),
|
|
9962
10478
|
`# hAIve Team Knowledge Hub
|
|
9963
10479
|
|
|
9964
10480
|
This repo is a shared knowledge hub for hAIve.
|
|
@@ -9979,13 +10495,13 @@ haive hub pull # import into a project
|
|
|
9979
10495
|
`,
|
|
9980
10496
|
"utf8"
|
|
9981
10497
|
);
|
|
9982
|
-
await
|
|
9983
|
-
|
|
10498
|
+
await writeFile28(
|
|
10499
|
+
path38.join(absPath, ".gitignore"),
|
|
9984
10500
|
".ai/.cache/\n.ai/memories/personal/\n",
|
|
9985
10501
|
"utf8"
|
|
9986
10502
|
);
|
|
9987
|
-
|
|
9988
|
-
|
|
10503
|
+
spawnSync5("git", ["add", "."], { cwd: absPath });
|
|
10504
|
+
spawnSync5("git", ["commit", "-m", "chore: initialize hAIve team-knowledge hub"], {
|
|
9989
10505
|
cwd: absPath,
|
|
9990
10506
|
encoding: "utf8"
|
|
9991
10507
|
});
|
|
@@ -9995,7 +10511,7 @@ haive hub pull # import into a project
|
|
|
9995
10511
|
`
|
|
9996
10512
|
Next steps:
|
|
9997
10513
|
1. Add hubPath to your project's .ai/haive.config.json:
|
|
9998
|
-
{ "hubPath": "${
|
|
10514
|
+
{ "hubPath": "${path38.relative(process.cwd(), absPath)}" }
|
|
9999
10515
|
2. Run \`haive hub push\` to publish your shared memories
|
|
10000
10516
|
3. Share ${absPath} with teammates (git remote, NFS, etc.)
|
|
10001
10517
|
`
|
|
@@ -10014,9 +10530,9 @@ Next steps:
|
|
|
10014
10530
|
haive hub push --commit --message "feat: add payment API contract memories"
|
|
10015
10531
|
`
|
|
10016
10532
|
).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) => {
|
|
10017
|
-
const root =
|
|
10018
|
-
const paths =
|
|
10019
|
-
const config = await
|
|
10533
|
+
const root = findProjectRoot35(opts.dir);
|
|
10534
|
+
const paths = resolveHaivePaths32(root);
|
|
10535
|
+
const config = await loadConfig8(paths);
|
|
10020
10536
|
if (!config.hubPath) {
|
|
10021
10537
|
ui.error(
|
|
10022
10538
|
'hubPath not configured in .ai/haive.config.json.\n Add: { "hubPath": "../team-hub" }\n Or run: haive hub init <path> first.'
|
|
@@ -10024,16 +10540,16 @@ Next steps:
|
|
|
10024
10540
|
process.exitCode = 1;
|
|
10025
10541
|
return;
|
|
10026
10542
|
}
|
|
10027
|
-
const hubRoot =
|
|
10028
|
-
if (!
|
|
10543
|
+
const hubRoot = path38.resolve(root, config.hubPath);
|
|
10544
|
+
if (!existsSync57(hubRoot)) {
|
|
10029
10545
|
ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
|
|
10030
10546
|
process.exitCode = 1;
|
|
10031
10547
|
return;
|
|
10032
10548
|
}
|
|
10033
|
-
const projectName =
|
|
10034
|
-
const destDir =
|
|
10549
|
+
const projectName = path38.basename(root);
|
|
10550
|
+
const destDir = path38.join(hubRoot, ".ai", "memories", "shared", projectName);
|
|
10035
10551
|
await mkdir16(destDir, { recursive: true });
|
|
10036
|
-
const all = await
|
|
10552
|
+
const all = await loadMemoriesFromDir29(paths.memoriesDir);
|
|
10037
10553
|
const shared = all.filter(
|
|
10038
10554
|
({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && // Don't push imported memories (avoid echo loops)
|
|
10039
10555
|
!memory2.frontmatter.tags.some((t) => t.startsWith("cross-repo:"))
|
|
@@ -10050,18 +10566,18 @@ Next steps:
|
|
|
10050
10566
|
for (const { memory: memory2 } of shared) {
|
|
10051
10567
|
const fm = memory2.frontmatter;
|
|
10052
10568
|
const fileName = `${fm.id}.md`;
|
|
10053
|
-
const destPath =
|
|
10054
|
-
await
|
|
10569
|
+
const destPath = path38.join(destDir, fileName);
|
|
10570
|
+
await writeFile28(destPath, serializeMemory24(memory2), "utf8");
|
|
10055
10571
|
pushed++;
|
|
10056
10572
|
}
|
|
10057
10573
|
console.log(ui.green(`\u2713 Pushed ${pushed} shared memor${pushed === 1 ? "y" : "ies"} to hub`));
|
|
10058
10574
|
console.log(ui.dim(` Location: ${destDir}`));
|
|
10059
10575
|
if (opts.commit) {
|
|
10060
10576
|
const message = opts.message ?? `haive: sync shared memories from ${projectName} (${pushed} memories)`;
|
|
10061
|
-
|
|
10577
|
+
spawnSync5("git", ["add", path38.join(".ai", "memories", "shared", projectName)], {
|
|
10062
10578
|
cwd: hubRoot
|
|
10063
10579
|
});
|
|
10064
|
-
const commit =
|
|
10580
|
+
const commit = spawnSync5("git", ["commit", "-m", message], {
|
|
10065
10581
|
cwd: hubRoot,
|
|
10066
10582
|
encoding: "utf8"
|
|
10067
10583
|
});
|
|
@@ -10083,9 +10599,9 @@ Next steps:
|
|
|
10083
10599
|
hub.command("pull").description(
|
|
10084
10600
|
"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"
|
|
10085
10601
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10086
|
-
const root =
|
|
10087
|
-
const paths =
|
|
10088
|
-
const config = await
|
|
10602
|
+
const root = findProjectRoot35(opts.dir);
|
|
10603
|
+
const paths = resolveHaivePaths32(root);
|
|
10604
|
+
const config = await loadConfig8(paths);
|
|
10089
10605
|
if (!config.hubPath) {
|
|
10090
10606
|
ui.error(
|
|
10091
10607
|
'hubPath not configured in .ai/haive.config.json.\n Add: { "hubPath": "../team-hub" }\n Or run: haive hub init <path> first.'
|
|
@@ -10093,13 +10609,13 @@ Next steps:
|
|
|
10093
10609
|
process.exitCode = 1;
|
|
10094
10610
|
return;
|
|
10095
10611
|
}
|
|
10096
|
-
const hubRoot =
|
|
10097
|
-
const hubSharedDir =
|
|
10098
|
-
if (!
|
|
10612
|
+
const hubRoot = path38.resolve(root, config.hubPath);
|
|
10613
|
+
const hubSharedDir = path38.join(hubRoot, ".ai", "memories", "shared");
|
|
10614
|
+
if (!existsSync57(hubSharedDir)) {
|
|
10099
10615
|
ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
|
|
10100
10616
|
return;
|
|
10101
10617
|
}
|
|
10102
|
-
const projectName =
|
|
10618
|
+
const projectName = path38.basename(root);
|
|
10103
10619
|
const { readdir: readdir6 } = await import("fs/promises");
|
|
10104
10620
|
const projectDirs = (await readdir6(hubSharedDir, { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== projectName).map((d) => d.name);
|
|
10105
10621
|
if (projectDirs.length === 0) {
|
|
@@ -10109,17 +10625,17 @@ Next steps:
|
|
|
10109
10625
|
let totalImported = 0;
|
|
10110
10626
|
let totalUpdated = 0;
|
|
10111
10627
|
for (const sourceName of projectDirs) {
|
|
10112
|
-
const sourceDir =
|
|
10113
|
-
const destDir =
|
|
10628
|
+
const sourceDir = path38.join(hubSharedDir, sourceName);
|
|
10629
|
+
const destDir = path38.join(paths.memoriesDir, "shared", sourceName);
|
|
10114
10630
|
await mkdir16(destDir, { recursive: true });
|
|
10115
10631
|
const sourceFiles = (await readdir6(sourceDir)).filter((f) => f.endsWith(".md"));
|
|
10116
10632
|
const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
|
|
10117
10633
|
const existingInDest = await loadDir(destDir);
|
|
10118
10634
|
const existingIds = new Set(existingInDest.map(({ memory: memory2 }) => memory2.frontmatter.id));
|
|
10119
10635
|
for (const file of sourceFiles) {
|
|
10120
|
-
const srcPath =
|
|
10121
|
-
const destPath =
|
|
10122
|
-
const fileContent = await
|
|
10636
|
+
const srcPath = path38.join(sourceDir, file);
|
|
10637
|
+
const destPath = path38.join(destDir, file);
|
|
10638
|
+
const fileContent = await readFile16(srcPath, "utf8");
|
|
10123
10639
|
const alreadyTagged = fileContent.includes(`cross-repo:${sourceName}`);
|
|
10124
10640
|
if (!alreadyTagged) {
|
|
10125
10641
|
await copyFile(srcPath, destPath);
|
|
@@ -10142,27 +10658,27 @@ Next steps:
|
|
|
10142
10658
|
);
|
|
10143
10659
|
});
|
|
10144
10660
|
hub.command("status").description("Show hub sync status.").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10145
|
-
const root =
|
|
10146
|
-
const paths =
|
|
10147
|
-
const config = await
|
|
10661
|
+
const root = findProjectRoot35(opts.dir);
|
|
10662
|
+
const paths = resolveHaivePaths32(root);
|
|
10663
|
+
const config = await loadConfig8(paths);
|
|
10148
10664
|
console.log(ui.bold("Hub status"));
|
|
10149
10665
|
console.log(
|
|
10150
10666
|
` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
|
|
10151
10667
|
);
|
|
10152
|
-
const sharedDir =
|
|
10153
|
-
if (
|
|
10668
|
+
const sharedDir = path38.join(paths.memoriesDir, "shared");
|
|
10669
|
+
if (existsSync57(sharedDir)) {
|
|
10154
10670
|
const { readdir: readdir6 } = await import("fs/promises");
|
|
10155
10671
|
const sources = (await readdir6(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
10156
10672
|
console.log(`
|
|
10157
10673
|
Imported from ${sources.length} source(s):`);
|
|
10158
10674
|
for (const src of sources) {
|
|
10159
|
-
const files = (await readdir6(
|
|
10675
|
+
const files = (await readdir6(path38.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
|
|
10160
10676
|
console.log(` ${src}: ${files.length} memor${files.length === 1 ? "y" : "ies"}`);
|
|
10161
10677
|
}
|
|
10162
10678
|
} else {
|
|
10163
10679
|
console.log(ui.dim(" No imported shared memories yet."));
|
|
10164
10680
|
}
|
|
10165
|
-
const all = await
|
|
10681
|
+
const all = await loadMemoriesFromDir29(paths.memoriesDir);
|
|
10166
10682
|
const outgoing = all.filter(
|
|
10167
10683
|
({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && !memory2.frontmatter.tags.some((t) => t.startsWith("cross-repo:"))
|
|
10168
10684
|
);
|
|
@@ -10171,25 +10687,25 @@ Next steps:
|
|
|
10171
10687
|
if (outgoing.length > 0) {
|
|
10172
10688
|
console.log(ui.dim(" Run `haive hub push` to publish them to the hub."));
|
|
10173
10689
|
}
|
|
10174
|
-
void
|
|
10175
|
-
void
|
|
10176
|
-
void
|
|
10690
|
+
void readFile16;
|
|
10691
|
+
void writeFile28;
|
|
10692
|
+
void saveConfig3;
|
|
10177
10693
|
});
|
|
10178
10694
|
}
|
|
10179
10695
|
|
|
10180
10696
|
// src/commands/stats.ts
|
|
10181
10697
|
import "commander";
|
|
10182
|
-
import { existsSync as
|
|
10183
|
-
import { mkdir as mkdir17, writeFile as
|
|
10184
|
-
import
|
|
10698
|
+
import { existsSync as existsSync58 } from "fs";
|
|
10699
|
+
import { mkdir as mkdir17, writeFile as writeFile29 } from "fs/promises";
|
|
10700
|
+
import path39 from "path";
|
|
10185
10701
|
import {
|
|
10186
10702
|
aggregateUsage,
|
|
10187
|
-
findProjectRoot as
|
|
10188
|
-
loadMemoriesFromDir as
|
|
10189
|
-
loadUsageIndex as
|
|
10703
|
+
findProjectRoot as findProjectRoot36,
|
|
10704
|
+
loadMemoriesFromDir as loadMemoriesFromDir30,
|
|
10705
|
+
loadUsageIndex as loadUsageIndex24,
|
|
10190
10706
|
parseSince,
|
|
10191
10707
|
readUsageEvents as readUsageEvents2,
|
|
10192
|
-
resolveHaivePaths as
|
|
10708
|
+
resolveHaivePaths as resolveHaivePaths33,
|
|
10193
10709
|
usageLogSize
|
|
10194
10710
|
} from "@hiveai/core";
|
|
10195
10711
|
function registerStats(program2) {
|
|
@@ -10198,8 +10714,8 @@ function registerStats(program2) {
|
|
|
10198
10714
|
"write a JSON rollup (tools + briefing counts + heuristic ROI hints). Parent dirs are created if needed.",
|
|
10199
10715
|
void 0
|
|
10200
10716
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10201
|
-
const root =
|
|
10202
|
-
const paths =
|
|
10717
|
+
const root = findProjectRoot36(opts.dir);
|
|
10718
|
+
const paths = resolveHaivePaths33(root);
|
|
10203
10719
|
if (opts.exportReport) {
|
|
10204
10720
|
await writeRoiReport(paths, root, opts.since ?? "30d", opts.exportReport);
|
|
10205
10721
|
return;
|
|
@@ -10253,12 +10769,12 @@ function registerStats(program2) {
|
|
|
10253
10769
|
});
|
|
10254
10770
|
}
|
|
10255
10771
|
async function writeRoiReport(paths, root, sinceRaw, outRelative) {
|
|
10256
|
-
const outAbs =
|
|
10772
|
+
const outAbs = path39.isAbsolute(outRelative) ? path39.resolve(outRelative) : path39.resolve(root, outRelative);
|
|
10257
10773
|
const size = await usageLogSize(paths);
|
|
10258
10774
|
let events = await readUsageEvents2(paths);
|
|
10259
10775
|
let memoryCount = { team: 0, personal: 0, total_skipped_session: 0 };
|
|
10260
|
-
if (
|
|
10261
|
-
const mems = await
|
|
10776
|
+
if (existsSync58(paths.memoriesDir)) {
|
|
10777
|
+
const mems = await loadMemoriesFromDir30(paths.memoriesDir);
|
|
10262
10778
|
for (const { memory: memory2 } of mems) {
|
|
10263
10779
|
const fm = memory2.frontmatter;
|
|
10264
10780
|
if (fm.type === "session_recap") memoryCount.total_skipped_session++;
|
|
@@ -10272,7 +10788,7 @@ async function writeRoiReport(paths, root, sinceRaw, outRelative) {
|
|
|
10272
10788
|
const briefingCalls = events.filter((e) => inWindow(e.at) && e.tool === "get_briefing").length;
|
|
10273
10789
|
let memoryHitsLeader = null;
|
|
10274
10790
|
try {
|
|
10275
|
-
const usageIdx = await
|
|
10791
|
+
const usageIdx = await loadUsageIndex24(paths);
|
|
10276
10792
|
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);
|
|
10277
10793
|
memoryHitsLeader = tops[0] ?? null;
|
|
10278
10794
|
} catch {
|
|
@@ -10288,7 +10804,7 @@ async function writeRoiReport(paths, root, sinceRaw, outRelative) {
|
|
|
10288
10804
|
ui.warn("Usage log missing or empty \u2014 report still written with partial data.");
|
|
10289
10805
|
events = [];
|
|
10290
10806
|
}
|
|
10291
|
-
await mkdir17(
|
|
10807
|
+
await mkdir17(path39.dirname(outAbs), { recursive: true });
|
|
10292
10808
|
const payload = {
|
|
10293
10809
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10294
10810
|
project_root: root,
|
|
@@ -10300,11 +10816,11 @@ async function writeRoiReport(paths, root, sinceRaw, outRelative) {
|
|
|
10300
10816
|
top_memory_reads: memoryHitsLeader,
|
|
10301
10817
|
roi_hints: roiHints
|
|
10302
10818
|
};
|
|
10303
|
-
await
|
|
10819
|
+
await writeFile29(outAbs, JSON.stringify(payload, null, 2), "utf8");
|
|
10304
10820
|
ui.success(`Wrote ROI / usage rollup \u2192 ${outAbs}`);
|
|
10305
10821
|
}
|
|
10306
10822
|
async function renderMemoryHits(paths, opts) {
|
|
10307
|
-
const index = await
|
|
10823
|
+
const index = await loadUsageIndex24(paths);
|
|
10308
10824
|
const since = parseSince(opts.since ?? "30d");
|
|
10309
10825
|
const sinceMs = since ? new Date(since).getTime() : null;
|
|
10310
10826
|
const entries = Object.entries(index.by_id).map(([id, usage]) => ({ id, ...usage })).filter((e) => e.read_count > 0).filter((e) => {
|
|
@@ -10352,13 +10868,13 @@ import { performance } from "perf_hooks";
|
|
|
10352
10868
|
import "commander";
|
|
10353
10869
|
import {
|
|
10354
10870
|
estimateTokens as estimateTokens3,
|
|
10355
|
-
findProjectRoot as
|
|
10356
|
-
resolveHaivePaths as
|
|
10871
|
+
findProjectRoot as findProjectRoot37,
|
|
10872
|
+
resolveHaivePaths as resolveHaivePaths34
|
|
10357
10873
|
} from "@hiveai/core";
|
|
10358
10874
|
function registerBench(program2) {
|
|
10359
10875
|
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) => {
|
|
10360
|
-
const root =
|
|
10361
|
-
const paths =
|
|
10876
|
+
const root = findProjectRoot37(opts.dir);
|
|
10877
|
+
const paths = resolveHaivePaths34(root);
|
|
10362
10878
|
const ctx = { paths };
|
|
10363
10879
|
const task = opts.task ?? "audit dependencies for security risks";
|
|
10364
10880
|
const scenarios = [
|
|
@@ -10477,11 +10993,11 @@ function summarize(name, t0, payload, notes) {
|
|
|
10477
10993
|
}
|
|
10478
10994
|
|
|
10479
10995
|
// src/commands/benchmark.ts
|
|
10480
|
-
import { existsSync as
|
|
10481
|
-
import { readdir as readdir5, readFile as
|
|
10482
|
-
import
|
|
10996
|
+
import { existsSync as existsSync59 } from "fs";
|
|
10997
|
+
import { readdir as readdir5, readFile as readFile17, writeFile as writeFile30 } from "fs/promises";
|
|
10998
|
+
import path40 from "path";
|
|
10483
10999
|
import "commander";
|
|
10484
|
-
import { estimateTokens as estimateTokens4, findProjectRoot as
|
|
11000
|
+
import { estimateTokens as estimateTokens4, findProjectRoot as findProjectRoot38 } from "@hiveai/core";
|
|
10485
11001
|
function registerBenchmark(program2) {
|
|
10486
11002
|
const benchmark = program2.command("benchmark").description("Official hAIve benchmark/demo utilities for measuring agent enforcement value.");
|
|
10487
11003
|
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) => {
|
|
@@ -10494,9 +11010,9 @@ function registerBenchmark(program2) {
|
|
|
10494
11010
|
}
|
|
10495
11011
|
const markdown = renderMarkdown(root, summary, rows);
|
|
10496
11012
|
if (opts.out) {
|
|
10497
|
-
const outFile =
|
|
10498
|
-
await
|
|
10499
|
-
ui.success(`wrote ${
|
|
11013
|
+
const outFile = path40.isAbsolute(opts.out) ? opts.out : path40.join(root, opts.out);
|
|
11014
|
+
await writeFile30(outFile, markdown, "utf8");
|
|
11015
|
+
ui.success(`wrote ${path40.relative(process.cwd(), outFile)}`);
|
|
10500
11016
|
return;
|
|
10501
11017
|
}
|
|
10502
11018
|
console.log(markdown);
|
|
@@ -10520,20 +11036,20 @@ function registerBenchmark(program2) {
|
|
|
10520
11036
|
}
|
|
10521
11037
|
function resolveBenchmarkRoot(dir) {
|
|
10522
11038
|
const candidate = dir ?? "benchmarks/agent-benchmark";
|
|
10523
|
-
if (
|
|
10524
|
-
const projectRoot =
|
|
10525
|
-
return
|
|
11039
|
+
if (path40.isAbsolute(candidate)) return candidate;
|
|
11040
|
+
const projectRoot = findProjectRoot38(process.cwd());
|
|
11041
|
+
return path40.join(projectRoot, candidate);
|
|
10526
11042
|
}
|
|
10527
11043
|
async function collectRows(root) {
|
|
10528
|
-
if (!
|
|
11044
|
+
if (!existsSync59(root)) throw new Error(`Benchmark directory not found: ${root}`);
|
|
10529
11045
|
const entries = await readdir5(root, { withFileTypes: true });
|
|
10530
11046
|
const rows = [];
|
|
10531
11047
|
for (const entry of entries) {
|
|
10532
11048
|
if (!entry.isDirectory()) continue;
|
|
10533
|
-
const fixtureDir =
|
|
10534
|
-
const reportFile =
|
|
10535
|
-
if (!
|
|
10536
|
-
const report = await
|
|
11049
|
+
const fixtureDir = path40.join(root, entry.name);
|
|
11050
|
+
const reportFile = path40.join(fixtureDir, "BENCHMARK_AGENT_REPORT.md");
|
|
11051
|
+
if (!existsSync59(reportFile)) continue;
|
|
11052
|
+
const report = await readFile17(reportFile, "utf8");
|
|
10537
11053
|
rows.push(parseAgentReport(entry.name, report));
|
|
10538
11054
|
}
|
|
10539
11055
|
return rows.sort((a, b) => a.fixture.localeCompare(b.fixture));
|
|
@@ -10622,20 +11138,21 @@ function escapeRegExp(value) {
|
|
|
10622
11138
|
}
|
|
10623
11139
|
|
|
10624
11140
|
// src/commands/memory-suggest.ts
|
|
10625
|
-
import { mkdir as mkdir18, writeFile as
|
|
10626
|
-
import { existsSync as
|
|
10627
|
-
import
|
|
11141
|
+
import { mkdir as mkdir18, writeFile as writeFile31 } from "fs/promises";
|
|
11142
|
+
import { existsSync as existsSync60 } from "fs";
|
|
11143
|
+
import path41 from "path";
|
|
10628
11144
|
import "commander";
|
|
10629
11145
|
import {
|
|
10630
11146
|
aggregateUsage as aggregateUsage2,
|
|
10631
11147
|
buildFrontmatter as buildFrontmatter11,
|
|
10632
|
-
findProjectRoot as
|
|
10633
|
-
|
|
11148
|
+
findProjectRoot as findProjectRoot39,
|
|
11149
|
+
loadConfig as loadConfig9,
|
|
11150
|
+
loadMemoriesFromDir as loadMemoriesFromDir31,
|
|
10634
11151
|
memoryFilePath as memoryFilePath10,
|
|
10635
11152
|
parseSince as parseSince2,
|
|
10636
11153
|
readUsageEvents as readUsageEvents3,
|
|
10637
|
-
resolveHaivePaths as
|
|
10638
|
-
serializeMemory as
|
|
11154
|
+
resolveHaivePaths as resolveHaivePaths35,
|
|
11155
|
+
serializeMemory as serializeMemory25
|
|
10639
11156
|
} from "@hiveai/core";
|
|
10640
11157
|
var SEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
10641
11158
|
"mem_search",
|
|
@@ -10643,12 +11160,16 @@ var SEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
|
10643
11160
|
"mem_relevant_to",
|
|
10644
11161
|
"get_briefing"
|
|
10645
11162
|
]);
|
|
11163
|
+
var SYNTHETIC_QUERY_RE = /\b(auto-promote-marker|local enforcement smoke|cli-test-session)\b/i;
|
|
11164
|
+
function isSyntheticSuggestionQuery(query) {
|
|
11165
|
+
return SYNTHETIC_QUERY_RE.test(query);
|
|
11166
|
+
}
|
|
10646
11167
|
function registerMemorySuggest(memory2) {
|
|
10647
11168
|
memory2.command("suggest").description(
|
|
10648
|
-
"Suggest memories to create based on recurring search queries in the usage log.\n\n Use --auto-save to
|
|
10649
|
-
).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
|
|
10650
|
-
const root =
|
|
10651
|
-
const paths =
|
|
11169
|
+
"Suggest memories to create based on recurring search queries in the usage log.\n\n Use --auto-save to save the top-N suggestions using the project defaults.\n In autopilot, suggestions land as validated team records; in manual mode they stay draft."
|
|
11170
|
+
).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 saved memories (personal | team; default: config default)").option("--auto-save", "save top-N suggestions as memories on disk", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
11171
|
+
const root = findProjectRoot39(opts.dir);
|
|
11172
|
+
const paths = resolveHaivePaths35(root);
|
|
10652
11173
|
const events = await readUsageEvents3(paths);
|
|
10653
11174
|
if (events.length === 0) {
|
|
10654
11175
|
if (opts.json) {
|
|
@@ -10667,6 +11188,7 @@ function registerMemorySuggest(memory2) {
|
|
|
10667
11188
|
if (!SEARCH_TOOLS.has(e.tool)) continue;
|
|
10668
11189
|
const key = (e.summary ?? "").toLowerCase().trim();
|
|
10669
11190
|
if (!key) continue;
|
|
11191
|
+
if (isSyntheticSuggestionQuery(key)) continue;
|
|
10670
11192
|
const prior = queries.get(key);
|
|
10671
11193
|
if (prior) {
|
|
10672
11194
|
prior.count++;
|
|
@@ -10685,16 +11207,18 @@ function registerMemorySuggest(memory2) {
|
|
|
10685
11207
|
inferred_type: inferType(v.tools, query)
|
|
10686
11208
|
})).sort((a, b) => b.count - a.count);
|
|
10687
11209
|
if (opts.autoSave) {
|
|
11210
|
+
const config = await loadConfig9(paths);
|
|
10688
11211
|
const topN = Math.max(1, parseInt(opts.topN ?? "3", 10));
|
|
10689
|
-
const scope = opts.scope === "
|
|
11212
|
+
const scope = opts.scope === "personal" || opts.scope === "team" ? opts.scope : config.defaultScope ?? "personal";
|
|
11213
|
+
const status = config.defaultStatus === "validated" ? "validated" : "draft";
|
|
10690
11214
|
const top = suggestions.slice(0, topN);
|
|
10691
11215
|
if (top.length === 0) {
|
|
10692
|
-
ui.warn(`No suggestions met --min=${minCount} \u2014 nothing to
|
|
11216
|
+
ui.warn(`No suggestions met --min=${minCount} \u2014 nothing to save.`);
|
|
10693
11217
|
return;
|
|
10694
11218
|
}
|
|
10695
11219
|
const created = [];
|
|
10696
11220
|
const skipped = [];
|
|
10697
|
-
const existing =
|
|
11221
|
+
const existing = existsSync60(paths.memoriesDir) ? await loadMemoriesFromDir31(paths.memoriesDir) : [];
|
|
10698
11222
|
for (const s of top) {
|
|
10699
11223
|
const slug = slugify(s.query);
|
|
10700
11224
|
if (!slug) {
|
|
@@ -10712,25 +11236,25 @@ function registerMemorySuggest(memory2) {
|
|
|
10712
11236
|
scope,
|
|
10713
11237
|
tags: ["auto-suggested", ...s.tools],
|
|
10714
11238
|
paths: [],
|
|
10715
|
-
symbols: []
|
|
11239
|
+
symbols: [],
|
|
11240
|
+
status
|
|
10716
11241
|
});
|
|
10717
|
-
fm.status
|
|
10718
|
-
const body = renderTemplate(s);
|
|
11242
|
+
const body = renderTemplate(s, fm.id, status);
|
|
10719
11243
|
const file = memoryFilePath10(paths, fm.scope, fm.id, fm.module);
|
|
10720
|
-
await mkdir18(
|
|
10721
|
-
if (
|
|
10722
|
-
skipped.push({ query: s.query, reason: `file already exists at ${
|
|
11244
|
+
await mkdir18(path41.dirname(file), { recursive: true });
|
|
11245
|
+
if (existsSync60(file)) {
|
|
11246
|
+
skipped.push({ query: s.query, reason: `file already exists at ${path41.relative(root, file)}` });
|
|
10723
11247
|
continue;
|
|
10724
11248
|
}
|
|
10725
|
-
await
|
|
10726
|
-
created.push({ id: fm.id, file:
|
|
11249
|
+
await writeFile31(file, serializeMemory25({ frontmatter: fm, body }), "utf8");
|
|
11250
|
+
created.push({ id: fm.id, file: path41.relative(root, file), query: s.query });
|
|
10727
11251
|
}
|
|
10728
11252
|
if (opts.json) {
|
|
10729
11253
|
console.log(JSON.stringify({ created, skipped }, null, 2));
|
|
10730
11254
|
return;
|
|
10731
11255
|
}
|
|
10732
11256
|
for (const c of created) {
|
|
10733
|
-
ui.success(
|
|
11257
|
+
ui.success(`${status === "validated" ? "Saved" : "Drafted"} ${c.id} \u2192 ${c.file}`);
|
|
10734
11258
|
console.log(` ${ui.dim("from query:")} ${truncate2(c.query, 60)}`);
|
|
10735
11259
|
}
|
|
10736
11260
|
for (const s of skipped) {
|
|
@@ -10738,7 +11262,11 @@ function registerMemorySuggest(memory2) {
|
|
|
10738
11262
|
}
|
|
10739
11263
|
if (created.length > 0) {
|
|
10740
11264
|
console.log();
|
|
10741
|
-
|
|
11265
|
+
if (status === "validated") {
|
|
11266
|
+
ui.info("Autopilot defaults applied: suggestions are status=validated and active.");
|
|
11267
|
+
} else {
|
|
11268
|
+
ui.info("Drafts are status=draft \u2014 edit them, then run `haive memory promote <id>`.");
|
|
11269
|
+
}
|
|
10742
11270
|
}
|
|
10743
11271
|
return;
|
|
10744
11272
|
}
|
|
@@ -10763,7 +11291,7 @@ function registerMemorySuggest(memory2) {
|
|
|
10763
11291
|
console.log(` ${ui.dim("\u2192")} ${s.reason}`);
|
|
10764
11292
|
}
|
|
10765
11293
|
console.log();
|
|
10766
|
-
ui.info("Run with --auto-save to
|
|
11294
|
+
ui.info("Run with --auto-save to save the top-3 using the project defaults.");
|
|
10767
11295
|
});
|
|
10768
11296
|
}
|
|
10769
11297
|
function chooseReason(tools, count) {
|
|
@@ -10784,7 +11312,8 @@ function inferType(tools, query) {
|
|
|
10784
11312
|
}
|
|
10785
11313
|
return "convention";
|
|
10786
11314
|
}
|
|
10787
|
-
function renderTemplate(s) {
|
|
11315
|
+
function renderTemplate(s, id, status) {
|
|
11316
|
+
const nextStep = status === "validated" ? `This record is already active because project autopilot defaults set status=validated. Replace the template body with the actual answer when known.` : `Then run \`haive memory promote ${id}\` to move it into team review.`;
|
|
10788
11317
|
return [
|
|
10789
11318
|
`# Auto-drafted from recurring searches`,
|
|
10790
11319
|
``,
|
|
@@ -10804,7 +11333,7 @@ function renderTemplate(s) {
|
|
|
10804
11333
|
`- **Why** \u2014 the rationale or root cause`,
|
|
10805
11334
|
`- **How to apply** \u2014 what an agent should do when this comes up again`,
|
|
10806
11335
|
``,
|
|
10807
|
-
|
|
11336
|
+
nextStep
|
|
10808
11337
|
].join("\n");
|
|
10809
11338
|
}
|
|
10810
11339
|
function slugify(s) {
|
|
@@ -10816,26 +11345,26 @@ function truncate2(text, max) {
|
|
|
10816
11345
|
}
|
|
10817
11346
|
|
|
10818
11347
|
// src/commands/memory-archive.ts
|
|
10819
|
-
import { existsSync as
|
|
10820
|
-
import { writeFile as
|
|
10821
|
-
import
|
|
11348
|
+
import { existsSync as existsSync61 } from "fs";
|
|
11349
|
+
import { writeFile as writeFile33 } from "fs/promises";
|
|
11350
|
+
import path43 from "path";
|
|
10822
11351
|
import "commander";
|
|
10823
11352
|
import {
|
|
10824
|
-
findProjectRoot as
|
|
10825
|
-
getUsage as
|
|
10826
|
-
loadMemoriesFromDir as
|
|
10827
|
-
loadUsageIndex as
|
|
10828
|
-
resolveHaivePaths as
|
|
10829
|
-
serializeMemory as
|
|
11353
|
+
findProjectRoot as findProjectRoot40,
|
|
11354
|
+
getUsage as getUsage19,
|
|
11355
|
+
loadMemoriesFromDir as loadMemoriesFromDir32,
|
|
11356
|
+
loadUsageIndex as loadUsageIndex25,
|
|
11357
|
+
resolveHaivePaths as resolveHaivePaths36,
|
|
11358
|
+
serializeMemory as serializeMemory26
|
|
10830
11359
|
} from "@hiveai/core";
|
|
10831
11360
|
var MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
|
|
10832
11361
|
function registerMemoryArchive(memory2) {
|
|
10833
11362
|
memory2.command("archive").description(
|
|
10834
11363
|
"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."
|
|
10835
11364
|
).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) => {
|
|
10836
|
-
const root =
|
|
10837
|
-
const paths =
|
|
10838
|
-
if (!
|
|
11365
|
+
const root = findProjectRoot40(opts.dir);
|
|
11366
|
+
const paths = resolveHaivePaths36(root);
|
|
11367
|
+
if (!existsSync61(paths.memoriesDir)) {
|
|
10839
11368
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
10840
11369
|
process.exitCode = 1;
|
|
10841
11370
|
return;
|
|
@@ -10847,8 +11376,8 @@ function registerMemoryArchive(memory2) {
|
|
|
10847
11376
|
return;
|
|
10848
11377
|
}
|
|
10849
11378
|
const cutoff = Date.now() - minDays * MS_PER_DAY2;
|
|
10850
|
-
const all = await
|
|
10851
|
-
const usage = await
|
|
11379
|
+
const all = await loadMemoriesFromDir32(paths.memoriesDir);
|
|
11380
|
+
const usage = await loadUsageIndex25(paths);
|
|
10852
11381
|
const typeFilter = opts.type === "all" ? null : opts.type ?? "attempt";
|
|
10853
11382
|
const candidates = [];
|
|
10854
11383
|
for (const { memory: mem, filePath } of all) {
|
|
@@ -10856,10 +11385,10 @@ function registerMemoryArchive(memory2) {
|
|
|
10856
11385
|
if (typeFilter && fm.type !== typeFilter) continue;
|
|
10857
11386
|
if (fm.status === "deprecated" || fm.status === "rejected") continue;
|
|
10858
11387
|
const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
|
|
10859
|
-
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !
|
|
11388
|
+
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync61(path43.join(paths.root, p)));
|
|
10860
11389
|
const isAnchorless = !hasAnyAnchor;
|
|
10861
11390
|
if (!isAnchorless && !allPathsGone) continue;
|
|
10862
|
-
const u =
|
|
11391
|
+
const u = getUsage19(usage, fm.id);
|
|
10863
11392
|
const lastSeen = u.last_read_at ?? fm.created_at;
|
|
10864
11393
|
if (Date.parse(lastSeen) >= cutoff) continue;
|
|
10865
11394
|
candidates.push({
|
|
@@ -10904,7 +11433,7 @@ function registerMemoryArchive(memory2) {
|
|
|
10904
11433
|
if (!found) continue;
|
|
10905
11434
|
const fm = { ...found.memory.frontmatter, status: "deprecated" };
|
|
10906
11435
|
try {
|
|
10907
|
-
await
|
|
11436
|
+
await writeFile33(c.filePath, serializeMemory26({ frontmatter: fm, body: found.memory.body }), "utf8");
|
|
10908
11437
|
archived++;
|
|
10909
11438
|
} catch (err) {
|
|
10910
11439
|
if (!opts.json) {
|
|
@@ -10930,31 +11459,33 @@ function parseDays(input) {
|
|
|
10930
11459
|
}
|
|
10931
11460
|
|
|
10932
11461
|
// src/commands/doctor.ts
|
|
10933
|
-
import { existsSync as
|
|
10934
|
-
import { readFile as
|
|
10935
|
-
import
|
|
11462
|
+
import { existsSync as existsSync63 } from "fs";
|
|
11463
|
+
import { readFile as readFile18, stat } from "fs/promises";
|
|
11464
|
+
import path44 from "path";
|
|
10936
11465
|
import { execFileSync, execSync as execSync3 } from "child_process";
|
|
10937
11466
|
import "commander";
|
|
10938
11467
|
import {
|
|
10939
11468
|
codeMapPath as codeMapPath2,
|
|
10940
|
-
findProjectRoot as
|
|
10941
|
-
getUsage as
|
|
10942
|
-
loadCodeMap as
|
|
10943
|
-
loadConfig as
|
|
10944
|
-
loadMemoriesFromDir as
|
|
10945
|
-
loadUsageIndex as
|
|
11469
|
+
findProjectRoot as findProjectRoot41,
|
|
11470
|
+
getUsage as getUsage20,
|
|
11471
|
+
loadCodeMap as loadCodeMap7,
|
|
11472
|
+
loadConfig as loadConfig10,
|
|
11473
|
+
loadMemoriesFromDir as loadMemoriesFromDir33,
|
|
11474
|
+
loadUsageIndex as loadUsageIndex26,
|
|
10946
11475
|
readUsageEvents as readUsageEvents4,
|
|
10947
|
-
resolveHaivePaths as
|
|
11476
|
+
resolveHaivePaths as resolveHaivePaths37
|
|
10948
11477
|
} from "@hiveai/core";
|
|
10949
11478
|
var MS_PER_DAY3 = 24 * 60 * 60 * 1e3;
|
|
10950
11479
|
function registerDoctor(program2) {
|
|
10951
11480
|
program2.command("doctor").description(
|
|
10952
11481
|
"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."
|
|
10953
11482
|
).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) => {
|
|
10954
|
-
const root =
|
|
10955
|
-
const paths =
|
|
11483
|
+
const root = findProjectRoot41(opts.dir);
|
|
11484
|
+
const paths = resolveHaivePaths37(root);
|
|
10956
11485
|
const findings = [];
|
|
10957
|
-
|
|
11486
|
+
const repairs = [];
|
|
11487
|
+
const config = await loadConfig10(paths);
|
|
11488
|
+
if (!existsSync63(paths.haiveDir)) {
|
|
10958
11489
|
findings.push({
|
|
10959
11490
|
severity: "error",
|
|
10960
11491
|
code: "not-initialized",
|
|
@@ -10963,7 +11494,18 @@ function registerDoctor(program2) {
|
|
|
10963
11494
|
});
|
|
10964
11495
|
return emit(findings, opts);
|
|
10965
11496
|
}
|
|
10966
|
-
if (!
|
|
11497
|
+
if (opts.fix && !opts.dryRun) {
|
|
11498
|
+
repairs.push(
|
|
11499
|
+
...await applyAutopilotRepairs(root, paths, {
|
|
11500
|
+
applyConfig: true,
|
|
11501
|
+
applyContext: true,
|
|
11502
|
+
applyCorpus: true,
|
|
11503
|
+
applyCodeMap: true,
|
|
11504
|
+
applyCodeSearch: true
|
|
11505
|
+
})
|
|
11506
|
+
);
|
|
11507
|
+
}
|
|
11508
|
+
if (!existsSync63(paths.projectContext)) {
|
|
10967
11509
|
findings.push({
|
|
10968
11510
|
severity: "warn",
|
|
10969
11511
|
code: "no-project-context",
|
|
@@ -10971,8 +11513,8 @@ function registerDoctor(program2) {
|
|
|
10971
11513
|
fix: "haive init"
|
|
10972
11514
|
});
|
|
10973
11515
|
} else {
|
|
10974
|
-
const { readFile:
|
|
10975
|
-
const content = await
|
|
11516
|
+
const { readFile: readFile20 } = await import("fs/promises");
|
|
11517
|
+
const content = await readFile20(paths.projectContext, "utf8");
|
|
10976
11518
|
const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
|
|
10977
11519
|
if (isTemplate) {
|
|
10978
11520
|
findings.push({
|
|
@@ -10982,8 +11524,17 @@ function registerDoctor(program2) {
|
|
|
10982
11524
|
fix: "Invoke the bootstrap_project MCP prompt from your AI client."
|
|
10983
11525
|
});
|
|
10984
11526
|
}
|
|
11527
|
+
const versionStatus = await projectContextVersionStatus(root, paths);
|
|
11528
|
+
if (versionStatus.mismatch) {
|
|
11529
|
+
findings.push({
|
|
11530
|
+
severity: "warn",
|
|
11531
|
+
code: "project-context-version-mismatch",
|
|
11532
|
+
message: `.ai/project-context.md version metadata (${versionStatus.currentVersion ?? "missing"}) does not match package.json (${versionStatus.expectedVersion}).`,
|
|
11533
|
+
fix: "haive doctor --fix"
|
|
11534
|
+
});
|
|
11535
|
+
}
|
|
10985
11536
|
}
|
|
10986
|
-
const memories =
|
|
11537
|
+
const memories = existsSync63(paths.memoriesDir) ? await loadMemoriesFromDir33(paths.memoriesDir) : [];
|
|
10987
11538
|
const now = Date.now();
|
|
10988
11539
|
if (memories.length === 0) {
|
|
10989
11540
|
findings.push({
|
|
@@ -10992,7 +11543,7 @@ function registerDoctor(program2) {
|
|
|
10992
11543
|
message: "No memories yet. Capture knowledge as agents work via mem_save / mem_observe / mem_tried."
|
|
10993
11544
|
});
|
|
10994
11545
|
} else {
|
|
10995
|
-
const usage = await
|
|
11546
|
+
const usage = await loadUsageIndex26(paths);
|
|
10996
11547
|
const stale = memories.filter((m) => m.memory.frontmatter.status === "stale");
|
|
10997
11548
|
if (stale.length > 0) {
|
|
10998
11549
|
findings.push({
|
|
@@ -11024,7 +11575,7 @@ function registerDoctor(program2) {
|
|
|
11024
11575
|
}
|
|
11025
11576
|
const decayCandidates = memories.filter((m) => {
|
|
11026
11577
|
if (m.memory.frontmatter.status !== "validated") return false;
|
|
11027
|
-
const u =
|
|
11578
|
+
const u = getUsage20(usage, m.memory.frontmatter.id);
|
|
11028
11579
|
const last = u.last_read_at ?? m.memory.frontmatter.created_at;
|
|
11029
11580
|
return (now - Date.parse(last)) / MS_PER_DAY3 > 180;
|
|
11030
11581
|
});
|
|
@@ -11037,7 +11588,19 @@ function registerDoctor(program2) {
|
|
|
11037
11588
|
});
|
|
11038
11589
|
}
|
|
11039
11590
|
}
|
|
11040
|
-
const
|
|
11591
|
+
const lintReport = await lintMemoriesAsync(root);
|
|
11592
|
+
if (lintReport.findings.length > 0) {
|
|
11593
|
+
const warnCount = lintReport.findings.filter((finding) => finding.severity === "warn").length;
|
|
11594
|
+
const errorCount = lintReport.findings.filter((finding) => finding.severity === "error").length;
|
|
11595
|
+
const severity = errorCount > 0 ? "error" : warnCount > 0 ? "warn" : "info";
|
|
11596
|
+
findings.push({
|
|
11597
|
+
severity,
|
|
11598
|
+
code: "memory-lint-findings",
|
|
11599
|
+
message: `memory lint reports ${lintReport.findings.length} finding${lintReport.findings.length === 1 ? "" : "s"} (${errorCount} error, ${warnCount} warn, ${lintReport.findings.length - errorCount - warnCount} info).`,
|
|
11600
|
+
fix: "haive memory lint --fix --apply"
|
|
11601
|
+
});
|
|
11602
|
+
}
|
|
11603
|
+
const codeMap = await loadCodeMap7(paths);
|
|
11041
11604
|
if (!codeMap) {
|
|
11042
11605
|
findings.push({
|
|
11043
11606
|
severity: "warn",
|
|
@@ -11058,6 +11621,7 @@ function registerDoctor(program2) {
|
|
|
11058
11621
|
});
|
|
11059
11622
|
}
|
|
11060
11623
|
}
|
|
11624
|
+
findings.push(...await collectSemanticIndexFindings(paths, config, memories.length, codeMap));
|
|
11061
11625
|
const events = await readUsageEvents4(paths);
|
|
11062
11626
|
if (events.length === 0) {
|
|
11063
11627
|
findings.push({
|
|
@@ -11071,6 +11635,7 @@ function registerDoctor(program2) {
|
|
|
11071
11635
|
if (!isSearchTool(e.tool)) continue;
|
|
11072
11636
|
const key = (e.summary ?? "").toLowerCase().trim();
|
|
11073
11637
|
if (!key) continue;
|
|
11638
|
+
if (isSyntheticSuggestionQuery(key)) continue;
|
|
11074
11639
|
queryRepeats.set(key, (queryRepeats.get(key) ?? 0) + 1);
|
|
11075
11640
|
}
|
|
11076
11641
|
const repeated = [...queryRepeats.entries()].filter(([, n]) => n >= 3);
|
|
@@ -11092,14 +11657,13 @@ function registerDoctor(program2) {
|
|
|
11092
11657
|
});
|
|
11093
11658
|
}
|
|
11094
11659
|
}
|
|
11095
|
-
const config = await loadConfig7(paths);
|
|
11096
11660
|
if (config.enforcement?.requireBriefingFirst) {
|
|
11097
|
-
const claudeSettings =
|
|
11661
|
+
const claudeSettings = path44.join(root, ".claude", "settings.local.json");
|
|
11098
11662
|
let hasClaudeEnforcement = false;
|
|
11099
|
-
if (
|
|
11663
|
+
if (existsSync63(claudeSettings)) {
|
|
11100
11664
|
try {
|
|
11101
|
-
const { readFile:
|
|
11102
|
-
const raw = await
|
|
11665
|
+
const { readFile: readFile20 } = await import("fs/promises");
|
|
11666
|
+
const raw = await readFile20(claudeSettings, "utf8");
|
|
11103
11667
|
hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
|
|
11104
11668
|
} catch {
|
|
11105
11669
|
hasClaudeEnforcement = false;
|
|
@@ -11122,14 +11686,14 @@ function registerDoctor(program2) {
|
|
|
11122
11686
|
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
|
|
11123
11687
|
});
|
|
11124
11688
|
}
|
|
11125
|
-
findings.push(...await collectInstallFindings(root, "0.9.
|
|
11689
|
+
findings.push(...await collectInstallFindings(root, "0.9.19"));
|
|
11126
11690
|
try {
|
|
11127
11691
|
const legacyRaw = execSync3("haive-mcp --version", {
|
|
11128
11692
|
encoding: "utf8",
|
|
11129
11693
|
timeout: 3e3,
|
|
11130
11694
|
stdio: ["ignore", "pipe", "ignore"]
|
|
11131
11695
|
}).trim();
|
|
11132
|
-
const cliVersion = "0.9.
|
|
11696
|
+
const cliVersion = "0.9.19";
|
|
11133
11697
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
11134
11698
|
findings.push({
|
|
11135
11699
|
severity: "warn",
|
|
@@ -11142,10 +11706,18 @@ npm uninstall -g @hiveai/mcp`
|
|
|
11142
11706
|
}
|
|
11143
11707
|
} catch {
|
|
11144
11708
|
}
|
|
11145
|
-
|
|
11709
|
+
if (repairs.length > 0) {
|
|
11710
|
+
findings.push({
|
|
11711
|
+
severity: "info",
|
|
11712
|
+
code: "autopilot-repairs-applied",
|
|
11713
|
+
message: repairs.map((repair) => repair.message).join(" "),
|
|
11714
|
+
section: "Next actions"
|
|
11715
|
+
});
|
|
11716
|
+
}
|
|
11717
|
+
emit(findings, opts, repairs);
|
|
11146
11718
|
});
|
|
11147
11719
|
}
|
|
11148
|
-
function emit(findings, opts) {
|
|
11720
|
+
function emit(findings, opts, repairs = []) {
|
|
11149
11721
|
const classified = findings.map((finding) => ({
|
|
11150
11722
|
...finding,
|
|
11151
11723
|
section: finding.section ?? sectionForFinding(finding)
|
|
@@ -11157,12 +11729,13 @@ function emit(findings, opts) {
|
|
|
11157
11729
|
findings: classified,
|
|
11158
11730
|
sections: groupBySection(classified),
|
|
11159
11731
|
next_actions: nextActions(classified),
|
|
11160
|
-
|
|
11732
|
+
repairs,
|
|
11733
|
+
fix_mode: opts.fix ? opts.dryRun ? "dry-run" : "apply" : "off"
|
|
11161
11734
|
}, null, 2));
|
|
11162
11735
|
return;
|
|
11163
11736
|
}
|
|
11164
11737
|
if (classified.length === 0) {
|
|
11165
|
-
ui.success("hAIve doctor \u2014 no issues found.");
|
|
11738
|
+
ui.success(repairs.length > 0 ? "hAIve doctor \u2014 autopilot repairs applied." : "hAIve doctor \u2014 no issues found.");
|
|
11166
11739
|
return;
|
|
11167
11740
|
}
|
|
11168
11741
|
console.log(ui.bold(`hAIve doctor \u2014 ${classified.length} finding${classified.length === 1 ? "" : "s"}`));
|
|
@@ -11198,6 +11771,11 @@ function emit(findings, opts) {
|
|
|
11198
11771
|
}
|
|
11199
11772
|
console.log();
|
|
11200
11773
|
}
|
|
11774
|
+
if (repairs.length > 0) {
|
|
11775
|
+
console.log(ui.bold("Autopilot repairs applied"));
|
|
11776
|
+
for (const repair of repairs) console.log(` ${ui.dim("\u2713")} ${repair.message}`);
|
|
11777
|
+
console.log();
|
|
11778
|
+
}
|
|
11201
11779
|
const actions = nextActions(classified);
|
|
11202
11780
|
if (actions.length > 0) {
|
|
11203
11781
|
console.log(ui.bold("Next actions"));
|
|
@@ -11245,6 +11823,56 @@ function groupBySection(findings) {
|
|
|
11245
11823
|
function nextActions(findings) {
|
|
11246
11824
|
return [...new Set(findings.flatMap((finding) => finding.fix ? finding.fix.split("\n") : []))].filter(Boolean);
|
|
11247
11825
|
}
|
|
11826
|
+
async function collectSemanticIndexFindings(paths, config, memoryCount, codeMap) {
|
|
11827
|
+
const findings = [];
|
|
11828
|
+
const autoWantsCodeSearch = Boolean(config.autopilot || config.autoRepair?.codeSearch);
|
|
11829
|
+
let mod;
|
|
11830
|
+
try {
|
|
11831
|
+
mod = await import("@hiveai/embeddings");
|
|
11832
|
+
} catch {
|
|
11833
|
+
findings.push({
|
|
11834
|
+
severity: autoWantsCodeSearch ? "warn" : "info",
|
|
11835
|
+
code: "embeddings-unavailable",
|
|
11836
|
+
message: "@hiveai/embeddings is not available, so get_briefing falls back to lexical ranking and code_search cannot run.",
|
|
11837
|
+
fix: "npm install -g @hiveai/cli@latest\nhaive embeddings status",
|
|
11838
|
+
section: "Index health"
|
|
11839
|
+
});
|
|
11840
|
+
return findings;
|
|
11841
|
+
}
|
|
11842
|
+
if (memoryCount > 0) {
|
|
11843
|
+
const stat2 = await mod.indexStat(paths).catch(() => ({ exists: false, count: 0 }));
|
|
11844
|
+
if (!stat2.exists || stat2.count === 0) {
|
|
11845
|
+
findings.push({
|
|
11846
|
+
severity: "warn",
|
|
11847
|
+
code: "semantic-memory-index-missing",
|
|
11848
|
+
message: "Memory embeddings index is missing or empty; get_briefing will report literal_fallback instead of semantic ranking.",
|
|
11849
|
+
fix: "haive embeddings index",
|
|
11850
|
+
section: "Index health"
|
|
11851
|
+
});
|
|
11852
|
+
}
|
|
11853
|
+
}
|
|
11854
|
+
if (autoWantsCodeSearch || codeMap) {
|
|
11855
|
+
const codeIndex = await mod.loadCodeIndex(paths).catch(() => null);
|
|
11856
|
+
if (!codeIndex || codeIndex.entries.length === 0) {
|
|
11857
|
+
findings.push({
|
|
11858
|
+
severity: autoWantsCodeSearch ? "warn" : "info",
|
|
11859
|
+
code: "code-search-index-missing",
|
|
11860
|
+
message: "Code-search embeddings index is missing or empty; MCP code_search is unavailable until it is built.",
|
|
11861
|
+
fix: "haive index code-search",
|
|
11862
|
+
section: "Index health"
|
|
11863
|
+
});
|
|
11864
|
+
} else if (codeMap && codeIndex.source_generated_at !== codeMap.generated_at) {
|
|
11865
|
+
findings.push({
|
|
11866
|
+
severity: "info",
|
|
11867
|
+
code: "code-search-index-outdated",
|
|
11868
|
+
message: "Code-search embeddings index was built from an older code-map; semantic code search may miss recent symbols.",
|
|
11869
|
+
fix: "haive index code-search",
|
|
11870
|
+
section: "Index health"
|
|
11871
|
+
});
|
|
11872
|
+
}
|
|
11873
|
+
}
|
|
11874
|
+
return findings;
|
|
11875
|
+
}
|
|
11248
11876
|
function isSearchTool(name) {
|
|
11249
11877
|
return ["mem_search", "code_search", "mem_relevant_to", "get_briefing"].includes(name);
|
|
11250
11878
|
}
|
|
@@ -11289,9 +11917,9 @@ which -a haive`
|
|
|
11289
11917
|
".vscode/mcp.json"
|
|
11290
11918
|
];
|
|
11291
11919
|
for (const rel of integrationFiles) {
|
|
11292
|
-
const file =
|
|
11293
|
-
if (!
|
|
11294
|
-
const text = await
|
|
11920
|
+
const file = path44.join(root, rel);
|
|
11921
|
+
if (!existsSync63(file)) continue;
|
|
11922
|
+
const text = await readFile18(file, "utf8").catch(() => "");
|
|
11295
11923
|
for (const bin of extractAbsoluteHaiveBins(text)) {
|
|
11296
11924
|
const version = versionForBinary(bin);
|
|
11297
11925
|
if (!version) {
|
|
@@ -11347,22 +11975,22 @@ function extractAbsoluteHaiveBins(text) {
|
|
|
11347
11975
|
}
|
|
11348
11976
|
|
|
11349
11977
|
// src/commands/playback.ts
|
|
11350
|
-
import { existsSync as
|
|
11978
|
+
import { existsSync as existsSync64 } from "fs";
|
|
11351
11979
|
import "commander";
|
|
11352
11980
|
import {
|
|
11353
|
-
findProjectRoot as
|
|
11354
|
-
loadMemoriesFromDir as
|
|
11981
|
+
findProjectRoot as findProjectRoot42,
|
|
11982
|
+
loadMemoriesFromDir as loadMemoriesFromDir34,
|
|
11355
11983
|
parseSince as parseSince3,
|
|
11356
11984
|
readUsageEvents as readUsageEvents5,
|
|
11357
|
-
resolveHaivePaths as
|
|
11985
|
+
resolveHaivePaths as resolveHaivePaths38
|
|
11358
11986
|
} from "@hiveai/core";
|
|
11359
11987
|
var MS_PER_MINUTE = 6e4;
|
|
11360
11988
|
function registerPlayback(program2) {
|
|
11361
11989
|
program2.command("playback").description(
|
|
11362
11990
|
"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?'"
|
|
11363
11991
|
).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) => {
|
|
11364
|
-
const root =
|
|
11365
|
-
const paths =
|
|
11992
|
+
const root = findProjectRoot42(opts.dir);
|
|
11993
|
+
const paths = resolveHaivePaths38(root);
|
|
11366
11994
|
const events = await readUsageEvents5(paths);
|
|
11367
11995
|
if (events.length === 0) {
|
|
11368
11996
|
if (opts.json) {
|
|
@@ -11377,7 +12005,7 @@ function registerPlayback(program2) {
|
|
|
11377
12005
|
const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
|
|
11378
12006
|
const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
|
|
11379
12007
|
const sessions = bucketSessions(filtered, gapMs);
|
|
11380
|
-
const all =
|
|
12008
|
+
const all = existsSync64(paths.memoriesDir) ? await loadMemoriesFromDir34(paths.memoriesDir) : [];
|
|
11381
12009
|
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);
|
|
11382
12010
|
const enriched = sessions.map((s, i) => {
|
|
11383
12011
|
const startMs = Date.parse(s.start);
|
|
@@ -11467,8 +12095,8 @@ function truncate3(text, max) {
|
|
|
11467
12095
|
import { spawn as spawn4 } from "child_process";
|
|
11468
12096
|
import "commander";
|
|
11469
12097
|
import {
|
|
11470
|
-
findProjectRoot as
|
|
11471
|
-
resolveHaivePaths as
|
|
12098
|
+
findProjectRoot as findProjectRoot43,
|
|
12099
|
+
resolveHaivePaths as resolveHaivePaths39
|
|
11472
12100
|
} from "@hiveai/core";
|
|
11473
12101
|
function registerPrecommit(program2) {
|
|
11474
12102
|
program2.command("precommit").description(
|
|
@@ -11478,8 +12106,8 @@ function registerPrecommit(program2) {
|
|
|
11478
12106
|
"'any' | 'high-confidence' (default) | 'never' (report only)",
|
|
11479
12107
|
"high-confidence"
|
|
11480
12108
|
).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) => {
|
|
11481
|
-
const root =
|
|
11482
|
-
const paths =
|
|
12109
|
+
const root = findProjectRoot43(opts.dir);
|
|
12110
|
+
const paths = resolveHaivePaths39(root);
|
|
11483
12111
|
const ctx = { paths };
|
|
11484
12112
|
let diff = "";
|
|
11485
12113
|
let touchedPaths = opts.paths ?? [];
|
|
@@ -11513,401 +12141,142 @@ function registerPrecommit(program2) {
|
|
|
11513
12141
|
` anti-patterns: ${result.summary.anti_patterns} blocking: ${result.summary.blocking_warnings ?? result.summary.anti_patterns} review: ${result.summary.review_warnings ?? 0} info: ${result.summary.info_warnings ?? 0} relevant memories: ${result.summary.relevant_memories} stale anchors: ${result.summary.stale_anchors}`
|
|
11514
12142
|
)
|
|
11515
12143
|
);
|
|
11516
|
-
console.log();
|
|
11517
|
-
const blocking = result.warnings.filter((w) => w.level === "blocking");
|
|
11518
|
-
const review = result.warnings.filter((w) => w.level === "review");
|
|
11519
|
-
const info = result.warnings.filter((w) => w.level === "info");
|
|
11520
|
-
printWarnings("Blocking anti-patterns", blocking, "error");
|
|
11521
|
-
printWarnings("Review anti-patterns", review.slice(0, 8), "warn");
|
|
11522
|
-
if (info.length > 0) {
|
|
11523
|
-
console.log(
|
|
11524
|
-
ui.dim(
|
|
11525
|
-
`${info.length} weak anti-pattern signal${info.length === 1 ? "" : "s"} hidden. Use --json to inspect FYI matches.`
|
|
11526
|
-
)
|
|
11527
|
-
);
|
|
11528
|
-
console.log();
|
|
11529
|
-
}
|
|
11530
|
-
if (result.relevant_memories.length > 0) {
|
|
11531
|
-
console.log(ui.bold("\u{1F4CC} Relevant conventions/decisions:"));
|
|
11532
|
-
for (const m of result.relevant_memories) {
|
|
11533
|
-
console.log(` \u2022 ${m.id} ${ui.dim(`(${m.type}, ${m.confidence})`)}`);
|
|
11534
|
-
}
|
|
11535
|
-
console.log();
|
|
11536
|
-
}
|
|
11537
|
-
if (result.stale_anchors.length > 0) {
|
|
11538
|
-
console.log(ui.bold("\u{1F552} Stale anchored memories:"));
|
|
11539
|
-
for (const s of result.stale_anchors) {
|
|
11540
|
-
console.log(` \u2022 ${s.id}`);
|
|
11541
|
-
if (s.body_preview) console.log(` ${ui.dim(s.body_preview)}`);
|
|
11542
|
-
}
|
|
11543
|
-
console.log();
|
|
11544
|
-
}
|
|
11545
|
-
if (result.should_block) {
|
|
11546
|
-
ui.error(`Blocking commit (block_on=${opts.blockOn ?? "high-confidence"}). Address the warnings above or pass --block-on never to bypass.`);
|
|
11547
|
-
process.exit(1);
|
|
11548
|
-
}
|
|
11549
|
-
if (result.warnings.length === 0 && result.stale_anchors.length === 0) {
|
|
11550
|
-
ui.success("No anti-patterns or stale anchors found.");
|
|
11551
|
-
} else {
|
|
11552
|
-
ui.success("Check passed (block_on threshold not met).");
|
|
11553
|
-
}
|
|
11554
|
-
});
|
|
11555
|
-
}
|
|
11556
|
-
function printWarnings(title, warnings, tone) {
|
|
11557
|
-
if (warnings.length === 0) return;
|
|
11558
|
-
console.log(ui.bold(tone === "error" ? `\u2717 ${title}:` : `\u26A0 ${title}:`));
|
|
11559
|
-
for (const w of warnings) {
|
|
11560
|
-
const marker = tone === "error" ? ui.red("\u2717") : ui.yellow("\u26A0");
|
|
11561
|
-
console.log(` ${marker} ${w.id} ${ui.dim(`(${w.type}, ${w.confidence})`)}`);
|
|
11562
|
-
for (const line of w.body_preview.split("\n").slice(0, 3)) {
|
|
11563
|
-
console.log(` ${ui.dim(line)}`);
|
|
11564
|
-
}
|
|
11565
|
-
console.log(` ${ui.dim("reasons:")} ${w.reasons.join(", ")}`);
|
|
11566
|
-
if (w.affected_files && w.affected_files.length > 0) {
|
|
11567
|
-
console.log(` ${ui.dim("files:")} ${w.affected_files.slice(0, 4).join(", ")}`);
|
|
11568
|
-
}
|
|
11569
|
-
if (w.rationale) console.log(` ${ui.dim("why shown:")} ${w.rationale}`);
|
|
11570
|
-
if (w.repair_command) console.log(` ${ui.dim("repair:")} ${w.repair_command}`);
|
|
11571
|
-
}
|
|
11572
|
-
console.log();
|
|
11573
|
-
}
|
|
11574
|
-
function runCommand3(cmd, args, cwd) {
|
|
11575
|
-
return new Promise((resolve, reject) => {
|
|
11576
|
-
const proc = spawn4(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
11577
|
-
let stdout = "";
|
|
11578
|
-
let stderr = "";
|
|
11579
|
-
proc.stdout.on("data", (chunk) => {
|
|
11580
|
-
stdout += chunk.toString();
|
|
11581
|
-
});
|
|
11582
|
-
proc.stderr.on("data", (chunk) => {
|
|
11583
|
-
stderr += chunk.toString();
|
|
11584
|
-
});
|
|
11585
|
-
proc.on("error", reject);
|
|
11586
|
-
proc.on("close", (code) => {
|
|
11587
|
-
if (code === 0) resolve(stdout);
|
|
11588
|
-
else reject(new Error(stderr || `${cmd} exited with code ${code}`));
|
|
11589
|
-
});
|
|
11590
|
-
});
|
|
11591
|
-
}
|
|
11592
|
-
|
|
11593
|
-
// src/commands/welcome.ts
|
|
11594
|
-
import { existsSync as existsSync63 } from "fs";
|
|
11595
|
-
import "commander";
|
|
11596
|
-
import {
|
|
11597
|
-
findProjectRoot as findProjectRoot43,
|
|
11598
|
-
loadMemoriesFromDir as loadMemoriesFromDir34,
|
|
11599
|
-
resolveHaivePaths as resolveHaivePaths39
|
|
11600
|
-
} from "@hiveai/core";
|
|
11601
|
-
var TYPE_RANK = {
|
|
11602
|
-
decision: 0,
|
|
11603
|
-
architecture: 1,
|
|
11604
|
-
convention: 2,
|
|
11605
|
-
glossary: 3,
|
|
11606
|
-
gotcha: 4,
|
|
11607
|
-
attempt: 5
|
|
11608
|
-
};
|
|
11609
|
-
function registerWelcome(program2) {
|
|
11610
|
-
program2.command("welcome").description(
|
|
11611
|
-
"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"
|
|
11612
|
-
).option("--limit <n>", "maximum memories listed", "20").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
11613
|
-
const root = findProjectRoot43(opts.dir);
|
|
11614
|
-
const paths = resolveHaivePaths39(root);
|
|
11615
|
-
if (!existsSync63(paths.memoriesDir)) {
|
|
11616
|
-
ui.error(`No memories at ${paths.memoriesDir}. Run 'haive init' first.`);
|
|
11617
|
-
process.exitCode = 1;
|
|
11618
|
-
return;
|
|
11619
|
-
}
|
|
11620
|
-
const all = await loadMemoriesFromDir34(paths.memoriesDir);
|
|
11621
|
-
const team = all.filter(
|
|
11622
|
-
({ memory: memory2 }) => memory2.frontmatter.scope === "team" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && memory2.frontmatter.status !== "stale" && memory2.frontmatter.type !== "session_recap"
|
|
11623
|
-
);
|
|
11624
|
-
team.sort((a, b) => {
|
|
11625
|
-
const ta = TYPE_RANK[a.memory.frontmatter.type] ?? 99;
|
|
11626
|
-
const tb = TYPE_RANK[b.memory.frontmatter.type] ?? 99;
|
|
11627
|
-
if (ta !== tb) return ta - tb;
|
|
11628
|
-
const sta = a.memory.frontmatter.status === "validated" ? 0 : 1;
|
|
11629
|
-
const stb = b.memory.frontmatter.status === "validated" ? 0 : 1;
|
|
11630
|
-
if (sta !== stb) return sta - stb;
|
|
11631
|
-
return b.memory.frontmatter.created_at.localeCompare(a.memory.frontmatter.created_at);
|
|
11632
|
-
});
|
|
11633
|
-
const cap = Math.max(1, Math.min(500, Number(opts.limit) || 20));
|
|
11634
|
-
const pick = team.slice(0, cap);
|
|
11635
|
-
console.log(ui.bold(`hAIve welcome \u2014 ${pick.length} team memories (${root})`));
|
|
11636
|
-
console.log(ui.dim(`Next: invoke get_briefing with your task or run 'haive briefing --task "\u2026"'`));
|
|
11637
|
-
if (pick.length === 0) {
|
|
11638
|
-
ui.warn("No team memories yet \u2014 add some with 'haive memory add' or promote personal ones.");
|
|
11639
|
-
return;
|
|
11640
|
-
}
|
|
11641
|
-
let i = 1;
|
|
11642
|
-
for (const { memory: memory2 } of pick) {
|
|
11643
|
-
const fm = memory2.frontmatter;
|
|
11644
|
-
const head = memory2.body.match(/^#\s+(.+)/m)?.[1]?.trim();
|
|
11645
|
-
const line = head ?? fm.id;
|
|
11646
|
-
console.log(
|
|
11647
|
-
`${String(i).padStart(2, " ")} ${fm.type.padEnd(12)} ${fm.status.padEnd(10)} ${ui.dim(fm.id)}
|
|
11648
|
-
${line}`
|
|
11649
|
-
);
|
|
11650
|
-
i++;
|
|
11651
|
-
}
|
|
11652
|
-
});
|
|
11653
|
-
}
|
|
11654
|
-
|
|
11655
|
-
// src/commands/memory-lint.ts
|
|
11656
|
-
import { existsSync as existsSync64 } from "fs";
|
|
11657
|
-
import { writeFile as writeFile31 } from "fs/promises";
|
|
11658
|
-
import path43 from "path";
|
|
11659
|
-
import "commander";
|
|
11660
|
-
import {
|
|
11661
|
-
findProjectRoot as findProjectRoot44,
|
|
11662
|
-
getUsage as getUsage20,
|
|
11663
|
-
loadCodeMap as loadCodeMap6,
|
|
11664
|
-
loadMemoriesFromDir as loadMemoriesFromDir35,
|
|
11665
|
-
loadUsageIndex as loadUsageIndex26,
|
|
11666
|
-
resolveHaivePaths as resolveHaivePaths40,
|
|
11667
|
-
serializeMemory as serializeMemory26
|
|
11668
|
-
} from "@hiveai/core";
|
|
11669
|
-
async function lintMemoriesAsync(root, options = {}) {
|
|
11670
|
-
const paths = resolveHaivePaths40(root);
|
|
11671
|
-
const out = [];
|
|
11672
|
-
const fixes = [];
|
|
11673
|
-
if (!existsSync64(paths.memoriesDir)) return { findings: out, fixes };
|
|
11674
|
-
const loaded = await loadMemoriesFromDir35(paths.memoriesDir);
|
|
11675
|
-
const usage = await loadUsageIndex26(paths);
|
|
11676
|
-
const codeMap = await loadCodeMap6(paths);
|
|
11677
|
-
const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
|
|
11678
|
-
const actionableWords = /\b(always|never|prefer|use|avoid|because|instead|why|rationale|do not|must|should)\b/i;
|
|
11679
|
-
for (const { filePath, memory: memory2 } of loaded) {
|
|
11680
|
-
const fm = memory2.frontmatter;
|
|
11681
|
-
if (fm.type === "session_recap") continue;
|
|
11682
|
-
const body = memory2.body.trim();
|
|
11683
|
-
const naked = body.replace(/^#.*$/gm, "").replace(/```[\s\S]*?```/g, "").trim();
|
|
11684
|
-
if (naked.length < 40 && fm.status !== "rejected") {
|
|
11685
|
-
out.push({
|
|
11686
|
-
file: filePath,
|
|
11687
|
-
id: fm.id,
|
|
11688
|
-
severity: "warn",
|
|
11689
|
-
code: "SHORT_BODY",
|
|
11690
|
-
message: "Body looks very short (< ~40 chars of prose after headings). Prefer actionable detail."
|
|
11691
|
-
});
|
|
11692
|
-
}
|
|
11693
|
-
if (["decision", "gotcha", "convention", "architecture", "attempt"].includes(fm.type) && fm.status !== "rejected" && !actionableWords.test(naked)) {
|
|
11694
|
-
out.push({
|
|
11695
|
-
file: filePath,
|
|
11696
|
-
id: fm.id,
|
|
11697
|
-
severity: "info",
|
|
11698
|
-
code: "LOW_ACTIONABILITY",
|
|
11699
|
-
message: "Record does not contain obvious action/rationale words. Add the concrete rule, why it exists, and what to do instead."
|
|
11700
|
-
});
|
|
11701
|
-
}
|
|
11702
|
-
const suggestedAnchors = suggestAnchors(root, { filePath, memory: memory2 }, codeMap);
|
|
11703
|
-
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated") {
|
|
11704
|
-
out.push({
|
|
11705
|
-
file: filePath,
|
|
11706
|
-
id: fm.id,
|
|
11707
|
-
severity: "warn",
|
|
11708
|
-
code: "MISSING_ANCHOR",
|
|
11709
|
-
message: `${fm.type} is validated without anchor paths \u2014 add anchor.paths so haive sync can flag staleness.`,
|
|
11710
|
-
...suggestedAnchors.paths.length > 0 || suggestedAnchors.symbols.length > 0 ? { suggested_anchors: suggestedAnchors } : {}
|
|
11711
|
-
});
|
|
11712
|
-
}
|
|
11713
|
-
if (fm.status === "stale" && !fm.stale_reason) {
|
|
11714
|
-
out.push({
|
|
11715
|
-
file: filePath,
|
|
11716
|
-
id: fm.id,
|
|
11717
|
-
severity: "info",
|
|
11718
|
-
code: "STALE_NO_REASON",
|
|
11719
|
-
message: "Status is stale but stale_reason is empty \u2014 document why when possible."
|
|
11720
|
-
});
|
|
11721
|
-
}
|
|
11722
|
-
if (fm.type === "glossary" && naked.length > 6e3) {
|
|
11723
|
-
out.push({
|
|
11724
|
-
file: filePath,
|
|
11725
|
-
id: fm.id,
|
|
11726
|
-
severity: "info",
|
|
11727
|
-
code: "LONG_GLOSSARY",
|
|
11728
|
-
message: "Very long glossary \u2014 consider splitting concepts for tighter briefings."
|
|
11729
|
-
});
|
|
11730
|
-
}
|
|
11731
|
-
const hasMarkdownHeading = /^#{1,3}\s+\S/m.test(memory2.body.trim());
|
|
11732
|
-
if (!hasMarkdownHeading) {
|
|
11733
|
-
out.push({
|
|
11734
|
-
file: filePath,
|
|
11735
|
-
id: fm.id,
|
|
11736
|
-
severity: "warn",
|
|
11737
|
-
code: "NO_MD_HEADING",
|
|
11738
|
-
message: "No Markdown heading (#/##/###) \u2014 add one so humans and auditors can skim the memo quickly."
|
|
11739
|
-
});
|
|
11740
|
-
}
|
|
11741
|
-
const u = getUsage20(usage, fm.id);
|
|
11742
|
-
if (fm.status === "validated" && u.read_count === 0) {
|
|
11743
|
-
out.push({
|
|
11744
|
-
file: filePath,
|
|
11745
|
-
id: fm.id,
|
|
11746
|
-
severity: "info",
|
|
11747
|
-
code: "NEVER_READ",
|
|
11748
|
-
message: "Validated record has never been surfaced/read. Consider improving tags/anchors or archiving it if it is not useful."
|
|
11749
|
-
});
|
|
11750
|
-
}
|
|
11751
|
-
if (options.fix) {
|
|
11752
|
-
const actions = [];
|
|
11753
|
-
let nextBody = memory2.body;
|
|
11754
|
-
let nextFrontmatter = memory2.frontmatter;
|
|
11755
|
-
if (!hasMarkdownHeading) {
|
|
11756
|
-
nextBody = `# ${titleFromId(fm.id)}
|
|
11757
|
-
|
|
11758
|
-
${nextBody.trim()}`;
|
|
11759
|
-
actions.push("add missing Markdown heading");
|
|
11760
|
-
}
|
|
11761
|
-
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.anchor.symbols.length === 0 && fm.status === "validated" && !fm.tags.includes("needs_anchor")) {
|
|
11762
|
-
nextFrontmatter = {
|
|
11763
|
-
...nextFrontmatter,
|
|
11764
|
-
tags: [...nextFrontmatter.tags, "needs_anchor"]
|
|
11765
|
-
};
|
|
11766
|
-
actions.push("tag validated anchorless record with needs_anchor");
|
|
11767
|
-
}
|
|
11768
|
-
if (actions.length > 0) {
|
|
11769
|
-
fixes.push({ file: filePath, id: fm.id, actions, applied: Boolean(options.apply) });
|
|
11770
|
-
if (options.apply) {
|
|
11771
|
-
await writeFile31(
|
|
11772
|
-
filePath,
|
|
11773
|
-
serializeMemory26({ frontmatter: nextFrontmatter, body: nextBody }),
|
|
11774
|
-
"utf8"
|
|
11775
|
-
);
|
|
11776
|
-
}
|
|
12144
|
+
console.log();
|
|
12145
|
+
const blocking = result.warnings.filter((w) => w.level === "blocking");
|
|
12146
|
+
const review = result.warnings.filter((w) => w.level === "review");
|
|
12147
|
+
const info = result.warnings.filter((w) => w.level === "info");
|
|
12148
|
+
printWarnings("Blocking anti-patterns", blocking, "error");
|
|
12149
|
+
printWarnings("Review anti-patterns", review.slice(0, 8), "warn");
|
|
12150
|
+
if (info.length > 0) {
|
|
12151
|
+
console.log(
|
|
12152
|
+
ui.dim(
|
|
12153
|
+
`${info.length} weak anti-pattern signal${info.length === 1 ? "" : "s"} hidden. Use --json to inspect FYI matches.`
|
|
12154
|
+
)
|
|
12155
|
+
);
|
|
12156
|
+
console.log();
|
|
12157
|
+
}
|
|
12158
|
+
if (result.relevant_memories.length > 0) {
|
|
12159
|
+
console.log(ui.bold("\u{1F4CC} Relevant conventions/decisions:"));
|
|
12160
|
+
for (const m of result.relevant_memories) {
|
|
12161
|
+
console.log(` \u2022 ${m.id} ${ui.dim(`(${m.type}, ${m.confidence})`)}`);
|
|
11777
12162
|
}
|
|
12163
|
+
console.log();
|
|
11778
12164
|
}
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
11782
|
-
|
|
11783
|
-
|
|
11784
|
-
severity: "warn",
|
|
11785
|
-
code: "NEAR_DUPLICATE",
|
|
11786
|
-
message: `Body overlaps ~${Math.round(dup.score * 100)}% with ${dup.otherId}. Merge or deprecate one record to reduce briefing noise.`
|
|
11787
|
-
});
|
|
11788
|
-
}
|
|
11789
|
-
return { findings: out, fixes };
|
|
11790
|
-
}
|
|
11791
|
-
function titleFromId(id) {
|
|
11792
|
-
const withoutDate = id.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
11793
|
-
return withoutDate.split("-").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
11794
|
-
}
|
|
11795
|
-
function suggestAnchors(root, loaded, codeMap) {
|
|
11796
|
-
const body = loaded.memory.body;
|
|
11797
|
-
const paths = /* @__PURE__ */ new Set();
|
|
11798
|
-
const symbols = /* @__PURE__ */ new Set();
|
|
11799
|
-
for (const match of body.matchAll(/`([^`\n]+\.[A-Za-z0-9]+)`|(?:^|\s)([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)/gm)) {
|
|
11800
|
-
const candidate = (match[1] ?? match[2] ?? "").replace(/^\.?\//, "");
|
|
11801
|
-
if (!candidate || candidate.startsWith("http")) continue;
|
|
11802
|
-
if (existsSync64(path43.join(root, candidate))) paths.add(candidate);
|
|
11803
|
-
}
|
|
11804
|
-
if (codeMap) {
|
|
11805
|
-
const lowered = body.toLowerCase();
|
|
11806
|
-
for (const [file, entry] of Object.entries(codeMap.files)) {
|
|
11807
|
-
for (const exp of entry.exports) {
|
|
11808
|
-
if (!exp.name || exp.name.length < 4) continue;
|
|
11809
|
-
if (lowered.includes(exp.name.toLowerCase())) {
|
|
11810
|
-
paths.add(file);
|
|
11811
|
-
symbols.add(exp.name);
|
|
11812
|
-
}
|
|
11813
|
-
if (paths.size >= 5 && symbols.size >= 5) break;
|
|
12165
|
+
if (result.stale_anchors.length > 0) {
|
|
12166
|
+
console.log(ui.bold("\u{1F552} Stale anchored memories:"));
|
|
12167
|
+
for (const s of result.stale_anchors) {
|
|
12168
|
+
console.log(` \u2022 ${s.id}`);
|
|
12169
|
+
if (s.body_preview) console.log(` ${ui.dim(s.body_preview)}`);
|
|
11814
12170
|
}
|
|
11815
|
-
|
|
12171
|
+
console.log();
|
|
12172
|
+
}
|
|
12173
|
+
if (result.should_block) {
|
|
12174
|
+
ui.error(`Blocking commit (block_on=${opts.blockOn ?? "high-confidence"}). Address the warnings above or pass --block-on never to bypass.`);
|
|
12175
|
+
process.exit(1);
|
|
12176
|
+
}
|
|
12177
|
+
if (result.warnings.length === 0 && result.stale_anchors.length === 0) {
|
|
12178
|
+
ui.success("No anti-patterns or stale anchors found.");
|
|
12179
|
+
} else {
|
|
12180
|
+
ui.success("Check passed (block_on threshold not met).");
|
|
11816
12181
|
}
|
|
11817
|
-
}
|
|
11818
|
-
return {
|
|
11819
|
-
paths: [...paths].slice(0, 5),
|
|
11820
|
-
symbols: [...symbols].slice(0, 5)
|
|
11821
|
-
};
|
|
11822
|
-
}
|
|
11823
|
-
function nearDuplicatePairs(loaded) {
|
|
11824
|
-
const out = [];
|
|
11825
|
-
const candidates = loaded.filter(({ memory: memory2 }) => {
|
|
11826
|
-
const fm = memory2.frontmatter;
|
|
11827
|
-
return fm.type !== "session_recap" && fm.status !== "rejected" && fm.status !== "deprecated";
|
|
11828
12182
|
});
|
|
11829
|
-
|
|
11830
|
-
|
|
11831
|
-
|
|
11832
|
-
|
|
11833
|
-
|
|
11834
|
-
|
|
11835
|
-
|
|
11836
|
-
|
|
11837
|
-
|
|
11838
|
-
|
|
11839
|
-
|
|
11840
|
-
|
|
11841
|
-
|
|
11842
|
-
});
|
|
11843
|
-
}
|
|
12183
|
+
}
|
|
12184
|
+
function printWarnings(title, warnings, tone) {
|
|
12185
|
+
if (warnings.length === 0) return;
|
|
12186
|
+
console.log(ui.bold(tone === "error" ? `\u2717 ${title}:` : `\u26A0 ${title}:`));
|
|
12187
|
+
for (const w of warnings) {
|
|
12188
|
+
const marker = tone === "error" ? ui.red("\u2717") : ui.yellow("\u26A0");
|
|
12189
|
+
console.log(` ${marker} ${w.id} ${ui.dim(`(${w.type}, ${w.confidence})`)}`);
|
|
12190
|
+
for (const line of w.body_preview.split("\n").slice(0, 3)) {
|
|
12191
|
+
console.log(` ${ui.dim(line)}`);
|
|
12192
|
+
}
|
|
12193
|
+
console.log(` ${ui.dim("reasons:")} ${w.reasons.join(", ")}`);
|
|
12194
|
+
if (w.affected_files && w.affected_files.length > 0) {
|
|
12195
|
+
console.log(` ${ui.dim("files:")} ${w.affected_files.slice(0, 4).join(", ")}`);
|
|
11844
12196
|
}
|
|
12197
|
+
if (w.rationale) console.log(` ${ui.dim("why shown:")} ${w.rationale}`);
|
|
12198
|
+
if (w.repair_command) console.log(` ${ui.dim("repair:")} ${w.repair_command}`);
|
|
11845
12199
|
}
|
|
11846
|
-
|
|
11847
|
-
}
|
|
11848
|
-
function tokenSet(body) {
|
|
11849
|
-
return new Set(
|
|
11850
|
-
(body.toLowerCase().match(/\b[a-z0-9]{4,}\b/g) ?? []).filter((word) => !["this", "that", "with", "from", "have"].includes(word))
|
|
11851
|
-
);
|
|
12200
|
+
console.log();
|
|
11852
12201
|
}
|
|
11853
|
-
function
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
|
|
11857
|
-
|
|
12202
|
+
function runCommand3(cmd, args, cwd) {
|
|
12203
|
+
return new Promise((resolve, reject) => {
|
|
12204
|
+
const proc = spawn4(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
12205
|
+
let stdout = "";
|
|
12206
|
+
let stderr = "";
|
|
12207
|
+
proc.stdout.on("data", (chunk) => {
|
|
12208
|
+
stdout += chunk.toString();
|
|
12209
|
+
});
|
|
12210
|
+
proc.stderr.on("data", (chunk) => {
|
|
12211
|
+
stderr += chunk.toString();
|
|
12212
|
+
});
|
|
12213
|
+
proc.on("error", reject);
|
|
12214
|
+
proc.on("close", (code) => {
|
|
12215
|
+
if (code === 0) resolve(stdout);
|
|
12216
|
+
else reject(new Error(stderr || `${cmd} exited with code ${code}`));
|
|
12217
|
+
});
|
|
12218
|
+
});
|
|
11858
12219
|
}
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
|
|
12220
|
+
|
|
12221
|
+
// src/commands/welcome.ts
|
|
12222
|
+
import { existsSync as existsSync65 } from "fs";
|
|
12223
|
+
import "commander";
|
|
12224
|
+
import {
|
|
12225
|
+
findProjectRoot as findProjectRoot44,
|
|
12226
|
+
loadMemoriesFromDir as loadMemoriesFromDir35,
|
|
12227
|
+
resolveHaivePaths as resolveHaivePaths40
|
|
12228
|
+
} from "@hiveai/core";
|
|
12229
|
+
var TYPE_RANK = {
|
|
12230
|
+
decision: 0,
|
|
12231
|
+
architecture: 1,
|
|
12232
|
+
convention: 2,
|
|
12233
|
+
glossary: 3,
|
|
12234
|
+
gotcha: 4,
|
|
12235
|
+
attempt: 5
|
|
12236
|
+
};
|
|
12237
|
+
function registerWelcome(program2) {
|
|
12238
|
+
program2.command("welcome").description(
|
|
12239
|
+
"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"
|
|
12240
|
+
).option("--limit <n>", "maximum memories listed", "20").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
11863
12241
|
const root = findProjectRoot44(opts.dir);
|
|
11864
|
-
const
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
if (opts.json) {
|
|
11869
|
-
console.log(JSON.stringify({
|
|
11870
|
-
findings_count: findings.length,
|
|
11871
|
-
findings,
|
|
11872
|
-
fixes_count: report.fixes.length,
|
|
11873
|
-
fixes: report.fixes,
|
|
11874
|
-
fix_mode: opts.fix ? apply ? "apply" : "dry-run" : "off"
|
|
11875
|
-
}, null, 2));
|
|
11876
|
-
process.exitCode = findings.some((f) => f.severity === "error") ? 1 : 0;
|
|
12242
|
+
const paths = resolveHaivePaths40(root);
|
|
12243
|
+
if (!existsSync65(paths.memoriesDir)) {
|
|
12244
|
+
ui.error(`No memories at ${paths.memoriesDir}. Run 'haive init' first.`);
|
|
12245
|
+
process.exitCode = 1;
|
|
11877
12246
|
return;
|
|
11878
12247
|
}
|
|
11879
|
-
|
|
11880
|
-
|
|
12248
|
+
const all = await loadMemoriesFromDir35(paths.memoriesDir);
|
|
12249
|
+
const team = all.filter(
|
|
12250
|
+
({ memory: memory2 }) => memory2.frontmatter.scope === "team" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && memory2.frontmatter.status !== "stale" && memory2.frontmatter.type !== "session_recap"
|
|
12251
|
+
);
|
|
12252
|
+
team.sort((a, b) => {
|
|
12253
|
+
const ta = TYPE_RANK[a.memory.frontmatter.type] ?? 99;
|
|
12254
|
+
const tb = TYPE_RANK[b.memory.frontmatter.type] ?? 99;
|
|
12255
|
+
if (ta !== tb) return ta - tb;
|
|
12256
|
+
const sta = a.memory.frontmatter.status === "validated" ? 0 : 1;
|
|
12257
|
+
const stb = b.memory.frontmatter.status === "validated" ? 0 : 1;
|
|
12258
|
+
if (sta !== stb) return sta - stb;
|
|
12259
|
+
return b.memory.frontmatter.created_at.localeCompare(a.memory.frontmatter.created_at);
|
|
12260
|
+
});
|
|
12261
|
+
const cap = Math.max(1, Math.min(500, Number(opts.limit) || 20));
|
|
12262
|
+
const pick = team.slice(0, cap);
|
|
12263
|
+
console.log(ui.bold(`hAIve welcome \u2014 ${pick.length} team memories (${root})`));
|
|
12264
|
+
console.log(ui.dim(`Next: invoke get_briefing with your task or run 'haive briefing --task "\u2026"'`));
|
|
12265
|
+
if (pick.length === 0) {
|
|
12266
|
+
ui.warn("No team memories yet \u2014 add some with 'haive memory add' or promote personal ones.");
|
|
11881
12267
|
return;
|
|
11882
12268
|
}
|
|
11883
|
-
|
|
11884
|
-
|
|
11885
|
-
|
|
11886
|
-
const
|
|
11887
|
-
const
|
|
11888
|
-
console.log(ui.bold(`fix ${mode}: ${report.fixes.length} file${report.fixes.length === 1 ? "" : "s"} ${verb}`));
|
|
11889
|
-
for (const fix of report.fixes) {
|
|
11890
|
-
console.log(` ${ui.dim(fix.id)} ${fix.actions.join("; ")}`);
|
|
11891
|
-
console.log(ui.dim(` \u2192 ${fix.file}`));
|
|
11892
|
-
}
|
|
11893
|
-
console.log();
|
|
11894
|
-
}
|
|
11895
|
-
const order = { error: 0, warn: 1, info: 2 };
|
|
11896
|
-
findings.sort((a, b) => order[a.severity] - order[b.severity] || a.id.localeCompare(b.id));
|
|
11897
|
-
for (const f of findings) {
|
|
11898
|
-
const color = f.severity === "error" ? ui.red : f.severity === "warn" ? ui.yellow : ui.dim;
|
|
12269
|
+
let i = 1;
|
|
12270
|
+
for (const { memory: memory2 } of pick) {
|
|
12271
|
+
const fm = memory2.frontmatter;
|
|
12272
|
+
const head = memory2.body.match(/^#\s+(.+)/m)?.[1]?.trim();
|
|
12273
|
+
const line = head ?? fm.id;
|
|
11899
12274
|
console.log(
|
|
11900
|
-
`${
|
|
12275
|
+
`${String(i).padStart(2, " ")} ${fm.type.padEnd(12)} ${fm.status.padEnd(10)} ${ui.dim(fm.id)}
|
|
12276
|
+
${line}`
|
|
11901
12277
|
);
|
|
11902
|
-
|
|
11903
|
-
if (f.suggested_anchors) {
|
|
11904
|
-
const pathHints = f.suggested_anchors.paths.length > 0 ? `paths: ${f.suggested_anchors.paths.join(", ")}` : "";
|
|
11905
|
-
const symbolHints = f.suggested_anchors.symbols.length > 0 ? `symbols: ${f.suggested_anchors.symbols.join(", ")}` : "";
|
|
11906
|
-
console.log(ui.dim(` suggested anchors: ${[pathHints, symbolHints].filter(Boolean).join(" \xB7 ")}`));
|
|
11907
|
-
}
|
|
11908
|
-
console.log(ui.dim(` \u2192 ${f.file}`));
|
|
12278
|
+
i++;
|
|
11909
12279
|
}
|
|
11910
|
-
process.exitCode = findings.some((x) => x.severity === "error") ? 1 : 0;
|
|
11911
12280
|
});
|
|
11912
12281
|
}
|
|
11913
12282
|
|
|
@@ -11930,21 +12299,21 @@ function registerMemorySuggestTopic(memory2) {
|
|
|
11930
12299
|
}
|
|
11931
12300
|
|
|
11932
12301
|
// src/commands/resolve-project.ts
|
|
11933
|
-
import
|
|
12302
|
+
import path45 from "path";
|
|
11934
12303
|
import "commander";
|
|
11935
12304
|
import { resolveProjectInfo as resolveProjectInfo2 } from "@hiveai/core";
|
|
11936
12305
|
function registerResolveProject(program2) {
|
|
11937
12306
|
program2.command("resolve-project").description(
|
|
11938
12307
|
"Print JSON for hAIve project root resolution (HAIVE_PROJECT_ROOT, markers, .ai layout)."
|
|
11939
12308
|
).option("-d, --dir <dir>", "working directory", process.cwd()).action((opts) => {
|
|
11940
|
-
const info = resolveProjectInfo2({ cwd:
|
|
12309
|
+
const info = resolveProjectInfo2({ cwd: path45.resolve(opts.dir) });
|
|
11941
12310
|
console.log(JSON.stringify({ ok: true, info }, null, 2));
|
|
11942
12311
|
});
|
|
11943
12312
|
}
|
|
11944
12313
|
|
|
11945
12314
|
// src/commands/runtime-journal.ts
|
|
11946
|
-
import { existsSync as
|
|
11947
|
-
import
|
|
12315
|
+
import { existsSync as existsSync66 } from "fs";
|
|
12316
|
+
import path46 from "path";
|
|
11948
12317
|
import "commander";
|
|
11949
12318
|
import {
|
|
11950
12319
|
appendRuntimeJournalEntry as appendRuntimeJournalEntry3,
|
|
@@ -11958,18 +12327,18 @@ function registerRuntime(program2) {
|
|
|
11958
12327
|
);
|
|
11959
12328
|
const journal = runtime.command("journal").description("Append or read the machine-local session journal (NDJSON)");
|
|
11960
12329
|
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) => {
|
|
11961
|
-
const root =
|
|
12330
|
+
const root = path46.resolve(opts.dir ?? process.cwd());
|
|
11962
12331
|
const paths = resolveHaivePaths41(findProjectRoot45(root));
|
|
11963
12332
|
const raw = opts.kind ?? "note";
|
|
11964
12333
|
const kind = ["note", "session_end", "mcp"].includes(raw) ? raw : "note";
|
|
11965
12334
|
await appendRuntimeJournalEntry3(paths, { kind, message });
|
|
11966
|
-
ui.success(`Appended to ${
|
|
12335
|
+
ui.success(`Appended to ${path46.relative(root, paths.runtimeDir)}/session-journal.ndjson`);
|
|
11967
12336
|
});
|
|
11968
12337
|
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) => {
|
|
11969
|
-
const root =
|
|
12338
|
+
const root = path46.resolve(opts.dir ?? process.cwd());
|
|
11970
12339
|
const paths = resolveHaivePaths41(findProjectRoot45(root));
|
|
11971
12340
|
const limit = Math.min(500, Math.max(1, parseInt(opts.limit, 10) || 30));
|
|
11972
|
-
if (!
|
|
12341
|
+
if (!existsSync66(paths.haiveDir)) {
|
|
11973
12342
|
ui.error("No .ai/ \u2014 run `haive init` first.");
|
|
11974
12343
|
process.exitCode = 1;
|
|
11975
12344
|
return;
|
|
@@ -11984,8 +12353,8 @@ function registerRuntime(program2) {
|
|
|
11984
12353
|
}
|
|
11985
12354
|
|
|
11986
12355
|
// src/commands/memory-timeline.ts
|
|
11987
|
-
import { existsSync as
|
|
11988
|
-
import
|
|
12356
|
+
import { existsSync as existsSync67 } from "fs";
|
|
12357
|
+
import path47 from "path";
|
|
11989
12358
|
import "commander";
|
|
11990
12359
|
import {
|
|
11991
12360
|
collectTimelineEntries as collectTimelineEntries2,
|
|
@@ -12001,15 +12370,15 @@ function registerMemoryTimeline(memory2) {
|
|
|
12001
12370
|
process.exitCode = 1;
|
|
12002
12371
|
return;
|
|
12003
12372
|
}
|
|
12004
|
-
const root =
|
|
12373
|
+
const root = path47.resolve(opts.dir ?? process.cwd());
|
|
12005
12374
|
const paths = resolveHaivePaths42(findProjectRoot46(root));
|
|
12006
|
-
if (!
|
|
12375
|
+
if (!existsSync67(paths.memoriesDir)) {
|
|
12007
12376
|
ui.error("No memories \u2014 run `haive init`.");
|
|
12008
12377
|
process.exitCode = 1;
|
|
12009
12378
|
return;
|
|
12010
12379
|
}
|
|
12011
12380
|
const limit = Math.min(100, Math.max(1, parseInt(opts.limit, 10) || 30));
|
|
12012
|
-
const all = await
|
|
12381
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
12013
12382
|
const { entries, notice } = collectTimelineEntries2(all, {
|
|
12014
12383
|
memoryId: opts.id,
|
|
12015
12384
|
topic: opts.topic,
|
|
@@ -12021,8 +12390,8 @@ function registerMemoryTimeline(memory2) {
|
|
|
12021
12390
|
}
|
|
12022
12391
|
|
|
12023
12392
|
// src/commands/memory-conflict-candidates.ts
|
|
12024
|
-
import { existsSync as
|
|
12025
|
-
import
|
|
12393
|
+
import { existsSync as existsSync68 } from "fs";
|
|
12394
|
+
import path48 from "path";
|
|
12026
12395
|
import "commander";
|
|
12027
12396
|
import {
|
|
12028
12397
|
findLexicalConflictPairs as findLexicalConflictPairs2,
|
|
@@ -12044,9 +12413,9 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
12044
12413
|
"decision,architecture,convention,gotcha (lexical scan)",
|
|
12045
12414
|
"decision,architecture"
|
|
12046
12415
|
).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) => {
|
|
12047
|
-
const root =
|
|
12416
|
+
const root = path48.resolve(opts.dir ?? process.cwd());
|
|
12048
12417
|
const paths = resolveHaivePaths43(findProjectRoot47(root));
|
|
12049
|
-
if (!
|
|
12418
|
+
if (!existsSync68(paths.memoriesDir)) {
|
|
12050
12419
|
ui.error("No memories \u2014 run `haive init`.");
|
|
12051
12420
|
process.exitCode = 1;
|
|
12052
12421
|
return;
|
|
@@ -12056,7 +12425,7 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
12056
12425
|
const maxPairs = Math.min(100, Math.max(1, parseInt(opts.maxPairs, 10) || 20));
|
|
12057
12426
|
const maxScan = Math.min(2e3, Math.max(1, parseInt(opts.maxScan, 10) || 500));
|
|
12058
12427
|
const maxTopicPairs = Math.min(100, Math.max(1, parseInt(opts.maxTopicPairs, 10) || 20));
|
|
12059
|
-
const all = await
|
|
12428
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
12060
12429
|
const lexical = findLexicalConflictPairs2(all, {
|
|
12061
12430
|
sinceDays,
|
|
12062
12431
|
types: parseTypes(opts.types),
|
|
@@ -12082,21 +12451,21 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
12082
12451
|
|
|
12083
12452
|
// src/commands/enforce.ts
|
|
12084
12453
|
import { execFileSync as execFileSync2, spawn as spawn5 } from "child_process";
|
|
12085
|
-
import { existsSync as
|
|
12086
|
-
import { chmod as chmod2, mkdir as mkdir19, readFile as
|
|
12087
|
-
import
|
|
12454
|
+
import { existsSync as existsSync69 } from "fs";
|
|
12455
|
+
import { chmod as chmod2, mkdir as mkdir19, readFile as readFile19, rm as rm3, writeFile as writeFile34 } from "fs/promises";
|
|
12456
|
+
import path49 from "path";
|
|
12088
12457
|
import "commander";
|
|
12089
12458
|
import {
|
|
12090
12459
|
findProjectRoot as findProjectRoot48,
|
|
12091
12460
|
hasRecentBriefingMarker,
|
|
12092
12461
|
isFreshIsoDate,
|
|
12093
|
-
loadConfig as
|
|
12462
|
+
loadConfig as loadConfig11,
|
|
12094
12463
|
loadMemoriesFromDir as loadMemoriesFromDir36,
|
|
12095
12464
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths6,
|
|
12096
12465
|
readRecentBriefingMarker,
|
|
12097
12466
|
resolveBriefingBudget as resolveBriefingBudget3,
|
|
12098
12467
|
resolveHaivePaths as resolveHaivePaths44,
|
|
12099
|
-
saveConfig as
|
|
12468
|
+
saveConfig as saveConfig4,
|
|
12100
12469
|
SESSION_RECAP_TTL_MS,
|
|
12101
12470
|
verifyAnchor as verifyAnchor4,
|
|
12102
12471
|
writeBriefingMarker as writeBriefingMarker2
|
|
@@ -12111,8 +12480,8 @@ function registerEnforce(program2) {
|
|
|
12111
12480
|
const root = findProjectRoot48(opts.dir);
|
|
12112
12481
|
const paths = resolveHaivePaths44(root);
|
|
12113
12482
|
await mkdir19(paths.haiveDir, { recursive: true });
|
|
12114
|
-
const current = await
|
|
12115
|
-
await
|
|
12483
|
+
const current = await loadConfig11(paths);
|
|
12484
|
+
await saveConfig4(paths, {
|
|
12116
12485
|
...current,
|
|
12117
12486
|
enforcement: {
|
|
12118
12487
|
...current.enforcement,
|
|
@@ -12134,7 +12503,7 @@ function registerEnforce(program2) {
|
|
|
12134
12503
|
if (opts.claude !== false) {
|
|
12135
12504
|
try {
|
|
12136
12505
|
const result = await installClaudeHooksAtPath(defaultClaudeSettingsPath("project", root));
|
|
12137
|
-
ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${
|
|
12506
|
+
ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${path49.relative(root, result.settingsPath)})`);
|
|
12138
12507
|
} catch (err) {
|
|
12139
12508
|
ui.warn(`Claude Code hooks not installed: ${err instanceof Error ? err.message : String(err)}`);
|
|
12140
12509
|
}
|
|
@@ -12156,12 +12525,12 @@ function registerEnforce(program2) {
|
|
|
12156
12525
|
const root = findProjectRoot48(opts.dir);
|
|
12157
12526
|
const paths = resolveHaivePaths44(root);
|
|
12158
12527
|
const targets = [
|
|
12159
|
-
|
|
12160
|
-
|
|
12528
|
+
path49.join(paths.haiveDir, ".cache"),
|
|
12529
|
+
path49.join(paths.haiveDir, ".runtime")
|
|
12161
12530
|
];
|
|
12162
12531
|
for (const target of targets) {
|
|
12163
|
-
if (!
|
|
12164
|
-
const rel =
|
|
12532
|
+
if (!existsSync69(target)) continue;
|
|
12533
|
+
const rel = path49.relative(root, target);
|
|
12165
12534
|
if (opts.dryRun) ui.info(`would remove ${rel}`);
|
|
12166
12535
|
else {
|
|
12167
12536
|
await rm3(target, { recursive: true, force: true });
|
|
@@ -12179,7 +12548,7 @@ function registerEnforce(program2) {
|
|
|
12179
12548
|
const root = resolveRoot(opts.dir, payload);
|
|
12180
12549
|
if (!root) return;
|
|
12181
12550
|
const paths = resolveHaivePaths44(root);
|
|
12182
|
-
if (!
|
|
12551
|
+
if (!existsSync69(paths.haiveDir)) return;
|
|
12183
12552
|
await mkdir19(paths.runtimeDir, { recursive: true });
|
|
12184
12553
|
const sessionId = opts.sessionId ?? payload.session_id;
|
|
12185
12554
|
const task = opts.task ?? payload.prompt ?? "Start an AI coding session in this hAIve-initialized project.";
|
|
@@ -12241,7 +12610,7 @@ ${briefing.project_context.content.slice(0, 1800)}`);
|
|
|
12241
12610
|
const root = resolveRoot(opts.dir, payload);
|
|
12242
12611
|
if (!root) return;
|
|
12243
12612
|
const paths = resolveHaivePaths44(root);
|
|
12244
|
-
if (!
|
|
12613
|
+
if (!existsSync69(paths.haiveDir)) return;
|
|
12245
12614
|
if (!isWriteLikeTool(payload)) return;
|
|
12246
12615
|
const ok = await hasRecentBriefingMarker(paths, payload.session_id);
|
|
12247
12616
|
if (ok) return;
|
|
@@ -12265,7 +12634,7 @@ ${briefing.project_context.content.slice(0, 1800)}`);
|
|
|
12265
12634
|
async function runWithEnforcement(command, args, opts) {
|
|
12266
12635
|
const root = findProjectRoot48(opts.dir);
|
|
12267
12636
|
const paths = resolveHaivePaths44(root);
|
|
12268
|
-
if (!
|
|
12637
|
+
if (!existsSync69(paths.haiveDir)) {
|
|
12269
12638
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
12270
12639
|
process.exit(1);
|
|
12271
12640
|
}
|
|
@@ -12284,7 +12653,7 @@ async function runWithEnforcement(command, args, opts) {
|
|
|
12284
12653
|
process.exit(2);
|
|
12285
12654
|
}
|
|
12286
12655
|
ui.info(`hAIve briefing marker created for wrapped agent session: ${sessionId}`);
|
|
12287
|
-
ui.info(`Briefing written to ${
|
|
12656
|
+
ui.info(`Briefing written to ${path49.relative(root, briefingFile)} and exported as HAIVE_BRIEFING_FILE`);
|
|
12288
12657
|
const child = spawn5(command, args, {
|
|
12289
12658
|
cwd: root,
|
|
12290
12659
|
stdio: "inherit",
|
|
@@ -12333,9 +12702,9 @@ async function writeWrapperBriefing(paths, sessionId, task) {
|
|
|
12333
12702
|
source: "haive-run",
|
|
12334
12703
|
memoryIds: briefing.memories.map((m) => m.id)
|
|
12335
12704
|
});
|
|
12336
|
-
const dir =
|
|
12705
|
+
const dir = path49.join(paths.runtimeDir, "enforcement", "briefings");
|
|
12337
12706
|
await mkdir19(dir, { recursive: true });
|
|
12338
|
-
const file =
|
|
12707
|
+
const file = path49.join(dir, `${sessionId}.md`);
|
|
12339
12708
|
const parts = [
|
|
12340
12709
|
"# hAIve Briefing",
|
|
12341
12710
|
"",
|
|
@@ -12353,14 +12722,14 @@ async function writeWrapperBriefing(paths, sessionId, task) {
|
|
|
12353
12722
|
if (briefing.setup_warnings.length > 0) {
|
|
12354
12723
|
parts.push("", "## Setup Warnings", ...briefing.setup_warnings.map((w) => `- ${w}`));
|
|
12355
12724
|
}
|
|
12356
|
-
await
|
|
12725
|
+
await writeFile34(file, parts.join("\n") + "\n", "utf8");
|
|
12357
12726
|
return file;
|
|
12358
12727
|
}
|
|
12359
12728
|
async function buildEnforcementReport(dir, stage, sessionId) {
|
|
12360
12729
|
const root = findProjectRoot48(dir);
|
|
12361
12730
|
const paths = resolveHaivePaths44(root);
|
|
12362
|
-
const initialized =
|
|
12363
|
-
const config = initialized ? await
|
|
12731
|
+
const initialized = existsSync69(paths.haiveDir);
|
|
12732
|
+
const config = initialized ? await loadConfig11(paths) : {};
|
|
12364
12733
|
const mode = config.enforcement?.mode ?? "strict";
|
|
12365
12734
|
const findings = [];
|
|
12366
12735
|
if (!initialized) {
|
|
@@ -12389,7 +12758,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
12389
12758
|
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
12390
12759
|
});
|
|
12391
12760
|
}
|
|
12392
|
-
findings.push(...await inspectIntegrationVersions(root, "0.9.
|
|
12761
|
+
findings.push(...await inspectIntegrationVersions(root, "0.9.19"));
|
|
12393
12762
|
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
12394
12763
|
const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
|
|
12395
12764
|
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
|
|
@@ -12459,7 +12828,7 @@ function withCategories(report) {
|
|
|
12459
12828
|
};
|
|
12460
12829
|
}
|
|
12461
12830
|
async function hasRecentSessionRecap(paths) {
|
|
12462
|
-
if (!
|
|
12831
|
+
if (!existsSync69(paths.memoriesDir)) return false;
|
|
12463
12832
|
const all = await loadMemoriesFromDir36(paths.memoriesDir);
|
|
12464
12833
|
return all.some(({ memory: memory2 }) => {
|
|
12465
12834
|
const fm = memory2.frontmatter;
|
|
@@ -12468,7 +12837,7 @@ async function hasRecentSessionRecap(paths) {
|
|
|
12468
12837
|
});
|
|
12469
12838
|
}
|
|
12470
12839
|
async function verifyMemoryPolicy(paths, config) {
|
|
12471
|
-
if (!
|
|
12840
|
+
if (!existsSync69(paths.memoriesDir)) return [];
|
|
12472
12841
|
const all = await loadMemoriesFromDir36(paths.memoriesDir);
|
|
12473
12842
|
const findings = [];
|
|
12474
12843
|
const staleImportant = [];
|
|
@@ -12506,7 +12875,7 @@ async function verifyMemoryPolicy(paths, config) {
|
|
|
12506
12875
|
return findings;
|
|
12507
12876
|
}
|
|
12508
12877
|
async function verifyDecisionCoverage(paths, stage, sessionId) {
|
|
12509
|
-
if (!
|
|
12878
|
+
if (!existsSync69(paths.memoriesDir)) return [];
|
|
12510
12879
|
const changedFiles = await getChangedFiles(paths.root, stage);
|
|
12511
12880
|
if (changedFiles.length === 0) {
|
|
12512
12881
|
return [{ severity: "info", code: "decision-coverage-no-changes", message: "No changed files to match against policy memories." }];
|
|
@@ -12599,9 +12968,9 @@ async function inspectIntegrationVersions(root, expectedVersion) {
|
|
|
12599
12968
|
];
|
|
12600
12969
|
const findings = [];
|
|
12601
12970
|
for (const rel of files) {
|
|
12602
|
-
const file =
|
|
12603
|
-
if (!
|
|
12604
|
-
const text = await
|
|
12971
|
+
const file = path49.join(root, rel);
|
|
12972
|
+
if (!existsSync69(file)) continue;
|
|
12973
|
+
const text = await readFile19(file, "utf8").catch(() => "");
|
|
12605
12974
|
for (const bin of extractAbsoluteHaiveBins2(text)) {
|
|
12606
12975
|
const version = versionForBinary2(bin);
|
|
12607
12976
|
if (!version) {
|
|
@@ -12689,8 +13058,8 @@ function buildScore(findings, threshold = 80) {
|
|
|
12689
13058
|
};
|
|
12690
13059
|
}
|
|
12691
13060
|
async function installGitEnforcement(root) {
|
|
12692
|
-
const hooksDir =
|
|
12693
|
-
if (!
|
|
13061
|
+
const hooksDir = path49.join(root, ".git", "hooks");
|
|
13062
|
+
if (!existsSync69(path49.join(root, ".git"))) {
|
|
12694
13063
|
ui.warn("No .git directory found; git enforcement hooks skipped.");
|
|
12695
13064
|
return;
|
|
12696
13065
|
}
|
|
@@ -12712,31 +13081,31 @@ haive enforce check --stage pre-push --dir . || exit $?
|
|
|
12712
13081
|
}
|
|
12713
13082
|
];
|
|
12714
13083
|
for (const hook of hooks) {
|
|
12715
|
-
const file =
|
|
12716
|
-
if (
|
|
12717
|
-
const current = await
|
|
13084
|
+
const file = path49.join(hooksDir, hook.name);
|
|
13085
|
+
if (existsSync69(file)) {
|
|
13086
|
+
const current = await readFile19(file, "utf8").catch(() => "");
|
|
12718
13087
|
if (current.includes(ENFORCE_HOOK_MARKER)) {
|
|
12719
|
-
await
|
|
13088
|
+
await writeFile34(file, hook.body, "utf8");
|
|
12720
13089
|
} else {
|
|
12721
|
-
await
|
|
13090
|
+
await writeFile34(file, `${current.trimEnd()}
|
|
12722
13091
|
|
|
12723
13092
|
${hook.body}`, "utf8");
|
|
12724
13093
|
}
|
|
12725
13094
|
} else {
|
|
12726
|
-
await
|
|
13095
|
+
await writeFile34(file, hook.body, "utf8");
|
|
12727
13096
|
}
|
|
12728
13097
|
await chmod2(file, 493);
|
|
12729
13098
|
}
|
|
12730
13099
|
ui.success("Installed blocking git enforcement hooks: pre-commit, pre-push");
|
|
12731
13100
|
}
|
|
12732
13101
|
async function installCiEnforcement(root) {
|
|
12733
|
-
const workflowPath =
|
|
12734
|
-
await mkdir19(
|
|
12735
|
-
if (
|
|
13102
|
+
const workflowPath = path49.join(root, ".github", "workflows", "haive-enforcement.yml");
|
|
13103
|
+
await mkdir19(path49.dirname(workflowPath), { recursive: true });
|
|
13104
|
+
if (existsSync69(workflowPath)) {
|
|
12736
13105
|
ui.info("GitHub Actions enforcement workflow already exists \u2014 skipped");
|
|
12737
13106
|
return;
|
|
12738
13107
|
}
|
|
12739
|
-
await
|
|
13108
|
+
await writeFile34(workflowPath, `name: haive-enforcement
|
|
12740
13109
|
|
|
12741
13110
|
on:
|
|
12742
13111
|
pull_request:
|
|
@@ -12760,7 +13129,7 @@ jobs:
|
|
|
12760
13129
|
- name: Enforce hAIve policy
|
|
12761
13130
|
run: haive enforce ci
|
|
12762
13131
|
`, "utf8");
|
|
12763
|
-
ui.success(`Created ${
|
|
13132
|
+
ui.success(`Created ${path49.relative(root, workflowPath)}`);
|
|
12764
13133
|
}
|
|
12765
13134
|
function printReport(report, json, explain = false) {
|
|
12766
13135
|
if (json) {
|
|
@@ -12877,7 +13246,7 @@ function registerRun(program2) {
|
|
|
12877
13246
|
|
|
12878
13247
|
// src/index.ts
|
|
12879
13248
|
var program = new Command51();
|
|
12880
|
-
program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.
|
|
13249
|
+
program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.19").option("--advanced", "show maintenance and experimental commands in help");
|
|
12881
13250
|
registerInit(program);
|
|
12882
13251
|
registerWelcome(program);
|
|
12883
13252
|
registerResolveProject(program);
|