@hiveai/cli 0.9.10 → 0.9.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/index.js +1263 -774
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command51 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/briefing.ts
|
|
7
7
|
import { existsSync } from "fs";
|
|
@@ -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(([path48, changes]) => ({ path: path48, 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)));
|
|
@@ -308,12 +308,14 @@ function registerBriefing(program2) {
|
|
|
308
308
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
309
309
|
const root = findProjectRoot(opts.dir);
|
|
310
310
|
const paths = resolveHaivePaths(root);
|
|
311
|
+
const markerFiles = parseCsv(opts.files);
|
|
311
312
|
if (existsSync(paths.haiveDir)) {
|
|
312
313
|
await mkdir(paths.runtimeDir, { recursive: true });
|
|
313
314
|
await writeBriefingMarker(paths, {
|
|
314
315
|
task: opts.task ?? "CLI briefing",
|
|
315
316
|
source: "haive-briefing-cli",
|
|
316
|
-
sessionId: process.env.HAIVE_SESSION_ID
|
|
317
|
+
sessionId: process.env.HAIVE_SESSION_ID,
|
|
318
|
+
files: markerFiles
|
|
317
319
|
}).catch(() => {
|
|
318
320
|
});
|
|
319
321
|
}
|
|
@@ -386,7 +388,7 @@ function registerBriefing(program2) {
|
|
|
386
388
|
}
|
|
387
389
|
}
|
|
388
390
|
const all = ownMemories;
|
|
389
|
-
const filePaths =
|
|
391
|
+
const filePaths = markerFiles;
|
|
390
392
|
const tokens = opts.task ? tokenizeQuery(opts.task) : null;
|
|
391
393
|
const scopeFilter = opts.scope ?? "all";
|
|
392
394
|
const recaps = all.filter(({ memory: mem }) => mem.frontmatter.type === "session_recap").sort(
|
|
@@ -498,6 +500,14 @@ function registerBriefing(program2) {
|
|
|
498
500
|
if (ids.length > 0) {
|
|
499
501
|
await trackReads(paths, ids).catch(() => {
|
|
500
502
|
});
|
|
503
|
+
await writeBriefingMarker(paths, {
|
|
504
|
+
task: opts.task ?? "CLI briefing",
|
|
505
|
+
source: "haive-briefing-cli",
|
|
506
|
+
sessionId: process.env.HAIVE_SESSION_ID,
|
|
507
|
+
memoryIds: ids,
|
|
508
|
+
files: filePaths
|
|
509
|
+
}).catch(() => {
|
|
510
|
+
});
|
|
501
511
|
}
|
|
502
512
|
const radarForced = opts.radar === true;
|
|
503
513
|
const radarAuto = opts.radar !== false && top.length < RADAR_AUTO_THRESHOLD;
|
|
@@ -720,23 +730,404 @@ function registerIndexCode(program2) {
|
|
|
720
730
|
}
|
|
721
731
|
|
|
722
732
|
// src/commands/init.ts
|
|
723
|
-
import { mkdir as
|
|
724
|
-
import { existsSync as
|
|
725
|
-
import
|
|
726
|
-
import { spawnSync } from "child_process";
|
|
733
|
+
import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
734
|
+
import { existsSync as existsSync7 } from "fs";
|
|
735
|
+
import path8 from "path";
|
|
736
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
727
737
|
import "commander";
|
|
728
738
|
import {
|
|
729
739
|
AUTOPILOT_DEFAULTS,
|
|
730
740
|
buildCodeMap as buildCodeMap2,
|
|
731
|
-
resolveHaivePaths as
|
|
741
|
+
resolveHaivePaths as resolveHaivePaths5,
|
|
732
742
|
saveCodeMap as saveCodeMap2,
|
|
733
743
|
saveConfig
|
|
734
744
|
} from "@hiveai/core";
|
|
735
745
|
|
|
736
|
-
// src/commands/
|
|
737
|
-
import {
|
|
746
|
+
// src/commands/agent.ts
|
|
747
|
+
import { spawnSync } from "child_process";
|
|
748
|
+
import { existsSync as existsSync4 } from "fs";
|
|
749
|
+
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
750
|
+
import os2 from "os";
|
|
751
|
+
import path5 from "path";
|
|
752
|
+
import { createInterface } from "readline/promises";
|
|
753
|
+
import "commander";
|
|
754
|
+
import { findProjectRoot as findProjectRoot5, resolveHaivePaths as resolveHaivePaths4 } from "@hiveai/core";
|
|
755
|
+
|
|
756
|
+
// src/commands/init-mcp-setup.ts
|
|
757
|
+
import { readFile as readFile2, writeFile, mkdir as mkdir2 } from "fs/promises";
|
|
738
758
|
import { existsSync as existsSync3 } from "fs";
|
|
739
759
|
import path4 from "path";
|
|
760
|
+
import os from "os";
|
|
761
|
+
var HOME = os.homedir();
|
|
762
|
+
var HAIVE_MCP_ENTRY = {
|
|
763
|
+
command: "haive",
|
|
764
|
+
args: ["mcp", "--stdio"]
|
|
765
|
+
};
|
|
766
|
+
function projectMcpEntry(root) {
|
|
767
|
+
return {
|
|
768
|
+
command: "haive",
|
|
769
|
+
args: ["mcp", "--stdio"],
|
|
770
|
+
env: { HAIVE_PROJECT_ROOT: root }
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function cursorMcpPath() {
|
|
774
|
+
return path4.join(HOME, ".cursor", "mcp.json");
|
|
775
|
+
}
|
|
776
|
+
async function configureCursor() {
|
|
777
|
+
const mcpPath = cursorMcpPath();
|
|
778
|
+
const cursorDir = path4.join(HOME, ".cursor");
|
|
779
|
+
if (!existsSync3(cursorDir)) return { client: "Cursor", status: "not_installed" };
|
|
780
|
+
let config = {};
|
|
781
|
+
if (existsSync3(mcpPath)) {
|
|
782
|
+
try {
|
|
783
|
+
config = JSON.parse(await readFile2(mcpPath, "utf8"));
|
|
784
|
+
} catch {
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
config.mcpServers ??= {};
|
|
788
|
+
if (config.mcpServers["haive"]) return { client: "Cursor", status: "already_configured" };
|
|
789
|
+
config.mcpServers["haive"] = HAIVE_MCP_ENTRY;
|
|
790
|
+
await mkdir2(cursorDir, { recursive: true });
|
|
791
|
+
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
792
|
+
return { client: "Cursor", status: "configured", path: mcpPath };
|
|
793
|
+
}
|
|
794
|
+
function vscodeMcpPath() {
|
|
795
|
+
const candidates = [
|
|
796
|
+
path4.join(HOME, ".config", "Code", "User", "mcp.json"),
|
|
797
|
+
// Linux
|
|
798
|
+
path4.join(HOME, "Library", "Application Support", "Code", "User", "mcp.json"),
|
|
799
|
+
// macOS
|
|
800
|
+
path4.join(HOME, "AppData", "Roaming", "Code", "User", "mcp.json"),
|
|
801
|
+
// Windows
|
|
802
|
+
path4.join(HOME, ".config", "Code - Insiders", "User", "mcp.json")
|
|
803
|
+
];
|
|
804
|
+
for (const c of candidates) {
|
|
805
|
+
if (existsSync3(path4.dirname(c))) return c;
|
|
806
|
+
}
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
async function configureVSCode() {
|
|
810
|
+
const mcpPath = vscodeMcpPath();
|
|
811
|
+
if (!mcpPath) return { client: "VS Code", status: "not_installed" };
|
|
812
|
+
let config = {};
|
|
813
|
+
if (existsSync3(mcpPath)) {
|
|
814
|
+
try {
|
|
815
|
+
config = JSON.parse(await readFile2(mcpPath, "utf8"));
|
|
816
|
+
} catch {
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
config.servers ??= {};
|
|
820
|
+
if (config.servers["haive"]) return { client: "VS Code", status: "already_configured" };
|
|
821
|
+
config.servers["haive"] = { ...HAIVE_MCP_ENTRY, type: "stdio" };
|
|
822
|
+
await mkdir2(path4.dirname(mcpPath), { recursive: true });
|
|
823
|
+
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
824
|
+
return { client: "VS Code", status: "configured", path: mcpPath };
|
|
825
|
+
}
|
|
826
|
+
function claudeConfigPath() {
|
|
827
|
+
const p = path4.join(HOME, ".claude.json");
|
|
828
|
+
if (existsSync3(p)) return p;
|
|
829
|
+
const p2 = path4.join(HOME, ".config", "claude", "claude.json");
|
|
830
|
+
if (existsSync3(path4.dirname(p2))) return p2;
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
async function configureClaude() {
|
|
834
|
+
const cfgPath = claudeConfigPath() ?? path4.join(HOME, ".claude.json");
|
|
835
|
+
if (!existsSync3(cfgPath) && !existsSync3(path4.join(HOME, ".claude"))) {
|
|
836
|
+
return { client: "Claude Code", status: "not_installed" };
|
|
837
|
+
}
|
|
838
|
+
let config = {};
|
|
839
|
+
if (existsSync3(cfgPath)) {
|
|
840
|
+
try {
|
|
841
|
+
config = JSON.parse(await readFile2(cfgPath, "utf8"));
|
|
842
|
+
} catch {
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
config.mcpServers ??= {};
|
|
846
|
+
if (config.mcpServers["haive"]) return { client: "Claude Code", status: "already_configured" };
|
|
847
|
+
config.mcpServers["haive"] = { ...HAIVE_MCP_ENTRY, type: "stdio" };
|
|
848
|
+
await writeFile(cfgPath, JSON.stringify(config, null, 2), "utf8");
|
|
849
|
+
return { client: "Claude Code", status: "configured", path: cfgPath };
|
|
850
|
+
}
|
|
851
|
+
function windsurfMcpPath() {
|
|
852
|
+
const candidates = [
|
|
853
|
+
path4.join(HOME, ".codeium", "windsurf", "mcp_config.json"),
|
|
854
|
+
path4.join(HOME, ".windsurf", "mcp.json")
|
|
855
|
+
];
|
|
856
|
+
for (const c of candidates) {
|
|
857
|
+
if (existsSync3(path4.dirname(c))) return c;
|
|
858
|
+
}
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
async function configureWindsurf() {
|
|
862
|
+
const mcpPath = windsurfMcpPath();
|
|
863
|
+
if (!mcpPath) return { client: "Windsurf", status: "not_installed" };
|
|
864
|
+
let config = {};
|
|
865
|
+
if (existsSync3(mcpPath)) {
|
|
866
|
+
try {
|
|
867
|
+
config = JSON.parse(await readFile2(mcpPath, "utf8"));
|
|
868
|
+
} catch {
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
config.mcpServers ??= {};
|
|
872
|
+
if (config.mcpServers["haive"]) return { client: "Windsurf", status: "already_configured" };
|
|
873
|
+
config.mcpServers["haive"] = HAIVE_MCP_ENTRY;
|
|
874
|
+
await mkdir2(path4.dirname(mcpPath), { recursive: true });
|
|
875
|
+
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
876
|
+
return { client: "Windsurf", status: "configured", path: mcpPath };
|
|
877
|
+
}
|
|
878
|
+
async function autoConfigureMcpClients() {
|
|
879
|
+
const results = [];
|
|
880
|
+
const configurators = [configureCursor, configureVSCode, configureClaude, configureWindsurf];
|
|
881
|
+
for (const fn of configurators) {
|
|
882
|
+
try {
|
|
883
|
+
results.push(await fn());
|
|
884
|
+
} catch (err) {
|
|
885
|
+
const name = fn.name.replace("configure", "");
|
|
886
|
+
results.push({ client: name, status: "error", error: String(err) });
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return results;
|
|
890
|
+
}
|
|
891
|
+
async function configureProjectMcpClients(root) {
|
|
892
|
+
const entry = projectMcpEntry(root);
|
|
893
|
+
const results = [];
|
|
894
|
+
try {
|
|
895
|
+
const cursorPath = path4.join(root, ".cursor", "mcp.json");
|
|
896
|
+
let config = {};
|
|
897
|
+
if (existsSync3(cursorPath)) {
|
|
898
|
+
try {
|
|
899
|
+
config = JSON.parse(await readFile2(cursorPath, "utf8"));
|
|
900
|
+
} catch {
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
config.mcpServers ??= {};
|
|
904
|
+
config.mcpServers["haive"] = entry;
|
|
905
|
+
await mkdir2(path4.dirname(cursorPath), { recursive: true });
|
|
906
|
+
await writeFile(cursorPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
907
|
+
results.push({ client: "Cursor (project)", status: "configured", path: cursorPath });
|
|
908
|
+
} catch (err) {
|
|
909
|
+
results.push({ client: "Cursor (project)", status: "error", error: String(err) });
|
|
910
|
+
}
|
|
911
|
+
try {
|
|
912
|
+
const vscodePath = path4.join(root, ".vscode", "mcp.json");
|
|
913
|
+
let config = {};
|
|
914
|
+
if (existsSync3(vscodePath)) {
|
|
915
|
+
try {
|
|
916
|
+
config = JSON.parse(await readFile2(vscodePath, "utf8"));
|
|
917
|
+
} catch {
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
config.servers ??= {};
|
|
921
|
+
config.servers["haive"] = { ...entry, type: "stdio" };
|
|
922
|
+
await mkdir2(path4.dirname(vscodePath), { recursive: true });
|
|
923
|
+
await writeFile(vscodePath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
924
|
+
results.push({ client: "VS Code (workspace)", status: "configured", path: vscodePath });
|
|
925
|
+
} catch (err) {
|
|
926
|
+
results.push({ client: "VS Code (workspace)", status: "error", error: String(err) });
|
|
927
|
+
}
|
|
928
|
+
try {
|
|
929
|
+
const mcpPath = path4.join(root, ".mcp.json");
|
|
930
|
+
let config = {};
|
|
931
|
+
if (existsSync3(mcpPath)) {
|
|
932
|
+
try {
|
|
933
|
+
config = JSON.parse(await readFile2(mcpPath, "utf8"));
|
|
934
|
+
} catch {
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
config.mcpServers ??= {};
|
|
938
|
+
config.mcpServers["haive"] = { ...entry, type: "stdio" };
|
|
939
|
+
await writeFile(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
940
|
+
results.push({ client: "Claude Code (project)", status: "configured", path: mcpPath });
|
|
941
|
+
} catch (err) {
|
|
942
|
+
results.push({ client: "Claude Code (project)", status: "error", error: String(err) });
|
|
943
|
+
}
|
|
944
|
+
return results;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// src/commands/agent.ts
|
|
948
|
+
function registerAgent(program2) {
|
|
949
|
+
const agent = program2.command("agent").description("Detect, configure, and report the best hAIve mode for AI coding agents.");
|
|
950
|
+
agent.command("detect").description("Detect available AI agents and hAIve MCP/wrapper readiness.").option("-d, --dir <dir>", "project root").option("--json", "emit JSON", false).action(async (opts) => {
|
|
951
|
+
const detection = await detectAgentMode(opts.dir);
|
|
952
|
+
printDetection(detection, Boolean(opts.json));
|
|
953
|
+
});
|
|
954
|
+
agent.command("status").description("Alias for agent detect.").option("-d, --dir <dir>", "project root").option("--json", "emit JSON", false).action(async (opts) => {
|
|
955
|
+
const detection = await detectAgentMode(opts.dir);
|
|
956
|
+
printDetection(detection, Boolean(opts.json));
|
|
957
|
+
});
|
|
958
|
+
agent.command("setup").description("Configure hAIve project MCP, optional global MCP clients, and wrapper fallback metadata.").option("-d, --dir <dir>", "project root").option("-y, --yes", "approve user-level/global MCP configuration without prompting", false).option("--no-global", "skip user-level/global MCP configuration").option("--json", "emit JSON", false).action(async (opts) => {
|
|
959
|
+
const result = await setupAgentMode(opts.dir, {
|
|
960
|
+
yes: Boolean(opts.yes),
|
|
961
|
+
global: opts.global !== false && opts.noGlobal !== true,
|
|
962
|
+
interactive: process.stdin.isTTY
|
|
963
|
+
});
|
|
964
|
+
if (opts.json) {
|
|
965
|
+
console.log(JSON.stringify(result, null, 2));
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
printSetupResult(result);
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
async function setupAgentMode(dir, opts = {}) {
|
|
972
|
+
const root = findProjectRoot5(dir);
|
|
973
|
+
const paths = resolveHaivePaths4(root);
|
|
974
|
+
const projectResults = await configureProjectMcpClients(root);
|
|
975
|
+
const detectionBeforeGlobal = await detectAgentMode(root);
|
|
976
|
+
let globalResults = [];
|
|
977
|
+
let globalSkippedReason;
|
|
978
|
+
const shouldConsiderGlobal = opts.global !== false;
|
|
979
|
+
if (shouldConsiderGlobal) {
|
|
980
|
+
const approved = opts.yes === true || (opts.interactive ? await confirmGlobalSetup() : false);
|
|
981
|
+
if (approved) {
|
|
982
|
+
globalResults = await autoConfigureMcpClients();
|
|
983
|
+
const codex = await configureCodexIfAvailable(root);
|
|
984
|
+
if (codex) globalResults.push(codex);
|
|
985
|
+
} else {
|
|
986
|
+
globalSkippedReason = opts.interactive ? "User declined user-level/global MCP configuration." : "Non-interactive shell; skipped user-level/global MCP configuration. Re-run `haive agent setup --yes` to apply it.";
|
|
987
|
+
}
|
|
988
|
+
} else {
|
|
989
|
+
globalSkippedReason = "User-level/global MCP configuration disabled.";
|
|
990
|
+
}
|
|
991
|
+
const detection = await detectAgentMode(root);
|
|
992
|
+
const modeFile = await writeAgentModeRecord(paths, detection, globalSkippedReason);
|
|
993
|
+
return {
|
|
994
|
+
detection,
|
|
995
|
+
project_results: projectResults,
|
|
996
|
+
global_results: globalResults,
|
|
997
|
+
mode_file: modeFile,
|
|
998
|
+
...globalSkippedReason ? { global_skipped_reason: globalSkippedReason } : {}
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
async function detectAgentMode(dir) {
|
|
1002
|
+
const root = findProjectRoot5(dir);
|
|
1003
|
+
const paths = resolveHaivePaths4(root);
|
|
1004
|
+
const projectMcp = [
|
|
1005
|
+
{ client: "Claude Code", path: path5.join(root, ".mcp.json"), present: existsSync4(path5.join(root, ".mcp.json")) },
|
|
1006
|
+
{ client: "Cursor", path: path5.join(root, ".cursor", "mcp.json"), present: existsSync4(path5.join(root, ".cursor", "mcp.json")) },
|
|
1007
|
+
{ client: "VS Code", path: path5.join(root, ".vscode", "mcp.json"), present: existsSync4(path5.join(root, ".vscode", "mcp.json")) }
|
|
1008
|
+
];
|
|
1009
|
+
const installedAgents = [
|
|
1010
|
+
{ agent: "Codex", command: "codex", installed: commandExists("codex"), mcp_configured: codexMcpConfigured() },
|
|
1011
|
+
{ agent: "Claude", command: "claude", installed: commandExists("claude") },
|
|
1012
|
+
{ agent: "Aider", command: "aider", installed: commandExists("aider") },
|
|
1013
|
+
{ agent: "Cursor", command: "cursor", installed: commandExists("cursor") }
|
|
1014
|
+
];
|
|
1015
|
+
const hasProjectMcp = projectMcp.some((item) => item.present);
|
|
1016
|
+
const hasNativeMcp = hasProjectMcp || installedAgents.some((a) => a.mcp_configured);
|
|
1017
|
+
const wrapperAgent = installedAgents.find((a) => a.installed && ["codex", "claude", "aider"].includes(a.command));
|
|
1018
|
+
const recommendedMode = hasNativeMcp ? "mcp" : wrapperAgent ? "wrapped" : "fallback";
|
|
1019
|
+
const recommendedCommand = recommendedMode === "mcp" ? "Restart your AI client, then call get_briefing before editing." : recommendedMode === "wrapped" && wrapperAgent ? `haive run -- ${wrapperAgent.command}` : 'haive briefing --task "..." --files "..."';
|
|
1020
|
+
return {
|
|
1021
|
+
root,
|
|
1022
|
+
initialized: existsSync4(paths.haiveDir),
|
|
1023
|
+
project_mcp: projectMcp,
|
|
1024
|
+
installed_agents: installedAgents,
|
|
1025
|
+
recommended_mode: recommendedMode,
|
|
1026
|
+
recommended_command: recommendedCommand
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
async function writeAgentModeRecord(paths, detection, skippedReason) {
|
|
1030
|
+
const dir = path5.join(paths.runtimeDir, "enforcement");
|
|
1031
|
+
await mkdir3(dir, { recursive: true });
|
|
1032
|
+
const file = path5.join(dir, "agent-mode.json");
|
|
1033
|
+
const record = {
|
|
1034
|
+
selected_mode: detection.recommended_mode,
|
|
1035
|
+
recommended_command: detection.recommended_command,
|
|
1036
|
+
configured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1037
|
+
project_root: detection.root,
|
|
1038
|
+
notes: [
|
|
1039
|
+
"mcp = native hAIve MCP tools are available or project MCP config exists.",
|
|
1040
|
+
"wrapped = use haive run when native MCP is unavailable.",
|
|
1041
|
+
"fallback = use haive briefing/enforce manually.",
|
|
1042
|
+
...skippedReason ? [skippedReason] : []
|
|
1043
|
+
]
|
|
1044
|
+
};
|
|
1045
|
+
await writeFile2(file, JSON.stringify(record, null, 2) + "\n", "utf8");
|
|
1046
|
+
return file;
|
|
1047
|
+
}
|
|
1048
|
+
async function confirmGlobalSetup() {
|
|
1049
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1050
|
+
try {
|
|
1051
|
+
const answer = await rl.question(
|
|
1052
|
+
"Configure hAIve in user-level AI client configs (Cursor/VS Code/Claude/Codex when detected)? [y/N] "
|
|
1053
|
+
);
|
|
1054
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1055
|
+
} finally {
|
|
1056
|
+
rl.close();
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
async function configureCodexIfAvailable(root) {
|
|
1060
|
+
if (!commandExists("codex")) return { client: "Codex", status: "not_installed" };
|
|
1061
|
+
if (codexMcpConfigured()) return { client: "Codex", status: "already_configured" };
|
|
1062
|
+
const result = spawnSync("codex", [
|
|
1063
|
+
"mcp",
|
|
1064
|
+
"add",
|
|
1065
|
+
"haive",
|
|
1066
|
+
"--env",
|
|
1067
|
+
`HAIVE_PROJECT_ROOT=${root}`,
|
|
1068
|
+
"--",
|
|
1069
|
+
"haive",
|
|
1070
|
+
"mcp",
|
|
1071
|
+
"--stdio"
|
|
1072
|
+
], { encoding: "utf8" });
|
|
1073
|
+
if (result.status === 0) return { client: "Codex", status: "configured", path: path5.join(os2.homedir(), ".codex", "config.toml") };
|
|
1074
|
+
return { client: "Codex", status: "error", error: result.stderr || result.stdout || "codex mcp add failed" };
|
|
1075
|
+
}
|
|
1076
|
+
function commandExists(command) {
|
|
1077
|
+
const result = spawnSync(process.platform === "win32" ? "where" : "which", [command], {
|
|
1078
|
+
encoding: "utf8",
|
|
1079
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1080
|
+
});
|
|
1081
|
+
return result.status === 0;
|
|
1082
|
+
}
|
|
1083
|
+
function codexMcpConfigured() {
|
|
1084
|
+
if (!commandExists("codex")) return false;
|
|
1085
|
+
const result = spawnSync("codex", ["mcp", "get", "haive"], {
|
|
1086
|
+
encoding: "utf8",
|
|
1087
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1088
|
+
});
|
|
1089
|
+
return result.status === 0;
|
|
1090
|
+
}
|
|
1091
|
+
function printDetection(detection, json) {
|
|
1092
|
+
if (json) {
|
|
1093
|
+
console.log(JSON.stringify(detection, null, 2));
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
console.log(ui.bold("hAIve agent status"));
|
|
1097
|
+
console.log(ui.dim(` root: ${detection.root}`));
|
|
1098
|
+
console.log(`${detection.initialized ? ui.green("\u2713") : ui.red("\u2717")} project initialized`);
|
|
1099
|
+
for (const cfg of detection.project_mcp) {
|
|
1100
|
+
console.log(`${cfg.present ? ui.green("\u2713") : ui.yellow("\u2022")} ${cfg.client} project MCP ${ui.dim(path5.relative(detection.root, cfg.path))}`);
|
|
1101
|
+
}
|
|
1102
|
+
for (const agent of detection.installed_agents) {
|
|
1103
|
+
const marker = agent.installed ? ui.green("\u2713") : ui.dim("\u2022");
|
|
1104
|
+
const mcp = agent.mcp_configured === true ? " + hAIve MCP" : "";
|
|
1105
|
+
console.log(`${marker} ${agent.agent} (${agent.command})${mcp}`);
|
|
1106
|
+
}
|
|
1107
|
+
console.log(ui.bold(`Recommended mode: ${detection.recommended_mode}`));
|
|
1108
|
+
console.log(` ${detection.recommended_command}`);
|
|
1109
|
+
}
|
|
1110
|
+
function printSetupResult(result) {
|
|
1111
|
+
for (const item of result.project_results) {
|
|
1112
|
+
if (item.status === "configured") ui.success(`${item.client} project MCP config written (${item.path})`);
|
|
1113
|
+
else if (item.status === "already_configured") ui.info(`${item.client} already configured`);
|
|
1114
|
+
else if (item.status === "error") ui.warn(`${item.client}: ${item.error}`);
|
|
1115
|
+
}
|
|
1116
|
+
for (const item of result.global_results) {
|
|
1117
|
+
if (item.status === "configured") ui.success(`${item.client} user-level MCP configured${item.path ? ` (${item.path})` : ""}`);
|
|
1118
|
+
else if (item.status === "already_configured") ui.info(`${item.client} user-level MCP already configured`);
|
|
1119
|
+
else if (item.status === "not_installed") ui.info(`${item.client} not detected`);
|
|
1120
|
+
else if (item.status === "error") ui.warn(`${item.client}: ${item.error}`);
|
|
1121
|
+
}
|
|
1122
|
+
if (result.global_skipped_reason) ui.warn(result.global_skipped_reason);
|
|
1123
|
+
ui.success(`Agent mode recorded at ${result.mode_file}`);
|
|
1124
|
+
printDetection(result.detection, false);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// src/commands/init-bootstrap.ts
|
|
1128
|
+
import { readdir, readFile as readFile3 } from "fs/promises";
|
|
1129
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1130
|
+
import path6 from "path";
|
|
740
1131
|
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
741
1132
|
"node_modules",
|
|
742
1133
|
"dist",
|
|
@@ -822,12 +1213,12 @@ function detectKeyDeps(allDeps) {
|
|
|
822
1213
|
return KEY_DEPS.filter((d) => allDeps[d] !== void 0);
|
|
823
1214
|
}
|
|
824
1215
|
function detectLanguage(root) {
|
|
825
|
-
if (
|
|
826
|
-
if (
|
|
827
|
-
if (
|
|
828
|
-
if (
|
|
829
|
-
if (
|
|
830
|
-
if (
|
|
1216
|
+
if (existsSync5(path6.join(root, "tsconfig.json"))) return "TypeScript";
|
|
1217
|
+
if (existsSync5(path6.join(root, "pyproject.toml")) || existsSync5(path6.join(root, "setup.py"))) return "Python";
|
|
1218
|
+
if (existsSync5(path6.join(root, "go.mod"))) return "Go";
|
|
1219
|
+
if (existsSync5(path6.join(root, "pom.xml")) || existsSync5(path6.join(root, "build.gradle"))) return "Java/Kotlin";
|
|
1220
|
+
if (existsSync5(path6.join(root, "Cargo.toml"))) return "Rust";
|
|
1221
|
+
if (existsSync5(path6.join(root, "package.json"))) return "JavaScript";
|
|
831
1222
|
return "Unknown";
|
|
832
1223
|
}
|
|
833
1224
|
function detectProjectType(frameworks, scripts, isMonorepo) {
|
|
@@ -844,7 +1235,7 @@ function detectProjectType(frameworks, scripts, isMonorepo) {
|
|
|
844
1235
|
if (frameworks.includes("Express") || frameworks.includes("Fastify") || frameworks.includes("Hono")) return "Backend API";
|
|
845
1236
|
if (frameworks.includes("React") || frameworks.includes("Vue") || frameworks.includes("Svelte")) return "Frontend SPA";
|
|
846
1237
|
if (scripts["build"] && !scripts["dev"]) return "CLI tool / library";
|
|
847
|
-
if (
|
|
1238
|
+
if (existsSync5("pom.xml")) return "Java backend";
|
|
848
1239
|
return "Application";
|
|
849
1240
|
}
|
|
850
1241
|
async function scanDirs(root, maxDepth = 2) {
|
|
@@ -860,9 +1251,9 @@ async function scanDirs(root, maxDepth = 2) {
|
|
|
860
1251
|
for (const entry of entries) {
|
|
861
1252
|
if (!entry.isDirectory()) continue;
|
|
862
1253
|
if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
863
|
-
const rel =
|
|
1254
|
+
const rel = path6.relative(root, path6.join(dir, entry.name));
|
|
864
1255
|
results.push(rel);
|
|
865
|
-
await walk(
|
|
1256
|
+
await walk(path6.join(dir, entry.name), depth + 1);
|
|
866
1257
|
}
|
|
867
1258
|
}
|
|
868
1259
|
await walk(root, 0);
|
|
@@ -979,10 +1370,10 @@ function readmeExcerpt(readme) {
|
|
|
979
1370
|
}
|
|
980
1371
|
async function generateBootstrapContext(root) {
|
|
981
1372
|
let pkg = {};
|
|
982
|
-
const pkgPath =
|
|
983
|
-
if (
|
|
1373
|
+
const pkgPath = path6.join(root, "package.json");
|
|
1374
|
+
if (existsSync5(pkgPath)) {
|
|
984
1375
|
try {
|
|
985
|
-
pkg = JSON.parse(await
|
|
1376
|
+
pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
|
|
986
1377
|
} catch {
|
|
987
1378
|
}
|
|
988
1379
|
}
|
|
@@ -992,270 +1383,79 @@ async function generateBootstrapContext(root) {
|
|
|
992
1383
|
const language = detectLanguage(root);
|
|
993
1384
|
const isMonorepo = pkg.workspaces !== void 0 && (Array.isArray(pkg.workspaces) ? pkg.workspaces.length > 0 : true);
|
|
994
1385
|
const projectType = detectProjectType(frameworks, pkg.scripts ?? {}, isMonorepo);
|
|
995
|
-
const projectName = pkg.name ??
|
|
1386
|
+
const projectName = pkg.name ?? path6.basename(root);
|
|
996
1387
|
const projectDesc = pkg.description ?? "";
|
|
997
1388
|
let readmeSummary = "";
|
|
998
1389
|
for (const name of ["README.md", "readme.md", "README"]) {
|
|
999
|
-
const p =
|
|
1000
|
-
if (
|
|
1390
|
+
const p = path6.join(root, name);
|
|
1391
|
+
if (existsSync5(p)) {
|
|
1001
1392
|
try {
|
|
1002
|
-
const content = await
|
|
1393
|
+
const content = await readFile3(p, "utf8");
|
|
1003
1394
|
readmeSummary = readmeExcerpt(content);
|
|
1004
1395
|
break;
|
|
1005
1396
|
} catch {
|
|
1006
1397
|
}
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
const dirs = await scanDirs(root, 2);
|
|
1010
|
-
const moduleLines = inferModuleDescriptions(dirs, frameworks);
|
|
1011
|
-
const scripts = pkg.scripts ?? {};
|
|
1012
|
-
const scriptLines = Object.entries(scripts).filter(([k]) => ["build", "dev", "start", "test", "lint", "deploy"].includes(k)).map(([k, v]) => `- \`${k}\`: ${v}`).slice(0, 6);
|
|
1013
|
-
const stackParts = [language];
|
|
1014
|
-
if (frameworks.length) stackParts.push(...frameworks);
|
|
1015
|
-
const techStack = stackParts.join(", ");
|
|
1016
|
-
const notableDeps = Object.keys(allDeps).filter((d) => !d.startsWith("@types/") && !["typescript", "eslint", "prettier", "jest"].includes(d)).filter((d) => !["react", "react-dom", "next", "vue", "express"].includes(d)).slice(0, 10).map((d) => `\`${d}\``);
|
|
1017
|
-
const lines = [
|
|
1018
|
-
`# Project context \u2014 ${projectName}`,
|
|
1019
|
-
"",
|
|
1020
|
-
`> Auto-generated by \`haive init --bootstrap\`. Review and refine \u2014 especially the Architecture and Gotchas sections.`,
|
|
1021
|
-
"",
|
|
1022
|
-
`## Overview`,
|
|
1023
|
-
`**Type:** ${projectType}`,
|
|
1024
|
-
`**Tech stack:** ${techStack}`,
|
|
1025
|
-
...projectDesc ? [`**Description:** ${projectDesc}`] : [],
|
|
1026
|
-
...readmeSummary ? [`**From README:** ${readmeSummary}`] : [],
|
|
1027
|
-
"",
|
|
1028
|
-
`## Architecture`,
|
|
1029
|
-
`TODO \u2014 fill in the high-level architecture (inferred structure below, verify manually):`,
|
|
1030
|
-
"",
|
|
1031
|
-
...moduleLines.length ? moduleLines : ["TODO \u2014 no clear structure detected."],
|
|
1032
|
-
"",
|
|
1033
|
-
`## Key modules`,
|
|
1034
|
-
`TODO \u2014 describe the purpose of the main modules. The directory scan found:`,
|
|
1035
|
-
...dirs.filter((d) => !d.includes("/")).slice(0, 8).map((d) => `- \`${d}/\``),
|
|
1036
|
-
"",
|
|
1037
|
-
`## Conventions`,
|
|
1038
|
-
`TODO \u2014 fill in coding conventions (naming, patterns, file layout).`,
|
|
1039
|
-
"",
|
|
1040
|
-
...scriptLines.length ? [
|
|
1041
|
-
`**Available scripts:**`,
|
|
1042
|
-
...scriptLines,
|
|
1043
|
-
""
|
|
1044
|
-
] : [],
|
|
1045
|
-
...keyDeps.length ? [
|
|
1046
|
-
`**Key dependencies in use:** ${keyDeps.map((d) => `\`${d}\``).join(", ")}`,
|
|
1047
|
-
""
|
|
1048
|
-
] : [],
|
|
1049
|
-
...notableDeps.length ? [
|
|
1050
|
-
`**Other notable packages:** ${notableDeps.join(", ")}`,
|
|
1051
|
-
""
|
|
1052
|
-
] : [],
|
|
1053
|
-
`## Glossary`,
|
|
1054
|
-
`TODO \u2014 domain terms and what they mean here.`,
|
|
1055
|
-
"",
|
|
1056
|
-
`## Gotchas`,
|
|
1057
|
-
`TODO \u2014 known traps, surprising behavior, things newcomers stub their toes on.`,
|
|
1058
|
-
`(Run \`haive memory import-changelog\` or \`haive memory import README.md\` to seed these automatically.)`,
|
|
1059
|
-
""
|
|
1060
|
-
];
|
|
1061
|
-
return lines.join("\n");
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
// src/commands/init-mcp-setup.ts
|
|
1065
|
-
import { readFile as readFile3, writeFile, mkdir as mkdir2 } from "fs/promises";
|
|
1066
|
-
import { existsSync as existsSync4 } from "fs";
|
|
1067
|
-
import path5 from "path";
|
|
1068
|
-
import os from "os";
|
|
1069
|
-
var HOME = os.homedir();
|
|
1070
|
-
var HAIVE_MCP_ENTRY = {
|
|
1071
|
-
command: "haive",
|
|
1072
|
-
args: ["mcp", "--stdio"]
|
|
1073
|
-
};
|
|
1074
|
-
function projectMcpEntry(root) {
|
|
1075
|
-
return {
|
|
1076
|
-
command: "haive",
|
|
1077
|
-
args: ["mcp", "--stdio"],
|
|
1078
|
-
env: { HAIVE_PROJECT_ROOT: root }
|
|
1079
|
-
};
|
|
1080
|
-
}
|
|
1081
|
-
function cursorMcpPath() {
|
|
1082
|
-
return path5.join(HOME, ".cursor", "mcp.json");
|
|
1083
|
-
}
|
|
1084
|
-
async function configureCursor() {
|
|
1085
|
-
const mcpPath = cursorMcpPath();
|
|
1086
|
-
const cursorDir = path5.join(HOME, ".cursor");
|
|
1087
|
-
if (!existsSync4(cursorDir)) return { client: "Cursor", status: "not_installed" };
|
|
1088
|
-
let config = {};
|
|
1089
|
-
if (existsSync4(mcpPath)) {
|
|
1090
|
-
try {
|
|
1091
|
-
config = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
1092
|
-
} catch {
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
config.mcpServers ??= {};
|
|
1096
|
-
if (config.mcpServers["haive"]) return { client: "Cursor", status: "already_configured" };
|
|
1097
|
-
config.mcpServers["haive"] = HAIVE_MCP_ENTRY;
|
|
1098
|
-
await mkdir2(cursorDir, { recursive: true });
|
|
1099
|
-
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
1100
|
-
return { client: "Cursor", status: "configured", path: mcpPath };
|
|
1101
|
-
}
|
|
1102
|
-
function vscodeMcpPath() {
|
|
1103
|
-
const candidates = [
|
|
1104
|
-
path5.join(HOME, ".config", "Code", "User", "mcp.json"),
|
|
1105
|
-
// Linux
|
|
1106
|
-
path5.join(HOME, "Library", "Application Support", "Code", "User", "mcp.json"),
|
|
1107
|
-
// macOS
|
|
1108
|
-
path5.join(HOME, "AppData", "Roaming", "Code", "User", "mcp.json"),
|
|
1109
|
-
// Windows
|
|
1110
|
-
path5.join(HOME, ".config", "Code - Insiders", "User", "mcp.json")
|
|
1111
|
-
];
|
|
1112
|
-
for (const c of candidates) {
|
|
1113
|
-
if (existsSync4(path5.dirname(c))) return c;
|
|
1114
|
-
}
|
|
1115
|
-
return null;
|
|
1116
|
-
}
|
|
1117
|
-
async function configureVSCode() {
|
|
1118
|
-
const mcpPath = vscodeMcpPath();
|
|
1119
|
-
if (!mcpPath) return { client: "VS Code", status: "not_installed" };
|
|
1120
|
-
let config = {};
|
|
1121
|
-
if (existsSync4(mcpPath)) {
|
|
1122
|
-
try {
|
|
1123
|
-
config = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
1124
|
-
} catch {
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
config.servers ??= {};
|
|
1128
|
-
if (config.servers["haive"]) return { client: "VS Code", status: "already_configured" };
|
|
1129
|
-
config.servers["haive"] = { ...HAIVE_MCP_ENTRY, type: "stdio" };
|
|
1130
|
-
await mkdir2(path5.dirname(mcpPath), { recursive: true });
|
|
1131
|
-
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
1132
|
-
return { client: "VS Code", status: "configured", path: mcpPath };
|
|
1133
|
-
}
|
|
1134
|
-
function claudeConfigPath() {
|
|
1135
|
-
const p = path5.join(HOME, ".claude.json");
|
|
1136
|
-
if (existsSync4(p)) return p;
|
|
1137
|
-
const p2 = path5.join(HOME, ".config", "claude", "claude.json");
|
|
1138
|
-
if (existsSync4(path5.dirname(p2))) return p2;
|
|
1139
|
-
return null;
|
|
1140
|
-
}
|
|
1141
|
-
async function configureClaude() {
|
|
1142
|
-
const cfgPath = claudeConfigPath() ?? path5.join(HOME, ".claude.json");
|
|
1143
|
-
if (!existsSync4(cfgPath) && !existsSync4(path5.join(HOME, ".claude"))) {
|
|
1144
|
-
return { client: "Claude Code", status: "not_installed" };
|
|
1145
|
-
}
|
|
1146
|
-
let config = {};
|
|
1147
|
-
if (existsSync4(cfgPath)) {
|
|
1148
|
-
try {
|
|
1149
|
-
config = JSON.parse(await readFile3(cfgPath, "utf8"));
|
|
1150
|
-
} catch {
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
config.mcpServers ??= {};
|
|
1154
|
-
if (config.mcpServers["haive"]) return { client: "Claude Code", status: "already_configured" };
|
|
1155
|
-
config.mcpServers["haive"] = { ...HAIVE_MCP_ENTRY, type: "stdio" };
|
|
1156
|
-
await writeFile(cfgPath, JSON.stringify(config, null, 2), "utf8");
|
|
1157
|
-
return { client: "Claude Code", status: "configured", path: cfgPath };
|
|
1158
|
-
}
|
|
1159
|
-
function windsurfMcpPath() {
|
|
1160
|
-
const candidates = [
|
|
1161
|
-
path5.join(HOME, ".codeium", "windsurf", "mcp_config.json"),
|
|
1162
|
-
path5.join(HOME, ".windsurf", "mcp.json")
|
|
1163
|
-
];
|
|
1164
|
-
for (const c of candidates) {
|
|
1165
|
-
if (existsSync4(path5.dirname(c))) return c;
|
|
1166
|
-
}
|
|
1167
|
-
return null;
|
|
1168
|
-
}
|
|
1169
|
-
async function configureWindsurf() {
|
|
1170
|
-
const mcpPath = windsurfMcpPath();
|
|
1171
|
-
if (!mcpPath) return { client: "Windsurf", status: "not_installed" };
|
|
1172
|
-
let config = {};
|
|
1173
|
-
if (existsSync4(mcpPath)) {
|
|
1174
|
-
try {
|
|
1175
|
-
config = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
1176
|
-
} catch {
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
config.mcpServers ??= {};
|
|
1180
|
-
if (config.mcpServers["haive"]) return { client: "Windsurf", status: "already_configured" };
|
|
1181
|
-
config.mcpServers["haive"] = HAIVE_MCP_ENTRY;
|
|
1182
|
-
await mkdir2(path5.dirname(mcpPath), { recursive: true });
|
|
1183
|
-
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
1184
|
-
return { client: "Windsurf", status: "configured", path: mcpPath };
|
|
1185
|
-
}
|
|
1186
|
-
async function autoConfigureMcpClients() {
|
|
1187
|
-
const results = [];
|
|
1188
|
-
const configurators = [configureCursor, configureVSCode, configureClaude, configureWindsurf];
|
|
1189
|
-
for (const fn of configurators) {
|
|
1190
|
-
try {
|
|
1191
|
-
results.push(await fn());
|
|
1192
|
-
} catch (err) {
|
|
1193
|
-
const name = fn.name.replace("configure", "");
|
|
1194
|
-
results.push({ client: name, status: "error", error: String(err) });
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
return results;
|
|
1198
|
-
}
|
|
1199
|
-
async function configureProjectMcpClients(root) {
|
|
1200
|
-
const entry = projectMcpEntry(root);
|
|
1201
|
-
const results = [];
|
|
1202
|
-
try {
|
|
1203
|
-
const cursorPath = path5.join(root, ".cursor", "mcp.json");
|
|
1204
|
-
let config = {};
|
|
1205
|
-
if (existsSync4(cursorPath)) {
|
|
1206
|
-
try {
|
|
1207
|
-
config = JSON.parse(await readFile3(cursorPath, "utf8"));
|
|
1208
|
-
} catch {
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
config.mcpServers ??= {};
|
|
1212
|
-
config.mcpServers["haive"] = entry;
|
|
1213
|
-
await mkdir2(path5.dirname(cursorPath), { recursive: true });
|
|
1214
|
-
await writeFile(cursorPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
1215
|
-
results.push({ client: "Cursor (project)", status: "configured", path: cursorPath });
|
|
1216
|
-
} catch (err) {
|
|
1217
|
-
results.push({ client: "Cursor (project)", status: "error", error: String(err) });
|
|
1218
|
-
}
|
|
1219
|
-
try {
|
|
1220
|
-
const vscodePath = path5.join(root, ".vscode", "mcp.json");
|
|
1221
|
-
let config = {};
|
|
1222
|
-
if (existsSync4(vscodePath)) {
|
|
1223
|
-
try {
|
|
1224
|
-
config = JSON.parse(await readFile3(vscodePath, "utf8"));
|
|
1225
|
-
} catch {
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
config.servers ??= {};
|
|
1229
|
-
config.servers["haive"] = { ...entry, type: "stdio" };
|
|
1230
|
-
await mkdir2(path5.dirname(vscodePath), { recursive: true });
|
|
1231
|
-
await writeFile(vscodePath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
1232
|
-
results.push({ client: "VS Code (workspace)", status: "configured", path: vscodePath });
|
|
1233
|
-
} catch (err) {
|
|
1234
|
-
results.push({ client: "VS Code (workspace)", status: "error", error: String(err) });
|
|
1235
|
-
}
|
|
1236
|
-
try {
|
|
1237
|
-
const mcpPath = path5.join(root, ".mcp.json");
|
|
1238
|
-
let config = {};
|
|
1239
|
-
if (existsSync4(mcpPath)) {
|
|
1240
|
-
try {
|
|
1241
|
-
config = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
1242
|
-
} catch {
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
config.mcpServers ??= {};
|
|
1246
|
-
config.mcpServers["haive"] = { ...entry, type: "stdio" };
|
|
1247
|
-
await writeFile(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
1248
|
-
results.push({ client: "Claude Code (project)", status: "configured", path: mcpPath });
|
|
1249
|
-
} catch (err) {
|
|
1250
|
-
results.push({ client: "Claude Code (project)", status: "error", error: String(err) });
|
|
1398
|
+
}
|
|
1251
1399
|
}
|
|
1252
|
-
|
|
1400
|
+
const dirs = await scanDirs(root, 2);
|
|
1401
|
+
const moduleLines = inferModuleDescriptions(dirs, frameworks);
|
|
1402
|
+
const scripts = pkg.scripts ?? {};
|
|
1403
|
+
const scriptLines = Object.entries(scripts).filter(([k]) => ["build", "dev", "start", "test", "lint", "deploy"].includes(k)).map(([k, v]) => `- \`${k}\`: ${v}`).slice(0, 6);
|
|
1404
|
+
const stackParts = [language];
|
|
1405
|
+
if (frameworks.length) stackParts.push(...frameworks);
|
|
1406
|
+
const techStack = stackParts.join(", ");
|
|
1407
|
+
const notableDeps = Object.keys(allDeps).filter((d) => !d.startsWith("@types/") && !["typescript", "eslint", "prettier", "jest"].includes(d)).filter((d) => !["react", "react-dom", "next", "vue", "express"].includes(d)).slice(0, 10).map((d) => `\`${d}\``);
|
|
1408
|
+
const lines = [
|
|
1409
|
+
`# Project context \u2014 ${projectName}`,
|
|
1410
|
+
"",
|
|
1411
|
+
`> Auto-generated by \`haive init --bootstrap\`. Review and refine \u2014 especially the Architecture and Gotchas sections.`,
|
|
1412
|
+
"",
|
|
1413
|
+
`## Overview`,
|
|
1414
|
+
`**Type:** ${projectType}`,
|
|
1415
|
+
`**Tech stack:** ${techStack}`,
|
|
1416
|
+
...projectDesc ? [`**Description:** ${projectDesc}`] : [],
|
|
1417
|
+
...readmeSummary ? [`**From README:** ${readmeSummary}`] : [],
|
|
1418
|
+
"",
|
|
1419
|
+
`## Architecture`,
|
|
1420
|
+
`TODO \u2014 fill in the high-level architecture (inferred structure below, verify manually):`,
|
|
1421
|
+
"",
|
|
1422
|
+
...moduleLines.length ? moduleLines : ["TODO \u2014 no clear structure detected."],
|
|
1423
|
+
"",
|
|
1424
|
+
`## Key modules`,
|
|
1425
|
+
`TODO \u2014 describe the purpose of the main modules. The directory scan found:`,
|
|
1426
|
+
...dirs.filter((d) => !d.includes("/")).slice(0, 8).map((d) => `- \`${d}/\``),
|
|
1427
|
+
"",
|
|
1428
|
+
`## Conventions`,
|
|
1429
|
+
`TODO \u2014 fill in coding conventions (naming, patterns, file layout).`,
|
|
1430
|
+
"",
|
|
1431
|
+
...scriptLines.length ? [
|
|
1432
|
+
`**Available scripts:**`,
|
|
1433
|
+
...scriptLines,
|
|
1434
|
+
""
|
|
1435
|
+
] : [],
|
|
1436
|
+
...keyDeps.length ? [
|
|
1437
|
+
`**Key dependencies in use:** ${keyDeps.map((d) => `\`${d}\``).join(", ")}`,
|
|
1438
|
+
""
|
|
1439
|
+
] : [],
|
|
1440
|
+
...notableDeps.length ? [
|
|
1441
|
+
`**Other notable packages:** ${notableDeps.join(", ")}`,
|
|
1442
|
+
""
|
|
1443
|
+
] : [],
|
|
1444
|
+
`## Glossary`,
|
|
1445
|
+
`TODO \u2014 domain terms and what they mean here.`,
|
|
1446
|
+
"",
|
|
1447
|
+
`## Gotchas`,
|
|
1448
|
+
`TODO \u2014 known traps, surprising behavior, things newcomers stub their toes on.`,
|
|
1449
|
+
`(Run \`haive memory import-changelog\` or \`haive memory import README.md\` to seed these automatically.)`,
|
|
1450
|
+
""
|
|
1451
|
+
];
|
|
1452
|
+
return lines.join("\n");
|
|
1253
1453
|
}
|
|
1254
1454
|
|
|
1255
1455
|
// src/commands/init-stack-packs.ts
|
|
1256
|
-
import { mkdir as
|
|
1257
|
-
import { existsSync as
|
|
1258
|
-
import
|
|
1456
|
+
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
1457
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1458
|
+
import path7 from "path";
|
|
1259
1459
|
import {
|
|
1260
1460
|
buildFrontmatter,
|
|
1261
1461
|
memoryFilePath,
|
|
@@ -1867,7 +2067,7 @@ function autoDetectStacks(deps) {
|
|
|
1867
2067
|
async function seedStackPack(haivePaths, stack) {
|
|
1868
2068
|
const memories = PACKS[stack];
|
|
1869
2069
|
if (!memories) return 0;
|
|
1870
|
-
await
|
|
2070
|
+
await mkdir4(haivePaths.teamDir, { recursive: true });
|
|
1871
2071
|
let count = 0;
|
|
1872
2072
|
for (const mem of memories) {
|
|
1873
2073
|
const fm = buildFrontmatter({
|
|
@@ -1878,10 +2078,10 @@ async function seedStackPack(haivePaths, stack) {
|
|
|
1878
2078
|
tags: mem.tags
|
|
1879
2079
|
});
|
|
1880
2080
|
const filePath = memoryFilePath(haivePaths, "team", fm.id);
|
|
1881
|
-
if (
|
|
2081
|
+
if (existsSync6(filePath)) continue;
|
|
1882
2082
|
const content = serializeMemory({ frontmatter: fm, body: mem.body });
|
|
1883
|
-
await
|
|
1884
|
-
await
|
|
2083
|
+
await mkdir4(path7.dirname(filePath), { recursive: true });
|
|
2084
|
+
await writeFile3(filePath, content, "utf8");
|
|
1885
2085
|
count++;
|
|
1886
2086
|
}
|
|
1887
2087
|
return count;
|
|
@@ -2095,38 +2295,42 @@ function registerInit(program2) {
|
|
|
2095
2295
|
).option(
|
|
2096
2296
|
"--no-mcp-setup",
|
|
2097
2297
|
"skip auto-configuring haive MCP (haive mcp --stdio) in Cursor / VS Code / Claude Code"
|
|
2298
|
+
).option(
|
|
2299
|
+
"-y, --yes",
|
|
2300
|
+
"approve user-level AI client configuration prompts during agent setup",
|
|
2301
|
+
false
|
|
2098
2302
|
).action(async (opts) => {
|
|
2099
|
-
const root =
|
|
2100
|
-
const paths =
|
|
2303
|
+
const root = path8.resolve(opts.dir);
|
|
2304
|
+
const paths = resolveHaivePaths5(root);
|
|
2101
2305
|
const autopilot = opts.manual !== true;
|
|
2102
2306
|
const wantBootstrap = opts.bootstrap === void 0 ? autopilot : opts.bootstrap;
|
|
2103
2307
|
const wantStack = opts.stack === void 0 ? autopilot ? "auto" : void 0 : opts.stack === "none" ? void 0 : opts.stack;
|
|
2104
|
-
if (
|
|
2308
|
+
if (existsSync7(paths.haiveDir)) {
|
|
2105
2309
|
ui.warn(`.ai/ already exists at ${paths.haiveDir} \u2014 leaving existing files in place.`);
|
|
2106
2310
|
}
|
|
2107
|
-
await
|
|
2108
|
-
await
|
|
2109
|
-
await
|
|
2110
|
-
await
|
|
2311
|
+
await mkdir5(paths.personalDir, { recursive: true });
|
|
2312
|
+
await mkdir5(paths.teamDir, { recursive: true });
|
|
2313
|
+
await mkdir5(paths.moduleDir, { recursive: true });
|
|
2314
|
+
await mkdir5(paths.modulesContextDir, { recursive: true });
|
|
2111
2315
|
await ensureAiRuntimeLayout(paths.runtimeDir);
|
|
2112
|
-
if (!
|
|
2316
|
+
if (!existsSync7(paths.projectContext)) {
|
|
2113
2317
|
if (wantBootstrap) {
|
|
2114
2318
|
ui.info("Bootstrapping project context from local files\u2026");
|
|
2115
2319
|
try {
|
|
2116
2320
|
const context = await generateBootstrapContext(root);
|
|
2117
|
-
await
|
|
2321
|
+
await writeFile4(paths.projectContext, context, "utf8");
|
|
2118
2322
|
ui.success("Created .ai/project-context.md (auto-bootstrapped from local files)");
|
|
2119
2323
|
} catch (err) {
|
|
2120
2324
|
ui.warn(`Bootstrap failed (${String(err)}) \u2014 writing default template instead`);
|
|
2121
|
-
await
|
|
2325
|
+
await writeFile4(paths.projectContext, PROJECT_CONTEXT_TEMPLATE, "utf8");
|
|
2122
2326
|
}
|
|
2123
2327
|
} else {
|
|
2124
|
-
await
|
|
2125
|
-
ui.success(`Created ${
|
|
2328
|
+
await writeFile4(paths.projectContext, PROJECT_CONTEXT_TEMPLATE, "utf8");
|
|
2329
|
+
ui.success(`Created ${path8.relative(root, paths.projectContext)}`);
|
|
2126
2330
|
}
|
|
2127
2331
|
}
|
|
2128
|
-
const configExists =
|
|
2129
|
-
|
|
2332
|
+
const configExists = existsSync7(
|
|
2333
|
+
path8.join(paths.haiveDir, "haive.config.json")
|
|
2130
2334
|
);
|
|
2131
2335
|
if (!configExists) {
|
|
2132
2336
|
await saveConfig(paths, autopilot ? AUTOPILOT_DEFAULTS : { autopilot: false });
|
|
@@ -2137,7 +2341,7 @@ function registerInit(program2) {
|
|
|
2137
2341
|
if (opts.bridges) {
|
|
2138
2342
|
await writeBridge(root, "CLAUDE.md");
|
|
2139
2343
|
await writeBridge(root, ".cursorrules");
|
|
2140
|
-
await writeBridge(root,
|
|
2344
|
+
await writeBridge(root, path8.join(".github", "copilot-instructions.md"));
|
|
2141
2345
|
await writeCursorHaiveRule(root);
|
|
2142
2346
|
}
|
|
2143
2347
|
const stacksToSeed = await resolveStacksToSeed(root, wantStack);
|
|
@@ -2160,18 +2364,18 @@ function registerInit(program2) {
|
|
|
2160
2364
|
}
|
|
2161
2365
|
const wantCi = opts.withCi || autopilot;
|
|
2162
2366
|
if (wantCi) {
|
|
2163
|
-
const ciPath =
|
|
2164
|
-
if (
|
|
2367
|
+
const ciPath = path8.join(root, ".github", "workflows", "haive-sync.yml");
|
|
2368
|
+
if (existsSync7(ciPath)) {
|
|
2165
2369
|
ui.info("CI workflow already exists \u2014 skipped");
|
|
2166
2370
|
} else {
|
|
2167
|
-
await
|
|
2168
|
-
await
|
|
2169
|
-
ui.success(`Created ${
|
|
2371
|
+
await mkdir5(path8.dirname(ciPath), { recursive: true });
|
|
2372
|
+
await writeFile4(ciPath, CI_WORKFLOW, "utf8");
|
|
2373
|
+
ui.success(`Created ${path8.relative(root, ciPath)}`);
|
|
2170
2374
|
}
|
|
2171
2375
|
}
|
|
2172
2376
|
if (autopilot) {
|
|
2173
2377
|
const haiveBin = process.argv[1];
|
|
2174
|
-
const enforcementResult =
|
|
2378
|
+
const enforcementResult = spawnSync2(
|
|
2175
2379
|
process.execPath,
|
|
2176
2380
|
[haiveBin, "enforce", "install", "--dir", root],
|
|
2177
2381
|
{ encoding: "utf8" }
|
|
@@ -2191,36 +2395,28 @@ function registerInit(program2) {
|
|
|
2191
2395
|
}
|
|
2192
2396
|
}
|
|
2193
2397
|
if (opts.mcpSetup !== false) {
|
|
2194
|
-
const
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
}
|
|
2203
|
-
if (configured.length === 0 && alreadyOk.length === 0) {
|
|
2204
|
-
ui.warn(
|
|
2205
|
-
'No supported AI client detected (Cursor, VS Code, Claude Code, Windsurf).\n Configure manually: command "haive", args ["mcp", "--stdio"].\n See: https://github.com/Doucs91/hAIve#mcp-setup'
|
|
2206
|
-
);
|
|
2398
|
+
const agentSetup = await setupAgentMode(root, {
|
|
2399
|
+
yes: opts.yes === true,
|
|
2400
|
+
global: true,
|
|
2401
|
+
interactive: process.stdin.isTTY
|
|
2402
|
+
});
|
|
2403
|
+
for (const r of agentSetup.project_results) {
|
|
2404
|
+
if (r.status === "configured" && r.path) ui.success(`haive MCP project config written (${path8.relative(root, r.path)})`);
|
|
2405
|
+
else if (r.status === "error") ui.warn(`${r.client}: ${r.error}`);
|
|
2207
2406
|
}
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
if (r.status === "
|
|
2211
|
-
ui.success(`haive MCP project config written (${path7.relative(root, r.path)})`);
|
|
2212
|
-
}
|
|
2407
|
+
for (const r of agentSetup.global_results) {
|
|
2408
|
+
if (r.status === "configured") ui.success(`haive MCP configured in ${r.client}${r.path ? ` (${r.path})` : ""}`);
|
|
2409
|
+
else if (r.status === "already_configured") ui.info(`haive MCP already configured in ${r.client} \u2014 skipped`);
|
|
2213
2410
|
}
|
|
2411
|
+
if (agentSetup.global_skipped_reason) ui.warn(agentSetup.global_skipped_reason);
|
|
2412
|
+
ui.info(`Recommended agent mode: ${agentSetup.detection.recommended_mode}`);
|
|
2413
|
+
ui.info(agentSetup.detection.recommended_command);
|
|
2214
2414
|
await ensureGitignoreEntries(root, [
|
|
2215
2415
|
".cursor/mcp.json",
|
|
2216
2416
|
".vscode/mcp.json",
|
|
2217
2417
|
".mcp.json"
|
|
2218
2418
|
]);
|
|
2219
|
-
|
|
2220
|
-
ui.info(
|
|
2221
|
-
ui.dim(" \u2192 Restart your AI client for MCP changes to take effect.")
|
|
2222
|
-
);
|
|
2223
|
-
}
|
|
2419
|
+
ui.info(ui.dim(" \u2192 Restart your AI client for MCP changes to take effect."));
|
|
2224
2420
|
}
|
|
2225
2421
|
ui.success(`hAIve initialized at ${root}${autopilot ? " (autopilot mode)" : ""}`);
|
|
2226
2422
|
console.log();
|
|
@@ -2269,8 +2465,8 @@ function registerInit(program2) {
|
|
|
2269
2465
|
async function resolveStacksToSeed(root, stackOpt) {
|
|
2270
2466
|
if (!stackOpt) return [];
|
|
2271
2467
|
if (stackOpt === "auto") {
|
|
2272
|
-
const pkgPath =
|
|
2273
|
-
if (!
|
|
2468
|
+
const pkgPath = path8.join(root, "package.json");
|
|
2469
|
+
if (!existsSync7(pkgPath)) return [];
|
|
2274
2470
|
try {
|
|
2275
2471
|
const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
|
|
2276
2472
|
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
@@ -2283,23 +2479,23 @@ async function resolveStacksToSeed(root, stackOpt) {
|
|
|
2283
2479
|
}
|
|
2284
2480
|
async function writeCursorHaiveRule(root) {
|
|
2285
2481
|
const relPath = ".cursor/rules/haive-mcp-required.mdc";
|
|
2286
|
-
const target =
|
|
2287
|
-
if (
|
|
2482
|
+
const target = path8.join(root, relPath);
|
|
2483
|
+
if (existsSync7(target)) {
|
|
2288
2484
|
ui.info(`Cursor rule ${relPath} already exists \u2014 skipped`);
|
|
2289
2485
|
return;
|
|
2290
2486
|
}
|
|
2291
|
-
await
|
|
2292
|
-
await
|
|
2487
|
+
await mkdir5(path8.dirname(target), { recursive: true });
|
|
2488
|
+
await writeFile4(target, CURSOR_HAIVE_RULE_MDC, "utf8");
|
|
2293
2489
|
ui.success(`Created Cursor rule ${relPath}`);
|
|
2294
2490
|
}
|
|
2295
2491
|
async function writeBridge(root, relPath) {
|
|
2296
|
-
const target =
|
|
2297
|
-
if (
|
|
2492
|
+
const target = path8.join(root, relPath);
|
|
2493
|
+
if (existsSync7(target)) {
|
|
2298
2494
|
ui.info(`Bridge ${relPath} already exists \u2014 skipped`);
|
|
2299
2495
|
return;
|
|
2300
2496
|
}
|
|
2301
|
-
await
|
|
2302
|
-
await
|
|
2497
|
+
await mkdir5(path8.dirname(target), { recursive: true });
|
|
2498
|
+
await writeFile4(target, BRIDGE_BODY, "utf8");
|
|
2303
2499
|
ui.success(`Created bridge ${relPath}`);
|
|
2304
2500
|
}
|
|
2305
2501
|
var RUNTIME_README_BODY = `# .ai/.runtime \u2014 disposable local layer
|
|
@@ -2315,43 +2511,43 @@ var RUNTIME_GITIGNORE_BODY = `*
|
|
|
2315
2511
|
!README.md
|
|
2316
2512
|
`;
|
|
2317
2513
|
async function ensureAiRuntimeLayout(runtimeDir) {
|
|
2318
|
-
await
|
|
2319
|
-
const gi =
|
|
2320
|
-
if (!
|
|
2321
|
-
await
|
|
2514
|
+
await mkdir5(runtimeDir, { recursive: true });
|
|
2515
|
+
const gi = path8.join(runtimeDir, ".gitignore");
|
|
2516
|
+
if (!existsSync7(gi)) {
|
|
2517
|
+
await writeFile4(gi, RUNTIME_GITIGNORE_BODY, "utf8");
|
|
2322
2518
|
}
|
|
2323
|
-
const readme =
|
|
2324
|
-
if (!
|
|
2325
|
-
await
|
|
2519
|
+
const readme = path8.join(runtimeDir, "README.md");
|
|
2520
|
+
if (!existsSync7(readme)) {
|
|
2521
|
+
await writeFile4(readme, RUNTIME_README_BODY, "utf8");
|
|
2326
2522
|
}
|
|
2327
2523
|
}
|
|
2328
2524
|
async function ensureGitignoreEntries(root, patterns) {
|
|
2329
2525
|
try {
|
|
2330
|
-
const gitignorePath =
|
|
2526
|
+
const gitignorePath = path8.join(root, ".gitignore");
|
|
2331
2527
|
let existing = "";
|
|
2332
|
-
if (
|
|
2528
|
+
if (existsSync7(gitignorePath)) {
|
|
2333
2529
|
existing = await readFile4(gitignorePath, "utf8");
|
|
2334
2530
|
}
|
|
2335
2531
|
const lines = existing.split("\n");
|
|
2336
2532
|
const missing = patterns.filter((p) => !lines.some((l) => l.trim() === p));
|
|
2337
2533
|
if (missing.length === 0) return;
|
|
2338
2534
|
const toAppend = (existing.endsWith("\n") || existing === "" ? "" : "\n") + "# hAIve project-level MCP configs (machine-specific absolute paths)\n" + missing.join("\n") + "\n";
|
|
2339
|
-
await
|
|
2535
|
+
await writeFile4(gitignorePath, existing + toAppend, "utf8");
|
|
2340
2536
|
} catch {
|
|
2341
2537
|
}
|
|
2342
2538
|
}
|
|
2343
2539
|
|
|
2344
2540
|
// src/commands/install-hooks.ts
|
|
2345
|
-
import { mkdir as
|
|
2346
|
-
import { existsSync as
|
|
2347
|
-
import
|
|
2541
|
+
import { mkdir as mkdir7, writeFile as writeFile6, chmod, readFile as readFile6 } from "fs/promises";
|
|
2542
|
+
import { existsSync as existsSync9 } from "fs";
|
|
2543
|
+
import path10 from "path";
|
|
2348
2544
|
import "commander";
|
|
2349
|
-
import { findProjectRoot as
|
|
2545
|
+
import { findProjectRoot as findProjectRoot7 } from "@hiveai/core";
|
|
2350
2546
|
|
|
2351
2547
|
// src/utils/claude-hooks.ts
|
|
2352
|
-
import { existsSync as
|
|
2353
|
-
import { mkdir as
|
|
2354
|
-
import
|
|
2548
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2549
|
+
import { mkdir as mkdir6, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
2550
|
+
import path9 from "path";
|
|
2355
2551
|
var HAIVE_HOOK_TAG = "haive-enforcement";
|
|
2356
2552
|
var POST_TOOL_USE_GROUP = {
|
|
2357
2553
|
matcher: "Edit|Write|Bash",
|
|
@@ -2437,7 +2633,7 @@ function unpatchClaudeSettings(input) {
|
|
|
2437
2633
|
async function installClaudeHooksAtPath(settingsPath) {
|
|
2438
2634
|
let raw = null;
|
|
2439
2635
|
let created = false;
|
|
2440
|
-
if (
|
|
2636
|
+
if (existsSync8(settingsPath)) {
|
|
2441
2637
|
try {
|
|
2442
2638
|
raw = JSON.parse(await readFile5(settingsPath, "utf8"));
|
|
2443
2639
|
} catch {
|
|
@@ -2447,25 +2643,25 @@ async function installClaudeHooksAtPath(settingsPath) {
|
|
|
2447
2643
|
created = true;
|
|
2448
2644
|
}
|
|
2449
2645
|
const patched = patchClaudeSettings(raw);
|
|
2450
|
-
await
|
|
2451
|
-
await
|
|
2646
|
+
await mkdir6(path9.dirname(settingsPath), { recursive: true });
|
|
2647
|
+
await writeFile5(settingsPath, JSON.stringify(patched, null, 2) + "\n", "utf8");
|
|
2452
2648
|
return { settingsPath, created };
|
|
2453
2649
|
}
|
|
2454
2650
|
async function uninstallClaudeHooksAtPath(settingsPath) {
|
|
2455
|
-
if (!
|
|
2651
|
+
if (!existsSync8(settingsPath)) {
|
|
2456
2652
|
return { settingsPath, created: false };
|
|
2457
2653
|
}
|
|
2458
2654
|
const raw = JSON.parse(await readFile5(settingsPath, "utf8"));
|
|
2459
2655
|
const cleaned = unpatchClaudeSettings(raw);
|
|
2460
|
-
await
|
|
2656
|
+
await writeFile5(settingsPath, JSON.stringify(cleaned, null, 2) + "\n", "utf8");
|
|
2461
2657
|
return { settingsPath, created: false };
|
|
2462
2658
|
}
|
|
2463
2659
|
function defaultClaudeSettingsPath(scope, projectRoot) {
|
|
2464
2660
|
if (scope === "user") {
|
|
2465
2661
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
2466
|
-
return
|
|
2662
|
+
return path9.join(home, ".claude", "settings.json");
|
|
2467
2663
|
}
|
|
2468
|
-
return
|
|
2664
|
+
return path9.join(projectRoot, ".claude", "settings.local.json");
|
|
2469
2665
|
}
|
|
2470
2666
|
|
|
2471
2667
|
// src/commands/install-hooks.ts
|
|
@@ -2521,20 +2717,20 @@ fi
|
|
|
2521
2717
|
}
|
|
2522
2718
|
];
|
|
2523
2719
|
async function installGitHooks(opts) {
|
|
2524
|
-
const root =
|
|
2525
|
-
const gitDir =
|
|
2526
|
-
if (!
|
|
2720
|
+
const root = findProjectRoot7(opts.dir);
|
|
2721
|
+
const gitDir = path10.join(root, ".git");
|
|
2722
|
+
if (!existsSync9(gitDir)) {
|
|
2527
2723
|
ui.error(`No .git directory at ${root}.`);
|
|
2528
2724
|
process.exitCode = 1;
|
|
2529
2725
|
return;
|
|
2530
2726
|
}
|
|
2531
|
-
const hooksDir =
|
|
2532
|
-
await
|
|
2727
|
+
const hooksDir = path10.join(gitDir, "hooks");
|
|
2728
|
+
await mkdir7(hooksDir, { recursive: true });
|
|
2533
2729
|
let installed = 0;
|
|
2534
2730
|
let skipped = 0;
|
|
2535
2731
|
for (const { name, body } of HOOKS) {
|
|
2536
|
-
const file =
|
|
2537
|
-
if (
|
|
2732
|
+
const file = path10.join(hooksDir, name);
|
|
2733
|
+
if (existsSync9(file) && !opts.force) {
|
|
2538
2734
|
const existing = await readFile6(file, "utf8");
|
|
2539
2735
|
if (!existing.includes(HOOK_MARKER)) {
|
|
2540
2736
|
ui.warn(`${name} already exists and was not written by hAIve. Re-run with --force to overwrite.`);
|
|
@@ -2542,7 +2738,7 @@ async function installGitHooks(opts) {
|
|
|
2542
2738
|
continue;
|
|
2543
2739
|
}
|
|
2544
2740
|
}
|
|
2545
|
-
await
|
|
2741
|
+
await writeFile6(file, body, "utf8");
|
|
2546
2742
|
await chmod(file, 493);
|
|
2547
2743
|
installed++;
|
|
2548
2744
|
}
|
|
@@ -2552,7 +2748,7 @@ async function installGitHooks(opts) {
|
|
|
2552
2748
|
ui.info("pre-push: haive enforce check blocks pushes that bypass briefing/session recap policy.");
|
|
2553
2749
|
}
|
|
2554
2750
|
async function installClaudeHooks(opts) {
|
|
2555
|
-
const root =
|
|
2751
|
+
const root = findProjectRoot7(opts.dir);
|
|
2556
2752
|
const scope = opts.scope ?? "user";
|
|
2557
2753
|
const settingsPath = opts.settings ?? defaultClaudeSettingsPath(scope, root);
|
|
2558
2754
|
if (opts.uninstall) {
|
|
@@ -2597,11 +2793,11 @@ function registerInstallHooks(program2) {
|
|
|
2597
2793
|
}
|
|
2598
2794
|
|
|
2599
2795
|
// src/commands/observe.ts
|
|
2600
|
-
import { appendFile, mkdir as
|
|
2601
|
-
import { existsSync as
|
|
2602
|
-
import
|
|
2796
|
+
import { appendFile, mkdir as mkdir8 } from "fs/promises";
|
|
2797
|
+
import { existsSync as existsSync10 } from "fs";
|
|
2798
|
+
import path11 from "path";
|
|
2603
2799
|
import "commander";
|
|
2604
|
-
import { findProjectRoot as
|
|
2800
|
+
import { findProjectRoot as findProjectRoot8, resolveHaivePaths as resolveHaivePaths6 } from "@hiveai/core";
|
|
2605
2801
|
var MAX_STDIN_BYTES = 256 * 1024;
|
|
2606
2802
|
var TRUNCATE_FIELD = 800;
|
|
2607
2803
|
function truncate(s, max = TRUNCATE_FIELD) {
|
|
@@ -2671,14 +2867,14 @@ function registerObserve(program2) {
|
|
|
2671
2867
|
}
|
|
2672
2868
|
const root = (() => {
|
|
2673
2869
|
try {
|
|
2674
|
-
return
|
|
2870
|
+
return findProjectRoot8(opts.dir ?? payload.cwd);
|
|
2675
2871
|
} catch {
|
|
2676
2872
|
return null;
|
|
2677
2873
|
}
|
|
2678
2874
|
})();
|
|
2679
2875
|
if (!root) return;
|
|
2680
|
-
const paths =
|
|
2681
|
-
if (!
|
|
2876
|
+
const paths = resolveHaivePaths6(root);
|
|
2877
|
+
if (!existsSync10(paths.haiveDir)) return;
|
|
2682
2878
|
const observation = {
|
|
2683
2879
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2684
2880
|
session_id: payload.session_id,
|
|
@@ -2687,10 +2883,10 @@ function registerObserve(program2) {
|
|
|
2687
2883
|
summary: buildSummary(payload),
|
|
2688
2884
|
files: extractFiles(payload)
|
|
2689
2885
|
};
|
|
2690
|
-
const cacheDir =
|
|
2691
|
-
await
|
|
2886
|
+
const cacheDir = path11.join(paths.haiveDir, ".cache");
|
|
2887
|
+
await mkdir8(cacheDir, { recursive: true });
|
|
2692
2888
|
await appendFile(
|
|
2693
|
-
|
|
2889
|
+
path11.join(cacheDir, "observations.jsonl"),
|
|
2694
2890
|
JSON.stringify(observation) + "\n",
|
|
2695
2891
|
"utf8"
|
|
2696
2892
|
);
|
|
@@ -2701,15 +2897,15 @@ function registerObserve(program2) {
|
|
|
2701
2897
|
|
|
2702
2898
|
// src/commands/mcp.ts
|
|
2703
2899
|
import "commander";
|
|
2704
|
-
import { findProjectRoot as
|
|
2900
|
+
import { findProjectRoot as findProjectRoot10 } from "@hiveai/core";
|
|
2705
2901
|
|
|
2706
2902
|
// ../mcp/dist/server.js
|
|
2707
2903
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2708
2904
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2709
|
-
import { findProjectRoot as
|
|
2710
|
-
import { mkdir as
|
|
2711
|
-
import { existsSync as
|
|
2712
|
-
import
|
|
2905
|
+
import { findProjectRoot as findProjectRoot9, resolveHaivePaths as resolveHaivePaths7 } from "@hiveai/core";
|
|
2906
|
+
import { mkdir as mkdir9, writeFile as writeFile7 } from "fs/promises";
|
|
2907
|
+
import { existsSync as existsSync11 } from "fs";
|
|
2908
|
+
import path12 from "path";
|
|
2713
2909
|
import { z } from "zod";
|
|
2714
2910
|
import { readFile as readFile7, readdir as readdir2 } from "fs/promises";
|
|
2715
2911
|
import { existsSync as existsSync22 } from "fs";
|
|
@@ -2793,7 +2989,7 @@ import {
|
|
|
2793
2989
|
} from "@hiveai/core";
|
|
2794
2990
|
import { z as z10 } from "zod";
|
|
2795
2991
|
import { writeFile as writeFile52 } from "fs/promises";
|
|
2796
|
-
import { existsSync as
|
|
2992
|
+
import { existsSync as existsSync112 } from "fs";
|
|
2797
2993
|
import { loadMemoriesFromDir as loadMemoriesFromDir9, serializeMemory as serializeMemory4 } from "@hiveai/core";
|
|
2798
2994
|
import { z as z11 } from "zod";
|
|
2799
2995
|
import { existsSync as existsSync12 } from "fs";
|
|
@@ -2810,7 +3006,7 @@ import {
|
|
|
2810
3006
|
serializeMemory as serializeMemory5
|
|
2811
3007
|
} from "@hiveai/core";
|
|
2812
3008
|
import { z as z13 } from "zod";
|
|
2813
|
-
import { mkdir as mkdir32, writeFile as
|
|
3009
|
+
import { mkdir as mkdir32, writeFile as writeFile72 } from "fs/promises";
|
|
2814
3010
|
import { existsSync as existsSync14 } from "fs";
|
|
2815
3011
|
import path52 from "path";
|
|
2816
3012
|
import {
|
|
@@ -2971,8 +3167,8 @@ import { loadConfigSync } from "@hiveai/core";
|
|
|
2971
3167
|
function createContext(options = {}) {
|
|
2972
3168
|
const env = options.env ?? process.env;
|
|
2973
3169
|
const cwd = options.cwd ?? process.cwd();
|
|
2974
|
-
const root = options.root ?? env.HAIVE_PROJECT_ROOT ??
|
|
2975
|
-
return { paths:
|
|
3170
|
+
const root = options.root ?? env.HAIVE_PROJECT_ROOT ?? findProjectRoot9(cwd);
|
|
3171
|
+
return { paths: resolveHaivePaths7(root) };
|
|
2976
3172
|
}
|
|
2977
3173
|
var BootstrapProjectSaveInputSchema = {
|
|
2978
3174
|
content: z.string().min(1).describe("Full Markdown content for the project (or module) context file"),
|
|
@@ -2982,15 +3178,15 @@ var BootstrapProjectSaveInputSchema = {
|
|
|
2982
3178
|
overwrite: z.boolean().default(false).describe("Overwrite an existing file instead of failing")
|
|
2983
3179
|
};
|
|
2984
3180
|
async function bootstrapProjectSave(input, ctx) {
|
|
2985
|
-
const target = input.module ?
|
|
2986
|
-
const exists =
|
|
3181
|
+
const target = input.module ? path12.join(ctx.paths.modulesContextDir, input.module, "context.md") : ctx.paths.projectContext;
|
|
3182
|
+
const exists = existsSync11(target);
|
|
2987
3183
|
if (exists && !input.overwrite) {
|
|
2988
3184
|
throw new Error(
|
|
2989
3185
|
`${target} already exists. Pass overwrite=true to replace it.`
|
|
2990
3186
|
);
|
|
2991
3187
|
}
|
|
2992
|
-
await
|
|
2993
|
-
await
|
|
3188
|
+
await mkdir9(path12.dirname(target), { recursive: true });
|
|
3189
|
+
await writeFile7(target, input.content, "utf8");
|
|
2994
3190
|
return {
|
|
2995
3191
|
file_path: target,
|
|
2996
3192
|
action: exists ? "overwritten" : "created"
|
|
@@ -3744,7 +3940,7 @@ var MemUpdateInputSchema = {
|
|
|
3744
3940
|
author: z11.string().optional().describe("New author handle or email")
|
|
3745
3941
|
};
|
|
3746
3942
|
async function memUpdate(input, ctx) {
|
|
3747
|
-
if (!
|
|
3943
|
+
if (!existsSync112(ctx.paths.memoriesDir)) {
|
|
3748
3944
|
throw new Error(`No .ai/memories at ${ctx.paths.root}.`);
|
|
3749
3945
|
}
|
|
3750
3946
|
const memories = await loadMemoriesFromDir9(ctx.paths.memoriesDir);
|
|
@@ -3880,7 +4076,7 @@ async function memTried(input, ctx) {
|
|
|
3880
4076
|
if (existsSync14(file)) {
|
|
3881
4077
|
throw new Error(`Memory already exists at ${file}`);
|
|
3882
4078
|
}
|
|
3883
|
-
await
|
|
4079
|
+
await writeFile72(file, serializeMemory6({ frontmatter, body }), "utf8");
|
|
3884
4080
|
return { id: frontmatter.id, scope: frontmatter.scope, file_path: file };
|
|
3885
4081
|
}
|
|
3886
4082
|
var MemObserveInputSchema = {
|
|
@@ -6072,7 +6268,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
6072
6268
|
};
|
|
6073
6269
|
}
|
|
6074
6270
|
var SERVER_NAME = "haive";
|
|
6075
|
-
var SERVER_VERSION = "0.9.
|
|
6271
|
+
var SERVER_VERSION = "0.9.12";
|
|
6076
6272
|
function jsonResult(data) {
|
|
6077
6273
|
return {
|
|
6078
6274
|
content: [
|
|
@@ -6086,14 +6282,11 @@ function jsonResult(data) {
|
|
|
6086
6282
|
var ENFORCEMENT_PROFILE_TOOLS = /* @__PURE__ */ new Set([
|
|
6087
6283
|
"get_briefing",
|
|
6088
6284
|
"mem_save",
|
|
6089
|
-
"mem_tried",
|
|
6090
6285
|
"mem_search",
|
|
6091
|
-
"mem_get",
|
|
6092
|
-
"mem_update",
|
|
6093
6286
|
"mem_verify",
|
|
6094
6287
|
"mem_relevant_to",
|
|
6095
|
-
"
|
|
6096
|
-
"
|
|
6288
|
+
"pre_commit_check",
|
|
6289
|
+
"mem_session_end"
|
|
6097
6290
|
]);
|
|
6098
6291
|
var BRIEFING_TOOLS = /* @__PURE__ */ new Set(["get_briefing", "mem_relevant_to"]);
|
|
6099
6292
|
var MUTATING_TOOLS = /* @__PURE__ */ new Set([
|
|
@@ -6984,7 +7177,7 @@ function registerMcp(program2) {
|
|
|
6984
7177
|
).option("-d, --dir <dir>", "project root (walks up from here for .ai/ / .git/)").option("-r, --root <dir>", "same as --dir (parity with legacy haive-mcp --root)").option("--stdio", "optional marker for client configs \u2014 transport is always stdio", false).action(async (opts) => {
|
|
6985
7178
|
void opts.stdio;
|
|
6986
7179
|
const raw = opts.root ?? opts.dir;
|
|
6987
|
-
const root = raw ?
|
|
7180
|
+
const root = raw ? findProjectRoot10(raw) : findProjectRoot10();
|
|
6988
7181
|
try {
|
|
6989
7182
|
await runHaiveMcpStdio({ root });
|
|
6990
7183
|
} catch (err) {
|
|
@@ -6995,15 +7188,15 @@ function registerMcp(program2) {
|
|
|
6995
7188
|
}
|
|
6996
7189
|
|
|
6997
7190
|
// src/commands/sync.ts
|
|
6998
|
-
import { spawnSync as
|
|
6999
|
-
import { readFile as readFile8, writeFile as writeFile13, mkdir as
|
|
7191
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
7192
|
+
import { readFile as readFile8, writeFile as writeFile13, mkdir as mkdir10 } from "fs/promises";
|
|
7000
7193
|
import { existsSync as existsSync29 } from "fs";
|
|
7001
|
-
import
|
|
7194
|
+
import path13 from "path";
|
|
7002
7195
|
import "commander";
|
|
7003
7196
|
import {
|
|
7004
7197
|
DEFAULT_AUTO_PROMOTE_RULE as DEFAULT_AUTO_PROMOTE_RULE2,
|
|
7005
7198
|
buildFrontmatter as buildFrontmatter6,
|
|
7006
|
-
findProjectRoot as
|
|
7199
|
+
findProjectRoot as findProjectRoot11,
|
|
7007
7200
|
getUsage as getUsage10,
|
|
7008
7201
|
isAutoPromoteEligible as isAutoPromoteEligible2,
|
|
7009
7202
|
isDecaying as isDecaying2,
|
|
@@ -7012,7 +7205,7 @@ import {
|
|
|
7012
7205
|
loadMemoriesFromDir as loadMemoriesFromDir23,
|
|
7013
7206
|
loadUsageIndex as loadUsageIndex12,
|
|
7014
7207
|
pullCrossRepoSources,
|
|
7015
|
-
resolveHaivePaths as
|
|
7208
|
+
resolveHaivePaths as resolveHaivePaths8,
|
|
7016
7209
|
resolveManifestFiles,
|
|
7017
7210
|
serializeMemory as serializeMemory11,
|
|
7018
7211
|
trackDependencies,
|
|
@@ -7031,8 +7224,8 @@ function registerSync(program2) {
|
|
|
7031
7224
|
"--inject-bridge",
|
|
7032
7225
|
"inject top validated memories into CLAUDE.md (or --bridge-file) between <!-- haive:memories-start/end --> markers"
|
|
7033
7226
|
).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) => {
|
|
7034
|
-
const root =
|
|
7035
|
-
const paths =
|
|
7227
|
+
const root = findProjectRoot11(opts.dir);
|
|
7228
|
+
const paths = resolveHaivePaths8(root);
|
|
7036
7229
|
if (!existsSync29(paths.memoriesDir)) {
|
|
7037
7230
|
if (!opts.quiet) ui.warn(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
7038
7231
|
process.exitCode = 1;
|
|
@@ -7165,7 +7358,7 @@ function registerSync(program2) {
|
|
|
7165
7358
|
);
|
|
7166
7359
|
}
|
|
7167
7360
|
if (opts.injectBridge) {
|
|
7168
|
-
const bridgeFile = opts.bridgeFile ?
|
|
7361
|
+
const bridgeFile = opts.bridgeFile ? path13.resolve(opts.bridgeFile) : path13.join(root, "CLAUDE.md");
|
|
7169
7362
|
const maxInject = Math.max(1, Number(opts.bridgeMaxMemories ?? 5));
|
|
7170
7363
|
await injectBridge(bridgeFile, paths.memoriesDir, maxInject, root, opts.quiet);
|
|
7171
7364
|
}
|
|
@@ -7271,10 +7464,10 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7271
7464
|
paths: [result.file],
|
|
7272
7465
|
topic: `dep-bump-${slugParts}`
|
|
7273
7466
|
});
|
|
7274
|
-
const teamDir =
|
|
7275
|
-
await
|
|
7467
|
+
const teamDir = path13.join(paths.memoriesDir, "team");
|
|
7468
|
+
await mkdir10(teamDir, { recursive: true });
|
|
7276
7469
|
await writeFile13(
|
|
7277
|
-
|
|
7470
|
+
path13.join(teamDir, `${fm.id}.md`),
|
|
7278
7471
|
serializeMemory11({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
7279
7472
|
"utf8"
|
|
7280
7473
|
);
|
|
@@ -7338,10 +7531,10 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7338
7531
|
paths: [diff.file],
|
|
7339
7532
|
topic: `contract-breaking-${diff.contract}`
|
|
7340
7533
|
});
|
|
7341
|
-
const teamDir =
|
|
7342
|
-
await
|
|
7534
|
+
const teamDir = path13.join(paths.memoriesDir, "team");
|
|
7535
|
+
await mkdir10(teamDir, { recursive: true });
|
|
7343
7536
|
await writeFile13(
|
|
7344
|
-
|
|
7537
|
+
path13.join(teamDir, `${fm.id}.md`),
|
|
7345
7538
|
serializeMemory11({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
7346
7539
|
"utf8"
|
|
7347
7540
|
);
|
|
@@ -7355,7 +7548,7 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7355
7548
|
const existingMap = await loadCodeMap4(paths);
|
|
7356
7549
|
if (existingMap) {
|
|
7357
7550
|
const mapAge = new Date(existingMap.generated_at).getTime();
|
|
7358
|
-
const gitResult =
|
|
7551
|
+
const gitResult = spawnSync3(
|
|
7359
7552
|
"git",
|
|
7360
7553
|
[
|
|
7361
7554
|
"diff",
|
|
@@ -7434,11 +7627,11 @@ ${BRIDGE_END}`;
|
|
|
7434
7627
|
const startIdx = existing.indexOf(BRIDGE_START);
|
|
7435
7628
|
const endIdx = existing.indexOf(BRIDGE_END);
|
|
7436
7629
|
if (startIdx !== -1 && endIdx === -1) {
|
|
7437
|
-
ui.warn(`${
|
|
7630
|
+
ui.warn(`${path13.relative(root, bridgeFile)}: found ${BRIDGE_START} without ${BRIDGE_END}. Fix the file manually before running --inject-bridge.`);
|
|
7438
7631
|
return;
|
|
7439
7632
|
}
|
|
7440
7633
|
if (startIdx === -1 && endIdx !== -1) {
|
|
7441
|
-
ui.warn(`${
|
|
7634
|
+
ui.warn(`${path13.relative(root, bridgeFile)}: found ${BRIDGE_END} without ${BRIDGE_START}. Fix the file manually before running --inject-bridge.`);
|
|
7442
7635
|
return;
|
|
7443
7636
|
}
|
|
7444
7637
|
let updated;
|
|
@@ -7446,19 +7639,19 @@ ${BRIDGE_END}`;
|
|
|
7446
7639
|
updated = existing.slice(0, startIdx) + injected + existing.slice(endIdx + BRIDGE_END.length);
|
|
7447
7640
|
} else {
|
|
7448
7641
|
if (!fileExists && !quiet) {
|
|
7449
|
-
ui.info(`Creating ${
|
|
7642
|
+
ui.info(`Creating ${path13.relative(root, bridgeFile)} with haive memory block.`);
|
|
7450
7643
|
}
|
|
7451
7644
|
updated = existing + (existing.endsWith("\n") ? "" : "\n") + "\n" + injected + "\n";
|
|
7452
7645
|
}
|
|
7453
7646
|
await writeFile13(bridgeFile, updated, "utf8");
|
|
7454
7647
|
if (!quiet) {
|
|
7455
7648
|
console.log(
|
|
7456
|
-
ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${
|
|
7649
|
+
ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${path13.relative(root, bridgeFile)}`)
|
|
7457
7650
|
);
|
|
7458
7651
|
}
|
|
7459
7652
|
}
|
|
7460
7653
|
function collectSinceChanges(root, ref) {
|
|
7461
|
-
const result =
|
|
7654
|
+
const result = spawnSync3(
|
|
7462
7655
|
"git",
|
|
7463
7656
|
["-C", root, "diff", "--name-status", "--diff-filter=AMD", `${ref}...HEAD`, "--", ".ai/memories"],
|
|
7464
7657
|
{ encoding: "utf8" }
|
|
@@ -7478,17 +7671,17 @@ function collectSinceChanges(root, ref) {
|
|
|
7478
7671
|
|
|
7479
7672
|
// src/commands/memory-add.ts
|
|
7480
7673
|
import { createHash as createHash2 } from "crypto";
|
|
7481
|
-
import { mkdir as
|
|
7674
|
+
import { mkdir as mkdir11, readFile as readFile9, writeFile as writeFile14 } from "fs/promises";
|
|
7482
7675
|
import { existsSync as existsSync30 } from "fs";
|
|
7483
|
-
import
|
|
7676
|
+
import path14 from "path";
|
|
7484
7677
|
import "commander";
|
|
7485
7678
|
import {
|
|
7486
7679
|
buildFrontmatter as buildFrontmatter7,
|
|
7487
|
-
findProjectRoot as
|
|
7680
|
+
findProjectRoot as findProjectRoot12,
|
|
7488
7681
|
inferModulesFromPaths as inferModulesFromPaths3,
|
|
7489
7682
|
loadMemoriesFromDir as loadMemoriesFromDir24,
|
|
7490
7683
|
memoryFilePath as memoryFilePath6,
|
|
7491
|
-
resolveHaivePaths as
|
|
7684
|
+
resolveHaivePaths as resolveHaivePaths9,
|
|
7492
7685
|
serializeMemory as serializeMemory12
|
|
7493
7686
|
} from "@hiveai/core";
|
|
7494
7687
|
function registerMemoryAdd(memory2) {
|
|
@@ -7516,8 +7709,8 @@ function registerMemoryAdd(memory2) {
|
|
|
7516
7709
|
--scope team --body "Never modify existing migrations. Create V{n+1}__desc.sql."
|
|
7517
7710
|
`
|
|
7518
7711
|
).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: personal, or team in autopilot)", "personal").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) => {
|
|
7519
|
-
const root =
|
|
7520
|
-
const paths =
|
|
7712
|
+
const root = findProjectRoot12(opts.dir);
|
|
7713
|
+
const paths = resolveHaivePaths9(root);
|
|
7521
7714
|
if (!existsSync30(paths.haiveDir)) {
|
|
7522
7715
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
7523
7716
|
process.exitCode = 1;
|
|
@@ -7529,7 +7722,7 @@ function registerMemoryAdd(memory2) {
|
|
|
7529
7722
|
const inferredTags = autoTagsEnabled ? inferModulesFromPaths3(anchorPaths) : [];
|
|
7530
7723
|
const mergedTags = Array.from(/* @__PURE__ */ new Set([...userTags, ...inferredTags]));
|
|
7531
7724
|
if (anchorPaths.length > 0) {
|
|
7532
|
-
const missing = anchorPaths.filter((p) => !existsSync30(
|
|
7725
|
+
const missing = anchorPaths.filter((p) => !existsSync30(path14.resolve(root, p)));
|
|
7533
7726
|
if (missing.length > 0) {
|
|
7534
7727
|
ui.warn(`Anchor path${missing.length > 1 ? "s" : ""} not found in project:`);
|
|
7535
7728
|
for (const p of missing) ui.warn(` \u2717 ${p}`);
|
|
@@ -7594,7 +7787,7 @@ TODO \u2014 write the memory body.
|
|
|
7594
7787
|
}
|
|
7595
7788
|
};
|
|
7596
7789
|
await writeFile14(topicMatch.filePath, serializeMemory12({ frontmatter: newFrontmatter, body }), "utf8");
|
|
7597
|
-
ui.success(`Updated (topic upsert) ${
|
|
7790
|
+
ui.success(`Updated (topic upsert) ${path14.relative(root, topicMatch.filePath)}`);
|
|
7598
7791
|
ui.info(`id=${fm.id} revision=${revisionCount}`);
|
|
7599
7792
|
return;
|
|
7600
7793
|
}
|
|
@@ -7613,7 +7806,7 @@ TODO \u2014 write the memory body.
|
|
|
7613
7806
|
topic: opts.topic
|
|
7614
7807
|
});
|
|
7615
7808
|
const file = memoryFilePath6(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
7616
|
-
await
|
|
7809
|
+
await mkdir11(path14.dirname(file), { recursive: true });
|
|
7617
7810
|
if (existsSync30(file)) {
|
|
7618
7811
|
ui.error(`Memory already exists at ${file}`);
|
|
7619
7812
|
process.exitCode = 1;
|
|
@@ -7632,7 +7825,7 @@ TODO \u2014 write the memory body.
|
|
|
7632
7825
|
}
|
|
7633
7826
|
}
|
|
7634
7827
|
await writeFile14(file, serializeMemory12({ frontmatter, body }), "utf8");
|
|
7635
|
-
ui.success(`Created ${
|
|
7828
|
+
ui.success(`Created ${path14.relative(root, file)}`);
|
|
7636
7829
|
ui.info(`id=${frontmatter.id} scope=${frontmatter.scope} status=${frontmatter.status}`);
|
|
7637
7830
|
if (inferredTags.length > 0) {
|
|
7638
7831
|
ui.info(`auto-tagged: ${inferredTags.join(", ")} (use --no-auto-tag to disable)`);
|
|
@@ -7663,9 +7856,9 @@ function parseCsv2(value) {
|
|
|
7663
7856
|
|
|
7664
7857
|
// src/commands/memory-list.ts
|
|
7665
7858
|
import { existsSync as existsSync31 } from "fs";
|
|
7666
|
-
import
|
|
7859
|
+
import path15 from "path";
|
|
7667
7860
|
import "commander";
|
|
7668
|
-
import { findProjectRoot as
|
|
7861
|
+
import { findProjectRoot as findProjectRoot13, resolveHaivePaths as resolveHaivePaths10 } from "@hiveai/core";
|
|
7669
7862
|
|
|
7670
7863
|
// src/utils/fs.ts
|
|
7671
7864
|
import {
|
|
@@ -7677,8 +7870,8 @@ import {
|
|
|
7677
7870
|
// src/commands/memory-list.ts
|
|
7678
7871
|
function registerMemoryList(memory2) {
|
|
7679
7872
|
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) => {
|
|
7680
|
-
const root =
|
|
7681
|
-
const paths =
|
|
7873
|
+
const root = findProjectRoot13(opts.dir);
|
|
7874
|
+
const paths = resolveHaivePaths10(root);
|
|
7682
7875
|
if (!existsSync31(paths.memoriesDir)) {
|
|
7683
7876
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
7684
7877
|
process.exitCode = 1;
|
|
@@ -7711,7 +7904,7 @@ function registerMemoryList(memory2) {
|
|
|
7711
7904
|
console.log(
|
|
7712
7905
|
`${ui.bold(fm.id)} ${ui.dim(fm.scope)}/${ui.dim(fm.type)} ${statusBadge}${moduleStr}${tagStr}`
|
|
7713
7906
|
);
|
|
7714
|
-
console.log(` ${ui.dim(
|
|
7907
|
+
console.log(` ${ui.dim(path15.relative(root, filePath))}`);
|
|
7715
7908
|
}
|
|
7716
7909
|
console.log(ui.dim(`
|
|
7717
7910
|
${filtered.length} memor${filtered.length === 1 ? "y" : "ies"}`));
|
|
@@ -7746,20 +7939,20 @@ function matchesFilters(loaded, opts) {
|
|
|
7746
7939
|
}
|
|
7747
7940
|
|
|
7748
7941
|
// src/commands/memory-promote.ts
|
|
7749
|
-
import { mkdir as
|
|
7942
|
+
import { mkdir as mkdir12, unlink as unlink2, writeFile as writeFile15 } from "fs/promises";
|
|
7750
7943
|
import { existsSync as existsSync33 } from "fs";
|
|
7751
|
-
import
|
|
7944
|
+
import path16 from "path";
|
|
7752
7945
|
import "commander";
|
|
7753
7946
|
import {
|
|
7754
|
-
findProjectRoot as
|
|
7947
|
+
findProjectRoot as findProjectRoot14,
|
|
7755
7948
|
memoryFilePath as memoryFilePath7,
|
|
7756
|
-
resolveHaivePaths as
|
|
7949
|
+
resolveHaivePaths as resolveHaivePaths11,
|
|
7757
7950
|
serializeMemory as serializeMemory13
|
|
7758
7951
|
} from "@hiveai/core";
|
|
7759
7952
|
function registerMemoryPromote(memory2) {
|
|
7760
7953
|
memory2.command("promote <id>").description("Promote a personal memory to team scope (status -> proposed)").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
7761
|
-
const root =
|
|
7762
|
-
const paths =
|
|
7954
|
+
const root = findProjectRoot14(opts.dir);
|
|
7955
|
+
const paths = resolveHaivePaths11(root);
|
|
7763
7956
|
if (!existsSync33(paths.memoriesDir)) {
|
|
7764
7957
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
7765
7958
|
process.exitCode = 1;
|
|
@@ -7795,11 +7988,11 @@ function registerMemoryPromote(memory2) {
|
|
|
7795
7988
|
body: found.memory.body
|
|
7796
7989
|
};
|
|
7797
7990
|
const newPath = memoryFilePath7(paths, "team", updated.frontmatter.id);
|
|
7798
|
-
await
|
|
7991
|
+
await mkdir12(path16.dirname(newPath), { recursive: true });
|
|
7799
7992
|
await writeFile15(newPath, serializeMemory13(updated), "utf8");
|
|
7800
7993
|
await unlink2(found.filePath);
|
|
7801
7994
|
ui.success(`Promoted ${id} to team scope (status=proposed)`);
|
|
7802
|
-
ui.info(`Now at ${
|
|
7995
|
+
ui.info(`Now at ${path16.relative(root, newPath)}`);
|
|
7803
7996
|
console.log(ui.dim(`\u2192 next: haive memory approve ${id} (validate for team use)`));
|
|
7804
7997
|
});
|
|
7805
7998
|
}
|
|
@@ -7807,17 +8000,17 @@ function registerMemoryPromote(memory2) {
|
|
|
7807
8000
|
// src/commands/memory-approve.ts
|
|
7808
8001
|
import { existsSync as existsSync34 } from "fs";
|
|
7809
8002
|
import { writeFile as writeFile16 } from "fs/promises";
|
|
7810
|
-
import
|
|
8003
|
+
import path17 from "path";
|
|
7811
8004
|
import "commander";
|
|
7812
8005
|
import {
|
|
7813
|
-
findProjectRoot as
|
|
7814
|
-
resolveHaivePaths as
|
|
8006
|
+
findProjectRoot as findProjectRoot15,
|
|
8007
|
+
resolveHaivePaths as resolveHaivePaths12,
|
|
7815
8008
|
serializeMemory as serializeMemory14
|
|
7816
8009
|
} from "@hiveai/core";
|
|
7817
8010
|
function registerMemoryApprove(memory2) {
|
|
7818
8011
|
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) => {
|
|
7819
|
-
const root =
|
|
7820
|
-
const paths =
|
|
8012
|
+
const root = findProjectRoot15(opts.dir);
|
|
8013
|
+
const paths = resolveHaivePaths12(root);
|
|
7821
8014
|
if (!existsSync34(paths.memoriesDir)) {
|
|
7822
8015
|
ui.error(`No .ai/memories at ${root}.`);
|
|
7823
8016
|
process.exitCode = 1;
|
|
@@ -7871,24 +8064,24 @@ function registerMemoryApprove(memory2) {
|
|
|
7871
8064
|
};
|
|
7872
8065
|
await writeFile16(found.filePath, serializeMemory14(next), "utf8");
|
|
7873
8066
|
ui.success(`Approved ${id} (status=validated)`);
|
|
7874
|
-
ui.info(
|
|
8067
|
+
ui.info(path17.relative(root, found.filePath));
|
|
7875
8068
|
});
|
|
7876
8069
|
}
|
|
7877
8070
|
|
|
7878
8071
|
// src/commands/memory-update.ts
|
|
7879
8072
|
import { writeFile as writeFile17 } from "fs/promises";
|
|
7880
8073
|
import { existsSync as existsSync35 } from "fs";
|
|
7881
|
-
import
|
|
8074
|
+
import path18 from "path";
|
|
7882
8075
|
import "commander";
|
|
7883
8076
|
import {
|
|
7884
|
-
findProjectRoot as
|
|
7885
|
-
resolveHaivePaths as
|
|
8077
|
+
findProjectRoot as findProjectRoot16,
|
|
8078
|
+
resolveHaivePaths as resolveHaivePaths13,
|
|
7886
8079
|
serializeMemory as serializeMemory15
|
|
7887
8080
|
} from "@hiveai/core";
|
|
7888
8081
|
function registerMemoryUpdate(memory2) {
|
|
7889
8082
|
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) => {
|
|
7890
|
-
const root =
|
|
7891
|
-
const paths =
|
|
8083
|
+
const root = findProjectRoot16(opts.dir);
|
|
8084
|
+
const paths = resolveHaivePaths13(root);
|
|
7892
8085
|
if (!existsSync35(paths.memoriesDir)) {
|
|
7893
8086
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
7894
8087
|
process.exitCode = 1;
|
|
@@ -7941,7 +8134,7 @@ function registerMemoryUpdate(memory2) {
|
|
|
7941
8134
|
serializeMemory15({ frontmatter: newFrontmatter, body: newBody }),
|
|
7942
8135
|
"utf8"
|
|
7943
8136
|
);
|
|
7944
|
-
ui.success(`Updated ${
|
|
8137
|
+
ui.success(`Updated ${path18.relative(root, loaded.filePath)}`);
|
|
7945
8138
|
ui.info(`fields: ${updated.join(", ")}`);
|
|
7946
8139
|
});
|
|
7947
8140
|
}
|
|
@@ -7962,15 +8155,15 @@ function parseCsv3(value) {
|
|
|
7962
8155
|
// src/commands/memory-auto-promote.ts
|
|
7963
8156
|
import { writeFile as writeFile18 } from "fs/promises";
|
|
7964
8157
|
import { existsSync as existsSync36 } from "fs";
|
|
7965
|
-
import
|
|
8158
|
+
import path19 from "path";
|
|
7966
8159
|
import "commander";
|
|
7967
8160
|
import {
|
|
7968
8161
|
DEFAULT_AUTO_PROMOTE_RULE as DEFAULT_AUTO_PROMOTE_RULE3,
|
|
7969
|
-
findProjectRoot as
|
|
8162
|
+
findProjectRoot as findProjectRoot17,
|
|
7970
8163
|
getUsage as getUsage11,
|
|
7971
8164
|
isAutoPromoteEligible as isAutoPromoteEligible3,
|
|
7972
8165
|
loadUsageIndex as loadUsageIndex13,
|
|
7973
|
-
resolveHaivePaths as
|
|
8166
|
+
resolveHaivePaths as resolveHaivePaths14,
|
|
7974
8167
|
serializeMemory as serializeMemory16
|
|
7975
8168
|
} from "@hiveai/core";
|
|
7976
8169
|
function registerMemoryAutoPromote(memory2) {
|
|
@@ -7979,8 +8172,8 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
7979
8172
|
"memories with more rejections than this are skipped",
|
|
7980
8173
|
String(DEFAULT_AUTO_PROMOTE_RULE3.maxRejections)
|
|
7981
8174
|
).option("--apply", "actually write status=validated to disk (default: dry-run)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
7982
|
-
const root =
|
|
7983
|
-
const paths =
|
|
8175
|
+
const root = findProjectRoot17(opts.dir);
|
|
8176
|
+
const paths = resolveHaivePaths14(root);
|
|
7984
8177
|
if (!existsSync36(paths.memoriesDir)) {
|
|
7985
8178
|
ui.error(`No .ai/memories at ${root}.`);
|
|
7986
8179
|
process.exitCode = 1;
|
|
@@ -8007,7 +8200,7 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
8007
8200
|
console.log(
|
|
8008
8201
|
`${ui.bold(opts.apply ? "PROMOTE" : "would promote")} ${mem.frontmatter.id} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
8009
8202
|
);
|
|
8010
|
-
console.log(` ${ui.dim(
|
|
8203
|
+
console.log(` ${ui.dim(path19.relative(root, filePath))}`);
|
|
8011
8204
|
if (opts.apply) {
|
|
8012
8205
|
const next = {
|
|
8013
8206
|
frontmatter: { ...mem.frontmatter, status: "validated" },
|
|
@@ -8026,17 +8219,17 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
8026
8219
|
import { spawn as spawn3 } from "child_process";
|
|
8027
8220
|
import { existsSync as existsSync37 } from "fs";
|
|
8028
8221
|
import { readFile as readFile10 } from "fs/promises";
|
|
8029
|
-
import
|
|
8222
|
+
import path20 from "path";
|
|
8030
8223
|
import "commander";
|
|
8031
8224
|
import {
|
|
8032
|
-
findProjectRoot as
|
|
8225
|
+
findProjectRoot as findProjectRoot18,
|
|
8033
8226
|
parseMemory,
|
|
8034
|
-
resolveHaivePaths as
|
|
8227
|
+
resolveHaivePaths as resolveHaivePaths15
|
|
8035
8228
|
} from "@hiveai/core";
|
|
8036
8229
|
function registerMemoryEdit(memory2) {
|
|
8037
8230
|
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) => {
|
|
8038
|
-
const root =
|
|
8039
|
-
const paths =
|
|
8231
|
+
const root = findProjectRoot18(opts.dir);
|
|
8232
|
+
const paths = resolveHaivePaths15(root);
|
|
8040
8233
|
if (!existsSync37(paths.memoriesDir)) {
|
|
8041
8234
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8042
8235
|
process.exitCode = 1;
|
|
@@ -8050,7 +8243,7 @@ function registerMemoryEdit(memory2) {
|
|
|
8050
8243
|
return;
|
|
8051
8244
|
}
|
|
8052
8245
|
const editor = opts.editor ?? process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
8053
|
-
ui.info(`Opening ${
|
|
8246
|
+
ui.info(`Opening ${path20.relative(root, found.filePath)} with ${editor}\u2026`);
|
|
8054
8247
|
const code = await runEditor(editor, found.filePath);
|
|
8055
8248
|
if (code !== 0) {
|
|
8056
8249
|
ui.warn(`Editor exited with status ${code}.`);
|
|
@@ -8078,21 +8271,21 @@ function runEditor(editor, file) {
|
|
|
8078
8271
|
|
|
8079
8272
|
// src/commands/memory-for-files.ts
|
|
8080
8273
|
import { existsSync as existsSync38 } from "fs";
|
|
8081
|
-
import
|
|
8274
|
+
import path21 from "path";
|
|
8082
8275
|
import "commander";
|
|
8083
8276
|
import {
|
|
8084
8277
|
deriveConfidence as deriveConfidence9,
|
|
8085
|
-
findProjectRoot as
|
|
8278
|
+
findProjectRoot as findProjectRoot19,
|
|
8086
8279
|
getUsage as getUsage12,
|
|
8087
8280
|
inferModulesFromPaths as inferModulesFromPaths4,
|
|
8088
8281
|
loadUsageIndex as loadUsageIndex14,
|
|
8089
8282
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths5,
|
|
8090
|
-
resolveHaivePaths as
|
|
8283
|
+
resolveHaivePaths as resolveHaivePaths16
|
|
8091
8284
|
} from "@hiveai/core";
|
|
8092
8285
|
function registerMemoryForFiles(memory2) {
|
|
8093
8286
|
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) => {
|
|
8094
|
-
const root =
|
|
8095
|
-
const paths =
|
|
8287
|
+
const root = findProjectRoot19(opts.dir);
|
|
8288
|
+
const paths = resolveHaivePaths16(root);
|
|
8096
8289
|
if (!existsSync38(paths.memoriesDir)) {
|
|
8097
8290
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8098
8291
|
process.exitCode = 1;
|
|
@@ -8200,24 +8393,24 @@ function printGroup(root, label, loaded, usage) {
|
|
|
8200
8393
|
const u = getUsage12(usage, fm.id);
|
|
8201
8394
|
const conf = deriveConfidence9(fm, u);
|
|
8202
8395
|
console.log(`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(conf)}`);
|
|
8203
|
-
console.log(` ${ui.dim(
|
|
8396
|
+
console.log(` ${ui.dim(path21.relative(root, filePath))}`);
|
|
8204
8397
|
}
|
|
8205
8398
|
}
|
|
8206
8399
|
|
|
8207
8400
|
// src/commands/memory-hot.ts
|
|
8208
8401
|
import { existsSync as existsSync39 } from "fs";
|
|
8209
|
-
import
|
|
8402
|
+
import path23 from "path";
|
|
8210
8403
|
import "commander";
|
|
8211
8404
|
import {
|
|
8212
|
-
findProjectRoot as
|
|
8405
|
+
findProjectRoot as findProjectRoot20,
|
|
8213
8406
|
getUsage as getUsage13,
|
|
8214
8407
|
loadUsageIndex as loadUsageIndex15,
|
|
8215
|
-
resolveHaivePaths as
|
|
8408
|
+
resolveHaivePaths as resolveHaivePaths17
|
|
8216
8409
|
} from "@hiveai/core";
|
|
8217
8410
|
function registerMemoryHot(memory2) {
|
|
8218
8411
|
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) => {
|
|
8219
|
-
const root =
|
|
8220
|
-
const paths =
|
|
8412
|
+
const root = findProjectRoot20(opts.dir);
|
|
8413
|
+
const paths = resolveHaivePaths17(root);
|
|
8221
8414
|
if (!existsSync39(paths.memoriesDir)) {
|
|
8222
8415
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8223
8416
|
process.exitCode = 1;
|
|
@@ -8246,7 +8439,7 @@ function registerMemoryHot(memory2) {
|
|
|
8246
8439
|
console.log(
|
|
8247
8440
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(fm.status)} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
8248
8441
|
);
|
|
8249
|
-
console.log(` ${ui.dim(
|
|
8442
|
+
console.log(` ${ui.dim(path23.relative(root, filePath))}`);
|
|
8250
8443
|
}
|
|
8251
8444
|
ui.info(
|
|
8252
8445
|
`${candidates.length} hot \u2014 promote drafts with \`haive memory promote <id>\`, then \`haive memory auto-promote --apply\`.`
|
|
@@ -8255,15 +8448,15 @@ function registerMemoryHot(memory2) {
|
|
|
8255
8448
|
}
|
|
8256
8449
|
|
|
8257
8450
|
// src/commands/memory-tried.ts
|
|
8258
|
-
import { mkdir as
|
|
8451
|
+
import { mkdir as mkdir13, writeFile as writeFile19 } from "fs/promises";
|
|
8259
8452
|
import { existsSync as existsSync40 } from "fs";
|
|
8260
|
-
import
|
|
8453
|
+
import path24 from "path";
|
|
8261
8454
|
import "commander";
|
|
8262
8455
|
import {
|
|
8263
8456
|
buildFrontmatter as buildFrontmatter8,
|
|
8264
|
-
findProjectRoot as
|
|
8457
|
+
findProjectRoot as findProjectRoot21,
|
|
8265
8458
|
memoryFilePath as memoryFilePath8,
|
|
8266
|
-
resolveHaivePaths as
|
|
8459
|
+
resolveHaivePaths as resolveHaivePaths18,
|
|
8267
8460
|
serializeMemory as serializeMemory17
|
|
8268
8461
|
} from "@hiveai/core";
|
|
8269
8462
|
function registerMemoryTried(memory2) {
|
|
@@ -8283,8 +8476,8 @@ function registerMemoryTried(memory2) {
|
|
|
8283
8476
|
--paths packages/cli/src/index.ts
|
|
8284
8477
|
`
|
|
8285
8478
|
).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) => {
|
|
8286
|
-
const root =
|
|
8287
|
-
const paths =
|
|
8479
|
+
const root = findProjectRoot21(opts.dir);
|
|
8480
|
+
const paths = resolveHaivePaths18(root);
|
|
8288
8481
|
if (!existsSync40(paths.haiveDir)) {
|
|
8289
8482
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
8290
8483
|
process.exitCode = 1;
|
|
@@ -8308,14 +8501,14 @@ function registerMemoryTried(memory2) {
|
|
|
8308
8501
|
}
|
|
8309
8502
|
const body = lines.join("\n") + "\n";
|
|
8310
8503
|
const file = memoryFilePath8(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
8311
|
-
await
|
|
8504
|
+
await mkdir13(path24.dirname(file), { recursive: true });
|
|
8312
8505
|
if (existsSync40(file)) {
|
|
8313
8506
|
ui.error(`Memory already exists at ${file}`);
|
|
8314
8507
|
process.exitCode = 1;
|
|
8315
8508
|
return;
|
|
8316
8509
|
}
|
|
8317
8510
|
await writeFile19(file, serializeMemory17({ frontmatter, body }), "utf8");
|
|
8318
|
-
ui.success(`Recorded: ${
|
|
8511
|
+
ui.success(`Recorded: ${path24.relative(root, file)}`);
|
|
8319
8512
|
ui.info(`id=${frontmatter.id} type=attempt status=validated (auto-approved)`);
|
|
8320
8513
|
});
|
|
8321
8514
|
}
|
|
@@ -8326,18 +8519,18 @@ function parseCsv4(value) {
|
|
|
8326
8519
|
|
|
8327
8520
|
// src/commands/memory-pending.ts
|
|
8328
8521
|
import { existsSync as existsSync41 } from "fs";
|
|
8329
|
-
import
|
|
8522
|
+
import path25 from "path";
|
|
8330
8523
|
import "commander";
|
|
8331
8524
|
import {
|
|
8332
|
-
findProjectRoot as
|
|
8525
|
+
findProjectRoot as findProjectRoot22,
|
|
8333
8526
|
getUsage as getUsage14,
|
|
8334
8527
|
loadUsageIndex as loadUsageIndex16,
|
|
8335
|
-
resolveHaivePaths as
|
|
8528
|
+
resolveHaivePaths as resolveHaivePaths19
|
|
8336
8529
|
} from "@hiveai/core";
|
|
8337
8530
|
function registerMemoryPending(memory2) {
|
|
8338
8531
|
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) => {
|
|
8339
|
-
const root =
|
|
8340
|
-
const paths =
|
|
8532
|
+
const root = findProjectRoot22(opts.dir);
|
|
8533
|
+
const paths = resolveHaivePaths19(root);
|
|
8341
8534
|
if (!existsSync41(paths.memoriesDir)) {
|
|
8342
8535
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8343
8536
|
process.exitCode = 1;
|
|
@@ -8366,7 +8559,7 @@ function registerMemoryPending(memory2) {
|
|
|
8366
8559
|
console.log(
|
|
8367
8560
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.dim(`age=${ageStr} reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
8368
8561
|
);
|
|
8369
|
-
console.log(` ${ui.dim(
|
|
8562
|
+
console.log(` ${ui.dim(path25.relative(root, filePath))}`);
|
|
8370
8563
|
}
|
|
8371
8564
|
ui.info(`${proposed.length} pending`);
|
|
8372
8565
|
});
|
|
@@ -8374,22 +8567,22 @@ function registerMemoryPending(memory2) {
|
|
|
8374
8567
|
|
|
8375
8568
|
// src/commands/memory-query.ts
|
|
8376
8569
|
import { existsSync as existsSync43 } from "fs";
|
|
8377
|
-
import
|
|
8570
|
+
import path26 from "path";
|
|
8378
8571
|
import "commander";
|
|
8379
8572
|
import {
|
|
8380
8573
|
extractSnippet as extractSnippet2,
|
|
8381
|
-
findProjectRoot as
|
|
8574
|
+
findProjectRoot as findProjectRoot23,
|
|
8382
8575
|
literalMatchesAllTokens as literalMatchesAllTokens3,
|
|
8383
8576
|
literalMatchesAnyToken as literalMatchesAnyToken4,
|
|
8384
8577
|
pickSnippetNeedle as pickSnippetNeedle2,
|
|
8385
|
-
resolveHaivePaths as
|
|
8578
|
+
resolveHaivePaths as resolveHaivePaths20,
|
|
8386
8579
|
tokenizeQuery as tokenizeQuery6,
|
|
8387
8580
|
trackReads as trackReads4
|
|
8388
8581
|
} from "@hiveai/core";
|
|
8389
8582
|
function registerMemoryQuery(memory2) {
|
|
8390
8583
|
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) => {
|
|
8391
|
-
const root =
|
|
8392
|
-
const paths =
|
|
8584
|
+
const root = findProjectRoot23(opts.dir);
|
|
8585
|
+
const paths = resolveHaivePaths20(root);
|
|
8393
8586
|
if (!existsSync43(paths.memoriesDir)) {
|
|
8394
8587
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
8395
8588
|
process.exitCode = 1;
|
|
@@ -8431,7 +8624,7 @@ function registerMemoryQuery(memory2) {
|
|
|
8431
8624
|
const fm = mem.frontmatter;
|
|
8432
8625
|
const statusBadge = ui.statusBadge(fm.status);
|
|
8433
8626
|
console.log(`${ui.bold(fm.id)} ${ui.dim(fm.scope)} ${statusBadge}`);
|
|
8434
|
-
console.log(` ${ui.dim(
|
|
8627
|
+
console.log(` ${ui.dim(path26.relative(root, filePath))}`);
|
|
8435
8628
|
const snippet = extractSnippet2(mem.body, snippetNeedle);
|
|
8436
8629
|
if (snippet) console.log(` ${snippet}`);
|
|
8437
8630
|
}
|
|
@@ -8452,17 +8645,17 @@ import { writeFile as writeFile20 } from "fs/promises";
|
|
|
8452
8645
|
import { existsSync as existsSync44 } from "fs";
|
|
8453
8646
|
import "commander";
|
|
8454
8647
|
import {
|
|
8455
|
-
findProjectRoot as
|
|
8648
|
+
findProjectRoot as findProjectRoot24,
|
|
8456
8649
|
loadUsageIndex as loadUsageIndex17,
|
|
8457
8650
|
recordRejection as recordRejection2,
|
|
8458
|
-
resolveHaivePaths as
|
|
8651
|
+
resolveHaivePaths as resolveHaivePaths21,
|
|
8459
8652
|
saveUsageIndex as saveUsageIndex3,
|
|
8460
8653
|
serializeMemory as serializeMemory18
|
|
8461
8654
|
} from "@hiveai/core";
|
|
8462
8655
|
function registerMemoryReject(memory2) {
|
|
8463
8656
|
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) => {
|
|
8464
|
-
const root =
|
|
8465
|
-
const paths =
|
|
8657
|
+
const root = findProjectRoot24(opts.dir);
|
|
8658
|
+
const paths = resolveHaivePaths21(root);
|
|
8466
8659
|
if (!existsSync44(paths.memoriesDir)) {
|
|
8467
8660
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8468
8661
|
process.exitCode = 1;
|
|
@@ -8501,19 +8694,19 @@ function registerMemoryReject(memory2) {
|
|
|
8501
8694
|
// src/commands/memory-rm.ts
|
|
8502
8695
|
import { existsSync as existsSync45 } from "fs";
|
|
8503
8696
|
import { unlink as unlink3 } from "fs/promises";
|
|
8504
|
-
import
|
|
8505
|
-
import { createInterface } from "readline/promises";
|
|
8697
|
+
import path27 from "path";
|
|
8698
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
8506
8699
|
import "commander";
|
|
8507
8700
|
import {
|
|
8508
|
-
findProjectRoot as
|
|
8701
|
+
findProjectRoot as findProjectRoot25,
|
|
8509
8702
|
loadUsageIndex as loadUsageIndex18,
|
|
8510
|
-
resolveHaivePaths as
|
|
8703
|
+
resolveHaivePaths as resolveHaivePaths22,
|
|
8511
8704
|
saveUsageIndex as saveUsageIndex4
|
|
8512
8705
|
} from "@hiveai/core";
|
|
8513
8706
|
function registerMemoryRm(memory2) {
|
|
8514
8707
|
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) => {
|
|
8515
|
-
const root =
|
|
8516
|
-
const paths =
|
|
8708
|
+
const root = findProjectRoot25(opts.dir);
|
|
8709
|
+
const paths = resolveHaivePaths22(root);
|
|
8517
8710
|
if (!existsSync45(paths.memoriesDir)) {
|
|
8518
8711
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8519
8712
|
process.exitCode = 1;
|
|
@@ -8526,9 +8719,9 @@ function registerMemoryRm(memory2) {
|
|
|
8526
8719
|
process.exitCode = 1;
|
|
8527
8720
|
return;
|
|
8528
8721
|
}
|
|
8529
|
-
const rel =
|
|
8722
|
+
const rel = path27.relative(root, found.filePath);
|
|
8530
8723
|
if (!opts.yes) {
|
|
8531
|
-
const rl =
|
|
8724
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
8532
8725
|
const answer = (await rl.question(`Delete ${rel}? [y/N] `)).trim().toLowerCase();
|
|
8533
8726
|
rl.close();
|
|
8534
8727
|
if (answer !== "y" && answer !== "yes") {
|
|
@@ -8552,19 +8745,19 @@ function registerMemoryRm(memory2) {
|
|
|
8552
8745
|
// src/commands/memory-show.ts
|
|
8553
8746
|
import { existsSync as existsSync46 } from "fs";
|
|
8554
8747
|
import { readFile as readFile11 } from "fs/promises";
|
|
8555
|
-
import
|
|
8748
|
+
import path28 from "path";
|
|
8556
8749
|
import "commander";
|
|
8557
8750
|
import {
|
|
8558
8751
|
deriveConfidence as deriveConfidence10,
|
|
8559
|
-
findProjectRoot as
|
|
8752
|
+
findProjectRoot as findProjectRoot26,
|
|
8560
8753
|
getUsage as getUsage15,
|
|
8561
8754
|
loadUsageIndex as loadUsageIndex19,
|
|
8562
|
-
resolveHaivePaths as
|
|
8755
|
+
resolveHaivePaths as resolveHaivePaths23
|
|
8563
8756
|
} from "@hiveai/core";
|
|
8564
8757
|
function registerMemoryShow(memory2) {
|
|
8565
8758
|
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) => {
|
|
8566
|
-
const root =
|
|
8567
|
-
const paths =
|
|
8759
|
+
const root = findProjectRoot26(opts.dir);
|
|
8760
|
+
const paths = resolveHaivePaths23(root);
|
|
8568
8761
|
if (!existsSync46(paths.memoriesDir)) {
|
|
8569
8762
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8570
8763
|
process.exitCode = 1;
|
|
@@ -8594,7 +8787,7 @@ function registerMemoryShow(memory2) {
|
|
|
8594
8787
|
if (fm.verified_at) console.log(`${ui.dim("verified:")} ${fm.verified_at}`);
|
|
8595
8788
|
if (fm.stale_reason) console.log(`${ui.dim("stale:")} ${fm.stale_reason}`);
|
|
8596
8789
|
console.log(`${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`);
|
|
8597
|
-
console.log(`${ui.dim("file:")} ${
|
|
8790
|
+
console.log(`${ui.dim("file:")} ${path28.relative(root, found.filePath)}`);
|
|
8598
8791
|
if (fm.anchor.paths.length || fm.anchor.symbols.length) {
|
|
8599
8792
|
console.log(ui.dim("anchor:"));
|
|
8600
8793
|
if (fm.anchor.commit) console.log(` ${ui.dim("commit:")} ${fm.anchor.commit}`);
|
|
@@ -8610,19 +8803,19 @@ function registerMemoryShow(memory2) {
|
|
|
8610
8803
|
|
|
8611
8804
|
// src/commands/memory-stats.ts
|
|
8612
8805
|
import { existsSync as existsSync47 } from "fs";
|
|
8613
|
-
import
|
|
8806
|
+
import path29 from "path";
|
|
8614
8807
|
import "commander";
|
|
8615
8808
|
import {
|
|
8616
8809
|
deriveConfidence as deriveConfidence11,
|
|
8617
|
-
findProjectRoot as
|
|
8810
|
+
findProjectRoot as findProjectRoot27,
|
|
8618
8811
|
getUsage as getUsage16,
|
|
8619
8812
|
loadUsageIndex as loadUsageIndex20,
|
|
8620
|
-
resolveHaivePaths as
|
|
8813
|
+
resolveHaivePaths as resolveHaivePaths24
|
|
8621
8814
|
} from "@hiveai/core";
|
|
8622
8815
|
function registerMemoryStats(memory2) {
|
|
8623
8816
|
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) => {
|
|
8624
|
-
const root =
|
|
8625
|
-
const paths =
|
|
8817
|
+
const root = findProjectRoot27(opts.dir);
|
|
8818
|
+
const paths = resolveHaivePaths24(root);
|
|
8626
8819
|
if (!existsSync47(paths.memoriesDir)) {
|
|
8627
8820
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
8628
8821
|
process.exitCode = 1;
|
|
@@ -8648,7 +8841,7 @@ function registerMemoryStats(memory2) {
|
|
|
8648
8841
|
console.log(
|
|
8649
8842
|
` ${ui.dim("status:")} ${fm.status} ${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`
|
|
8650
8843
|
);
|
|
8651
|
-
console.log(` ${ui.dim(
|
|
8844
|
+
console.log(` ${ui.dim(path29.relative(root, filePath))}`);
|
|
8652
8845
|
}
|
|
8653
8846
|
});
|
|
8654
8847
|
}
|
|
@@ -8656,11 +8849,11 @@ function registerMemoryStats(memory2) {
|
|
|
8656
8849
|
// src/commands/memory-verify.ts
|
|
8657
8850
|
import { writeFile as writeFile21 } from "fs/promises";
|
|
8658
8851
|
import { existsSync as existsSync48 } from "fs";
|
|
8659
|
-
import
|
|
8852
|
+
import path30 from "path";
|
|
8660
8853
|
import "commander";
|
|
8661
8854
|
import {
|
|
8662
|
-
findProjectRoot as
|
|
8663
|
-
resolveHaivePaths as
|
|
8855
|
+
findProjectRoot as findProjectRoot28,
|
|
8856
|
+
resolveHaivePaths as resolveHaivePaths25,
|
|
8664
8857
|
serializeMemory as serializeMemory19,
|
|
8665
8858
|
verifyAnchor as verifyAnchor3
|
|
8666
8859
|
} from "@hiveai/core";
|
|
@@ -8668,8 +8861,8 @@ function registerMemoryVerify(memory2) {
|
|
|
8668
8861
|
memory2.command("verify").description(
|
|
8669
8862
|
"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"
|
|
8670
8863
|
).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) => {
|
|
8671
|
-
const root =
|
|
8672
|
-
const paths =
|
|
8864
|
+
const root = findProjectRoot28(opts.dir);
|
|
8865
|
+
const paths = resolveHaivePaths25(root);
|
|
8673
8866
|
if (!existsSync48(paths.memoriesDir)) {
|
|
8674
8867
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
8675
8868
|
process.exitCode = 1;
|
|
@@ -8693,7 +8886,7 @@ function registerMemoryVerify(memory2) {
|
|
|
8693
8886
|
anchorlessIds.push(mem.frontmatter.id);
|
|
8694
8887
|
continue;
|
|
8695
8888
|
}
|
|
8696
|
-
const rel =
|
|
8889
|
+
const rel = path30.relative(root, filePath);
|
|
8697
8890
|
if (result.stale) {
|
|
8698
8891
|
staleCount++;
|
|
8699
8892
|
console.log(`${ui.bold("STALE")} ${mem.frontmatter.id}`);
|
|
@@ -8760,15 +8953,15 @@ import { readFile as readFile12 } from "fs/promises";
|
|
|
8760
8953
|
import { existsSync as existsSync49 } from "fs";
|
|
8761
8954
|
import "commander";
|
|
8762
8955
|
import {
|
|
8763
|
-
findProjectRoot as
|
|
8764
|
-
resolveHaivePaths as
|
|
8956
|
+
findProjectRoot as findProjectRoot29,
|
|
8957
|
+
resolveHaivePaths as resolveHaivePaths26
|
|
8765
8958
|
} from "@hiveai/core";
|
|
8766
8959
|
function registerMemoryImport(memory2) {
|
|
8767
8960
|
memory2.command("import").description(
|
|
8768
8961
|
"Parse a Markdown file and suggest memories via the import_docs MCP prompt (prints a ready-to-use prompt invocation)"
|
|
8769
8962
|
).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) => {
|
|
8770
|
-
const root =
|
|
8771
|
-
const paths =
|
|
8963
|
+
const root = findProjectRoot29(opts.dir);
|
|
8964
|
+
const paths = resolveHaivePaths26(root);
|
|
8772
8965
|
if (!existsSync49(paths.haiveDir)) {
|
|
8773
8966
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
8774
8967
|
process.exitCode = 1;
|
|
@@ -8808,21 +9001,21 @@ function registerMemoryImport(memory2) {
|
|
|
8808
9001
|
|
|
8809
9002
|
// src/commands/memory-import-changelog.ts
|
|
8810
9003
|
import { existsSync as existsSync50 } from "fs";
|
|
8811
|
-
import { readFile as readFile13, mkdir as
|
|
8812
|
-
import
|
|
9004
|
+
import { readFile as readFile13, mkdir as mkdir14, writeFile as writeFile23 } from "fs/promises";
|
|
9005
|
+
import path31 from "path";
|
|
8813
9006
|
import "commander";
|
|
8814
9007
|
import {
|
|
8815
9008
|
buildFrontmatter as buildFrontmatter9,
|
|
8816
|
-
findProjectRoot as
|
|
8817
|
-
resolveHaivePaths as
|
|
9009
|
+
findProjectRoot as findProjectRoot30,
|
|
9010
|
+
resolveHaivePaths as resolveHaivePaths27,
|
|
8818
9011
|
serializeMemory as serializeMemory20
|
|
8819
9012
|
} from "@hiveai/core";
|
|
8820
9013
|
function parseChangelog(content) {
|
|
8821
9014
|
const entries = [];
|
|
8822
9015
|
const versionRe = /^#{1,3}\s+(?:\[?)([0-9]+\.[0-9]+[.0-9]*)/m;
|
|
8823
9016
|
const sections = content.split(/^#{1,3}\s+/m).slice(1);
|
|
8824
|
-
for (const
|
|
8825
|
-
const versionMatch =
|
|
9017
|
+
for (const section2 of sections) {
|
|
9018
|
+
const versionMatch = section2.match(/^(?:\[?)([0-9]+\.[0-9]+[.0-9]*)/);
|
|
8826
9019
|
const version = versionMatch?.[1];
|
|
8827
9020
|
if (!version) continue;
|
|
8828
9021
|
const entry = {
|
|
@@ -8833,7 +9026,7 @@ function parseChangelog(content) {
|
|
|
8833
9026
|
fixed: [],
|
|
8834
9027
|
added: []
|
|
8835
9028
|
};
|
|
8836
|
-
const subSections =
|
|
9029
|
+
const subSections = section2.split(/^#{2,4}\s+/m);
|
|
8837
9030
|
for (const sub of subSections) {
|
|
8838
9031
|
const firstLine = (sub.split("\n")[0] ?? "").toLowerCase().trim();
|
|
8839
9032
|
const items = sub.split("\n").slice(1).filter((l) => l.trim().startsWith("-") || l.trim().startsWith("*")).map((l) => l.replace(/^[\s\-*]+/, "").trim()).filter(Boolean);
|
|
@@ -8859,7 +9052,7 @@ function parseChangelog(content) {
|
|
|
8859
9052
|
}
|
|
8860
9053
|
}
|
|
8861
9054
|
if (entry.breaking.length === 0) {
|
|
8862
|
-
for (const line of
|
|
9055
|
+
for (const line of section2.split("\n")) {
|
|
8863
9056
|
if (/breaking|⚠|deprecated|removed/.test(line.toLowerCase())) {
|
|
8864
9057
|
const item = line.replace(/^[\s\-*#]+/, "").trim();
|
|
8865
9058
|
if (item) entry.breaking.push(item);
|
|
@@ -8879,9 +9072,9 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
8879
9072
|
"--versions <csv>",
|
|
8880
9073
|
"only import specific versions (comma-separated), or 'latest' for the most recent breaking version"
|
|
8881
9074
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
8882
|
-
const root =
|
|
8883
|
-
const paths =
|
|
8884
|
-
const changelogPath =
|
|
9075
|
+
const root = findProjectRoot30(opts.dir);
|
|
9076
|
+
const paths = resolveHaivePaths27(root);
|
|
9077
|
+
const changelogPath = path31.resolve(root, opts.fromChangelog);
|
|
8885
9078
|
if (!existsSync50(changelogPath)) {
|
|
8886
9079
|
ui.error(`CHANGELOG not found: ${changelogPath}`);
|
|
8887
9080
|
process.exitCode = 1;
|
|
@@ -8902,10 +9095,10 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
8902
9095
|
entries = entries.filter((e) => requested.includes(e.version));
|
|
8903
9096
|
}
|
|
8904
9097
|
}
|
|
8905
|
-
const pkgName = opts.package ??
|
|
9098
|
+
const pkgName = opts.package ?? path31.basename(path31.dirname(changelogPath));
|
|
8906
9099
|
const scope = opts.scope ?? "team";
|
|
8907
|
-
const teamDir =
|
|
8908
|
-
await
|
|
9100
|
+
const teamDir = path31.join(paths.memoriesDir, scope);
|
|
9101
|
+
await mkdir14(teamDir, { recursive: true });
|
|
8909
9102
|
let saved = 0;
|
|
8910
9103
|
for (const entry of entries) {
|
|
8911
9104
|
const lines = [];
|
|
@@ -8927,7 +9120,7 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
8927
9120
|
lines.push("");
|
|
8928
9121
|
}
|
|
8929
9122
|
lines.push(
|
|
8930
|
-
`**Source:** \`${
|
|
9123
|
+
`**Source:** \`${path31.relative(root, changelogPath)}\`
|
|
8931
9124
|
**Action:** Update all usages of ${pkgName} if they rely on any of the above.`
|
|
8932
9125
|
);
|
|
8933
9126
|
const slug = `changelog-${pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase()}-v${entry.version.replace(/\./g, "-")}`;
|
|
@@ -8942,11 +9135,11 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
8942
9135
|
pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase(),
|
|
8943
9136
|
`v${entry.version}`
|
|
8944
9137
|
],
|
|
8945
|
-
paths: [
|
|
9138
|
+
paths: [path31.relative(root, changelogPath)],
|
|
8946
9139
|
topic: `changelog-${pkgName}-${entry.version}`
|
|
8947
9140
|
});
|
|
8948
9141
|
await writeFile23(
|
|
8949
|
-
|
|
9142
|
+
path31.join(teamDir, `${fm.id}.md`),
|
|
8950
9143
|
serializeMemory20({ frontmatter: fm, body: lines.join("\n") }),
|
|
8951
9144
|
"utf8"
|
|
8952
9145
|
);
|
|
@@ -8971,15 +9164,15 @@ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${p
|
|
|
8971
9164
|
// src/commands/memory-digest.ts
|
|
8972
9165
|
import { existsSync as existsSync51 } from "fs";
|
|
8973
9166
|
import { writeFile as writeFile24 } from "fs/promises";
|
|
8974
|
-
import
|
|
9167
|
+
import path33 from "path";
|
|
8975
9168
|
import "commander";
|
|
8976
9169
|
import {
|
|
8977
9170
|
deriveConfidence as deriveConfidence12,
|
|
8978
|
-
findProjectRoot as
|
|
9171
|
+
findProjectRoot as findProjectRoot31,
|
|
8979
9172
|
getUsage as getUsage17,
|
|
8980
9173
|
loadMemoriesFromDir as loadMemoriesFromDir26,
|
|
8981
9174
|
loadUsageIndex as loadUsageIndex21,
|
|
8982
|
-
resolveHaivePaths as
|
|
9175
|
+
resolveHaivePaths as resolveHaivePaths28
|
|
8983
9176
|
} from "@hiveai/core";
|
|
8984
9177
|
var CONFIDENCE_EMOJI = {
|
|
8985
9178
|
unverified: "\u2B1C",
|
|
@@ -8992,8 +9185,8 @@ function registerMemoryDigest(program2) {
|
|
|
8992
9185
|
program2.command("digest").description(
|
|
8993
9186
|
"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"
|
|
8994
9187
|
).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) => {
|
|
8995
|
-
const root =
|
|
8996
|
-
const paths =
|
|
9188
|
+
const root = findProjectRoot31(opts.dir);
|
|
9189
|
+
const paths = resolveHaivePaths28(root);
|
|
8997
9190
|
if (!existsSync51(paths.memoriesDir)) {
|
|
8998
9191
|
ui.error("No .ai/memories found. Run `haive init` first.");
|
|
8999
9192
|
process.exitCode = 1;
|
|
@@ -9066,7 +9259,7 @@ function registerMemoryDigest(program2) {
|
|
|
9066
9259
|
);
|
|
9067
9260
|
const digest = lines.join("\n");
|
|
9068
9261
|
if (opts.out) {
|
|
9069
|
-
const outPath =
|
|
9262
|
+
const outPath = path33.resolve(process.cwd(), opts.out);
|
|
9070
9263
|
await writeFile24(outPath, digest, "utf8");
|
|
9071
9264
|
ui.success(`Digest written to ${opts.out} (${recent.length} memor${recent.length === 1 ? "y" : "ies"})`);
|
|
9072
9265
|
} else {
|
|
@@ -9076,20 +9269,20 @@ function registerMemoryDigest(program2) {
|
|
|
9076
9269
|
}
|
|
9077
9270
|
|
|
9078
9271
|
// src/commands/session-end.ts
|
|
9079
|
-
import { writeFile as writeFile25, mkdir as
|
|
9272
|
+
import { writeFile as writeFile25, mkdir as mkdir15, readFile as readFile14, rm as rm2 } from "fs/promises";
|
|
9080
9273
|
import { existsSync as existsSync53 } from "fs";
|
|
9081
|
-
import
|
|
9274
|
+
import path34 from "path";
|
|
9082
9275
|
import "commander";
|
|
9083
9276
|
import {
|
|
9084
9277
|
buildFrontmatter as buildFrontmatter10,
|
|
9085
|
-
findProjectRoot as
|
|
9278
|
+
findProjectRoot as findProjectRoot32,
|
|
9086
9279
|
loadMemoriesFromDir as loadMemoriesFromDir27,
|
|
9087
9280
|
memoryFilePath as memoryFilePath9,
|
|
9088
|
-
resolveHaivePaths as
|
|
9281
|
+
resolveHaivePaths as resolveHaivePaths29,
|
|
9089
9282
|
serializeMemory as serializeMemory21
|
|
9090
9283
|
} from "@hiveai/core";
|
|
9091
9284
|
async function buildAutoRecap(paths) {
|
|
9092
|
-
const obsFile =
|
|
9285
|
+
const obsFile = path34.join(paths.haiveDir, ".cache", "observations.jsonl");
|
|
9093
9286
|
if (!existsSync53(obsFile)) return null;
|
|
9094
9287
|
const raw = await readFile14(obsFile, "utf8").catch(() => "");
|
|
9095
9288
|
if (!raw.trim()) return null;
|
|
@@ -9169,8 +9362,8 @@ function registerSessionEnd(session2) {
|
|
|
9169
9362
|
--next "Add integration tests for webhook signature validation"
|
|
9170
9363
|
`
|
|
9171
9364
|
).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) => {
|
|
9172
|
-
const root =
|
|
9173
|
-
const paths =
|
|
9365
|
+
const root = findProjectRoot32(opts.dir);
|
|
9366
|
+
const paths = resolveHaivePaths29(root);
|
|
9174
9367
|
if (!existsSync53(paths.haiveDir)) {
|
|
9175
9368
|
if (opts.auto || opts.quiet) return;
|
|
9176
9369
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
@@ -9203,14 +9396,14 @@ function registerSessionEnd(session2) {
|
|
|
9203
9396
|
});
|
|
9204
9397
|
const topic = recapTopic2(scope, opts.module);
|
|
9205
9398
|
const filesTouched = parseCsv5(resolvedFiles);
|
|
9206
|
-
const missingPaths = filesTouched.filter((p) => !existsSync53(
|
|
9399
|
+
const missingPaths = filesTouched.filter((p) => !existsSync53(path34.resolve(root, p)));
|
|
9207
9400
|
if (missingPaths.length > 0 && !opts.quiet) {
|
|
9208
9401
|
ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
|
|
9209
9402
|
for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
|
|
9210
9403
|
}
|
|
9211
9404
|
const cleanupObservations = async () => {
|
|
9212
9405
|
if (!opts.auto) return;
|
|
9213
|
-
const obsFile =
|
|
9406
|
+
const obsFile = path34.join(paths.haiveDir, ".cache", "observations.jsonl");
|
|
9214
9407
|
if (existsSync53(obsFile)) await rm2(obsFile).catch(() => {
|
|
9215
9408
|
});
|
|
9216
9409
|
};
|
|
@@ -9234,7 +9427,7 @@ function registerSessionEnd(session2) {
|
|
|
9234
9427
|
await cleanupObservations();
|
|
9235
9428
|
if (!opts.quiet) {
|
|
9236
9429
|
ui.success(`Session recap updated (revision #${revisionCount})`);
|
|
9237
|
-
ui.info(`id=${fm.id} file=${
|
|
9430
|
+
ui.info(`id=${fm.id} file=${path34.relative(root, topicMatch.filePath)}`);
|
|
9238
9431
|
ui.info("Tip: `haive stats --export-report` generates a usage JSON suitable for dashboards.");
|
|
9239
9432
|
}
|
|
9240
9433
|
return;
|
|
@@ -9251,12 +9444,12 @@ function registerSessionEnd(session2) {
|
|
|
9251
9444
|
status: "validated"
|
|
9252
9445
|
});
|
|
9253
9446
|
const file = memoryFilePath9(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
9254
|
-
await
|
|
9447
|
+
await mkdir15(path34.dirname(file), { recursive: true });
|
|
9255
9448
|
await writeFile25(file, serializeMemory21({ frontmatter, body }), "utf8");
|
|
9256
9449
|
await cleanupObservations();
|
|
9257
9450
|
if (!opts.quiet) {
|
|
9258
9451
|
ui.success(`Session recap created`);
|
|
9259
|
-
ui.info(`id=${frontmatter.id} scope=${scope} file=${
|
|
9452
|
+
ui.info(`id=${frontmatter.id} scope=${scope} file=${path34.relative(root, file)}`);
|
|
9260
9453
|
ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
|
|
9261
9454
|
ui.info("Tip: export a local MCP usage rollup with `haive stats --export-report .ai/tool-usage-roi-report.json`.");
|
|
9262
9455
|
}
|
|
@@ -9270,13 +9463,13 @@ function parseCsv5(value) {
|
|
|
9270
9463
|
// src/commands/snapshot.ts
|
|
9271
9464
|
import { existsSync as existsSync54 } from "fs";
|
|
9272
9465
|
import { readdir as readdir4 } from "fs/promises";
|
|
9273
|
-
import
|
|
9466
|
+
import path35 from "path";
|
|
9274
9467
|
import "commander";
|
|
9275
9468
|
import {
|
|
9276
9469
|
diffContract,
|
|
9277
|
-
findProjectRoot as
|
|
9470
|
+
findProjectRoot as findProjectRoot33,
|
|
9278
9471
|
loadConfig as loadConfig5,
|
|
9279
|
-
resolveHaivePaths as
|
|
9472
|
+
resolveHaivePaths as resolveHaivePaths30,
|
|
9280
9473
|
snapshotContract
|
|
9281
9474
|
} from "@hiveai/core";
|
|
9282
9475
|
function registerSnapshot(program2) {
|
|
@@ -9301,15 +9494,15 @@ function registerSnapshot(program2) {
|
|
|
9301
9494
|
"--format <format>",
|
|
9302
9495
|
"contract format: openapi | graphql | proto | typescript | json-schema (auto-detected if omitted)"
|
|
9303
9496
|
).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) => {
|
|
9304
|
-
const root =
|
|
9305
|
-
const paths =
|
|
9497
|
+
const root = findProjectRoot33(opts.dir);
|
|
9498
|
+
const paths = resolveHaivePaths30(root);
|
|
9306
9499
|
if (!existsSync54(paths.haiveDir)) {
|
|
9307
9500
|
ui.error("No .ai/ found. Run `haive init` first.");
|
|
9308
9501
|
process.exitCode = 1;
|
|
9309
9502
|
return;
|
|
9310
9503
|
}
|
|
9311
9504
|
if (opts.list) {
|
|
9312
|
-
const contractsDir =
|
|
9505
|
+
const contractsDir = path35.join(paths.haiveDir, "contracts");
|
|
9313
9506
|
if (!existsSync54(contractsDir)) {
|
|
9314
9507
|
console.log(ui.dim("No contract snapshots found."));
|
|
9315
9508
|
return;
|
|
@@ -9365,7 +9558,7 @@ function registerSnapshot(program2) {
|
|
|
9365
9558
|
return;
|
|
9366
9559
|
}
|
|
9367
9560
|
const contractPath = opts.contract;
|
|
9368
|
-
const name = opts.name ??
|
|
9561
|
+
const name = opts.name ?? path35.basename(contractPath, path35.extname(contractPath));
|
|
9369
9562
|
const format = opts.format ?? detectFormat(contractPath) ?? "openapi";
|
|
9370
9563
|
const contract = { name, path: contractPath, format };
|
|
9371
9564
|
try {
|
|
@@ -9420,8 +9613,8 @@ async function runDiff(root, haiveDir, contract) {
|
|
|
9420
9613
|
}
|
|
9421
9614
|
}
|
|
9422
9615
|
function detectFormat(filePath) {
|
|
9423
|
-
const ext =
|
|
9424
|
-
const base =
|
|
9616
|
+
const ext = path35.extname(filePath).toLowerCase();
|
|
9617
|
+
const base = path35.basename(filePath).toLowerCase();
|
|
9425
9618
|
if (ext === ".yaml" || ext === ".yml" || ext === ".json") {
|
|
9426
9619
|
if (base.includes("openapi") || base.includes("swagger")) return "openapi";
|
|
9427
9620
|
if (base.includes("schema") || base.includes("graphql")) return "graphql";
|
|
@@ -9435,15 +9628,15 @@ function detectFormat(filePath) {
|
|
|
9435
9628
|
|
|
9436
9629
|
// src/commands/hub.ts
|
|
9437
9630
|
import { existsSync as existsSync55 } from "fs";
|
|
9438
|
-
import { mkdir as
|
|
9439
|
-
import
|
|
9440
|
-
import { spawnSync as
|
|
9631
|
+
import { mkdir as mkdir16, readFile as readFile15, writeFile as writeFile26, copyFile } from "fs/promises";
|
|
9632
|
+
import path36 from "path";
|
|
9633
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
9441
9634
|
import "commander";
|
|
9442
9635
|
import {
|
|
9443
|
-
findProjectRoot as
|
|
9636
|
+
findProjectRoot as findProjectRoot34,
|
|
9444
9637
|
loadConfig as loadConfig6,
|
|
9445
9638
|
loadMemoriesFromDir as loadMemoriesFromDir28,
|
|
9446
|
-
resolveHaivePaths as
|
|
9639
|
+
resolveHaivePaths as resolveHaivePaths31,
|
|
9447
9640
|
saveConfig as saveConfig2,
|
|
9448
9641
|
serializeMemory as serializeMemory23
|
|
9449
9642
|
} from "@hiveai/core";
|
|
@@ -9455,21 +9648,21 @@ function registerHub(program2) {
|
|
|
9455
9648
|
hub.command("init <hubPath>").description(
|
|
9456
9649
|
"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"
|
|
9457
9650
|
).action(async (hubPath) => {
|
|
9458
|
-
const absPath =
|
|
9459
|
-
await
|
|
9460
|
-
const gitCheck =
|
|
9651
|
+
const absPath = path36.resolve(hubPath);
|
|
9652
|
+
await mkdir16(absPath, { recursive: true });
|
|
9653
|
+
const gitCheck = spawnSync4("git", ["rev-parse", "--git-dir"], { cwd: absPath });
|
|
9461
9654
|
if (gitCheck.status !== 0) {
|
|
9462
|
-
const init =
|
|
9655
|
+
const init = spawnSync4("git", ["init"], { cwd: absPath, encoding: "utf8" });
|
|
9463
9656
|
if (init.status !== 0) {
|
|
9464
9657
|
ui.error(`git init failed: ${init.stderr}`);
|
|
9465
9658
|
process.exitCode = 1;
|
|
9466
9659
|
return;
|
|
9467
9660
|
}
|
|
9468
9661
|
}
|
|
9469
|
-
const sharedDir =
|
|
9470
|
-
await
|
|
9662
|
+
const sharedDir = path36.join(absPath, ".ai", "memories", "shared");
|
|
9663
|
+
await mkdir16(sharedDir, { recursive: true });
|
|
9471
9664
|
await writeFile26(
|
|
9472
|
-
|
|
9665
|
+
path36.join(absPath, ".ai", "README.md"),
|
|
9473
9666
|
`# hAIve Team Knowledge Hub
|
|
9474
9667
|
|
|
9475
9668
|
This repo is a shared knowledge hub for hAIve.
|
|
@@ -9491,12 +9684,12 @@ haive hub pull # import into a project
|
|
|
9491
9684
|
"utf8"
|
|
9492
9685
|
);
|
|
9493
9686
|
await writeFile26(
|
|
9494
|
-
|
|
9687
|
+
path36.join(absPath, ".gitignore"),
|
|
9495
9688
|
".ai/.cache/\n.ai/memories/personal/\n",
|
|
9496
9689
|
"utf8"
|
|
9497
9690
|
);
|
|
9498
|
-
|
|
9499
|
-
|
|
9691
|
+
spawnSync4("git", ["add", "."], { cwd: absPath });
|
|
9692
|
+
spawnSync4("git", ["commit", "-m", "chore: initialize hAIve team-knowledge hub"], {
|
|
9500
9693
|
cwd: absPath,
|
|
9501
9694
|
encoding: "utf8"
|
|
9502
9695
|
});
|
|
@@ -9506,7 +9699,7 @@ haive hub pull # import into a project
|
|
|
9506
9699
|
`
|
|
9507
9700
|
Next steps:
|
|
9508
9701
|
1. Add hubPath to your project's .ai/haive.config.json:
|
|
9509
|
-
{ "hubPath": "${
|
|
9702
|
+
{ "hubPath": "${path36.relative(process.cwd(), absPath)}" }
|
|
9510
9703
|
2. Run \`haive hub push\` to publish your shared memories
|
|
9511
9704
|
3. Share ${absPath} with teammates (git remote, NFS, etc.)
|
|
9512
9705
|
`
|
|
@@ -9525,8 +9718,8 @@ Next steps:
|
|
|
9525
9718
|
haive hub push --commit --message "feat: add payment API contract memories"
|
|
9526
9719
|
`
|
|
9527
9720
|
).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) => {
|
|
9528
|
-
const root =
|
|
9529
|
-
const paths =
|
|
9721
|
+
const root = findProjectRoot34(opts.dir);
|
|
9722
|
+
const paths = resolveHaivePaths31(root);
|
|
9530
9723
|
const config = await loadConfig6(paths);
|
|
9531
9724
|
if (!config.hubPath) {
|
|
9532
9725
|
ui.error(
|
|
@@ -9535,15 +9728,15 @@ Next steps:
|
|
|
9535
9728
|
process.exitCode = 1;
|
|
9536
9729
|
return;
|
|
9537
9730
|
}
|
|
9538
|
-
const hubRoot =
|
|
9731
|
+
const hubRoot = path36.resolve(root, config.hubPath);
|
|
9539
9732
|
if (!existsSync55(hubRoot)) {
|
|
9540
9733
|
ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
|
|
9541
9734
|
process.exitCode = 1;
|
|
9542
9735
|
return;
|
|
9543
9736
|
}
|
|
9544
|
-
const projectName =
|
|
9545
|
-
const destDir =
|
|
9546
|
-
await
|
|
9737
|
+
const projectName = path36.basename(root);
|
|
9738
|
+
const destDir = path36.join(hubRoot, ".ai", "memories", "shared", projectName);
|
|
9739
|
+
await mkdir16(destDir, { recursive: true });
|
|
9547
9740
|
const all = await loadMemoriesFromDir28(paths.memoriesDir);
|
|
9548
9741
|
const shared = all.filter(
|
|
9549
9742
|
({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && // Don't push imported memories (avoid echo loops)
|
|
@@ -9561,7 +9754,7 @@ Next steps:
|
|
|
9561
9754
|
for (const { memory: memory2 } of shared) {
|
|
9562
9755
|
const fm = memory2.frontmatter;
|
|
9563
9756
|
const fileName = `${fm.id}.md`;
|
|
9564
|
-
const destPath =
|
|
9757
|
+
const destPath = path36.join(destDir, fileName);
|
|
9565
9758
|
await writeFile26(destPath, serializeMemory23(memory2), "utf8");
|
|
9566
9759
|
pushed++;
|
|
9567
9760
|
}
|
|
@@ -9569,10 +9762,10 @@ Next steps:
|
|
|
9569
9762
|
console.log(ui.dim(` Location: ${destDir}`));
|
|
9570
9763
|
if (opts.commit) {
|
|
9571
9764
|
const message = opts.message ?? `haive: sync shared memories from ${projectName} (${pushed} memories)`;
|
|
9572
|
-
|
|
9765
|
+
spawnSync4("git", ["add", path36.join(".ai", "memories", "shared", projectName)], {
|
|
9573
9766
|
cwd: hubRoot
|
|
9574
9767
|
});
|
|
9575
|
-
const commit =
|
|
9768
|
+
const commit = spawnSync4("git", ["commit", "-m", message], {
|
|
9576
9769
|
cwd: hubRoot,
|
|
9577
9770
|
encoding: "utf8"
|
|
9578
9771
|
});
|
|
@@ -9594,8 +9787,8 @@ Next steps:
|
|
|
9594
9787
|
hub.command("pull").description(
|
|
9595
9788
|
"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"
|
|
9596
9789
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9597
|
-
const root =
|
|
9598
|
-
const paths =
|
|
9790
|
+
const root = findProjectRoot34(opts.dir);
|
|
9791
|
+
const paths = resolveHaivePaths31(root);
|
|
9599
9792
|
const config = await loadConfig6(paths);
|
|
9600
9793
|
if (!config.hubPath) {
|
|
9601
9794
|
ui.error(
|
|
@@ -9604,15 +9797,15 @@ Next steps:
|
|
|
9604
9797
|
process.exitCode = 1;
|
|
9605
9798
|
return;
|
|
9606
9799
|
}
|
|
9607
|
-
const hubRoot =
|
|
9608
|
-
const hubSharedDir =
|
|
9800
|
+
const hubRoot = path36.resolve(root, config.hubPath);
|
|
9801
|
+
const hubSharedDir = path36.join(hubRoot, ".ai", "memories", "shared");
|
|
9609
9802
|
if (!existsSync55(hubSharedDir)) {
|
|
9610
9803
|
ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
|
|
9611
9804
|
return;
|
|
9612
9805
|
}
|
|
9613
|
-
const projectName =
|
|
9614
|
-
const { readdir:
|
|
9615
|
-
const projectDirs = (await
|
|
9806
|
+
const projectName = path36.basename(root);
|
|
9807
|
+
const { readdir: readdir6 } = await import("fs/promises");
|
|
9808
|
+
const projectDirs = (await readdir6(hubSharedDir, { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== projectName).map((d) => d.name);
|
|
9616
9809
|
if (projectDirs.length === 0) {
|
|
9617
9810
|
console.log(ui.dim("No other projects have pushed to the hub yet."));
|
|
9618
9811
|
return;
|
|
@@ -9620,16 +9813,16 @@ Next steps:
|
|
|
9620
9813
|
let totalImported = 0;
|
|
9621
9814
|
let totalUpdated = 0;
|
|
9622
9815
|
for (const sourceName of projectDirs) {
|
|
9623
|
-
const sourceDir =
|
|
9624
|
-
const destDir =
|
|
9625
|
-
await
|
|
9626
|
-
const sourceFiles = (await
|
|
9816
|
+
const sourceDir = path36.join(hubSharedDir, sourceName);
|
|
9817
|
+
const destDir = path36.join(paths.memoriesDir, "shared", sourceName);
|
|
9818
|
+
await mkdir16(destDir, { recursive: true });
|
|
9819
|
+
const sourceFiles = (await readdir6(sourceDir)).filter((f) => f.endsWith(".md"));
|
|
9627
9820
|
const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
|
|
9628
9821
|
const existingInDest = await loadDir(destDir);
|
|
9629
9822
|
const existingIds = new Set(existingInDest.map(({ memory: memory2 }) => memory2.frontmatter.id));
|
|
9630
9823
|
for (const file of sourceFiles) {
|
|
9631
|
-
const srcPath =
|
|
9632
|
-
const destPath =
|
|
9824
|
+
const srcPath = path36.join(sourceDir, file);
|
|
9825
|
+
const destPath = path36.join(destDir, file);
|
|
9633
9826
|
const fileContent = await readFile15(srcPath, "utf8");
|
|
9634
9827
|
const alreadyTagged = fileContent.includes(`cross-repo:${sourceName}`);
|
|
9635
9828
|
if (!alreadyTagged) {
|
|
@@ -9653,21 +9846,21 @@ Next steps:
|
|
|
9653
9846
|
);
|
|
9654
9847
|
});
|
|
9655
9848
|
hub.command("status").description("Show hub sync status.").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9656
|
-
const root =
|
|
9657
|
-
const paths =
|
|
9849
|
+
const root = findProjectRoot34(opts.dir);
|
|
9850
|
+
const paths = resolveHaivePaths31(root);
|
|
9658
9851
|
const config = await loadConfig6(paths);
|
|
9659
9852
|
console.log(ui.bold("Hub status"));
|
|
9660
9853
|
console.log(
|
|
9661
9854
|
` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
|
|
9662
9855
|
);
|
|
9663
|
-
const sharedDir =
|
|
9856
|
+
const sharedDir = path36.join(paths.memoriesDir, "shared");
|
|
9664
9857
|
if (existsSync55(sharedDir)) {
|
|
9665
|
-
const { readdir:
|
|
9666
|
-
const sources = (await
|
|
9858
|
+
const { readdir: readdir6 } = await import("fs/promises");
|
|
9859
|
+
const sources = (await readdir6(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
9667
9860
|
console.log(`
|
|
9668
9861
|
Imported from ${sources.length} source(s):`);
|
|
9669
9862
|
for (const src of sources) {
|
|
9670
|
-
const files = (await
|
|
9863
|
+
const files = (await readdir6(path36.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
|
|
9671
9864
|
console.log(` ${src}: ${files.length} memor${files.length === 1 ? "y" : "ies"}`);
|
|
9672
9865
|
}
|
|
9673
9866
|
} else {
|
|
@@ -9691,16 +9884,16 @@ Next steps:
|
|
|
9691
9884
|
// src/commands/stats.ts
|
|
9692
9885
|
import "commander";
|
|
9693
9886
|
import { existsSync as existsSync56 } from "fs";
|
|
9694
|
-
import { mkdir as
|
|
9695
|
-
import
|
|
9887
|
+
import { mkdir as mkdir17, writeFile as writeFile27 } from "fs/promises";
|
|
9888
|
+
import path37 from "path";
|
|
9696
9889
|
import {
|
|
9697
9890
|
aggregateUsage,
|
|
9698
|
-
findProjectRoot as
|
|
9891
|
+
findProjectRoot as findProjectRoot35,
|
|
9699
9892
|
loadMemoriesFromDir as loadMemoriesFromDir29,
|
|
9700
9893
|
loadUsageIndex as loadUsageIndex23,
|
|
9701
9894
|
parseSince,
|
|
9702
9895
|
readUsageEvents as readUsageEvents2,
|
|
9703
|
-
resolveHaivePaths as
|
|
9896
|
+
resolveHaivePaths as resolveHaivePaths32,
|
|
9704
9897
|
usageLogSize
|
|
9705
9898
|
} from "@hiveai/core";
|
|
9706
9899
|
function registerStats(program2) {
|
|
@@ -9709,8 +9902,8 @@ function registerStats(program2) {
|
|
|
9709
9902
|
"write a JSON rollup (tools + briefing counts + heuristic ROI hints). Parent dirs are created if needed.",
|
|
9710
9903
|
void 0
|
|
9711
9904
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9712
|
-
const root =
|
|
9713
|
-
const paths =
|
|
9905
|
+
const root = findProjectRoot35(opts.dir);
|
|
9906
|
+
const paths = resolveHaivePaths32(root);
|
|
9714
9907
|
if (opts.exportReport) {
|
|
9715
9908
|
await writeRoiReport(paths, root, opts.since ?? "30d", opts.exportReport);
|
|
9716
9909
|
return;
|
|
@@ -9764,7 +9957,7 @@ function registerStats(program2) {
|
|
|
9764
9957
|
});
|
|
9765
9958
|
}
|
|
9766
9959
|
async function writeRoiReport(paths, root, sinceRaw, outRelative) {
|
|
9767
|
-
const outAbs =
|
|
9960
|
+
const outAbs = path37.isAbsolute(outRelative) ? path37.resolve(outRelative) : path37.resolve(root, outRelative);
|
|
9768
9961
|
const size = await usageLogSize(paths);
|
|
9769
9962
|
let events = await readUsageEvents2(paths);
|
|
9770
9963
|
let memoryCount = { team: 0, personal: 0, total_skipped_session: 0 };
|
|
@@ -9799,7 +9992,7 @@ async function writeRoiReport(paths, root, sinceRaw, outRelative) {
|
|
|
9799
9992
|
ui.warn("Usage log missing or empty \u2014 report still written with partial data.");
|
|
9800
9993
|
events = [];
|
|
9801
9994
|
}
|
|
9802
|
-
await
|
|
9995
|
+
await mkdir17(path37.dirname(outAbs), { recursive: true });
|
|
9803
9996
|
const payload = {
|
|
9804
9997
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9805
9998
|
project_root: root,
|
|
@@ -9863,13 +10056,13 @@ import { performance } from "perf_hooks";
|
|
|
9863
10056
|
import "commander";
|
|
9864
10057
|
import {
|
|
9865
10058
|
estimateTokens as estimateTokens3,
|
|
9866
|
-
findProjectRoot as
|
|
9867
|
-
resolveHaivePaths as
|
|
10059
|
+
findProjectRoot as findProjectRoot36,
|
|
10060
|
+
resolveHaivePaths as resolveHaivePaths33
|
|
9868
10061
|
} from "@hiveai/core";
|
|
9869
10062
|
function registerBench(program2) {
|
|
9870
10063
|
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) => {
|
|
9871
|
-
const root =
|
|
9872
|
-
const paths =
|
|
10064
|
+
const root = findProjectRoot36(opts.dir);
|
|
10065
|
+
const paths = resolveHaivePaths33(root);
|
|
9873
10066
|
const ctx = { paths };
|
|
9874
10067
|
const task = opts.task ?? "audit dependencies for security risks";
|
|
9875
10068
|
const scenarios = [
|
|
@@ -9987,20 +10180,165 @@ function summarize(name, t0, payload, notes) {
|
|
|
9987
10180
|
};
|
|
9988
10181
|
}
|
|
9989
10182
|
|
|
9990
|
-
// src/commands/
|
|
9991
|
-
import { mkdir as mkdir17, writeFile as writeFile28 } from "fs/promises";
|
|
10183
|
+
// src/commands/benchmark.ts
|
|
9992
10184
|
import { existsSync as existsSync57 } from "fs";
|
|
9993
|
-
import
|
|
10185
|
+
import { readdir as readdir5, readFile as readFile16, writeFile as writeFile28 } from "fs/promises";
|
|
10186
|
+
import path38 from "path";
|
|
10187
|
+
import "commander";
|
|
10188
|
+
import { estimateTokens as estimateTokens4, findProjectRoot as findProjectRoot37 } from "@hiveai/core";
|
|
10189
|
+
function registerBenchmark(program2) {
|
|
10190
|
+
const benchmark = program2.command("benchmark").description("Official hAIve benchmark/demo utilities for measuring agent enforcement value.");
|
|
10191
|
+
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) => {
|
|
10192
|
+
const root = resolveBenchmarkRoot(opts.dir);
|
|
10193
|
+
const rows = await collectRows(root);
|
|
10194
|
+
const summary = summarizeRows(rows);
|
|
10195
|
+
if (opts.json) {
|
|
10196
|
+
console.log(JSON.stringify({ root, summary, rows }, null, 2));
|
|
10197
|
+
return;
|
|
10198
|
+
}
|
|
10199
|
+
const markdown = renderMarkdown(root, summary, rows);
|
|
10200
|
+
if (opts.out) {
|
|
10201
|
+
const outFile = path38.isAbsolute(opts.out) ? opts.out : path38.join(root, opts.out);
|
|
10202
|
+
await writeFile28(outFile, markdown, "utf8");
|
|
10203
|
+
ui.success(`wrote ${path38.relative(process.cwd(), outFile)}`);
|
|
10204
|
+
return;
|
|
10205
|
+
}
|
|
10206
|
+
console.log(markdown);
|
|
10207
|
+
});
|
|
10208
|
+
benchmark.command("demo").description("Print the recommended protocol for running a hAIve vs plain agent benchmark.").action(() => {
|
|
10209
|
+
console.log([
|
|
10210
|
+
"# hAIve Agent Benchmark Demo",
|
|
10211
|
+
"",
|
|
10212
|
+
"1. Create paired fixtures: one `*-haive`, one `*-plain`.",
|
|
10213
|
+
"2. Put the same failing tests in both fixtures.",
|
|
10214
|
+
"3. Add precise `.ai/memories/team/*.md` policy memories only to the hAIve fixture.",
|
|
10215
|
+
"4. Run equal agents in parallel:",
|
|
10216
|
+
" - hAIve agents must run `haive briefing --files ... --task ...` first.",
|
|
10217
|
+
" - Plain agents must not read `.ai` or call hAIve.",
|
|
10218
|
+
"5. Require every agent to write `BENCHMARK_AGENT_REPORT.md`.",
|
|
10219
|
+
"6. Run `haive benchmark report --dir <benchmark-root> --out RESULTS.md`.",
|
|
10220
|
+
"",
|
|
10221
|
+
"Recommended metrics: pass rate, test iterations, files read, files changed, visible artifacts, decision quality, and token proxy."
|
|
10222
|
+
].join("\n"));
|
|
10223
|
+
});
|
|
10224
|
+
}
|
|
10225
|
+
function resolveBenchmarkRoot(dir) {
|
|
10226
|
+
const candidate = dir ?? "benchmarks/agent-benchmark";
|
|
10227
|
+
if (path38.isAbsolute(candidate)) return candidate;
|
|
10228
|
+
const projectRoot = findProjectRoot37(process.cwd());
|
|
10229
|
+
return path38.join(projectRoot, candidate);
|
|
10230
|
+
}
|
|
10231
|
+
async function collectRows(root) {
|
|
10232
|
+
if (!existsSync57(root)) throw new Error(`Benchmark directory not found: ${root}`);
|
|
10233
|
+
const entries = await readdir5(root, { withFileTypes: true });
|
|
10234
|
+
const rows = [];
|
|
10235
|
+
for (const entry of entries) {
|
|
10236
|
+
if (!entry.isDirectory()) continue;
|
|
10237
|
+
const fixtureDir = path38.join(root, entry.name);
|
|
10238
|
+
const reportFile = path38.join(fixtureDir, "BENCHMARK_AGENT_REPORT.md");
|
|
10239
|
+
if (!existsSync57(reportFile)) continue;
|
|
10240
|
+
const report = await readFile16(reportFile, "utf8");
|
|
10241
|
+
rows.push(parseAgentReport(entry.name, report));
|
|
10242
|
+
}
|
|
10243
|
+
return rows.sort((a, b) => a.fixture.localeCompare(b.fixture));
|
|
10244
|
+
}
|
|
10245
|
+
function parseAgentReport(fixture, report) {
|
|
10246
|
+
const group = fixture.endsWith("-haive") ? "haive" : fixture.endsWith("-plain") ? "plain" : "unknown";
|
|
10247
|
+
return {
|
|
10248
|
+
fixture,
|
|
10249
|
+
group,
|
|
10250
|
+
commands: sectionBulletCount(report, "Commands"),
|
|
10251
|
+
files_read: sectionBulletCount(report, "Files Read"),
|
|
10252
|
+
files_modified: sectionBulletCount(report, "Files Modified"),
|
|
10253
|
+
test_iterations: countMatches(section(report, "Test Iterations"), /Iteration\s+\d+|^- /gim),
|
|
10254
|
+
terminal_failures: countMatches(section(report, "Terminal Errors"), /fail|error|not raised|exited with code 1/gi),
|
|
10255
|
+
decision_mentions: sectionBulletCount(report, "Key Decisions"),
|
|
10256
|
+
token_proxy: estimateTokens4(report),
|
|
10257
|
+
haive_impact: /hAIve Memory Impact[\s\S]*?\b(yes|directly|changed|shaped|confirmed)\b/i.test(report)
|
|
10258
|
+
};
|
|
10259
|
+
}
|
|
10260
|
+
function summarizeRows(rows) {
|
|
10261
|
+
const byGroup = (group) => rows.filter((r) => r.group === group);
|
|
10262
|
+
return {
|
|
10263
|
+
fixtures: rows.length,
|
|
10264
|
+
haive: summarizeGroup(byGroup("haive")),
|
|
10265
|
+
plain: summarizeGroup(byGroup("plain"))
|
|
10266
|
+
};
|
|
10267
|
+
}
|
|
10268
|
+
function summarizeGroup(rows) {
|
|
10269
|
+
const sum = (key) => rows.reduce((total, row) => total + Number(row[key] ?? 0), 0);
|
|
10270
|
+
return {
|
|
10271
|
+
fixtures: rows.length,
|
|
10272
|
+
commands: sum("commands"),
|
|
10273
|
+
files_read: sum("files_read"),
|
|
10274
|
+
files_modified: sum("files_modified"),
|
|
10275
|
+
test_iterations: sum("test_iterations"),
|
|
10276
|
+
terminal_failures: sum("terminal_failures"),
|
|
10277
|
+
decision_mentions: sum("decision_mentions"),
|
|
10278
|
+
token_proxy: sum("token_proxy"),
|
|
10279
|
+
haive_impact_count: rows.filter((r) => r.haive_impact).length
|
|
10280
|
+
};
|
|
10281
|
+
}
|
|
10282
|
+
function renderMarkdown(root, summary, rows) {
|
|
10283
|
+
const lines = [
|
|
10284
|
+
"# hAIve Agent Benchmark Report",
|
|
10285
|
+
"",
|
|
10286
|
+
`Benchmark root: \`${root}\``,
|
|
10287
|
+
"",
|
|
10288
|
+
"## Summary",
|
|
10289
|
+
"",
|
|
10290
|
+
"| Group | Fixtures | Commands | Files read | Files modified | Test iterations | Terminal failures | Decision mentions | Token proxy | hAIve impact |",
|
|
10291
|
+
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
|
|
10292
|
+
groupLine("hAIve", summary.haive),
|
|
10293
|
+
groupLine("Plain", summary.plain),
|
|
10294
|
+
"",
|
|
10295
|
+
"## Fixtures",
|
|
10296
|
+
"",
|
|
10297
|
+
"| Fixture | Group | Commands | Files read | Files modified | Test iterations | Terminal failures | Decisions | Token proxy | hAIve impact |",
|
|
10298
|
+
"| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |",
|
|
10299
|
+
...rows.map(
|
|
10300
|
+
(row) => `| \`${row.fixture}\` | ${row.group} | ${row.commands} | ${row.files_read} | ${row.files_modified} | ${row.test_iterations} | ${row.terminal_failures} | ${row.decision_mentions} | ${row.token_proxy} | ${row.haive_impact ? "yes" : "no"} |`
|
|
10301
|
+
),
|
|
10302
|
+
"",
|
|
10303
|
+
"## Reading",
|
|
10304
|
+
"",
|
|
10305
|
+
"The token proxy is estimated from the agent report size, not from private model billing data.",
|
|
10306
|
+
"Use this report to compare relative effort and decision quality, then pair it with final test results and a human review of the diffs.",
|
|
10307
|
+
""
|
|
10308
|
+
];
|
|
10309
|
+
return lines.join("\n");
|
|
10310
|
+
}
|
|
10311
|
+
function groupLine(label, group) {
|
|
10312
|
+
return `| ${label} | ${group.fixtures} | ${group.commands} | ${group.files_read} | ${group.files_modified} | ${group.test_iterations} | ${group.terminal_failures} | ${group.decision_mentions} | ${group.token_proxy} | ${group.haive_impact_count} |`;
|
|
10313
|
+
}
|
|
10314
|
+
function sectionBulletCount(markdown, title) {
|
|
10315
|
+
return countMatches(section(markdown, title), /^- |^\d+\.\s/gm);
|
|
10316
|
+
}
|
|
10317
|
+
function section(markdown, title) {
|
|
10318
|
+
const re = new RegExp(`##\\s+[^\\n]*${escapeRegExp(title)}[^\\n]*\\n([\\s\\S]*?)(?=\\n##\\s+|$)`, "i");
|
|
10319
|
+
return re.exec(markdown)?.[1] ?? "";
|
|
10320
|
+
}
|
|
10321
|
+
function countMatches(text, re) {
|
|
10322
|
+
return [...text.matchAll(re)].length;
|
|
10323
|
+
}
|
|
10324
|
+
function escapeRegExp(value) {
|
|
10325
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10326
|
+
}
|
|
10327
|
+
|
|
10328
|
+
// src/commands/memory-suggest.ts
|
|
10329
|
+
import { mkdir as mkdir18, writeFile as writeFile29 } from "fs/promises";
|
|
10330
|
+
import { existsSync as existsSync58 } from "fs";
|
|
10331
|
+
import path39 from "path";
|
|
9994
10332
|
import "commander";
|
|
9995
10333
|
import {
|
|
9996
10334
|
aggregateUsage as aggregateUsage2,
|
|
9997
10335
|
buildFrontmatter as buildFrontmatter11,
|
|
9998
|
-
findProjectRoot as
|
|
10336
|
+
findProjectRoot as findProjectRoot38,
|
|
9999
10337
|
loadMemoriesFromDir as loadMemoriesFromDir30,
|
|
10000
10338
|
memoryFilePath as memoryFilePath10,
|
|
10001
10339
|
parseSince as parseSince2,
|
|
10002
10340
|
readUsageEvents as readUsageEvents3,
|
|
10003
|
-
resolveHaivePaths as
|
|
10341
|
+
resolveHaivePaths as resolveHaivePaths34,
|
|
10004
10342
|
serializeMemory as serializeMemory24
|
|
10005
10343
|
} from "@hiveai/core";
|
|
10006
10344
|
var SEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
@@ -10013,8 +10351,8 @@ function registerMemorySuggest(memory2) {
|
|
|
10013
10351
|
memory2.command("suggest").description(
|
|
10014
10352
|
"Suggest memories to create based on recurring search queries in the usage log.\n\n Use --auto-save to draft the top-N suggestions as draft memories. They land\n in personal scope by default with status=draft, ready for you to edit and promote."
|
|
10015
10353
|
).option("--since <window>", "ISO date or relative (e.g. '7d', '24h')", "30d").option("--min <count>", "minimum repeat count to surface a query", "2").option("--top-n <n>", "with --auto-save, draft this many top suggestions", "3").option("--scope <scope>", "with --auto-save, scope of drafted memories (personal | team)", "personal").option("--auto-save", "draft top-N suggestions as draft memories on disk", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10016
|
-
const root =
|
|
10017
|
-
const paths =
|
|
10354
|
+
const root = findProjectRoot38(opts.dir);
|
|
10355
|
+
const paths = resolveHaivePaths34(root);
|
|
10018
10356
|
const events = await readUsageEvents3(paths);
|
|
10019
10357
|
if (events.length === 0) {
|
|
10020
10358
|
if (opts.json) {
|
|
@@ -10060,7 +10398,7 @@ function registerMemorySuggest(memory2) {
|
|
|
10060
10398
|
}
|
|
10061
10399
|
const created = [];
|
|
10062
10400
|
const skipped = [];
|
|
10063
|
-
const existing =
|
|
10401
|
+
const existing = existsSync58(paths.memoriesDir) ? await loadMemoriesFromDir30(paths.memoriesDir) : [];
|
|
10064
10402
|
for (const s of top) {
|
|
10065
10403
|
const slug = slugify(s.query);
|
|
10066
10404
|
if (!slug) {
|
|
@@ -10083,13 +10421,13 @@ function registerMemorySuggest(memory2) {
|
|
|
10083
10421
|
fm.status = "draft";
|
|
10084
10422
|
const body = renderTemplate(s);
|
|
10085
10423
|
const file = memoryFilePath10(paths, fm.scope, fm.id, fm.module);
|
|
10086
|
-
await
|
|
10087
|
-
if (
|
|
10088
|
-
skipped.push({ query: s.query, reason: `file already exists at ${
|
|
10424
|
+
await mkdir18(path39.dirname(file), { recursive: true });
|
|
10425
|
+
if (existsSync58(file)) {
|
|
10426
|
+
skipped.push({ query: s.query, reason: `file already exists at ${path39.relative(root, file)}` });
|
|
10089
10427
|
continue;
|
|
10090
10428
|
}
|
|
10091
|
-
await
|
|
10092
|
-
created.push({ id: fm.id, file:
|
|
10429
|
+
await writeFile29(file, serializeMemory24({ frontmatter: fm, body }), "utf8");
|
|
10430
|
+
created.push({ id: fm.id, file: path39.relative(root, file), query: s.query });
|
|
10093
10431
|
}
|
|
10094
10432
|
if (opts.json) {
|
|
10095
10433
|
console.log(JSON.stringify({ created, skipped }, null, 2));
|
|
@@ -10182,16 +10520,16 @@ function truncate2(text, max) {
|
|
|
10182
10520
|
}
|
|
10183
10521
|
|
|
10184
10522
|
// src/commands/memory-archive.ts
|
|
10185
|
-
import { existsSync as
|
|
10186
|
-
import { writeFile as
|
|
10187
|
-
import
|
|
10523
|
+
import { existsSync as existsSync59 } from "fs";
|
|
10524
|
+
import { writeFile as writeFile30 } from "fs/promises";
|
|
10525
|
+
import path40 from "path";
|
|
10188
10526
|
import "commander";
|
|
10189
10527
|
import {
|
|
10190
|
-
findProjectRoot as
|
|
10528
|
+
findProjectRoot as findProjectRoot39,
|
|
10191
10529
|
getUsage as getUsage18,
|
|
10192
10530
|
loadMemoriesFromDir as loadMemoriesFromDir31,
|
|
10193
10531
|
loadUsageIndex as loadUsageIndex24,
|
|
10194
|
-
resolveHaivePaths as
|
|
10532
|
+
resolveHaivePaths as resolveHaivePaths35,
|
|
10195
10533
|
serializeMemory as serializeMemory25
|
|
10196
10534
|
} from "@hiveai/core";
|
|
10197
10535
|
var MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
|
|
@@ -10199,9 +10537,9 @@ function registerMemoryArchive(memory2) {
|
|
|
10199
10537
|
memory2.command("archive").description(
|
|
10200
10538
|
"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."
|
|
10201
10539
|
).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) => {
|
|
10202
|
-
const root =
|
|
10203
|
-
const paths =
|
|
10204
|
-
if (!
|
|
10540
|
+
const root = findProjectRoot39(opts.dir);
|
|
10541
|
+
const paths = resolveHaivePaths35(root);
|
|
10542
|
+
if (!existsSync59(paths.memoriesDir)) {
|
|
10205
10543
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
10206
10544
|
process.exitCode = 1;
|
|
10207
10545
|
return;
|
|
@@ -10222,7 +10560,7 @@ function registerMemoryArchive(memory2) {
|
|
|
10222
10560
|
if (typeFilter && fm.type !== typeFilter) continue;
|
|
10223
10561
|
if (fm.status === "deprecated" || fm.status === "rejected") continue;
|
|
10224
10562
|
const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
|
|
10225
|
-
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !
|
|
10563
|
+
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync59(path40.join(paths.root, p)));
|
|
10226
10564
|
const isAnchorless = !hasAnyAnchor;
|
|
10227
10565
|
if (!isAnchorless && !allPathsGone) continue;
|
|
10228
10566
|
const u = getUsage18(usage, fm.id);
|
|
@@ -10270,7 +10608,7 @@ function registerMemoryArchive(memory2) {
|
|
|
10270
10608
|
if (!found) continue;
|
|
10271
10609
|
const fm = { ...found.memory.frontmatter, status: "deprecated" };
|
|
10272
10610
|
try {
|
|
10273
|
-
await
|
|
10611
|
+
await writeFile30(c.filePath, serializeMemory25({ frontmatter: fm, body: found.memory.body }), "utf8");
|
|
10274
10612
|
archived++;
|
|
10275
10613
|
} catch (err) {
|
|
10276
10614
|
if (!opts.json) {
|
|
@@ -10296,31 +10634,31 @@ function parseDays(input) {
|
|
|
10296
10634
|
}
|
|
10297
10635
|
|
|
10298
10636
|
// src/commands/doctor.ts
|
|
10299
|
-
import { existsSync as
|
|
10637
|
+
import { existsSync as existsSync60 } from "fs";
|
|
10300
10638
|
import { stat } from "fs/promises";
|
|
10301
|
-
import
|
|
10639
|
+
import path41 from "path";
|
|
10302
10640
|
import { execSync as execSync3 } from "child_process";
|
|
10303
10641
|
import "commander";
|
|
10304
10642
|
import {
|
|
10305
10643
|
codeMapPath as codeMapPath2,
|
|
10306
|
-
findProjectRoot as
|
|
10644
|
+
findProjectRoot as findProjectRoot40,
|
|
10307
10645
|
getUsage as getUsage19,
|
|
10308
10646
|
loadCodeMap as loadCodeMap5,
|
|
10309
10647
|
loadConfig as loadConfig7,
|
|
10310
10648
|
loadMemoriesFromDir as loadMemoriesFromDir32,
|
|
10311
10649
|
loadUsageIndex as loadUsageIndex25,
|
|
10312
10650
|
readUsageEvents as readUsageEvents4,
|
|
10313
|
-
resolveHaivePaths as
|
|
10651
|
+
resolveHaivePaths as resolveHaivePaths36
|
|
10314
10652
|
} from "@hiveai/core";
|
|
10315
10653
|
var MS_PER_DAY3 = 24 * 60 * 60 * 1e3;
|
|
10316
10654
|
function registerDoctor(program2) {
|
|
10317
10655
|
program2.command("doctor").description(
|
|
10318
10656
|
"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."
|
|
10319
10657
|
).option("--json", "emit JSON instead of human-readable output", false).option("--fix", "include suggested fix commands in human output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10320
|
-
const root =
|
|
10321
|
-
const paths =
|
|
10658
|
+
const root = findProjectRoot40(opts.dir);
|
|
10659
|
+
const paths = resolveHaivePaths36(root);
|
|
10322
10660
|
const findings = [];
|
|
10323
|
-
if (!
|
|
10661
|
+
if (!existsSync60(paths.haiveDir)) {
|
|
10324
10662
|
findings.push({
|
|
10325
10663
|
severity: "error",
|
|
10326
10664
|
code: "not-initialized",
|
|
@@ -10329,7 +10667,7 @@ function registerDoctor(program2) {
|
|
|
10329
10667
|
});
|
|
10330
10668
|
return emit(findings, opts);
|
|
10331
10669
|
}
|
|
10332
|
-
if (!
|
|
10670
|
+
if (!existsSync60(paths.projectContext)) {
|
|
10333
10671
|
findings.push({
|
|
10334
10672
|
severity: "warn",
|
|
10335
10673
|
code: "no-project-context",
|
|
@@ -10337,8 +10675,8 @@ function registerDoctor(program2) {
|
|
|
10337
10675
|
fix: "haive init"
|
|
10338
10676
|
});
|
|
10339
10677
|
} else {
|
|
10340
|
-
const { readFile:
|
|
10341
|
-
const content = await
|
|
10678
|
+
const { readFile: readFile18 } = await import("fs/promises");
|
|
10679
|
+
const content = await readFile18(paths.projectContext, "utf8");
|
|
10342
10680
|
const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
|
|
10343
10681
|
if (isTemplate) {
|
|
10344
10682
|
findings.push({
|
|
@@ -10349,7 +10687,7 @@ function registerDoctor(program2) {
|
|
|
10349
10687
|
});
|
|
10350
10688
|
}
|
|
10351
10689
|
}
|
|
10352
|
-
const memories =
|
|
10690
|
+
const memories = existsSync60(paths.memoriesDir) ? await loadMemoriesFromDir32(paths.memoriesDir) : [];
|
|
10353
10691
|
const now = Date.now();
|
|
10354
10692
|
if (memories.length === 0) {
|
|
10355
10693
|
findings.push({
|
|
@@ -10460,12 +10798,12 @@ function registerDoctor(program2) {
|
|
|
10460
10798
|
}
|
|
10461
10799
|
const config = await loadConfig7(paths);
|
|
10462
10800
|
if (config.enforcement?.requireBriefingFirst) {
|
|
10463
|
-
const claudeSettings =
|
|
10801
|
+
const claudeSettings = path41.join(root, ".claude", "settings.local.json");
|
|
10464
10802
|
let hasClaudeEnforcement = false;
|
|
10465
|
-
if (
|
|
10803
|
+
if (existsSync60(claudeSettings)) {
|
|
10466
10804
|
try {
|
|
10467
|
-
const { readFile:
|
|
10468
|
-
const raw = await
|
|
10805
|
+
const { readFile: readFile18 } = await import("fs/promises");
|
|
10806
|
+
const raw = await readFile18(claudeSettings, "utf8");
|
|
10469
10807
|
hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
|
|
10470
10808
|
} catch {
|
|
10471
10809
|
hasClaudeEnforcement = false;
|
|
@@ -10494,7 +10832,7 @@ function registerDoctor(program2) {
|
|
|
10494
10832
|
timeout: 3e3,
|
|
10495
10833
|
stdio: ["ignore", "pipe", "ignore"]
|
|
10496
10834
|
}).trim();
|
|
10497
|
-
const cliVersion = "0.9.
|
|
10835
|
+
const cliVersion = "0.9.12";
|
|
10498
10836
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
10499
10837
|
findings.push({
|
|
10500
10838
|
severity: "warn",
|
|
@@ -10543,22 +10881,22 @@ function isSearchTool(name) {
|
|
|
10543
10881
|
}
|
|
10544
10882
|
|
|
10545
10883
|
// src/commands/playback.ts
|
|
10546
|
-
import { existsSync as
|
|
10884
|
+
import { existsSync as existsSync61 } from "fs";
|
|
10547
10885
|
import "commander";
|
|
10548
10886
|
import {
|
|
10549
|
-
findProjectRoot as
|
|
10887
|
+
findProjectRoot as findProjectRoot41,
|
|
10550
10888
|
loadMemoriesFromDir as loadMemoriesFromDir33,
|
|
10551
10889
|
parseSince as parseSince3,
|
|
10552
10890
|
readUsageEvents as readUsageEvents5,
|
|
10553
|
-
resolveHaivePaths as
|
|
10891
|
+
resolveHaivePaths as resolveHaivePaths37
|
|
10554
10892
|
} from "@hiveai/core";
|
|
10555
10893
|
var MS_PER_MINUTE = 6e4;
|
|
10556
10894
|
function registerPlayback(program2) {
|
|
10557
10895
|
program2.command("playback").description(
|
|
10558
10896
|
"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?'"
|
|
10559
10897
|
).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) => {
|
|
10560
|
-
const root =
|
|
10561
|
-
const paths =
|
|
10898
|
+
const root = findProjectRoot41(opts.dir);
|
|
10899
|
+
const paths = resolveHaivePaths37(root);
|
|
10562
10900
|
const events = await readUsageEvents5(paths);
|
|
10563
10901
|
if (events.length === 0) {
|
|
10564
10902
|
if (opts.json) {
|
|
@@ -10573,7 +10911,7 @@ function registerPlayback(program2) {
|
|
|
10573
10911
|
const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
|
|
10574
10912
|
const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
|
|
10575
10913
|
const sessions = bucketSessions(filtered, gapMs);
|
|
10576
|
-
const all =
|
|
10914
|
+
const all = existsSync61(paths.memoriesDir) ? await loadMemoriesFromDir33(paths.memoriesDir) : [];
|
|
10577
10915
|
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);
|
|
10578
10916
|
const enriched = sessions.map((s, i) => {
|
|
10579
10917
|
const startMs = Date.parse(s.start);
|
|
@@ -10663,8 +11001,8 @@ function truncate3(text, max) {
|
|
|
10663
11001
|
import { spawn as spawn4 } from "child_process";
|
|
10664
11002
|
import "commander";
|
|
10665
11003
|
import {
|
|
10666
|
-
findProjectRoot as
|
|
10667
|
-
resolveHaivePaths as
|
|
11004
|
+
findProjectRoot as findProjectRoot42,
|
|
11005
|
+
resolveHaivePaths as resolveHaivePaths38
|
|
10668
11006
|
} from "@hiveai/core";
|
|
10669
11007
|
function registerPrecommit(program2) {
|
|
10670
11008
|
program2.command("precommit").description(
|
|
@@ -10674,8 +11012,8 @@ function registerPrecommit(program2) {
|
|
|
10674
11012
|
"'any' | 'high-confidence' (default) | 'never' (report only)",
|
|
10675
11013
|
"high-confidence"
|
|
10676
11014
|
).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) => {
|
|
10677
|
-
const root =
|
|
10678
|
-
const paths =
|
|
11015
|
+
const root = findProjectRoot42(opts.dir);
|
|
11016
|
+
const paths = resolveHaivePaths38(root);
|
|
10679
11017
|
const ctx = { paths };
|
|
10680
11018
|
let diff = "";
|
|
10681
11019
|
let touchedPaths = opts.paths ?? [];
|
|
@@ -10767,12 +11105,12 @@ function runCommand3(cmd, args, cwd) {
|
|
|
10767
11105
|
}
|
|
10768
11106
|
|
|
10769
11107
|
// src/commands/welcome.ts
|
|
10770
|
-
import { existsSync as
|
|
11108
|
+
import { existsSync as existsSync63 } from "fs";
|
|
10771
11109
|
import "commander";
|
|
10772
11110
|
import {
|
|
10773
|
-
findProjectRoot as
|
|
11111
|
+
findProjectRoot as findProjectRoot43,
|
|
10774
11112
|
loadMemoriesFromDir as loadMemoriesFromDir34,
|
|
10775
|
-
resolveHaivePaths as
|
|
11113
|
+
resolveHaivePaths as resolveHaivePaths39
|
|
10776
11114
|
} from "@hiveai/core";
|
|
10777
11115
|
var TYPE_RANK = {
|
|
10778
11116
|
decision: 0,
|
|
@@ -10786,9 +11124,9 @@ function registerWelcome(program2) {
|
|
|
10786
11124
|
program2.command("welcome").description(
|
|
10787
11125
|
"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"
|
|
10788
11126
|
).option("--limit <n>", "maximum memories listed", "20").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10789
|
-
const root =
|
|
10790
|
-
const paths =
|
|
10791
|
-
if (!
|
|
11127
|
+
const root = findProjectRoot43(opts.dir);
|
|
11128
|
+
const paths = resolveHaivePaths39(root);
|
|
11129
|
+
if (!existsSync63(paths.memoriesDir)) {
|
|
10792
11130
|
ui.error(`No memories at ${paths.memoriesDir}. Run 'haive init' first.`);
|
|
10793
11131
|
process.exitCode = 1;
|
|
10794
11132
|
return;
|
|
@@ -10829,17 +11167,17 @@ function registerWelcome(program2) {
|
|
|
10829
11167
|
}
|
|
10830
11168
|
|
|
10831
11169
|
// src/commands/memory-lint.ts
|
|
10832
|
-
import { existsSync as
|
|
11170
|
+
import { existsSync as existsSync64 } from "fs";
|
|
10833
11171
|
import "commander";
|
|
10834
11172
|
import {
|
|
10835
|
-
findProjectRoot as
|
|
11173
|
+
findProjectRoot as findProjectRoot44,
|
|
10836
11174
|
loadMemoriesFromDir as loadMemoriesFromDir35,
|
|
10837
|
-
resolveHaivePaths as
|
|
11175
|
+
resolveHaivePaths as resolveHaivePaths40
|
|
10838
11176
|
} from "@hiveai/core";
|
|
10839
11177
|
async function lintMemoriesAsync(root) {
|
|
10840
|
-
const paths =
|
|
11178
|
+
const paths = resolveHaivePaths40(root);
|
|
10841
11179
|
const out = [];
|
|
10842
|
-
if (!
|
|
11180
|
+
if (!existsSync64(paths.memoriesDir)) return out;
|
|
10843
11181
|
const loaded = await loadMemoriesFromDir35(paths.memoriesDir);
|
|
10844
11182
|
const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
|
|
10845
11183
|
for (const { filePath, memory: memory2 } of loaded) {
|
|
@@ -10900,7 +11238,7 @@ function registerMemoryLint(parent) {
|
|
|
10900
11238
|
parent.command("lint").description(
|
|
10901
11239
|
"Heuristic corpus checks (anchors on key types, headings, verbosity). Static analysis only."
|
|
10902
11240
|
).option("--json", "emit findings as JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10903
|
-
const root =
|
|
11241
|
+
const root = findProjectRoot44(opts.dir);
|
|
10904
11242
|
const findings = await lintMemoriesAsync(root);
|
|
10905
11243
|
if (opts.json) {
|
|
10906
11244
|
console.log(JSON.stringify({ findings_count: findings.length, findings }, null, 2));
|
|
@@ -10946,27 +11284,27 @@ function registerMemorySuggestTopic(memory2) {
|
|
|
10946
11284
|
}
|
|
10947
11285
|
|
|
10948
11286
|
// src/commands/resolve-project.ts
|
|
10949
|
-
import
|
|
11287
|
+
import path43 from "path";
|
|
10950
11288
|
import "commander";
|
|
10951
11289
|
import { resolveProjectInfo as resolveProjectInfo2 } from "@hiveai/core";
|
|
10952
11290
|
function registerResolveProject(program2) {
|
|
10953
11291
|
program2.command("resolve-project").description(
|
|
10954
11292
|
"Print JSON for hAIve project root resolution (HAIVE_PROJECT_ROOT, markers, .ai layout)."
|
|
10955
11293
|
).option("-d, --dir <dir>", "working directory", process.cwd()).action((opts) => {
|
|
10956
|
-
const info = resolveProjectInfo2({ cwd:
|
|
11294
|
+
const info = resolveProjectInfo2({ cwd: path43.resolve(opts.dir) });
|
|
10957
11295
|
console.log(JSON.stringify({ ok: true, info }, null, 2));
|
|
10958
11296
|
});
|
|
10959
11297
|
}
|
|
10960
11298
|
|
|
10961
11299
|
// src/commands/runtime-journal.ts
|
|
10962
|
-
import { existsSync as
|
|
10963
|
-
import
|
|
11300
|
+
import { existsSync as existsSync65 } from "fs";
|
|
11301
|
+
import path44 from "path";
|
|
10964
11302
|
import "commander";
|
|
10965
11303
|
import {
|
|
10966
11304
|
appendRuntimeJournalEntry as appendRuntimeJournalEntry3,
|
|
10967
|
-
findProjectRoot as
|
|
11305
|
+
findProjectRoot as findProjectRoot45,
|
|
10968
11306
|
readRuntimeJournalTail as readRuntimeJournalTail2,
|
|
10969
|
-
resolveHaivePaths as
|
|
11307
|
+
resolveHaivePaths as resolveHaivePaths41
|
|
10970
11308
|
} from "@hiveai/core";
|
|
10971
11309
|
function registerRuntime(program2) {
|
|
10972
11310
|
const runtime = program2.command("runtime").description(
|
|
@@ -10974,18 +11312,18 @@ function registerRuntime(program2) {
|
|
|
10974
11312
|
);
|
|
10975
11313
|
const journal = runtime.command("journal").description("Append or read the machine-local session journal (NDJSON)");
|
|
10976
11314
|
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) => {
|
|
10977
|
-
const root =
|
|
10978
|
-
const paths =
|
|
11315
|
+
const root = path44.resolve(opts.dir ?? process.cwd());
|
|
11316
|
+
const paths = resolveHaivePaths41(findProjectRoot45(root));
|
|
10979
11317
|
const raw = opts.kind ?? "note";
|
|
10980
11318
|
const kind = ["note", "session_end", "mcp"].includes(raw) ? raw : "note";
|
|
10981
11319
|
await appendRuntimeJournalEntry3(paths, { kind, message });
|
|
10982
|
-
ui.success(`Appended to ${
|
|
11320
|
+
ui.success(`Appended to ${path44.relative(root, paths.runtimeDir)}/session-journal.ndjson`);
|
|
10983
11321
|
});
|
|
10984
11322
|
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) => {
|
|
10985
|
-
const root =
|
|
10986
|
-
const paths =
|
|
11323
|
+
const root = path44.resolve(opts.dir ?? process.cwd());
|
|
11324
|
+
const paths = resolveHaivePaths41(findProjectRoot45(root));
|
|
10987
11325
|
const limit = Math.min(500, Math.max(1, parseInt(opts.limit, 10) || 30));
|
|
10988
|
-
if (!
|
|
11326
|
+
if (!existsSync65(paths.haiveDir)) {
|
|
10989
11327
|
ui.error("No .ai/ \u2014 run `haive init` first.");
|
|
10990
11328
|
process.exitCode = 1;
|
|
10991
11329
|
return;
|
|
@@ -11000,13 +11338,13 @@ function registerRuntime(program2) {
|
|
|
11000
11338
|
}
|
|
11001
11339
|
|
|
11002
11340
|
// src/commands/memory-timeline.ts
|
|
11003
|
-
import { existsSync as
|
|
11004
|
-
import
|
|
11341
|
+
import { existsSync as existsSync66 } from "fs";
|
|
11342
|
+
import path45 from "path";
|
|
11005
11343
|
import "commander";
|
|
11006
11344
|
import {
|
|
11007
11345
|
collectTimelineEntries as collectTimelineEntries2,
|
|
11008
|
-
findProjectRoot as
|
|
11009
|
-
resolveHaivePaths as
|
|
11346
|
+
findProjectRoot as findProjectRoot46,
|
|
11347
|
+
resolveHaivePaths as resolveHaivePaths42
|
|
11010
11348
|
} from "@hiveai/core";
|
|
11011
11349
|
function registerMemoryTimeline(memory2) {
|
|
11012
11350
|
memory2.command("timeline").description(
|
|
@@ -11017,9 +11355,9 @@ function registerMemoryTimeline(memory2) {
|
|
|
11017
11355
|
process.exitCode = 1;
|
|
11018
11356
|
return;
|
|
11019
11357
|
}
|
|
11020
|
-
const root =
|
|
11021
|
-
const paths =
|
|
11022
|
-
if (!
|
|
11358
|
+
const root = path45.resolve(opts.dir ?? process.cwd());
|
|
11359
|
+
const paths = resolveHaivePaths42(findProjectRoot46(root));
|
|
11360
|
+
if (!existsSync66(paths.memoriesDir)) {
|
|
11023
11361
|
ui.error("No memories \u2014 run `haive init`.");
|
|
11024
11362
|
process.exitCode = 1;
|
|
11025
11363
|
return;
|
|
@@ -11037,14 +11375,14 @@ function registerMemoryTimeline(memory2) {
|
|
|
11037
11375
|
}
|
|
11038
11376
|
|
|
11039
11377
|
// src/commands/memory-conflict-candidates.ts
|
|
11040
|
-
import { existsSync as
|
|
11041
|
-
import
|
|
11378
|
+
import { existsSync as existsSync67 } from "fs";
|
|
11379
|
+
import path46 from "path";
|
|
11042
11380
|
import "commander";
|
|
11043
11381
|
import {
|
|
11044
11382
|
findLexicalConflictPairs as findLexicalConflictPairs2,
|
|
11045
11383
|
findTopicStatusConflictPairs as findTopicStatusConflictPairs2,
|
|
11046
|
-
findProjectRoot as
|
|
11047
|
-
resolveHaivePaths as
|
|
11384
|
+
findProjectRoot as findProjectRoot47,
|
|
11385
|
+
resolveHaivePaths as resolveHaivePaths43
|
|
11048
11386
|
} from "@hiveai/core";
|
|
11049
11387
|
function parseTypes(csv) {
|
|
11050
11388
|
const allowed = ["decision", "architecture", "convention", "gotcha"];
|
|
@@ -11060,9 +11398,9 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
11060
11398
|
"decision,architecture,convention,gotcha (lexical scan)",
|
|
11061
11399
|
"decision,architecture"
|
|
11062
11400
|
).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) => {
|
|
11063
|
-
const root =
|
|
11064
|
-
const paths =
|
|
11065
|
-
if (!
|
|
11401
|
+
const root = path46.resolve(opts.dir ?? process.cwd());
|
|
11402
|
+
const paths = resolveHaivePaths43(findProjectRoot47(root));
|
|
11403
|
+
if (!existsSync67(paths.memoriesDir)) {
|
|
11066
11404
|
ui.error("No memories \u2014 run `haive init`.");
|
|
11067
11405
|
process.exitCode = 1;
|
|
11068
11406
|
return;
|
|
@@ -11098,18 +11436,20 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
11098
11436
|
|
|
11099
11437
|
// src/commands/enforce.ts
|
|
11100
11438
|
import { spawn as spawn5 } from "child_process";
|
|
11101
|
-
import { existsSync as
|
|
11102
|
-
import { chmod as chmod2, mkdir as
|
|
11103
|
-
import
|
|
11439
|
+
import { existsSync as existsSync68 } from "fs";
|
|
11440
|
+
import { chmod as chmod2, mkdir as mkdir19, readFile as readFile17, rm as rm3, writeFile as writeFile31 } from "fs/promises";
|
|
11441
|
+
import path47 from "path";
|
|
11104
11442
|
import "commander";
|
|
11105
11443
|
import {
|
|
11106
|
-
findProjectRoot as
|
|
11444
|
+
findProjectRoot as findProjectRoot48,
|
|
11107
11445
|
hasRecentBriefingMarker,
|
|
11108
11446
|
isFreshIsoDate,
|
|
11109
11447
|
loadConfig as loadConfig8,
|
|
11110
11448
|
loadMemoriesFromDir as loadMemoriesFromDir36,
|
|
11449
|
+
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths6,
|
|
11450
|
+
readRecentBriefingMarker,
|
|
11111
11451
|
resolveBriefingBudget as resolveBriefingBudget3,
|
|
11112
|
-
resolveHaivePaths as
|
|
11452
|
+
resolveHaivePaths as resolveHaivePaths44,
|
|
11113
11453
|
saveConfig as saveConfig3,
|
|
11114
11454
|
SESSION_RECAP_TTL_MS,
|
|
11115
11455
|
verifyAnchor as verifyAnchor4,
|
|
@@ -11122,9 +11462,9 @@ function registerEnforce(program2) {
|
|
|
11122
11462
|
"Agent-agnostic enforcement helpers: install policy gates, report status, and block unsafe workflows."
|
|
11123
11463
|
);
|
|
11124
11464
|
enforce.command("install").description("Install hAIve enforcement across MCP config, git hooks, CI template, and supported client hooks.").option("-d, --dir <dir>", "project root").option("--no-git", "skip git pre-commit/pre-push enforcement hooks").option("--no-claude", "skip Claude Code hooks").option("--no-ci", "skip GitHub Actions enforcement workflow").action(async (opts) => {
|
|
11125
|
-
const root =
|
|
11126
|
-
const paths =
|
|
11127
|
-
await
|
|
11465
|
+
const root = findProjectRoot48(opts.dir);
|
|
11466
|
+
const paths = resolveHaivePaths44(root);
|
|
11467
|
+
await mkdir19(paths.haiveDir, { recursive: true });
|
|
11128
11468
|
const current = await loadConfig8(paths);
|
|
11129
11469
|
await saveConfig3(paths, {
|
|
11130
11470
|
...current,
|
|
@@ -11135,7 +11475,11 @@ function registerEnforce(program2) {
|
|
|
11135
11475
|
requireSessionRecap: true,
|
|
11136
11476
|
requireMemoryVerify: true,
|
|
11137
11477
|
blockStaleDecisionChanges: true,
|
|
11138
|
-
|
|
11478
|
+
requireDecisionCoverage: true,
|
|
11479
|
+
scoreThreshold: 85,
|
|
11480
|
+
cleanupGeneratedArtifacts: true,
|
|
11481
|
+
toolProfile: "enforcement",
|
|
11482
|
+
policyPacks: ["architecture", "gotchas", "security", "domain", "release"]
|
|
11139
11483
|
}
|
|
11140
11484
|
});
|
|
11141
11485
|
ui.success("hAIve strict enforcement enabled in .ai/haive.config.json");
|
|
@@ -11144,7 +11488,7 @@ function registerEnforce(program2) {
|
|
|
11144
11488
|
if (opts.claude !== false) {
|
|
11145
11489
|
try {
|
|
11146
11490
|
const result = await installClaudeHooksAtPath(defaultClaudeSettingsPath("project", root));
|
|
11147
|
-
ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${
|
|
11491
|
+
ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${path47.relative(root, result.settingsPath)})`);
|
|
11148
11492
|
} catch (err) {
|
|
11149
11493
|
ui.warn(`Claude Code hooks not installed: ${err instanceof Error ? err.message : String(err)}`);
|
|
11150
11494
|
}
|
|
@@ -11162,6 +11506,23 @@ function registerEnforce(program2) {
|
|
|
11162
11506
|
printReport(report, Boolean(opts.json));
|
|
11163
11507
|
if (report.should_block) process.exit(2);
|
|
11164
11508
|
});
|
|
11509
|
+
enforce.command("cleanup").description("Remove generated hAIve runtime/cache artifacts that should not appear in commits.").option("-d, --dir <dir>", "project root").option("--dry-run", "print what would be removed without deleting", false).action(async (opts) => {
|
|
11510
|
+
const root = findProjectRoot48(opts.dir);
|
|
11511
|
+
const paths = resolveHaivePaths44(root);
|
|
11512
|
+
const targets = [
|
|
11513
|
+
path47.join(paths.haiveDir, ".cache"),
|
|
11514
|
+
path47.join(paths.haiveDir, ".runtime")
|
|
11515
|
+
];
|
|
11516
|
+
for (const target of targets) {
|
|
11517
|
+
if (!existsSync68(target)) continue;
|
|
11518
|
+
const rel = path47.relative(root, target);
|
|
11519
|
+
if (opts.dryRun) ui.info(`would remove ${rel}`);
|
|
11520
|
+
else {
|
|
11521
|
+
await rm3(target, { recursive: true, force: true });
|
|
11522
|
+
ui.success(`removed ${rel}`);
|
|
11523
|
+
}
|
|
11524
|
+
}
|
|
11525
|
+
});
|
|
11165
11526
|
enforce.command("ci").description("CI entrypoint: fail if the repository violates hAIve enforcement policy.").option("-d, --dir <dir>", "project root").option("--json", "emit JSON", false).action(async (opts) => {
|
|
11166
11527
|
const report = await buildEnforcementReport(opts.dir, "ci");
|
|
11167
11528
|
printReport(report, Boolean(opts.json));
|
|
@@ -11171,16 +11532,11 @@ function registerEnforce(program2) {
|
|
|
11171
11532
|
const payload = await readHookPayload();
|
|
11172
11533
|
const root = resolveRoot(opts.dir, payload);
|
|
11173
11534
|
if (!root) return;
|
|
11174
|
-
const paths =
|
|
11175
|
-
if (!
|
|
11176
|
-
await
|
|
11535
|
+
const paths = resolveHaivePaths44(root);
|
|
11536
|
+
if (!existsSync68(paths.haiveDir)) return;
|
|
11537
|
+
await mkdir19(paths.runtimeDir, { recursive: true });
|
|
11177
11538
|
const sessionId = opts.sessionId ?? payload.session_id;
|
|
11178
11539
|
const task = opts.task ?? payload.prompt ?? "Start an AI coding session in this hAIve-initialized project.";
|
|
11179
|
-
await writeBriefingMarker2(paths, {
|
|
11180
|
-
sessionId,
|
|
11181
|
-
task,
|
|
11182
|
-
source: opts.source ?? "claude-session-start"
|
|
11183
|
-
});
|
|
11184
11540
|
const budget = resolveBriefingBudget3("quick", {
|
|
11185
11541
|
max_tokens: 2500,
|
|
11186
11542
|
max_memories: 5,
|
|
@@ -11204,6 +11560,12 @@ function registerEnforce(program2) {
|
|
|
11204
11560
|
},
|
|
11205
11561
|
{ paths }
|
|
11206
11562
|
);
|
|
11563
|
+
await writeBriefingMarker2(paths, {
|
|
11564
|
+
sessionId,
|
|
11565
|
+
task,
|
|
11566
|
+
source: opts.source ?? "claude-session-start",
|
|
11567
|
+
memoryIds: briefing.memories.map((m) => m.id)
|
|
11568
|
+
});
|
|
11207
11569
|
console.log("hAIve briefing loaded. Agents must consult this before editing.");
|
|
11208
11570
|
if (briefing.last_session) {
|
|
11209
11571
|
console.log(`
|
|
@@ -11232,8 +11594,8 @@ ${briefing.project_context.content.slice(0, 1800)}`);
|
|
|
11232
11594
|
const payload = await readHookPayload();
|
|
11233
11595
|
const root = resolveRoot(opts.dir, payload);
|
|
11234
11596
|
if (!root) return;
|
|
11235
|
-
const paths =
|
|
11236
|
-
if (!
|
|
11597
|
+
const paths = resolveHaivePaths44(root);
|
|
11598
|
+
if (!existsSync68(paths.haiveDir)) return;
|
|
11237
11599
|
if (!isWriteLikeTool(payload)) return;
|
|
11238
11600
|
const ok = await hasRecentBriefingMarker(paths, payload.session_id);
|
|
11239
11601
|
if (ok) return;
|
|
@@ -11255,9 +11617,9 @@ ${briefing.project_context.content.slice(0, 1800)}`);
|
|
|
11255
11617
|
});
|
|
11256
11618
|
}
|
|
11257
11619
|
async function runWithEnforcement(command, args, opts) {
|
|
11258
|
-
const root =
|
|
11259
|
-
const paths =
|
|
11260
|
-
if (!
|
|
11620
|
+
const root = findProjectRoot48(opts.dir);
|
|
11621
|
+
const paths = resolveHaivePaths44(root);
|
|
11622
|
+
if (!existsSync68(paths.haiveDir)) {
|
|
11261
11623
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
11262
11624
|
process.exit(1);
|
|
11263
11625
|
}
|
|
@@ -11276,7 +11638,7 @@ async function runWithEnforcement(command, args, opts) {
|
|
|
11276
11638
|
process.exit(2);
|
|
11277
11639
|
}
|
|
11278
11640
|
ui.info(`hAIve briefing marker created for wrapped agent session: ${sessionId}`);
|
|
11279
|
-
ui.info(`Briefing written to ${
|
|
11641
|
+
ui.info(`Briefing written to ${path47.relative(root, briefingFile)} and exported as HAIVE_BRIEFING_FILE`);
|
|
11280
11642
|
const child = spawn5(command, args, {
|
|
11281
11643
|
cwd: root,
|
|
11282
11644
|
stdio: "inherit",
|
|
@@ -11319,9 +11681,15 @@ async function writeWrapperBriefing(paths, sessionId, task) {
|
|
|
11319
11681
|
min_semantic_score: 0.25,
|
|
11320
11682
|
budget_preset: "quick"
|
|
11321
11683
|
}, { paths });
|
|
11322
|
-
|
|
11323
|
-
|
|
11324
|
-
|
|
11684
|
+
await writeBriefingMarker2(paths, {
|
|
11685
|
+
sessionId,
|
|
11686
|
+
task,
|
|
11687
|
+
source: "haive-run",
|
|
11688
|
+
memoryIds: briefing.memories.map((m) => m.id)
|
|
11689
|
+
});
|
|
11690
|
+
const dir = path47.join(paths.runtimeDir, "enforcement", "briefings");
|
|
11691
|
+
await mkdir19(dir, { recursive: true });
|
|
11692
|
+
const file = path47.join(dir, `${sessionId}.md`);
|
|
11325
11693
|
const parts = [
|
|
11326
11694
|
"# hAIve Briefing",
|
|
11327
11695
|
"",
|
|
@@ -11339,13 +11707,13 @@ async function writeWrapperBriefing(paths, sessionId, task) {
|
|
|
11339
11707
|
if (briefing.setup_warnings.length > 0) {
|
|
11340
11708
|
parts.push("", "## Setup Warnings", ...briefing.setup_warnings.map((w) => `- ${w}`));
|
|
11341
11709
|
}
|
|
11342
|
-
await
|
|
11710
|
+
await writeFile31(file, parts.join("\n") + "\n", "utf8");
|
|
11343
11711
|
return file;
|
|
11344
11712
|
}
|
|
11345
11713
|
async function buildEnforcementReport(dir, stage, sessionId) {
|
|
11346
|
-
const root =
|
|
11347
|
-
const paths =
|
|
11348
|
-
const initialized =
|
|
11714
|
+
const root = findProjectRoot48(dir);
|
|
11715
|
+
const paths = resolveHaivePaths44(root);
|
|
11716
|
+
const initialized = existsSync68(paths.haiveDir);
|
|
11349
11717
|
const config = initialized ? await loadConfig8(paths) : {};
|
|
11350
11718
|
const mode = config.enforcement?.mode ?? "strict";
|
|
11351
11719
|
const findings = [];
|
|
@@ -11354,12 +11722,14 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
11354
11722
|
root,
|
|
11355
11723
|
initialized,
|
|
11356
11724
|
mode,
|
|
11725
|
+
score: buildScore([], config.enforcement?.scoreThreshold),
|
|
11357
11726
|
should_block: true,
|
|
11358
11727
|
findings: [{
|
|
11359
11728
|
severity: "error",
|
|
11360
11729
|
code: "not-initialized",
|
|
11361
11730
|
message: "This repository is not initialized with hAIve.",
|
|
11362
|
-
fix: "Run `haive init` or `haive enforce install`."
|
|
11731
|
+
fix: "Run `haive init` or `haive enforce install`.",
|
|
11732
|
+
impact: 100
|
|
11363
11733
|
}]
|
|
11364
11734
|
};
|
|
11365
11735
|
}
|
|
@@ -11368,6 +11738,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
11368
11738
|
root,
|
|
11369
11739
|
initialized,
|
|
11370
11740
|
mode,
|
|
11741
|
+
score: buildScore([], config.enforcement?.scoreThreshold),
|
|
11371
11742
|
should_block: false,
|
|
11372
11743
|
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
11373
11744
|
};
|
|
@@ -11378,42 +11749,67 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
11378
11749
|
severity: "error",
|
|
11379
11750
|
code: "briefing-missing",
|
|
11380
11751
|
message: "No recent hAIve briefing marker was found for this workflow.",
|
|
11381
|
-
fix: 'Run `haive briefing --task "..."`, `haive enforce session-start`, or wrap the agent with `haive run -- <agent>`.'
|
|
11752
|
+
fix: 'Run `haive briefing --task "..."`, `haive enforce session-start`, or wrap the agent with `haive run -- <agent>`.',
|
|
11753
|
+
impact: 35
|
|
11382
11754
|
});
|
|
11383
11755
|
}
|
|
11384
11756
|
if (config.enforcement?.requireSessionRecap !== false && (stage === "pre-push" || stage === "ci")) {
|
|
11385
11757
|
const hasRecap = await hasRecentSessionRecap(paths);
|
|
11386
|
-
findings.push(hasRecap ? { severity: "ok", code: "session-recap-present", message: "A recent session_recap memory exists." } : {
|
|
11758
|
+
findings.push(hasRecap ? { severity: "ok", code: "session-recap-present", message: "A recent session_recap memory exists." } : stage === "ci" ? {
|
|
11759
|
+
severity: "warn",
|
|
11760
|
+
code: "session-recap-missing",
|
|
11761
|
+
message: "No recent session_recap memory was found. CI reports this as a warning because personal recaps are usually not committed.",
|
|
11762
|
+
fix: "Run `haive session end --scope team --goal ... --accomplished ...` if you want a team recap visible in CI.",
|
|
11763
|
+
impact: 5
|
|
11764
|
+
} : {
|
|
11387
11765
|
severity: "error",
|
|
11388
11766
|
code: "session-recap-missing",
|
|
11389
11767
|
message: "No recent session_recap memory was found.",
|
|
11390
|
-
fix: "Run `haive session end --goal ... --accomplished ...` before pushing."
|
|
11768
|
+
fix: "Run `haive session end --goal ... --accomplished ...` before pushing.",
|
|
11769
|
+
impact: 20
|
|
11391
11770
|
});
|
|
11392
11771
|
}
|
|
11393
11772
|
if (config.enforcement?.requireMemoryVerify !== false) {
|
|
11394
11773
|
findings.push(...await verifyMemoryPolicy(paths, config));
|
|
11395
11774
|
}
|
|
11775
|
+
if (config.enforcement?.requireDecisionCoverage !== false) {
|
|
11776
|
+
findings.push(...await verifyDecisionCoverage(paths, stage, sessionId));
|
|
11777
|
+
}
|
|
11396
11778
|
if (stage === "pre-commit" || stage === "ci") {
|
|
11397
11779
|
findings.push(...await runPrecommitPolicy(paths));
|
|
11398
11780
|
}
|
|
11781
|
+
if (config.enforcement?.cleanupGeneratedArtifacts !== false) {
|
|
11782
|
+
findings.push(...await findGeneratedArtifacts(paths));
|
|
11783
|
+
}
|
|
11784
|
+
const score = buildScore(findings, config.enforcement?.scoreThreshold);
|
|
11785
|
+
if (score.score < score.threshold) {
|
|
11786
|
+
findings.push({
|
|
11787
|
+
severity: "error",
|
|
11788
|
+
code: "enforcement-score-below-threshold",
|
|
11789
|
+
message: `Enforcement score ${score.score}% is below required threshold ${score.threshold}%.`,
|
|
11790
|
+
fix: "Load the relevant briefing, address policy findings, then rerun `haive enforce check`.",
|
|
11791
|
+
impact: 0
|
|
11792
|
+
});
|
|
11793
|
+
}
|
|
11399
11794
|
const hasErrors = findings.some((f) => f.severity === "error");
|
|
11400
11795
|
return {
|
|
11401
11796
|
root,
|
|
11402
11797
|
initialized,
|
|
11403
11798
|
mode,
|
|
11799
|
+
score: buildScore(findings, config.enforcement?.scoreThreshold),
|
|
11404
11800
|
should_block: mode === "strict" && hasErrors,
|
|
11405
11801
|
findings
|
|
11406
11802
|
};
|
|
11407
11803
|
}
|
|
11408
11804
|
async function hasRecentSessionRecap(paths) {
|
|
11409
|
-
if (!
|
|
11805
|
+
if (!existsSync68(paths.memoriesDir)) return false;
|
|
11410
11806
|
const all = await loadMemoriesFromDir36(paths.memoriesDir);
|
|
11411
11807
|
return all.some(
|
|
11412
11808
|
({ memory: memory2 }) => memory2.frontmatter.type === "session_recap" && memory2.frontmatter.status !== "rejected" && isFreshIsoDate(memory2.frontmatter.created_at, SESSION_RECAP_TTL_MS)
|
|
11413
11809
|
);
|
|
11414
11810
|
}
|
|
11415
11811
|
async function verifyMemoryPolicy(paths, config) {
|
|
11416
|
-
if (!
|
|
11812
|
+
if (!existsSync68(paths.memoriesDir)) return [];
|
|
11417
11813
|
const all = await loadMemoriesFromDir36(paths.memoriesDir);
|
|
11418
11814
|
const findings = [];
|
|
11419
11815
|
const staleImportant = [];
|
|
@@ -11444,11 +11840,51 @@ async function verifyMemoryPolicy(paths, config) {
|
|
|
11444
11840
|
severity: "error",
|
|
11445
11841
|
code: "stale-important-memories",
|
|
11446
11842
|
message: `${staleImportant.length} important anchored memories are stale: ${staleImportant.slice(0, 8).join(", ")}`,
|
|
11447
|
-
fix: "Run `haive memory verify --update`, then update or delete stale decisions/gotchas before merging."
|
|
11843
|
+
fix: "Run `haive memory verify --update`, then update or delete stale decisions/gotchas before merging.",
|
|
11844
|
+
impact: 40
|
|
11448
11845
|
});
|
|
11449
11846
|
}
|
|
11450
11847
|
return findings;
|
|
11451
11848
|
}
|
|
11849
|
+
async function verifyDecisionCoverage(paths, stage, sessionId) {
|
|
11850
|
+
if (!existsSync68(paths.memoriesDir)) return [];
|
|
11851
|
+
const changedFiles = await getChangedFiles(paths.root, stage);
|
|
11852
|
+
if (changedFiles.length === 0) {
|
|
11853
|
+
return [{ severity: "info", code: "decision-coverage-no-changes", message: "No changed files to match against policy memories." }];
|
|
11854
|
+
}
|
|
11855
|
+
const all = await loadMemoriesFromDir36(paths.memoriesDir);
|
|
11856
|
+
const policyTypes = /* @__PURE__ */ new Set(["decision", "gotcha", "architecture", "convention"]);
|
|
11857
|
+
const relevant = all.map(({ memory: memory2 }) => memory2).filter((memory2) => {
|
|
11858
|
+
const fm = memory2.frontmatter;
|
|
11859
|
+
if (!policyTypes.has(fm.type)) return false;
|
|
11860
|
+
if (fm.status === "rejected" || fm.status === "deprecated" || fm.status === "stale") return false;
|
|
11861
|
+
return memoryMatchesAnchorPaths6(memory2, changedFiles);
|
|
11862
|
+
});
|
|
11863
|
+
if (relevant.length === 0) {
|
|
11864
|
+
return [{
|
|
11865
|
+
severity: "ok",
|
|
11866
|
+
code: "decision-coverage-none-required",
|
|
11867
|
+
message: `No anchored decisions or policies matched ${changedFiles.length} changed file(s).`
|
|
11868
|
+
}];
|
|
11869
|
+
}
|
|
11870
|
+
const marker = await readRecentBriefingMarker(paths, sessionId);
|
|
11871
|
+
const consulted = new Set(marker?.memory_ids ?? []);
|
|
11872
|
+
const missing = relevant.filter((memory2) => !consulted.has(memory2.frontmatter.id));
|
|
11873
|
+
if (missing.length === 0) {
|
|
11874
|
+
return [{
|
|
11875
|
+
severity: "ok",
|
|
11876
|
+
code: "decision-coverage-pass",
|
|
11877
|
+
message: `Relevant decisions/policies were surfaced for ${changedFiles.length} changed file(s): ${relevant.length}/${relevant.length}.`
|
|
11878
|
+
}];
|
|
11879
|
+
}
|
|
11880
|
+
return [{
|
|
11881
|
+
severity: stage === "local" ? "warn" : "error",
|
|
11882
|
+
code: "decision-coverage-missing",
|
|
11883
|
+
message: `${missing.length}/${relevant.length} relevant anchored decisions/policies were not present in the latest briefing: ${missing.slice(0, 6).map((m) => m.frontmatter.id).join(", ")}`,
|
|
11884
|
+
fix: `Run \`haive briefing --files "${changedFiles.slice(0, 10).join(",")}" --task "..."\` before committing.`,
|
|
11885
|
+
impact: Math.min(35, 10 + missing.length * 5)
|
|
11886
|
+
}];
|
|
11887
|
+
}
|
|
11452
11888
|
async function runPrecommitPolicy(paths) {
|
|
11453
11889
|
const staged = await runCommand4("git", ["diff", "--cached", "--name-only"], paths.root).catch(() => "");
|
|
11454
11890
|
const touchedPaths = staged.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
@@ -11473,16 +11909,66 @@ async function runPrecommitPolicy(paths) {
|
|
|
11473
11909
|
severity: "error",
|
|
11474
11910
|
code: "precommit-policy-block",
|
|
11475
11911
|
message: `Pre-commit policy matched ${result.summary.anti_patterns} anti-pattern(s), ${result.summary.stale_anchors} stale anchor(s).`,
|
|
11476
|
-
fix: "Review the hAIve warnings, then update the code or the relevant memories."
|
|
11912
|
+
fix: "Review the hAIve warnings, then update the code or the relevant memories.",
|
|
11913
|
+
impact: 45
|
|
11914
|
+
}];
|
|
11915
|
+
}
|
|
11916
|
+
async function findGeneratedArtifacts(paths) {
|
|
11917
|
+
const dirty = await runCommand4("git", ["status", "--short", "--untracked-files=all"], paths.root).catch(() => "");
|
|
11918
|
+
const generated = dirty.split("\n").map((line) => line.trim()).filter(Boolean).filter(
|
|
11919
|
+
(line) => line.includes(".ai/.cache/") || line.includes(".ai/.runtime/") || line.includes("__pycache__/") || line.endsWith(".pyc")
|
|
11920
|
+
);
|
|
11921
|
+
if (generated.length === 0) {
|
|
11922
|
+
return [{ severity: "ok", code: "generated-artifacts-clean", message: "No generated runtime/cache artifacts are visible to git." }];
|
|
11923
|
+
}
|
|
11924
|
+
return [{
|
|
11925
|
+
severity: "warn",
|
|
11926
|
+
code: "generated-artifacts-visible",
|
|
11927
|
+
message: `${generated.length} generated artifact(s) are visible in git status.`,
|
|
11928
|
+
fix: "Run `haive enforce cleanup`, update .gitignore, or remove test/runtime outputs before committing.",
|
|
11929
|
+
impact: 10
|
|
11477
11930
|
}];
|
|
11478
11931
|
}
|
|
11932
|
+
async function getChangedFiles(root, stage) {
|
|
11933
|
+
const commands = stage === "pre-commit" ? [["diff", "--cached", "--name-only"]] : [
|
|
11934
|
+
["diff", "--cached", "--name-only"],
|
|
11935
|
+
["diff", "--name-only"]
|
|
11936
|
+
];
|
|
11937
|
+
const files = /* @__PURE__ */ new Set();
|
|
11938
|
+
for (const args of commands) {
|
|
11939
|
+
const out = await runCommand4("git", args, root).catch(() => "");
|
|
11940
|
+
for (const line of out.split("\n")) {
|
|
11941
|
+
const file = line.trim();
|
|
11942
|
+
if (file) files.add(file);
|
|
11943
|
+
}
|
|
11944
|
+
}
|
|
11945
|
+
return [...files].filter((file) => !file.startsWith(".ai/.runtime/") && !file.startsWith(".ai/.cache/"));
|
|
11946
|
+
}
|
|
11947
|
+
function buildScore(findings, threshold = 80) {
|
|
11948
|
+
const checks = {
|
|
11949
|
+
total: findings.length,
|
|
11950
|
+
ok: findings.filter((f) => f.severity === "ok").length,
|
|
11951
|
+
warn: findings.filter((f) => f.severity === "warn").length,
|
|
11952
|
+
error: findings.filter((f) => f.severity === "error").length
|
|
11953
|
+
};
|
|
11954
|
+
const penalty = findings.reduce((sum, f) => {
|
|
11955
|
+
if (f.severity === "error") return sum + (f.impact ?? 25);
|
|
11956
|
+
if (f.severity === "warn") return sum + (f.impact ?? 8);
|
|
11957
|
+
return sum;
|
|
11958
|
+
}, 0);
|
|
11959
|
+
return {
|
|
11960
|
+
score: Math.max(0, Math.min(100, 100 - penalty)),
|
|
11961
|
+
threshold,
|
|
11962
|
+
checks
|
|
11963
|
+
};
|
|
11964
|
+
}
|
|
11479
11965
|
async function installGitEnforcement(root) {
|
|
11480
|
-
const hooksDir =
|
|
11481
|
-
if (!
|
|
11966
|
+
const hooksDir = path47.join(root, ".git", "hooks");
|
|
11967
|
+
if (!existsSync68(path47.join(root, ".git"))) {
|
|
11482
11968
|
ui.warn("No .git directory found; git enforcement hooks skipped.");
|
|
11483
11969
|
return;
|
|
11484
11970
|
}
|
|
11485
|
-
await
|
|
11971
|
+
await mkdir19(hooksDir, { recursive: true });
|
|
11486
11972
|
const hooks = [
|
|
11487
11973
|
{
|
|
11488
11974
|
name: "pre-commit",
|
|
@@ -11500,31 +11986,31 @@ haive enforce check --stage pre-push --dir . || exit $?
|
|
|
11500
11986
|
}
|
|
11501
11987
|
];
|
|
11502
11988
|
for (const hook of hooks) {
|
|
11503
|
-
const file =
|
|
11504
|
-
if (
|
|
11505
|
-
const current = await
|
|
11989
|
+
const file = path47.join(hooksDir, hook.name);
|
|
11990
|
+
if (existsSync68(file)) {
|
|
11991
|
+
const current = await readFile17(file, "utf8").catch(() => "");
|
|
11506
11992
|
if (current.includes(ENFORCE_HOOK_MARKER)) {
|
|
11507
|
-
await
|
|
11993
|
+
await writeFile31(file, hook.body, "utf8");
|
|
11508
11994
|
} else {
|
|
11509
|
-
await
|
|
11995
|
+
await writeFile31(file, `${current.trimEnd()}
|
|
11510
11996
|
|
|
11511
11997
|
${hook.body}`, "utf8");
|
|
11512
11998
|
}
|
|
11513
11999
|
} else {
|
|
11514
|
-
await
|
|
12000
|
+
await writeFile31(file, hook.body, "utf8");
|
|
11515
12001
|
}
|
|
11516
12002
|
await chmod2(file, 493);
|
|
11517
12003
|
}
|
|
11518
12004
|
ui.success("Installed blocking git enforcement hooks: pre-commit, pre-push");
|
|
11519
12005
|
}
|
|
11520
12006
|
async function installCiEnforcement(root) {
|
|
11521
|
-
const workflowPath =
|
|
11522
|
-
await
|
|
11523
|
-
if (
|
|
12007
|
+
const workflowPath = path47.join(root, ".github", "workflows", "haive-enforcement.yml");
|
|
12008
|
+
await mkdir19(path47.dirname(workflowPath), { recursive: true });
|
|
12009
|
+
if (existsSync68(workflowPath)) {
|
|
11524
12010
|
ui.info("GitHub Actions enforcement workflow already exists \u2014 skipped");
|
|
11525
12011
|
return;
|
|
11526
12012
|
}
|
|
11527
|
-
await
|
|
12013
|
+
await writeFile31(workflowPath, `name: haive-enforcement
|
|
11528
12014
|
|
|
11529
12015
|
on:
|
|
11530
12016
|
pull_request:
|
|
@@ -11548,7 +12034,7 @@ jobs:
|
|
|
11548
12034
|
- name: Enforce hAIve policy
|
|
11549
12035
|
run: haive enforce ci
|
|
11550
12036
|
`, "utf8");
|
|
11551
|
-
ui.success(`Created ${
|
|
12037
|
+
ui.success(`Created ${path47.relative(root, workflowPath)}`);
|
|
11552
12038
|
}
|
|
11553
12039
|
function printReport(report, json) {
|
|
11554
12040
|
if (json) {
|
|
@@ -11557,6 +12043,7 @@ function printReport(report, json) {
|
|
|
11557
12043
|
}
|
|
11558
12044
|
console.log(ui.bold(`hAIve enforcement \u2014 ${report.mode}`));
|
|
11559
12045
|
console.log(ui.dim(` root: ${report.root}`));
|
|
12046
|
+
console.log(ui.dim(` score: ${report.score.score}% / threshold ${report.score.threshold}%`));
|
|
11560
12047
|
for (const finding of report.findings) {
|
|
11561
12048
|
const marker = finding.severity === "error" ? ui.red("\u2717") : finding.severity === "warn" ? ui.yellow("\u26A0") : finding.severity === "ok" ? ui.green("\u2713") : ui.dim("\u2022");
|
|
11562
12049
|
console.log(`${marker} ${finding.code}: ${finding.message}`);
|
|
@@ -11576,7 +12063,7 @@ async function readHookPayload() {
|
|
|
11576
12063
|
}
|
|
11577
12064
|
function resolveRoot(dir, payload) {
|
|
11578
12065
|
try {
|
|
11579
|
-
return
|
|
12066
|
+
return findProjectRoot48(dir ?? payload.cwd);
|
|
11580
12067
|
} catch {
|
|
11581
12068
|
return null;
|
|
11582
12069
|
}
|
|
@@ -11645,14 +12132,15 @@ function registerRun(program2) {
|
|
|
11645
12132
|
}
|
|
11646
12133
|
|
|
11647
12134
|
// src/index.ts
|
|
11648
|
-
var program = new
|
|
11649
|
-
program.name("haive").description("hAIve \u2014
|
|
12135
|
+
var program = new Command51();
|
|
12136
|
+
program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.12");
|
|
11650
12137
|
registerInit(program);
|
|
11651
12138
|
registerWelcome(program);
|
|
11652
12139
|
registerResolveProject(program);
|
|
11653
12140
|
registerRuntime(program);
|
|
11654
12141
|
registerEnforce(program);
|
|
11655
12142
|
registerRun(program);
|
|
12143
|
+
registerAgent(program);
|
|
11656
12144
|
registerMcp(program);
|
|
11657
12145
|
registerBriefing(program);
|
|
11658
12146
|
registerTui(program);
|
|
@@ -11694,6 +12182,7 @@ registerSnapshot(program);
|
|
|
11694
12182
|
registerHub(program);
|
|
11695
12183
|
registerStats(program);
|
|
11696
12184
|
registerBench(program);
|
|
12185
|
+
registerBenchmark(program);
|
|
11697
12186
|
registerDoctor(program);
|
|
11698
12187
|
registerPlayback(program);
|
|
11699
12188
|
registerPrecommit(program);
|