@hiveai/cli 0.9.23 → 0.9.25
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 +1567 -1358
- 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,1448 +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");
|
|
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 ")}`));
|
|
471
537
|
}
|
|
472
|
-
|
|
538
|
+
console.log(ui.dim(` \u2192 ${f.file}`));
|
|
473
539
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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("");
|
|
540
|
+
process.exitCode = findings.some((x) => x.severity === "error") ? 1 : 0;
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
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
|
+
});
|
|
518
555
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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."
|
|
523
565
|
});
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
566
|
+
}
|
|
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"}.`
|
|
531
575
|
});
|
|
532
576
|
}
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
printRadar(radar, out, radarForced ? "forced" : "low-memory-signal");
|
|
540
|
-
}
|
|
577
|
+
const indexed = await refreshMemorySemanticIndex(paths);
|
|
578
|
+
if (indexed) {
|
|
579
|
+
repairs.push({
|
|
580
|
+
code: "memory-embeddings-index",
|
|
581
|
+
message: "Refreshed memory embeddings index."
|
|
582
|
+
});
|
|
541
583
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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("");
|
|
571
|
-
}
|
|
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
|
+
});
|
|
572
592
|
}
|
|
573
|
-
}
|
|
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
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return repairs;
|
|
574
604
|
}
|
|
575
|
-
function
|
|
576
|
-
const
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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"
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
if (JSON.stringify(current) === JSON.stringify(next)) return false;
|
|
636
|
+
await saveConfig(paths, next);
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
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
|
+
);
|
|
581
656
|
}
|
|
582
|
-
|
|
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
|
+
);
|
|
664
|
+
}
|
|
665
|
+
if (updated === original) return false;
|
|
666
|
+
await writeFile2(paths.projectContext, updated, "utf8");
|
|
667
|
+
return true;
|
|
583
668
|
}
|
|
584
|
-
function
|
|
585
|
-
if (
|
|
586
|
-
|
|
587
|
-
|
|
669
|
+
async function projectContextVersionStatus(root, paths) {
|
|
670
|
+
if (!existsSync2(paths.projectContext)) {
|
|
671
|
+
return { mismatch: false, canSync: false };
|
|
672
|
+
}
|
|
673
|
+
const packagePath = path2.join(root, "package.json");
|
|
674
|
+
if (!existsSync2(packagePath)) {
|
|
675
|
+
return { mismatch: false, canSync: false };
|
|
676
|
+
}
|
|
677
|
+
const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
|
|
678
|
+
const expectedVersion = packageJson.version;
|
|
679
|
+
if (!expectedVersion) {
|
|
680
|
+
return { mismatch: false, canSync: false };
|
|
681
|
+
}
|
|
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
|
+
};
|
|
588
692
|
}
|
|
589
|
-
function
|
|
590
|
-
|
|
591
|
-
|
|
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;
|
|
712
|
+
}
|
|
713
|
+
await saveCodeMap(paths, map);
|
|
714
|
+
return true;
|
|
592
715
|
}
|
|
593
|
-
function
|
|
594
|
-
|
|
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;
|
|
724
|
+
}
|
|
595
725
|
}
|
|
596
|
-
|
|
597
|
-
// src/commands/tui.ts
|
|
598
|
-
import "commander";
|
|
599
|
-
import { findProjectRoot as findProjectRoot2 } from "@hiveai/core";
|
|
600
|
-
function registerTui(program2) {
|
|
601
|
-
program2.command("tui").description(
|
|
602
|
-
"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"
|
|
603
|
-
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
604
|
-
if (!process.stdout.isTTY) {
|
|
605
|
-
console.error("haive tui requires an interactive terminal (TTY).");
|
|
606
|
-
process.exitCode = 1;
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
const root = findProjectRoot2(opts.dir);
|
|
610
|
-
const { render } = await import("ink");
|
|
611
|
-
const { createElement } = await import("react");
|
|
612
|
-
const { Dashboard } = await import("./Dashboard-Y2AIWFZK.js");
|
|
613
|
-
const { waitUntilExit } = render(createElement(Dashboard, { root }));
|
|
614
|
-
await waitUntilExit();
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// src/commands/embeddings.ts
|
|
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;
|
|
632
|
-
}
|
|
633
|
-
const { Embedder, rebuildIndex } = await loadEmbeddings();
|
|
634
|
-
ui.info("Loading embedding model (first run downloads ~110MB)\u2026");
|
|
635
|
-
const embedder = await Embedder.create();
|
|
636
|
-
ui.info(`Model ready: ${embedder.model} (dim=${embedder.dimension}). Indexing memories\u2026`);
|
|
637
|
-
const { report } = await rebuildIndex(paths, embedder);
|
|
638
|
-
ui.success(
|
|
639
|
-
`Indexed ${report.total} memories \u2014 added=${report.added} updated=${report.updated} unchanged=${report.unchanged} removed=${report.removed}`
|
|
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;
|
|
654
|
-
}
|
|
655
|
-
if (result.hits.length === 0) {
|
|
656
|
-
ui.info("No semantic matches above the threshold.");
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
for (const hit of result.hits) {
|
|
660
|
-
const score = hit.score.toFixed(3);
|
|
661
|
-
console.log(`${ui.bold(score)} ${hit.id}`);
|
|
662
|
-
console.log(` ${ui.dim(path2.relative(root, hit.file_path))}`);
|
|
663
|
-
}
|
|
664
|
-
});
|
|
665
|
-
embeddings.command("status").description("Show the embeddings index status").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
666
|
-
const root = findProjectRoot3(opts.dir);
|
|
667
|
-
const paths = resolveHaivePaths2(root);
|
|
668
|
-
const { indexStat } = await loadEmbeddings();
|
|
669
|
-
const stat2 = await indexStat(paths);
|
|
670
|
-
if (!stat2.exists) {
|
|
671
|
-
ui.warn("No embeddings index. Run `haive embeddings index` to create one.");
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
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() {
|
|
726
|
+
async function refreshMemorySemanticIndex(paths) {
|
|
681
727
|
try {
|
|
682
|
-
|
|
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;
|
|
683
735
|
} catch {
|
|
684
|
-
|
|
685
|
-
"Could not load @hiveai/embeddings. Run: npm install -g @hiveai/embeddings (or `pnpm build` in the monorepo)"
|
|
686
|
-
);
|
|
687
|
-
process.exit(1);
|
|
736
|
+
return false;
|
|
688
737
|
}
|
|
689
738
|
}
|
|
690
739
|
|
|
691
|
-
// src/commands/
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
"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"
|
|
708
|
-
).option("-d, --dir <dir>", "project root").option(
|
|
709
|
-
"--exclude <csv>",
|
|
710
|
-
"extra directory names to skip (comma-separated)",
|
|
711
|
-
""
|
|
712
|
-
).action(async (opts) => {
|
|
713
|
-
const root = findProjectRoot4(opts.dir);
|
|
714
|
-
const paths = resolveHaivePaths3(root);
|
|
715
|
-
const extraExcludes = (opts.exclude ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
716
|
-
ui.info(`Indexing source files in ${root}\u2026`);
|
|
717
|
-
const map = await buildCodeMap(root, {
|
|
718
|
-
excludeDirs: [
|
|
719
|
-
"node_modules",
|
|
720
|
-
"dist",
|
|
721
|
-
"build",
|
|
722
|
-
"out",
|
|
723
|
-
".git",
|
|
724
|
-
".next",
|
|
725
|
-
".turbo",
|
|
726
|
-
".vitest-cache",
|
|
727
|
-
"coverage",
|
|
728
|
-
...extraExcludes
|
|
729
|
-
]
|
|
730
|
-
});
|
|
731
|
-
await saveCodeMap(paths, map);
|
|
732
|
-
const fileCount = Object.keys(map.files).length;
|
|
733
|
-
const exportCount = Object.values(map.files).reduce((s, f) => s + f.exports.length, 0);
|
|
734
|
-
ui.success(
|
|
735
|
-
`Indexed ${fileCount} file(s) with ${exportCount} export(s) \u2192 ${path3.relative(root, codeMapPath(paths))}`
|
|
736
|
-
);
|
|
737
|
-
});
|
|
738
|
-
idx.command("code-search").description(
|
|
739
|
-
"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"
|
|
740
|
-
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
741
|
-
const root = findProjectRoot4(opts.dir);
|
|
742
|
-
const paths = resolveHaivePaths3(root);
|
|
743
|
-
let mod;
|
|
744
|
-
try {
|
|
745
|
-
mod = await import("@hiveai/embeddings");
|
|
746
|
-
} catch {
|
|
747
|
-
ui.error(
|
|
748
|
-
"@hiveai/embeddings is not installed. Install it (`pnpm add @hiveai/embeddings`) or run `haive embeddings install`."
|
|
749
|
-
);
|
|
750
|
-
process.exit(1);
|
|
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}`));
|
|
751
756
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
ui.
|
|
758
|
-
`Code-search index ready: ${report.total} symbols (+${report.added} new, ~${report.updated} updated, =${report.unchanged} cached, -${report.removed} removed)`
|
|
759
|
-
);
|
|
760
|
-
} catch (err) {
|
|
761
|
-
ui.error(err instanceof Error ? err.message : String(err));
|
|
762
|
-
process.exit(1);
|
|
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}`);
|
|
763
763
|
}
|
|
764
|
-
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
import path10 from "path";
|
|
771
|
-
import { spawnSync as spawnSync3 } from "child_process";
|
|
772
|
-
import "commander";
|
|
773
|
-
import {
|
|
774
|
-
AUTOPILOT_DEFAULTS as AUTOPILOT_DEFAULTS2,
|
|
775
|
-
buildCodeMap as buildCodeMap3,
|
|
776
|
-
resolveHaivePaths as resolveHaivePaths6,
|
|
777
|
-
saveCodeMap as saveCodeMap3,
|
|
778
|
-
saveConfig as saveConfig2
|
|
779
|
-
} from "@hiveai/core";
|
|
780
|
-
|
|
781
|
-
// src/commands/agent.ts
|
|
782
|
-
import { spawnSync } from "child_process";
|
|
783
|
-
import { existsSync as existsSync4 } from "fs";
|
|
784
|
-
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
785
|
-
import os2 from "os";
|
|
786
|
-
import path5 from "path";
|
|
787
|
-
import { createInterface } from "readline/promises";
|
|
788
|
-
import "commander";
|
|
789
|
-
import { findProjectRoot as findProjectRoot5, resolveHaivePaths as resolveHaivePaths4 } from "@hiveai/core";
|
|
790
|
-
|
|
791
|
-
// src/commands/init-mcp-setup.ts
|
|
792
|
-
import { readFile as readFile2, writeFile, mkdir as mkdir2 } from "fs/promises";
|
|
793
|
-
import { existsSync as existsSync3 } from "fs";
|
|
794
|
-
import path4 from "path";
|
|
795
|
-
import os from "os";
|
|
796
|
-
var HOME = os.homedir();
|
|
797
|
-
var HAIVE_MCP_ENTRY = {
|
|
798
|
-
command: "haive",
|
|
799
|
-
args: ["mcp", "--stdio"]
|
|
800
|
-
};
|
|
801
|
-
function projectMcpEntry(root) {
|
|
802
|
-
return {
|
|
803
|
-
command: "haive",
|
|
804
|
-
args: ["mcp", "--stdio"],
|
|
805
|
-
env: { HAIVE_PROJECT_ROOT: root }
|
|
806
|
-
};
|
|
807
|
-
}
|
|
808
|
-
function cursorMcpPath() {
|
|
809
|
-
return path4.join(HOME, ".cursor", "mcp.json");
|
|
810
|
-
}
|
|
811
|
-
async function configureCursor() {
|
|
812
|
-
const mcpPath = cursorMcpPath();
|
|
813
|
-
const cursorDir = path4.join(HOME, ".cursor");
|
|
814
|
-
if (!existsSync3(cursorDir)) return { client: "Cursor", status: "not_installed" };
|
|
815
|
-
let config = {};
|
|
816
|
-
if (existsSync3(mcpPath)) {
|
|
817
|
-
try {
|
|
818
|
-
config = JSON.parse(await readFile2(mcpPath, "utf8"));
|
|
819
|
-
} catch {
|
|
764
|
+
out("");
|
|
765
|
+
}
|
|
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)}`);
|
|
820
770
|
}
|
|
771
|
+
out("");
|
|
821
772
|
}
|
|
822
|
-
config.mcpServers ??= {};
|
|
823
|
-
if (config.mcpServers["haive"]) return { client: "Cursor", status: "already_configured" };
|
|
824
|
-
config.mcpServers["haive"] = HAIVE_MCP_ENTRY;
|
|
825
|
-
await mkdir2(cursorDir, { recursive: true });
|
|
826
|
-
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
827
|
-
return { client: "Cursor", status: "configured", path: mcpPath };
|
|
828
773
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
// Linux
|
|
833
|
-
path4.join(HOME, "Library", "Application Support", "Code", "User", "mcp.json"),
|
|
834
|
-
// macOS
|
|
835
|
-
path4.join(HOME, "AppData", "Roaming", "Code", "User", "mcp.json"),
|
|
836
|
-
// Windows
|
|
837
|
-
path4.join(HOME, ".config", "Code - Insiders", "User", "mcp.json")
|
|
838
|
-
];
|
|
839
|
-
for (const c of candidates) {
|
|
840
|
-
if (existsSync3(path4.dirname(c))) return c;
|
|
774
|
+
var TokenBudgetWriter = class {
|
|
775
|
+
constructor(budgetChars) {
|
|
776
|
+
this.budgetChars = budgetChars;
|
|
841
777
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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;
|
|
852
788
|
}
|
|
789
|
+
console.log(text);
|
|
790
|
+
this.used = next;
|
|
791
|
+
return true;
|
|
853
792
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
config.servers["haive"] = { ...HAIVE_MCP_ENTRY, type: "stdio" };
|
|
857
|
-
await mkdir2(path4.dirname(mcpPath), { recursive: true });
|
|
858
|
-
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
859
|
-
return { client: "VS Code", status: "configured", path: mcpPath };
|
|
860
|
-
}
|
|
861
|
-
function claudeConfigPath() {
|
|
862
|
-
const p = path4.join(HOME, ".claude.json");
|
|
863
|
-
if (existsSync3(p)) return p;
|
|
864
|
-
const p2 = path4.join(HOME, ".config", "claude", "claude.json");
|
|
865
|
-
if (existsSync3(path4.dirname(p2))) return p2;
|
|
866
|
-
return null;
|
|
867
|
-
}
|
|
868
|
-
async function configureClaude() {
|
|
869
|
-
const cfgPath = claudeConfigPath() ?? path4.join(HOME, ".claude.json");
|
|
870
|
-
if (!existsSync3(cfgPath) && !existsSync3(path4.join(HOME, ".claude"))) {
|
|
871
|
-
return { client: "Claude Code", status: "not_installed" };
|
|
872
|
-
}
|
|
873
|
-
let config = {};
|
|
874
|
-
if (existsSync3(cfgPath)) {
|
|
875
|
-
try {
|
|
876
|
-
config = JSON.parse(await readFile2(cfgPath, "utf8"));
|
|
877
|
-
} catch {
|
|
878
|
-
}
|
|
793
|
+
isTruncated() {
|
|
794
|
+
return this.truncated;
|
|
879
795
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
config.mcpServers["haive"] = { ...HAIVE_MCP_ENTRY, type: "stdio" };
|
|
883
|
-
await writeFile(cfgPath, JSON.stringify(config, null, 2), "utf8");
|
|
884
|
-
return { client: "Claude Code", status: "configured", path: cfgPath };
|
|
885
|
-
}
|
|
886
|
-
function windsurfMcpPath() {
|
|
887
|
-
const candidates = [
|
|
888
|
-
path4.join(HOME, ".codeium", "windsurf", "mcp_config.json"),
|
|
889
|
-
path4.join(HOME, ".windsurf", "mcp.json")
|
|
890
|
-
];
|
|
891
|
-
for (const c of candidates) {
|
|
892
|
-
if (existsSync3(path4.dirname(c))) return c;
|
|
796
|
+
remainingChars() {
|
|
797
|
+
return Math.max(0, this.budgetChars - this.used);
|
|
893
798
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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(() => {
|
|
837
|
+
});
|
|
904
838
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
const results = [];
|
|
915
|
-
const configurators = [configureCursor, configureVSCode, configureClaude, configureWindsurf];
|
|
916
|
-
for (const fn of configurators) {
|
|
917
|
-
try {
|
|
918
|
-
results.push(await fn());
|
|
919
|
-
} catch (err) {
|
|
920
|
-
const name = fn.name.replace("configure", "");
|
|
921
|
-
results.push({ client: name, status: "error", error: String(err) });
|
|
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(() => {
|
|
847
|
+
});
|
|
922
848
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
const results = [];
|
|
929
|
-
try {
|
|
930
|
-
const cursorPath = path4.join(root, ".cursor", "mcp.json");
|
|
931
|
-
let config = {};
|
|
932
|
-
if (existsSync3(cursorPath)) {
|
|
933
|
-
try {
|
|
934
|
-
config = JSON.parse(await readFile2(cursorPath, "utf8"));
|
|
935
|
-
} catch {
|
|
936
|
-
}
|
|
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).`);
|
|
937
854
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
let config = {};
|
|
949
|
-
if (existsSync3(vscodePath)) {
|
|
950
|
-
try {
|
|
951
|
-
config = JSON.parse(await readFile2(vscodePath, "utf8"));
|
|
952
|
-
} catch {
|
|
953
|
-
}
|
|
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
|
|
862
|
+
});
|
|
863
|
+
budgetTokensCap = presetNums.max_tokens;
|
|
864
|
+
maxMemories = presetNums.max_memories;
|
|
954
865
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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");
|
|
970
887
|
}
|
|
971
|
-
}
|
|
972
|
-
config.mcpServers ??= {};
|
|
973
|
-
config.mcpServers["haive"] = { ...entry, type: "stdio" };
|
|
974
|
-
await writeFile(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
975
|
-
results.push({ client: "Claude Code (project)", status: "configured", path: mcpPath });
|
|
976
|
-
} catch (err) {
|
|
977
|
-
results.push({ client: "Claude Code (project)", status: "error", error: String(err) });
|
|
978
|
-
}
|
|
979
|
-
return results;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
// src/commands/agent.ts
|
|
983
|
-
function registerAgent(program2) {
|
|
984
|
-
const agent = program2.command("agent").description("Detect, configure, and report the best hAIve mode for AI coding agents.");
|
|
985
|
-
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) => {
|
|
986
|
-
const detection = await detectAgentMode(opts.dir);
|
|
987
|
-
printDetection(detection, Boolean(opts.json));
|
|
988
|
-
});
|
|
989
|
-
agent.command("status").description("Alias for agent detect.").option("-d, --dir <dir>", "project root").option("--json", "emit JSON", false).action(async (opts) => {
|
|
990
|
-
const detection = await detectAgentMode(opts.dir);
|
|
991
|
-
printDetection(detection, Boolean(opts.json));
|
|
992
|
-
});
|
|
993
|
-
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) => {
|
|
994
|
-
const result = await setupAgentMode(opts.dir, {
|
|
995
|
-
yes: Boolean(opts.yes),
|
|
996
|
-
global: opts.global !== false && opts.noGlobal !== true,
|
|
997
|
-
interactive: process.stdin.isTTY
|
|
998
|
-
});
|
|
999
|
-
if (opts.json) {
|
|
1000
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1001
888
|
return;
|
|
1002
889
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
+
}
|
|
1022
916
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
detection,
|
|
1030
|
-
project_results: projectResults,
|
|
1031
|
-
global_results: globalResults,
|
|
1032
|
-
mode_file: modeFile,
|
|
1033
|
-
...globalSkippedReason ? { global_skipped_reason: globalSkippedReason } : {}
|
|
1034
|
-
};
|
|
1035
|
-
}
|
|
1036
|
-
async function detectAgentMode(dir) {
|
|
1037
|
-
const root = findProjectRoot5(dir);
|
|
1038
|
-
const paths = resolveHaivePaths4(root);
|
|
1039
|
-
const projectMcp = [
|
|
1040
|
-
{ client: "Claude Code", path: path5.join(root, ".mcp.json"), present: existsSync4(path5.join(root, ".mcp.json")) },
|
|
1041
|
-
{ client: "Cursor", path: path5.join(root, ".cursor", "mcp.json"), present: existsSync4(path5.join(root, ".cursor", "mcp.json")) },
|
|
1042
|
-
{ client: "VS Code", path: path5.join(root, ".vscode", "mcp.json"), present: existsSync4(path5.join(root, ".vscode", "mcp.json")) }
|
|
1043
|
-
];
|
|
1044
|
-
const installedAgents = [
|
|
1045
|
-
{ agent: "Codex", command: "codex", installed: commandExists("codex"), mcp_configured: codexMcpConfigured() },
|
|
1046
|
-
{ agent: "Claude", command: "claude", installed: commandExists("claude") },
|
|
1047
|
-
{ agent: "Aider", command: "aider", installed: commandExists("aider") },
|
|
1048
|
-
{ agent: "Cursor", command: "cursor", installed: commandExists("cursor") }
|
|
1049
|
-
];
|
|
1050
|
-
const hasProjectMcp = projectMcp.some((item) => item.present);
|
|
1051
|
-
const hasNativeMcp = hasProjectMcp || installedAgents.some((a) => a.mcp_configured);
|
|
1052
|
-
const wrapperAgent = installedAgents.find((a) => a.installed && ["codex", "claude", "aider"].includes(a.command));
|
|
1053
|
-
const recommendedMode = hasNativeMcp ? "mcp" : wrapperAgent ? "wrapped" : "fallback";
|
|
1054
|
-
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 "..."';
|
|
1055
|
-
return {
|
|
1056
|
-
root,
|
|
1057
|
-
initialized: existsSync4(paths.haiveDir),
|
|
1058
|
-
project_mcp: projectMcp,
|
|
1059
|
-
installed_agents: installedAgents,
|
|
1060
|
-
recommended_mode: recommendedMode,
|
|
1061
|
-
recommended_command: recommendedCommand
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
async function writeAgentModeRecord(paths, detection, skippedReason) {
|
|
1065
|
-
const dir = path5.join(paths.runtimeDir, "enforcement");
|
|
1066
|
-
await mkdir3(dir, { recursive: true });
|
|
1067
|
-
const file = path5.join(dir, "agent-mode.json");
|
|
1068
|
-
const record = {
|
|
1069
|
-
selected_mode: detection.recommended_mode,
|
|
1070
|
-
recommended_command: detection.recommended_command,
|
|
1071
|
-
configured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1072
|
-
project_root: detection.root,
|
|
1073
|
-
notes: [
|
|
1074
|
-
"mcp = native hAIve MCP tools are available or project MCP config exists.",
|
|
1075
|
-
"wrapped = use haive run when native MCP is unavailable.",
|
|
1076
|
-
"fallback = use haive briefing/enforce manually.",
|
|
1077
|
-
...skippedReason ? [skippedReason] : []
|
|
1078
|
-
]
|
|
1079
|
-
};
|
|
1080
|
-
await writeFile2(file, JSON.stringify(record, null, 2) + "\n", "utf8");
|
|
1081
|
-
return file;
|
|
1082
|
-
}
|
|
1083
|
-
async function confirmGlobalSetup() {
|
|
1084
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1085
|
-
try {
|
|
1086
|
-
const answer = await rl.question(
|
|
1087
|
-
"Configure hAIve in user-level AI client configs (Cursor/VS Code/Claude/Codex when detected)? [y/N] "
|
|
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()
|
|
1088
923
|
);
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
"mcp",
|
|
1099
|
-
"add",
|
|
1100
|
-
"haive",
|
|
1101
|
-
"--env",
|
|
1102
|
-
`HAIVE_PROJECT_ROOT=${root}`,
|
|
1103
|
-
"--",
|
|
1104
|
-
"haive",
|
|
1105
|
-
"mcp",
|
|
1106
|
-
"--stdio"
|
|
1107
|
-
], { encoding: "utf8" });
|
|
1108
|
-
if (result.status === 0) return { client: "Codex", status: "configured", path: path5.join(os2.homedir(), ".codex", "config.toml") };
|
|
1109
|
-
return { client: "Codex", status: "error", error: result.stderr || result.stdout || "codex mcp add failed" };
|
|
1110
|
-
}
|
|
1111
|
-
function commandExists(command) {
|
|
1112
|
-
const result = spawnSync(process.platform === "win32" ? "where" : "which", [command], {
|
|
1113
|
-
encoding: "utf8",
|
|
1114
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1115
|
-
});
|
|
1116
|
-
return result.status === 0;
|
|
1117
|
-
}
|
|
1118
|
-
function codexMcpConfigured() {
|
|
1119
|
-
if (!commandExists("codex")) return false;
|
|
1120
|
-
const result = spawnSync("codex", ["mcp", "get", "haive"], {
|
|
1121
|
-
encoding: "utf8",
|
|
1122
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1123
|
-
});
|
|
1124
|
-
return result.status === 0;
|
|
1125
|
-
}
|
|
1126
|
-
function printDetection(detection, json) {
|
|
1127
|
-
if (json) {
|
|
1128
|
-
console.log(JSON.stringify(detection, null, 2));
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
console.log(ui.bold("hAIve agent status"));
|
|
1132
|
-
console.log(ui.dim(` root: ${detection.root}`));
|
|
1133
|
-
console.log(`${detection.initialized ? ui.green("\u2713") : ui.red("\u2717")} project initialized`);
|
|
1134
|
-
for (const cfg of detection.project_mcp) {
|
|
1135
|
-
console.log(`${cfg.present ? ui.green("\u2713") : ui.yellow("\u2022")} ${cfg.client} project MCP ${ui.dim(path5.relative(detection.root, cfg.path))}`);
|
|
1136
|
-
}
|
|
1137
|
-
for (const agent of detection.installed_agents) {
|
|
1138
|
-
const marker = agent.installed ? ui.green("\u2713") : ui.dim("\u2022");
|
|
1139
|
-
const mcp = agent.mcp_configured === true ? " + hAIve MCP" : "";
|
|
1140
|
-
console.log(`${marker} ${agent.agent} (${agent.command})${mcp}`);
|
|
1141
|
-
}
|
|
1142
|
-
console.log(ui.bold(`Recommended mode: ${detection.recommended_mode}`));
|
|
1143
|
-
console.log(` ${detection.recommended_command}`);
|
|
1144
|
-
}
|
|
1145
|
-
function printSetupResult(result) {
|
|
1146
|
-
for (const item of result.project_results) {
|
|
1147
|
-
if (item.status === "configured") ui.success(`${item.client} project MCP config written (${item.path})`);
|
|
1148
|
-
else if (item.status === "already_configured") ui.info(`${item.client} already configured`);
|
|
1149
|
-
else if (item.status === "error") ui.warn(`${item.client}: ${item.error}`);
|
|
1150
|
-
}
|
|
1151
|
-
for (const item of result.global_results) {
|
|
1152
|
-
if (item.status === "configured") ui.success(`${item.client} user-level MCP configured${item.path ? ` (${item.path})` : ""}`);
|
|
1153
|
-
else if (item.status === "already_configured") ui.info(`${item.client} user-level MCP already configured`);
|
|
1154
|
-
else if (item.status === "not_installed") ui.info(`${item.client} not detected`);
|
|
1155
|
-
else if (item.status === "error") ui.warn(`${item.client}: ${item.error}`);
|
|
1156
|
-
}
|
|
1157
|
-
if (result.global_skipped_reason) ui.warn(result.global_skipped_reason);
|
|
1158
|
-
ui.success(`Agent mode recorded at ${result.mode_file}`);
|
|
1159
|
-
printDetection(result.detection, false);
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// src/utils/autopilot.ts
|
|
1163
|
-
import { existsSync as existsSync6 } from "fs";
|
|
1164
|
-
import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
1165
|
-
import path7 from "path";
|
|
1166
|
-
import {
|
|
1167
|
-
AUTOPILOT_DEFAULTS,
|
|
1168
|
-
buildCodeMap as buildCodeMap2,
|
|
1169
|
-
loadCodeMap as loadCodeMap3,
|
|
1170
|
-
loadConfig,
|
|
1171
|
-
loadMemoriesFromDir as loadMemoriesFromDir3,
|
|
1172
|
-
saveCodeMap as saveCodeMap2,
|
|
1173
|
-
saveConfig
|
|
1174
|
-
} from "@hiveai/core";
|
|
1175
|
-
|
|
1176
|
-
// src/commands/memory-lint.ts
|
|
1177
|
-
import { existsSync as existsSync5 } from "fs";
|
|
1178
|
-
import { writeFile as writeFile3 } from "fs/promises";
|
|
1179
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
1180
|
-
import path6 from "path";
|
|
1181
|
-
import "commander";
|
|
1182
|
-
import {
|
|
1183
|
-
findProjectRoot as findProjectRoot6,
|
|
1184
|
-
getUsage,
|
|
1185
|
-
loadCodeMap as loadCodeMap2,
|
|
1186
|
-
loadMemoriesFromDir as loadMemoriesFromDir2,
|
|
1187
|
-
loadUsageIndex as loadUsageIndex2,
|
|
1188
|
-
resolveHaivePaths as resolveHaivePaths5,
|
|
1189
|
-
serializeMemory
|
|
1190
|
-
} from "@hiveai/core";
|
|
1191
|
-
async function lintMemoriesAsync(root, options = {}) {
|
|
1192
|
-
const paths = resolveHaivePaths5(root);
|
|
1193
|
-
const out = [];
|
|
1194
|
-
const fixes = [];
|
|
1195
|
-
if (!existsSync5(paths.memoriesDir)) return { findings: out, fixes };
|
|
1196
|
-
const loaded = await loadMemoriesFromDir2(paths.memoriesDir);
|
|
1197
|
-
const usage = await loadUsageIndex2(paths);
|
|
1198
|
-
const codeMap = await loadCodeMap2(paths);
|
|
1199
|
-
const trackedFiles = gitTrackedFiles(root);
|
|
1200
|
-
const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
|
|
1201
|
-
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;
|
|
1202
|
-
for (const { filePath, memory: memory2 } of loaded) {
|
|
1203
|
-
const fm = memory2.frontmatter;
|
|
1204
|
-
if (fm.type === "session_recap") continue;
|
|
1205
|
-
const body = memory2.body.trim();
|
|
1206
|
-
const naked = body.replace(/^#.*$/gm, "").replace(/```[\s\S]*?```/g, "").trim();
|
|
1207
|
-
if (naked.length < 40 && fm.status !== "rejected") {
|
|
1208
|
-
out.push({
|
|
1209
|
-
file: filePath,
|
|
1210
|
-
id: fm.id,
|
|
1211
|
-
severity: "warn",
|
|
1212
|
-
code: "SHORT_BODY",
|
|
1213
|
-
message: "Body looks very short (< ~40 chars of prose after headings). Prefer actionable detail."
|
|
1214
|
-
});
|
|
1215
|
-
}
|
|
1216
|
-
if (["decision", "gotcha", "convention", "architecture", "attempt"].includes(fm.type) && fm.status !== "rejected" && !actionableWords.test(naked)) {
|
|
1217
|
-
out.push({
|
|
1218
|
-
file: filePath,
|
|
1219
|
-
id: fm.id,
|
|
1220
|
-
severity: "info",
|
|
1221
|
-
code: "LOW_ACTIONABILITY",
|
|
1222
|
-
message: "Record does not contain obvious action/rationale words. Add the concrete rule, why it exists, and what to do instead."
|
|
1223
|
-
});
|
|
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("");
|
|
1224
933
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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("");
|
|
950
|
+
}
|
|
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
|
+
);
|
|
1235
955
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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;
|
|
976
|
+
}
|
|
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)`);
|
|
988
|
+
}
|
|
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;
|
|
1244
995
|
}
|
|
1245
|
-
if (
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
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("");
|
|
1253
1040
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
id: fm.id,
|
|
1259
|
-
severity: "warn",
|
|
1260
|
-
code: "NO_MD_HEADING",
|
|
1261
|
-
message: "No Markdown heading (#/##/###) \u2014 add one so humans and auditors can skim the memo quickly."
|
|
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(() => {
|
|
1262
1045
|
});
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
id: fm.id,
|
|
1271
|
-
severity: "info",
|
|
1272
|
-
code: "NEVER_READ",
|
|
1273
|
-
message: "Validated record has never been surfaced/read. Consider improving tags/anchors or archiving it if it is not useful."
|
|
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(() => {
|
|
1274
1053
|
});
|
|
1275
1054
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
if (
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
${nextBody.trim()}`;
|
|
1284
|
-
actions.push("add missing Markdown heading");
|
|
1285
|
-
}
|
|
1286
|
-
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated" && suggestedAnchors.paths.length > 0) {
|
|
1287
|
-
nextFrontmatter = {
|
|
1288
|
-
...nextFrontmatter,
|
|
1289
|
-
anchor: {
|
|
1290
|
-
...nextFrontmatter.anchor,
|
|
1291
|
-
paths: [.../* @__PURE__ */ new Set([...nextFrontmatter.anchor.paths, ...suggestedAnchors.paths])],
|
|
1292
|
-
symbols: [
|
|
1293
|
-
.../* @__PURE__ */ new Set([...nextFrontmatter.anchor.symbols, ...suggestedAnchors.symbols])
|
|
1294
|
-
]
|
|
1295
|
-
},
|
|
1296
|
-
tags: nextFrontmatter.tags.filter((tag) => tag !== "needs_anchor")
|
|
1297
|
-
};
|
|
1298
|
-
actions.push("add suggested tracked anchor paths");
|
|
1299
|
-
if (suggestedAnchors.symbols.length > 0) {
|
|
1300
|
-
actions.push("add suggested anchor symbols");
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
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")) {
|
|
1304
|
-
nextFrontmatter = {
|
|
1305
|
-
...nextFrontmatter,
|
|
1306
|
-
tags: [...nextFrontmatter.tags, "needs_anchor"]
|
|
1307
|
-
};
|
|
1308
|
-
actions.push("tag validated anchorless record with needs_anchor");
|
|
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");
|
|
1309
1062
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
+
}
|
|
1318
1091
|
}
|
|
1092
|
+
out("");
|
|
1319
1093
|
}
|
|
1320
1094
|
}
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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";
|
|
1103
|
+
}
|
|
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)
|
|
1329
1171
|
});
|
|
1330
|
-
|
|
1331
|
-
|
|
1172
|
+
if (!result) {
|
|
1173
|
+
ui.error("No embeddings index found. Run `haive embeddings index` first.");
|
|
1174
|
+
process.exitCode = 1;
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
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))}`);
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
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
|
+
});
|
|
1332
1201
|
}
|
|
1333
|
-
function
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
+
}
|
|
1336
1211
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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
|
+
);
|
|
1260
|
+
});
|
|
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);
|
|
1346
1274
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
}
|
|
1359
|
-
if (paths.size >= 5 && symbols.size >= 5) break;
|
|
1360
|
-
}
|
|
1361
|
-
if (paths.size >= 5 && symbols.size >= 5) break;
|
|
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);
|
|
1362
1286
|
}
|
|
1363
|
-
}
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
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) {
|
|
1364
1325
|
return {
|
|
1365
|
-
|
|
1366
|
-
|
|
1326
|
+
command: "haive",
|
|
1327
|
+
args: ["mcp", "--stdio"],
|
|
1328
|
+
env: { HAIVE_PROJECT_ROOT: root }
|
|
1367
1329
|
};
|
|
1368
1330
|
}
|
|
1369
|
-
function
|
|
1370
|
-
|
|
1371
|
-
cwd: root,
|
|
1372
|
-
encoding: "utf8",
|
|
1373
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1374
|
-
});
|
|
1375
|
-
if (result.status !== 0) return null;
|
|
1376
|
-
const files = result.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1377
|
-
return new Set(files);
|
|
1331
|
+
function cursorMcpPath() {
|
|
1332
|
+
return path6.join(HOME, ".cursor", "mcp.json");
|
|
1378
1333
|
}
|
|
1379
|
-
function
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
if (
|
|
1383
|
-
|
|
1384
|
-
if (
|
|
1385
|
-
|
|
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 {
|
|
1343
|
+
}
|
|
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 };
|
|
1386
1351
|
}
|
|
1387
|
-
function
|
|
1388
|
-
const
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
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 {
|
|
1408
1375
|
}
|
|
1409
1376
|
}
|
|
1410
|
-
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
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 };
|
|
1416
1383
|
}
|
|
1417
|
-
function
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
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;
|
|
1422
1390
|
}
|
|
1423
|
-
function
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
console.log(JSON.stringify({
|
|
1434
|
-
findings_count: findings.length,
|
|
1435
|
-
findings,
|
|
1436
|
-
fixes_count: report.fixes.length,
|
|
1437
|
-
fixes: report.fixes,
|
|
1438
|
-
fix_mode: opts.fix ? apply ? "apply" : "dry-run" : "off"
|
|
1439
|
-
}, null, 2));
|
|
1440
|
-
process.exitCode = findings.some((f) => f.severity === "error") ? 1 : 0;
|
|
1441
|
-
return;
|
|
1442
|
-
}
|
|
1443
|
-
if (findings.length === 0) {
|
|
1444
|
-
ui.success(`memory lint OK \u2014 ${root}`);
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1447
|
-
console.log(ui.bold(`memory lint (${findings.length} finding${findings.length === 1 ? "" : "s"})`) + `
|
|
1448
|
-
`);
|
|
1449
|
-
if (opts.fix) {
|
|
1450
|
-
const mode = apply ? "apply" : dryRun ? "dry-run" : "dry-run";
|
|
1451
|
-
const verb = apply ? "changed" : "would change";
|
|
1452
|
-
console.log(ui.bold(`fix ${mode}: ${report.fixes.length} file${report.fixes.length === 1 ? "" : "s"} ${verb}`));
|
|
1453
|
-
for (const fix of report.fixes) {
|
|
1454
|
-
console.log(` ${ui.dim(fix.id)} ${fix.actions.join("; ")}`);
|
|
1455
|
-
console.log(ui.dim(` \u2192 ${fix.file}`));
|
|
1456
|
-
}
|
|
1457
|
-
console.log();
|
|
1458
|
-
}
|
|
1459
|
-
const order = { error: 0, warn: 1, info: 2 };
|
|
1460
|
-
findings.sort((a, b) => order[a.severity] - order[b.severity] || a.id.localeCompare(b.id));
|
|
1461
|
-
for (const f of findings) {
|
|
1462
|
-
const color = f.severity === "error" ? ui.red : f.severity === "warn" ? ui.yellow : ui.dim;
|
|
1463
|
-
console.log(
|
|
1464
|
-
`${color(f.severity.padEnd(5))} ${ui.dim(f.code)} ${f.id}`
|
|
1465
|
-
);
|
|
1466
|
-
console.log(` ${f.message}`);
|
|
1467
|
-
if (f.suggested_anchors) {
|
|
1468
|
-
const pathHints = f.suggested_anchors.paths.length > 0 ? `paths: ${f.suggested_anchors.paths.join(", ")}` : "";
|
|
1469
|
-
const symbolHints = f.suggested_anchors.symbols.length > 0 ? `symbols: ${f.suggested_anchors.symbols.join(", ")}` : "";
|
|
1470
|
-
console.log(ui.dim(` suggested anchors: ${[pathHints, symbolHints].filter(Boolean).join(" \xB7 ")}`));
|
|
1471
|
-
}
|
|
1472
|
-
console.log(ui.dim(` \u2192 ${f.file}`));
|
|
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 {
|
|
1473
1401
|
}
|
|
1474
|
-
|
|
1475
|
-
}
|
|
1402
|
+
}
|
|
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 };
|
|
1476
1408
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
if (changed) {
|
|
1485
|
-
repairs.push({
|
|
1486
|
-
code: "autopilot-config",
|
|
1487
|
-
message: "Enabled autopilot defaults in .ai/haive.config.json."
|
|
1488
|
-
});
|
|
1489
|
-
}
|
|
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;
|
|
1490
1416
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
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 {
|
|
1500
1427
|
}
|
|
1501
1428
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
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) });
|
|
1510
1445
|
}
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
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
|
+
}
|
|
1517
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) });
|
|
1518
1468
|
}
|
|
1519
|
-
|
|
1520
|
-
const
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
}
|
|
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
|
+
}
|
|
1526
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) });
|
|
1527
1485
|
}
|
|
1528
|
-
|
|
1529
|
-
const
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
}
|
|
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
|
+
}
|
|
1535
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) });
|
|
1536
1501
|
}
|
|
1537
|
-
return
|
|
1502
|
+
return results;
|
|
1538
1503
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
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;
|
|
1525
|
+
}
|
|
1526
|
+
printSetupResult(result);
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
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.";
|
|
1567
1545
|
}
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
await saveConfig(paths, next);
|
|
1571
|
-
return true;
|
|
1572
|
-
}
|
|
1573
|
-
async function syncProjectContextVersion(root, paths) {
|
|
1574
|
-
const status = await projectContextVersionStatus(root, paths);
|
|
1575
|
-
if (!status.canSync || !status.expectedVersion) return false;
|
|
1576
|
-
const original = await readFile3(paths.projectContext, "utf8");
|
|
1577
|
-
let updated = original.replace(
|
|
1578
|
-
/^# Project context — hAIve \(v[^)]+\)$/m,
|
|
1579
|
-
`# Project context \u2014 hAIve (v${status.expectedVersion})`
|
|
1580
|
-
).replace(
|
|
1581
|
-
/> \*\*Current version\*\*: [^—\n]+—/m,
|
|
1582
|
-
`> **Current version**: ${status.expectedVersion} \u2014`
|
|
1583
|
-
);
|
|
1584
|
-
if (updated === original && !original.includes("Current version")) {
|
|
1585
|
-
updated = original.replace(
|
|
1586
|
-
/^(> Repo-native context enforcement[^\n]*\n)/m,
|
|
1587
|
-
`$1> **Current version**: ${status.expectedVersion} \u2014 @hiveai/core, cli, mcp, embeddings are versioned together.
|
|
1588
|
-
`
|
|
1589
|
-
);
|
|
1590
|
-
}
|
|
1591
|
-
if (updated === original && !original.includes("Current version")) {
|
|
1592
|
-
updated = original.replace(
|
|
1593
|
-
/^(# Project context[^\n]*\n)/m,
|
|
1594
|
-
`$1
|
|
1595
|
-
> **Current version**: ${status.expectedVersion}
|
|
1596
|
-
`
|
|
1597
|
-
);
|
|
1546
|
+
} else {
|
|
1547
|
+
globalSkippedReason = "User-level/global MCP configuration disabled.";
|
|
1598
1548
|
}
|
|
1599
|
-
|
|
1600
|
-
await
|
|
1601
|
-
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
|
+
};
|
|
1602
1558
|
}
|
|
1603
|
-
async function
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
const
|
|
1618
|
-
const
|
|
1619
|
-
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 "..."';
|
|
1620
1578
|
return {
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1579
|
+
root,
|
|
1580
|
+
initialized: existsSync6(paths.haiveDir),
|
|
1581
|
+
project_mcp: projectMcp,
|
|
1582
|
+
installed_agents: installedAgents,
|
|
1583
|
+
recommended_mode: recommendedMode,
|
|
1584
|
+
recommended_command: recommendedCommand
|
|
1625
1585
|
};
|
|
1626
1586
|
}
|
|
1627
|
-
async function
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
"
|
|
1638
|
-
".
|
|
1639
|
-
".
|
|
1640
|
-
|
|
1641
|
-
".vitest-cache",
|
|
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
|
-
await
|
|
1646
|
-
return
|
|
1602
|
+
};
|
|
1603
|
+
await writeFile4(file, JSON.stringify(record, null, 2) + "\n", "utf8");
|
|
1604
|
+
return file;
|
|
1647
1605
|
}
|
|
1648
|
-
async function
|
|
1606
|
+
async function confirmGlobalSetup() {
|
|
1607
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1649
1608
|
try {
|
|
1650
|
-
const
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
return
|
|
1654
|
-
}
|
|
1655
|
-
|
|
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();
|
|
1656
1615
|
}
|
|
1657
1616
|
}
|
|
1658
|
-
async function
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
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}`);
|
|
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}`);
|
|
1669
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);
|
|
1670
1683
|
}
|
|
1671
1684
|
|
|
1672
1685
|
// src/commands/init-bootstrap.ts
|
|
@@ -2934,7 +2947,7 @@ function registerInit(program2) {
|
|
|
2934
2947
|
}
|
|
2935
2948
|
try {
|
|
2936
2949
|
ui.info("Building code-map\u2026");
|
|
2937
|
-
const map = await buildCodeMap3(root);
|
|
2950
|
+
const map = await buildCodeMap3(root, { includeUntracked: true });
|
|
2938
2951
|
await saveCodeMap3(paths, map);
|
|
2939
2952
|
ui.success(`Code-map built (${Object.keys(map.files).length} files)`);
|
|
2940
2953
|
} catch {
|
|
@@ -3422,6 +3435,27 @@ async function readStdin(maxBytes) {
|
|
|
3422
3435
|
setTimeout(finish, 2e3);
|
|
3423
3436
|
});
|
|
3424
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
|
+
}
|
|
3425
3459
|
function registerObserve(program2) {
|
|
3426
3460
|
program2.command("observe").description(
|
|
3427
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`."
|
|
@@ -3445,13 +3479,15 @@ function registerObserve(program2) {
|
|
|
3445
3479
|
if (!root) return;
|
|
3446
3480
|
const paths = resolveHaivePaths7(root);
|
|
3447
3481
|
if (!existsSync12(paths.haiveDir)) return;
|
|
3482
|
+
const failureHint = detectFailure(payload);
|
|
3448
3483
|
const observation = {
|
|
3449
3484
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3450
3485
|
session_id: payload.session_id,
|
|
3451
3486
|
cwd: payload.cwd,
|
|
3452
3487
|
tool: payload.tool_name ?? "?",
|
|
3453
3488
|
summary: buildSummary(payload),
|
|
3454
|
-
files: extractFiles(payload)
|
|
3489
|
+
files: extractFiles(payload),
|
|
3490
|
+
...failureHint ? { failure_hint: true } : {}
|
|
3455
3491
|
};
|
|
3456
3492
|
const cacheDir = path13.join(paths.haiveDir, ".cache");
|
|
3457
3493
|
await mkdir8(cacheDir, { recursive: true });
|
|
@@ -7059,7 +7095,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
7059
7095
|
};
|
|
7060
7096
|
}
|
|
7061
7097
|
var SERVER_NAME = "haive";
|
|
7062
|
-
var SERVER_VERSION = "0.9.
|
|
7098
|
+
var SERVER_VERSION = "0.9.25";
|
|
7063
7099
|
function jsonResult(data) {
|
|
7064
7100
|
return {
|
|
7065
7101
|
content: [
|
|
@@ -8657,6 +8693,7 @@ TODO \u2014 write the memory body.
|
|
|
8657
8693
|
await writeFile14(topicMatch.filePath, serializeMemory12({ frontmatter: newFrontmatter, body }), "utf8");
|
|
8658
8694
|
ui.success(`Updated (topic upsert) ${path16.relative(root, topicMatch.filePath)}`);
|
|
8659
8695
|
ui.info(`id=${fm.id} revision=${revisionCount}`);
|
|
8696
|
+
await runPostMemoryAutopilot(root, paths, config);
|
|
8660
8697
|
return;
|
|
8661
8698
|
}
|
|
8662
8699
|
}
|
|
@@ -8696,6 +8733,7 @@ TODO \u2014 write the memory body.
|
|
|
8696
8733
|
await writeFile14(file, serializeMemory12({ frontmatter, body }), "utf8");
|
|
8697
8734
|
ui.success(`Created ${path16.relative(root, file)}`);
|
|
8698
8735
|
ui.info(`id=${frontmatter.id} scope=${frontmatter.scope} status=${frontmatter.status}`);
|
|
8736
|
+
await runPostMemoryAutopilot(root, paths, config);
|
|
8699
8737
|
if (inferredTags.length > 0) {
|
|
8700
8738
|
ui.info(`auto-tagged: ${inferredTags.join(", ")} (use --no-auto-tag to disable)`);
|
|
8701
8739
|
}
|
|
@@ -8721,6 +8759,19 @@ TODO \u2014 write the memory body.
|
|
|
8721
8759
|
}
|
|
8722
8760
|
});
|
|
8723
8761
|
}
|
|
8762
|
+
async function runPostMemoryAutopilot(root, paths, config) {
|
|
8763
|
+
if (!config.autopilot && config.autoRepair?.corpus !== true) return;
|
|
8764
|
+
const repairs = await applyAutopilotRepairs(root, paths, {
|
|
8765
|
+
applyConfig: false,
|
|
8766
|
+
applyContext: false,
|
|
8767
|
+
applyCorpus: true,
|
|
8768
|
+
applyCodeMap: false,
|
|
8769
|
+
applyCodeSearch: false
|
|
8770
|
+
});
|
|
8771
|
+
for (const repair of repairs) {
|
|
8772
|
+
ui.info(repair.message);
|
|
8773
|
+
}
|
|
8774
|
+
}
|
|
8724
8775
|
function parseCsv2(value) {
|
|
8725
8776
|
if (!value) return [];
|
|
8726
8777
|
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -10208,6 +10259,7 @@ function registerMemoryDigest(program2) {
|
|
|
10208
10259
|
// src/commands/session-end.ts
|
|
10209
10260
|
import { writeFile as writeFile25, mkdir as mkdir15, readFile as readFile16, rm as rm2 } from "fs/promises";
|
|
10210
10261
|
import { existsSync as existsSync53 } from "fs";
|
|
10262
|
+
import { spawn as spawn4 } from "child_process";
|
|
10211
10263
|
import path36 from "path";
|
|
10212
10264
|
import "commander";
|
|
10213
10265
|
import {
|
|
@@ -10220,9 +10272,9 @@ import {
|
|
|
10220
10272
|
} from "@hiveai/core";
|
|
10221
10273
|
async function buildAutoRecap(paths) {
|
|
10222
10274
|
const obsFile = path36.join(paths.haiveDir, ".cache", "observations.jsonl");
|
|
10223
|
-
if (!existsSync53(obsFile)) return
|
|
10275
|
+
if (!existsSync53(obsFile)) return await buildGitAutoRecap(paths);
|
|
10224
10276
|
const raw = await readFile16(obsFile, "utf8").catch(() => "");
|
|
10225
|
-
if (!raw.trim()) return
|
|
10277
|
+
if (!raw.trim()) return await buildGitAutoRecap(paths);
|
|
10226
10278
|
const lines = raw.split("\n").filter(Boolean);
|
|
10227
10279
|
const obs = [];
|
|
10228
10280
|
for (const line of lines) {
|
|
@@ -10231,7 +10283,7 @@ async function buildAutoRecap(paths) {
|
|
|
10231
10283
|
} catch {
|
|
10232
10284
|
}
|
|
10233
10285
|
}
|
|
10234
|
-
if (obs.length === 0) return
|
|
10286
|
+
if (obs.length === 0) return await buildGitAutoRecap(paths);
|
|
10235
10287
|
const toolCounts = /* @__PURE__ */ new Map();
|
|
10236
10288
|
const fileCounts = /* @__PURE__ */ new Map();
|
|
10237
10289
|
const summaries = [];
|
|
@@ -10245,13 +10297,48 @@ async function buildAutoRecap(paths) {
|
|
|
10245
10297
|
const goal = `Auto-captured session \u2014 ${obs.length} tool calls (${topTools})`;
|
|
10246
10298
|
const accomplished = summaries.length ? `Recent activity:
|
|
10247
10299
|
${summaries.join("\n")}` : `Activity captured but no parseable summaries.`;
|
|
10300
|
+
const failures = obs.filter((o) => o.failure_hint);
|
|
10301
|
+
const discoveriesParts = [];
|
|
10302
|
+
if (failures.length > 0) {
|
|
10303
|
+
discoveriesParts.push(
|
|
10304
|
+
`\u26A0\uFE0F ${failures.length} failure${failures.length === 1 ? "" : "s"} detected \u2014 call \`mem_tried\` for each unresolved one:`,
|
|
10305
|
+
...failures.slice(0, 8).map((o) => `- ${o.summary.slice(0, 180)}`)
|
|
10306
|
+
);
|
|
10307
|
+
}
|
|
10248
10308
|
return {
|
|
10249
10309
|
goal,
|
|
10250
10310
|
accomplished,
|
|
10311
|
+
...discoveriesParts.length > 0 ? { discoveries: discoveriesParts.join("\n") } : {},
|
|
10251
10312
|
files: topFiles.map(([f]) => f),
|
|
10252
10313
|
rawCount: obs.length
|
|
10253
10314
|
};
|
|
10254
10315
|
}
|
|
10316
|
+
async function buildGitAutoRecap(paths) {
|
|
10317
|
+
const changed = await runGit(paths.root, ["diff", "--name-only"]).catch(() => "");
|
|
10318
|
+
const staged = await runGit(paths.root, ["diff", "--cached", "--name-only"]).catch(() => "");
|
|
10319
|
+
const status = await runGit(paths.root, ["status", "--porcelain", "--untracked-files=all"]).catch(() => "");
|
|
10320
|
+
const files = Array.from(new Set(
|
|
10321
|
+
[
|
|
10322
|
+
...changed.split("\n"),
|
|
10323
|
+
...staged.split("\n"),
|
|
10324
|
+
...status.split("\n").map((line) => line.replace(/^[ MADRCU?!]{1,2}\s+/, ""))
|
|
10325
|
+
].map((s) => s.trim()).filter(Boolean).filter((file) => !file.startsWith(".ai/.runtime/") && !file.startsWith(".ai/.cache/"))
|
|
10326
|
+
)).sort();
|
|
10327
|
+
if (files.length === 0) return null;
|
|
10328
|
+
const diffStat = await runGit(paths.root, ["diff", "--stat"]).catch(() => "");
|
|
10329
|
+
return {
|
|
10330
|
+
goal: `Auto-captured session \u2014 ${files.length} changed file${files.length === 1 ? "" : "s"}`,
|
|
10331
|
+
accomplished: [
|
|
10332
|
+
"Detected local changes:",
|
|
10333
|
+
...files.slice(0, 12).map((file) => `- ${file}`),
|
|
10334
|
+
files.length > 12 ? `- ...and ${files.length - 12} more` : ""
|
|
10335
|
+
].filter(Boolean).join("\n"),
|
|
10336
|
+
discoveries: diffStat.trim() ? `Git diff summary:
|
|
10337
|
+
${diffStat.trim()}` : void 0,
|
|
10338
|
+
files,
|
|
10339
|
+
rawCount: files.length
|
|
10340
|
+
};
|
|
10341
|
+
}
|
|
10255
10342
|
function buildRecapBody(opts) {
|
|
10256
10343
|
const lines = [];
|
|
10257
10344
|
lines.push(`## Goal
|
|
@@ -10277,6 +10364,24 @@ ${opts.next}`);
|
|
|
10277
10364
|
}
|
|
10278
10365
|
return lines.join("\n");
|
|
10279
10366
|
}
|
|
10367
|
+
function runGit(cwd, args) {
|
|
10368
|
+
return new Promise((resolve, reject) => {
|
|
10369
|
+
const proc = spawn4("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
10370
|
+
let stdout = "";
|
|
10371
|
+
let stderr = "";
|
|
10372
|
+
proc.stdout.on("data", (chunk) => {
|
|
10373
|
+
stdout += chunk.toString();
|
|
10374
|
+
});
|
|
10375
|
+
proc.stderr.on("data", (chunk) => {
|
|
10376
|
+
stderr += chunk.toString();
|
|
10377
|
+
});
|
|
10378
|
+
proc.on("error", reject);
|
|
10379
|
+
proc.on("close", (code) => {
|
|
10380
|
+
if (code === 0) resolve(stdout);
|
|
10381
|
+
else reject(new Error(stderr || `git ${args.join(" ")} exited with code ${code}`));
|
|
10382
|
+
});
|
|
10383
|
+
});
|
|
10384
|
+
}
|
|
10280
10385
|
function recapTopic2(scope, module) {
|
|
10281
10386
|
return module ? `session-recap-${scope}-${module}` : `session-recap-${scope}`;
|
|
10282
10387
|
}
|
|
@@ -10315,6 +10420,7 @@ function registerSessionEnd(session2) {
|
|
|
10315
10420
|
if (!synth) return;
|
|
10316
10421
|
goal = goal ?? synth.goal;
|
|
10317
10422
|
accomplished = accomplished ?? synth.accomplished;
|
|
10423
|
+
opts.discoveries = opts.discoveries ?? synth.discoveries;
|
|
10318
10424
|
if (!resolvedFiles && synth.files.length) resolvedFiles = synth.files.join(",");
|
|
10319
10425
|
}
|
|
10320
10426
|
if (!goal || !accomplished) {
|
|
@@ -11627,7 +11733,8 @@ function registerDoctor(program2) {
|
|
|
11627
11733
|
applyContext: true,
|
|
11628
11734
|
applyCorpus: true,
|
|
11629
11735
|
applyCodeMap: true,
|
|
11630
|
-
applyCodeSearch: true
|
|
11736
|
+
applyCodeSearch: true,
|
|
11737
|
+
forceCodeMap: true
|
|
11631
11738
|
})
|
|
11632
11739
|
);
|
|
11633
11740
|
}
|
|
@@ -11813,14 +11920,14 @@ function registerDoctor(program2) {
|
|
|
11813
11920
|
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
|
|
11814
11921
|
});
|
|
11815
11922
|
}
|
|
11816
|
-
findings.push(...await collectInstallFindings(root, "0.9.
|
|
11923
|
+
findings.push(...await collectInstallFindings(root, "0.9.25"));
|
|
11817
11924
|
try {
|
|
11818
11925
|
const legacyRaw = execSync3("haive-mcp --version", {
|
|
11819
11926
|
encoding: "utf8",
|
|
11820
11927
|
timeout: 3e3,
|
|
11821
11928
|
stdio: ["ignore", "pipe", "ignore"]
|
|
11822
11929
|
}).trim();
|
|
11823
|
-
const cliVersion = "0.9.
|
|
11930
|
+
const cliVersion = "0.9.25";
|
|
11824
11931
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
11825
11932
|
findings.push({
|
|
11826
11933
|
severity: "warn",
|
|
@@ -12338,7 +12445,7 @@ function truncate3(text, max) {
|
|
|
12338
12445
|
}
|
|
12339
12446
|
|
|
12340
12447
|
// src/commands/precommit.ts
|
|
12341
|
-
import { spawn as
|
|
12448
|
+
import { spawn as spawn5 } from "child_process";
|
|
12342
12449
|
import "commander";
|
|
12343
12450
|
import {
|
|
12344
12451
|
findProjectRoot as findProjectRoot43,
|
|
@@ -12361,6 +12468,24 @@ function registerPrecommit(program2) {
|
|
|
12361
12468
|
try {
|
|
12362
12469
|
diff = await runCommand3("git", ["diff", "--cached"], root);
|
|
12363
12470
|
if (!diff.trim()) {
|
|
12471
|
+
if (opts.json) {
|
|
12472
|
+
console.log(JSON.stringify({
|
|
12473
|
+
should_block: false,
|
|
12474
|
+
summary: {
|
|
12475
|
+
anti_patterns: 0,
|
|
12476
|
+
blocking_warnings: 0,
|
|
12477
|
+
review_warnings: 0,
|
|
12478
|
+
info_warnings: 0,
|
|
12479
|
+
relevant_memories: 0,
|
|
12480
|
+
stale_anchors: 0
|
|
12481
|
+
},
|
|
12482
|
+
warnings: [],
|
|
12483
|
+
relevant_memories: [],
|
|
12484
|
+
stale_anchors: [],
|
|
12485
|
+
notice: "No staged changes \u2014 nothing to check."
|
|
12486
|
+
}, null, 2));
|
|
12487
|
+
return;
|
|
12488
|
+
}
|
|
12364
12489
|
ui.warn("No staged changes \u2014 nothing to check. Stage with `git add` first.");
|
|
12365
12490
|
return;
|
|
12366
12491
|
}
|
|
@@ -12447,7 +12572,7 @@ function printWarnings(title, warnings, tone) {
|
|
|
12447
12572
|
}
|
|
12448
12573
|
function runCommand3(cmd, args, cwd) {
|
|
12449
12574
|
return new Promise((resolve, reject) => {
|
|
12450
|
-
const proc =
|
|
12575
|
+
const proc = spawn5(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
12451
12576
|
let stdout = "";
|
|
12452
12577
|
let stderr = "";
|
|
12453
12578
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -12697,7 +12822,7 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
12697
12822
|
}
|
|
12698
12823
|
|
|
12699
12824
|
// src/commands/enforce.ts
|
|
12700
|
-
import { execFileSync as execFileSync2, spawn as
|
|
12825
|
+
import { execFileSync as execFileSync2, spawn as spawn6 } from "child_process";
|
|
12701
12826
|
import { existsSync as existsSync67 } from "fs";
|
|
12702
12827
|
import { chmod as chmod2, mkdir as mkdir19, readFile as readFile20, readdir as readdir6, rm as rm3, writeFile as writeFile31 } from "fs/promises";
|
|
12703
12828
|
import path49 from "path";
|
|
@@ -12801,6 +12926,7 @@ function registerEnforce(program2) {
|
|
|
12801
12926
|
await mkdir19(paths.runtimeDir, { recursive: true });
|
|
12802
12927
|
const sessionId = opts.sessionId ?? payload.session_id;
|
|
12803
12928
|
const task = opts.task ?? payload.prompt ?? "Start an AI coding session in this hAIve-initialized project.";
|
|
12929
|
+
await applyLightweightRepairs(root, paths);
|
|
12804
12930
|
const budget = resolveBriefingBudget3("quick", {
|
|
12805
12931
|
max_tokens: 2500,
|
|
12806
12932
|
max_memories: 5,
|
|
@@ -12862,7 +12988,27 @@ ${briefing.project_context.content.slice(0, 1800)}`);
|
|
|
12862
12988
|
if (!existsSync67(paths.haiveDir)) return;
|
|
12863
12989
|
if (!isWriteLikeTool(payload)) return;
|
|
12864
12990
|
const ok = await hasRecentBriefingMarker(paths, payload.session_id);
|
|
12865
|
-
if (ok)
|
|
12991
|
+
if (ok) {
|
|
12992
|
+
const targetFiles = extractToolPaths(payload, root);
|
|
12993
|
+
if (targetFiles.length === 0) return;
|
|
12994
|
+
const missing = await missingRequiredMemoriesForFiles(paths, targetFiles, payload.session_id);
|
|
12995
|
+
if (missing.length === 0) return;
|
|
12996
|
+
const ids = missing.slice(0, 6).map((memory2) => memory2.memory.frontmatter.id);
|
|
12997
|
+
console.error(
|
|
12998
|
+
[
|
|
12999
|
+
"hAIve enforcement blocked this action.",
|
|
13000
|
+
`Tool: ${payload.tool_name ?? "write tool"}`,
|
|
13001
|
+
`Files: ${targetFiles.slice(0, 6).join(", ")}`,
|
|
13002
|
+
"",
|
|
13003
|
+
"These files have required hAIve context that was not in the current briefing:",
|
|
13004
|
+
...ids.map((id) => ` - ${id}`),
|
|
13005
|
+
"",
|
|
13006
|
+
"Load the targeted briefing before editing:",
|
|
13007
|
+
` ${briefingCommandForFiles(targetFiles)}`
|
|
13008
|
+
].join("\n")
|
|
13009
|
+
);
|
|
13010
|
+
process.exit(2);
|
|
13011
|
+
}
|
|
12866
13012
|
const tool = payload.tool_name ?? "write tool";
|
|
12867
13013
|
console.error(
|
|
12868
13014
|
[
|
|
@@ -12903,7 +13049,7 @@ async function runWithEnforcement(command, args, opts) {
|
|
|
12903
13049
|
}
|
|
12904
13050
|
ui.info(`hAIve briefing marker created for wrapped agent session: ${sessionId}`);
|
|
12905
13051
|
ui.info(`Briefing written to ${path49.relative(root, briefingFile)} and exported as HAIVE_BRIEFING_FILE`);
|
|
12906
|
-
const child =
|
|
13052
|
+
const child = spawn6(command, args, {
|
|
12907
13053
|
cwd: root,
|
|
12908
13054
|
stdio: "inherit",
|
|
12909
13055
|
env: {
|
|
@@ -12925,6 +13071,7 @@ async function runWithEnforcement(command, args, opts) {
|
|
|
12925
13071
|
});
|
|
12926
13072
|
}
|
|
12927
13073
|
async function writeWrapperBriefing(paths, sessionId, task) {
|
|
13074
|
+
await applyLightweightRepairs(paths.root, paths);
|
|
12928
13075
|
const budget = resolveBriefingBudget3("quick", {
|
|
12929
13076
|
max_tokens: 2500,
|
|
12930
13077
|
max_memories: 5,
|
|
@@ -12979,6 +13126,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
12979
13126
|
const paths = resolveHaivePaths44(root);
|
|
12980
13127
|
const initialized = existsSync67(paths.haiveDir);
|
|
12981
13128
|
const config = initialized ? await loadConfig10(paths) : {};
|
|
13129
|
+
if (initialized) await applyLightweightRepairs(root, paths);
|
|
12982
13130
|
const mode = config.enforcement?.mode ?? "strict";
|
|
12983
13131
|
const findings = [];
|
|
12984
13132
|
if (!initialized) {
|
|
@@ -13007,7 +13155,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
13007
13155
|
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
13008
13156
|
});
|
|
13009
13157
|
}
|
|
13010
|
-
findings.push(...await inspectIntegrationVersions(root, "0.9.
|
|
13158
|
+
findings.push(...await inspectIntegrationVersions(root, "0.9.25"));
|
|
13011
13159
|
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
13012
13160
|
const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
|
|
13013
13161
|
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
|
|
@@ -13159,6 +13307,9 @@ async function verifyDecisionCoverage(paths, stage, sessionId) {
|
|
|
13159
13307
|
code: "decision-coverage-missing",
|
|
13160
13308
|
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(", ")}`,
|
|
13161
13309
|
fix: `Run \`haive briefing --files "${changedFiles.slice(0, 10).join(",")}" --task "..."\` before committing.`,
|
|
13310
|
+
reason: "Changed files overlap validated anchored policy memories that were not recorded in the latest briefing marker.",
|
|
13311
|
+
affected_files: changedFiles.slice(0, 10),
|
|
13312
|
+
memory_ids: missing.slice(0, 10).map((m) => m.frontmatter.id),
|
|
13162
13313
|
impact: Math.min(35, 10 + missing.length * 5)
|
|
13163
13314
|
}];
|
|
13164
13315
|
}
|
|
@@ -13464,8 +13615,21 @@ function printFindingGroup(title, findings, tone) {
|
|
|
13464
13615
|
function printFinding(finding, explain = false) {
|
|
13465
13616
|
const marker = finding.severity === "error" ? ui.red("\u2717") : finding.severity === "warn" ? ui.yellow("\u26A0") : finding.severity === "ok" ? ui.green("\u2713") : ui.dim("\u2022");
|
|
13466
13617
|
console.log(`${marker} ${finding.code}: ${finding.message}`);
|
|
13618
|
+
if (explain && finding.reason) console.log(ui.dim(` why: ${finding.reason}`));
|
|
13619
|
+
if (explain && finding.affected_files?.length) console.log(ui.dim(` files: ${finding.affected_files.join(", ")}`));
|
|
13620
|
+
if (explain && finding.memory_ids?.length) console.log(ui.dim(` memories: ${finding.memory_ids.join(", ")}`));
|
|
13467
13621
|
if (finding.fix) console.log(ui.dim(`${explain ? " repair: " : " fix: "}${finding.fix}`));
|
|
13468
13622
|
}
|
|
13623
|
+
async function applyLightweightRepairs(root, paths) {
|
|
13624
|
+
await applyAutopilotRepairs(root, paths, {
|
|
13625
|
+
applyConfig: false,
|
|
13626
|
+
applyContext: true,
|
|
13627
|
+
applyCorpus: true,
|
|
13628
|
+
applyCodeMap: false,
|
|
13629
|
+
applyCodeSearch: true
|
|
13630
|
+
}).catch(() => {
|
|
13631
|
+
});
|
|
13632
|
+
}
|
|
13469
13633
|
async function readHookPayload() {
|
|
13470
13634
|
const raw = await readStdin2(MAX_STDIN_BYTES2);
|
|
13471
13635
|
if (!raw.trim()) return {};
|
|
@@ -13489,6 +13653,51 @@ function isWriteLikeTool(payload) {
|
|
|
13489
13653
|
const command = String(payload.tool_input?.["command"] ?? "");
|
|
13490
13654
|
return /\b(rm|mv|cp|mkdir|touch|tee|sed|perl|python|node|npm|pnpm|yarn|git)\b/.test(command) || />{1,2}/.test(command);
|
|
13491
13655
|
}
|
|
13656
|
+
function extractToolPaths(payload, root) {
|
|
13657
|
+
const input = payload.tool_input ?? {};
|
|
13658
|
+
const values = [
|
|
13659
|
+
input["file_path"],
|
|
13660
|
+
input["path"],
|
|
13661
|
+
input["notebook_path"]
|
|
13662
|
+
];
|
|
13663
|
+
if (Array.isArray(input["file_paths"])) values.push(...input["file_paths"]);
|
|
13664
|
+
if (Array.isArray(input["files"])) values.push(...input["files"]);
|
|
13665
|
+
if (payload.tool_name === "MultiEdit" && Array.isArray(input["edits"])) {
|
|
13666
|
+
for (const edit of input["edits"]) {
|
|
13667
|
+
if (edit && typeof edit === "object" && "file_path" in edit) {
|
|
13668
|
+
values.push(edit.file_path);
|
|
13669
|
+
}
|
|
13670
|
+
}
|
|
13671
|
+
}
|
|
13672
|
+
const out = /* @__PURE__ */ new Set();
|
|
13673
|
+
for (const value of values) {
|
|
13674
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
13675
|
+
out.add(normalizeToolPath(value, root));
|
|
13676
|
+
}
|
|
13677
|
+
return [...out].filter(Boolean).sort();
|
|
13678
|
+
}
|
|
13679
|
+
function normalizeToolPath(file, root) {
|
|
13680
|
+
const normalized = file.replace(/\\/g, "/");
|
|
13681
|
+
if (!path49.isAbsolute(normalized)) return normalized.replace(/^\.\//, "");
|
|
13682
|
+
return path49.relative(root, normalized).replace(/\\/g, "/");
|
|
13683
|
+
}
|
|
13684
|
+
async function missingRequiredMemoriesForFiles(paths, files, sessionId) {
|
|
13685
|
+
if (!existsSync67(paths.memoriesDir)) return [];
|
|
13686
|
+
const marker = await readRecentBriefingMarker(paths, sessionId);
|
|
13687
|
+
const consulted = new Set(marker?.memory_ids ?? []);
|
|
13688
|
+
const policyTypes = /* @__PURE__ */ new Set(["decision", "gotcha", "architecture", "convention", "attempt"]);
|
|
13689
|
+
const all = await loadMemoriesFromDir36(paths.memoriesDir);
|
|
13690
|
+
return all.filter(({ memory: memory2 }) => {
|
|
13691
|
+
const fm = memory2.frontmatter;
|
|
13692
|
+
if (!policyTypes.has(fm.type)) return false;
|
|
13693
|
+
if (fm.status !== "validated") return false;
|
|
13694
|
+
if (consulted.has(fm.id)) return false;
|
|
13695
|
+
return memoryMatchesAnchorPaths6(memory2, files);
|
|
13696
|
+
}).map(({ memory: memory2, filePath }) => ({ memory: memory2, filePath }));
|
|
13697
|
+
}
|
|
13698
|
+
function briefingCommandForFiles(files) {
|
|
13699
|
+
return `haive briefing --files "${files.slice(0, 10).join(",")}" --task "edit ${files.slice(0, 3).join(", ")}"`;
|
|
13700
|
+
}
|
|
13492
13701
|
async function readStdin2(maxBytes) {
|
|
13493
13702
|
if (process.stdin.isTTY) return "";
|
|
13494
13703
|
return await new Promise((resolve) => {
|
|
@@ -13516,7 +13725,7 @@ async function readStdin2(maxBytes) {
|
|
|
13516
13725
|
}
|
|
13517
13726
|
function runCommand4(cmd, args, cwd) {
|
|
13518
13727
|
return new Promise((resolve, reject) => {
|
|
13519
|
-
const proc =
|
|
13728
|
+
const proc = spawn6(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
13520
13729
|
let stdout = "";
|
|
13521
13730
|
let stderr = "";
|
|
13522
13731
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -13547,7 +13756,7 @@ function registerRun(program2) {
|
|
|
13547
13756
|
|
|
13548
13757
|
// src/index.ts
|
|
13549
13758
|
var program = new Command51();
|
|
13550
|
-
program.name("haive").description("hAIve \u2014 the memory and enforcement layer of your agent harness").version("0.9.
|
|
13759
|
+
program.name("haive").description("hAIve \u2014 the memory and enforcement layer of your agent harness").version("0.9.25").option("--advanced", "show maintenance and experimental commands in help");
|
|
13551
13760
|
registerInit(program);
|
|
13552
13761
|
registerWelcome(program);
|
|
13553
13762
|
registerResolveProject(program);
|