@hiveai/cli 0.9.8 → 0.9.10
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 +62 -19
- package/dist/index.js +499 -97
- package/dist/index.js.map +1 -1
- package/package.json +9 -6
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command49 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/briefing.ts
|
|
7
7
|
import { existsSync } from "fs";
|
|
8
|
-
import { readFile } from "fs/promises";
|
|
8
|
+
import { mkdir, readFile } from "fs/promises";
|
|
9
9
|
import path from "path";
|
|
10
10
|
import "commander";
|
|
11
11
|
import {
|
|
@@ -21,7 +21,8 @@ import {
|
|
|
21
21
|
resolveBriefingBudget,
|
|
22
22
|
resolveHaivePaths,
|
|
23
23
|
tokenizeQuery,
|
|
24
|
-
trackReads
|
|
24
|
+
trackReads,
|
|
25
|
+
writeBriefingMarker
|
|
25
26
|
} from "@hiveai/core";
|
|
26
27
|
|
|
27
28
|
// src/utils/ui.ts
|
|
@@ -197,7 +198,7 @@ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
|
|
|
197
198
|
if (!f) continue;
|
|
198
199
|
counts.set(f, (counts.get(f) ?? 0) + 1);
|
|
199
200
|
}
|
|
200
|
-
let entries = [...counts.entries()].map(([
|
|
201
|
+
let entries = [...counts.entries()].map(([path46, changes]) => ({ path: path46, changes }));
|
|
201
202
|
const lowerPaths = filePaths.map((p) => p.toLowerCase());
|
|
202
203
|
if (lowerPaths.length > 0) {
|
|
203
204
|
entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
|
|
@@ -307,6 +308,15 @@ function registerBriefing(program2) {
|
|
|
307
308
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
308
309
|
const root = findProjectRoot(opts.dir);
|
|
309
310
|
const paths = resolveHaivePaths(root);
|
|
311
|
+
if (existsSync(paths.haiveDir)) {
|
|
312
|
+
await mkdir(paths.runtimeDir, { recursive: true });
|
|
313
|
+
await writeBriefingMarker(paths, {
|
|
314
|
+
task: opts.task ?? "CLI briefing",
|
|
315
|
+
source: "haive-briefing-cli",
|
|
316
|
+
sessionId: process.env.HAIVE_SESSION_ID
|
|
317
|
+
}).catch(() => {
|
|
318
|
+
});
|
|
319
|
+
}
|
|
310
320
|
let budgetPreset = null;
|
|
311
321
|
if (opts.budget) {
|
|
312
322
|
const b = opts.budget.trim().toLowerCase();
|
|
@@ -710,7 +720,7 @@ function registerIndexCode(program2) {
|
|
|
710
720
|
}
|
|
711
721
|
|
|
712
722
|
// src/commands/init.ts
|
|
713
|
-
import { mkdir as
|
|
723
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
714
724
|
import { existsSync as existsSync6 } from "fs";
|
|
715
725
|
import path7 from "path";
|
|
716
726
|
import { spawnSync } from "child_process";
|
|
@@ -1052,7 +1062,7 @@ async function generateBootstrapContext(root) {
|
|
|
1052
1062
|
}
|
|
1053
1063
|
|
|
1054
1064
|
// src/commands/init-mcp-setup.ts
|
|
1055
|
-
import { readFile as readFile3, writeFile, mkdir } from "fs/promises";
|
|
1065
|
+
import { readFile as readFile3, writeFile, mkdir as mkdir2 } from "fs/promises";
|
|
1056
1066
|
import { existsSync as existsSync4 } from "fs";
|
|
1057
1067
|
import path5 from "path";
|
|
1058
1068
|
import os from "os";
|
|
@@ -1085,7 +1095,7 @@ async function configureCursor() {
|
|
|
1085
1095
|
config.mcpServers ??= {};
|
|
1086
1096
|
if (config.mcpServers["haive"]) return { client: "Cursor", status: "already_configured" };
|
|
1087
1097
|
config.mcpServers["haive"] = HAIVE_MCP_ENTRY;
|
|
1088
|
-
await
|
|
1098
|
+
await mkdir2(cursorDir, { recursive: true });
|
|
1089
1099
|
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
1090
1100
|
return { client: "Cursor", status: "configured", path: mcpPath };
|
|
1091
1101
|
}
|
|
@@ -1117,7 +1127,7 @@ async function configureVSCode() {
|
|
|
1117
1127
|
config.servers ??= {};
|
|
1118
1128
|
if (config.servers["haive"]) return { client: "VS Code", status: "already_configured" };
|
|
1119
1129
|
config.servers["haive"] = { ...HAIVE_MCP_ENTRY, type: "stdio" };
|
|
1120
|
-
await
|
|
1130
|
+
await mkdir2(path5.dirname(mcpPath), { recursive: true });
|
|
1121
1131
|
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
1122
1132
|
return { client: "VS Code", status: "configured", path: mcpPath };
|
|
1123
1133
|
}
|
|
@@ -1169,7 +1179,7 @@ async function configureWindsurf() {
|
|
|
1169
1179
|
config.mcpServers ??= {};
|
|
1170
1180
|
if (config.mcpServers["haive"]) return { client: "Windsurf", status: "already_configured" };
|
|
1171
1181
|
config.mcpServers["haive"] = HAIVE_MCP_ENTRY;
|
|
1172
|
-
await
|
|
1182
|
+
await mkdir2(path5.dirname(mcpPath), { recursive: true });
|
|
1173
1183
|
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
1174
1184
|
return { client: "Windsurf", status: "configured", path: mcpPath };
|
|
1175
1185
|
}
|
|
@@ -1200,7 +1210,7 @@ async function configureProjectMcpClients(root) {
|
|
|
1200
1210
|
}
|
|
1201
1211
|
config.mcpServers ??= {};
|
|
1202
1212
|
config.mcpServers["haive"] = entry;
|
|
1203
|
-
await
|
|
1213
|
+
await mkdir2(path5.dirname(cursorPath), { recursive: true });
|
|
1204
1214
|
await writeFile(cursorPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
1205
1215
|
results.push({ client: "Cursor (project)", status: "configured", path: cursorPath });
|
|
1206
1216
|
} catch (err) {
|
|
@@ -1217,7 +1227,7 @@ async function configureProjectMcpClients(root) {
|
|
|
1217
1227
|
}
|
|
1218
1228
|
config.servers ??= {};
|
|
1219
1229
|
config.servers["haive"] = { ...entry, type: "stdio" };
|
|
1220
|
-
await
|
|
1230
|
+
await mkdir2(path5.dirname(vscodePath), { recursive: true });
|
|
1221
1231
|
await writeFile(vscodePath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
1222
1232
|
results.push({ client: "VS Code (workspace)", status: "configured", path: vscodePath });
|
|
1223
1233
|
} catch (err) {
|
|
@@ -1243,7 +1253,7 @@ async function configureProjectMcpClients(root) {
|
|
|
1243
1253
|
}
|
|
1244
1254
|
|
|
1245
1255
|
// src/commands/init-stack-packs.ts
|
|
1246
|
-
import { mkdir as
|
|
1256
|
+
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
1247
1257
|
import { existsSync as existsSync5 } from "fs";
|
|
1248
1258
|
import path6 from "path";
|
|
1249
1259
|
import {
|
|
@@ -1857,7 +1867,7 @@ function autoDetectStacks(deps) {
|
|
|
1857
1867
|
async function seedStackPack(haivePaths, stack) {
|
|
1858
1868
|
const memories = PACKS[stack];
|
|
1859
1869
|
if (!memories) return 0;
|
|
1860
|
-
await
|
|
1870
|
+
await mkdir3(haivePaths.teamDir, { recursive: true });
|
|
1861
1871
|
let count = 0;
|
|
1862
1872
|
for (const mem of memories) {
|
|
1863
1873
|
const fm = buildFrontmatter({
|
|
@@ -1870,7 +1880,7 @@ async function seedStackPack(haivePaths, stack) {
|
|
|
1870
1880
|
const filePath = memoryFilePath(haivePaths, "team", fm.id);
|
|
1871
1881
|
if (existsSync5(filePath)) continue;
|
|
1872
1882
|
const content = serializeMemory({ frontmatter: fm, body: mem.body });
|
|
1873
|
-
await
|
|
1883
|
+
await mkdir3(path6.dirname(filePath), { recursive: true });
|
|
1874
1884
|
await writeFile2(filePath, content, "utf8");
|
|
1875
1885
|
count++;
|
|
1876
1886
|
}
|
|
@@ -2094,10 +2104,10 @@ function registerInit(program2) {
|
|
|
2094
2104
|
if (existsSync6(paths.haiveDir)) {
|
|
2095
2105
|
ui.warn(`.ai/ already exists at ${paths.haiveDir} \u2014 leaving existing files in place.`);
|
|
2096
2106
|
}
|
|
2097
|
-
await
|
|
2098
|
-
await
|
|
2099
|
-
await
|
|
2100
|
-
await
|
|
2107
|
+
await mkdir4(paths.personalDir, { recursive: true });
|
|
2108
|
+
await mkdir4(paths.teamDir, { recursive: true });
|
|
2109
|
+
await mkdir4(paths.moduleDir, { recursive: true });
|
|
2110
|
+
await mkdir4(paths.modulesContextDir, { recursive: true });
|
|
2101
2111
|
await ensureAiRuntimeLayout(paths.runtimeDir);
|
|
2102
2112
|
if (!existsSync6(paths.projectContext)) {
|
|
2103
2113
|
if (wantBootstrap) {
|
|
@@ -2154,32 +2164,22 @@ function registerInit(program2) {
|
|
|
2154
2164
|
if (existsSync6(ciPath)) {
|
|
2155
2165
|
ui.info("CI workflow already exists \u2014 skipped");
|
|
2156
2166
|
} else {
|
|
2157
|
-
await
|
|
2167
|
+
await mkdir4(path7.dirname(ciPath), { recursive: true });
|
|
2158
2168
|
await writeFile3(ciPath, CI_WORKFLOW, "utf8");
|
|
2159
2169
|
ui.success(`Created ${path7.relative(root, ciPath)}`);
|
|
2160
2170
|
}
|
|
2161
2171
|
}
|
|
2162
2172
|
if (autopilot) {
|
|
2163
2173
|
const haiveBin = process.argv[1];
|
|
2164
|
-
const
|
|
2174
|
+
const enforcementResult = spawnSync(
|
|
2165
2175
|
process.execPath,
|
|
2166
|
-
[haiveBin, "install
|
|
2176
|
+
[haiveBin, "enforce", "install", "--dir", root],
|
|
2167
2177
|
{ encoding: "utf8" }
|
|
2168
2178
|
);
|
|
2169
|
-
if (
|
|
2170
|
-
ui.success("
|
|
2179
|
+
if (enforcementResult.status === 0) {
|
|
2180
|
+
ui.success("hAIve enforcement installed (MCP, git, CI, client hooks where available)");
|
|
2171
2181
|
} else {
|
|
2172
|
-
ui.warn("
|
|
2173
|
-
}
|
|
2174
|
-
const claudeHookResult = spawnSync(
|
|
2175
|
-
process.execPath,
|
|
2176
|
-
[haiveBin, "install-hooks", "claude", "--scope", "project", "--dir", root],
|
|
2177
|
-
{ encoding: "utf8" }
|
|
2178
|
-
);
|
|
2179
|
-
if (claudeHookResult.status === 0) {
|
|
2180
|
-
ui.success("Claude Code enforcement hooks installed (.claude/settings.local.json)");
|
|
2181
|
-
} else {
|
|
2182
|
-
ui.warn("Claude Code hooks not installed \u2014 run `haive install-hooks claude --scope project` manually");
|
|
2182
|
+
ui.warn("hAIve enforcement not fully installed \u2014 run `haive enforce install` manually");
|
|
2183
2183
|
}
|
|
2184
2184
|
try {
|
|
2185
2185
|
ui.info("Building code-map\u2026");
|
|
@@ -2230,8 +2230,8 @@ function registerInit(program2) {
|
|
|
2230
2230
|
console.log(ui.dim(" \u2713 Proposed memories auto-approve after 72h without rejection"));
|
|
2231
2231
|
console.log(ui.dim(" \u2713 Session recap saved automatically when the AI session closes"));
|
|
2232
2232
|
console.log(ui.dim(" \u2713 Code-map refreshes automatically after every pull"));
|
|
2233
|
-
console.log(ui.dim(" \u2713
|
|
2234
|
-
console.log(ui.dim(" \u2713 CI
|
|
2233
|
+
console.log(ui.dim(" \u2713 Agent-agnostic enforcement gates installed (MCP, git, CI, wrapper-ready)"));
|
|
2234
|
+
console.log(ui.dim(" \u2713 CI workflows created (sync + enforcement)"));
|
|
2235
2235
|
if (stacksToSeed.length > 0) {
|
|
2236
2236
|
console.log(ui.dim(` \u2713 Stack memory packs pre-seeded (${stacksToSeed.join(", ")})`));
|
|
2237
2237
|
}
|
|
@@ -2288,7 +2288,7 @@ async function writeCursorHaiveRule(root) {
|
|
|
2288
2288
|
ui.info(`Cursor rule ${relPath} already exists \u2014 skipped`);
|
|
2289
2289
|
return;
|
|
2290
2290
|
}
|
|
2291
|
-
await
|
|
2291
|
+
await mkdir4(path7.dirname(target), { recursive: true });
|
|
2292
2292
|
await writeFile3(target, CURSOR_HAIVE_RULE_MDC, "utf8");
|
|
2293
2293
|
ui.success(`Created Cursor rule ${relPath}`);
|
|
2294
2294
|
}
|
|
@@ -2298,7 +2298,7 @@ async function writeBridge(root, relPath) {
|
|
|
2298
2298
|
ui.info(`Bridge ${relPath} already exists \u2014 skipped`);
|
|
2299
2299
|
return;
|
|
2300
2300
|
}
|
|
2301
|
-
await
|
|
2301
|
+
await mkdir4(path7.dirname(target), { recursive: true });
|
|
2302
2302
|
await writeFile3(target, BRIDGE_BODY, "utf8");
|
|
2303
2303
|
ui.success(`Created bridge ${relPath}`);
|
|
2304
2304
|
}
|
|
@@ -2315,7 +2315,7 @@ var RUNTIME_GITIGNORE_BODY = `*
|
|
|
2315
2315
|
!README.md
|
|
2316
2316
|
`;
|
|
2317
2317
|
async function ensureAiRuntimeLayout(runtimeDir) {
|
|
2318
|
-
await
|
|
2318
|
+
await mkdir4(runtimeDir, { recursive: true });
|
|
2319
2319
|
const gi = path7.join(runtimeDir, ".gitignore");
|
|
2320
2320
|
if (!existsSync6(gi)) {
|
|
2321
2321
|
await writeFile3(gi, RUNTIME_GITIGNORE_BODY, "utf8");
|
|
@@ -2342,7 +2342,7 @@ async function ensureGitignoreEntries(root, patterns) {
|
|
|
2342
2342
|
}
|
|
2343
2343
|
|
|
2344
2344
|
// src/commands/install-hooks.ts
|
|
2345
|
-
import { mkdir as
|
|
2345
|
+
import { mkdir as mkdir6, writeFile as writeFile5, chmod, readFile as readFile6 } from "fs/promises";
|
|
2346
2346
|
import { existsSync as existsSync8 } from "fs";
|
|
2347
2347
|
import path9 from "path";
|
|
2348
2348
|
import "commander";
|
|
@@ -2350,7 +2350,7 @@ import { findProjectRoot as findProjectRoot6 } from "@hiveai/core";
|
|
|
2350
2350
|
|
|
2351
2351
|
// src/utils/claude-hooks.ts
|
|
2352
2352
|
import { existsSync as existsSync7 } from "fs";
|
|
2353
|
-
import { mkdir as
|
|
2353
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
2354
2354
|
import path8 from "path";
|
|
2355
2355
|
var HAIVE_HOOK_TAG = "haive-enforcement";
|
|
2356
2356
|
var POST_TOOL_USE_GROUP = {
|
|
@@ -2447,7 +2447,7 @@ async function installClaudeHooksAtPath(settingsPath) {
|
|
|
2447
2447
|
created = true;
|
|
2448
2448
|
}
|
|
2449
2449
|
const patched = patchClaudeSettings(raw);
|
|
2450
|
-
await
|
|
2450
|
+
await mkdir5(path8.dirname(settingsPath), { recursive: true });
|
|
2451
2451
|
await writeFile4(settingsPath, JSON.stringify(patched, null, 2) + "\n", "utf8");
|
|
2452
2452
|
return { settingsPath, created };
|
|
2453
2453
|
}
|
|
@@ -2484,9 +2484,8 @@ fi
|
|
|
2484
2484
|
var PRE_PUSH_BODY = `#!/bin/sh
|
|
2485
2485
|
${HOOK_MARKER} \u2014 keep this block to allow upgrades. Hand-edit anything outside it.
|
|
2486
2486
|
|
|
2487
|
-
# Before pushing, run
|
|
2488
|
-
#
|
|
2489
|
-
HAIVE_BLOCK=\${HAIVE_BLOCK:-0}
|
|
2487
|
+
# Before pushing, run the hAIve workflow policy gate. This is blocking by default:
|
|
2488
|
+
# initialized projects should not accept AI changes that bypass hAIve.
|
|
2490
2489
|
|
|
2491
2490
|
_haive() {
|
|
2492
2491
|
if command -v haive >/dev/null 2>&1; then haive "$@"
|
|
@@ -2495,15 +2494,7 @@ _haive() {
|
|
|
2495
2494
|
fi
|
|
2496
2495
|
}
|
|
2497
2496
|
|
|
2498
|
-
|
|
2499
|
-
LOCAL_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
2500
|
-
REMOTE_SHA=$(git rev-parse --verify "@{u}" 2>/dev/null || echo "")
|
|
2501
|
-
if [ -n "$REMOTE_SHA" ]; then
|
|
2502
|
-
DIFF=$(git diff "$REMOTE_SHA"..HEAD 2>/dev/null || "")
|
|
2503
|
-
if [ -n "$DIFF" ]; then
|
|
2504
|
-
_haive precommit --quiet 2>/dev/null || true
|
|
2505
|
-
fi
|
|
2506
|
-
fi
|
|
2497
|
+
_haive enforce check --stage pre-push --dir . || exit $?
|
|
2507
2498
|
|
|
2508
2499
|
# Remind agent to save session recap if env var is set
|
|
2509
2500
|
if [ "$HAIVE_SESSION_REMINDER" = "1" ]; then
|
|
@@ -2515,7 +2506,19 @@ exit 0
|
|
|
2515
2506
|
var HOOKS = [
|
|
2516
2507
|
{ name: "post-merge", body: POST_MERGE_BODY },
|
|
2517
2508
|
{ name: "post-rewrite", body: POST_MERGE_BODY },
|
|
2518
|
-
{ name: "pre-push", body: PRE_PUSH_BODY }
|
|
2509
|
+
{ name: "pre-push", body: PRE_PUSH_BODY },
|
|
2510
|
+
{
|
|
2511
|
+
name: "pre-commit",
|
|
2512
|
+
body: `#!/bin/sh
|
|
2513
|
+
${HOOK_MARKER} \u2014 keep this block to allow upgrades. Hand-edit anything outside it.
|
|
2514
|
+
|
|
2515
|
+
if command -v haive >/dev/null 2>&1; then
|
|
2516
|
+
haive enforce check --stage pre-commit --dir . || exit $?
|
|
2517
|
+
elif [ -x ./node_modules/.bin/haive ]; then
|
|
2518
|
+
./node_modules/.bin/haive enforce check --stage pre-commit --dir . || exit $?
|
|
2519
|
+
fi
|
|
2520
|
+
`
|
|
2521
|
+
}
|
|
2519
2522
|
];
|
|
2520
2523
|
async function installGitHooks(opts) {
|
|
2521
2524
|
const root = findProjectRoot6(opts.dir);
|
|
@@ -2526,7 +2529,7 @@ async function installGitHooks(opts) {
|
|
|
2526
2529
|
return;
|
|
2527
2530
|
}
|
|
2528
2531
|
const hooksDir = path9.join(gitDir, "hooks");
|
|
2529
|
-
await
|
|
2532
|
+
await mkdir6(hooksDir, { recursive: true });
|
|
2530
2533
|
let installed = 0;
|
|
2531
2534
|
let skipped = 0;
|
|
2532
2535
|
for (const { name, body } of HOOKS) {
|
|
@@ -2545,8 +2548,8 @@ async function installGitHooks(opts) {
|
|
|
2545
2548
|
}
|
|
2546
2549
|
ui.success(`Installed ${installed} git hook(s) in .git/hooks/${skipped ? `, skipped ${skipped}` : ""}`);
|
|
2547
2550
|
ui.info("post-merge: haive sync runs after every pull/merge.");
|
|
2548
|
-
ui.info("pre-
|
|
2549
|
-
ui.info("
|
|
2551
|
+
ui.info("pre-commit: haive enforce check blocks unsafe staged changes.");
|
|
2552
|
+
ui.info("pre-push: haive enforce check blocks pushes that bypass briefing/session recap policy.");
|
|
2550
2553
|
}
|
|
2551
2554
|
async function installClaudeHooks(opts) {
|
|
2552
2555
|
const root = findProjectRoot6(opts.dir);
|
|
@@ -2594,7 +2597,7 @@ function registerInstallHooks(program2) {
|
|
|
2594
2597
|
}
|
|
2595
2598
|
|
|
2596
2599
|
// src/commands/observe.ts
|
|
2597
|
-
import { appendFile, mkdir as
|
|
2600
|
+
import { appendFile, mkdir as mkdir7 } from "fs/promises";
|
|
2598
2601
|
import { existsSync as existsSync9 } from "fs";
|
|
2599
2602
|
import path10 from "path";
|
|
2600
2603
|
import "commander";
|
|
@@ -2685,7 +2688,7 @@ function registerObserve(program2) {
|
|
|
2685
2688
|
files: extractFiles(payload)
|
|
2686
2689
|
};
|
|
2687
2690
|
const cacheDir = path10.join(paths.haiveDir, ".cache");
|
|
2688
|
-
await
|
|
2691
|
+
await mkdir7(cacheDir, { recursive: true });
|
|
2689
2692
|
await appendFile(
|
|
2690
2693
|
path10.join(cacheDir, "observations.jsonl"),
|
|
2691
2694
|
JSON.stringify(observation) + "\n",
|
|
@@ -2704,7 +2707,7 @@ import { findProjectRoot as findProjectRoot9 } from "@hiveai/core";
|
|
|
2704
2707
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2705
2708
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2706
2709
|
import { findProjectRoot as findProjectRoot8, resolveHaivePaths as resolveHaivePaths6 } from "@hiveai/core";
|
|
2707
|
-
import { mkdir as
|
|
2710
|
+
import { mkdir as mkdir8, writeFile as writeFile6 } from "fs/promises";
|
|
2708
2711
|
import { existsSync as existsSync10 } from "fs";
|
|
2709
2712
|
import path11 from "path";
|
|
2710
2713
|
import { z } from "zod";
|
|
@@ -2986,7 +2989,7 @@ async function bootstrapProjectSave(input, ctx) {
|
|
|
2986
2989
|
`${target} already exists. Pass overwrite=true to replace it.`
|
|
2987
2990
|
);
|
|
2988
2991
|
}
|
|
2989
|
-
await
|
|
2992
|
+
await mkdir8(path11.dirname(target), { recursive: true });
|
|
2990
2993
|
await writeFile6(target, input.content, "utf8");
|
|
2991
2994
|
return {
|
|
2992
2995
|
file_path: target,
|
|
@@ -6069,7 +6072,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
6069
6072
|
};
|
|
6070
6073
|
}
|
|
6071
6074
|
var SERVER_NAME = "haive";
|
|
6072
|
-
var SERVER_VERSION = "0.9.
|
|
6075
|
+
var SERVER_VERSION = "0.9.10";
|
|
6073
6076
|
function jsonResult(data) {
|
|
6074
6077
|
return {
|
|
6075
6078
|
content: [
|
|
@@ -6993,7 +6996,7 @@ function registerMcp(program2) {
|
|
|
6993
6996
|
|
|
6994
6997
|
// src/commands/sync.ts
|
|
6995
6998
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
6996
|
-
import { readFile as readFile8, writeFile as writeFile13, mkdir as
|
|
6999
|
+
import { readFile as readFile8, writeFile as writeFile13, mkdir as mkdir9 } from "fs/promises";
|
|
6997
7000
|
import { existsSync as existsSync29 } from "fs";
|
|
6998
7001
|
import path12 from "path";
|
|
6999
7002
|
import "commander";
|
|
@@ -7269,7 +7272,7 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7269
7272
|
topic: `dep-bump-${slugParts}`
|
|
7270
7273
|
});
|
|
7271
7274
|
const teamDir = path12.join(paths.memoriesDir, "team");
|
|
7272
|
-
await
|
|
7275
|
+
await mkdir9(teamDir, { recursive: true });
|
|
7273
7276
|
await writeFile13(
|
|
7274
7277
|
path12.join(teamDir, `${fm.id}.md`),
|
|
7275
7278
|
serializeMemory11({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
@@ -7336,7 +7339,7 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7336
7339
|
topic: `contract-breaking-${diff.contract}`
|
|
7337
7340
|
});
|
|
7338
7341
|
const teamDir = path12.join(paths.memoriesDir, "team");
|
|
7339
|
-
await
|
|
7342
|
+
await mkdir9(teamDir, { recursive: true });
|
|
7340
7343
|
await writeFile13(
|
|
7341
7344
|
path12.join(teamDir, `${fm.id}.md`),
|
|
7342
7345
|
serializeMemory11({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
@@ -7475,7 +7478,7 @@ function collectSinceChanges(root, ref) {
|
|
|
7475
7478
|
|
|
7476
7479
|
// src/commands/memory-add.ts
|
|
7477
7480
|
import { createHash as createHash2 } from "crypto";
|
|
7478
|
-
import { mkdir as
|
|
7481
|
+
import { mkdir as mkdir10, readFile as readFile9, writeFile as writeFile14 } from "fs/promises";
|
|
7479
7482
|
import { existsSync as existsSync30 } from "fs";
|
|
7480
7483
|
import path13 from "path";
|
|
7481
7484
|
import "commander";
|
|
@@ -7610,7 +7613,7 @@ TODO \u2014 write the memory body.
|
|
|
7610
7613
|
topic: opts.topic
|
|
7611
7614
|
});
|
|
7612
7615
|
const file = memoryFilePath6(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
7613
|
-
await
|
|
7616
|
+
await mkdir10(path13.dirname(file), { recursive: true });
|
|
7614
7617
|
if (existsSync30(file)) {
|
|
7615
7618
|
ui.error(`Memory already exists at ${file}`);
|
|
7616
7619
|
process.exitCode = 1;
|
|
@@ -7743,7 +7746,7 @@ function matchesFilters(loaded, opts) {
|
|
|
7743
7746
|
}
|
|
7744
7747
|
|
|
7745
7748
|
// src/commands/memory-promote.ts
|
|
7746
|
-
import { mkdir as
|
|
7749
|
+
import { mkdir as mkdir11, unlink as unlink2, writeFile as writeFile15 } from "fs/promises";
|
|
7747
7750
|
import { existsSync as existsSync33 } from "fs";
|
|
7748
7751
|
import path15 from "path";
|
|
7749
7752
|
import "commander";
|
|
@@ -7792,7 +7795,7 @@ function registerMemoryPromote(memory2) {
|
|
|
7792
7795
|
body: found.memory.body
|
|
7793
7796
|
};
|
|
7794
7797
|
const newPath = memoryFilePath7(paths, "team", updated.frontmatter.id);
|
|
7795
|
-
await
|
|
7798
|
+
await mkdir11(path15.dirname(newPath), { recursive: true });
|
|
7796
7799
|
await writeFile15(newPath, serializeMemory13(updated), "utf8");
|
|
7797
7800
|
await unlink2(found.filePath);
|
|
7798
7801
|
ui.success(`Promoted ${id} to team scope (status=proposed)`);
|
|
@@ -8252,7 +8255,7 @@ function registerMemoryHot(memory2) {
|
|
|
8252
8255
|
}
|
|
8253
8256
|
|
|
8254
8257
|
// src/commands/memory-tried.ts
|
|
8255
|
-
import { mkdir as
|
|
8258
|
+
import { mkdir as mkdir12, writeFile as writeFile19 } from "fs/promises";
|
|
8256
8259
|
import { existsSync as existsSync40 } from "fs";
|
|
8257
8260
|
import path23 from "path";
|
|
8258
8261
|
import "commander";
|
|
@@ -8305,7 +8308,7 @@ function registerMemoryTried(memory2) {
|
|
|
8305
8308
|
}
|
|
8306
8309
|
const body = lines.join("\n") + "\n";
|
|
8307
8310
|
const file = memoryFilePath8(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
8308
|
-
await
|
|
8311
|
+
await mkdir12(path23.dirname(file), { recursive: true });
|
|
8309
8312
|
if (existsSync40(file)) {
|
|
8310
8313
|
ui.error(`Memory already exists at ${file}`);
|
|
8311
8314
|
process.exitCode = 1;
|
|
@@ -8805,7 +8808,7 @@ function registerMemoryImport(memory2) {
|
|
|
8805
8808
|
|
|
8806
8809
|
// src/commands/memory-import-changelog.ts
|
|
8807
8810
|
import { existsSync as existsSync50 } from "fs";
|
|
8808
|
-
import { readFile as readFile13, mkdir as
|
|
8811
|
+
import { readFile as readFile13, mkdir as mkdir13, writeFile as writeFile23 } from "fs/promises";
|
|
8809
8812
|
import path30 from "path";
|
|
8810
8813
|
import "commander";
|
|
8811
8814
|
import {
|
|
@@ -8902,7 +8905,7 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
8902
8905
|
const pkgName = opts.package ?? path30.basename(path30.dirname(changelogPath));
|
|
8903
8906
|
const scope = opts.scope ?? "team";
|
|
8904
8907
|
const teamDir = path30.join(paths.memoriesDir, scope);
|
|
8905
|
-
await
|
|
8908
|
+
await mkdir13(teamDir, { recursive: true });
|
|
8906
8909
|
let saved = 0;
|
|
8907
8910
|
for (const entry of entries) {
|
|
8908
8911
|
const lines = [];
|
|
@@ -9073,7 +9076,7 @@ function registerMemoryDigest(program2) {
|
|
|
9073
9076
|
}
|
|
9074
9077
|
|
|
9075
9078
|
// src/commands/session-end.ts
|
|
9076
|
-
import { writeFile as writeFile25, mkdir as
|
|
9079
|
+
import { writeFile as writeFile25, mkdir as mkdir14, readFile as readFile14, rm as rm2 } from "fs/promises";
|
|
9077
9080
|
import { existsSync as existsSync53 } from "fs";
|
|
9078
9081
|
import path33 from "path";
|
|
9079
9082
|
import "commander";
|
|
@@ -9248,7 +9251,7 @@ function registerSessionEnd(session2) {
|
|
|
9248
9251
|
status: "validated"
|
|
9249
9252
|
});
|
|
9250
9253
|
const file = memoryFilePath9(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
9251
|
-
await
|
|
9254
|
+
await mkdir14(path33.dirname(file), { recursive: true });
|
|
9252
9255
|
await writeFile25(file, serializeMemory21({ frontmatter, body }), "utf8");
|
|
9253
9256
|
await cleanupObservations();
|
|
9254
9257
|
if (!opts.quiet) {
|
|
@@ -9432,7 +9435,7 @@ function detectFormat(filePath) {
|
|
|
9432
9435
|
|
|
9433
9436
|
// src/commands/hub.ts
|
|
9434
9437
|
import { existsSync as existsSync55 } from "fs";
|
|
9435
|
-
import { mkdir as
|
|
9438
|
+
import { mkdir as mkdir15, readFile as readFile15, writeFile as writeFile26, copyFile } from "fs/promises";
|
|
9436
9439
|
import path35 from "path";
|
|
9437
9440
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
9438
9441
|
import "commander";
|
|
@@ -9453,7 +9456,7 @@ function registerHub(program2) {
|
|
|
9453
9456
|
"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"
|
|
9454
9457
|
).action(async (hubPath) => {
|
|
9455
9458
|
const absPath = path35.resolve(hubPath);
|
|
9456
|
-
await
|
|
9459
|
+
await mkdir15(absPath, { recursive: true });
|
|
9457
9460
|
const gitCheck = spawnSync3("git", ["rev-parse", "--git-dir"], { cwd: absPath });
|
|
9458
9461
|
if (gitCheck.status !== 0) {
|
|
9459
9462
|
const init = spawnSync3("git", ["init"], { cwd: absPath, encoding: "utf8" });
|
|
@@ -9464,7 +9467,7 @@ function registerHub(program2) {
|
|
|
9464
9467
|
}
|
|
9465
9468
|
}
|
|
9466
9469
|
const sharedDir = path35.join(absPath, ".ai", "memories", "shared");
|
|
9467
|
-
await
|
|
9470
|
+
await mkdir15(sharedDir, { recursive: true });
|
|
9468
9471
|
await writeFile26(
|
|
9469
9472
|
path35.join(absPath, ".ai", "README.md"),
|
|
9470
9473
|
`# hAIve Team Knowledge Hub
|
|
@@ -9540,7 +9543,7 @@ Next steps:
|
|
|
9540
9543
|
}
|
|
9541
9544
|
const projectName = path35.basename(root);
|
|
9542
9545
|
const destDir = path35.join(hubRoot, ".ai", "memories", "shared", projectName);
|
|
9543
|
-
await
|
|
9546
|
+
await mkdir15(destDir, { recursive: true });
|
|
9544
9547
|
const all = await loadMemoriesFromDir28(paths.memoriesDir);
|
|
9545
9548
|
const shared = all.filter(
|
|
9546
9549
|
({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && // Don't push imported memories (avoid echo loops)
|
|
@@ -9619,7 +9622,7 @@ Next steps:
|
|
|
9619
9622
|
for (const sourceName of projectDirs) {
|
|
9620
9623
|
const sourceDir = path35.join(hubSharedDir, sourceName);
|
|
9621
9624
|
const destDir = path35.join(paths.memoriesDir, "shared", sourceName);
|
|
9622
|
-
await
|
|
9625
|
+
await mkdir15(destDir, { recursive: true });
|
|
9623
9626
|
const sourceFiles = (await readdir5(sourceDir)).filter((f) => f.endsWith(".md"));
|
|
9624
9627
|
const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
|
|
9625
9628
|
const existingInDest = await loadDir(destDir);
|
|
@@ -9688,7 +9691,7 @@ Next steps:
|
|
|
9688
9691
|
// src/commands/stats.ts
|
|
9689
9692
|
import "commander";
|
|
9690
9693
|
import { existsSync as existsSync56 } from "fs";
|
|
9691
|
-
import { mkdir as
|
|
9694
|
+
import { mkdir as mkdir16, writeFile as writeFile27 } from "fs/promises";
|
|
9692
9695
|
import path36 from "path";
|
|
9693
9696
|
import {
|
|
9694
9697
|
aggregateUsage,
|
|
@@ -9796,7 +9799,7 @@ async function writeRoiReport(paths, root, sinceRaw, outRelative) {
|
|
|
9796
9799
|
ui.warn("Usage log missing or empty \u2014 report still written with partial data.");
|
|
9797
9800
|
events = [];
|
|
9798
9801
|
}
|
|
9799
|
-
await
|
|
9802
|
+
await mkdir16(path36.dirname(outAbs), { recursive: true });
|
|
9800
9803
|
const payload = {
|
|
9801
9804
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9802
9805
|
project_root: root,
|
|
@@ -9985,7 +9988,7 @@ function summarize(name, t0, payload, notes) {
|
|
|
9985
9988
|
}
|
|
9986
9989
|
|
|
9987
9990
|
// src/commands/memory-suggest.ts
|
|
9988
|
-
import { mkdir as
|
|
9991
|
+
import { mkdir as mkdir17, writeFile as writeFile28 } from "fs/promises";
|
|
9989
9992
|
import { existsSync as existsSync57 } from "fs";
|
|
9990
9993
|
import path37 from "path";
|
|
9991
9994
|
import "commander";
|
|
@@ -10080,7 +10083,7 @@ function registerMemorySuggest(memory2) {
|
|
|
10080
10083
|
fm.status = "draft";
|
|
10081
10084
|
const body = renderTemplate(s);
|
|
10082
10085
|
const file = memoryFilePath10(paths, fm.scope, fm.id, fm.module);
|
|
10083
|
-
await
|
|
10086
|
+
await mkdir17(path37.dirname(file), { recursive: true });
|
|
10084
10087
|
if (existsSync57(file)) {
|
|
10085
10088
|
skipped.push({ query: s.query, reason: `file already exists at ${path37.relative(root, file)}` });
|
|
10086
10089
|
continue;
|
|
@@ -10334,8 +10337,8 @@ function registerDoctor(program2) {
|
|
|
10334
10337
|
fix: "haive init"
|
|
10335
10338
|
});
|
|
10336
10339
|
} else {
|
|
10337
|
-
const { readFile:
|
|
10338
|
-
const content = await
|
|
10340
|
+
const { readFile: readFile17 } = await import("fs/promises");
|
|
10341
|
+
const content = await readFile17(paths.projectContext, "utf8");
|
|
10339
10342
|
const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
|
|
10340
10343
|
if (isTemplate) {
|
|
10341
10344
|
findings.push({
|
|
@@ -10461,8 +10464,8 @@ function registerDoctor(program2) {
|
|
|
10461
10464
|
let hasClaudeEnforcement = false;
|
|
10462
10465
|
if (existsSync59(claudeSettings)) {
|
|
10463
10466
|
try {
|
|
10464
|
-
const { readFile:
|
|
10465
|
-
const raw = await
|
|
10467
|
+
const { readFile: readFile17 } = await import("fs/promises");
|
|
10468
|
+
const raw = await readFile17(claudeSettings, "utf8");
|
|
10466
10469
|
hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
|
|
10467
10470
|
} catch {
|
|
10468
10471
|
hasClaudeEnforcement = false;
|
|
@@ -10491,7 +10494,7 @@ function registerDoctor(program2) {
|
|
|
10491
10494
|
timeout: 3e3,
|
|
10492
10495
|
stdio: ["ignore", "pipe", "ignore"]
|
|
10493
10496
|
}).trim();
|
|
10494
|
-
const cliVersion = "0.9.
|
|
10497
|
+
const cliVersion = "0.9.10";
|
|
10495
10498
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
10496
10499
|
findings.push({
|
|
10497
10500
|
severity: "warn",
|
|
@@ -11094,29 +11097,86 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
11094
11097
|
}
|
|
11095
11098
|
|
|
11096
11099
|
// src/commands/enforce.ts
|
|
11100
|
+
import { spawn as spawn5 } from "child_process";
|
|
11097
11101
|
import { existsSync as existsSync67 } from "fs";
|
|
11098
|
-
import { mkdir as
|
|
11102
|
+
import { chmod as chmod2, mkdir as mkdir18, readFile as readFile16, writeFile as writeFile30 } from "fs/promises";
|
|
11103
|
+
import path45 from "path";
|
|
11099
11104
|
import "commander";
|
|
11100
11105
|
import {
|
|
11101
11106
|
findProjectRoot as findProjectRoot46,
|
|
11102
11107
|
hasRecentBriefingMarker,
|
|
11108
|
+
isFreshIsoDate,
|
|
11109
|
+
loadConfig as loadConfig8,
|
|
11110
|
+
loadMemoriesFromDir as loadMemoriesFromDir36,
|
|
11103
11111
|
resolveBriefingBudget as resolveBriefingBudget3,
|
|
11104
11112
|
resolveHaivePaths as resolveHaivePaths43,
|
|
11105
|
-
|
|
11113
|
+
saveConfig as saveConfig3,
|
|
11114
|
+
SESSION_RECAP_TTL_MS,
|
|
11115
|
+
verifyAnchor as verifyAnchor4,
|
|
11116
|
+
writeBriefingMarker as writeBriefingMarker2
|
|
11106
11117
|
} from "@hiveai/core";
|
|
11107
11118
|
var MAX_STDIN_BYTES2 = 256 * 1024;
|
|
11119
|
+
var ENFORCE_HOOK_MARKER = "# hAIve enforcement hook";
|
|
11108
11120
|
function registerEnforce(program2) {
|
|
11109
|
-
const enforce = program2.command("enforce").description(
|
|
11121
|
+
const enforce = program2.command("enforce").description(
|
|
11122
|
+
"Agent-agnostic enforcement helpers: install policy gates, report status, and block unsafe workflows."
|
|
11123
|
+
);
|
|
11124
|
+
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 = findProjectRoot46(opts.dir);
|
|
11126
|
+
const paths = resolveHaivePaths43(root);
|
|
11127
|
+
await mkdir18(paths.haiveDir, { recursive: true });
|
|
11128
|
+
const current = await loadConfig8(paths);
|
|
11129
|
+
await saveConfig3(paths, {
|
|
11130
|
+
...current,
|
|
11131
|
+
enforcement: {
|
|
11132
|
+
...current.enforcement,
|
|
11133
|
+
mode: "strict",
|
|
11134
|
+
requireBriefingFirst: true,
|
|
11135
|
+
requireSessionRecap: true,
|
|
11136
|
+
requireMemoryVerify: true,
|
|
11137
|
+
blockStaleDecisionChanges: true,
|
|
11138
|
+
toolProfile: "enforcement"
|
|
11139
|
+
}
|
|
11140
|
+
});
|
|
11141
|
+
ui.success("hAIve strict enforcement enabled in .ai/haive.config.json");
|
|
11142
|
+
if (opts.git !== false) await installGitEnforcement(root);
|
|
11143
|
+
if (opts.ci !== false) await installCiEnforcement(root);
|
|
11144
|
+
if (opts.claude !== false) {
|
|
11145
|
+
try {
|
|
11146
|
+
const result = await installClaudeHooksAtPath(defaultClaudeSettingsPath("project", root));
|
|
11147
|
+
ui.success(`${result.created ? "Created" : "Patched"} Claude Code hooks (${path45.relative(root, result.settingsPath)})`);
|
|
11148
|
+
} catch (err) {
|
|
11149
|
+
ui.warn(`Claude Code hooks not installed: ${err instanceof Error ? err.message : String(err)}`);
|
|
11150
|
+
}
|
|
11151
|
+
}
|
|
11152
|
+
ui.info("Agent-agnostic gates are now active at workflow level: MCP, git, CI, and optional client hooks.");
|
|
11153
|
+
ui.info("Use `haive run -- <agent command>` for agents that do not expose blocking hooks.");
|
|
11154
|
+
});
|
|
11155
|
+
enforce.command("status").description("Show whether this project has agent-agnostic hAIve enforcement installed.").option("-d, --dir <dir>", "project root").option("--json", "emit JSON", false).action(async (opts) => {
|
|
11156
|
+
const report = await buildEnforcementReport(opts.dir, "local");
|
|
11157
|
+
printReport(report, Boolean(opts.json));
|
|
11158
|
+
if (report.should_block) process.exitCode = 1;
|
|
11159
|
+
});
|
|
11160
|
+
enforce.command("check").description("Run the hAIve policy gate. Intended for pre-commit, pre-push, wrappers, and any agent client.").option("-d, --dir <dir>", "project root").option("--stage <stage>", "local | pre-commit | pre-push | ci", "local").option("--json", "emit JSON", false).action(async (opts) => {
|
|
11161
|
+
const report = await buildEnforcementReport(opts.dir, opts.stage ?? "local");
|
|
11162
|
+
printReport(report, Boolean(opts.json));
|
|
11163
|
+
if (report.should_block) process.exit(2);
|
|
11164
|
+
});
|
|
11165
|
+
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
|
+
const report = await buildEnforcementReport(opts.dir, "ci");
|
|
11167
|
+
printReport(report, Boolean(opts.json));
|
|
11168
|
+
if (report.should_block) process.exit(2);
|
|
11169
|
+
});
|
|
11110
11170
|
enforce.command("session-start").description("Claude Code SessionStart hook: inject briefing and write a local briefing marker.").option("-d, --dir <dir>", "project root").option("--task <text>", "task text to rank memories").option("--source <name>", "marker source", "claude-session-start").option("--session-id <id>", "agent session id").action(async (opts) => {
|
|
11111
11171
|
const payload = await readHookPayload();
|
|
11112
11172
|
const root = resolveRoot(opts.dir, payload);
|
|
11113
11173
|
if (!root) return;
|
|
11114
11174
|
const paths = resolveHaivePaths43(root);
|
|
11115
11175
|
if (!existsSync67(paths.haiveDir)) return;
|
|
11116
|
-
await
|
|
11176
|
+
await mkdir18(paths.runtimeDir, { recursive: true });
|
|
11117
11177
|
const sessionId = opts.sessionId ?? payload.session_id;
|
|
11118
11178
|
const task = opts.task ?? payload.prompt ?? "Start an AI coding session in this hAIve-initialized project.";
|
|
11119
|
-
await
|
|
11179
|
+
await writeBriefingMarker2(paths, {
|
|
11120
11180
|
sessionId,
|
|
11121
11181
|
task,
|
|
11122
11182
|
source: opts.source ?? "claude-session-start"
|
|
@@ -11194,6 +11254,317 @@ ${briefing.project_context.content.slice(0, 1800)}`);
|
|
|
11194
11254
|
process.exit(2);
|
|
11195
11255
|
});
|
|
11196
11256
|
}
|
|
11257
|
+
async function runWithEnforcement(command, args, opts) {
|
|
11258
|
+
const root = findProjectRoot46(opts.dir);
|
|
11259
|
+
const paths = resolveHaivePaths43(root);
|
|
11260
|
+
if (!existsSync67(paths.haiveDir)) {
|
|
11261
|
+
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
11262
|
+
process.exit(1);
|
|
11263
|
+
}
|
|
11264
|
+
const sessionId = `haive-run-${process.pid}-${Date.now()}`;
|
|
11265
|
+
const task = opts.task ?? `Run agent command: ${[command, ...args].join(" ")}`;
|
|
11266
|
+
await writeBriefingMarker2(paths, {
|
|
11267
|
+
sessionId,
|
|
11268
|
+
task,
|
|
11269
|
+
source: "haive-run"
|
|
11270
|
+
});
|
|
11271
|
+
const briefingFile = await writeWrapperBriefing(paths, sessionId, task);
|
|
11272
|
+
const before = await buildEnforcementReport(root, "local", sessionId);
|
|
11273
|
+
const blocking = before.findings.filter((f) => f.severity === "error" && f.code !== "session-recap-missing");
|
|
11274
|
+
if (blocking.length > 0) {
|
|
11275
|
+
printReport({ ...before, should_block: true, findings: blocking }, false);
|
|
11276
|
+
process.exit(2);
|
|
11277
|
+
}
|
|
11278
|
+
ui.info(`hAIve briefing marker created for wrapped agent session: ${sessionId}`);
|
|
11279
|
+
ui.info(`Briefing written to ${path45.relative(root, briefingFile)} and exported as HAIVE_BRIEFING_FILE`);
|
|
11280
|
+
const child = spawn5(command, args, {
|
|
11281
|
+
cwd: root,
|
|
11282
|
+
stdio: "inherit",
|
|
11283
|
+
env: {
|
|
11284
|
+
...process.env,
|
|
11285
|
+
HAIVE_PROJECT_ROOT: root,
|
|
11286
|
+
HAIVE_SESSION_ID: sessionId,
|
|
11287
|
+
HAIVE_BRIEFING_FILE: briefingFile,
|
|
11288
|
+
HAIVE_ENFORCEMENT: "strict",
|
|
11289
|
+
HAIVE_TOOL_PROFILE: process.env.HAIVE_TOOL_PROFILE ?? "enforcement"
|
|
11290
|
+
}
|
|
11291
|
+
});
|
|
11292
|
+
await new Promise((resolve, reject) => {
|
|
11293
|
+
child.on("error", reject);
|
|
11294
|
+
child.on("close", (code, signal) => {
|
|
11295
|
+
if (signal) process.exit(128);
|
|
11296
|
+
process.exitCode = code ?? 0;
|
|
11297
|
+
resolve();
|
|
11298
|
+
});
|
|
11299
|
+
});
|
|
11300
|
+
}
|
|
11301
|
+
async function writeWrapperBriefing(paths, sessionId, task) {
|
|
11302
|
+
const budget = resolveBriefingBudget3("quick", {
|
|
11303
|
+
max_tokens: 2500,
|
|
11304
|
+
max_memories: 5,
|
|
11305
|
+
include_module_contexts: false
|
|
11306
|
+
});
|
|
11307
|
+
const briefing = await getBriefing({
|
|
11308
|
+
task,
|
|
11309
|
+
files: [],
|
|
11310
|
+
max_tokens: budget.max_tokens,
|
|
11311
|
+
max_memories: budget.max_memories,
|
|
11312
|
+
include_project_context: true,
|
|
11313
|
+
include_module_contexts: budget.include_module_contexts,
|
|
11314
|
+
semantic: true,
|
|
11315
|
+
include_stale: false,
|
|
11316
|
+
track: true,
|
|
11317
|
+
format: "actions",
|
|
11318
|
+
symbols: [],
|
|
11319
|
+
min_semantic_score: 0.25,
|
|
11320
|
+
budget_preset: "quick"
|
|
11321
|
+
}, { paths });
|
|
11322
|
+
const dir = path45.join(paths.runtimeDir, "enforcement", "briefings");
|
|
11323
|
+
await mkdir18(dir, { recursive: true });
|
|
11324
|
+
const file = path45.join(dir, `${sessionId}.md`);
|
|
11325
|
+
const parts = [
|
|
11326
|
+
"# hAIve Briefing",
|
|
11327
|
+
"",
|
|
11328
|
+
`Task: ${task}`,
|
|
11329
|
+
""
|
|
11330
|
+
];
|
|
11331
|
+
if (briefing.last_session) parts.push("## Last Session", briefing.last_session.body.trim(), "");
|
|
11332
|
+
if (briefing.project_context?.content) parts.push("## Project Context", briefing.project_context.content.trim(), "");
|
|
11333
|
+
if (briefing.memories.length > 0) {
|
|
11334
|
+
parts.push("## Relevant Memories");
|
|
11335
|
+
for (const memory2 of briefing.memories) {
|
|
11336
|
+
parts.push("", `### ${memory2.id}`, memory2.body.trim());
|
|
11337
|
+
}
|
|
11338
|
+
}
|
|
11339
|
+
if (briefing.setup_warnings.length > 0) {
|
|
11340
|
+
parts.push("", "## Setup Warnings", ...briefing.setup_warnings.map((w) => `- ${w}`));
|
|
11341
|
+
}
|
|
11342
|
+
await writeFile30(file, parts.join("\n") + "\n", "utf8");
|
|
11343
|
+
return file;
|
|
11344
|
+
}
|
|
11345
|
+
async function buildEnforcementReport(dir, stage, sessionId) {
|
|
11346
|
+
const root = findProjectRoot46(dir);
|
|
11347
|
+
const paths = resolveHaivePaths43(root);
|
|
11348
|
+
const initialized = existsSync67(paths.haiveDir);
|
|
11349
|
+
const config = initialized ? await loadConfig8(paths) : {};
|
|
11350
|
+
const mode = config.enforcement?.mode ?? "strict";
|
|
11351
|
+
const findings = [];
|
|
11352
|
+
if (!initialized) {
|
|
11353
|
+
return {
|
|
11354
|
+
root,
|
|
11355
|
+
initialized,
|
|
11356
|
+
mode,
|
|
11357
|
+
should_block: true,
|
|
11358
|
+
findings: [{
|
|
11359
|
+
severity: "error",
|
|
11360
|
+
code: "not-initialized",
|
|
11361
|
+
message: "This repository is not initialized with hAIve.",
|
|
11362
|
+
fix: "Run `haive init` or `haive enforce install`."
|
|
11363
|
+
}]
|
|
11364
|
+
};
|
|
11365
|
+
}
|
|
11366
|
+
if (mode === "off") {
|
|
11367
|
+
return {
|
|
11368
|
+
root,
|
|
11369
|
+
initialized,
|
|
11370
|
+
mode,
|
|
11371
|
+
should_block: false,
|
|
11372
|
+
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
11373
|
+
};
|
|
11374
|
+
}
|
|
11375
|
+
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
11376
|
+
const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
|
|
11377
|
+
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
|
|
11378
|
+
severity: "error",
|
|
11379
|
+
code: "briefing-missing",
|
|
11380
|
+
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>`.'
|
|
11382
|
+
});
|
|
11383
|
+
}
|
|
11384
|
+
if (config.enforcement?.requireSessionRecap !== false && (stage === "pre-push" || stage === "ci")) {
|
|
11385
|
+
const hasRecap = await hasRecentSessionRecap(paths);
|
|
11386
|
+
findings.push(hasRecap ? { severity: "ok", code: "session-recap-present", message: "A recent session_recap memory exists." } : {
|
|
11387
|
+
severity: "error",
|
|
11388
|
+
code: "session-recap-missing",
|
|
11389
|
+
message: "No recent session_recap memory was found.",
|
|
11390
|
+
fix: "Run `haive session end --goal ... --accomplished ...` before pushing."
|
|
11391
|
+
});
|
|
11392
|
+
}
|
|
11393
|
+
if (config.enforcement?.requireMemoryVerify !== false) {
|
|
11394
|
+
findings.push(...await verifyMemoryPolicy(paths, config));
|
|
11395
|
+
}
|
|
11396
|
+
if (stage === "pre-commit" || stage === "ci") {
|
|
11397
|
+
findings.push(...await runPrecommitPolicy(paths));
|
|
11398
|
+
}
|
|
11399
|
+
const hasErrors = findings.some((f) => f.severity === "error");
|
|
11400
|
+
return {
|
|
11401
|
+
root,
|
|
11402
|
+
initialized,
|
|
11403
|
+
mode,
|
|
11404
|
+
should_block: mode === "strict" && hasErrors,
|
|
11405
|
+
findings
|
|
11406
|
+
};
|
|
11407
|
+
}
|
|
11408
|
+
async function hasRecentSessionRecap(paths) {
|
|
11409
|
+
if (!existsSync67(paths.memoriesDir)) return false;
|
|
11410
|
+
const all = await loadMemoriesFromDir36(paths.memoriesDir);
|
|
11411
|
+
return all.some(
|
|
11412
|
+
({ memory: memory2 }) => memory2.frontmatter.type === "session_recap" && memory2.frontmatter.status !== "rejected" && isFreshIsoDate(memory2.frontmatter.created_at, SESSION_RECAP_TTL_MS)
|
|
11413
|
+
);
|
|
11414
|
+
}
|
|
11415
|
+
async function verifyMemoryPolicy(paths, config) {
|
|
11416
|
+
if (!existsSync67(paths.memoriesDir)) return [];
|
|
11417
|
+
const all = await loadMemoriesFromDir36(paths.memoriesDir);
|
|
11418
|
+
const findings = [];
|
|
11419
|
+
const staleImportant = [];
|
|
11420
|
+
let verified = 0;
|
|
11421
|
+
for (const { memory: memory2 } of all) {
|
|
11422
|
+
const fm = memory2.frontmatter;
|
|
11423
|
+
const anchored = fm.anchor.paths.length > 0 || fm.anchor.symbols.length > 0;
|
|
11424
|
+
if (!anchored || fm.status === "rejected" || fm.status === "deprecated") continue;
|
|
11425
|
+
verified++;
|
|
11426
|
+
if (fm.status === "stale") {
|
|
11427
|
+
if (["decision", "gotcha", "architecture", "convention"].includes(fm.type)) {
|
|
11428
|
+
staleImportant.push(fm.id);
|
|
11429
|
+
}
|
|
11430
|
+
continue;
|
|
11431
|
+
}
|
|
11432
|
+
if (config.enforcement?.blockStaleDecisionChanges !== false && ["decision", "gotcha"].includes(fm.type)) {
|
|
11433
|
+
const result = await verifyAnchor4(memory2, { projectRoot: paths.root });
|
|
11434
|
+
if (result.stale) staleImportant.push(fm.id);
|
|
11435
|
+
}
|
|
11436
|
+
}
|
|
11437
|
+
findings.push({
|
|
11438
|
+
severity: "ok",
|
|
11439
|
+
code: "memory-verify-ran",
|
|
11440
|
+
message: `Checked ${verified} anchored memories for stale enforcement policy.`
|
|
11441
|
+
});
|
|
11442
|
+
if (staleImportant.length > 0) {
|
|
11443
|
+
findings.push({
|
|
11444
|
+
severity: "error",
|
|
11445
|
+
code: "stale-important-memories",
|
|
11446
|
+
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."
|
|
11448
|
+
});
|
|
11449
|
+
}
|
|
11450
|
+
return findings;
|
|
11451
|
+
}
|
|
11452
|
+
async function runPrecommitPolicy(paths) {
|
|
11453
|
+
const staged = await runCommand4("git", ["diff", "--cached", "--name-only"], paths.root).catch(() => "");
|
|
11454
|
+
const touchedPaths = staged.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
11455
|
+
if (touchedPaths.length === 0) {
|
|
11456
|
+
return [{ severity: "info", code: "no-staged-changes", message: "No staged changes found for pre-commit policy." }];
|
|
11457
|
+
}
|
|
11458
|
+
const diff = await runCommand4("git", ["diff", "--cached"], paths.root).catch(() => "");
|
|
11459
|
+
const result = await preCommitCheck({
|
|
11460
|
+
diff,
|
|
11461
|
+
paths: touchedPaths,
|
|
11462
|
+
block_on: "high-confidence",
|
|
11463
|
+
semantic: true
|
|
11464
|
+
}, { paths });
|
|
11465
|
+
if (!result.should_block) {
|
|
11466
|
+
return [{
|
|
11467
|
+
severity: "ok",
|
|
11468
|
+
code: "precommit-policy-pass",
|
|
11469
|
+
message: `Pre-commit policy passed for ${touchedPaths.length} staged file(s).`
|
|
11470
|
+
}];
|
|
11471
|
+
}
|
|
11472
|
+
return [{
|
|
11473
|
+
severity: "error",
|
|
11474
|
+
code: "precommit-policy-block",
|
|
11475
|
+
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."
|
|
11477
|
+
}];
|
|
11478
|
+
}
|
|
11479
|
+
async function installGitEnforcement(root) {
|
|
11480
|
+
const hooksDir = path45.join(root, ".git", "hooks");
|
|
11481
|
+
if (!existsSync67(path45.join(root, ".git"))) {
|
|
11482
|
+
ui.warn("No .git directory found; git enforcement hooks skipped.");
|
|
11483
|
+
return;
|
|
11484
|
+
}
|
|
11485
|
+
await mkdir18(hooksDir, { recursive: true });
|
|
11486
|
+
const hooks = [
|
|
11487
|
+
{
|
|
11488
|
+
name: "pre-commit",
|
|
11489
|
+
body: `#!/bin/sh
|
|
11490
|
+
${ENFORCE_HOOK_MARKER}
|
|
11491
|
+
haive enforce check --stage pre-commit --dir . || exit $?
|
|
11492
|
+
`
|
|
11493
|
+
},
|
|
11494
|
+
{
|
|
11495
|
+
name: "pre-push",
|
|
11496
|
+
body: `#!/bin/sh
|
|
11497
|
+
${ENFORCE_HOOK_MARKER}
|
|
11498
|
+
haive enforce check --stage pre-push --dir . || exit $?
|
|
11499
|
+
`
|
|
11500
|
+
}
|
|
11501
|
+
];
|
|
11502
|
+
for (const hook of hooks) {
|
|
11503
|
+
const file = path45.join(hooksDir, hook.name);
|
|
11504
|
+
if (existsSync67(file)) {
|
|
11505
|
+
const current = await readFile16(file, "utf8").catch(() => "");
|
|
11506
|
+
if (current.includes(ENFORCE_HOOK_MARKER)) {
|
|
11507
|
+
await writeFile30(file, hook.body, "utf8");
|
|
11508
|
+
} else {
|
|
11509
|
+
await writeFile30(file, `${current.trimEnd()}
|
|
11510
|
+
|
|
11511
|
+
${hook.body}`, "utf8");
|
|
11512
|
+
}
|
|
11513
|
+
} else {
|
|
11514
|
+
await writeFile30(file, hook.body, "utf8");
|
|
11515
|
+
}
|
|
11516
|
+
await chmod2(file, 493);
|
|
11517
|
+
}
|
|
11518
|
+
ui.success("Installed blocking git enforcement hooks: pre-commit, pre-push");
|
|
11519
|
+
}
|
|
11520
|
+
async function installCiEnforcement(root) {
|
|
11521
|
+
const workflowPath = path45.join(root, ".github", "workflows", "haive-enforcement.yml");
|
|
11522
|
+
await mkdir18(path45.dirname(workflowPath), { recursive: true });
|
|
11523
|
+
if (existsSync67(workflowPath)) {
|
|
11524
|
+
ui.info("GitHub Actions enforcement workflow already exists \u2014 skipped");
|
|
11525
|
+
return;
|
|
11526
|
+
}
|
|
11527
|
+
await writeFile30(workflowPath, `name: haive-enforcement
|
|
11528
|
+
|
|
11529
|
+
on:
|
|
11530
|
+
pull_request:
|
|
11531
|
+
push:
|
|
11532
|
+
branches: [main, master]
|
|
11533
|
+
|
|
11534
|
+
jobs:
|
|
11535
|
+
haive-enforcement:
|
|
11536
|
+
runs-on: ubuntu-latest
|
|
11537
|
+
permissions:
|
|
11538
|
+
contents: read
|
|
11539
|
+
steps:
|
|
11540
|
+
- uses: actions/checkout@v4
|
|
11541
|
+
with:
|
|
11542
|
+
fetch-depth: 0
|
|
11543
|
+
- uses: actions/setup-node@v4
|
|
11544
|
+
with:
|
|
11545
|
+
node-version: '20'
|
|
11546
|
+
- name: Install hAIve
|
|
11547
|
+
run: npm install -g @hiveai/cli
|
|
11548
|
+
- name: Enforce hAIve policy
|
|
11549
|
+
run: haive enforce ci
|
|
11550
|
+
`, "utf8");
|
|
11551
|
+
ui.success(`Created ${path45.relative(root, workflowPath)}`);
|
|
11552
|
+
}
|
|
11553
|
+
function printReport(report, json) {
|
|
11554
|
+
if (json) {
|
|
11555
|
+
console.log(JSON.stringify(report, null, 2));
|
|
11556
|
+
return;
|
|
11557
|
+
}
|
|
11558
|
+
console.log(ui.bold(`hAIve enforcement \u2014 ${report.mode}`));
|
|
11559
|
+
console.log(ui.dim(` root: ${report.root}`));
|
|
11560
|
+
for (const finding of report.findings) {
|
|
11561
|
+
const marker = finding.severity === "error" ? ui.red("\u2717") : finding.severity === "warn" ? ui.yellow("\u26A0") : finding.severity === "ok" ? ui.green("\u2713") : ui.dim("\u2022");
|
|
11562
|
+
console.log(`${marker} ${finding.code}: ${finding.message}`);
|
|
11563
|
+
if (finding.fix) console.log(ui.dim(` fix: ${finding.fix}`));
|
|
11564
|
+
}
|
|
11565
|
+
if (report.should_block) ui.error("hAIve enforcement gate failed.");
|
|
11566
|
+
else ui.success("hAIve enforcement gate passed.");
|
|
11567
|
+
}
|
|
11197
11568
|
async function readHookPayload() {
|
|
11198
11569
|
const raw = await readStdin2(MAX_STDIN_BYTES2);
|
|
11199
11570
|
if (!raw.trim()) return {};
|
|
@@ -11242,15 +11613,46 @@ async function readStdin2(maxBytes) {
|
|
|
11242
11613
|
setTimeout(finish, 2e3);
|
|
11243
11614
|
});
|
|
11244
11615
|
}
|
|
11616
|
+
function runCommand4(cmd, args, cwd) {
|
|
11617
|
+
return new Promise((resolve, reject) => {
|
|
11618
|
+
const proc = spawn5(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
11619
|
+
let stdout = "";
|
|
11620
|
+
let stderr = "";
|
|
11621
|
+
proc.stdout.on("data", (chunk) => {
|
|
11622
|
+
stdout += chunk.toString();
|
|
11623
|
+
});
|
|
11624
|
+
proc.stderr.on("data", (chunk) => {
|
|
11625
|
+
stderr += chunk.toString();
|
|
11626
|
+
});
|
|
11627
|
+
proc.on("error", reject);
|
|
11628
|
+
proc.on("close", (code) => {
|
|
11629
|
+
if (code === 0) resolve(stdout);
|
|
11630
|
+
else reject(new Error(stderr || `${cmd} exited with code ${code}`));
|
|
11631
|
+
});
|
|
11632
|
+
});
|
|
11633
|
+
}
|
|
11634
|
+
|
|
11635
|
+
// src/commands/run.ts
|
|
11636
|
+
import "commander";
|
|
11637
|
+
function registerRun(program2) {
|
|
11638
|
+
program2.command("run").description("Run any AI agent command inside a hAIve-enforced session.").option("-d, --dir <dir>", "project root").option("--task <text>", "task text used for the hAIve briefing marker").allowUnknownOption(true).argument("[cmd]", "agent command to run").argument("[args...]", "agent command arguments").action(async (cmd, args, opts) => {
|
|
11639
|
+
if (!cmd) {
|
|
11640
|
+
ui.error("Usage: haive run -- <agent command> [args...]");
|
|
11641
|
+
process.exit(1);
|
|
11642
|
+
}
|
|
11643
|
+
await runWithEnforcement(cmd, args, opts);
|
|
11644
|
+
});
|
|
11645
|
+
}
|
|
11245
11646
|
|
|
11246
11647
|
// src/index.ts
|
|
11247
|
-
var program = new
|
|
11248
|
-
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.9.
|
|
11648
|
+
var program = new Command49();
|
|
11649
|
+
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.9.10");
|
|
11249
11650
|
registerInit(program);
|
|
11250
11651
|
registerWelcome(program);
|
|
11251
11652
|
registerResolveProject(program);
|
|
11252
11653
|
registerRuntime(program);
|
|
11253
11654
|
registerEnforce(program);
|
|
11655
|
+
registerRun(program);
|
|
11254
11656
|
registerMcp(program);
|
|
11255
11657
|
registerBriefing(program);
|
|
11256
11658
|
registerTui(program);
|