@hiveai/cli 0.9.7 → 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +830 -156
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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,22 +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("
|
|
2182
|
+
ui.warn("hAIve enforcement not fully installed \u2014 run `haive enforce install` manually");
|
|
2173
2183
|
}
|
|
2174
2184
|
try {
|
|
2175
2185
|
ui.info("Building code-map\u2026");
|
|
@@ -2220,8 +2230,8 @@ function registerInit(program2) {
|
|
|
2220
2230
|
console.log(ui.dim(" \u2713 Proposed memories auto-approve after 72h without rejection"));
|
|
2221
2231
|
console.log(ui.dim(" \u2713 Session recap saved automatically when the AI session closes"));
|
|
2222
2232
|
console.log(ui.dim(" \u2713 Code-map refreshes automatically after every pull"));
|
|
2223
|
-
console.log(ui.dim(" \u2713
|
|
2224
|
-
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)"));
|
|
2225
2235
|
if (stacksToSeed.length > 0) {
|
|
2226
2236
|
console.log(ui.dim(` \u2713 Stack memory packs pre-seeded (${stacksToSeed.join(", ")})`));
|
|
2227
2237
|
}
|
|
@@ -2278,7 +2288,7 @@ async function writeCursorHaiveRule(root) {
|
|
|
2278
2288
|
ui.info(`Cursor rule ${relPath} already exists \u2014 skipped`);
|
|
2279
2289
|
return;
|
|
2280
2290
|
}
|
|
2281
|
-
await
|
|
2291
|
+
await mkdir4(path7.dirname(target), { recursive: true });
|
|
2282
2292
|
await writeFile3(target, CURSOR_HAIVE_RULE_MDC, "utf8");
|
|
2283
2293
|
ui.success(`Created Cursor rule ${relPath}`);
|
|
2284
2294
|
}
|
|
@@ -2288,7 +2298,7 @@ async function writeBridge(root, relPath) {
|
|
|
2288
2298
|
ui.info(`Bridge ${relPath} already exists \u2014 skipped`);
|
|
2289
2299
|
return;
|
|
2290
2300
|
}
|
|
2291
|
-
await
|
|
2301
|
+
await mkdir4(path7.dirname(target), { recursive: true });
|
|
2292
2302
|
await writeFile3(target, BRIDGE_BODY, "utf8");
|
|
2293
2303
|
ui.success(`Created bridge ${relPath}`);
|
|
2294
2304
|
}
|
|
@@ -2305,7 +2315,7 @@ var RUNTIME_GITIGNORE_BODY = `*
|
|
|
2305
2315
|
!README.md
|
|
2306
2316
|
`;
|
|
2307
2317
|
async function ensureAiRuntimeLayout(runtimeDir) {
|
|
2308
|
-
await
|
|
2318
|
+
await mkdir4(runtimeDir, { recursive: true });
|
|
2309
2319
|
const gi = path7.join(runtimeDir, ".gitignore");
|
|
2310
2320
|
if (!existsSync6(gi)) {
|
|
2311
2321
|
await writeFile3(gi, RUNTIME_GITIGNORE_BODY, "utf8");
|
|
@@ -2332,7 +2342,7 @@ async function ensureGitignoreEntries(root, patterns) {
|
|
|
2332
2342
|
}
|
|
2333
2343
|
|
|
2334
2344
|
// src/commands/install-hooks.ts
|
|
2335
|
-
import { mkdir as
|
|
2345
|
+
import { mkdir as mkdir6, writeFile as writeFile5, chmod, readFile as readFile6 } from "fs/promises";
|
|
2336
2346
|
import { existsSync as existsSync8 } from "fs";
|
|
2337
2347
|
import path9 from "path";
|
|
2338
2348
|
import "commander";
|
|
@@ -2340,9 +2350,9 @@ import { findProjectRoot as findProjectRoot6 } from "@hiveai/core";
|
|
|
2340
2350
|
|
|
2341
2351
|
// src/utils/claude-hooks.ts
|
|
2342
2352
|
import { existsSync as existsSync7 } from "fs";
|
|
2343
|
-
import { mkdir as
|
|
2353
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
2344
2354
|
import path8 from "path";
|
|
2345
|
-
var HAIVE_HOOK_TAG = "haive-
|
|
2355
|
+
var HAIVE_HOOK_TAG = "haive-enforcement";
|
|
2346
2356
|
var POST_TOOL_USE_GROUP = {
|
|
2347
2357
|
matcher: "Edit|Write|Bash",
|
|
2348
2358
|
hooks: [
|
|
@@ -2353,6 +2363,25 @@ var POST_TOOL_USE_GROUP = {
|
|
|
2353
2363
|
}
|
|
2354
2364
|
]
|
|
2355
2365
|
};
|
|
2366
|
+
var PRE_TOOL_USE_GROUP = {
|
|
2367
|
+
matcher: "Edit|Write|MultiEdit|NotebookEdit|Bash",
|
|
2368
|
+
hooks: [
|
|
2369
|
+
{
|
|
2370
|
+
type: "command",
|
|
2371
|
+
command: "haive enforce pre-tool-use",
|
|
2372
|
+
haive_tag: HAIVE_HOOK_TAG
|
|
2373
|
+
}
|
|
2374
|
+
]
|
|
2375
|
+
};
|
|
2376
|
+
var SESSION_START_GROUP = {
|
|
2377
|
+
hooks: [
|
|
2378
|
+
{
|
|
2379
|
+
type: "command",
|
|
2380
|
+
command: "haive enforce session-start",
|
|
2381
|
+
haive_tag: HAIVE_HOOK_TAG
|
|
2382
|
+
}
|
|
2383
|
+
]
|
|
2384
|
+
};
|
|
2356
2385
|
var SESSION_END_GROUP = {
|
|
2357
2386
|
hooks: [
|
|
2358
2387
|
{
|
|
@@ -2370,6 +2399,14 @@ function dropHaiveGroups(groups) {
|
|
|
2370
2399
|
function patchClaudeSettings(input) {
|
|
2371
2400
|
const settings = input ? { ...input } : {};
|
|
2372
2401
|
const hooks = settings.hooks ? { ...settings.hooks } : {};
|
|
2402
|
+
hooks.SessionStart = [
|
|
2403
|
+
...dropHaiveGroups(hooks.SessionStart ?? []),
|
|
2404
|
+
SESSION_START_GROUP
|
|
2405
|
+
];
|
|
2406
|
+
hooks.PreToolUse = [
|
|
2407
|
+
...dropHaiveGroups(hooks.PreToolUse ?? []),
|
|
2408
|
+
PRE_TOOL_USE_GROUP
|
|
2409
|
+
];
|
|
2373
2410
|
hooks.PostToolUse = [
|
|
2374
2411
|
...dropHaiveGroups(hooks.PostToolUse ?? []),
|
|
2375
2412
|
POST_TOOL_USE_GROUP
|
|
@@ -2410,7 +2447,7 @@ async function installClaudeHooksAtPath(settingsPath) {
|
|
|
2410
2447
|
created = true;
|
|
2411
2448
|
}
|
|
2412
2449
|
const patched = patchClaudeSettings(raw);
|
|
2413
|
-
await
|
|
2450
|
+
await mkdir5(path8.dirname(settingsPath), { recursive: true });
|
|
2414
2451
|
await writeFile4(settingsPath, JSON.stringify(patched, null, 2) + "\n", "utf8");
|
|
2415
2452
|
return { settingsPath, created };
|
|
2416
2453
|
}
|
|
@@ -2447,9 +2484,8 @@ fi
|
|
|
2447
2484
|
var PRE_PUSH_BODY = `#!/bin/sh
|
|
2448
2485
|
${HOOK_MARKER} \u2014 keep this block to allow upgrades. Hand-edit anything outside it.
|
|
2449
2486
|
|
|
2450
|
-
# Before pushing, run
|
|
2451
|
-
#
|
|
2452
|
-
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.
|
|
2453
2489
|
|
|
2454
2490
|
_haive() {
|
|
2455
2491
|
if command -v haive >/dev/null 2>&1; then haive "$@"
|
|
@@ -2458,15 +2494,7 @@ _haive() {
|
|
|
2458
2494
|
fi
|
|
2459
2495
|
}
|
|
2460
2496
|
|
|
2461
|
-
|
|
2462
|
-
LOCAL_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
2463
|
-
REMOTE_SHA=$(git rev-parse --verify "@{u}" 2>/dev/null || echo "")
|
|
2464
|
-
if [ -n "$REMOTE_SHA" ]; then
|
|
2465
|
-
DIFF=$(git diff "$REMOTE_SHA"..HEAD 2>/dev/null || "")
|
|
2466
|
-
if [ -n "$DIFF" ]; then
|
|
2467
|
-
_haive precommit --quiet 2>/dev/null || true
|
|
2468
|
-
fi
|
|
2469
|
-
fi
|
|
2497
|
+
_haive enforce check --stage pre-push --dir . || exit $?
|
|
2470
2498
|
|
|
2471
2499
|
# Remind agent to save session recap if env var is set
|
|
2472
2500
|
if [ "$HAIVE_SESSION_REMINDER" = "1" ]; then
|
|
@@ -2478,7 +2506,19 @@ exit 0
|
|
|
2478
2506
|
var HOOKS = [
|
|
2479
2507
|
{ name: "post-merge", body: POST_MERGE_BODY },
|
|
2480
2508
|
{ name: "post-rewrite", body: POST_MERGE_BODY },
|
|
2481
|
-
{ 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
|
+
}
|
|
2482
2522
|
];
|
|
2483
2523
|
async function installGitHooks(opts) {
|
|
2484
2524
|
const root = findProjectRoot6(opts.dir);
|
|
@@ -2489,7 +2529,7 @@ async function installGitHooks(opts) {
|
|
|
2489
2529
|
return;
|
|
2490
2530
|
}
|
|
2491
2531
|
const hooksDir = path9.join(gitDir, "hooks");
|
|
2492
|
-
await
|
|
2532
|
+
await mkdir6(hooksDir, { recursive: true });
|
|
2493
2533
|
let installed = 0;
|
|
2494
2534
|
let skipped = 0;
|
|
2495
2535
|
for (const { name, body } of HOOKS) {
|
|
@@ -2508,8 +2548,8 @@ async function installGitHooks(opts) {
|
|
|
2508
2548
|
}
|
|
2509
2549
|
ui.success(`Installed ${installed} git hook(s) in .git/hooks/${skipped ? `, skipped ${skipped}` : ""}`);
|
|
2510
2550
|
ui.info("post-merge: haive sync runs after every pull/merge.");
|
|
2511
|
-
ui.info("pre-
|
|
2512
|
-
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.");
|
|
2513
2553
|
}
|
|
2514
2554
|
async function installClaudeHooks(opts) {
|
|
2515
2555
|
const root = findProjectRoot6(opts.dir);
|
|
@@ -2523,7 +2563,7 @@ async function installClaudeHooks(opts) {
|
|
|
2523
2563
|
try {
|
|
2524
2564
|
const result = await installClaudeHooksAtPath(settingsPath);
|
|
2525
2565
|
if (result.created) {
|
|
2526
|
-
ui.success(`Created ${result.settingsPath} with hAIve
|
|
2566
|
+
ui.success(`Created ${result.settingsPath} with hAIve enforcement hooks`);
|
|
2527
2567
|
} else {
|
|
2528
2568
|
ui.success(`Patched ${result.settingsPath} (existing user hooks preserved)`);
|
|
2529
2569
|
}
|
|
@@ -2532,8 +2572,9 @@ async function installClaudeHooks(opts) {
|
|
|
2532
2572
|
process.exitCode = 1;
|
|
2533
2573
|
return;
|
|
2534
2574
|
}
|
|
2535
|
-
ui.info("
|
|
2536
|
-
ui.info("
|
|
2575
|
+
ui.info("SessionStart hook: `haive enforce session-start` injects briefing context");
|
|
2576
|
+
ui.info("PreToolUse hook: blocks Edit/Write/dangerous Bash until briefing is loaded");
|
|
2577
|
+
ui.info("PostToolUse hook: `haive observe` captures Edit/Write/Bash activity");
|
|
2537
2578
|
ui.info("SessionEnd hook: `haive session end --auto --quiet` distills observations");
|
|
2538
2579
|
ui.info(" into a session_recap memory at session close");
|
|
2539
2580
|
ui.info("Restart Claude Code (or open a new conversation) for the hooks to take effect.");
|
|
@@ -2541,7 +2582,7 @@ async function installClaudeHooks(opts) {
|
|
|
2541
2582
|
}
|
|
2542
2583
|
function registerInstallHooks(program2) {
|
|
2543
2584
|
program2.command("install-hooks [target]").description(
|
|
2544
|
-
"Install hAIve hooks. Targets:\n\n git (default) post-merge / post-rewrite / pre-push for haive sync + precommit\n claude PostToolUse + SessionEnd hooks
|
|
2585
|
+
"Install hAIve hooks. Targets:\n\n git (default) post-merge / post-rewrite / pre-push for haive sync + precommit\n claude SessionStart + PreToolUse + PostToolUse + SessionEnd hooks\n for briefing injection, pre-edit blocking, and capture (Claude Code only)\n\n Examples:\n haive install-hooks # git hooks (legacy default)\n haive install-hooks git\n haive install-hooks claude\n haive install-hooks claude --scope project\n haive install-hooks claude --uninstall\n"
|
|
2545
2586
|
).option("-d, --dir <dir>", "project root").option("--force", "overwrite existing hooks (git target only)").option("--scope <scope>", "claude target: 'user' (~/.claude) or 'project' (.claude/)", "user").option("--uninstall", "remove previously installed hAIve hooks (claude target only)").option("--settings <path>", "explicit path to settings.json (claude target only)").action(async (target, opts) => {
|
|
2546
2587
|
const t = (target ?? "git").toLowerCase();
|
|
2547
2588
|
if (t === "git") {
|
|
@@ -2556,7 +2597,7 @@ function registerInstallHooks(program2) {
|
|
|
2556
2597
|
}
|
|
2557
2598
|
|
|
2558
2599
|
// src/commands/observe.ts
|
|
2559
|
-
import { appendFile, mkdir as
|
|
2600
|
+
import { appendFile, mkdir as mkdir7 } from "fs/promises";
|
|
2560
2601
|
import { existsSync as existsSync9 } from "fs";
|
|
2561
2602
|
import path10 from "path";
|
|
2562
2603
|
import "commander";
|
|
@@ -2647,7 +2688,7 @@ function registerObserve(program2) {
|
|
|
2647
2688
|
files: extractFiles(payload)
|
|
2648
2689
|
};
|
|
2649
2690
|
const cacheDir = path10.join(paths.haiveDir, ".cache");
|
|
2650
|
-
await
|
|
2691
|
+
await mkdir7(cacheDir, { recursive: true });
|
|
2651
2692
|
await appendFile(
|
|
2652
2693
|
path10.join(cacheDir, "observations.jsonl"),
|
|
2653
2694
|
JSON.stringify(observation) + "\n",
|
|
@@ -2666,7 +2707,7 @@ import { findProjectRoot as findProjectRoot9 } from "@hiveai/core";
|
|
|
2666
2707
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2667
2708
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2668
2709
|
import { findProjectRoot as findProjectRoot8, resolveHaivePaths as resolveHaivePaths6 } from "@hiveai/core";
|
|
2669
|
-
import { mkdir as
|
|
2710
|
+
import { mkdir as mkdir8, writeFile as writeFile6 } from "fs/promises";
|
|
2670
2711
|
import { existsSync as existsSync10 } from "fs";
|
|
2671
2712
|
import path11 from "path";
|
|
2672
2713
|
import { z } from "zod";
|
|
@@ -2926,6 +2967,7 @@ import { z as z35 } from "zod";
|
|
|
2926
2967
|
import { z as z36 } from "zod";
|
|
2927
2968
|
import { z as z37 } from "zod";
|
|
2928
2969
|
import { z as z38 } from "zod";
|
|
2970
|
+
import { loadConfigSync } from "@hiveai/core";
|
|
2929
2971
|
function createContext(options = {}) {
|
|
2930
2972
|
const env = options.env ?? process.env;
|
|
2931
2973
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -2947,7 +2989,7 @@ async function bootstrapProjectSave(input, ctx) {
|
|
|
2947
2989
|
`${target} already exists. Pass overwrite=true to replace it.`
|
|
2948
2990
|
);
|
|
2949
2991
|
}
|
|
2950
|
-
await
|
|
2992
|
+
await mkdir8(path11.dirname(target), { recursive: true });
|
|
2951
2993
|
await writeFile6(target, input.content, "utf8");
|
|
2952
2994
|
return {
|
|
2953
2995
|
file_path: target,
|
|
@@ -6030,7 +6072,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
6030
6072
|
};
|
|
6031
6073
|
}
|
|
6032
6074
|
var SERVER_NAME = "haive";
|
|
6033
|
-
var SERVER_VERSION = "0.9.
|
|
6075
|
+
var SERVER_VERSION = "0.9.9";
|
|
6034
6076
|
function jsonResult(data) {
|
|
6035
6077
|
return {
|
|
6036
6078
|
content: [
|
|
@@ -6041,15 +6083,70 @@ function jsonResult(data) {
|
|
|
6041
6083
|
]
|
|
6042
6084
|
};
|
|
6043
6085
|
}
|
|
6086
|
+
var ENFORCEMENT_PROFILE_TOOLS = /* @__PURE__ */ new Set([
|
|
6087
|
+
"get_briefing",
|
|
6088
|
+
"mem_save",
|
|
6089
|
+
"mem_tried",
|
|
6090
|
+
"mem_search",
|
|
6091
|
+
"mem_get",
|
|
6092
|
+
"mem_update",
|
|
6093
|
+
"mem_verify",
|
|
6094
|
+
"mem_relevant_to",
|
|
6095
|
+
"code_map",
|
|
6096
|
+
"pre_commit_check"
|
|
6097
|
+
]);
|
|
6098
|
+
var BRIEFING_TOOLS = /* @__PURE__ */ new Set(["get_briefing", "mem_relevant_to"]);
|
|
6099
|
+
var MUTATING_TOOLS = /* @__PURE__ */ new Set([
|
|
6100
|
+
"mem_save",
|
|
6101
|
+
"mem_tried",
|
|
6102
|
+
"mem_observe",
|
|
6103
|
+
"mem_session_end",
|
|
6104
|
+
"bootstrap_project_save",
|
|
6105
|
+
"mem_update",
|
|
6106
|
+
"mem_approve",
|
|
6107
|
+
"mem_reject",
|
|
6108
|
+
"mem_delete",
|
|
6109
|
+
"runtime_journal_append",
|
|
6110
|
+
"pattern_detect"
|
|
6111
|
+
]);
|
|
6044
6112
|
function createHaiveServer(options = {}) {
|
|
6045
6113
|
const context = createContext(options);
|
|
6114
|
+
const config = loadConfigSync(context.paths);
|
|
6115
|
+
const toolProfile = options.env?.HAIVE_TOOL_PROFILE ?? config.enforcement?.toolProfile ?? "enforcement";
|
|
6116
|
+
const requireBriefingFirst = options.env?.HAIVE_REQUIRE_BRIEFING_FIRST === "0" ? false : config.enforcement?.requireBriefingFirst ?? true;
|
|
6117
|
+
let briefingLoaded = false;
|
|
6046
6118
|
const tracker = new SessionTracker(context);
|
|
6047
6119
|
void tracker.init();
|
|
6048
6120
|
const server = new McpServer(
|
|
6049
6121
|
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
6050
6122
|
{ capabilities: { tools: {}, prompts: {} } }
|
|
6051
6123
|
);
|
|
6052
|
-
|
|
6124
|
+
const shouldRegisterTool = (name) => toolProfile === "full" || ENFORCEMENT_PROFILE_TOOLS.has(name);
|
|
6125
|
+
const registerTool = (name, description, schema, handler) => {
|
|
6126
|
+
if (!shouldRegisterTool(name)) return;
|
|
6127
|
+
const tool = server.tool.bind(server);
|
|
6128
|
+
tool(
|
|
6129
|
+
name,
|
|
6130
|
+
description,
|
|
6131
|
+
schema,
|
|
6132
|
+
async (input) => {
|
|
6133
|
+
if (BRIEFING_TOOLS.has(name)) {
|
|
6134
|
+
briefingLoaded = true;
|
|
6135
|
+
return await handler(input);
|
|
6136
|
+
}
|
|
6137
|
+
if (requireBriefingFirst && MUTATING_TOOLS.has(name) && !briefingLoaded) {
|
|
6138
|
+
return jsonResult({
|
|
6139
|
+
error: "haive_briefing_required",
|
|
6140
|
+
message: "This hAIve project requires get_briefing or mem_relevant_to before state-changing hAIve tools. Call get_briefing({ task: '...' }) first.",
|
|
6141
|
+
tool: name
|
|
6142
|
+
});
|
|
6143
|
+
}
|
|
6144
|
+
return await handler(input);
|
|
6145
|
+
}
|
|
6146
|
+
);
|
|
6147
|
+
};
|
|
6148
|
+
const shouldRegisterPrompt = (name) => toolProfile === "full" || name === "bootstrap_project" || name === "post_task";
|
|
6149
|
+
registerTool(
|
|
6053
6150
|
"mem_save",
|
|
6054
6151
|
[
|
|
6055
6152
|
"Save a piece of knowledge as a persistent memory that survives across AI sessions.",
|
|
@@ -6081,7 +6178,7 @@ function createHaiveServer(options = {}) {
|
|
|
6081
6178
|
return jsonResult(await memSave(input, context));
|
|
6082
6179
|
}
|
|
6083
6180
|
);
|
|
6084
|
-
|
|
6181
|
+
registerTool(
|
|
6085
6182
|
"mem_suggest_topic",
|
|
6086
6183
|
[
|
|
6087
6184
|
"Propose a stable `topic` key (topic-upsert) from type + short title.",
|
|
@@ -6098,7 +6195,7 @@ function createHaiveServer(options = {}) {
|
|
|
6098
6195
|
MemSuggestTopicInputSchema,
|
|
6099
6196
|
async (input) => jsonResult(await memSuggestTopic(input, context))
|
|
6100
6197
|
);
|
|
6101
|
-
|
|
6198
|
+
registerTool(
|
|
6102
6199
|
"mem_tried",
|
|
6103
6200
|
[
|
|
6104
6201
|
"Record a FAILED approach so future agents don't repeat the same mistake.",
|
|
@@ -6125,7 +6222,7 @@ function createHaiveServer(options = {}) {
|
|
|
6125
6222
|
return jsonResult(await memTried(input, context));
|
|
6126
6223
|
}
|
|
6127
6224
|
);
|
|
6128
|
-
|
|
6225
|
+
registerTool(
|
|
6129
6226
|
"mem_observe",
|
|
6130
6227
|
[
|
|
6131
6228
|
"Capture a code-level discovery made WHILE READING existing code.",
|
|
@@ -6157,7 +6254,7 @@ function createHaiveServer(options = {}) {
|
|
|
6157
6254
|
return jsonResult(await memObserve(input, context));
|
|
6158
6255
|
}
|
|
6159
6256
|
);
|
|
6160
|
-
|
|
6257
|
+
registerTool(
|
|
6161
6258
|
"mem_session_end",
|
|
6162
6259
|
[
|
|
6163
6260
|
"Save an end-of-session recap so the NEXT session starts with fresh context.",
|
|
@@ -6186,7 +6283,7 @@ function createHaiveServer(options = {}) {
|
|
|
6186
6283
|
return jsonResult(await memSessionEnd(input, context));
|
|
6187
6284
|
}
|
|
6188
6285
|
);
|
|
6189
|
-
|
|
6286
|
+
registerTool(
|
|
6190
6287
|
"get_briefing",
|
|
6191
6288
|
[
|
|
6192
6289
|
"\u2B50 DEFAULT-FIRST for coding agents on any repo where `haive init` ran: call this BEFORE",
|
|
@@ -6237,7 +6334,7 @@ function createHaiveServer(options = {}) {
|
|
|
6237
6334
|
return jsonResult(await getBriefing(input, context));
|
|
6238
6335
|
}
|
|
6239
6336
|
);
|
|
6240
|
-
|
|
6337
|
+
registerTool(
|
|
6241
6338
|
"mem_search",
|
|
6242
6339
|
[
|
|
6243
6340
|
"Search memories by keyword or semantic similarity.",
|
|
@@ -6269,7 +6366,7 @@ function createHaiveServer(options = {}) {
|
|
|
6269
6366
|
return jsonResult(await memSearch(input, context));
|
|
6270
6367
|
}
|
|
6271
6368
|
);
|
|
6272
|
-
|
|
6369
|
+
registerTool(
|
|
6273
6370
|
"mem_timeline",
|
|
6274
6371
|
[
|
|
6275
6372
|
"Chronological view of related memories: by shared frontmatter.topic OR expanded from a seed id",
|
|
@@ -6285,7 +6382,7 @@ function createHaiveServer(options = {}) {
|
|
|
6285
6382
|
MemTimelineInputSchema,
|
|
6286
6383
|
async (input) => jsonResult(await memTimeline(input, context))
|
|
6287
6384
|
);
|
|
6288
|
-
|
|
6385
|
+
registerTool(
|
|
6289
6386
|
"mem_for_files",
|
|
6290
6387
|
[
|
|
6291
6388
|
"Surface memories relevant to the files you are currently editing.",
|
|
@@ -6309,7 +6406,7 @@ function createHaiveServer(options = {}) {
|
|
|
6309
6406
|
MemForFilesInputSchema,
|
|
6310
6407
|
async (input) => jsonResult(await memForFiles(input, context))
|
|
6311
6408
|
);
|
|
6312
|
-
|
|
6409
|
+
registerTool(
|
|
6313
6410
|
"mem_get",
|
|
6314
6411
|
[
|
|
6315
6412
|
"Fetch a single memory by its full id with all details.",
|
|
@@ -6325,7 +6422,7 @@ function createHaiveServer(options = {}) {
|
|
|
6325
6422
|
MemGetInputSchema,
|
|
6326
6423
|
async (input) => jsonResult(await memGet(input, context))
|
|
6327
6424
|
);
|
|
6328
|
-
|
|
6425
|
+
registerTool(
|
|
6329
6426
|
"mem_list",
|
|
6330
6427
|
[
|
|
6331
6428
|
"List memories with optional filters. Use for browsing, not for task onboarding.",
|
|
@@ -6345,7 +6442,7 @@ function createHaiveServer(options = {}) {
|
|
|
6345
6442
|
MemListInputSchema,
|
|
6346
6443
|
async (input) => jsonResult(await memList(input, context))
|
|
6347
6444
|
);
|
|
6348
|
-
|
|
6445
|
+
registerTool(
|
|
6349
6446
|
"get_project_context",
|
|
6350
6447
|
[
|
|
6351
6448
|
"Read .ai/project-context.md (and optionally a module context) directly.",
|
|
@@ -6363,7 +6460,7 @@ function createHaiveServer(options = {}) {
|
|
|
6363
6460
|
GetProjectContextInputSchema,
|
|
6364
6461
|
async (input) => jsonResult(await getProjectContext(input, context))
|
|
6365
6462
|
);
|
|
6366
|
-
|
|
6463
|
+
registerTool(
|
|
6367
6464
|
"bootstrap_project_save",
|
|
6368
6465
|
[
|
|
6369
6466
|
"Persist the project context document (.ai/project-context.md) or a module",
|
|
@@ -6381,7 +6478,7 @@ function createHaiveServer(options = {}) {
|
|
|
6381
6478
|
BootstrapProjectSaveInputSchema,
|
|
6382
6479
|
async (input) => jsonResult(await bootstrapProjectSave(input, context))
|
|
6383
6480
|
);
|
|
6384
|
-
|
|
6481
|
+
registerTool(
|
|
6385
6482
|
"code_map",
|
|
6386
6483
|
[
|
|
6387
6484
|
"Look up where symbols (classes, functions, interfaces) are defined in the codebase.",
|
|
@@ -6402,7 +6499,7 @@ function createHaiveServer(options = {}) {
|
|
|
6402
6499
|
CodeMapInputSchema,
|
|
6403
6500
|
async (input) => jsonResult(await codeMapTool(input, context))
|
|
6404
6501
|
);
|
|
6405
|
-
|
|
6502
|
+
registerTool(
|
|
6406
6503
|
"mem_resolve_project",
|
|
6407
6504
|
[
|
|
6408
6505
|
"Diagnostics: resolve which project root hAIve is using (never throws).",
|
|
@@ -6418,7 +6515,7 @@ function createHaiveServer(options = {}) {
|
|
|
6418
6515
|
MemResolveProjectInputSchema,
|
|
6419
6516
|
async (input) => jsonResult(await memResolveProject(input, context))
|
|
6420
6517
|
);
|
|
6421
|
-
|
|
6518
|
+
registerTool(
|
|
6422
6519
|
"mem_update",
|
|
6423
6520
|
[
|
|
6424
6521
|
"Update the body, tags, or anchor of an existing memory in-place.",
|
|
@@ -6441,7 +6538,7 @@ function createHaiveServer(options = {}) {
|
|
|
6441
6538
|
MemUpdateInputSchema,
|
|
6442
6539
|
async (input) => jsonResult(await memUpdate(input, context))
|
|
6443
6540
|
);
|
|
6444
|
-
|
|
6541
|
+
registerTool(
|
|
6445
6542
|
"mem_verify",
|
|
6446
6543
|
[
|
|
6447
6544
|
"Check whether memory anchor paths and symbols still exist in the current code.",
|
|
@@ -6460,7 +6557,7 @@ function createHaiveServer(options = {}) {
|
|
|
6460
6557
|
MemVerifyInputSchema,
|
|
6461
6558
|
async (input) => jsonResult(await memVerify(input, context))
|
|
6462
6559
|
);
|
|
6463
|
-
|
|
6560
|
+
registerTool(
|
|
6464
6561
|
"mem_approve",
|
|
6465
6562
|
[
|
|
6466
6563
|
"Mark a memory as validated (trusted, approved by a human or the team).",
|
|
@@ -6476,7 +6573,7 @@ function createHaiveServer(options = {}) {
|
|
|
6476
6573
|
MemApproveInputSchema,
|
|
6477
6574
|
async (input) => jsonResult(await memApprove(input, context))
|
|
6478
6575
|
);
|
|
6479
|
-
|
|
6576
|
+
registerTool(
|
|
6480
6577
|
"mem_reject",
|
|
6481
6578
|
[
|
|
6482
6579
|
"Mark a memory as rejected and record a reason.",
|
|
@@ -6494,7 +6591,7 @@ function createHaiveServer(options = {}) {
|
|
|
6494
6591
|
MemRejectInputSchema,
|
|
6495
6592
|
async (input) => jsonResult(await memReject(input, context))
|
|
6496
6593
|
);
|
|
6497
|
-
|
|
6594
|
+
registerTool(
|
|
6498
6595
|
"mem_pending",
|
|
6499
6596
|
[
|
|
6500
6597
|
"List memories in 'proposed' status awaiting review, sorted by read count.",
|
|
@@ -6510,7 +6607,7 @@ function createHaiveServer(options = {}) {
|
|
|
6510
6607
|
MemPendingInputSchema,
|
|
6511
6608
|
async (input) => jsonResult(await memPending(input, context))
|
|
6512
6609
|
);
|
|
6513
|
-
|
|
6610
|
+
registerTool(
|
|
6514
6611
|
"mem_delete",
|
|
6515
6612
|
[
|
|
6516
6613
|
"Permanently delete a memory by id.",
|
|
@@ -6527,7 +6624,7 @@ function createHaiveServer(options = {}) {
|
|
|
6527
6624
|
MemDeleteInputSchema,
|
|
6528
6625
|
async (input) => jsonResult(await memDelete(input, context))
|
|
6529
6626
|
);
|
|
6530
|
-
|
|
6627
|
+
registerTool(
|
|
6531
6628
|
"get_recap",
|
|
6532
6629
|
[
|
|
6533
6630
|
"Return ONLY the most recent session_recap. Cheaper than get_briefing when",
|
|
@@ -6545,7 +6642,7 @@ function createHaiveServer(options = {}) {
|
|
|
6545
6642
|
return jsonResult(await getRecap(input, context));
|
|
6546
6643
|
}
|
|
6547
6644
|
);
|
|
6548
|
-
|
|
6645
|
+
registerTool(
|
|
6549
6646
|
"mem_relevant_to",
|
|
6550
6647
|
[
|
|
6551
6648
|
"One-shot ranked memories for a task \u2014 use instead of get_briefing when",
|
|
@@ -6571,7 +6668,7 @@ function createHaiveServer(options = {}) {
|
|
|
6571
6668
|
return jsonResult(await memRelevantTo(input, context));
|
|
6572
6669
|
}
|
|
6573
6670
|
);
|
|
6574
|
-
|
|
6671
|
+
registerTool(
|
|
6575
6672
|
"code_search",
|
|
6576
6673
|
[
|
|
6577
6674
|
"Semantic search over the codebase \u2014 finds exported symbols (functions, classes,",
|
|
@@ -6594,7 +6691,7 @@ function createHaiveServer(options = {}) {
|
|
|
6594
6691
|
return jsonResult(await codeSearch(input, context));
|
|
6595
6692
|
}
|
|
6596
6693
|
);
|
|
6597
|
-
|
|
6694
|
+
registerTool(
|
|
6598
6695
|
"why_this_file",
|
|
6599
6696
|
[
|
|
6600
6697
|
"One-shot file-context lookup: combines recent git history, memories anchored",
|
|
@@ -6614,7 +6711,7 @@ function createHaiveServer(options = {}) {
|
|
|
6614
6711
|
return jsonResult(await whyThisFile(input, context));
|
|
6615
6712
|
}
|
|
6616
6713
|
);
|
|
6617
|
-
|
|
6714
|
+
registerTool(
|
|
6618
6715
|
"anti_patterns_check",
|
|
6619
6716
|
[
|
|
6620
6717
|
"Scan a diff (or set of paths) against documented attempt/gotcha memories.",
|
|
@@ -6637,7 +6734,7 @@ function createHaiveServer(options = {}) {
|
|
|
6637
6734
|
return jsonResult(await antiPatternsCheck(input, context));
|
|
6638
6735
|
}
|
|
6639
6736
|
);
|
|
6640
|
-
|
|
6737
|
+
registerTool(
|
|
6641
6738
|
"mem_distill",
|
|
6642
6739
|
[
|
|
6643
6740
|
"Cluster recurring observations / failed attempts so a human can collapse",
|
|
@@ -6662,7 +6759,7 @@ function createHaiveServer(options = {}) {
|
|
|
6662
6759
|
return jsonResult(await memDistill(input, context));
|
|
6663
6760
|
}
|
|
6664
6761
|
);
|
|
6665
|
-
|
|
6762
|
+
registerTool(
|
|
6666
6763
|
"why_this_decision",
|
|
6667
6764
|
[
|
|
6668
6765
|
"Trace the genealogy of a memory (especially decision/architecture):",
|
|
@@ -6684,7 +6781,7 @@ function createHaiveServer(options = {}) {
|
|
|
6684
6781
|
return jsonResult(await whyThisDecision(input, context));
|
|
6685
6782
|
}
|
|
6686
6783
|
);
|
|
6687
|
-
|
|
6784
|
+
registerTool(
|
|
6688
6785
|
"mem_conflicts_with",
|
|
6689
6786
|
[
|
|
6690
6787
|
"Detect memories that potentially CONTRADICT a given memory.",
|
|
@@ -6710,7 +6807,7 @@ function createHaiveServer(options = {}) {
|
|
|
6710
6807
|
return jsonResult(await memConflicts(input, context));
|
|
6711
6808
|
}
|
|
6712
6809
|
);
|
|
6713
|
-
|
|
6810
|
+
registerTool(
|
|
6714
6811
|
"mem_conflict_candidates",
|
|
6715
6812
|
[
|
|
6716
6813
|
"Bulk scan for conflict CANDIDATES (not proof):",
|
|
@@ -6731,7 +6828,7 @@ function createHaiveServer(options = {}) {
|
|
|
6731
6828
|
return jsonResult(await memConflictCandidates(input, context));
|
|
6732
6829
|
}
|
|
6733
6830
|
);
|
|
6734
|
-
|
|
6831
|
+
registerTool(
|
|
6735
6832
|
"runtime_journal_append",
|
|
6736
6833
|
[
|
|
6737
6834
|
"Append one line to `.ai/.runtime/session-journal.ndjson` \u2014 machine-local session continuity.",
|
|
@@ -6745,7 +6842,7 @@ function createHaiveServer(options = {}) {
|
|
|
6745
6842
|
RuntimeJournalAppendInputSchema,
|
|
6746
6843
|
async (input) => jsonResult(await runtimeJournalAppend(input, context))
|
|
6747
6844
|
);
|
|
6748
|
-
|
|
6845
|
+
registerTool(
|
|
6749
6846
|
"runtime_journal_tail",
|
|
6750
6847
|
[
|
|
6751
6848
|
"Read the last N entries from the runtime session journal (parsed JSON lines).",
|
|
@@ -6757,7 +6854,7 @@ function createHaiveServer(options = {}) {
|
|
|
6757
6854
|
RuntimeJournalTailInputSchema,
|
|
6758
6855
|
async (input) => jsonResult(await runtimeJournalTail(input, context))
|
|
6759
6856
|
);
|
|
6760
|
-
|
|
6857
|
+
registerTool(
|
|
6761
6858
|
"pre_commit_check",
|
|
6762
6859
|
[
|
|
6763
6860
|
"One-shot 'should I block this commit?' check. Combines three signals:",
|
|
@@ -6782,7 +6879,7 @@ function createHaiveServer(options = {}) {
|
|
|
6782
6879
|
return jsonResult(await preCommitCheck(input, context));
|
|
6783
6880
|
}
|
|
6784
6881
|
);
|
|
6785
|
-
|
|
6882
|
+
registerTool(
|
|
6786
6883
|
"pattern_detect",
|
|
6787
6884
|
[
|
|
6788
6885
|
"Heuristic memory detector \u2014 finds knowledge worth saving WITHOUT calling an LLM.",
|
|
@@ -6813,7 +6910,7 @@ function createHaiveServer(options = {}) {
|
|
|
6813
6910
|
return jsonResult(await patternDetect(input, context));
|
|
6814
6911
|
}
|
|
6815
6912
|
);
|
|
6816
|
-
|
|
6913
|
+
registerTool(
|
|
6817
6914
|
"mem_diff",
|
|
6818
6915
|
[
|
|
6819
6916
|
"Compare two memories side-by-side to decide if they should be merged.",
|
|
@@ -6830,44 +6927,50 @@ function createHaiveServer(options = {}) {
|
|
|
6830
6927
|
MemDiffInputSchema,
|
|
6831
6928
|
async (input) => jsonResult(await memDiff(input, context))
|
|
6832
6929
|
);
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
"
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
"
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6930
|
+
if (shouldRegisterPrompt("bootstrap_project")) {
|
|
6931
|
+
server.prompt(
|
|
6932
|
+
"bootstrap_project",
|
|
6933
|
+
[
|
|
6934
|
+
"Analyze the project codebase and write .ai/project-context.md \u2014 run once after haive init.",
|
|
6935
|
+
"The AI explores the directory structure, reads key files (package.json, README, config),",
|
|
6936
|
+
"identifies the tech stack, architectural patterns, key modules, and conventions,",
|
|
6937
|
+
"then persists everything via bootstrap_project_save.",
|
|
6938
|
+
"For multi-component projects, run with module param to create .ai/modules/<name>/context.md."
|
|
6939
|
+
].join(" "),
|
|
6940
|
+
BootstrapProjectArgsSchema,
|
|
6941
|
+
(args) => bootstrapProjectPrompt(args, context)
|
|
6942
|
+
);
|
|
6943
|
+
}
|
|
6944
|
+
if (shouldRegisterPrompt("post_task")) {
|
|
6945
|
+
server.prompt(
|
|
6946
|
+
"post_task",
|
|
6947
|
+
[
|
|
6948
|
+
"\u2B50 Post-task reflection \u2014 run at the end of every session to capture what you learned:",
|
|
6949
|
+
"failed approaches (mem_tried), new conventions/decisions/gotchas (mem_save),",
|
|
6950
|
+
"code discoveries (mem_observe), and an end-of-session recap (mem_session_end).",
|
|
6951
|
+
"In autopilot mode a minimal recap saves automatically; calling this produces a richer one."
|
|
6952
|
+
].join(" "),
|
|
6953
|
+
PostTaskArgsSchema,
|
|
6954
|
+
(args) => postTaskPrompt(args, context)
|
|
6955
|
+
);
|
|
6956
|
+
}
|
|
6957
|
+
if (shouldRegisterPrompt("import_docs")) {
|
|
6958
|
+
server.prompt(
|
|
6959
|
+
"import_docs",
|
|
6960
|
+
[
|
|
6961
|
+
"Import knowledge from a document (README, ADR, wiki, API spec) as hAIve memories.",
|
|
6962
|
+
"Pass the full document content; the AI extracts up to 10 actionable memories",
|
|
6963
|
+
"(conventions, decisions, gotchas, architecture) and saves them via mem_save.",
|
|
6964
|
+
"Good candidates: ADRs, onboarding docs, runbooks, team wikis."
|
|
6965
|
+
].join(" "),
|
|
6966
|
+
ImportDocsArgsSchema,
|
|
6967
|
+
(args) => importDocsPrompt(args, context)
|
|
6968
|
+
);
|
|
6969
|
+
}
|
|
6867
6970
|
return { server, context, tracker };
|
|
6868
6971
|
}
|
|
6869
6972
|
async function runHaiveMcpStdio(options) {
|
|
6870
|
-
const { server, context } = createHaiveServer({ root: options.root });
|
|
6973
|
+
const { server, context } = createHaiveServer({ root: options.root, env: process.env });
|
|
6871
6974
|
console.error(
|
|
6872
6975
|
`[haive-mcp] starting server v${SERVER_VERSION} (project root: ${context.paths.root})`
|
|
6873
6976
|
);
|
|
@@ -6893,7 +6996,7 @@ function registerMcp(program2) {
|
|
|
6893
6996
|
|
|
6894
6997
|
// src/commands/sync.ts
|
|
6895
6998
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
6896
|
-
import { readFile as readFile8, writeFile as writeFile13, mkdir as
|
|
6999
|
+
import { readFile as readFile8, writeFile as writeFile13, mkdir as mkdir9 } from "fs/promises";
|
|
6897
7000
|
import { existsSync as existsSync29 } from "fs";
|
|
6898
7001
|
import path12 from "path";
|
|
6899
7002
|
import "commander";
|
|
@@ -7169,7 +7272,7 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7169
7272
|
topic: `dep-bump-${slugParts}`
|
|
7170
7273
|
});
|
|
7171
7274
|
const teamDir = path12.join(paths.memoriesDir, "team");
|
|
7172
|
-
await
|
|
7275
|
+
await mkdir9(teamDir, { recursive: true });
|
|
7173
7276
|
await writeFile13(
|
|
7174
7277
|
path12.join(teamDir, `${fm.id}.md`),
|
|
7175
7278
|
serializeMemory11({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
@@ -7236,7 +7339,7 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
7236
7339
|
topic: `contract-breaking-${diff.contract}`
|
|
7237
7340
|
});
|
|
7238
7341
|
const teamDir = path12.join(paths.memoriesDir, "team");
|
|
7239
|
-
await
|
|
7342
|
+
await mkdir9(teamDir, { recursive: true });
|
|
7240
7343
|
await writeFile13(
|
|
7241
7344
|
path12.join(teamDir, `${fm.id}.md`),
|
|
7242
7345
|
serializeMemory11({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
@@ -7375,7 +7478,7 @@ function collectSinceChanges(root, ref) {
|
|
|
7375
7478
|
|
|
7376
7479
|
// src/commands/memory-add.ts
|
|
7377
7480
|
import { createHash as createHash2 } from "crypto";
|
|
7378
|
-
import { mkdir as
|
|
7481
|
+
import { mkdir as mkdir10, readFile as readFile9, writeFile as writeFile14 } from "fs/promises";
|
|
7379
7482
|
import { existsSync as existsSync30 } from "fs";
|
|
7380
7483
|
import path13 from "path";
|
|
7381
7484
|
import "commander";
|
|
@@ -7510,7 +7613,7 @@ TODO \u2014 write the memory body.
|
|
|
7510
7613
|
topic: opts.topic
|
|
7511
7614
|
});
|
|
7512
7615
|
const file = memoryFilePath6(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
7513
|
-
await
|
|
7616
|
+
await mkdir10(path13.dirname(file), { recursive: true });
|
|
7514
7617
|
if (existsSync30(file)) {
|
|
7515
7618
|
ui.error(`Memory already exists at ${file}`);
|
|
7516
7619
|
process.exitCode = 1;
|
|
@@ -7643,7 +7746,7 @@ function matchesFilters(loaded, opts) {
|
|
|
7643
7746
|
}
|
|
7644
7747
|
|
|
7645
7748
|
// src/commands/memory-promote.ts
|
|
7646
|
-
import { mkdir as
|
|
7749
|
+
import { mkdir as mkdir11, unlink as unlink2, writeFile as writeFile15 } from "fs/promises";
|
|
7647
7750
|
import { existsSync as existsSync33 } from "fs";
|
|
7648
7751
|
import path15 from "path";
|
|
7649
7752
|
import "commander";
|
|
@@ -7692,7 +7795,7 @@ function registerMemoryPromote(memory2) {
|
|
|
7692
7795
|
body: found.memory.body
|
|
7693
7796
|
};
|
|
7694
7797
|
const newPath = memoryFilePath7(paths, "team", updated.frontmatter.id);
|
|
7695
|
-
await
|
|
7798
|
+
await mkdir11(path15.dirname(newPath), { recursive: true });
|
|
7696
7799
|
await writeFile15(newPath, serializeMemory13(updated), "utf8");
|
|
7697
7800
|
await unlink2(found.filePath);
|
|
7698
7801
|
ui.success(`Promoted ${id} to team scope (status=proposed)`);
|
|
@@ -8152,7 +8255,7 @@ function registerMemoryHot(memory2) {
|
|
|
8152
8255
|
}
|
|
8153
8256
|
|
|
8154
8257
|
// src/commands/memory-tried.ts
|
|
8155
|
-
import { mkdir as
|
|
8258
|
+
import { mkdir as mkdir12, writeFile as writeFile19 } from "fs/promises";
|
|
8156
8259
|
import { existsSync as existsSync40 } from "fs";
|
|
8157
8260
|
import path23 from "path";
|
|
8158
8261
|
import "commander";
|
|
@@ -8205,7 +8308,7 @@ function registerMemoryTried(memory2) {
|
|
|
8205
8308
|
}
|
|
8206
8309
|
const body = lines.join("\n") + "\n";
|
|
8207
8310
|
const file = memoryFilePath8(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
8208
|
-
await
|
|
8311
|
+
await mkdir12(path23.dirname(file), { recursive: true });
|
|
8209
8312
|
if (existsSync40(file)) {
|
|
8210
8313
|
ui.error(`Memory already exists at ${file}`);
|
|
8211
8314
|
process.exitCode = 1;
|
|
@@ -8705,7 +8808,7 @@ function registerMemoryImport(memory2) {
|
|
|
8705
8808
|
|
|
8706
8809
|
// src/commands/memory-import-changelog.ts
|
|
8707
8810
|
import { existsSync as existsSync50 } from "fs";
|
|
8708
|
-
import { readFile as readFile13, mkdir as
|
|
8811
|
+
import { readFile as readFile13, mkdir as mkdir13, writeFile as writeFile23 } from "fs/promises";
|
|
8709
8812
|
import path30 from "path";
|
|
8710
8813
|
import "commander";
|
|
8711
8814
|
import {
|
|
@@ -8802,7 +8905,7 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
8802
8905
|
const pkgName = opts.package ?? path30.basename(path30.dirname(changelogPath));
|
|
8803
8906
|
const scope = opts.scope ?? "team";
|
|
8804
8907
|
const teamDir = path30.join(paths.memoriesDir, scope);
|
|
8805
|
-
await
|
|
8908
|
+
await mkdir13(teamDir, { recursive: true });
|
|
8806
8909
|
let saved = 0;
|
|
8807
8910
|
for (const entry of entries) {
|
|
8808
8911
|
const lines = [];
|
|
@@ -8973,7 +9076,7 @@ function registerMemoryDigest(program2) {
|
|
|
8973
9076
|
}
|
|
8974
9077
|
|
|
8975
9078
|
// src/commands/session-end.ts
|
|
8976
|
-
import { writeFile as writeFile25, mkdir as
|
|
9079
|
+
import { writeFile as writeFile25, mkdir as mkdir14, readFile as readFile14, rm as rm2 } from "fs/promises";
|
|
8977
9080
|
import { existsSync as existsSync53 } from "fs";
|
|
8978
9081
|
import path33 from "path";
|
|
8979
9082
|
import "commander";
|
|
@@ -9148,7 +9251,7 @@ function registerSessionEnd(session2) {
|
|
|
9148
9251
|
status: "validated"
|
|
9149
9252
|
});
|
|
9150
9253
|
const file = memoryFilePath9(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
9151
|
-
await
|
|
9254
|
+
await mkdir14(path33.dirname(file), { recursive: true });
|
|
9152
9255
|
await writeFile25(file, serializeMemory21({ frontmatter, body }), "utf8");
|
|
9153
9256
|
await cleanupObservations();
|
|
9154
9257
|
if (!opts.quiet) {
|
|
@@ -9332,7 +9435,7 @@ function detectFormat(filePath) {
|
|
|
9332
9435
|
|
|
9333
9436
|
// src/commands/hub.ts
|
|
9334
9437
|
import { existsSync as existsSync55 } from "fs";
|
|
9335
|
-
import { mkdir as
|
|
9438
|
+
import { mkdir as mkdir15, readFile as readFile15, writeFile as writeFile26, copyFile } from "fs/promises";
|
|
9336
9439
|
import path35 from "path";
|
|
9337
9440
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
9338
9441
|
import "commander";
|
|
@@ -9353,7 +9456,7 @@ function registerHub(program2) {
|
|
|
9353
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"
|
|
9354
9457
|
).action(async (hubPath) => {
|
|
9355
9458
|
const absPath = path35.resolve(hubPath);
|
|
9356
|
-
await
|
|
9459
|
+
await mkdir15(absPath, { recursive: true });
|
|
9357
9460
|
const gitCheck = spawnSync3("git", ["rev-parse", "--git-dir"], { cwd: absPath });
|
|
9358
9461
|
if (gitCheck.status !== 0) {
|
|
9359
9462
|
const init = spawnSync3("git", ["init"], { cwd: absPath, encoding: "utf8" });
|
|
@@ -9364,7 +9467,7 @@ function registerHub(program2) {
|
|
|
9364
9467
|
}
|
|
9365
9468
|
}
|
|
9366
9469
|
const sharedDir = path35.join(absPath, ".ai", "memories", "shared");
|
|
9367
|
-
await
|
|
9470
|
+
await mkdir15(sharedDir, { recursive: true });
|
|
9368
9471
|
await writeFile26(
|
|
9369
9472
|
path35.join(absPath, ".ai", "README.md"),
|
|
9370
9473
|
`# hAIve Team Knowledge Hub
|
|
@@ -9440,7 +9543,7 @@ Next steps:
|
|
|
9440
9543
|
}
|
|
9441
9544
|
const projectName = path35.basename(root);
|
|
9442
9545
|
const destDir = path35.join(hubRoot, ".ai", "memories", "shared", projectName);
|
|
9443
|
-
await
|
|
9546
|
+
await mkdir15(destDir, { recursive: true });
|
|
9444
9547
|
const all = await loadMemoriesFromDir28(paths.memoriesDir);
|
|
9445
9548
|
const shared = all.filter(
|
|
9446
9549
|
({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && // Don't push imported memories (avoid echo loops)
|
|
@@ -9519,7 +9622,7 @@ Next steps:
|
|
|
9519
9622
|
for (const sourceName of projectDirs) {
|
|
9520
9623
|
const sourceDir = path35.join(hubSharedDir, sourceName);
|
|
9521
9624
|
const destDir = path35.join(paths.memoriesDir, "shared", sourceName);
|
|
9522
|
-
await
|
|
9625
|
+
await mkdir15(destDir, { recursive: true });
|
|
9523
9626
|
const sourceFiles = (await readdir5(sourceDir)).filter((f) => f.endsWith(".md"));
|
|
9524
9627
|
const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
|
|
9525
9628
|
const existingInDest = await loadDir(destDir);
|
|
@@ -9588,7 +9691,7 @@ Next steps:
|
|
|
9588
9691
|
// src/commands/stats.ts
|
|
9589
9692
|
import "commander";
|
|
9590
9693
|
import { existsSync as existsSync56 } from "fs";
|
|
9591
|
-
import { mkdir as
|
|
9694
|
+
import { mkdir as mkdir16, writeFile as writeFile27 } from "fs/promises";
|
|
9592
9695
|
import path36 from "path";
|
|
9593
9696
|
import {
|
|
9594
9697
|
aggregateUsage,
|
|
@@ -9696,7 +9799,7 @@ async function writeRoiReport(paths, root, sinceRaw, outRelative) {
|
|
|
9696
9799
|
ui.warn("Usage log missing or empty \u2014 report still written with partial data.");
|
|
9697
9800
|
events = [];
|
|
9698
9801
|
}
|
|
9699
|
-
await
|
|
9802
|
+
await mkdir16(path36.dirname(outAbs), { recursive: true });
|
|
9700
9803
|
const payload = {
|
|
9701
9804
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9702
9805
|
project_root: root,
|
|
@@ -9885,7 +9988,7 @@ function summarize(name, t0, payload, notes) {
|
|
|
9885
9988
|
}
|
|
9886
9989
|
|
|
9887
9990
|
// src/commands/memory-suggest.ts
|
|
9888
|
-
import { mkdir as
|
|
9991
|
+
import { mkdir as mkdir17, writeFile as writeFile28 } from "fs/promises";
|
|
9889
9992
|
import { existsSync as existsSync57 } from "fs";
|
|
9890
9993
|
import path37 from "path";
|
|
9891
9994
|
import "commander";
|
|
@@ -9980,7 +10083,7 @@ function registerMemorySuggest(memory2) {
|
|
|
9980
10083
|
fm.status = "draft";
|
|
9981
10084
|
const body = renderTemplate(s);
|
|
9982
10085
|
const file = memoryFilePath10(paths, fm.scope, fm.id, fm.module);
|
|
9983
|
-
await
|
|
10086
|
+
await mkdir17(path37.dirname(file), { recursive: true });
|
|
9984
10087
|
if (existsSync57(file)) {
|
|
9985
10088
|
skipped.push({ query: s.query, reason: `file already exists at ${path37.relative(root, file)}` });
|
|
9986
10089
|
continue;
|
|
@@ -10195,7 +10298,7 @@ function parseDays(input) {
|
|
|
10195
10298
|
// src/commands/doctor.ts
|
|
10196
10299
|
import { existsSync as existsSync59 } from "fs";
|
|
10197
10300
|
import { stat } from "fs/promises";
|
|
10198
|
-
import "path";
|
|
10301
|
+
import path39 from "path";
|
|
10199
10302
|
import { execSync as execSync3 } from "child_process";
|
|
10200
10303
|
import "commander";
|
|
10201
10304
|
import {
|
|
@@ -10234,8 +10337,8 @@ function registerDoctor(program2) {
|
|
|
10234
10337
|
fix: "haive init"
|
|
10235
10338
|
});
|
|
10236
10339
|
} else {
|
|
10237
|
-
const { readFile:
|
|
10238
|
-
const content = await
|
|
10340
|
+
const { readFile: readFile17 } = await import("fs/promises");
|
|
10341
|
+
const content = await readFile17(paths.projectContext, "utf8");
|
|
10239
10342
|
const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
|
|
10240
10343
|
if (isTemplate) {
|
|
10241
10344
|
findings.push({
|
|
@@ -10356,6 +10459,27 @@ function registerDoctor(program2) {
|
|
|
10356
10459
|
}
|
|
10357
10460
|
}
|
|
10358
10461
|
const config = await loadConfig7(paths);
|
|
10462
|
+
if (config.enforcement?.requireBriefingFirst) {
|
|
10463
|
+
const claudeSettings = path39.join(root, ".claude", "settings.local.json");
|
|
10464
|
+
let hasClaudeEnforcement = false;
|
|
10465
|
+
if (existsSync59(claudeSettings)) {
|
|
10466
|
+
try {
|
|
10467
|
+
const { readFile: readFile17 } = await import("fs/promises");
|
|
10468
|
+
const raw = await readFile17(claudeSettings, "utf8");
|
|
10469
|
+
hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
|
|
10470
|
+
} catch {
|
|
10471
|
+
hasClaudeEnforcement = false;
|
|
10472
|
+
}
|
|
10473
|
+
}
|
|
10474
|
+
if (!hasClaudeEnforcement) {
|
|
10475
|
+
findings.push({
|
|
10476
|
+
severity: "info",
|
|
10477
|
+
code: "claude-enforcement-hooks-missing",
|
|
10478
|
+
message: "hAIve enforcement is enabled, but project-scoped Claude Code hooks are not installed.",
|
|
10479
|
+
fix: "haive install-hooks claude --scope project"
|
|
10480
|
+
});
|
|
10481
|
+
}
|
|
10482
|
+
}
|
|
10359
10483
|
if (!config.autoSessionEnd) {
|
|
10360
10484
|
findings.push({
|
|
10361
10485
|
severity: "info",
|
|
@@ -10370,7 +10494,7 @@ function registerDoctor(program2) {
|
|
|
10370
10494
|
timeout: 3e3,
|
|
10371
10495
|
stdio: ["ignore", "pipe", "ignore"]
|
|
10372
10496
|
}).trim();
|
|
10373
|
-
const cliVersion = "0.9.
|
|
10497
|
+
const cliVersion = "0.9.9";
|
|
10374
10498
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
10375
10499
|
findings.push({
|
|
10376
10500
|
severity: "warn",
|
|
@@ -10972,13 +11096,563 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
10972
11096
|
});
|
|
10973
11097
|
}
|
|
10974
11098
|
|
|
11099
|
+
// src/commands/enforce.ts
|
|
11100
|
+
import { spawn as spawn5 } from "child_process";
|
|
11101
|
+
import { existsSync as existsSync67 } from "fs";
|
|
11102
|
+
import { chmod as chmod2, mkdir as mkdir18, readFile as readFile16, writeFile as writeFile30 } from "fs/promises";
|
|
11103
|
+
import path45 from "path";
|
|
11104
|
+
import "commander";
|
|
11105
|
+
import {
|
|
11106
|
+
findProjectRoot as findProjectRoot46,
|
|
11107
|
+
hasRecentBriefingMarker,
|
|
11108
|
+
isFreshIsoDate,
|
|
11109
|
+
loadConfig as loadConfig8,
|
|
11110
|
+
loadMemoriesFromDir as loadMemoriesFromDir36,
|
|
11111
|
+
resolveBriefingBudget as resolveBriefingBudget3,
|
|
11112
|
+
resolveHaivePaths as resolveHaivePaths43,
|
|
11113
|
+
saveConfig as saveConfig3,
|
|
11114
|
+
SESSION_RECAP_TTL_MS,
|
|
11115
|
+
verifyAnchor as verifyAnchor4,
|
|
11116
|
+
writeBriefingMarker as writeBriefingMarker2
|
|
11117
|
+
} from "@hiveai/core";
|
|
11118
|
+
var MAX_STDIN_BYTES2 = 256 * 1024;
|
|
11119
|
+
var ENFORCE_HOOK_MARKER = "# hAIve enforcement hook";
|
|
11120
|
+
function registerEnforce(program2) {
|
|
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
|
+
});
|
|
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) => {
|
|
11171
|
+
const payload = await readHookPayload();
|
|
11172
|
+
const root = resolveRoot(opts.dir, payload);
|
|
11173
|
+
if (!root) return;
|
|
11174
|
+
const paths = resolveHaivePaths43(root);
|
|
11175
|
+
if (!existsSync67(paths.haiveDir)) return;
|
|
11176
|
+
await mkdir18(paths.runtimeDir, { recursive: true });
|
|
11177
|
+
const sessionId = opts.sessionId ?? payload.session_id;
|
|
11178
|
+
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
|
+
const budget = resolveBriefingBudget3("quick", {
|
|
11185
|
+
max_tokens: 2500,
|
|
11186
|
+
max_memories: 5,
|
|
11187
|
+
include_module_contexts: false
|
|
11188
|
+
});
|
|
11189
|
+
const briefing = await getBriefing(
|
|
11190
|
+
{
|
|
11191
|
+
task,
|
|
11192
|
+
files: [],
|
|
11193
|
+
max_tokens: budget.max_tokens,
|
|
11194
|
+
max_memories: budget.max_memories,
|
|
11195
|
+
include_project_context: true,
|
|
11196
|
+
include_module_contexts: budget.include_module_contexts,
|
|
11197
|
+
semantic: true,
|
|
11198
|
+
include_stale: false,
|
|
11199
|
+
track: true,
|
|
11200
|
+
format: "actions",
|
|
11201
|
+
symbols: [],
|
|
11202
|
+
min_semantic_score: 0.25,
|
|
11203
|
+
budget_preset: "quick"
|
|
11204
|
+
},
|
|
11205
|
+
{ paths }
|
|
11206
|
+
);
|
|
11207
|
+
console.log("hAIve briefing loaded. Agents must consult this before editing.");
|
|
11208
|
+
if (briefing.last_session) {
|
|
11209
|
+
console.log(`
|
|
11210
|
+
## Last session
|
|
11211
|
+
${briefing.last_session.body.slice(0, 1200)}`);
|
|
11212
|
+
}
|
|
11213
|
+
if (briefing.project_context?.content) {
|
|
11214
|
+
console.log(`
|
|
11215
|
+
## Project context
|
|
11216
|
+
${briefing.project_context.content.slice(0, 1800)}`);
|
|
11217
|
+
}
|
|
11218
|
+
if (briefing.memories.length > 0) {
|
|
11219
|
+
console.log("\n## Relevant memories");
|
|
11220
|
+
for (const memory2 of briefing.memories.slice(0, 6)) {
|
|
11221
|
+
console.log(`
|
|
11222
|
+
### ${memory2.id} (${memory2.scope}/${memory2.type}, ${memory2.confidence})`);
|
|
11223
|
+
console.log(memory2.body.slice(0, 1e3));
|
|
11224
|
+
}
|
|
11225
|
+
}
|
|
11226
|
+
for (const warning of briefing.setup_warnings) {
|
|
11227
|
+
console.log(`
|
|
11228
|
+
[setup warning] ${warning}`);
|
|
11229
|
+
}
|
|
11230
|
+
});
|
|
11231
|
+
enforce.command("pre-tool-use").description("Claude Code PreToolUse hook: block writes until hAIve briefing has been loaded.").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
11232
|
+
const payload = await readHookPayload();
|
|
11233
|
+
const root = resolveRoot(opts.dir, payload);
|
|
11234
|
+
if (!root) return;
|
|
11235
|
+
const paths = resolveHaivePaths43(root);
|
|
11236
|
+
if (!existsSync67(paths.haiveDir)) return;
|
|
11237
|
+
if (!isWriteLikeTool(payload)) return;
|
|
11238
|
+
const ok = await hasRecentBriefingMarker(paths, payload.session_id);
|
|
11239
|
+
if (ok) return;
|
|
11240
|
+
const tool = payload.tool_name ?? "write tool";
|
|
11241
|
+
console.error(
|
|
11242
|
+
[
|
|
11243
|
+
"hAIve enforcement blocked this action.",
|
|
11244
|
+
`Tool: ${tool}`,
|
|
11245
|
+
"",
|
|
11246
|
+
"This project is initialized with hAIve. Load the team briefing before editing:",
|
|
11247
|
+
" haive enforce session-start",
|
|
11248
|
+
"or call MCP get_briefing / mem_relevant_to from your AI client.",
|
|
11249
|
+
"",
|
|
11250
|
+
"If this is intentional, a human can disable enforcement in .ai/haive.config.json:",
|
|
11251
|
+
' { "enforcement": { "requireBriefingFirst": false } }'
|
|
11252
|
+
].join("\n")
|
|
11253
|
+
);
|
|
11254
|
+
process.exit(2);
|
|
11255
|
+
});
|
|
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
|
+
}
|
|
11568
|
+
async function readHookPayload() {
|
|
11569
|
+
const raw = await readStdin2(MAX_STDIN_BYTES2);
|
|
11570
|
+
if (!raw.trim()) return {};
|
|
11571
|
+
try {
|
|
11572
|
+
return JSON.parse(raw);
|
|
11573
|
+
} catch {
|
|
11574
|
+
return {};
|
|
11575
|
+
}
|
|
11576
|
+
}
|
|
11577
|
+
function resolveRoot(dir, payload) {
|
|
11578
|
+
try {
|
|
11579
|
+
return findProjectRoot46(dir ?? payload.cwd);
|
|
11580
|
+
} catch {
|
|
11581
|
+
return null;
|
|
11582
|
+
}
|
|
11583
|
+
}
|
|
11584
|
+
function isWriteLikeTool(payload) {
|
|
11585
|
+
const tool = payload.tool_name ?? "";
|
|
11586
|
+
if (["Edit", "Write", "MultiEdit", "NotebookEdit"].includes(tool)) return true;
|
|
11587
|
+
if (tool !== "Bash") return false;
|
|
11588
|
+
const command = String(payload.tool_input?.["command"] ?? "");
|
|
11589
|
+
return /\b(rm|mv|cp|mkdir|touch|tee|sed|perl|python|node|npm|pnpm|yarn|git)\b/.test(command) || />{1,2}/.test(command);
|
|
11590
|
+
}
|
|
11591
|
+
async function readStdin2(maxBytes) {
|
|
11592
|
+
if (process.stdin.isTTY) return "";
|
|
11593
|
+
return await new Promise((resolve) => {
|
|
11594
|
+
const chunks = [];
|
|
11595
|
+
let total = 0;
|
|
11596
|
+
let done = false;
|
|
11597
|
+
const finish = () => {
|
|
11598
|
+
if (done) return;
|
|
11599
|
+
done = true;
|
|
11600
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
11601
|
+
};
|
|
11602
|
+
process.stdin.on("data", (c) => {
|
|
11603
|
+
total += c.length;
|
|
11604
|
+
if (total > maxBytes) {
|
|
11605
|
+
process.stdin.destroy();
|
|
11606
|
+
finish();
|
|
11607
|
+
return;
|
|
11608
|
+
}
|
|
11609
|
+
chunks.push(c);
|
|
11610
|
+
});
|
|
11611
|
+
process.stdin.on("end", finish);
|
|
11612
|
+
process.stdin.on("error", finish);
|
|
11613
|
+
setTimeout(finish, 2e3);
|
|
11614
|
+
});
|
|
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
|
+
}
|
|
11646
|
+
|
|
10975
11647
|
// src/index.ts
|
|
10976
|
-
var program = new
|
|
10977
|
-
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.9");
|
|
10978
11650
|
registerInit(program);
|
|
10979
11651
|
registerWelcome(program);
|
|
10980
11652
|
registerResolveProject(program);
|
|
10981
11653
|
registerRuntime(program);
|
|
11654
|
+
registerEnforce(program);
|
|
11655
|
+
registerRun(program);
|
|
10982
11656
|
registerMcp(program);
|
|
10983
11657
|
registerBriefing(program);
|
|
10984
11658
|
registerTui(program);
|