@hiveai/cli 0.9.24 → 0.9.26
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 +1652 -1372
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -4,22 +4,22 @@
|
|
|
4
4
|
import { Command as Command51 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/briefing.ts
|
|
7
|
-
import { existsSync } from "fs";
|
|
8
|
-
import { mkdir, readFile } from "fs/promises";
|
|
9
|
-
import
|
|
7
|
+
import { existsSync as existsSync3 } from "fs";
|
|
8
|
+
import { mkdir, readFile as readFile2 } from "fs/promises";
|
|
9
|
+
import path3 from "path";
|
|
10
10
|
import "commander";
|
|
11
11
|
import {
|
|
12
12
|
extractActionsBriefBody,
|
|
13
|
-
findProjectRoot,
|
|
13
|
+
findProjectRoot as findProjectRoot2,
|
|
14
14
|
literalMatchesAllTokens,
|
|
15
15
|
literalMatchesAnyToken,
|
|
16
|
-
loadCodeMap,
|
|
17
|
-
loadMemoriesFromDir,
|
|
18
|
-
loadUsageIndex,
|
|
16
|
+
loadCodeMap as loadCodeMap3,
|
|
17
|
+
loadMemoriesFromDir as loadMemoriesFromDir3,
|
|
18
|
+
loadUsageIndex as loadUsageIndex2,
|
|
19
19
|
memoryMatchesAnchorPaths,
|
|
20
20
|
queryCodeMap,
|
|
21
21
|
resolveBriefingBudget,
|
|
22
|
-
resolveHaivePaths,
|
|
22
|
+
resolveHaivePaths as resolveHaivePaths2,
|
|
23
23
|
tokenizeQuery,
|
|
24
24
|
trackReads,
|
|
25
25
|
writeBriefingMarker
|
|
@@ -225,1451 +225,1461 @@ function radarHasContent(r) {
|
|
|
225
225
|
return r.recentCommits.length > 0 || r.openTodos.length > 0 || r.hotFiles.length > 0;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
// src/
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
228
|
+
// src/utils/autopilot.ts
|
|
229
|
+
import { existsSync as existsSync2 } from "fs";
|
|
230
|
+
import { readFile, writeFile as writeFile2 } from "fs/promises";
|
|
231
|
+
import path2 from "path";
|
|
232
|
+
import {
|
|
233
|
+
AUTOPILOT_DEFAULTS,
|
|
234
|
+
buildCodeMap,
|
|
235
|
+
loadCodeMap as loadCodeMap2,
|
|
236
|
+
loadConfig,
|
|
237
|
+
loadMemoriesFromDir as loadMemoriesFromDir2,
|
|
238
|
+
saveCodeMap,
|
|
239
|
+
saveConfig
|
|
240
|
+
} from "@hiveai/core";
|
|
241
|
+
|
|
242
|
+
// src/commands/memory-lint.ts
|
|
243
|
+
import { existsSync } from "fs";
|
|
244
|
+
import { writeFile } from "fs/promises";
|
|
245
|
+
import { spawnSync } from "child_process";
|
|
246
|
+
import path from "path";
|
|
247
|
+
import "commander";
|
|
248
|
+
import {
|
|
249
|
+
findProjectRoot,
|
|
250
|
+
getUsage,
|
|
251
|
+
loadCodeMap,
|
|
252
|
+
loadMemoriesFromDir,
|
|
253
|
+
loadUsageIndex,
|
|
254
|
+
resolveHaivePaths,
|
|
255
|
+
serializeMemory
|
|
256
|
+
} from "@hiveai/core";
|
|
257
|
+
async function lintMemoriesAsync(root, options = {}) {
|
|
258
|
+
const paths = resolveHaivePaths(root);
|
|
259
|
+
const out = [];
|
|
260
|
+
const fixes = [];
|
|
261
|
+
if (!existsSync(paths.memoriesDir)) return { findings: out, fixes };
|
|
262
|
+
const loaded = await loadMemoriesFromDir(paths.memoriesDir);
|
|
263
|
+
const usage = await loadUsageIndex(paths);
|
|
264
|
+
const codeMap = await loadCodeMap(paths);
|
|
265
|
+
const trackedFiles = gitTrackedFiles(root);
|
|
266
|
+
const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
|
|
267
|
+
const actionableWords = /\b(always|never|prefer|use|run|avoid|because|instead|why|rationale|do not|must|should|require|required|requires|fix|fail|failed|fails|prevent|prevents|allow|allows|lets|ensure|ensures|catch|catches)\b/i;
|
|
268
|
+
for (const { filePath, memory: memory2 } of loaded) {
|
|
269
|
+
const fm = memory2.frontmatter;
|
|
270
|
+
if (fm.type === "session_recap") continue;
|
|
271
|
+
const body = memory2.body.trim();
|
|
272
|
+
const naked = body.replace(/^#.*$/gm, "").replace(/```[\s\S]*?```/g, "").trim();
|
|
273
|
+
if (naked.length < 40 && fm.status !== "rejected") {
|
|
274
|
+
out.push({
|
|
275
|
+
file: filePath,
|
|
276
|
+
id: fm.id,
|
|
277
|
+
severity: "warn",
|
|
278
|
+
code: "SHORT_BODY",
|
|
279
|
+
message: "Body looks very short (< ~40 chars of prose after headings). Prefer actionable detail."
|
|
280
|
+
});
|
|
244
281
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
282
|
+
if (["decision", "gotcha", "convention", "architecture", "attempt"].includes(fm.type) && fm.status !== "rejected" && !actionableWords.test(naked)) {
|
|
283
|
+
out.push({
|
|
284
|
+
file: filePath,
|
|
285
|
+
id: fm.id,
|
|
286
|
+
severity: "info",
|
|
287
|
+
code: "LOW_ACTIONABILITY",
|
|
288
|
+
message: "Record does not contain obvious action/rationale words. Add the concrete rule, why it exists, and what to do instead."
|
|
289
|
+
});
|
|
251
290
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
291
|
+
const suggestedAnchors = suggestAnchors(root, { filePath, memory: memory2 }, codeMap, trackedFiles);
|
|
292
|
+
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated") {
|
|
293
|
+
out.push({
|
|
294
|
+
file: filePath,
|
|
295
|
+
id: fm.id,
|
|
296
|
+
severity: "warn",
|
|
297
|
+
code: "MISSING_ANCHOR",
|
|
298
|
+
message: `${fm.type} is validated without anchor paths \u2014 add anchor.paths so haive sync can flag staleness.`,
|
|
299
|
+
...suggestedAnchors.paths.length > 0 || suggestedAnchors.symbols.length > 0 ? { suggested_anchors: suggestedAnchors } : {}
|
|
300
|
+
});
|
|
258
301
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
used = 0;
|
|
268
|
-
truncated = false;
|
|
269
|
-
write(text) {
|
|
270
|
-
if (this.truncated) return false;
|
|
271
|
-
const next = this.used + text.length + 1;
|
|
272
|
-
if (next > this.budgetChars) {
|
|
273
|
-
console.log(ui.dim(`... [briefing truncated to fit --max-tokens budget \xB7 ${Math.round(this.used / CHARS_PER_TOKEN)} tokens used]`));
|
|
274
|
-
this.truncated = true;
|
|
275
|
-
return false;
|
|
302
|
+
if (fm.status === "stale" && !fm.stale_reason) {
|
|
303
|
+
out.push({
|
|
304
|
+
file: filePath,
|
|
305
|
+
id: fm.id,
|
|
306
|
+
severity: "info",
|
|
307
|
+
code: "STALE_NO_REASON",
|
|
308
|
+
message: "Status is stale but stale_reason is empty \u2014 document why when possible."
|
|
309
|
+
});
|
|
276
310
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
remainingChars() {
|
|
285
|
-
return Math.max(0, this.budgetChars - this.used);
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
function registerBriefing(program2) {
|
|
289
|
-
program2.command("briefing").description(
|
|
290
|
-
'Print the full project briefing: last session recap + project context + relevant memories.\n Equivalent to calling get_briefing via MCP. Run before starting any task.\n\n Examples:\n haive briefing\n haive briefing --task "add Stripe payment" --files src/payments/PaymentService.ts\n haive briefing --budget quick --task "tiny fix"\n'
|
|
291
|
-
).option("--task <text>", "what you are about to do \u2014 filters memories by relevance").option("--files <csv>", "comma-separated file paths being worked on (surfaces anchored memories)").option("--symbols <csv>", "symbol names to look up in the code-map (e.g. PaymentService,TenantFilter) \u2014 requires haive index code").option("--max-memories <n>", "cap on memories surfaced", "8").option("--max-tokens <n>", "approximate token budget for the entire briefing (truncates if exceeded)").option("--explain-source", "annotate each memory with [source: <relative-path> \xB7 anchors: <files>] for traceable citations").option("--radar", "force project radar (recent commits, open TODOs, hot files) even when memories are plentiful").option("--no-radar", "disable the project radar even when memories are scarce").option(
|
|
292
|
-
"--budget <preset>",
|
|
293
|
-
"align with MCP get_briefing budget_preset: quick | balanced | deep \u2014 sets cap + truncation budget (overrides --max-memories / replaces default open-ended output)",
|
|
294
|
-
void 0
|
|
295
|
-
).option(
|
|
296
|
-
"--memory-format <mode>",
|
|
297
|
-
"printed memory bodies: full (default) | actions (cheap bullet-focused excerpt)",
|
|
298
|
-
"full"
|
|
299
|
-
).option(
|
|
300
|
-
"--format <mode>",
|
|
301
|
-
"alias for --memory-format; accepts full | actions | compact"
|
|
302
|
-
).option(
|
|
303
|
-
"--scope <scope>",
|
|
304
|
-
"personal | team | shared | all (default: all \u2014 includes team + shared cross-repo memories)",
|
|
305
|
-
"all"
|
|
306
|
-
).option("--include-draft", "include draft memories (excluded by default)").option("--include-stale", "include stale memories (excluded by default \u2014 may be outdated)").option(
|
|
307
|
-
"--include <path>",
|
|
308
|
-
"merge memories from another haive-initialized project (repeatable). Useful for teams with multiple coordinated repos (e.g. backend + frontend).",
|
|
309
|
-
collectInclude,
|
|
310
|
-
[]
|
|
311
|
-
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
312
|
-
const root = findProjectRoot(opts.dir);
|
|
313
|
-
const paths = resolveHaivePaths(root);
|
|
314
|
-
const requestedFormat = (opts.format ?? opts.memoryFormat ?? "full").toLowerCase();
|
|
315
|
-
opts.memoryFormat = requestedFormat === "compact" ? "actions" : requestedFormat;
|
|
316
|
-
const markerFiles = parseCsv(opts.files);
|
|
317
|
-
if (existsSync(paths.haiveDir)) {
|
|
318
|
-
await mkdir(paths.runtimeDir, { recursive: true });
|
|
319
|
-
await writeBriefingMarker(paths, {
|
|
320
|
-
task: opts.task ?? "CLI briefing",
|
|
321
|
-
source: "haive-briefing-cli",
|
|
322
|
-
sessionId: process.env.HAIVE_SESSION_ID,
|
|
323
|
-
files: markerFiles
|
|
324
|
-
}).catch(() => {
|
|
311
|
+
if (fm.type === "glossary" && naked.length > 6e3) {
|
|
312
|
+
out.push({
|
|
313
|
+
file: filePath,
|
|
314
|
+
id: fm.id,
|
|
315
|
+
severity: "info",
|
|
316
|
+
code: "LONG_GLOSSARY",
|
|
317
|
+
message: "Very long glossary \u2014 consider splitting concepts for tighter briefings."
|
|
325
318
|
});
|
|
326
319
|
}
|
|
327
|
-
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
320
|
+
const hasMarkdownHeading = /^#{1,3}\s+\S/m.test(memory2.body.trim());
|
|
321
|
+
if (!hasMarkdownHeading) {
|
|
322
|
+
out.push({
|
|
323
|
+
file: filePath,
|
|
324
|
+
id: fm.id,
|
|
325
|
+
severity: "warn",
|
|
326
|
+
code: "NO_MD_HEADING",
|
|
327
|
+
message: "No Markdown heading (#/##/###) \u2014 add one so humans and auditors can skim the memo quickly."
|
|
328
|
+
});
|
|
332
329
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
330
|
+
const u = getUsage(usage, fm.id);
|
|
331
|
+
const createdAt = Date.parse(fm.created_at);
|
|
332
|
+
const ageDays = Number.isFinite(createdAt) ? (Date.now() - createdAt) / (24 * 60 * 60 * 1e3) : 0;
|
|
333
|
+
if (fm.status === "validated" && u.read_count === 0 && ageDays >= 7) {
|
|
334
|
+
out.push({
|
|
335
|
+
file: filePath,
|
|
336
|
+
id: fm.id,
|
|
337
|
+
severity: "info",
|
|
338
|
+
code: "NEVER_READ",
|
|
339
|
+
message: "Validated record has never been surfaced/read. Consider improving tags/anchors or archiving it if it is not useful."
|
|
340
340
|
});
|
|
341
|
-
budgetTokensCap = presetNums.max_tokens;
|
|
342
|
-
maxMemories = presetNums.max_memories;
|
|
343
341
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
out(`${ui.bold("=== Project Context ===")}
|
|
354
|
-
`);
|
|
355
|
-
out((await readFile(paths.projectContext, "utf8")).trim());
|
|
356
|
-
out("");
|
|
357
|
-
} else {
|
|
358
|
-
ui.warn("No project-context.md found. Run `haive init` and the `bootstrap_project` MCP prompt to set it up.");
|
|
342
|
+
if (options.fix) {
|
|
343
|
+
const actions = [];
|
|
344
|
+
let nextBody = memory2.body;
|
|
345
|
+
let nextFrontmatter = memory2.frontmatter;
|
|
346
|
+
if (!hasMarkdownHeading) {
|
|
347
|
+
nextBody = `# ${titleFromId(fm.id)}
|
|
348
|
+
|
|
349
|
+
${nextBody.trim()}`;
|
|
350
|
+
actions.push("add missing Markdown heading");
|
|
359
351
|
}
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
352
|
+
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated" && suggestedAnchors.paths.length > 0) {
|
|
353
|
+
nextFrontmatter = {
|
|
354
|
+
...nextFrontmatter,
|
|
355
|
+
anchor: {
|
|
356
|
+
...nextFrontmatter.anchor,
|
|
357
|
+
paths: [.../* @__PURE__ */ new Set([...nextFrontmatter.anchor.paths, ...suggestedAnchors.paths])],
|
|
358
|
+
symbols: [
|
|
359
|
+
.../* @__PURE__ */ new Set([...nextFrontmatter.anchor.symbols, ...suggestedAnchors.symbols])
|
|
360
|
+
]
|
|
361
|
+
},
|
|
362
|
+
tags: nextFrontmatter.tags.filter((tag) => tag !== "needs_anchor")
|
|
363
|
+
};
|
|
364
|
+
actions.push("add suggested tracked anchor paths");
|
|
365
|
+
if (suggestedAnchors.symbols.length > 0) {
|
|
366
|
+
actions.push("add suggested anchor symbols");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.anchor.symbols.length === 0 && suggestedAnchors.paths.length === 0 && fm.status === "validated" && !fm.tags.includes("needs_anchor")) {
|
|
370
|
+
nextFrontmatter = {
|
|
371
|
+
...nextFrontmatter,
|
|
372
|
+
tags: [...nextFrontmatter.tags, "needs_anchor"]
|
|
373
|
+
};
|
|
374
|
+
actions.push("tag validated anchorless record with needs_anchor");
|
|
375
|
+
}
|
|
376
|
+
if (actions.length > 0) {
|
|
377
|
+
fixes.push({ file: filePath, id: fm.id, actions, applied: Boolean(options.apply) });
|
|
378
|
+
if (options.apply) {
|
|
379
|
+
await writeFile(
|
|
380
|
+
filePath,
|
|
381
|
+
serializeMemory({ frontmatter: nextFrontmatter, body: nextBody }),
|
|
382
|
+
"utf8"
|
|
383
|
+
);
|
|
384
|
+
}
|
|
365
385
|
}
|
|
366
|
-
return;
|
|
367
386
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
387
|
+
}
|
|
388
|
+
for (const dup of nearDuplicatePairs(loaded)) {
|
|
389
|
+
out.push({
|
|
390
|
+
file: dup.file,
|
|
391
|
+
id: dup.id,
|
|
392
|
+
severity: "warn",
|
|
393
|
+
code: "NEAR_DUPLICATE",
|
|
394
|
+
message: `Body overlaps ~${Math.round(dup.score * 100)}% with ${dup.otherId}. Merge or deprecate one record to reduce briefing noise.`
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return { findings: out, fixes };
|
|
398
|
+
}
|
|
399
|
+
function titleFromId(id) {
|
|
400
|
+
const withoutDate = id.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
401
|
+
return withoutDate.split("-").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
402
|
+
}
|
|
403
|
+
function suggestAnchors(root, loaded, codeMap, trackedFiles) {
|
|
404
|
+
const body = loaded.memory.body;
|
|
405
|
+
const paths = /* @__PURE__ */ new Set();
|
|
406
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
407
|
+
for (const match of body.matchAll(/`([^`\n]+\.[A-Za-z0-9]+)`|(?:^|\s)([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)/gm)) {
|
|
408
|
+
const candidate = (match[1] ?? match[2] ?? "").replace(/^\.?\//, "");
|
|
409
|
+
if (!candidate || candidate.startsWith("http")) continue;
|
|
410
|
+
if (existsSync(path.join(root, candidate)) && isSafeAnchorPath(candidate, trackedFiles)) {
|
|
411
|
+
paths.add(candidate);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (codeMap) {
|
|
415
|
+
const lowered = body.toLowerCase();
|
|
416
|
+
for (const [file, entry] of Object.entries(codeMap.files)) {
|
|
417
|
+
for (const exp of entry.exports) {
|
|
418
|
+
if (!exp.name || exp.name.length < 4) continue;
|
|
419
|
+
if (lowered.includes(exp.name.toLowerCase())) {
|
|
420
|
+
if (isSafeAnchorPath(file, trackedFiles)) {
|
|
421
|
+
paths.add(file);
|
|
422
|
+
symbols.add(exp.name);
|
|
384
423
|
}
|
|
385
|
-
externalRoots.push(`${tag} (${otherMemories.length})`);
|
|
386
|
-
} catch (err) {
|
|
387
|
-
ui.warn(`--include ${includePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
388
424
|
}
|
|
425
|
+
if (paths.size >= 5 && symbols.size >= 5) break;
|
|
389
426
|
}
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
|
|
427
|
+
if (paths.size >= 5 && symbols.size >= 5) break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
paths: [...paths].slice(0, 5),
|
|
432
|
+
symbols: [...symbols].slice(0, 5)
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function gitTrackedFiles(root) {
|
|
436
|
+
const result = spawnSync("git", ["ls-files"], {
|
|
437
|
+
cwd: root,
|
|
438
|
+
encoding: "utf8",
|
|
439
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
440
|
+
});
|
|
441
|
+
if (result.status !== 0) return null;
|
|
442
|
+
const files = result.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
443
|
+
return new Set(files);
|
|
444
|
+
}
|
|
445
|
+
function isSafeAnchorPath(file, trackedFiles) {
|
|
446
|
+
const normalized = file.replace(/\\/g, "/").replace(/^\.?\//, "");
|
|
447
|
+
if (normalized.startsWith(".ai/.cache/") || normalized.startsWith(".ai/.runtime/")) return false;
|
|
448
|
+
if (normalized.includes("/node_modules/") || normalized.startsWith("node_modules/")) return false;
|
|
449
|
+
if (normalized.includes("/dist/") || normalized.startsWith("dist/")) return false;
|
|
450
|
+
if (trackedFiles && !trackedFiles.has(normalized)) return false;
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
function nearDuplicatePairs(loaded) {
|
|
454
|
+
const out = [];
|
|
455
|
+
const candidates = loaded.filter(({ memory: memory2 }) => {
|
|
456
|
+
const fm = memory2.frontmatter;
|
|
457
|
+
return fm.type !== "session_recap" && fm.status !== "rejected" && fm.status !== "deprecated";
|
|
458
|
+
});
|
|
459
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
460
|
+
for (let j = i + 1; j < candidates.length; j++) {
|
|
461
|
+
const a = candidates[i];
|
|
462
|
+
const b = candidates[j];
|
|
463
|
+
if (a.memory.frontmatter.scope !== b.memory.frontmatter.scope) continue;
|
|
464
|
+
if (a.memory.frontmatter.type !== b.memory.frontmatter.type) continue;
|
|
465
|
+
const score = jaccard(tokenSet(a.memory.body), tokenSet(b.memory.body));
|
|
466
|
+
if (score >= 0.72) {
|
|
467
|
+
out.push({
|
|
468
|
+
id: a.memory.frontmatter.id,
|
|
469
|
+
otherId: b.memory.frontmatter.id,
|
|
470
|
+
file: a.filePath,
|
|
471
|
+
score
|
|
472
|
+
});
|
|
393
473
|
}
|
|
394
474
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
475
|
+
}
|
|
476
|
+
return out;
|
|
477
|
+
}
|
|
478
|
+
function tokenSet(body) {
|
|
479
|
+
return new Set(
|
|
480
|
+
(body.toLowerCase().match(/\b[a-z0-9]{4,}\b/g) ?? []).filter((word) => !["this", "that", "with", "from", "have"].includes(word))
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
function jaccard(a, b) {
|
|
484
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
485
|
+
let inter = 0;
|
|
486
|
+
for (const item of a) if (b.has(item)) inter++;
|
|
487
|
+
return inter / (a.size + b.size - inter);
|
|
488
|
+
}
|
|
489
|
+
function registerMemoryLint(parent) {
|
|
490
|
+
parent.command("lint").description(
|
|
491
|
+
"Heuristic corpus checks (anchors on key types, headings, verbosity). Static analysis only."
|
|
492
|
+
).option("--json", "emit findings as JSON", false).option("--fix", "prepare simple automatic fixes (use with --dry-run or --apply)", false).option("--dry-run", "with --fix, show files that would change without writing", false).option("--apply", "with --fix, write simple fixes to disk", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
493
|
+
const root = findProjectRoot(opts.dir);
|
|
494
|
+
const apply = Boolean(opts.fix && opts.apply);
|
|
495
|
+
const dryRun = Boolean(opts.fix && (opts.dryRun || !opts.apply));
|
|
496
|
+
const report = await lintMemoriesAsync(root, { fix: Boolean(opts.fix), apply });
|
|
497
|
+
const findings = report.findings;
|
|
498
|
+
if (opts.json) {
|
|
499
|
+
console.log(JSON.stringify({
|
|
500
|
+
findings_count: findings.length,
|
|
501
|
+
findings,
|
|
502
|
+
fixes_count: report.fixes.length,
|
|
503
|
+
fixes: report.fixes,
|
|
504
|
+
fix_mode: opts.fix ? apply ? "apply" : "dry-run" : "off"
|
|
505
|
+
}, null, 2));
|
|
506
|
+
process.exitCode = findings.some((f) => f.severity === "error") ? 1 : 0;
|
|
507
|
+
return;
|
|
411
508
|
}
|
|
412
|
-
if (
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
"project-context.md still contains the default template \u2014 get_briefing will return little value."
|
|
418
|
-
);
|
|
419
|
-
ui.warn(
|
|
420
|
-
"Fix: in your AI client, invoke the MCP prompt bootstrap_project to auto-fill it from your codebase."
|
|
421
|
-
);
|
|
422
|
-
out("");
|
|
423
|
-
} else {
|
|
424
|
-
out(`${ui.bold("=== Project Context ===")}
|
|
509
|
+
if (findings.length === 0) {
|
|
510
|
+
ui.success(`memory lint OK \u2014 ${root}`);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
console.log(ui.bold(`memory lint (${findings.length} finding${findings.length === 1 ? "" : "s"})`) + `
|
|
425
514
|
`);
|
|
426
|
-
|
|
427
|
-
|
|
515
|
+
if (opts.fix) {
|
|
516
|
+
const mode = apply ? "apply" : dryRun ? "dry-run" : "dry-run";
|
|
517
|
+
const verb = apply ? "changed" : "would change";
|
|
518
|
+
console.log(ui.bold(`fix ${mode}: ${report.fixes.length} file${report.fixes.length === 1 ? "" : "s"} ${verb}`));
|
|
519
|
+
for (const fix of report.fixes) {
|
|
520
|
+
console.log(` ${ui.dim(fix.id)} ${fix.actions.join("; ")}`);
|
|
521
|
+
console.log(ui.dim(` \u2192 ${fix.file}`));
|
|
428
522
|
}
|
|
429
|
-
|
|
430
|
-
ui.warn(
|
|
431
|
-
"No project-context.md found. Run `haive init` then invoke the bootstrap_project MCP prompt."
|
|
432
|
-
);
|
|
523
|
+
console.log();
|
|
433
524
|
}
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const fm = mem.frontmatter;
|
|
447
|
-
let score = 0;
|
|
448
|
-
if (fm.status === "validated") score += 3;
|
|
449
|
-
else if (fm.status === "proposed") score += 1;
|
|
450
|
-
if (filePaths.length > 0 && memoryMatchesAnchorPaths(mem, filePaths)) score += 4;
|
|
451
|
-
if (tokens) {
|
|
452
|
-
if (andTaskHits?.has(fm.id)) score += 3;
|
|
453
|
-
else if (useOrFallback && literalMatchesAnyToken(mem, tokens)) score += 1;
|
|
454
|
-
}
|
|
455
|
-
return { memory: mem, filePath, score };
|
|
456
|
-
});
|
|
457
|
-
scored.sort((a, b) => b.score - a.score);
|
|
458
|
-
const top = scored.slice(0, maxMemories);
|
|
459
|
-
if (top.length === 0) {
|
|
460
|
-
ui.info("No relevant memories found.");
|
|
461
|
-
const draftCount = all.filter(
|
|
462
|
-
(m) => m.memory.frontmatter.status === "draft" && (scopeFilter === "all" || m.memory.frontmatter.scope === scopeFilter)
|
|
463
|
-
).length;
|
|
464
|
-
if (draftCount > 0) {
|
|
465
|
-
ui.info(`(${draftCount} draft memories excluded \u2014 use --include-draft to show)`);
|
|
466
|
-
}
|
|
467
|
-
if (opts.radar !== false && !stopped()) {
|
|
468
|
-
const radar = await buildRadar({ root, taskTokens: tokens, filePaths });
|
|
469
|
-
out("");
|
|
470
|
-
printRadar(radar, out, "low-memory-signal");
|
|
471
|
-
}
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
if (stopped()) return;
|
|
475
|
-
const usageIndex = await loadUsageIndex(paths).catch(() => null);
|
|
476
|
-
out(`${ui.bold("=== Relevant Memories ===")}
|
|
477
|
-
`);
|
|
478
|
-
const priorities = top.map(
|
|
479
|
-
(item) => classifyCliPriority(
|
|
480
|
-
item,
|
|
481
|
-
filePaths,
|
|
482
|
-
tokens,
|
|
483
|
-
Boolean(andTaskHits?.has(item.memory.frontmatter.id)),
|
|
484
|
-
Boolean(useOrFallback && tokens && literalMatchesAnyToken(item.memory, tokens))
|
|
485
|
-
)
|
|
486
|
-
);
|
|
487
|
-
const mustReadCount = priorities.filter((p) => p === "must_read").length;
|
|
488
|
-
const usefulCount = priorities.filter((p) => p === "useful").length;
|
|
489
|
-
const backgroundCount = priorities.filter((p) => p === "background").length;
|
|
490
|
-
const quality = mustReadCount > 0 || usefulCount > 0 ? backgroundCount > mustReadCount + usefulCount && backgroundCount > 2 ? "noisy" : "strong" : "thin";
|
|
491
|
-
out(ui.dim(`briefing_quality: ${quality} \xB7 must_read=${mustReadCount} useful=${usefulCount} background=${backgroundCount}`));
|
|
492
|
-
out("");
|
|
493
|
-
for (const [idx, item] of top.entries()) {
|
|
494
|
-
if (stopped()) break;
|
|
495
|
-
const fm = item.memory.frontmatter;
|
|
496
|
-
const badge = ui.statusBadge(fm.status);
|
|
497
|
-
const draftMarker = fm.status === "draft" ? ui.yellow(" [DRAFT]") : "";
|
|
498
|
-
const unverifiedMarker = fm.status === "proposed" ? ui.yellow(" [UNVERIFIED]") : "";
|
|
499
|
-
const originMarker = item.origin ? ` ${ui.yellow("[from " + item.origin + "]")}` : "";
|
|
500
|
-
const reads = usageIndex?.by_id[fm.id]?.read_count ?? 0;
|
|
501
|
-
const hitMarker = reads > 0 ? ` ${ui.dim("\xB7 " + reads + "\xD7 read")}` : "";
|
|
502
|
-
const priority = priorities[idx] ?? "background";
|
|
503
|
-
out(
|
|
504
|
-
`${ui.bold(fm.id)} ${priorityBadge(priority)} ${ui.dim(fm.scope + "/" + fm.type)} ${badge}${draftMarker}${unverifiedMarker}${originMarker}${hitMarker}`
|
|
505
|
-
);
|
|
506
|
-
if (opts.explainSource) {
|
|
507
|
-
const relPath = path.relative(root, item.filePath);
|
|
508
|
-
const anchorPaths = fm.anchor?.paths ?? [];
|
|
509
|
-
const anchorSymbols = fm.anchor?.symbols ?? [];
|
|
510
|
-
const parts = [`source: ${relPath}`];
|
|
511
|
-
if (anchorPaths.length > 0) parts.push(`paths: ${anchorPaths.join(", ")}`);
|
|
512
|
-
if (anchorSymbols.length > 0) parts.push(`symbols: ${anchorSymbols.join(", ")}`);
|
|
513
|
-
out(ui.dim(` [${parts.join(" \xB7 ")}]`));
|
|
514
|
-
}
|
|
515
|
-
const memBody = opts.memoryFormat?.toLowerCase() === "actions" ? extractActionsBriefBody(item.memory.body) : item.memory.body.trim();
|
|
516
|
-
out(memBody);
|
|
517
|
-
out("");
|
|
518
|
-
}
|
|
519
|
-
if (!stopped()) out(ui.dim(`${top.length} memor${top.length === 1 ? "y" : "ies"} surfaced`));
|
|
520
|
-
const ids = top.map(({ memory: mem }) => mem.frontmatter.id);
|
|
521
|
-
if (ids.length > 0) {
|
|
522
|
-
await trackReads(paths, ids).catch(() => {
|
|
523
|
-
});
|
|
524
|
-
await writeBriefingMarker(paths, {
|
|
525
|
-
task: opts.task ?? "CLI briefing",
|
|
526
|
-
source: "haive-briefing-cli",
|
|
527
|
-
sessionId: process.env.HAIVE_SESSION_ID,
|
|
528
|
-
memoryIds: ids,
|
|
529
|
-
files: filePaths
|
|
530
|
-
}).catch(() => {
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
const radarForced = opts.radar === true;
|
|
534
|
-
const radarAuto = opts.radar !== false && top.length < RADAR_AUTO_THRESHOLD;
|
|
535
|
-
if ((radarForced || radarAuto) && !stopped()) {
|
|
536
|
-
const radar = await buildRadar({ root, taskTokens: tokens, filePaths });
|
|
537
|
-
if (radarHasContent(radar)) {
|
|
538
|
-
out("");
|
|
539
|
-
printRadar(radar, out, radarForced ? "forced" : "low-memory-signal");
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
const requestedSymbols = (opts.symbols ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
543
|
-
if (requestedSymbols.length > 0 && !stopped()) {
|
|
544
|
-
const codeMap = await loadCodeMap(paths);
|
|
545
|
-
if (!codeMap) {
|
|
546
|
-
ui.warn("No code-map found. Run `haive index code` first to enable symbol lookup.");
|
|
547
|
-
} else {
|
|
548
|
-
out(`
|
|
549
|
-
${ui.bold("=== Symbol Locations ===")}
|
|
550
|
-
`);
|
|
551
|
-
for (const sym of requestedSymbols) {
|
|
552
|
-
if (stopped()) break;
|
|
553
|
-
const { files } = queryCodeMap(codeMap, { symbol: sym });
|
|
554
|
-
if (files.length === 0) {
|
|
555
|
-
out(`${ui.dim(sym)} (not found in code-map)`);
|
|
556
|
-
} else {
|
|
557
|
-
for (const f of files) {
|
|
558
|
-
if (stopped()) break;
|
|
559
|
-
const exports = f.entry.exports.filter(
|
|
560
|
-
(e) => e.name.toLowerCase().includes(sym.toLowerCase())
|
|
561
|
-
);
|
|
562
|
-
for (const e of exports) {
|
|
563
|
-
if (stopped()) break;
|
|
564
|
-
const desc = e.description ? ` \u2014 ${e.description}` : "";
|
|
565
|
-
out(`${ui.bold(e.name)} ${ui.dim(f.path + ":" + e.line)} [${e.kind}]${desc}`);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
out("");
|
|
525
|
+
const order = { error: 0, warn: 1, info: 2 };
|
|
526
|
+
findings.sort((a, b) => order[a.severity] - order[b.severity] || a.id.localeCompare(b.id));
|
|
527
|
+
for (const f of findings) {
|
|
528
|
+
const color = f.severity === "error" ? ui.red : f.severity === "warn" ? ui.yellow : ui.dim;
|
|
529
|
+
console.log(
|
|
530
|
+
`${color(f.severity.padEnd(5))} ${ui.dim(f.code)} ${f.id}`
|
|
531
|
+
);
|
|
532
|
+
console.log(` ${f.message}`);
|
|
533
|
+
if (f.suggested_anchors) {
|
|
534
|
+
const pathHints = f.suggested_anchors.paths.length > 0 ? `paths: ${f.suggested_anchors.paths.join(", ")}` : "";
|
|
535
|
+
const symbolHints = f.suggested_anchors.symbols.length > 0 ? `symbols: ${f.suggested_anchors.symbols.join(", ")}` : "";
|
|
536
|
+
console.log(ui.dim(` suggested anchors: ${[pathHints, symbolHints].filter(Boolean).join(" \xB7 ")}`));
|
|
571
537
|
}
|
|
538
|
+
console.log(ui.dim(` \u2192 ${f.file}`));
|
|
572
539
|
}
|
|
540
|
+
process.exitCode = findings.some((x) => x.severity === "error") ? 1 : 0;
|
|
573
541
|
});
|
|
574
542
|
}
|
|
575
|
-
function classifyCliPriority(item, filePaths, tokens, exactTaskHit, partialTaskHit) {
|
|
576
|
-
const fm = item.memory.frontmatter;
|
|
577
|
-
const anchored = filePaths.length > 0 && memoryMatchesAnchorPaths(item.memory, filePaths);
|
|
578
|
-
if (anchored || fm.type === "attempt" && exactTaskHit) return "must_read";
|
|
579
|
-
if (exactTaskHit || partialTaskHit || item.score >= 4 || tokens && fm.tags.some((tag) => tokens.includes(tag))) {
|
|
580
|
-
return "useful";
|
|
581
|
-
}
|
|
582
|
-
return "background";
|
|
583
|
-
}
|
|
584
|
-
function priorityBadge(priority) {
|
|
585
|
-
if (priority === "must_read") return ui.red("[must_read]");
|
|
586
|
-
if (priority === "useful") return ui.yellow("[useful]");
|
|
587
|
-
return ui.dim("[background]");
|
|
588
|
-
}
|
|
589
|
-
function parseCsv(value) {
|
|
590
|
-
if (!value) return [];
|
|
591
|
-
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
592
|
-
}
|
|
593
|
-
function collectInclude(value, previous) {
|
|
594
|
-
return [...previous, value];
|
|
595
|
-
}
|
|
596
543
|
|
|
597
|
-
// src/
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
544
|
+
// src/utils/autopilot.ts
|
|
545
|
+
async function applyAutopilotRepairs(root, paths, options = {}) {
|
|
546
|
+
const repairs = [];
|
|
547
|
+
const config = await loadConfig(paths);
|
|
548
|
+
if (options.applyConfig) {
|
|
549
|
+
const changed = await ensureAutopilotConfig(paths, config);
|
|
550
|
+
if (changed) {
|
|
551
|
+
repairs.push({
|
|
552
|
+
code: "autopilot-config",
|
|
553
|
+
message: "Enabled autopilot defaults in .ai/haive.config.json."
|
|
554
|
+
});
|
|
608
555
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
import { existsSync as existsSync2 } from "fs";
|
|
620
|
-
import path2 from "path";
|
|
621
|
-
import "commander";
|
|
622
|
-
import { findProjectRoot as findProjectRoot3, resolveHaivePaths as resolveHaivePaths2 } from "@hiveai/core";
|
|
623
|
-
function registerEmbeddings(program2) {
|
|
624
|
-
const embeddings = program2.command("embeddings").description("Manage local embeddings index for semantic search");
|
|
625
|
-
embeddings.command("index").description("Generate or refresh the embeddings index for all memories").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
626
|
-
const root = findProjectRoot3(opts.dir);
|
|
627
|
-
const paths = resolveHaivePaths2(root);
|
|
628
|
-
if (!existsSync2(paths.memoriesDir)) {
|
|
629
|
-
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
630
|
-
process.exitCode = 1;
|
|
631
|
-
return;
|
|
556
|
+
}
|
|
557
|
+
const current = await loadConfig(paths);
|
|
558
|
+
const autoRepair = current.autoRepair ?? {};
|
|
559
|
+
if (options.applyContext ?? autoRepair.context ?? current.autopilot) {
|
|
560
|
+
const changed = await syncProjectContextVersion(root, paths);
|
|
561
|
+
if (changed) {
|
|
562
|
+
repairs.push({
|
|
563
|
+
code: "project-context-version",
|
|
564
|
+
message: "Updated .ai/project-context.md version metadata from package.json."
|
|
565
|
+
});
|
|
632
566
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
embeddings.command("query <text>").description("Run a semantic search against the local embeddings index").option("-d, --dir <dir>", "project root").option("--limit <n>", "max results", "10").option("--min-score <n>", "minimum cosine similarity (0-1)", "0").action(async (text, opts) => {
|
|
643
|
-
const root = findProjectRoot3(opts.dir);
|
|
644
|
-
const paths = resolveHaivePaths2(root);
|
|
645
|
-
const { semanticSearch } = await loadEmbeddings();
|
|
646
|
-
const result = await semanticSearch(paths, text, {
|
|
647
|
-
limit: Number(opts.limit ?? 10),
|
|
648
|
-
minScore: Number(opts.minScore ?? 0)
|
|
649
|
-
});
|
|
650
|
-
if (!result) {
|
|
651
|
-
ui.error("No embeddings index found. Run `haive embeddings index` first.");
|
|
652
|
-
process.exitCode = 1;
|
|
653
|
-
return;
|
|
567
|
+
}
|
|
568
|
+
if (options.applyCorpus ?? autoRepair.corpus ?? current.autopilot) {
|
|
569
|
+
const report = await lintMemoriesAsync(root, { fix: true, apply: true });
|
|
570
|
+
const applied = report.fixes.filter((fix) => fix.applied);
|
|
571
|
+
if (applied.length > 0) {
|
|
572
|
+
repairs.push({
|
|
573
|
+
code: "memory-lint-fix",
|
|
574
|
+
message: `Applied ${applied.length} safe memory lint fix${applied.length === 1 ? "" : "es"}.`
|
|
575
|
+
});
|
|
654
576
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
577
|
+
const indexed = await refreshMemorySemanticIndex(paths);
|
|
578
|
+
if (indexed) {
|
|
579
|
+
repairs.push({
|
|
580
|
+
code: "memory-embeddings-index",
|
|
581
|
+
message: "Refreshed memory embeddings index."
|
|
582
|
+
});
|
|
658
583
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
584
|
+
}
|
|
585
|
+
if (options.applyCodeMap ?? autoRepair.codeMap ?? current.autopilot) {
|
|
586
|
+
const refreshed = await refreshCodeMap(root, paths, Boolean(options.forceCodeMap));
|
|
587
|
+
if (refreshed) {
|
|
588
|
+
repairs.push({
|
|
589
|
+
code: "code-map-refresh",
|
|
590
|
+
message: "Refreshed .ai/code-map.json."
|
|
591
|
+
});
|
|
663
592
|
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
return;
|
|
593
|
+
}
|
|
594
|
+
if (options.applyCodeSearch ?? autoRepair.codeSearch ?? current.autopilot) {
|
|
595
|
+
const indexed = await refreshCodeSearchIndex(paths);
|
|
596
|
+
if (indexed) {
|
|
597
|
+
repairs.push({
|
|
598
|
+
code: "code-search-index",
|
|
599
|
+
message: "Refreshed code-search embeddings index."
|
|
600
|
+
});
|
|
673
601
|
}
|
|
674
|
-
console.log(`${ui.bold("entries:")} ${stat2.count}`);
|
|
675
|
-
console.log(`${ui.bold("model:")} ${stat2.model}`);
|
|
676
|
-
console.log(`${ui.bold("updated_at:")} ${stat2.updatedAt}`);
|
|
677
|
-
console.log(`${ui.bold("size:")} ${(stat2.sizeBytes / 1024).toFixed(1)} KB`);
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
async function loadEmbeddings() {
|
|
681
|
-
try {
|
|
682
|
-
return await import("@hiveai/embeddings");
|
|
683
|
-
} catch {
|
|
684
|
-
ui.error(
|
|
685
|
-
"Could not load @hiveai/embeddings. Run: npm install -g @hiveai/embeddings (or `pnpm build` in the monorepo)"
|
|
686
|
-
);
|
|
687
|
-
process.exit(1);
|
|
688
602
|
}
|
|
603
|
+
return repairs;
|
|
689
604
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
includeUntracked: true,
|
|
719
|
-
excludeDirs: [
|
|
720
|
-
"node_modules",
|
|
721
|
-
"dist",
|
|
722
|
-
"build",
|
|
723
|
-
"out",
|
|
724
|
-
".git",
|
|
725
|
-
".next",
|
|
726
|
-
".turbo",
|
|
727
|
-
".vitest-cache",
|
|
728
|
-
"coverage",
|
|
729
|
-
...extraExcludes
|
|
730
|
-
]
|
|
731
|
-
});
|
|
732
|
-
await saveCodeMap(paths, map);
|
|
733
|
-
const fileCount = Object.keys(map.files).length;
|
|
734
|
-
const exportCount = Object.values(map.files).reduce((s, f) => s + f.exports.length, 0);
|
|
735
|
-
ui.success(
|
|
736
|
-
`Indexed ${fileCount} file(s) with ${exportCount} export(s) \u2192 ${path3.relative(root, codeMapPath(paths))}`
|
|
737
|
-
);
|
|
738
|
-
});
|
|
739
|
-
idx.command("code-search").description(
|
|
740
|
-
"Build the semantic-search embeddings index for code (powers the code_search MCP tool).\n\n Reads .ai/code-map.json (run `haive index code` first) and embeds each exported\n symbol's metadata (filename + name + kind + description).\n\n Re-runs are incremental: unchanged entries keep their cached vectors, only the\n diff is re-embedded. First run downloads the bge-small-en-v1.5 model (~110MB).\n"
|
|
741
|
-
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
742
|
-
const root = findProjectRoot4(opts.dir);
|
|
743
|
-
const paths = resolveHaivePaths3(root);
|
|
744
|
-
let mod;
|
|
745
|
-
try {
|
|
746
|
-
mod = await import("@hiveai/embeddings");
|
|
747
|
-
} catch {
|
|
748
|
-
ui.error(
|
|
749
|
-
"@hiveai/embeddings is not installed. Install it (`pnpm add @hiveai/embeddings`) or run `haive embeddings install`."
|
|
750
|
-
);
|
|
751
|
-
process.exit(1);
|
|
752
|
-
}
|
|
753
|
-
ui.info("Loading embedder (first run downloads ~110MB)\u2026");
|
|
754
|
-
const embedder = await mod.Embedder.create();
|
|
755
|
-
ui.info(`Embedding code-map symbols\u2026`);
|
|
756
|
-
try {
|
|
757
|
-
const { report } = await mod.rebuildCodeIndex(paths, embedder);
|
|
758
|
-
ui.success(
|
|
759
|
-
`Code-search index ready: ${report.total} symbols (+${report.added} new, ~${report.updated} updated, =${report.unchanged} cached, -${report.removed} removed)`
|
|
760
|
-
);
|
|
761
|
-
} catch (err) {
|
|
762
|
-
ui.error(err instanceof Error ? err.message : String(err));
|
|
763
|
-
process.exit(1);
|
|
605
|
+
async function ensureAutopilotConfig(paths, currentConfig) {
|
|
606
|
+
const current = currentConfig ?? await loadConfig(paths);
|
|
607
|
+
const next = {
|
|
608
|
+
...current,
|
|
609
|
+
autopilot: true,
|
|
610
|
+
defaultScope: "team",
|
|
611
|
+
defaultStatus: "validated",
|
|
612
|
+
autoApproveDelayHours: current.autoApproveDelayHours ?? AUTOPILOT_DEFAULTS.autoApproveDelayHours,
|
|
613
|
+
autoPromoteMinReads: current.autoPromoteMinReads ?? AUTOPILOT_DEFAULTS.autoPromoteMinReads,
|
|
614
|
+
autoSessionEnd: true,
|
|
615
|
+
autoContext: true,
|
|
616
|
+
autoRepair: {
|
|
617
|
+
context: true,
|
|
618
|
+
corpus: true,
|
|
619
|
+
codeMap: true,
|
|
620
|
+
codeSearch: current.autoRepair?.codeSearch ?? true
|
|
621
|
+
},
|
|
622
|
+
enforcement: {
|
|
623
|
+
...AUTOPILOT_DEFAULTS.enforcement,
|
|
624
|
+
...current.enforcement,
|
|
625
|
+
mode: "strict",
|
|
626
|
+
requireBriefingFirst: true,
|
|
627
|
+
requireSessionRecap: true,
|
|
628
|
+
requireMemoryVerify: true,
|
|
629
|
+
blockStaleDecisionChanges: true,
|
|
630
|
+
requireDecisionCoverage: true,
|
|
631
|
+
cleanupGeneratedArtifacts: true,
|
|
632
|
+
toolProfile: current.enforcement?.toolProfile ?? "enforcement"
|
|
764
633
|
}
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// src/commands/init.ts
|
|
769
|
-
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
|
|
770
|
-
import { existsSync as existsSync9 } from "fs";
|
|
771
|
-
import path10 from "path";
|
|
772
|
-
import { spawnSync as spawnSync3 } from "child_process";
|
|
773
|
-
import "commander";
|
|
774
|
-
import {
|
|
775
|
-
AUTOPILOT_DEFAULTS as AUTOPILOT_DEFAULTS2,
|
|
776
|
-
buildCodeMap as buildCodeMap3,
|
|
777
|
-
resolveHaivePaths as resolveHaivePaths6,
|
|
778
|
-
saveCodeMap as saveCodeMap3,
|
|
779
|
-
saveConfig as saveConfig2
|
|
780
|
-
} from "@hiveai/core";
|
|
781
|
-
|
|
782
|
-
// src/commands/agent.ts
|
|
783
|
-
import { spawnSync } from "child_process";
|
|
784
|
-
import { existsSync as existsSync4 } from "fs";
|
|
785
|
-
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
786
|
-
import os2 from "os";
|
|
787
|
-
import path5 from "path";
|
|
788
|
-
import { createInterface } from "readline/promises";
|
|
789
|
-
import "commander";
|
|
790
|
-
import { findProjectRoot as findProjectRoot5, resolveHaivePaths as resolveHaivePaths4 } from "@hiveai/core";
|
|
791
|
-
|
|
792
|
-
// src/commands/init-mcp-setup.ts
|
|
793
|
-
import { readFile as readFile2, writeFile, mkdir as mkdir2 } from "fs/promises";
|
|
794
|
-
import { existsSync as existsSync3 } from "fs";
|
|
795
|
-
import path4 from "path";
|
|
796
|
-
import os from "os";
|
|
797
|
-
var HOME = os.homedir();
|
|
798
|
-
var HAIVE_MCP_ENTRY = {
|
|
799
|
-
command: "haive",
|
|
800
|
-
args: ["mcp", "--stdio"]
|
|
801
|
-
};
|
|
802
|
-
function projectMcpEntry(root) {
|
|
803
|
-
return {
|
|
804
|
-
command: "haive",
|
|
805
|
-
args: ["mcp", "--stdio"],
|
|
806
|
-
env: { HAIVE_PROJECT_ROOT: root }
|
|
807
634
|
};
|
|
635
|
+
if (JSON.stringify(current) === JSON.stringify(next)) return false;
|
|
636
|
+
await saveConfig(paths, next);
|
|
637
|
+
return true;
|
|
808
638
|
}
|
|
809
|
-
function
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
639
|
+
async function syncProjectContextVersion(root, paths) {
|
|
640
|
+
const status = await projectContextVersionStatus(root, paths);
|
|
641
|
+
if (!status.canSync || !status.expectedVersion) return false;
|
|
642
|
+
const original = await readFile(paths.projectContext, "utf8");
|
|
643
|
+
let updated = original.replace(
|
|
644
|
+
/^# Project context — hAIve \(v[^)]+\)$/m,
|
|
645
|
+
`# Project context \u2014 hAIve (v${status.expectedVersion})`
|
|
646
|
+
).replace(
|
|
647
|
+
/> \*\*Current version\*\*: [^—\n]+—/m,
|
|
648
|
+
`> **Current version**: ${status.expectedVersion} \u2014`
|
|
649
|
+
);
|
|
650
|
+
if (updated === original && !original.includes("Current version")) {
|
|
651
|
+
updated = original.replace(
|
|
652
|
+
/^(> Repo-native context enforcement[^\n]*\n)/m,
|
|
653
|
+
`$1> **Current version**: ${status.expectedVersion} \u2014 @hiveai/core, cli, mcp, embeddings are versioned together.
|
|
654
|
+
`
|
|
655
|
+
);
|
|
822
656
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
function vscodeMcpPath() {
|
|
831
|
-
const candidates = [
|
|
832
|
-
path4.join(HOME, ".config", "Code", "User", "mcp.json"),
|
|
833
|
-
// Linux
|
|
834
|
-
path4.join(HOME, "Library", "Application Support", "Code", "User", "mcp.json"),
|
|
835
|
-
// macOS
|
|
836
|
-
path4.join(HOME, "AppData", "Roaming", "Code", "User", "mcp.json"),
|
|
837
|
-
// Windows
|
|
838
|
-
path4.join(HOME, ".config", "Code - Insiders", "User", "mcp.json")
|
|
839
|
-
];
|
|
840
|
-
for (const c of candidates) {
|
|
841
|
-
if (existsSync3(path4.dirname(c))) return c;
|
|
657
|
+
if (updated === original && !original.includes("Current version")) {
|
|
658
|
+
updated = original.replace(
|
|
659
|
+
/^(# Project context[^\n]*\n)/m,
|
|
660
|
+
`$1
|
|
661
|
+
> **Current version**: ${status.expectedVersion}
|
|
662
|
+
`
|
|
663
|
+
);
|
|
842
664
|
}
|
|
843
|
-
return
|
|
665
|
+
if (updated === original) return false;
|
|
666
|
+
await writeFile2(paths.projectContext, updated, "utf8");
|
|
667
|
+
return true;
|
|
844
668
|
}
|
|
845
|
-
async function
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
let config = {};
|
|
849
|
-
if (existsSync3(mcpPath)) {
|
|
850
|
-
try {
|
|
851
|
-
config = JSON.parse(await readFile2(mcpPath, "utf8"));
|
|
852
|
-
} catch {
|
|
853
|
-
}
|
|
669
|
+
async function projectContextVersionStatus(root, paths) {
|
|
670
|
+
if (!existsSync2(paths.projectContext)) {
|
|
671
|
+
return { mismatch: false, canSync: false };
|
|
854
672
|
}
|
|
855
|
-
|
|
856
|
-
if (
|
|
857
|
-
|
|
858
|
-
await mkdir2(path4.dirname(mcpPath), { recursive: true });
|
|
859
|
-
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
860
|
-
return { client: "VS Code", status: "configured", path: mcpPath };
|
|
861
|
-
}
|
|
862
|
-
function claudeConfigPath() {
|
|
863
|
-
const p = path4.join(HOME, ".claude.json");
|
|
864
|
-
if (existsSync3(p)) return p;
|
|
865
|
-
const p2 = path4.join(HOME, ".config", "claude", "claude.json");
|
|
866
|
-
if (existsSync3(path4.dirname(p2))) return p2;
|
|
867
|
-
return null;
|
|
868
|
-
}
|
|
869
|
-
async function configureClaude() {
|
|
870
|
-
const cfgPath = claudeConfigPath() ?? path4.join(HOME, ".claude.json");
|
|
871
|
-
if (!existsSync3(cfgPath) && !existsSync3(path4.join(HOME, ".claude"))) {
|
|
872
|
-
return { client: "Claude Code", status: "not_installed" };
|
|
673
|
+
const packagePath = path2.join(root, "package.json");
|
|
674
|
+
if (!existsSync2(packagePath)) {
|
|
675
|
+
return { mismatch: false, canSync: false };
|
|
873
676
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
} catch {
|
|
879
|
-
}
|
|
677
|
+
const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
|
|
678
|
+
const expectedVersion = packageJson.version;
|
|
679
|
+
if (!expectedVersion) {
|
|
680
|
+
return { mismatch: false, canSync: false };
|
|
880
681
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
return {
|
|
682
|
+
const content = await readFile(paths.projectContext, "utf8");
|
|
683
|
+
const headingVersion = content.match(/^# Project context — hAIve \(v([^)]+)\)$/m)?.[1];
|
|
684
|
+
const currentLineVersion = content.match(/^> \*\*Current version\*\*: ([^—\n]+)/m)?.[1]?.trim();
|
|
685
|
+
const currentVersion = currentLineVersion ?? headingVersion;
|
|
686
|
+
return {
|
|
687
|
+
expectedVersion,
|
|
688
|
+
currentVersion,
|
|
689
|
+
mismatch: currentVersion !== expectedVersion,
|
|
690
|
+
canSync: true
|
|
691
|
+
};
|
|
886
692
|
}
|
|
887
|
-
function
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
693
|
+
async function refreshCodeMap(root, paths, force) {
|
|
694
|
+
const existing = await loadCodeMap2(paths);
|
|
695
|
+
if (existing && !force) return false;
|
|
696
|
+
const map = await buildCodeMap(root, {
|
|
697
|
+
includeUntracked: true,
|
|
698
|
+
excludeDirs: [
|
|
699
|
+
"node_modules",
|
|
700
|
+
"dist",
|
|
701
|
+
"build",
|
|
702
|
+
"out",
|
|
703
|
+
".git",
|
|
704
|
+
".next",
|
|
705
|
+
".turbo",
|
|
706
|
+
".vitest-cache",
|
|
707
|
+
"coverage"
|
|
708
|
+
]
|
|
709
|
+
});
|
|
710
|
+
if (existing && existing.root === map.root && JSON.stringify(existing.files) === JSON.stringify(map.files)) {
|
|
711
|
+
return false;
|
|
894
712
|
}
|
|
895
|
-
|
|
713
|
+
await saveCodeMap(paths, map);
|
|
714
|
+
return true;
|
|
896
715
|
}
|
|
897
|
-
async function
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
}
|
|
716
|
+
async function refreshCodeSearchIndex(paths) {
|
|
717
|
+
try {
|
|
718
|
+
const mod = await import("@hiveai/embeddings");
|
|
719
|
+
const embedder = await mod.Embedder.create();
|
|
720
|
+
const { report } = await mod.rebuildCodeIndex(paths, embedder);
|
|
721
|
+
return report.added > 0 || report.updated > 0 || report.removed > 0;
|
|
722
|
+
} catch {
|
|
723
|
+
return false;
|
|
906
724
|
}
|
|
907
|
-
config.mcpServers ??= {};
|
|
908
|
-
if (config.mcpServers["haive"]) return { client: "Windsurf", status: "already_configured" };
|
|
909
|
-
config.mcpServers["haive"] = HAIVE_MCP_ENTRY;
|
|
910
|
-
await mkdir2(path4.dirname(mcpPath), { recursive: true });
|
|
911
|
-
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
912
|
-
return { client: "Windsurf", status: "configured", path: mcpPath };
|
|
913
725
|
}
|
|
914
|
-
async function
|
|
915
|
-
const results = [];
|
|
916
|
-
const configurators = [configureCursor, configureVSCode, configureClaude, configureWindsurf];
|
|
917
|
-
for (const fn of configurators) {
|
|
918
|
-
try {
|
|
919
|
-
results.push(await fn());
|
|
920
|
-
} catch (err) {
|
|
921
|
-
const name = fn.name.replace("configure", "");
|
|
922
|
-
results.push({ client: name, status: "error", error: String(err) });
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
return results;
|
|
926
|
-
}
|
|
927
|
-
async function configureProjectMcpClients(root) {
|
|
928
|
-
const entry = projectMcpEntry(root);
|
|
929
|
-
const results = [];
|
|
930
|
-
try {
|
|
931
|
-
const cursorPath = path4.join(root, ".cursor", "mcp.json");
|
|
932
|
-
let config = {};
|
|
933
|
-
if (existsSync3(cursorPath)) {
|
|
934
|
-
try {
|
|
935
|
-
config = JSON.parse(await readFile2(cursorPath, "utf8"));
|
|
936
|
-
} catch {
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
config.mcpServers ??= {};
|
|
940
|
-
config.mcpServers["haive"] = entry;
|
|
941
|
-
await mkdir2(path4.dirname(cursorPath), { recursive: true });
|
|
942
|
-
await writeFile(cursorPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
943
|
-
results.push({ client: "Cursor (project)", status: "configured", path: cursorPath });
|
|
944
|
-
} catch (err) {
|
|
945
|
-
results.push({ client: "Cursor (project)", status: "error", error: String(err) });
|
|
946
|
-
}
|
|
947
|
-
try {
|
|
948
|
-
const vscodePath = path4.join(root, ".vscode", "mcp.json");
|
|
949
|
-
let config = {};
|
|
950
|
-
if (existsSync3(vscodePath)) {
|
|
951
|
-
try {
|
|
952
|
-
config = JSON.parse(await readFile2(vscodePath, "utf8"));
|
|
953
|
-
} catch {
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
config.servers ??= {};
|
|
957
|
-
config.servers["haive"] = { ...entry, type: "stdio" };
|
|
958
|
-
await mkdir2(path4.dirname(vscodePath), { recursive: true });
|
|
959
|
-
await writeFile(vscodePath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
960
|
-
results.push({ client: "VS Code (workspace)", status: "configured", path: vscodePath });
|
|
961
|
-
} catch (err) {
|
|
962
|
-
results.push({ client: "VS Code (workspace)", status: "error", error: String(err) });
|
|
963
|
-
}
|
|
726
|
+
async function refreshMemorySemanticIndex(paths) {
|
|
964
727
|
try {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
if (
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
config.mcpServers["haive"] = { ...entry, type: "stdio" };
|
|
975
|
-
await writeFile(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
976
|
-
results.push({ client: "Claude Code (project)", status: "configured", path: mcpPath });
|
|
977
|
-
} catch (err) {
|
|
978
|
-
results.push({ client: "Claude Code (project)", status: "error", error: String(err) });
|
|
728
|
+
if (!existsSync2(paths.memoriesDir)) return false;
|
|
729
|
+
const memories = await loadMemoriesFromDir2(paths.memoriesDir);
|
|
730
|
+
if (memories.length === 0) return false;
|
|
731
|
+
const mod = await import("@hiveai/embeddings");
|
|
732
|
+
const embedder = await mod.Embedder.create();
|
|
733
|
+
const { report } = await mod.rebuildIndex(paths, embedder);
|
|
734
|
+
return report.added > 0 || report.updated > 0 || report.removed > 0;
|
|
735
|
+
} catch {
|
|
736
|
+
return false;
|
|
979
737
|
}
|
|
980
|
-
return results;
|
|
981
738
|
}
|
|
982
739
|
|
|
983
|
-
// src/commands/
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
});
|
|
1000
|
-
if (opts.json) {
|
|
1001
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1002
|
-
return;
|
|
740
|
+
// src/commands/briefing.ts
|
|
741
|
+
var RADAR_AUTO_THRESHOLD = 3;
|
|
742
|
+
var CHARS_PER_TOKEN = 4;
|
|
743
|
+
function printRadar(radar, out, reason) {
|
|
744
|
+
if (!radar.insideGitRepo) return;
|
|
745
|
+
if (!radarHasContent(radar)) return;
|
|
746
|
+
const header = reason === "low-memory-signal" ? "=== Project Radar (few relevant memories \u2014 surfacing live signals) ===" : "=== Project Radar ===";
|
|
747
|
+
out(`${ui.bold(header)}
|
|
748
|
+
`);
|
|
749
|
+
if (radar.recentCommits.length > 0) {
|
|
750
|
+
out(ui.bold("Recent commits:"));
|
|
751
|
+
for (const c of radar.recentCommits) {
|
|
752
|
+
const filesBlurb = c.files.slice(0, 3).join(", ");
|
|
753
|
+
const more = c.files.length > 3 ? ` (+${c.files.length - 3})` : "";
|
|
754
|
+
out(` ${ui.dim(c.date)} ${c.sha} ${c.subject}`);
|
|
755
|
+
if (filesBlurb) out(ui.dim(` ${filesBlurb}${more}`));
|
|
1003
756
|
}
|
|
1004
|
-
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
const projectResults = await configureProjectMcpClients(root);
|
|
1011
|
-
const detectionBeforeGlobal = await detectAgentMode(root);
|
|
1012
|
-
let globalResults = [];
|
|
1013
|
-
let globalSkippedReason;
|
|
1014
|
-
const shouldConsiderGlobal = opts.global !== false;
|
|
1015
|
-
if (shouldConsiderGlobal) {
|
|
1016
|
-
const approved = opts.yes === true || (opts.interactive ? await confirmGlobalSetup() : false);
|
|
1017
|
-
if (approved) {
|
|
1018
|
-
globalResults = await autoConfigureMcpClients();
|
|
1019
|
-
const codex = await configureCodexIfAvailable(root);
|
|
1020
|
-
if (codex) globalResults.push(codex);
|
|
1021
|
-
} else {
|
|
1022
|
-
globalSkippedReason = opts.interactive ? "User declined user-level/global MCP configuration." : "Non-interactive shell; skipped user-level/global MCP configuration. Re-run `haive agent setup --yes` to apply it.";
|
|
757
|
+
out("");
|
|
758
|
+
}
|
|
759
|
+
if (radar.openTodos.length > 0) {
|
|
760
|
+
out(ui.bold("Open TODOs/FIXMEs:"));
|
|
761
|
+
for (const t of radar.openTodos) {
|
|
762
|
+
out(` ${ui.dim(t.file + ":" + t.line)} ${t.text}`);
|
|
1023
763
|
}
|
|
1024
|
-
|
|
1025
|
-
globalSkippedReason = "User-level/global MCP configuration disabled.";
|
|
764
|
+
out("");
|
|
1026
765
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
mode_file: modeFile,
|
|
1034
|
-
...globalSkippedReason ? { global_skipped_reason: globalSkippedReason } : {}
|
|
1035
|
-
};
|
|
1036
|
-
}
|
|
1037
|
-
async function detectAgentMode(dir) {
|
|
1038
|
-
const root = findProjectRoot5(dir);
|
|
1039
|
-
const paths = resolveHaivePaths4(root);
|
|
1040
|
-
const projectMcp = [
|
|
1041
|
-
{ client: "Claude Code", path: path5.join(root, ".mcp.json"), present: existsSync4(path5.join(root, ".mcp.json")) },
|
|
1042
|
-
{ client: "Cursor", path: path5.join(root, ".cursor", "mcp.json"), present: existsSync4(path5.join(root, ".cursor", "mcp.json")) },
|
|
1043
|
-
{ client: "VS Code", path: path5.join(root, ".vscode", "mcp.json"), present: existsSync4(path5.join(root, ".vscode", "mcp.json")) }
|
|
1044
|
-
];
|
|
1045
|
-
const installedAgents = [
|
|
1046
|
-
{ agent: "Codex", command: "codex", installed: commandExists("codex"), mcp_configured: codexMcpConfigured() },
|
|
1047
|
-
{ agent: "Claude", command: "claude", installed: commandExists("claude") },
|
|
1048
|
-
{ agent: "Aider", command: "aider", installed: commandExists("aider") },
|
|
1049
|
-
{ agent: "Cursor", command: "cursor", installed: commandExists("cursor") }
|
|
1050
|
-
];
|
|
1051
|
-
const hasProjectMcp = projectMcp.some((item) => item.present);
|
|
1052
|
-
const hasNativeMcp = hasProjectMcp || installedAgents.some((a) => a.mcp_configured);
|
|
1053
|
-
const wrapperAgent = installedAgents.find((a) => a.installed && ["codex", "claude", "aider"].includes(a.command));
|
|
1054
|
-
const recommendedMode = hasNativeMcp ? "mcp" : wrapperAgent ? "wrapped" : "fallback";
|
|
1055
|
-
const recommendedCommand = recommendedMode === "mcp" ? "Restart your AI client, then call get_briefing before editing." : recommendedMode === "wrapped" && wrapperAgent ? `haive run -- ${wrapperAgent.command}` : 'haive briefing --task "..." --files "..."';
|
|
1056
|
-
return {
|
|
1057
|
-
root,
|
|
1058
|
-
initialized: existsSync4(paths.haiveDir),
|
|
1059
|
-
project_mcp: projectMcp,
|
|
1060
|
-
installed_agents: installedAgents,
|
|
1061
|
-
recommended_mode: recommendedMode,
|
|
1062
|
-
recommended_command: recommendedCommand
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
async function writeAgentModeRecord(paths, detection, skippedReason) {
|
|
1066
|
-
const dir = path5.join(paths.runtimeDir, "enforcement");
|
|
1067
|
-
await mkdir3(dir, { recursive: true });
|
|
1068
|
-
const file = path5.join(dir, "agent-mode.json");
|
|
1069
|
-
const record = {
|
|
1070
|
-
selected_mode: detection.recommended_mode,
|
|
1071
|
-
recommended_command: detection.recommended_command,
|
|
1072
|
-
configured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1073
|
-
project_root: detection.root,
|
|
1074
|
-
notes: [
|
|
1075
|
-
"mcp = native hAIve MCP tools are available or project MCP config exists.",
|
|
1076
|
-
"wrapped = use haive run when native MCP is unavailable.",
|
|
1077
|
-
"fallback = use haive briefing/enforce manually.",
|
|
1078
|
-
...skippedReason ? [skippedReason] : []
|
|
1079
|
-
]
|
|
1080
|
-
};
|
|
1081
|
-
await writeFile2(file, JSON.stringify(record, null, 2) + "\n", "utf8");
|
|
1082
|
-
return file;
|
|
1083
|
-
}
|
|
1084
|
-
async function confirmGlobalSetup() {
|
|
1085
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1086
|
-
try {
|
|
1087
|
-
const answer = await rl.question(
|
|
1088
|
-
"Configure hAIve in user-level AI client configs (Cursor/VS Code/Claude/Codex when detected)? [y/N] "
|
|
1089
|
-
);
|
|
1090
|
-
return /^y(es)?$/i.test(answer.trim());
|
|
1091
|
-
} finally {
|
|
1092
|
-
rl.close();
|
|
766
|
+
if (radar.hotFiles.length > 0) {
|
|
767
|
+
out(ui.bold("Hot files (most modified recently):"));
|
|
768
|
+
for (const f of radar.hotFiles) {
|
|
769
|
+
out(` ${f.changes}\xD7 ${ui.dim(f.path)}`);
|
|
770
|
+
}
|
|
771
|
+
out("");
|
|
1093
772
|
}
|
|
1094
773
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
const result = spawnSync("codex", [
|
|
1099
|
-
"mcp",
|
|
1100
|
-
"add",
|
|
1101
|
-
"haive",
|
|
1102
|
-
"--env",
|
|
1103
|
-
`HAIVE_PROJECT_ROOT=${root}`,
|
|
1104
|
-
"--",
|
|
1105
|
-
"haive",
|
|
1106
|
-
"mcp",
|
|
1107
|
-
"--stdio"
|
|
1108
|
-
], { encoding: "utf8" });
|
|
1109
|
-
if (result.status === 0) return { client: "Codex", status: "configured", path: path5.join(os2.homedir(), ".codex", "config.toml") };
|
|
1110
|
-
return { client: "Codex", status: "error", error: result.stderr || result.stdout || "codex mcp add failed" };
|
|
1111
|
-
}
|
|
1112
|
-
function commandExists(command) {
|
|
1113
|
-
const result = spawnSync(process.platform === "win32" ? "where" : "which", [command], {
|
|
1114
|
-
encoding: "utf8",
|
|
1115
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1116
|
-
});
|
|
1117
|
-
return result.status === 0;
|
|
1118
|
-
}
|
|
1119
|
-
function codexMcpConfigured() {
|
|
1120
|
-
if (!commandExists("codex")) return false;
|
|
1121
|
-
const result = spawnSync("codex", ["mcp", "get", "haive"], {
|
|
1122
|
-
encoding: "utf8",
|
|
1123
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1124
|
-
});
|
|
1125
|
-
return result.status === 0;
|
|
1126
|
-
}
|
|
1127
|
-
function printDetection(detection, json) {
|
|
1128
|
-
if (json) {
|
|
1129
|
-
console.log(JSON.stringify(detection, null, 2));
|
|
1130
|
-
return;
|
|
1131
|
-
}
|
|
1132
|
-
console.log(ui.bold("hAIve agent status"));
|
|
1133
|
-
console.log(ui.dim(` root: ${detection.root}`));
|
|
1134
|
-
console.log(`${detection.initialized ? ui.green("\u2713") : ui.red("\u2717")} project initialized`);
|
|
1135
|
-
for (const cfg of detection.project_mcp) {
|
|
1136
|
-
console.log(`${cfg.present ? ui.green("\u2713") : ui.yellow("\u2022")} ${cfg.client} project MCP ${ui.dim(path5.relative(detection.root, cfg.path))}`);
|
|
774
|
+
var TokenBudgetWriter = class {
|
|
775
|
+
constructor(budgetChars) {
|
|
776
|
+
this.budgetChars = budgetChars;
|
|
1137
777
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
778
|
+
budgetChars;
|
|
779
|
+
used = 0;
|
|
780
|
+
truncated = false;
|
|
781
|
+
write(text) {
|
|
782
|
+
if (this.truncated) return false;
|
|
783
|
+
const next = this.used + text.length + 1;
|
|
784
|
+
if (next > this.budgetChars) {
|
|
785
|
+
console.log(ui.dim(`... [briefing truncated to fit --max-tokens budget \xB7 ${Math.round(this.used / CHARS_PER_TOKEN)} tokens used]`));
|
|
786
|
+
this.truncated = true;
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
console.log(text);
|
|
790
|
+
this.used = next;
|
|
791
|
+
return true;
|
|
1142
792
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
}
|
|
1146
|
-
function printSetupResult(result) {
|
|
1147
|
-
for (const item of result.project_results) {
|
|
1148
|
-
if (item.status === "configured") ui.success(`${item.client} project MCP config written (${item.path})`);
|
|
1149
|
-
else if (item.status === "already_configured") ui.info(`${item.client} already configured`);
|
|
1150
|
-
else if (item.status === "error") ui.warn(`${item.client}: ${item.error}`);
|
|
793
|
+
isTruncated() {
|
|
794
|
+
return this.truncated;
|
|
1151
795
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
else if (item.status === "already_configured") ui.info(`${item.client} user-level MCP already configured`);
|
|
1155
|
-
else if (item.status === "not_installed") ui.info(`${item.client} not detected`);
|
|
1156
|
-
else if (item.status === "error") ui.warn(`${item.client}: ${item.error}`);
|
|
796
|
+
remainingChars() {
|
|
797
|
+
return Math.max(0, this.budgetChars - this.used);
|
|
1157
798
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
if (!existsSync5(paths.memoriesDir)) return { findings: out, fixes };
|
|
1197
|
-
const loaded = await loadMemoriesFromDir2(paths.memoriesDir);
|
|
1198
|
-
const usage = await loadUsageIndex2(paths);
|
|
1199
|
-
const codeMap = await loadCodeMap2(paths);
|
|
1200
|
-
const trackedFiles = gitTrackedFiles(root);
|
|
1201
|
-
const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
|
|
1202
|
-
const actionableWords = /\b(always|never|prefer|use|run|avoid|because|instead|why|rationale|do not|must|should|require|required|requires|fix|fail|failed|fails|prevent|prevents|allow|allows|lets|ensure|ensures|catch|catches)\b/i;
|
|
1203
|
-
for (const { filePath, memory: memory2 } of loaded) {
|
|
1204
|
-
const fm = memory2.frontmatter;
|
|
1205
|
-
if (fm.type === "session_recap") continue;
|
|
1206
|
-
const body = memory2.body.trim();
|
|
1207
|
-
const naked = body.replace(/^#.*$/gm, "").replace(/```[\s\S]*?```/g, "").trim();
|
|
1208
|
-
if (naked.length < 40 && fm.status !== "rejected") {
|
|
1209
|
-
out.push({
|
|
1210
|
-
file: filePath,
|
|
1211
|
-
id: fm.id,
|
|
1212
|
-
severity: "warn",
|
|
1213
|
-
code: "SHORT_BODY",
|
|
1214
|
-
message: "Body looks very short (< ~40 chars of prose after headings). Prefer actionable detail."
|
|
799
|
+
};
|
|
800
|
+
function registerBriefing(program2) {
|
|
801
|
+
program2.command("briefing").description(
|
|
802
|
+
'Print the full project briefing: last session recap + project context + relevant memories.\n Equivalent to calling get_briefing via MCP. Run before starting any task.\n\n Examples:\n haive briefing\n haive briefing --task "add Stripe payment" --files src/payments/PaymentService.ts\n haive briefing --budget quick --task "tiny fix"\n'
|
|
803
|
+
).option("--task <text>", "what you are about to do \u2014 filters memories by relevance").option("--files <csv>", "comma-separated file paths being worked on (surfaces anchored memories)").option("--symbols <csv>", "symbol names to look up in the code-map (e.g. PaymentService,TenantFilter) \u2014 requires haive index code").option("--max-memories <n>", "cap on memories surfaced", "8").option("--max-tokens <n>", "approximate token budget for the entire briefing (truncates if exceeded)").option("--explain-source", "annotate each memory with [source: <relative-path> \xB7 anchors: <files>] for traceable citations").option("--radar", "force project radar (recent commits, open TODOs, hot files) even when memories are plentiful").option("--no-radar", "disable the project radar even when memories are scarce").option(
|
|
804
|
+
"--budget <preset>",
|
|
805
|
+
"align with MCP get_briefing budget_preset: quick | balanced | deep \u2014 sets cap + truncation budget (overrides --max-memories / replaces default open-ended output)",
|
|
806
|
+
void 0
|
|
807
|
+
).option(
|
|
808
|
+
"--memory-format <mode>",
|
|
809
|
+
"printed memory bodies: full (default) | actions (cheap bullet-focused excerpt)",
|
|
810
|
+
"full"
|
|
811
|
+
).option(
|
|
812
|
+
"--format <mode>",
|
|
813
|
+
"alias for --memory-format; accepts full | actions | compact"
|
|
814
|
+
).option(
|
|
815
|
+
"--scope <scope>",
|
|
816
|
+
"personal | team | shared | all (default: all \u2014 includes team + shared cross-repo memories)",
|
|
817
|
+
"all"
|
|
818
|
+
).option("--include-draft", "include draft memories (excluded by default)").option("--include-stale", "include stale memories (excluded by default \u2014 may be outdated)").option(
|
|
819
|
+
"--include <path>",
|
|
820
|
+
"merge memories from another haive-initialized project (repeatable). Useful for teams with multiple coordinated repos (e.g. backend + frontend).",
|
|
821
|
+
collectInclude,
|
|
822
|
+
[]
|
|
823
|
+
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
824
|
+
const root = findProjectRoot2(opts.dir);
|
|
825
|
+
const paths = resolveHaivePaths2(root);
|
|
826
|
+
const requestedFormat = (opts.format ?? opts.memoryFormat ?? "full").toLowerCase();
|
|
827
|
+
opts.memoryFormat = requestedFormat === "compact" ? "actions" : requestedFormat;
|
|
828
|
+
const markerFiles = parseCsv(opts.files);
|
|
829
|
+
if (existsSync3(paths.haiveDir)) {
|
|
830
|
+
await applyAutopilotRepairs(root, paths, {
|
|
831
|
+
applyConfig: false,
|
|
832
|
+
applyContext: true,
|
|
833
|
+
applyCorpus: true,
|
|
834
|
+
applyCodeMap: false,
|
|
835
|
+
applyCodeSearch: true
|
|
836
|
+
}).catch(() => {
|
|
1215
837
|
});
|
|
1216
838
|
}
|
|
1217
|
-
if (
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
839
|
+
if (existsSync3(paths.haiveDir)) {
|
|
840
|
+
await mkdir(paths.runtimeDir, { recursive: true });
|
|
841
|
+
await writeBriefingMarker(paths, {
|
|
842
|
+
task: opts.task ?? "CLI briefing",
|
|
843
|
+
source: "haive-briefing-cli",
|
|
844
|
+
sessionId: process.env.HAIVE_SESSION_ID,
|
|
845
|
+
files: markerFiles
|
|
846
|
+
}).catch(() => {
|
|
1224
847
|
});
|
|
1225
848
|
}
|
|
1226
|
-
|
|
1227
|
-
if (
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
severity: "warn",
|
|
1232
|
-
code: "MISSING_ANCHOR",
|
|
1233
|
-
message: `${fm.type} is validated without anchor paths \u2014 add anchor.paths so haive sync can flag staleness.`,
|
|
1234
|
-
...suggestedAnchors.paths.length > 0 || suggestedAnchors.symbols.length > 0 ? { suggested_anchors: suggestedAnchors } : {}
|
|
1235
|
-
});
|
|
849
|
+
let budgetPreset = null;
|
|
850
|
+
if (opts.budget) {
|
|
851
|
+
const b = opts.budget.trim().toLowerCase();
|
|
852
|
+
if (b === "quick" || b === "balanced" || b === "deep") budgetPreset = b;
|
|
853
|
+
else ui.warn(`Unknown --budget '${opts.budget}' \u2014 ignoring (use quick|balanced|deep).`);
|
|
1236
854
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
855
|
+
let maxMemories = Math.max(1, Number(opts.maxMemories ?? 8));
|
|
856
|
+
let budgetTokensCap = opts.maxTokens ? Math.max(100, Number(opts.maxTokens)) : null;
|
|
857
|
+
if (budgetPreset !== null) {
|
|
858
|
+
const presetNums = resolveBriefingBudget(budgetPreset, {
|
|
859
|
+
max_tokens: 8e3,
|
|
860
|
+
max_memories: 8,
|
|
861
|
+
include_module_contexts: true
|
|
1244
862
|
});
|
|
863
|
+
budgetTokensCap = presetNums.max_tokens;
|
|
864
|
+
maxMemories = presetNums.max_memories;
|
|
1245
865
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
866
|
+
const writer = budgetTokensCap !== null ? new TokenBudgetWriter(budgetTokensCap * CHARS_PER_TOKEN) : null;
|
|
867
|
+
const out = (text) => {
|
|
868
|
+
if (writer) return writer.write(text);
|
|
869
|
+
console.log(text);
|
|
870
|
+
return true;
|
|
871
|
+
};
|
|
872
|
+
const stopped = () => writer?.isTruncated() ?? false;
|
|
873
|
+
if (!existsSync3(paths.memoriesDir)) {
|
|
874
|
+
if (existsSync3(paths.projectContext)) {
|
|
875
|
+
out(`${ui.bold("=== Project Context ===")}
|
|
876
|
+
`);
|
|
877
|
+
out((await readFile2(paths.projectContext, "utf8")).trim());
|
|
878
|
+
out("");
|
|
879
|
+
} else {
|
|
880
|
+
ui.warn("No project-context.md found. Run `haive init` and the `bootstrap_project` MCP prompt to set it up.");
|
|
881
|
+
}
|
|
882
|
+
if (opts.radar !== false && !stopped()) {
|
|
883
|
+
const filePathsEarly = parseCsv(opts.files);
|
|
884
|
+
const tokensEarly = opts.task ? tokenizeQuery(opts.task) : null;
|
|
885
|
+
const radar = await buildRadar({ root, taskTokens: tokensEarly, filePaths: filePathsEarly });
|
|
886
|
+
printRadar(radar, out, "low-memory-signal");
|
|
887
|
+
}
|
|
888
|
+
return;
|
|
1254
889
|
}
|
|
1255
|
-
const
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
890
|
+
const ownMemories = await loadMemoriesFromDir3(paths.memoriesDir);
|
|
891
|
+
const externalRoots = [];
|
|
892
|
+
if (opts.include && opts.include.length > 0) {
|
|
893
|
+
for (const includePath of opts.include) {
|
|
894
|
+
try {
|
|
895
|
+
const otherRoot = findProjectRoot2(includePath);
|
|
896
|
+
if (otherRoot === root) continue;
|
|
897
|
+
const otherPaths = resolveHaivePaths2(otherRoot);
|
|
898
|
+
if (!existsSync3(otherPaths.memoriesDir)) {
|
|
899
|
+
ui.warn(`--include ${includePath}: no .ai/memories at ${otherRoot} \u2014 skipping`);
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const otherMemories = await loadMemoriesFromDir3(otherPaths.memoriesDir);
|
|
903
|
+
const tag = path3.basename(otherRoot);
|
|
904
|
+
for (const m of otherMemories) {
|
|
905
|
+
ownMemories.push({ ...m, origin: tag });
|
|
906
|
+
}
|
|
907
|
+
externalRoots.push(`${tag} (${otherMemories.length})`);
|
|
908
|
+
} catch (err) {
|
|
909
|
+
ui.warn(`--include ${includePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (externalRoots.length > 0) {
|
|
913
|
+
ui.info(`merged from: ${externalRoots.join(", ")}`);
|
|
914
|
+
console.log();
|
|
915
|
+
}
|
|
1264
916
|
}
|
|
1265
|
-
const
|
|
1266
|
-
const
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
}
|
|
917
|
+
const all = ownMemories;
|
|
918
|
+
const filePaths = markerFiles;
|
|
919
|
+
const tokens = opts.task ? tokenizeQuery(opts.task) : null;
|
|
920
|
+
const scopeFilter = opts.scope ?? "all";
|
|
921
|
+
const recaps = all.filter(({ memory: mem }) => mem.frontmatter.type === "session_recap").sort(
|
|
922
|
+
(a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
|
|
923
|
+
);
|
|
924
|
+
if (recaps.length > 0 && !stopped()) {
|
|
925
|
+
const recap = recaps[0];
|
|
926
|
+
const fm = recap.memory.frontmatter;
|
|
927
|
+
const rev = fm.revision_count ? ` \xB7 revision #${fm.revision_count}` : "";
|
|
928
|
+
out(`${ui.bold("=== Last Session Recap ===")}
|
|
929
|
+
`);
|
|
930
|
+
out(ui.dim(`${fm.id} (${fm.scope}${rev})`));
|
|
931
|
+
out(recap.memory.body.trim());
|
|
932
|
+
out("");
|
|
1276
933
|
}
|
|
1277
|
-
if (
|
|
1278
|
-
const
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
934
|
+
if (existsSync3(paths.projectContext) && !stopped()) {
|
|
935
|
+
const ctx = await readFile2(paths.projectContext, "utf8");
|
|
936
|
+
const isTemplate = ctx.includes("TODO \u2014 high-level overview") || ctx.includes("Generated by `haive init`");
|
|
937
|
+
if (isTemplate) {
|
|
938
|
+
ui.warn(
|
|
939
|
+
"project-context.md still contains the default template \u2014 get_briefing will return little value."
|
|
940
|
+
);
|
|
941
|
+
ui.warn(
|
|
942
|
+
"Fix: in your AI client, invoke the MCP prompt bootstrap_project to auto-fill it from your codebase."
|
|
943
|
+
);
|
|
944
|
+
out("");
|
|
945
|
+
} else {
|
|
946
|
+
out(`${ui.bold("=== Project Context ===")}
|
|
947
|
+
`);
|
|
948
|
+
out(ctx.trim());
|
|
949
|
+
out("");
|
|
1286
950
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
951
|
+
} else if (!existsSync3(paths.projectContext)) {
|
|
952
|
+
ui.warn(
|
|
953
|
+
"No project-context.md found. Run `haive init` then invoke the bootstrap_project MCP prompt."
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
const candidates = all.filter(({ memory: mem }) => {
|
|
957
|
+
const fm = mem.frontmatter;
|
|
958
|
+
if (fm.status === "rejected" || fm.status === "deprecated") return false;
|
|
959
|
+
if (!opts.includeDraft && fm.status === "draft") return false;
|
|
960
|
+
if (!opts.includeStale && fm.status === "stale") return false;
|
|
961
|
+
if (scopeFilter !== "all" && fm.scope !== scopeFilter && !(scopeFilter === "team" && fm.scope === "shared")) return false;
|
|
962
|
+
if (fm.type === "session_recap") return false;
|
|
963
|
+
return true;
|
|
964
|
+
});
|
|
965
|
+
const andTaskHits = tokens ? new Set(candidates.filter(({ memory: mem }) => literalMatchesAllTokens(mem, tokens)).map(({ memory: mem }) => mem.frontmatter.id)) : null;
|
|
966
|
+
const useOrFallback = andTaskHits !== null && andTaskHits.size === 0 && (tokens?.length ?? 0) > 1;
|
|
967
|
+
const scored = candidates.map(({ memory: mem, filePath }) => {
|
|
968
|
+
const fm = mem.frontmatter;
|
|
969
|
+
let score = 0;
|
|
970
|
+
if (fm.status === "validated") score += 3;
|
|
971
|
+
else if (fm.status === "proposed") score += 1;
|
|
972
|
+
if (filePaths.length > 0 && memoryMatchesAnchorPaths(mem, filePaths)) score += 4;
|
|
973
|
+
if (tokens) {
|
|
974
|
+
if (andTaskHits?.has(fm.id)) score += 3;
|
|
975
|
+
else if (useOrFallback && literalMatchesAnyToken(mem, tokens)) score += 1;
|
|
1303
976
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
977
|
+
return { memory: mem, filePath, score };
|
|
978
|
+
});
|
|
979
|
+
scored.sort((a, b) => b.score - a.score);
|
|
980
|
+
const top = scored.slice(0, maxMemories);
|
|
981
|
+
if (top.length === 0) {
|
|
982
|
+
ui.info("No relevant memories found.");
|
|
983
|
+
const draftCount = all.filter(
|
|
984
|
+
(m) => m.memory.frontmatter.status === "draft" && (scopeFilter === "all" || m.memory.frontmatter.scope === scopeFilter)
|
|
985
|
+
).length;
|
|
986
|
+
if (draftCount > 0) {
|
|
987
|
+
ui.info(`(${draftCount} draft memories excluded \u2014 use --include-draft to show)`);
|
|
1310
988
|
}
|
|
1311
|
-
if (
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
989
|
+
if (opts.radar !== false && !stopped()) {
|
|
990
|
+
const radar = await buildRadar({ root, taskTokens: tokens, filePaths });
|
|
991
|
+
out("");
|
|
992
|
+
printRadar(radar, out, "low-memory-signal");
|
|
993
|
+
}
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
if (stopped()) return;
|
|
997
|
+
const usageIndex = await loadUsageIndex2(paths).catch(() => null);
|
|
998
|
+
out(`${ui.bold("=== Relevant Memories ===")}
|
|
999
|
+
`);
|
|
1000
|
+
const priorities = top.map(
|
|
1001
|
+
(item) => classifyCliPriority(
|
|
1002
|
+
item,
|
|
1003
|
+
filePaths,
|
|
1004
|
+
tokens,
|
|
1005
|
+
Boolean(andTaskHits?.has(item.memory.frontmatter.id)),
|
|
1006
|
+
Boolean(useOrFallback && tokens && literalMatchesAnyToken(item.memory, tokens))
|
|
1007
|
+
)
|
|
1008
|
+
);
|
|
1009
|
+
const mustReadCount = priorities.filter((p) => p === "must_read").length;
|
|
1010
|
+
const usefulCount = priorities.filter((p) => p === "useful").length;
|
|
1011
|
+
const backgroundCount = priorities.filter((p) => p === "background").length;
|
|
1012
|
+
const quality = mustReadCount > 0 || usefulCount > 0 ? backgroundCount > mustReadCount + usefulCount && backgroundCount > 2 ? "noisy" : "strong" : "thin";
|
|
1013
|
+
out(ui.dim(`briefing_quality: ${quality} \xB7 must_read=${mustReadCount} useful=${usefulCount} background=${backgroundCount}`));
|
|
1014
|
+
out("");
|
|
1015
|
+
for (const [idx, item] of top.entries()) {
|
|
1016
|
+
if (stopped()) break;
|
|
1017
|
+
const fm = item.memory.frontmatter;
|
|
1018
|
+
const badge = ui.statusBadge(fm.status);
|
|
1019
|
+
const draftMarker = fm.status === "draft" ? ui.yellow(" [DRAFT]") : "";
|
|
1020
|
+
const unverifiedMarker = fm.status === "proposed" ? ui.yellow(" [UNVERIFIED]") : "";
|
|
1021
|
+
const originMarker = item.origin ? ` ${ui.yellow("[from " + item.origin + "]")}` : "";
|
|
1022
|
+
const reads = usageIndex?.by_id[fm.id]?.read_count ?? 0;
|
|
1023
|
+
const hitMarker = reads > 0 ? ` ${ui.dim("\xB7 " + reads + "\xD7 read")}` : "";
|
|
1024
|
+
const priority = priorities[idx] ?? "background";
|
|
1025
|
+
out(
|
|
1026
|
+
`${ui.bold(fm.id)} ${priorityBadge(priority)} ${ui.dim(fm.scope + "/" + fm.type)} ${badge}${draftMarker}${unverifiedMarker}${originMarker}${hitMarker}`
|
|
1027
|
+
);
|
|
1028
|
+
if (opts.explainSource) {
|
|
1029
|
+
const relPath = path3.relative(root, item.filePath);
|
|
1030
|
+
const anchorPaths = fm.anchor?.paths ?? [];
|
|
1031
|
+
const anchorSymbols = fm.anchor?.symbols ?? [];
|
|
1032
|
+
const parts = [`source: ${relPath}`];
|
|
1033
|
+
if (anchorPaths.length > 0) parts.push(`paths: ${anchorPaths.join(", ")}`);
|
|
1034
|
+
if (anchorSymbols.length > 0) parts.push(`symbols: ${anchorSymbols.join(", ")}`);
|
|
1035
|
+
out(ui.dim(` [${parts.join(" \xB7 ")}]`));
|
|
1036
|
+
}
|
|
1037
|
+
const memBody = opts.memoryFormat?.toLowerCase() === "actions" ? extractActionsBriefBody(item.memory.body) : item.memory.body.trim();
|
|
1038
|
+
out(memBody);
|
|
1039
|
+
out("");
|
|
1040
|
+
}
|
|
1041
|
+
if (!stopped()) out(ui.dim(`${top.length} memor${top.length === 1 ? "y" : "ies"} surfaced`));
|
|
1042
|
+
const ids = top.map(({ memory: mem }) => mem.frontmatter.id);
|
|
1043
|
+
if (ids.length > 0) {
|
|
1044
|
+
await trackReads(paths, ids).catch(() => {
|
|
1045
|
+
});
|
|
1046
|
+
await writeBriefingMarker(paths, {
|
|
1047
|
+
task: opts.task ?? "CLI briefing",
|
|
1048
|
+
source: "haive-briefing-cli",
|
|
1049
|
+
sessionId: process.env.HAIVE_SESSION_ID,
|
|
1050
|
+
memoryIds: ids,
|
|
1051
|
+
files: filePaths
|
|
1052
|
+
}).catch(() => {
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
const radarForced = opts.radar === true;
|
|
1056
|
+
const radarAuto = opts.radar !== false && top.length < RADAR_AUTO_THRESHOLD;
|
|
1057
|
+
if ((radarForced || radarAuto) && !stopped()) {
|
|
1058
|
+
const radar = await buildRadar({ root, taskTokens: tokens, filePaths });
|
|
1059
|
+
if (radarHasContent(radar)) {
|
|
1060
|
+
out("");
|
|
1061
|
+
printRadar(radar, out, radarForced ? "forced" : "low-memory-signal");
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
const requestedSymbols = (opts.symbols ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1065
|
+
if (requestedSymbols.length > 0 && !stopped()) {
|
|
1066
|
+
const codeMap = await loadCodeMap3(paths);
|
|
1067
|
+
if (!codeMap) {
|
|
1068
|
+
ui.warn("No code-map found. Run `haive index code` first to enable symbol lookup.");
|
|
1069
|
+
} else {
|
|
1070
|
+
out(`
|
|
1071
|
+
${ui.bold("=== Symbol Locations ===")}
|
|
1072
|
+
`);
|
|
1073
|
+
for (const sym of requestedSymbols) {
|
|
1074
|
+
if (stopped()) break;
|
|
1075
|
+
const { files } = queryCodeMap(codeMap, { symbol: sym });
|
|
1076
|
+
if (files.length === 0) {
|
|
1077
|
+
out(`${ui.dim(sym)} (not found in code-map)`);
|
|
1078
|
+
} else {
|
|
1079
|
+
for (const f of files) {
|
|
1080
|
+
if (stopped()) break;
|
|
1081
|
+
const exports = f.entry.exports.filter(
|
|
1082
|
+
(e) => e.name.toLowerCase().includes(sym.toLowerCase())
|
|
1083
|
+
);
|
|
1084
|
+
for (const e of exports) {
|
|
1085
|
+
if (stopped()) break;
|
|
1086
|
+
const desc = e.description ? ` \u2014 ${e.description}` : "";
|
|
1087
|
+
out(`${ui.bold(e.name)} ${ui.dim(f.path + ":" + e.line)} [${e.kind}]${desc}`);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1319
1091
|
}
|
|
1092
|
+
out("");
|
|
1320
1093
|
}
|
|
1321
1094
|
}
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
function classifyCliPriority(item, filePaths, tokens, exactTaskHit, partialTaskHit) {
|
|
1098
|
+
const fm = item.memory.frontmatter;
|
|
1099
|
+
const anchored = filePaths.length > 0 && memoryMatchesAnchorPaths(item.memory, filePaths);
|
|
1100
|
+
if (anchored || fm.type === "attempt" && exactTaskHit) return "must_read";
|
|
1101
|
+
if (exactTaskHit || partialTaskHit || item.score >= 4 || tokens && fm.tags.some((tag) => tokens.includes(tag))) {
|
|
1102
|
+
return "useful";
|
|
1322
1103
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1104
|
+
return "background";
|
|
1105
|
+
}
|
|
1106
|
+
function priorityBadge(priority) {
|
|
1107
|
+
if (priority === "must_read") return ui.red("[must_read]");
|
|
1108
|
+
if (priority === "useful") return ui.yellow("[useful]");
|
|
1109
|
+
return ui.dim("[background]");
|
|
1110
|
+
}
|
|
1111
|
+
function parseCsv(value) {
|
|
1112
|
+
if (!value) return [];
|
|
1113
|
+
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1114
|
+
}
|
|
1115
|
+
function collectInclude(value, previous) {
|
|
1116
|
+
return [...previous, value];
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/commands/tui.ts
|
|
1120
|
+
import "commander";
|
|
1121
|
+
import { findProjectRoot as findProjectRoot3 } from "@hiveai/core";
|
|
1122
|
+
function registerTui(program2) {
|
|
1123
|
+
program2.command("tui").description(
|
|
1124
|
+
"Interactive terminal dashboard for browsing and managing memories.\n\n Screens (switch with 1 / 2 / 3):\n 1 \u2014 Memories: list + preview, filter by status (Tab), actions (a/r/p/d)\n 2 \u2014 Health: stale, pending review, anchorless memories\n 3 \u2014 Stats: most-read, decaying, total counts\n\n Key bindings:\n \u2191 \u2193 navigate list\n Tab cycle status filter (all \u2192 proposed \u2192 validated \u2192 stale)\n a approve selected memory\n r reject selected memory\n p promote personal \u2192 team (proposed)\n d delete selected memory\n q / Esc exit\n"
|
|
1125
|
+
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
1126
|
+
if (!process.stdout.isTTY) {
|
|
1127
|
+
console.error("haive tui requires an interactive terminal (TTY).");
|
|
1128
|
+
process.exitCode = 1;
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
const root = findProjectRoot3(opts.dir);
|
|
1132
|
+
const { render } = await import("ink");
|
|
1133
|
+
const { createElement } = await import("react");
|
|
1134
|
+
const { Dashboard } = await import("./Dashboard-Y2AIWFZK.js");
|
|
1135
|
+
const { waitUntilExit } = render(createElement(Dashboard, { root }));
|
|
1136
|
+
await waitUntilExit();
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// src/commands/embeddings.ts
|
|
1141
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1142
|
+
import path4 from "path";
|
|
1143
|
+
import "commander";
|
|
1144
|
+
import { findProjectRoot as findProjectRoot4, resolveHaivePaths as resolveHaivePaths3 } from "@hiveai/core";
|
|
1145
|
+
function registerEmbeddings(program2) {
|
|
1146
|
+
const embeddings = program2.command("embeddings").description("Manage local embeddings index for semantic search");
|
|
1147
|
+
embeddings.command("index").description("Generate or refresh the embeddings index for all memories").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
1148
|
+
const root = findProjectRoot4(opts.dir);
|
|
1149
|
+
const paths = resolveHaivePaths3(root);
|
|
1150
|
+
if (!existsSync4(paths.memoriesDir)) {
|
|
1151
|
+
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
1152
|
+
process.exitCode = 1;
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
const { Embedder, rebuildIndex } = await loadEmbeddings();
|
|
1156
|
+
ui.info("Loading embedding model (first run downloads ~110MB)\u2026");
|
|
1157
|
+
const embedder = await Embedder.create();
|
|
1158
|
+
ui.info(`Model ready: ${embedder.model} (dim=${embedder.dimension}). Indexing memories\u2026`);
|
|
1159
|
+
const { report } = await rebuildIndex(paths, embedder);
|
|
1160
|
+
ui.success(
|
|
1161
|
+
`Indexed ${report.total} memories \u2014 added=${report.added} updated=${report.updated} unchanged=${report.unchanged} removed=${report.removed}`
|
|
1162
|
+
);
|
|
1163
|
+
});
|
|
1164
|
+
embeddings.command("query <text>").description("Run a semantic search against the local embeddings index").option("-d, --dir <dir>", "project root").option("--limit <n>", "max results", "10").option("--min-score <n>", "minimum cosine similarity (0-1)", "0").action(async (text, opts) => {
|
|
1165
|
+
const root = findProjectRoot4(opts.dir);
|
|
1166
|
+
const paths = resolveHaivePaths3(root);
|
|
1167
|
+
const { semanticSearch } = await loadEmbeddings();
|
|
1168
|
+
const result = await semanticSearch(paths, text, {
|
|
1169
|
+
limit: Number(opts.limit ?? 10),
|
|
1170
|
+
minScore: Number(opts.minScore ?? 0)
|
|
1330
1171
|
});
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
const withoutDate = id.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
1336
|
-
return withoutDate.split("-").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
1337
|
-
}
|
|
1338
|
-
function suggestAnchors(root, loaded, codeMap, trackedFiles) {
|
|
1339
|
-
const body = loaded.memory.body;
|
|
1340
|
-
const paths = /* @__PURE__ */ new Set();
|
|
1341
|
-
const symbols = /* @__PURE__ */ new Set();
|
|
1342
|
-
for (const match of body.matchAll(/`([^`\n]+\.[A-Za-z0-9]+)`|(?:^|\s)([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)/gm)) {
|
|
1343
|
-
const candidate = (match[1] ?? match[2] ?? "").replace(/^\.?\//, "");
|
|
1344
|
-
if (!candidate || candidate.startsWith("http")) continue;
|
|
1345
|
-
if (existsSync5(path6.join(root, candidate)) && isSafeAnchorPath(candidate, trackedFiles)) {
|
|
1346
|
-
paths.add(candidate);
|
|
1172
|
+
if (!result) {
|
|
1173
|
+
ui.error("No embeddings index found. Run `haive embeddings index` first.");
|
|
1174
|
+
process.exitCode = 1;
|
|
1175
|
+
return;
|
|
1347
1176
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
paths.add(file);
|
|
1357
|
-
symbols.add(exp.name);
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
if (paths.size >= 5 && symbols.size >= 5) break;
|
|
1361
|
-
}
|
|
1362
|
-
if (paths.size >= 5 && symbols.size >= 5) break;
|
|
1177
|
+
if (result.hits.length === 0) {
|
|
1178
|
+
ui.info("No semantic matches above the threshold.");
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
for (const hit of result.hits) {
|
|
1182
|
+
const score = hit.score.toFixed(3);
|
|
1183
|
+
console.log(`${ui.bold(score)} ${hit.id}`);
|
|
1184
|
+
console.log(` ${ui.dim(path4.relative(root, hit.file_path))}`);
|
|
1363
1185
|
}
|
|
1364
|
-
}
|
|
1365
|
-
return {
|
|
1366
|
-
paths: [...paths].slice(0, 5),
|
|
1367
|
-
symbols: [...symbols].slice(0, 5)
|
|
1368
|
-
};
|
|
1369
|
-
}
|
|
1370
|
-
function gitTrackedFiles(root) {
|
|
1371
|
-
const result = spawnSync2("git", ["ls-files"], {
|
|
1372
|
-
cwd: root,
|
|
1373
|
-
encoding: "utf8",
|
|
1374
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1375
1186
|
});
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1187
|
+
embeddings.command("status").description("Show the embeddings index status").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
1188
|
+
const root = findProjectRoot4(opts.dir);
|
|
1189
|
+
const paths = resolveHaivePaths3(root);
|
|
1190
|
+
const { indexStat } = await loadEmbeddings();
|
|
1191
|
+
const stat2 = await indexStat(paths);
|
|
1192
|
+
if (!stat2.exists) {
|
|
1193
|
+
ui.warn("No embeddings index. Run `haive embeddings index` to create one.");
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
console.log(`${ui.bold("entries:")} ${stat2.count}`);
|
|
1197
|
+
console.log(`${ui.bold("model:")} ${stat2.model}`);
|
|
1198
|
+
console.log(`${ui.bold("updated_at:")} ${stat2.updatedAt}`);
|
|
1199
|
+
console.log(`${ui.bold("size:")} ${(stat2.sizeBytes / 1024).toFixed(1)} KB`);
|
|
1200
|
+
});
|
|
1379
1201
|
}
|
|
1380
|
-
function
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1202
|
+
async function loadEmbeddings() {
|
|
1203
|
+
try {
|
|
1204
|
+
return await import("@hiveai/embeddings");
|
|
1205
|
+
} catch {
|
|
1206
|
+
ui.error(
|
|
1207
|
+
"Could not load @hiveai/embeddings. Run: npm install -g @hiveai/embeddings (or `pnpm build` in the monorepo)"
|
|
1208
|
+
);
|
|
1209
|
+
process.exit(1);
|
|
1210
|
+
}
|
|
1387
1211
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1212
|
+
|
|
1213
|
+
// src/commands/index-code.ts
|
|
1214
|
+
import path5 from "path";
|
|
1215
|
+
import "commander";
|
|
1216
|
+
import {
|
|
1217
|
+
buildCodeMap as buildCodeMap2,
|
|
1218
|
+
codeMapPath,
|
|
1219
|
+
findProjectRoot as findProjectRoot5,
|
|
1220
|
+
resolveHaivePaths as resolveHaivePaths4,
|
|
1221
|
+
saveCodeMap as saveCodeMap2
|
|
1222
|
+
} from "@hiveai/core";
|
|
1223
|
+
function registerIndexCode(program2) {
|
|
1224
|
+
const idx = program2.command("index").description(
|
|
1225
|
+
"Build local indexes that let AIs look up symbols instead of grepping.\n\n Run once after init, then haive sync refreshes it automatically when source changes."
|
|
1226
|
+
);
|
|
1227
|
+
idx.action(() => idx.help());
|
|
1228
|
+
idx.command("code").description(
|
|
1229
|
+
"Scan source files and write .ai/code-map.json (file \u2192 exports + 1-line description).\n\n Supported languages: TypeScript, JavaScript, Java, Python, Go, Rust, C#, PHP.\n The map is used by:\n \u2022 get_briefing (symbol_locations) \u2014 look up where a class/function lives\n \u2022 code_map MCP tool \u2014 browse exports without grepping\n \u2022 haive briefing --symbols \u2014 look up symbols from the CLI\n\n Run automatically by haive init (autopilot mode) and haive sync (if source changed).\n\n Example:\n haive index code\n haive index code --exclude generated,proto\n"
|
|
1230
|
+
).option("-d, --dir <dir>", "project root").option(
|
|
1231
|
+
"--exclude <csv>",
|
|
1232
|
+
"extra directory names to skip (comma-separated)",
|
|
1233
|
+
""
|
|
1234
|
+
).action(async (opts) => {
|
|
1235
|
+
const root = findProjectRoot5(opts.dir);
|
|
1236
|
+
const paths = resolveHaivePaths4(root);
|
|
1237
|
+
const extraExcludes = (opts.exclude ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1238
|
+
ui.info(`Indexing source files in ${root}\u2026`);
|
|
1239
|
+
const map = await buildCodeMap2(root, {
|
|
1240
|
+
includeUntracked: true,
|
|
1241
|
+
excludeDirs: [
|
|
1242
|
+
"node_modules",
|
|
1243
|
+
"dist",
|
|
1244
|
+
"build",
|
|
1245
|
+
"out",
|
|
1246
|
+
".git",
|
|
1247
|
+
".next",
|
|
1248
|
+
".turbo",
|
|
1249
|
+
".vitest-cache",
|
|
1250
|
+
"coverage",
|
|
1251
|
+
...extraExcludes
|
|
1252
|
+
]
|
|
1253
|
+
});
|
|
1254
|
+
await saveCodeMap2(paths, map);
|
|
1255
|
+
const fileCount = Object.keys(map.files).length;
|
|
1256
|
+
const exportCount = Object.values(map.files).reduce((s, f) => s + f.exports.length, 0);
|
|
1257
|
+
ui.success(
|
|
1258
|
+
`Indexed ${fileCount} file(s) with ${exportCount} export(s) \u2192 ${path5.relative(root, codeMapPath(paths))}`
|
|
1259
|
+
);
|
|
1393
1260
|
});
|
|
1394
|
-
|
|
1395
|
-
for (
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1261
|
+
idx.command("code-search").description(
|
|
1262
|
+
"Build the semantic-search embeddings index for code (powers the code_search MCP tool).\n\n Reads .ai/code-map.json (run `haive index code` first) and embeds each exported\n symbol's metadata (filename + name + kind + description).\n\n Re-runs are incremental: unchanged entries keep their cached vectors, only the\n diff is re-embedded. First run downloads the bge-small-en-v1.5 model (~110MB).\n"
|
|
1263
|
+
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
1264
|
+
const root = findProjectRoot5(opts.dir);
|
|
1265
|
+
const paths = resolveHaivePaths4(root);
|
|
1266
|
+
let mod;
|
|
1267
|
+
try {
|
|
1268
|
+
mod = await import("@hiveai/embeddings");
|
|
1269
|
+
} catch {
|
|
1270
|
+
ui.error(
|
|
1271
|
+
"@hiveai/embeddings is not installed. Install it (`pnpm add @hiveai/embeddings`) or run `haive embeddings install`."
|
|
1272
|
+
);
|
|
1273
|
+
process.exit(1);
|
|
1409
1274
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1275
|
+
ui.info("Loading embedder (first run downloads ~110MB)\u2026");
|
|
1276
|
+
const embedder = await mod.Embedder.create();
|
|
1277
|
+
ui.info(`Embedding code-map symbols\u2026`);
|
|
1278
|
+
try {
|
|
1279
|
+
const { report } = await mod.rebuildCodeIndex(paths, embedder);
|
|
1280
|
+
ui.success(
|
|
1281
|
+
`Code-search index ready: ${report.total} symbols (+${report.added} new, ~${report.updated} updated, =${report.unchanged} cached, -${report.removed} removed)`
|
|
1282
|
+
);
|
|
1283
|
+
} catch (err) {
|
|
1284
|
+
ui.error(err instanceof Error ? err.message : String(err));
|
|
1285
|
+
process.exit(1);
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1412
1288
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1289
|
+
|
|
1290
|
+
// src/commands/init.ts
|
|
1291
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
|
|
1292
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1293
|
+
import path10 from "path";
|
|
1294
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
1295
|
+
import "commander";
|
|
1296
|
+
import {
|
|
1297
|
+
AUTOPILOT_DEFAULTS as AUTOPILOT_DEFAULTS2,
|
|
1298
|
+
buildCodeMap as buildCodeMap3,
|
|
1299
|
+
resolveHaivePaths as resolveHaivePaths6,
|
|
1300
|
+
saveCodeMap as saveCodeMap3,
|
|
1301
|
+
saveConfig as saveConfig2
|
|
1302
|
+
} from "@hiveai/core";
|
|
1303
|
+
|
|
1304
|
+
// src/commands/agent.ts
|
|
1305
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1306
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1307
|
+
import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
|
|
1308
|
+
import os2 from "os";
|
|
1309
|
+
import path7 from "path";
|
|
1310
|
+
import { createInterface } from "readline/promises";
|
|
1311
|
+
import "commander";
|
|
1312
|
+
import { findProjectRoot as findProjectRoot6, resolveHaivePaths as resolveHaivePaths5 } from "@hiveai/core";
|
|
1313
|
+
|
|
1314
|
+
// src/commands/init-mcp-setup.ts
|
|
1315
|
+
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
1316
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1317
|
+
import path6 from "path";
|
|
1318
|
+
import os from "os";
|
|
1319
|
+
var HOME = os.homedir();
|
|
1320
|
+
var HAIVE_MCP_ENTRY = {
|
|
1321
|
+
command: "haive",
|
|
1322
|
+
args: ["mcp", "--stdio"]
|
|
1323
|
+
};
|
|
1324
|
+
function projectMcpEntry(root) {
|
|
1325
|
+
return {
|
|
1326
|
+
command: "haive",
|
|
1327
|
+
args: ["mcp", "--stdio"],
|
|
1328
|
+
env: { HAIVE_PROJECT_ROOT: root }
|
|
1329
|
+
};
|
|
1417
1330
|
}
|
|
1418
|
-
function
|
|
1419
|
-
|
|
1420
|
-
let inter = 0;
|
|
1421
|
-
for (const item of a) if (b.has(item)) inter++;
|
|
1422
|
-
return inter / (a.size + b.size - inter);
|
|
1331
|
+
function cursorMcpPath() {
|
|
1332
|
+
return path6.join(HOME, ".cursor", "mcp.json");
|
|
1423
1333
|
}
|
|
1424
|
-
function
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
if (opts.json) {
|
|
1434
|
-
console.log(JSON.stringify({
|
|
1435
|
-
findings_count: findings.length,
|
|
1436
|
-
findings,
|
|
1437
|
-
fixes_count: report.fixes.length,
|
|
1438
|
-
fixes: report.fixes,
|
|
1439
|
-
fix_mode: opts.fix ? apply ? "apply" : "dry-run" : "off"
|
|
1440
|
-
}, null, 2));
|
|
1441
|
-
process.exitCode = findings.some((f) => f.severity === "error") ? 1 : 0;
|
|
1442
|
-
return;
|
|
1443
|
-
}
|
|
1444
|
-
if (findings.length === 0) {
|
|
1445
|
-
ui.success(`memory lint OK \u2014 ${root}`);
|
|
1446
|
-
return;
|
|
1447
|
-
}
|
|
1448
|
-
console.log(ui.bold(`memory lint (${findings.length} finding${findings.length === 1 ? "" : "s"})`) + `
|
|
1449
|
-
`);
|
|
1450
|
-
if (opts.fix) {
|
|
1451
|
-
const mode = apply ? "apply" : dryRun ? "dry-run" : "dry-run";
|
|
1452
|
-
const verb = apply ? "changed" : "would change";
|
|
1453
|
-
console.log(ui.bold(`fix ${mode}: ${report.fixes.length} file${report.fixes.length === 1 ? "" : "s"} ${verb}`));
|
|
1454
|
-
for (const fix of report.fixes) {
|
|
1455
|
-
console.log(` ${ui.dim(fix.id)} ${fix.actions.join("; ")}`);
|
|
1456
|
-
console.log(ui.dim(` \u2192 ${fix.file}`));
|
|
1457
|
-
}
|
|
1458
|
-
console.log();
|
|
1334
|
+
async function configureCursor() {
|
|
1335
|
+
const mcpPath = cursorMcpPath();
|
|
1336
|
+
const cursorDir = path6.join(HOME, ".cursor");
|
|
1337
|
+
if (!existsSync5(cursorDir)) return { client: "Cursor", status: "not_installed" };
|
|
1338
|
+
let config = {};
|
|
1339
|
+
if (existsSync5(mcpPath)) {
|
|
1340
|
+
try {
|
|
1341
|
+
config = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
1342
|
+
} catch {
|
|
1459
1343
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1344
|
+
}
|
|
1345
|
+
config.mcpServers ??= {};
|
|
1346
|
+
if (config.mcpServers["haive"]) return { client: "Cursor", status: "already_configured" };
|
|
1347
|
+
config.mcpServers["haive"] = HAIVE_MCP_ENTRY;
|
|
1348
|
+
await mkdir2(cursorDir, { recursive: true });
|
|
1349
|
+
await writeFile3(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
1350
|
+
return { client: "Cursor", status: "configured", path: mcpPath };
|
|
1351
|
+
}
|
|
1352
|
+
function vscodeMcpPath() {
|
|
1353
|
+
const candidates = [
|
|
1354
|
+
path6.join(HOME, ".config", "Code", "User", "mcp.json"),
|
|
1355
|
+
// Linux
|
|
1356
|
+
path6.join(HOME, "Library", "Application Support", "Code", "User", "mcp.json"),
|
|
1357
|
+
// macOS
|
|
1358
|
+
path6.join(HOME, "AppData", "Roaming", "Code", "User", "mcp.json"),
|
|
1359
|
+
// Windows
|
|
1360
|
+
path6.join(HOME, ".config", "Code - Insiders", "User", "mcp.json")
|
|
1361
|
+
];
|
|
1362
|
+
for (const c of candidates) {
|
|
1363
|
+
if (existsSync5(path6.dirname(c))) return c;
|
|
1364
|
+
}
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
async function configureVSCode() {
|
|
1368
|
+
const mcpPath = vscodeMcpPath();
|
|
1369
|
+
if (!mcpPath) return { client: "VS Code", status: "not_installed" };
|
|
1370
|
+
let config = {};
|
|
1371
|
+
if (existsSync5(mcpPath)) {
|
|
1372
|
+
try {
|
|
1373
|
+
config = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
1374
|
+
} catch {
|
|
1474
1375
|
}
|
|
1475
|
-
|
|
1476
|
-
}
|
|
1376
|
+
}
|
|
1377
|
+
config.servers ??= {};
|
|
1378
|
+
if (config.servers["haive"]) return { client: "VS Code", status: "already_configured" };
|
|
1379
|
+
config.servers["haive"] = { ...HAIVE_MCP_ENTRY, type: "stdio" };
|
|
1380
|
+
await mkdir2(path6.dirname(mcpPath), { recursive: true });
|
|
1381
|
+
await writeFile3(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
1382
|
+
return { client: "VS Code", status: "configured", path: mcpPath };
|
|
1477
1383
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
const
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1384
|
+
function claudeConfigPath() {
|
|
1385
|
+
const p = path6.join(HOME, ".claude.json");
|
|
1386
|
+
if (existsSync5(p)) return p;
|
|
1387
|
+
const p2 = path6.join(HOME, ".config", "claude", "claude.json");
|
|
1388
|
+
if (existsSync5(path6.dirname(p2))) return p2;
|
|
1389
|
+
return null;
|
|
1390
|
+
}
|
|
1391
|
+
async function configureClaude() {
|
|
1392
|
+
const cfgPath = claudeConfigPath() ?? path6.join(HOME, ".claude.json");
|
|
1393
|
+
if (!existsSync5(cfgPath) && !existsSync5(path6.join(HOME, ".claude"))) {
|
|
1394
|
+
return { client: "Claude Code", status: "not_installed" };
|
|
1395
|
+
}
|
|
1396
|
+
let config = {};
|
|
1397
|
+
if (existsSync5(cfgPath)) {
|
|
1398
|
+
try {
|
|
1399
|
+
config = JSON.parse(await readFile3(cfgPath, "utf8"));
|
|
1400
|
+
} catch {
|
|
1490
1401
|
}
|
|
1491
1402
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1403
|
+
config.mcpServers ??= {};
|
|
1404
|
+
if (config.mcpServers["haive"]) return { client: "Claude Code", status: "already_configured" };
|
|
1405
|
+
config.mcpServers["haive"] = { ...HAIVE_MCP_ENTRY, type: "stdio" };
|
|
1406
|
+
await writeFile3(cfgPath, JSON.stringify(config, null, 2), "utf8");
|
|
1407
|
+
return { client: "Claude Code", status: "configured", path: cfgPath };
|
|
1408
|
+
}
|
|
1409
|
+
function windsurfMcpPath() {
|
|
1410
|
+
const candidates = [
|
|
1411
|
+
path6.join(HOME, ".codeium", "windsurf", "mcp_config.json"),
|
|
1412
|
+
path6.join(HOME, ".windsurf", "mcp.json")
|
|
1413
|
+
];
|
|
1414
|
+
for (const c of candidates) {
|
|
1415
|
+
if (existsSync5(path6.dirname(c))) return c;
|
|
1416
|
+
}
|
|
1417
|
+
return null;
|
|
1418
|
+
}
|
|
1419
|
+
async function configureWindsurf() {
|
|
1420
|
+
const mcpPath = windsurfMcpPath();
|
|
1421
|
+
if (!mcpPath) return { client: "Windsurf", status: "not_installed" };
|
|
1422
|
+
let config = {};
|
|
1423
|
+
if (existsSync5(mcpPath)) {
|
|
1424
|
+
try {
|
|
1425
|
+
config = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
1426
|
+
} catch {
|
|
1501
1427
|
}
|
|
1502
1428
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1429
|
+
config.mcpServers ??= {};
|
|
1430
|
+
if (config.mcpServers["haive"]) return { client: "Windsurf", status: "already_configured" };
|
|
1431
|
+
config.mcpServers["haive"] = HAIVE_MCP_ENTRY;
|
|
1432
|
+
await mkdir2(path6.dirname(mcpPath), { recursive: true });
|
|
1433
|
+
await writeFile3(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
1434
|
+
return { client: "Windsurf", status: "configured", path: mcpPath };
|
|
1435
|
+
}
|
|
1436
|
+
async function autoConfigureMcpClients() {
|
|
1437
|
+
const results = [];
|
|
1438
|
+
const configurators = [configureCursor, configureVSCode, configureClaude, configureWindsurf];
|
|
1439
|
+
for (const fn of configurators) {
|
|
1440
|
+
try {
|
|
1441
|
+
results.push(await fn());
|
|
1442
|
+
} catch (err) {
|
|
1443
|
+
const name = fn.name.replace("configure", "");
|
|
1444
|
+
results.push({ client: name, status: "error", error: String(err) });
|
|
1511
1445
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1446
|
+
}
|
|
1447
|
+
return results;
|
|
1448
|
+
}
|
|
1449
|
+
async function configureProjectMcpClients(root) {
|
|
1450
|
+
const entry = projectMcpEntry(root);
|
|
1451
|
+
const results = [];
|
|
1452
|
+
try {
|
|
1453
|
+
const cursorPath = path6.join(root, ".cursor", "mcp.json");
|
|
1454
|
+
let config = {};
|
|
1455
|
+
if (existsSync5(cursorPath)) {
|
|
1456
|
+
try {
|
|
1457
|
+
config = JSON.parse(await readFile3(cursorPath, "utf8"));
|
|
1458
|
+
} catch {
|
|
1459
|
+
}
|
|
1518
1460
|
}
|
|
1461
|
+
config.mcpServers ??= {};
|
|
1462
|
+
config.mcpServers["haive"] = entry;
|
|
1463
|
+
await mkdir2(path6.dirname(cursorPath), { recursive: true });
|
|
1464
|
+
await writeFile3(cursorPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
1465
|
+
results.push({ client: "Cursor (project)", status: "configured", path: cursorPath });
|
|
1466
|
+
} catch (err) {
|
|
1467
|
+
results.push({ client: "Cursor (project)", status: "error", error: String(err) });
|
|
1519
1468
|
}
|
|
1520
|
-
|
|
1521
|
-
const
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
}
|
|
1469
|
+
try {
|
|
1470
|
+
const vscodePath = path6.join(root, ".vscode", "mcp.json");
|
|
1471
|
+
let config = {};
|
|
1472
|
+
if (existsSync5(vscodePath)) {
|
|
1473
|
+
try {
|
|
1474
|
+
config = JSON.parse(await readFile3(vscodePath, "utf8"));
|
|
1475
|
+
} catch {
|
|
1476
|
+
}
|
|
1527
1477
|
}
|
|
1478
|
+
config.servers ??= {};
|
|
1479
|
+
config.servers["haive"] = { ...entry, type: "stdio" };
|
|
1480
|
+
await mkdir2(path6.dirname(vscodePath), { recursive: true });
|
|
1481
|
+
await writeFile3(vscodePath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
1482
|
+
results.push({ client: "VS Code (workspace)", status: "configured", path: vscodePath });
|
|
1483
|
+
} catch (err) {
|
|
1484
|
+
results.push({ client: "VS Code (workspace)", status: "error", error: String(err) });
|
|
1528
1485
|
}
|
|
1529
|
-
|
|
1530
|
-
const
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
}
|
|
1486
|
+
try {
|
|
1487
|
+
const mcpPath = path6.join(root, ".mcp.json");
|
|
1488
|
+
let config = {};
|
|
1489
|
+
if (existsSync5(mcpPath)) {
|
|
1490
|
+
try {
|
|
1491
|
+
config = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
1492
|
+
} catch {
|
|
1493
|
+
}
|
|
1536
1494
|
}
|
|
1495
|
+
config.mcpServers ??= {};
|
|
1496
|
+
config.mcpServers["haive"] = { ...entry, type: "stdio" };
|
|
1497
|
+
await writeFile3(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
1498
|
+
results.push({ client: "Claude Code (project)", status: "configured", path: mcpPath });
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
results.push({ client: "Claude Code (project)", status: "error", error: String(err) });
|
|
1537
1501
|
}
|
|
1538
|
-
return
|
|
1502
|
+
return results;
|
|
1539
1503
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
requireBriefingFirst: true,
|
|
1562
|
-
requireSessionRecap: true,
|
|
1563
|
-
requireMemoryVerify: true,
|
|
1564
|
-
blockStaleDecisionChanges: true,
|
|
1565
|
-
requireDecisionCoverage: true,
|
|
1566
|
-
cleanupGeneratedArtifacts: true,
|
|
1567
|
-
toolProfile: current.enforcement?.toolProfile ?? "enforcement"
|
|
1504
|
+
|
|
1505
|
+
// src/commands/agent.ts
|
|
1506
|
+
function registerAgent(program2) {
|
|
1507
|
+
const agent = program2.command("agent").description("Detect, configure, and report the best hAIve mode for AI coding agents.");
|
|
1508
|
+
agent.command("detect").description("Detect available AI agents and hAIve MCP/wrapper readiness.").option("-d, --dir <dir>", "project root").option("--json", "emit JSON", false).action(async (opts) => {
|
|
1509
|
+
const detection = await detectAgentMode(opts.dir);
|
|
1510
|
+
printDetection(detection, Boolean(opts.json));
|
|
1511
|
+
});
|
|
1512
|
+
agent.command("status").description("Alias for agent detect.").option("-d, --dir <dir>", "project root").option("--json", "emit JSON", false).action(async (opts) => {
|
|
1513
|
+
const detection = await detectAgentMode(opts.dir);
|
|
1514
|
+
printDetection(detection, Boolean(opts.json));
|
|
1515
|
+
});
|
|
1516
|
+
agent.command("setup").description("Configure hAIve project MCP, optional global MCP clients, and wrapper fallback metadata.").option("-d, --dir <dir>", "project root").option("-y, --yes", "approve user-level/global MCP configuration without prompting", false).option("--no-global", "skip user-level/global MCP configuration").option("--json", "emit JSON", false).action(async (opts) => {
|
|
1517
|
+
const result = await setupAgentMode(opts.dir, {
|
|
1518
|
+
yes: Boolean(opts.yes),
|
|
1519
|
+
global: opts.global !== false && opts.noGlobal !== true,
|
|
1520
|
+
interactive: process.stdin.isTTY
|
|
1521
|
+
});
|
|
1522
|
+
if (opts.json) {
|
|
1523
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1524
|
+
return;
|
|
1568
1525
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
await saveConfig(paths, next);
|
|
1572
|
-
return true;
|
|
1526
|
+
printSetupResult(result);
|
|
1527
|
+
});
|
|
1573
1528
|
}
|
|
1574
|
-
async function
|
|
1575
|
-
const
|
|
1576
|
-
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
`
|
|
1590
|
-
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
updated = original.replace(
|
|
1594
|
-
/^(# Project context[^\n]*\n)/m,
|
|
1595
|
-
`$1
|
|
1596
|
-
> **Current version**: ${status.expectedVersion}
|
|
1597
|
-
`
|
|
1598
|
-
);
|
|
1529
|
+
async function setupAgentMode(dir, opts = {}) {
|
|
1530
|
+
const root = findProjectRoot6(dir);
|
|
1531
|
+
const paths = resolveHaivePaths5(root);
|
|
1532
|
+
const projectResults = await configureProjectMcpClients(root);
|
|
1533
|
+
const detectionBeforeGlobal = await detectAgentMode(root);
|
|
1534
|
+
let globalResults = [];
|
|
1535
|
+
let globalSkippedReason;
|
|
1536
|
+
const shouldConsiderGlobal = opts.global !== false;
|
|
1537
|
+
if (shouldConsiderGlobal) {
|
|
1538
|
+
const approved = opts.yes === true || (opts.interactive ? await confirmGlobalSetup() : false);
|
|
1539
|
+
if (approved) {
|
|
1540
|
+
globalResults = await autoConfigureMcpClients();
|
|
1541
|
+
const codex = await configureCodexIfAvailable(root);
|
|
1542
|
+
if (codex) globalResults.push(codex);
|
|
1543
|
+
} else {
|
|
1544
|
+
globalSkippedReason = opts.interactive ? "User declined user-level/global MCP configuration." : "Non-interactive shell; skipped user-level/global MCP configuration. Re-run `haive agent setup --yes` to apply it.";
|
|
1545
|
+
}
|
|
1546
|
+
} else {
|
|
1547
|
+
globalSkippedReason = "User-level/global MCP configuration disabled.";
|
|
1599
1548
|
}
|
|
1600
|
-
|
|
1601
|
-
await
|
|
1602
|
-
return
|
|
1549
|
+
const detection = await detectAgentMode(root);
|
|
1550
|
+
const modeFile = await writeAgentModeRecord(paths, detection, globalSkippedReason);
|
|
1551
|
+
return {
|
|
1552
|
+
detection,
|
|
1553
|
+
project_results: projectResults,
|
|
1554
|
+
global_results: globalResults,
|
|
1555
|
+
mode_file: modeFile,
|
|
1556
|
+
...globalSkippedReason ? { global_skipped_reason: globalSkippedReason } : {}
|
|
1557
|
+
};
|
|
1603
1558
|
}
|
|
1604
|
-
async function
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
const
|
|
1619
|
-
const
|
|
1620
|
-
const
|
|
1559
|
+
async function detectAgentMode(dir) {
|
|
1560
|
+
const root = findProjectRoot6(dir);
|
|
1561
|
+
const paths = resolveHaivePaths5(root);
|
|
1562
|
+
const projectMcp = [
|
|
1563
|
+
{ client: "Claude Code", path: path7.join(root, ".mcp.json"), present: existsSync6(path7.join(root, ".mcp.json")) },
|
|
1564
|
+
{ client: "Cursor", path: path7.join(root, ".cursor", "mcp.json"), present: existsSync6(path7.join(root, ".cursor", "mcp.json")) },
|
|
1565
|
+
{ client: "VS Code", path: path7.join(root, ".vscode", "mcp.json"), present: existsSync6(path7.join(root, ".vscode", "mcp.json")) }
|
|
1566
|
+
];
|
|
1567
|
+
const installedAgents = [
|
|
1568
|
+
{ agent: "Codex", command: "codex", installed: commandExists("codex"), mcp_configured: codexMcpConfigured() },
|
|
1569
|
+
{ agent: "Claude", command: "claude", installed: commandExists("claude") },
|
|
1570
|
+
{ agent: "Aider", command: "aider", installed: commandExists("aider") },
|
|
1571
|
+
{ agent: "Cursor", command: "cursor", installed: commandExists("cursor") }
|
|
1572
|
+
];
|
|
1573
|
+
const hasProjectMcp = projectMcp.some((item) => item.present);
|
|
1574
|
+
const hasNativeMcp = hasProjectMcp || installedAgents.some((a) => a.mcp_configured);
|
|
1575
|
+
const wrapperAgent = installedAgents.find((a) => a.installed && ["codex", "claude", "aider"].includes(a.command));
|
|
1576
|
+
const recommendedMode = hasNativeMcp ? "mcp" : wrapperAgent ? "wrapped" : "fallback";
|
|
1577
|
+
const recommendedCommand = recommendedMode === "mcp" ? "Restart your AI client, then call get_briefing before editing." : recommendedMode === "wrapped" && wrapperAgent ? `haive run -- ${wrapperAgent.command}` : 'haive briefing --task "..." --files "..."';
|
|
1621
1578
|
return {
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1579
|
+
root,
|
|
1580
|
+
initialized: existsSync6(paths.haiveDir),
|
|
1581
|
+
project_mcp: projectMcp,
|
|
1582
|
+
installed_agents: installedAgents,
|
|
1583
|
+
recommended_mode: recommendedMode,
|
|
1584
|
+
recommended_command: recommendedCommand
|
|
1626
1585
|
};
|
|
1627
1586
|
}
|
|
1628
|
-
async function
|
|
1629
|
-
const
|
|
1630
|
-
|
|
1631
|
-
const
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
".
|
|
1639
|
-
".
|
|
1640
|
-
".
|
|
1641
|
-
|
|
1642
|
-
"coverage"
|
|
1587
|
+
async function writeAgentModeRecord(paths, detection, skippedReason) {
|
|
1588
|
+
const dir = path7.join(paths.runtimeDir, "enforcement");
|
|
1589
|
+
await mkdir3(dir, { recursive: true });
|
|
1590
|
+
const file = path7.join(dir, "agent-mode.json");
|
|
1591
|
+
const record = {
|
|
1592
|
+
selected_mode: detection.recommended_mode,
|
|
1593
|
+
recommended_command: detection.recommended_command,
|
|
1594
|
+
configured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1595
|
+
project_root: detection.root,
|
|
1596
|
+
notes: [
|
|
1597
|
+
"mcp = native hAIve MCP tools are available or project MCP config exists.",
|
|
1598
|
+
"wrapped = use haive run when native MCP is unavailable.",
|
|
1599
|
+
"fallback = use haive briefing/enforce manually.",
|
|
1600
|
+
...skippedReason ? [skippedReason] : []
|
|
1643
1601
|
]
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
}
|
|
1648
|
-
await saveCodeMap2(paths, map);
|
|
1649
|
-
return true;
|
|
1602
|
+
};
|
|
1603
|
+
await writeFile4(file, JSON.stringify(record, null, 2) + "\n", "utf8");
|
|
1604
|
+
return file;
|
|
1650
1605
|
}
|
|
1651
|
-
async function
|
|
1606
|
+
async function confirmGlobalSetup() {
|
|
1607
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1652
1608
|
try {
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
return
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1609
|
+
const answer = await rl.question(
|
|
1610
|
+
"Configure hAIve in user-level AI client configs (Cursor/VS Code/Claude/Codex when detected)? [y/N] "
|
|
1611
|
+
);
|
|
1612
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1613
|
+
} finally {
|
|
1614
|
+
rl.close();
|
|
1659
1615
|
}
|
|
1660
1616
|
}
|
|
1661
|
-
async function
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1617
|
+
async function configureCodexIfAvailable(root) {
|
|
1618
|
+
if (!commandExists("codex")) return { client: "Codex", status: "not_installed" };
|
|
1619
|
+
if (codexMcpConfigured()) return { client: "Codex", status: "already_configured" };
|
|
1620
|
+
const result = spawnSync2("codex", [
|
|
1621
|
+
"mcp",
|
|
1622
|
+
"add",
|
|
1623
|
+
"haive",
|
|
1624
|
+
"--env",
|
|
1625
|
+
`HAIVE_PROJECT_ROOT=${root}`,
|
|
1626
|
+
"--",
|
|
1627
|
+
"haive",
|
|
1628
|
+
"mcp",
|
|
1629
|
+
"--stdio"
|
|
1630
|
+
], { encoding: "utf8" });
|
|
1631
|
+
if (result.status === 0) return { client: "Codex", status: "configured", path: path7.join(os2.homedir(), ".codex", "config.toml") };
|
|
1632
|
+
return { client: "Codex", status: "error", error: result.stderr || result.stdout || "codex mcp add failed" };
|
|
1633
|
+
}
|
|
1634
|
+
function commandExists(command) {
|
|
1635
|
+
const result = spawnSync2(process.platform === "win32" ? "where" : "which", [command], {
|
|
1636
|
+
encoding: "utf8",
|
|
1637
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1638
|
+
});
|
|
1639
|
+
return result.status === 0;
|
|
1640
|
+
}
|
|
1641
|
+
function codexMcpConfigured() {
|
|
1642
|
+
if (!commandExists("codex")) return false;
|
|
1643
|
+
const result = spawnSync2("codex", ["mcp", "get", "haive"], {
|
|
1644
|
+
encoding: "utf8",
|
|
1645
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1646
|
+
});
|
|
1647
|
+
return result.status === 0;
|
|
1648
|
+
}
|
|
1649
|
+
function printDetection(detection, json) {
|
|
1650
|
+
if (json) {
|
|
1651
|
+
console.log(JSON.stringify(detection, null, 2));
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
console.log(ui.bold("hAIve agent status"));
|
|
1655
|
+
console.log(ui.dim(` root: ${detection.root}`));
|
|
1656
|
+
console.log(`${detection.initialized ? ui.green("\u2713") : ui.red("\u2717")} project initialized`);
|
|
1657
|
+
for (const cfg of detection.project_mcp) {
|
|
1658
|
+
console.log(`${cfg.present ? ui.green("\u2713") : ui.yellow("\u2022")} ${cfg.client} project MCP ${ui.dim(path7.relative(detection.root, cfg.path))}`);
|
|
1659
|
+
}
|
|
1660
|
+
for (const agent of detection.installed_agents) {
|
|
1661
|
+
const marker = agent.installed ? ui.green("\u2713") : ui.dim("\u2022");
|
|
1662
|
+
const mcp = agent.mcp_configured === true ? " + hAIve MCP" : "";
|
|
1663
|
+
console.log(`${marker} ${agent.agent} (${agent.command})${mcp}`);
|
|
1664
|
+
}
|
|
1665
|
+
console.log(ui.bold(`Recommended mode: ${detection.recommended_mode}`));
|
|
1666
|
+
console.log(` ${detection.recommended_command}`);
|
|
1667
|
+
}
|
|
1668
|
+
function printSetupResult(result) {
|
|
1669
|
+
for (const item of result.project_results) {
|
|
1670
|
+
if (item.status === "configured") ui.success(`${item.client} project MCP config written (${item.path})`);
|
|
1671
|
+
else if (item.status === "already_configured") ui.info(`${item.client} already configured`);
|
|
1672
|
+
else if (item.status === "error") ui.warn(`${item.client}: ${item.error}`);
|
|
1672
1673
|
}
|
|
1674
|
+
for (const item of result.global_results) {
|
|
1675
|
+
if (item.status === "configured") ui.success(`${item.client} user-level MCP configured${item.path ? ` (${item.path})` : ""}`);
|
|
1676
|
+
else if (item.status === "already_configured") ui.info(`${item.client} user-level MCP already configured`);
|
|
1677
|
+
else if (item.status === "not_installed") ui.info(`${item.client} not detected`);
|
|
1678
|
+
else if (item.status === "error") ui.warn(`${item.client}: ${item.error}`);
|
|
1679
|
+
}
|
|
1680
|
+
if (result.global_skipped_reason) ui.warn(result.global_skipped_reason);
|
|
1681
|
+
ui.success(`Agent mode recorded at ${result.mode_file}`);
|
|
1682
|
+
printDetection(result.detection, false);
|
|
1673
1683
|
}
|
|
1674
1684
|
|
|
1675
1685
|
// src/commands/init-bootstrap.ts
|
|
@@ -3425,6 +3435,27 @@ async function readStdin(maxBytes) {
|
|
|
3425
3435
|
setTimeout(finish, 2e3);
|
|
3426
3436
|
});
|
|
3427
3437
|
}
|
|
3438
|
+
function detectFailure(payload) {
|
|
3439
|
+
const response = payload.tool_response;
|
|
3440
|
+
if (!response) return false;
|
|
3441
|
+
const responseText = typeof response === "string" ? response : JSON.stringify(response);
|
|
3442
|
+
if (payload.tool_name === "Bash") {
|
|
3443
|
+
if (typeof response === "object") {
|
|
3444
|
+
const code = response["exit_code"] ?? response["exitCode"];
|
|
3445
|
+
if (typeof code === "number" && code !== 0) return true;
|
|
3446
|
+
}
|
|
3447
|
+
if (/\b(command not found|No such file or directory|ERR_MODULE_NOT_FOUND|ENOENT|EACCES)\b/.test(responseText)) return true;
|
|
3448
|
+
if (/\berror TS\d+:/i.test(responseText)) return true;
|
|
3449
|
+
}
|
|
3450
|
+
if (["Edit", "Write", "Read"].includes(payload.tool_name ?? "")) {
|
|
3451
|
+
if (typeof response === "object") {
|
|
3452
|
+
const err = response["error"] ?? response["message"];
|
|
3453
|
+
if (typeof err === "string" && err.length > 0) return true;
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
if (/^\s*(Error|FAILED|ENOENT|EACCES|unknown option|Cannot find module)\b/m.test(responseText)) return true;
|
|
3457
|
+
return false;
|
|
3458
|
+
}
|
|
3428
3459
|
function registerObserve(program2) {
|
|
3429
3460
|
program2.command("observe").description(
|
|
3430
3461
|
"Passive-capture endpoint for Claude Code PostToolUse hooks.\n\n Reads a JSON payload on stdin and appends an observation record to\n .ai/.cache/observations.jsonl. Always exits 0; never blocks the agent.\n Wired up automatically by `haive install-hooks claude`."
|
|
@@ -3448,13 +3479,15 @@ function registerObserve(program2) {
|
|
|
3448
3479
|
if (!root) return;
|
|
3449
3480
|
const paths = resolveHaivePaths7(root);
|
|
3450
3481
|
if (!existsSync12(paths.haiveDir)) return;
|
|
3482
|
+
const failureHint = detectFailure(payload);
|
|
3451
3483
|
const observation = {
|
|
3452
3484
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3453
3485
|
session_id: payload.session_id,
|
|
3454
3486
|
cwd: payload.cwd,
|
|
3455
3487
|
tool: payload.tool_name ?? "?",
|
|
3456
3488
|
summary: buildSummary(payload),
|
|
3457
|
-
files: extractFiles(payload)
|
|
3489
|
+
files: extractFiles(payload),
|
|
3490
|
+
...failureHint ? { failure_hint: true } : {}
|
|
3458
3491
|
};
|
|
3459
3492
|
const cacheDir = path13.join(paths.haiveDir, ".cache");
|
|
3460
3493
|
await mkdir8(cacheDir, { recursive: true });
|
|
@@ -4874,7 +4907,12 @@ async function memSessionEnd(input, ctx) {
|
|
|
4874
4907
|
}
|
|
4875
4908
|
const body = buildBody(input);
|
|
4876
4909
|
const topic = recapTopic(input.scope, input.module);
|
|
4877
|
-
const
|
|
4910
|
+
const normalizedFiles = input.files_touched.map((p) => {
|
|
4911
|
+
if (!p || !path82.isAbsolute(p)) return p;
|
|
4912
|
+
const rel = path82.relative(ctx.paths.root, p);
|
|
4913
|
+
return rel.startsWith("..") ? p : rel;
|
|
4914
|
+
});
|
|
4915
|
+
const invalidPaths = normalizedFiles.filter(
|
|
4878
4916
|
(p) => !existsSync17(path82.resolve(ctx.paths.root, p))
|
|
4879
4917
|
);
|
|
4880
4918
|
if (invalidPaths.length > 0) {
|
|
@@ -4893,7 +4931,7 @@ async function memSessionEnd(input, ctx) {
|
|
|
4893
4931
|
revision_count: revisionCount,
|
|
4894
4932
|
anchor: {
|
|
4895
4933
|
...fm.anchor,
|
|
4896
|
-
paths:
|
|
4934
|
+
paths: normalizedFiles.length ? normalizedFiles : fm.anchor.paths
|
|
4897
4935
|
}
|
|
4898
4936
|
};
|
|
4899
4937
|
await writeFile10(
|
|
@@ -4916,7 +4954,7 @@ async function memSessionEnd(input, ctx) {
|
|
|
4916
4954
|
scope: input.scope,
|
|
4917
4955
|
module: input.module,
|
|
4918
4956
|
tags: ["session", "recap"],
|
|
4919
|
-
paths:
|
|
4957
|
+
paths: normalizedFiles,
|
|
4920
4958
|
topic,
|
|
4921
4959
|
status: "validated"
|
|
4922
4960
|
});
|
|
@@ -6525,7 +6563,9 @@ function isDocLikePath(file) {
|
|
|
6525
6563
|
}
|
|
6526
6564
|
function isPackageOrConfigPath(file) {
|
|
6527
6565
|
const lower = file.toLowerCase();
|
|
6528
|
-
|
|
6566
|
+
const base = lower.split("/").pop() ?? lower;
|
|
6567
|
+
return lower.endsWith("package.json") || lower.endsWith("package-lock.json") || lower.endsWith("pnpm-lock.yaml") || lower.endsWith("yarn.lock") || lower.endsWith("bun.lockb") || lower.endsWith(".config.ts") || lower.endsWith(".config.js") || lower.endsWith(".json") || lower.endsWith(".yml") || lower.endsWith(".yaml") || lower.endsWith(".toml") || lower.startsWith(".github/workflows/") || lower.startsWith(".github/") && lower.endsWith(".yml") || // Dotfiles that are pure configuration/tooling — never trigger runtime gotchas
|
|
6568
|
+
base === ".gitignore" || base === ".gitattributes" || base === ".gitmodules" || base === ".editorconfig" || base === ".nvmrc" || base === ".node-version" || base === ".npmrc" || base === ".yarnrc" || base === ".yarnrc.yml" || base === ".dockerignore" || base === "dockerfile" || base.startsWith("dockerfile.") || base === ".env.example" || base === ".env.template" || lower.endsWith(".prettierrc") || lower.endsWith(".eslintrc") || lower.endsWith(".eslintignore") || lower.endsWith(".prettierignore") || lower.endsWith(".stylelintrc") || lower.endsWith(".browserslistrc");
|
|
6529
6569
|
}
|
|
6530
6570
|
function repairCommandForWarning(warning, paths) {
|
|
6531
6571
|
const firstPath = paths[0];
|
|
@@ -7062,7 +7102,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
7062
7102
|
};
|
|
7063
7103
|
}
|
|
7064
7104
|
var SERVER_NAME = "haive";
|
|
7065
|
-
var SERVER_VERSION = "0.9.
|
|
7105
|
+
var SERVER_VERSION = "0.9.26";
|
|
7066
7106
|
function jsonResult(data) {
|
|
7067
7107
|
return {
|
|
7068
7108
|
content: [
|
|
@@ -10226,6 +10266,7 @@ function registerMemoryDigest(program2) {
|
|
|
10226
10266
|
// src/commands/session-end.ts
|
|
10227
10267
|
import { writeFile as writeFile25, mkdir as mkdir15, readFile as readFile16, rm as rm2 } from "fs/promises";
|
|
10228
10268
|
import { existsSync as existsSync53 } from "fs";
|
|
10269
|
+
import { spawn as spawn4 } from "child_process";
|
|
10229
10270
|
import path36 from "path";
|
|
10230
10271
|
import "commander";
|
|
10231
10272
|
import {
|
|
@@ -10238,9 +10279,9 @@ import {
|
|
|
10238
10279
|
} from "@hiveai/core";
|
|
10239
10280
|
async function buildAutoRecap(paths) {
|
|
10240
10281
|
const obsFile = path36.join(paths.haiveDir, ".cache", "observations.jsonl");
|
|
10241
|
-
if (!existsSync53(obsFile)) return
|
|
10282
|
+
if (!existsSync53(obsFile)) return await buildGitAutoRecap(paths);
|
|
10242
10283
|
const raw = await readFile16(obsFile, "utf8").catch(() => "");
|
|
10243
|
-
if (!raw.trim()) return
|
|
10284
|
+
if (!raw.trim()) return await buildGitAutoRecap(paths);
|
|
10244
10285
|
const lines = raw.split("\n").filter(Boolean);
|
|
10245
10286
|
const obs = [];
|
|
10246
10287
|
for (const line of lines) {
|
|
@@ -10249,27 +10290,119 @@ async function buildAutoRecap(paths) {
|
|
|
10249
10290
|
} catch {
|
|
10250
10291
|
}
|
|
10251
10292
|
}
|
|
10252
|
-
if (obs.length === 0) return
|
|
10293
|
+
if (obs.length === 0) return await buildGitAutoRecap(paths);
|
|
10253
10294
|
const toolCounts = /* @__PURE__ */ new Map();
|
|
10254
|
-
const
|
|
10255
|
-
const
|
|
10295
|
+
const writeFiles = /* @__PURE__ */ new Set();
|
|
10296
|
+
const readFiles = /* @__PURE__ */ new Set();
|
|
10256
10297
|
for (const o of obs) {
|
|
10257
10298
|
toolCounts.set(o.tool, (toolCounts.get(o.tool) ?? 0) + 1);
|
|
10258
|
-
|
|
10259
|
-
|
|
10299
|
+
const isWrite = ["Edit", "Write", "NotebookEdit"].includes(o.tool);
|
|
10300
|
+
for (const f of o.files ?? []) {
|
|
10301
|
+
const rel = normalizeAnchorPath(paths.root, f);
|
|
10302
|
+
if (isWrite) writeFiles.add(rel);
|
|
10303
|
+
else readFiles.add(rel);
|
|
10304
|
+
}
|
|
10260
10305
|
}
|
|
10306
|
+
for (const f of writeFiles) readFiles.delete(f);
|
|
10261
10307
|
const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([t, c]) => `${t} \xD7${c}`).join(", ");
|
|
10262
|
-
const
|
|
10263
|
-
const
|
|
10264
|
-
|
|
10265
|
-
|
|
10308
|
+
const recentCommits = await runGit(paths.root, ["log", "--oneline", "-5"]).catch(() => "");
|
|
10309
|
+
const accomplishedParts = [];
|
|
10310
|
+
if (writeFiles.size > 0) {
|
|
10311
|
+
accomplishedParts.push(
|
|
10312
|
+
`**Files modified (${writeFiles.size}):**`,
|
|
10313
|
+
...[...writeFiles].slice(0, 10).map((f) => `- \`${f}\``),
|
|
10314
|
+
...writeFiles.size > 10 ? [`- ...and ${writeFiles.size - 10} more`] : []
|
|
10315
|
+
);
|
|
10316
|
+
}
|
|
10317
|
+
if (recentCommits.trim()) {
|
|
10318
|
+
accomplishedParts.push("", "**Recent commits:**");
|
|
10319
|
+
for (const line of recentCommits.trim().split("\n").slice(0, 5)) {
|
|
10320
|
+
accomplishedParts.push(`- ${line}`);
|
|
10321
|
+
}
|
|
10322
|
+
}
|
|
10323
|
+
if (accomplishedParts.length === 0) {
|
|
10324
|
+
accomplishedParts.push(`${obs.length} tool calls (${topTools}) \u2014 no file writes detected.`);
|
|
10325
|
+
}
|
|
10326
|
+
const failures = obs.filter((o) => o.failure_hint);
|
|
10327
|
+
const discoveriesParts = [];
|
|
10328
|
+
if (failures.length > 0) {
|
|
10329
|
+
discoveriesParts.push(
|
|
10330
|
+
`\u26A0\uFE0F ${failures.length} failure${failures.length === 1 ? "" : "s"} detected \u2014 call \`mem_tried\` for each unresolved one:`,
|
|
10331
|
+
...failures.slice(0, 8).map((o) => `- ${o.summary.slice(0, 180)}`)
|
|
10332
|
+
);
|
|
10333
|
+
}
|
|
10334
|
+
const goal = writeFiles.size > 0 ? `Edited ${writeFiles.size} file${writeFiles.size === 1 ? "" : "s"} across ${obs.length} tool calls` : `Session with ${obs.length} tool calls (${topTools}) \u2014 read-only or no writes captured`;
|
|
10266
10335
|
return {
|
|
10267
10336
|
goal,
|
|
10268
|
-
accomplished,
|
|
10269
|
-
|
|
10337
|
+
accomplished: accomplishedParts.join("\n"),
|
|
10338
|
+
...discoveriesParts.length > 0 ? { discoveries: discoveriesParts.join("\n") } : {},
|
|
10339
|
+
files: [...writeFiles].slice(0, 12),
|
|
10270
10340
|
rawCount: obs.length
|
|
10271
10341
|
};
|
|
10272
10342
|
}
|
|
10343
|
+
async function buildGitAutoRecap(paths) {
|
|
10344
|
+
const changed = await runGit(paths.root, ["diff", "--name-only"]).catch(() => "");
|
|
10345
|
+
const staged = await runGit(paths.root, ["diff", "--cached", "--name-only"]).catch(() => "");
|
|
10346
|
+
const statusRaw = await runGit(paths.root, ["status", "--porcelain"]).catch(() => "");
|
|
10347
|
+
const recentLog = await runGit(paths.root, ["log", "--oneline", "-5"]).catch(() => "");
|
|
10348
|
+
const diffStat = await runGit(paths.root, ["diff", "--stat", "HEAD"]).catch(() => "");
|
|
10349
|
+
const files = Array.from(new Set(
|
|
10350
|
+
[
|
|
10351
|
+
...changed.split("\n"),
|
|
10352
|
+
...staged.split("\n"),
|
|
10353
|
+
...statusRaw.split("\n").map((line) => line.replace(/^[ MADRCU?!]{1,2}\s+/, ""))
|
|
10354
|
+
].map((s) => s.trim()).filter(Boolean).filter((file) => !file.startsWith(".ai/.runtime/") && !file.startsWith(".ai/.cache/"))
|
|
10355
|
+
)).sort();
|
|
10356
|
+
const modified = [];
|
|
10357
|
+
const added = [];
|
|
10358
|
+
const deleted = [];
|
|
10359
|
+
for (const line of statusRaw.split("\n")) {
|
|
10360
|
+
const code = line.substring(0, 2).trim();
|
|
10361
|
+
const file = line.substring(3).trim().replace(/".+"/g, (m) => m.slice(1, -1));
|
|
10362
|
+
if (!file || file.startsWith(".ai/.runtime/") || file.startsWith(".ai/.cache/")) continue;
|
|
10363
|
+
if (code === "D" || code === "DD") deleted.push(file);
|
|
10364
|
+
else if (code === "A" || code === "??") added.push(file);
|
|
10365
|
+
else if (file) modified.push(file);
|
|
10366
|
+
}
|
|
10367
|
+
const accomplishedParts = [];
|
|
10368
|
+
if (modified.length > 0) {
|
|
10369
|
+
accomplishedParts.push(`**Modified (${modified.length}):**`);
|
|
10370
|
+
for (const f of modified.slice(0, 8)) accomplishedParts.push(`- \`${f}\``);
|
|
10371
|
+
if (modified.length > 8) accomplishedParts.push(`- ...and ${modified.length - 8} more`);
|
|
10372
|
+
}
|
|
10373
|
+
if (added.length > 0) {
|
|
10374
|
+
accomplishedParts.push(`
|
|
10375
|
+
**Added (${added.length}):**`);
|
|
10376
|
+
for (const f of added.slice(0, 5)) accomplishedParts.push(`- \`${f}\``);
|
|
10377
|
+
if (added.length > 5) accomplishedParts.push(`- ...and ${added.length - 5} more`);
|
|
10378
|
+
}
|
|
10379
|
+
if (deleted.length > 0) {
|
|
10380
|
+
accomplishedParts.push(`
|
|
10381
|
+
**Deleted (${deleted.length}):**`);
|
|
10382
|
+
for (const f of deleted.slice(0, 5)) accomplishedParts.push(`- \`${f}\``);
|
|
10383
|
+
}
|
|
10384
|
+
if (recentLog.trim()) {
|
|
10385
|
+
accomplishedParts.push("\n**Recent commits:**");
|
|
10386
|
+
for (const line of recentLog.trim().split("\n").slice(0, 5)) {
|
|
10387
|
+
accomplishedParts.push(`- ${line}`);
|
|
10388
|
+
}
|
|
10389
|
+
}
|
|
10390
|
+
if (accomplishedParts.length === 0 && files.length === 0) return null;
|
|
10391
|
+
if (accomplishedParts.length === 0) {
|
|
10392
|
+
accomplishedParts.push(...files.slice(0, 12).map((f) => `- \`${f}\``));
|
|
10393
|
+
if (files.length > 12) accomplishedParts.push(`- ...and ${files.length - 12} more`);
|
|
10394
|
+
}
|
|
10395
|
+
return {
|
|
10396
|
+
goal: files.length > 0 ? `Session with ${files.length} changed file${files.length === 1 ? "" : "s"}` : `Session with recent commits (no uncommitted changes)`,
|
|
10397
|
+
accomplished: accomplishedParts.join("\n"),
|
|
10398
|
+
discoveries: diffStat.trim() ? `Git diff stat:
|
|
10399
|
+
\`\`\`
|
|
10400
|
+
${diffStat.trim()}
|
|
10401
|
+
\`\`\`` : void 0,
|
|
10402
|
+
files: files.slice(0, 12),
|
|
10403
|
+
rawCount: files.length
|
|
10404
|
+
};
|
|
10405
|
+
}
|
|
10273
10406
|
function buildRecapBody(opts) {
|
|
10274
10407
|
const lines = [];
|
|
10275
10408
|
lines.push(`## Goal
|
|
@@ -10295,6 +10428,24 @@ ${opts.next}`);
|
|
|
10295
10428
|
}
|
|
10296
10429
|
return lines.join("\n");
|
|
10297
10430
|
}
|
|
10431
|
+
function runGit(cwd, args) {
|
|
10432
|
+
return new Promise((resolve, reject) => {
|
|
10433
|
+
const proc = spawn4("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
10434
|
+
let stdout = "";
|
|
10435
|
+
let stderr = "";
|
|
10436
|
+
proc.stdout.on("data", (chunk) => {
|
|
10437
|
+
stdout += chunk.toString();
|
|
10438
|
+
});
|
|
10439
|
+
proc.stderr.on("data", (chunk) => {
|
|
10440
|
+
stderr += chunk.toString();
|
|
10441
|
+
});
|
|
10442
|
+
proc.on("error", reject);
|
|
10443
|
+
proc.on("close", (code) => {
|
|
10444
|
+
if (code === 0) resolve(stdout);
|
|
10445
|
+
else reject(new Error(stderr || `git ${args.join(" ")} exited with code ${code}`));
|
|
10446
|
+
});
|
|
10447
|
+
});
|
|
10448
|
+
}
|
|
10298
10449
|
function recapTopic2(scope, module) {
|
|
10299
10450
|
return module ? `session-recap-${scope}-${module}` : `session-recap-${scope}`;
|
|
10300
10451
|
}
|
|
@@ -10333,6 +10484,7 @@ function registerSessionEnd(session2) {
|
|
|
10333
10484
|
if (!synth) return;
|
|
10334
10485
|
goal = goal ?? synth.goal;
|
|
10335
10486
|
accomplished = accomplished ?? synth.accomplished;
|
|
10487
|
+
opts.discoveries = opts.discoveries ?? synth.discoveries;
|
|
10336
10488
|
if (!resolvedFiles && synth.files.length) resolvedFiles = synth.files.join(",");
|
|
10337
10489
|
}
|
|
10338
10490
|
if (!goal || !accomplished) {
|
|
@@ -10350,7 +10502,7 @@ function registerSessionEnd(session2) {
|
|
|
10350
10502
|
next: opts.next
|
|
10351
10503
|
});
|
|
10352
10504
|
const topic = recapTopic2(scope, opts.module);
|
|
10353
|
-
const filesTouched = parseCsv5(resolvedFiles);
|
|
10505
|
+
const filesTouched = parseCsv5(resolvedFiles).map((p) => normalizeAnchorPath(root, p));
|
|
10354
10506
|
const missingPaths = filesTouched.filter((p) => !existsSync53(path36.resolve(root, p)));
|
|
10355
10507
|
if (missingPaths.length > 0 && !opts.quiet) {
|
|
10356
10508
|
ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
|
|
@@ -10415,6 +10567,13 @@ function parseCsv5(value) {
|
|
|
10415
10567
|
if (!value) return [];
|
|
10416
10568
|
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
10417
10569
|
}
|
|
10570
|
+
function normalizeAnchorPath(root, filePath) {
|
|
10571
|
+
if (!filePath) return filePath;
|
|
10572
|
+
if (!path36.isAbsolute(filePath)) return filePath;
|
|
10573
|
+
const rel = path36.relative(root, filePath);
|
|
10574
|
+
if (rel.startsWith("..")) return filePath;
|
|
10575
|
+
return rel;
|
|
10576
|
+
}
|
|
10418
10577
|
|
|
10419
10578
|
// src/commands/snapshot.ts
|
|
10420
10579
|
import { existsSync as existsSync54 } from "fs";
|
|
@@ -11603,7 +11762,7 @@ function parseDays(input) {
|
|
|
11603
11762
|
}
|
|
11604
11763
|
|
|
11605
11764
|
// src/commands/doctor.ts
|
|
11606
|
-
import { existsSync as existsSync60 } from "fs";
|
|
11765
|
+
import { existsSync as existsSync60, statSync } from "fs";
|
|
11607
11766
|
import { readFile as readFile19, stat } from "fs/promises";
|
|
11608
11767
|
import path44 from "path";
|
|
11609
11768
|
import { execFileSync, execSync as execSync3 } from "child_process";
|
|
@@ -11707,6 +11866,22 @@ function registerDoctor(program2) {
|
|
|
11707
11866
|
fix: "haive memory pending # list them\nhaive memory auto-promote # promote those with high read_count"
|
|
11708
11867
|
});
|
|
11709
11868
|
}
|
|
11869
|
+
const OLD_DRAFT_DAYS = 30;
|
|
11870
|
+
const oldDrafts = memories.filter((m) => {
|
|
11871
|
+
if (m.memory.frontmatter.status !== "draft") return false;
|
|
11872
|
+
const age = (now - Date.parse(m.memory.frontmatter.created_at)) / MS_PER_DAY3;
|
|
11873
|
+
return age > OLD_DRAFT_DAYS;
|
|
11874
|
+
});
|
|
11875
|
+
if (oldDrafts.length > 0) {
|
|
11876
|
+
const ids = oldDrafts.slice(0, 4).map((m) => m.memory.frontmatter.id).join(", ");
|
|
11877
|
+
const more = oldDrafts.length > 4 ? ` (+${oldDrafts.length - 4} more)` : "";
|
|
11878
|
+
findings.push({
|
|
11879
|
+
severity: "warn",
|
|
11880
|
+
code: "stale-draft-memories",
|
|
11881
|
+
message: `${oldDrafts.length} draft memor${oldDrafts.length === 1 ? "y has" : "ies have"} been in draft status for 30+ days: ${ids}${more}`,
|
|
11882
|
+
fix: "haive memory approve <id> # activate\nhaive memory rm <id> # or delete if obsolete"
|
|
11883
|
+
});
|
|
11884
|
+
}
|
|
11710
11885
|
const anchorless = memories.filter(
|
|
11711
11886
|
(m) => m.memory.frontmatter.anchor.paths.length === 0 && m.memory.frontmatter.anchor.symbols.length === 0 && m.memory.frontmatter.type !== "session_recap" && m.memory.frontmatter.type !== "glossary" && m.memory.frontmatter.type !== "skill"
|
|
11712
11887
|
);
|
|
@@ -11832,14 +12007,14 @@ function registerDoctor(program2) {
|
|
|
11832
12007
|
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
|
|
11833
12008
|
});
|
|
11834
12009
|
}
|
|
11835
|
-
findings.push(...await collectInstallFindings(root, "0.9.
|
|
12010
|
+
findings.push(...await collectInstallFindings(root, "0.9.26"));
|
|
11836
12011
|
try {
|
|
11837
12012
|
const legacyRaw = execSync3("haive-mcp --version", {
|
|
11838
12013
|
encoding: "utf8",
|
|
11839
12014
|
timeout: 3e3,
|
|
11840
12015
|
stdio: ["ignore", "pipe", "ignore"]
|
|
11841
12016
|
}).trim();
|
|
11842
|
-
const cliVersion = "0.9.
|
|
12017
|
+
const cliVersion = "0.9.26";
|
|
11843
12018
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
11844
12019
|
findings.push({
|
|
11845
12020
|
severity: "warn",
|
|
@@ -11996,13 +12171,22 @@ async function collectHarnessCoverageFindings(codeMap, memories) {
|
|
|
11996
12171
|
}
|
|
11997
12172
|
const covered = coveredFiles.size;
|
|
11998
12173
|
const pct = Math.round(covered / total * 100);
|
|
12174
|
+
const uncovered = codeFiles.filter((f) => !coveredFiles.has(f)).sort((a, b) => {
|
|
12175
|
+
const depthA = a.split("/").length;
|
|
12176
|
+
const depthB = b.split("/").length;
|
|
12177
|
+
if (depthA !== depthB) return depthA - depthB;
|
|
12178
|
+
return a.localeCompare(b);
|
|
12179
|
+
}).slice(0, 5);
|
|
12180
|
+
const coverageDesc = pct < 10 && total > 10 ? "Low coverage \u2014 add memory anchors on key modules to improve harness enforcement." : pct < 50 ? "Partial coverage \u2014 useful but not yet broad enough to call the harness mature." : pct < 80 ? "Good coverage \u2014 critical modules are increasingly protected." : "Good harness coverage.";
|
|
12181
|
+
const uncoveredHint = uncovered.length > 0 ? `
|
|
12182
|
+
Top uncovered: ${uncovered.map((f) => `\`${f}\``).join(", ")}` : "";
|
|
11999
12183
|
const findings = [];
|
|
12000
12184
|
findings.push({
|
|
12001
12185
|
severity: "info",
|
|
12002
12186
|
code: "harness-coverage",
|
|
12003
12187
|
coverage_percent: pct,
|
|
12004
|
-
message: `${covered}/${total} code-map files have validated memory anchors (${pct}%). ` +
|
|
12005
|
-
fix: pct < 50 && total > 10 ?
|
|
12188
|
+
message: `${covered}/${total} code-map files have validated memory anchors (${pct}%). ` + coverageDesc + uncoveredHint,
|
|
12189
|
+
fix: pct < 50 && total > 10 ? `haive memory add --type gotcha|convention|architecture --paths <key-file> --scope team` : void 0,
|
|
12006
12190
|
section: "Harness coverage"
|
|
12007
12191
|
});
|
|
12008
12192
|
return findings;
|
|
@@ -12234,7 +12418,13 @@ function extractAbsoluteHaiveBins(text) {
|
|
|
12234
12418
|
const re = /(["'\s])((?:\/[^"'\s]+)*\/haive)\b/g;
|
|
12235
12419
|
let match;
|
|
12236
12420
|
while (match = re.exec(text)) {
|
|
12237
|
-
|
|
12421
|
+
const p = match[2];
|
|
12422
|
+
if (!p) continue;
|
|
12423
|
+
try {
|
|
12424
|
+
if (statSync(p).isDirectory()) continue;
|
|
12425
|
+
} catch {
|
|
12426
|
+
}
|
|
12427
|
+
out.add(p);
|
|
12238
12428
|
}
|
|
12239
12429
|
return [...out].sort();
|
|
12240
12430
|
}
|
|
@@ -12357,7 +12547,7 @@ function truncate3(text, max) {
|
|
|
12357
12547
|
}
|
|
12358
12548
|
|
|
12359
12549
|
// src/commands/precommit.ts
|
|
12360
|
-
import { spawn as
|
|
12550
|
+
import { spawn as spawn5 } from "child_process";
|
|
12361
12551
|
import "commander";
|
|
12362
12552
|
import {
|
|
12363
12553
|
findProjectRoot as findProjectRoot43,
|
|
@@ -12484,7 +12674,7 @@ function printWarnings(title, warnings, tone) {
|
|
|
12484
12674
|
}
|
|
12485
12675
|
function runCommand3(cmd, args, cwd) {
|
|
12486
12676
|
return new Promise((resolve, reject) => {
|
|
12487
|
-
const proc =
|
|
12677
|
+
const proc = spawn5(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
12488
12678
|
let stdout = "";
|
|
12489
12679
|
let stderr = "";
|
|
12490
12680
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -12734,8 +12924,8 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
12734
12924
|
}
|
|
12735
12925
|
|
|
12736
12926
|
// src/commands/enforce.ts
|
|
12737
|
-
import { execFileSync as execFileSync2, spawn as
|
|
12738
|
-
import { existsSync as existsSync67 } from "fs";
|
|
12927
|
+
import { execFileSync as execFileSync2, spawn as spawn6 } from "child_process";
|
|
12928
|
+
import { existsSync as existsSync67, statSync as statSync2 } from "fs";
|
|
12739
12929
|
import { chmod as chmod2, mkdir as mkdir19, readFile as readFile20, readdir as readdir6, rm as rm3, writeFile as writeFile31 } from "fs/promises";
|
|
12740
12930
|
import path49 from "path";
|
|
12741
12931
|
import "commander";
|
|
@@ -12838,6 +13028,7 @@ function registerEnforce(program2) {
|
|
|
12838
13028
|
await mkdir19(paths.runtimeDir, { recursive: true });
|
|
12839
13029
|
const sessionId = opts.sessionId ?? payload.session_id;
|
|
12840
13030
|
const task = opts.task ?? payload.prompt ?? "Start an AI coding session in this hAIve-initialized project.";
|
|
13031
|
+
await applyLightweightRepairs(root, paths);
|
|
12841
13032
|
const budget = resolveBriefingBudget3("quick", {
|
|
12842
13033
|
max_tokens: 2500,
|
|
12843
13034
|
max_memories: 5,
|
|
@@ -12899,7 +13090,27 @@ ${briefing.project_context.content.slice(0, 1800)}`);
|
|
|
12899
13090
|
if (!existsSync67(paths.haiveDir)) return;
|
|
12900
13091
|
if (!isWriteLikeTool(payload)) return;
|
|
12901
13092
|
const ok = await hasRecentBriefingMarker(paths, payload.session_id);
|
|
12902
|
-
if (ok)
|
|
13093
|
+
if (ok) {
|
|
13094
|
+
const targetFiles = extractToolPaths(payload, root);
|
|
13095
|
+
if (targetFiles.length === 0) return;
|
|
13096
|
+
const missing = await missingRequiredMemoriesForFiles(paths, targetFiles, payload.session_id);
|
|
13097
|
+
if (missing.length === 0) return;
|
|
13098
|
+
const ids = missing.slice(0, 6).map((memory2) => memory2.memory.frontmatter.id);
|
|
13099
|
+
console.error(
|
|
13100
|
+
[
|
|
13101
|
+
"hAIve enforcement blocked this action.",
|
|
13102
|
+
`Tool: ${payload.tool_name ?? "write tool"}`,
|
|
13103
|
+
`Files: ${targetFiles.slice(0, 6).join(", ")}`,
|
|
13104
|
+
"",
|
|
13105
|
+
"These files have required hAIve context that was not in the current briefing:",
|
|
13106
|
+
...ids.map((id) => ` - ${id}`),
|
|
13107
|
+
"",
|
|
13108
|
+
"Load the targeted briefing before editing:",
|
|
13109
|
+
` ${briefingCommandForFiles(targetFiles)}`
|
|
13110
|
+
].join("\n")
|
|
13111
|
+
);
|
|
13112
|
+
process.exit(2);
|
|
13113
|
+
}
|
|
12903
13114
|
const tool = payload.tool_name ?? "write tool";
|
|
12904
13115
|
console.error(
|
|
12905
13116
|
[
|
|
@@ -12940,7 +13151,7 @@ async function runWithEnforcement(command, args, opts) {
|
|
|
12940
13151
|
}
|
|
12941
13152
|
ui.info(`hAIve briefing marker created for wrapped agent session: ${sessionId}`);
|
|
12942
13153
|
ui.info(`Briefing written to ${path49.relative(root, briefingFile)} and exported as HAIVE_BRIEFING_FILE`);
|
|
12943
|
-
const child =
|
|
13154
|
+
const child = spawn6(command, args, {
|
|
12944
13155
|
cwd: root,
|
|
12945
13156
|
stdio: "inherit",
|
|
12946
13157
|
env: {
|
|
@@ -12962,6 +13173,7 @@ async function runWithEnforcement(command, args, opts) {
|
|
|
12962
13173
|
});
|
|
12963
13174
|
}
|
|
12964
13175
|
async function writeWrapperBriefing(paths, sessionId, task) {
|
|
13176
|
+
await applyLightweightRepairs(paths.root, paths);
|
|
12965
13177
|
const budget = resolveBriefingBudget3("quick", {
|
|
12966
13178
|
max_tokens: 2500,
|
|
12967
13179
|
max_memories: 5,
|
|
@@ -13016,6 +13228,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
13016
13228
|
const paths = resolveHaivePaths44(root);
|
|
13017
13229
|
const initialized = existsSync67(paths.haiveDir);
|
|
13018
13230
|
const config = initialized ? await loadConfig10(paths) : {};
|
|
13231
|
+
if (initialized) await applyLightweightRepairs(root, paths);
|
|
13019
13232
|
const mode = config.enforcement?.mode ?? "strict";
|
|
13020
13233
|
const findings = [];
|
|
13021
13234
|
if (!initialized) {
|
|
@@ -13044,7 +13257,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
13044
13257
|
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
13045
13258
|
});
|
|
13046
13259
|
}
|
|
13047
|
-
findings.push(...await inspectIntegrationVersions(root, "0.9.
|
|
13260
|
+
findings.push(...await inspectIntegrationVersions(root, "0.9.26"));
|
|
13048
13261
|
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
13049
13262
|
const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
|
|
13050
13263
|
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
|
|
@@ -13196,6 +13409,9 @@ async function verifyDecisionCoverage(paths, stage, sessionId) {
|
|
|
13196
13409
|
code: "decision-coverage-missing",
|
|
13197
13410
|
message: `${missing.length}/${relevant.length} relevant anchored decisions/policies were not present in the latest briefing: ${missing.slice(0, 6).map((m) => m.frontmatter.id).join(", ")}`,
|
|
13198
13411
|
fix: `Run \`haive briefing --files "${changedFiles.slice(0, 10).join(",")}" --task "..."\` before committing.`,
|
|
13412
|
+
reason: "Changed files overlap validated anchored policy memories that were not recorded in the latest briefing marker.",
|
|
13413
|
+
affected_files: changedFiles.slice(0, 10),
|
|
13414
|
+
memory_ids: missing.slice(0, 10).map((m) => m.frontmatter.id),
|
|
13199
13415
|
impact: Math.min(35, 10 + missing.length * 5)
|
|
13200
13416
|
}];
|
|
13201
13417
|
}
|
|
@@ -13344,7 +13560,13 @@ function extractAbsoluteHaiveBins2(text) {
|
|
|
13344
13560
|
const re = /(["'\s])((?:\/[^"'\s]+)*\/haive)\b/g;
|
|
13345
13561
|
let match;
|
|
13346
13562
|
while (match = re.exec(text)) {
|
|
13347
|
-
|
|
13563
|
+
const p = match[2];
|
|
13564
|
+
if (!p) continue;
|
|
13565
|
+
try {
|
|
13566
|
+
if (statSync2(p).isDirectory()) continue;
|
|
13567
|
+
} catch {
|
|
13568
|
+
}
|
|
13569
|
+
out.add(p);
|
|
13348
13570
|
}
|
|
13349
13571
|
return [...out].sort();
|
|
13350
13572
|
}
|
|
@@ -13501,8 +13723,21 @@ function printFindingGroup(title, findings, tone) {
|
|
|
13501
13723
|
function printFinding(finding, explain = false) {
|
|
13502
13724
|
const marker = finding.severity === "error" ? ui.red("\u2717") : finding.severity === "warn" ? ui.yellow("\u26A0") : finding.severity === "ok" ? ui.green("\u2713") : ui.dim("\u2022");
|
|
13503
13725
|
console.log(`${marker} ${finding.code}: ${finding.message}`);
|
|
13726
|
+
if (explain && finding.reason) console.log(ui.dim(` why: ${finding.reason}`));
|
|
13727
|
+
if (explain && finding.affected_files?.length) console.log(ui.dim(` files: ${finding.affected_files.join(", ")}`));
|
|
13728
|
+
if (explain && finding.memory_ids?.length) console.log(ui.dim(` memories: ${finding.memory_ids.join(", ")}`));
|
|
13504
13729
|
if (finding.fix) console.log(ui.dim(`${explain ? " repair: " : " fix: "}${finding.fix}`));
|
|
13505
13730
|
}
|
|
13731
|
+
async function applyLightweightRepairs(root, paths) {
|
|
13732
|
+
await applyAutopilotRepairs(root, paths, {
|
|
13733
|
+
applyConfig: false,
|
|
13734
|
+
applyContext: true,
|
|
13735
|
+
applyCorpus: true,
|
|
13736
|
+
applyCodeMap: false,
|
|
13737
|
+
applyCodeSearch: true
|
|
13738
|
+
}).catch(() => {
|
|
13739
|
+
});
|
|
13740
|
+
}
|
|
13506
13741
|
async function readHookPayload() {
|
|
13507
13742
|
const raw = await readStdin2(MAX_STDIN_BYTES2);
|
|
13508
13743
|
if (!raw.trim()) return {};
|
|
@@ -13526,6 +13761,51 @@ function isWriteLikeTool(payload) {
|
|
|
13526
13761
|
const command = String(payload.tool_input?.["command"] ?? "");
|
|
13527
13762
|
return /\b(rm|mv|cp|mkdir|touch|tee|sed|perl|python|node|npm|pnpm|yarn|git)\b/.test(command) || />{1,2}/.test(command);
|
|
13528
13763
|
}
|
|
13764
|
+
function extractToolPaths(payload, root) {
|
|
13765
|
+
const input = payload.tool_input ?? {};
|
|
13766
|
+
const values = [
|
|
13767
|
+
input["file_path"],
|
|
13768
|
+
input["path"],
|
|
13769
|
+
input["notebook_path"]
|
|
13770
|
+
];
|
|
13771
|
+
if (Array.isArray(input["file_paths"])) values.push(...input["file_paths"]);
|
|
13772
|
+
if (Array.isArray(input["files"])) values.push(...input["files"]);
|
|
13773
|
+
if (payload.tool_name === "MultiEdit" && Array.isArray(input["edits"])) {
|
|
13774
|
+
for (const edit of input["edits"]) {
|
|
13775
|
+
if (edit && typeof edit === "object" && "file_path" in edit) {
|
|
13776
|
+
values.push(edit.file_path);
|
|
13777
|
+
}
|
|
13778
|
+
}
|
|
13779
|
+
}
|
|
13780
|
+
const out = /* @__PURE__ */ new Set();
|
|
13781
|
+
for (const value of values) {
|
|
13782
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
13783
|
+
out.add(normalizeToolPath(value, root));
|
|
13784
|
+
}
|
|
13785
|
+
return [...out].filter(Boolean).sort();
|
|
13786
|
+
}
|
|
13787
|
+
function normalizeToolPath(file, root) {
|
|
13788
|
+
const normalized = file.replace(/\\/g, "/");
|
|
13789
|
+
if (!path49.isAbsolute(normalized)) return normalized.replace(/^\.\//, "");
|
|
13790
|
+
return path49.relative(root, normalized).replace(/\\/g, "/");
|
|
13791
|
+
}
|
|
13792
|
+
async function missingRequiredMemoriesForFiles(paths, files, sessionId) {
|
|
13793
|
+
if (!existsSync67(paths.memoriesDir)) return [];
|
|
13794
|
+
const marker = await readRecentBriefingMarker(paths, sessionId);
|
|
13795
|
+
const consulted = new Set(marker?.memory_ids ?? []);
|
|
13796
|
+
const policyTypes = /* @__PURE__ */ new Set(["decision", "gotcha", "architecture", "convention", "attempt"]);
|
|
13797
|
+
const all = await loadMemoriesFromDir36(paths.memoriesDir);
|
|
13798
|
+
return all.filter(({ memory: memory2 }) => {
|
|
13799
|
+
const fm = memory2.frontmatter;
|
|
13800
|
+
if (!policyTypes.has(fm.type)) return false;
|
|
13801
|
+
if (fm.status !== "validated") return false;
|
|
13802
|
+
if (consulted.has(fm.id)) return false;
|
|
13803
|
+
return memoryMatchesAnchorPaths6(memory2, files);
|
|
13804
|
+
}).map(({ memory: memory2, filePath }) => ({ memory: memory2, filePath }));
|
|
13805
|
+
}
|
|
13806
|
+
function briefingCommandForFiles(files) {
|
|
13807
|
+
return `haive briefing --files "${files.slice(0, 10).join(",")}" --task "edit ${files.slice(0, 3).join(", ")}"`;
|
|
13808
|
+
}
|
|
13529
13809
|
async function readStdin2(maxBytes) {
|
|
13530
13810
|
if (process.stdin.isTTY) return "";
|
|
13531
13811
|
return await new Promise((resolve) => {
|
|
@@ -13553,7 +13833,7 @@ async function readStdin2(maxBytes) {
|
|
|
13553
13833
|
}
|
|
13554
13834
|
function runCommand4(cmd, args, cwd) {
|
|
13555
13835
|
return new Promise((resolve, reject) => {
|
|
13556
|
-
const proc =
|
|
13836
|
+
const proc = spawn6(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
13557
13837
|
let stdout = "";
|
|
13558
13838
|
let stderr = "";
|
|
13559
13839
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -13584,7 +13864,7 @@ function registerRun(program2) {
|
|
|
13584
13864
|
|
|
13585
13865
|
// src/index.ts
|
|
13586
13866
|
var program = new Command51();
|
|
13587
|
-
program.name("haive").description("hAIve \u2014 the memory and enforcement layer of your agent harness").version("0.9.
|
|
13867
|
+
program.name("haive").description("hAIve \u2014 the memory and enforcement layer of your agent harness").version("0.9.26").option("--advanced", "show maintenance and experimental commands in help");
|
|
13588
13868
|
registerInit(program);
|
|
13589
13869
|
registerWelcome(program);
|
|
13590
13870
|
registerResolveProject(program);
|