@hiveai/cli 0.20.1 → 0.23.0
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 +313 -125
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -208,7 +208,7 @@ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
|
|
|
208
208
|
if (!f) continue;
|
|
209
209
|
counts.set(f, (counts.get(f) ?? 0) + 1);
|
|
210
210
|
}
|
|
211
|
-
let entries = [...counts.entries()].map(([
|
|
211
|
+
let entries = [...counts.entries()].map(([path61, changes]) => ({ path: path61, changes }));
|
|
212
212
|
const lowerPaths = filePaths.map((p) => p.toLowerCase());
|
|
213
213
|
if (lowerPaths.length > 0) {
|
|
214
214
|
entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
|
|
@@ -834,7 +834,7 @@ var TokenBudgetWriter = class {
|
|
|
834
834
|
function registerBriefing(program2) {
|
|
835
835
|
program2.command("briefing").description(
|
|
836
836
|
'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'
|
|
837
|
-
).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(
|
|
837
|
+
).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("--json", "emit the ranked briefing as JSON (memories + scores + priority), like the MCP get_briefing tool", false).option(
|
|
838
838
|
"--budget <preset>",
|
|
839
839
|
"align with MCP get_briefing budget_preset: quick | balanced | deep \u2014 sets cap + truncation budget (overrides --max-memories / replaces default open-ended output)",
|
|
840
840
|
void 0
|
|
@@ -897,8 +897,10 @@ function registerBriefing(program2) {
|
|
|
897
897
|
budgetTokensCap = presetNums.max_tokens;
|
|
898
898
|
maxMemories = presetNums.max_memories;
|
|
899
899
|
}
|
|
900
|
+
const json = opts.json === true;
|
|
900
901
|
const writer = budgetTokensCap !== null ? new TokenBudgetWriter(budgetTokensCap * CHARS_PER_TOKEN) : null;
|
|
901
902
|
const out = (text) => {
|
|
903
|
+
if (json) return true;
|
|
902
904
|
if (writer) return writer.write(text);
|
|
903
905
|
console.log(text);
|
|
904
906
|
return true;
|
|
@@ -1015,6 +1017,10 @@ function registerBriefing(program2) {
|
|
|
1015
1017
|
scored.sort((a, b) => b.score - a.score);
|
|
1016
1018
|
const top = scored.slice(0, maxMemories);
|
|
1017
1019
|
if (top.length === 0) {
|
|
1020
|
+
if (json) {
|
|
1021
|
+
console.log(JSON.stringify({ task: opts.task ?? null, memories: [], briefing_quality: "thin" }, null, 2));
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1018
1024
|
ui.info("No relevant memories found.");
|
|
1019
1025
|
const draftCount = all.filter(
|
|
1020
1026
|
(m) => m.memory.frontmatter.status === "draft" && (scopeFilter === "all" || m.memory.frontmatter.scope === scopeFilter)
|
|
@@ -1046,6 +1052,26 @@ function registerBriefing(program2) {
|
|
|
1046
1052
|
const usefulCount = priorities.filter((p) => p === "useful").length;
|
|
1047
1053
|
const backgroundCount = priorities.filter((p) => p === "background").length;
|
|
1048
1054
|
const quality = mustReadCount > 0 || usefulCount > 0 ? backgroundCount > mustReadCount + usefulCount && backgroundCount > 2 ? "noisy" : "strong" : "thin";
|
|
1055
|
+
if (json) {
|
|
1056
|
+
console.log(JSON.stringify({
|
|
1057
|
+
task: opts.task ?? null,
|
|
1058
|
+
files: filePaths,
|
|
1059
|
+
briefing_quality: quality,
|
|
1060
|
+
counts: { must_read: mustReadCount, useful: usefulCount, background: backgroundCount },
|
|
1061
|
+
recap_id: recaps[0]?.memory.frontmatter.id ?? null,
|
|
1062
|
+
memories: top.map((item, i) => ({
|
|
1063
|
+
id: item.memory.frontmatter.id,
|
|
1064
|
+
scope: item.memory.frontmatter.scope,
|
|
1065
|
+
type: item.memory.frontmatter.type,
|
|
1066
|
+
status: item.memory.frontmatter.status,
|
|
1067
|
+
priority: priorities[i],
|
|
1068
|
+
score: item.score,
|
|
1069
|
+
file: path3.relative(root, item.filePath),
|
|
1070
|
+
summary: (item.memory.body.split("\n").map((l) => l.replace(/^#+\s*/, "").trim()).find((l) => l.length > 0) ?? "").slice(0, 140)
|
|
1071
|
+
}))
|
|
1072
|
+
}, null, 2));
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1049
1075
|
out(ui.dim(`briefing_quality: ${quality} \xB7 must_read=${mustReadCount} useful=${usefulCount} background=${backgroundCount}`));
|
|
1050
1076
|
out("");
|
|
1051
1077
|
for (const [idx, item] of top.entries()) {
|
|
@@ -3339,7 +3365,13 @@ async function seedStackPack(haivePaths, stack) {
|
|
|
3339
3365
|
});
|
|
3340
3366
|
const filePath = memoryFilePath(haivePaths, "team", fm.id);
|
|
3341
3367
|
if (existsSync10(filePath)) continue;
|
|
3342
|
-
const
|
|
3368
|
+
const ruleSlug = combinedSlug.startsWith(`${stack}-`) ? combinedSlug.slice(stack.length + 1) : combinedSlug;
|
|
3369
|
+
const titleCase = (s) => s.split("-").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
3370
|
+
const heading = `${titleCase(stack)}: ${titleCase(ruleSlug)}`;
|
|
3371
|
+
const titledBody = /^#{1,3}\s+\S/m.test(mem.body.trim()) ? mem.body : `# ${heading}
|
|
3372
|
+
|
|
3373
|
+
${mem.body}`;
|
|
3374
|
+
const content = serializeMemory2({ frontmatter: fm, body: `${titledBody}
|
|
3343
3375
|
|
|
3344
3376
|
${SEED_FOOTER(stack)}` });
|
|
3345
3377
|
await mkdir5(path10.dirname(filePath), { recursive: true });
|
|
@@ -3354,7 +3386,7 @@ ${SEED_FOOTER(stack)}` });
|
|
|
3354
3386
|
|
|
3355
3387
|
// src/commands/init.ts
|
|
3356
3388
|
var execFileAsync = promisify2(execFile2);
|
|
3357
|
-
var HAIVE_GITHUB_ACTION_REF = `v${"0.
|
|
3389
|
+
var HAIVE_GITHUB_ACTION_REF = `v${"0.23.0"}`;
|
|
3358
3390
|
var PROJECT_CONTEXT_TEMPLATE = `# Project context
|
|
3359
3391
|
|
|
3360
3392
|
> Generated by \`haive init\`. Run \`haive init --bootstrap\` to auto-fill from your codebase,
|
|
@@ -4710,7 +4742,6 @@ import { z as z25 } from "zod";
|
|
|
4710
4742
|
import { existsSync as existsSync25 } from "fs";
|
|
4711
4743
|
import {
|
|
4712
4744
|
addedLinesFromDiff,
|
|
4713
|
-
appendPreventionEvent,
|
|
4714
4745
|
BRIDGE_TARGET_PATH,
|
|
4715
4746
|
buildDocFrequency,
|
|
4716
4747
|
CODE_STOPWORDS,
|
|
@@ -4722,9 +4753,8 @@ import {
|
|
|
4722
4753
|
loadUsageIndex as loadUsageIndex10,
|
|
4723
4754
|
literalMatchesAnyToken as literalMatchesAnyToken3,
|
|
4724
4755
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths4,
|
|
4725
|
-
|
|
4756
|
+
recordPreventionHits,
|
|
4726
4757
|
runSensors,
|
|
4727
|
-
saveUsageIndex as saveUsageIndex4,
|
|
4728
4758
|
sensorTargetsFromDiff,
|
|
4729
4759
|
tokenizeQuery as tokenizeQuery3
|
|
4730
4760
|
} from "@hiveai/core";
|
|
@@ -4771,7 +4801,8 @@ import { existsSync as existsSync30 } from "fs";
|
|
|
4771
4801
|
import {
|
|
4772
4802
|
findLexicalConflictPairs,
|
|
4773
4803
|
findTopicStatusConflictPairs,
|
|
4774
|
-
loadMemoriesFromDir as loadMemoriesFromDir23
|
|
4804
|
+
loadMemoriesFromDir as loadMemoriesFromDir23,
|
|
4805
|
+
planConflictResolution
|
|
4775
4806
|
} from "@hiveai/core";
|
|
4776
4807
|
import { z as z32 } from "zod";
|
|
4777
4808
|
import { resolveProjectInfo } from "@hiveai/core";
|
|
@@ -5775,7 +5806,15 @@ async function memTried(input, ctx) {
|
|
|
5775
5806
|
throw new Error(`Memory already exists at ${file}`);
|
|
5776
5807
|
}
|
|
5777
5808
|
await writeFile82(file, serializeMemory7({ frontmatter, body }), "utf8");
|
|
5778
|
-
|
|
5809
|
+
const sensorGenerated = Boolean(sensor);
|
|
5810
|
+
const hint = sensorGenerated ? void 0 : input.paths.length === 0 ? "No sensor was generated (no `paths` given), so this lesson is feedforward-only \u2014 it will be briefed but the gate cannot block the repeat. Re-run with `paths` set to the file(s) where the mistake lives to close the loop." : "No sensor could be derived from the wording (no distinctive code token). The lesson is briefed but not enforced; add a concrete forbidden token/value, or attach a sensor manually, to make the gate block the repeat.";
|
|
5811
|
+
return {
|
|
5812
|
+
id: frontmatter.id,
|
|
5813
|
+
scope: frontmatter.scope,
|
|
5814
|
+
file_path: file,
|
|
5815
|
+
sensor_generated: sensorGenerated,
|
|
5816
|
+
...hint ? { hint } : {}
|
|
5817
|
+
};
|
|
5779
5818
|
}
|
|
5780
5819
|
var IngestFindingsInputSchema = {
|
|
5781
5820
|
format: z16.enum(["sarif", "sonar"]).describe("Report format: 'sarif' (ESLint/Semgrep/CodeQL) or 'sonar' (SonarQube issues JSON)"),
|
|
@@ -7326,19 +7365,7 @@ async function antiPatternsCheck(input, ctx) {
|
|
|
7326
7365
|
const strongCatches = warnings.filter(
|
|
7327
7366
|
(w) => w.reasons.includes("sensor") || w.reasons.includes("anchor") && w.reasons.includes("literal")
|
|
7328
7367
|
);
|
|
7329
|
-
|
|
7330
|
-
const recordedIds = [];
|
|
7331
|
-
for (const w of strongCatches) if (recordPrevention(usage, w.id)) recordedIds.push(w.id);
|
|
7332
|
-
if (recordedIds.length > 0) {
|
|
7333
|
-
await saveUsageIndex4(ctx.paths, usage).catch(() => {
|
|
7334
|
-
});
|
|
7335
|
-
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
7336
|
-
for (const id of recordedIds) {
|
|
7337
|
-
await appendPreventionEvent(ctx.paths, { at, id, source: "anti-pattern" }).catch(() => {
|
|
7338
|
-
});
|
|
7339
|
-
}
|
|
7340
|
-
}
|
|
7341
|
-
}
|
|
7368
|
+
await recordPreventionHits(ctx.paths, strongCatches.map((w) => w.id), "anti-pattern");
|
|
7342
7369
|
return {
|
|
7343
7370
|
scanned: negative.length,
|
|
7344
7371
|
warnings
|
|
@@ -8256,6 +8283,18 @@ function gitFileDiff(root, file, sinceDays) {
|
|
|
8256
8283
|
return null;
|
|
8257
8284
|
}
|
|
8258
8285
|
}
|
|
8286
|
+
function suggestResolution(byId, idA, idB) {
|
|
8287
|
+
const a = byId.get(idA);
|
|
8288
|
+
const b = byId.get(idB);
|
|
8289
|
+
if (!a || !b) return null;
|
|
8290
|
+
const plan = planConflictResolution(a, b);
|
|
8291
|
+
return {
|
|
8292
|
+
keep_id: plan.keep_id,
|
|
8293
|
+
supersede_id: plan.supersede_id,
|
|
8294
|
+
reason: plan.reason,
|
|
8295
|
+
command: `haive memory resolve-conflict ${plan.keep_id} ${plan.supersede_id} --yes`
|
|
8296
|
+
};
|
|
8297
|
+
}
|
|
8259
8298
|
var MemConflictCandidatesInputSchema = {
|
|
8260
8299
|
since_days: z32.number().int().positive().max(3650).default(365).describe("Only memories created since N days ago"),
|
|
8261
8300
|
types: z32.array(z32.enum(["decision", "architecture", "convention", "gotcha"])).default(["decision", "architecture"]).describe("Memory types scanned for pairwise lexical overlap"),
|
|
@@ -8277,6 +8316,7 @@ async function memConflictCandidates(input, ctx) {
|
|
|
8277
8316
|
};
|
|
8278
8317
|
}
|
|
8279
8318
|
const all = await loadMemoriesFromDir23(ctx.paths.memoriesDir);
|
|
8319
|
+
const byId = new Map(all.map((m) => [m.memory.frontmatter.id, m]));
|
|
8280
8320
|
const { pairs, scanned, truncated } = findLexicalConflictPairs(all, {
|
|
8281
8321
|
sinceDays: input.since_days,
|
|
8282
8322
|
types: input.types,
|
|
@@ -8285,8 +8325,22 @@ async function memConflictCandidates(input, ctx) {
|
|
|
8285
8325
|
maxScan: input.max_scan
|
|
8286
8326
|
});
|
|
8287
8327
|
const topicStatusPairs = findTopicStatusConflictPairs(all, input.max_topic_pairs);
|
|
8328
|
+
const enrichedPairs = pairs.map((p) => ({
|
|
8329
|
+
...p,
|
|
8330
|
+
suggested_resolution: suggestResolution(byId, p.id_a, p.id_b)
|
|
8331
|
+
}));
|
|
8332
|
+
const enrichedTopicStatusPairs = topicStatusPairs.map((p) => ({
|
|
8333
|
+
...p,
|
|
8334
|
+
suggested_resolution: suggestResolution(byId, p.id_a, p.id_b)
|
|
8335
|
+
}));
|
|
8288
8336
|
const notice = pairs.length === 0 && topicStatusPairs.length === 0 ? "No lexical or topic-status candidates \u2014 widen since_days/types or lower min_jaccard." : void 0;
|
|
8289
|
-
return {
|
|
8337
|
+
return {
|
|
8338
|
+
pairs: enrichedPairs,
|
|
8339
|
+
topic_status_pairs: enrichedTopicStatusPairs,
|
|
8340
|
+
scanned,
|
|
8341
|
+
truncated,
|
|
8342
|
+
notice
|
|
8343
|
+
};
|
|
8290
8344
|
}
|
|
8291
8345
|
var MemResolveProjectInputSchema = {
|
|
8292
8346
|
cwd: z33.string().optional().describe("Directory used for root discovery when HAIVE_PROJECT_ROOT is unset.")
|
|
@@ -8600,7 +8654,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
8600
8654
|
};
|
|
8601
8655
|
}
|
|
8602
8656
|
var SERVER_NAME = "haive";
|
|
8603
|
-
var SERVER_VERSION = "0.
|
|
8657
|
+
var SERVER_VERSION = "0.23.0";
|
|
8604
8658
|
function jsonResult(data) {
|
|
8605
8659
|
return {
|
|
8606
8660
|
content: [
|
|
@@ -11134,7 +11188,13 @@ function registerMemoryTried(memory2) {
|
|
|
11134
11188
|
await writeFile21(file, serializeMemory19({ frontmatter, body }), "utf8");
|
|
11135
11189
|
ui.success(`Recorded: ${path27.relative(root, file)}`);
|
|
11136
11190
|
ui.info(`id=${frontmatter.id} type=attempt status=validated (auto-approved)`);
|
|
11137
|
-
if (sensor)
|
|
11191
|
+
if (sensor) {
|
|
11192
|
+
ui.info(`sensor=regex warn autogen pattern=${JSON.stringify(sensor.pattern)}`);
|
|
11193
|
+
} else {
|
|
11194
|
+
ui.warn(
|
|
11195
|
+
frontmatter.anchor.paths.length === 0 ? "No sensor generated (no --paths) \u2014 lesson is briefed but NOT enforced. Add --paths to close the loop." : "No sensor could be derived from the wording \u2014 lesson is briefed but NOT enforced. Use a concrete forbidden token to enable a gate block."
|
|
11196
|
+
);
|
|
11197
|
+
}
|
|
11138
11198
|
});
|
|
11139
11199
|
}
|
|
11140
11200
|
function parseCsv4(value) {
|
|
@@ -11394,7 +11454,7 @@ import {
|
|
|
11394
11454
|
loadUsageIndex as loadUsageIndex18,
|
|
11395
11455
|
recordRejection as recordRejection3,
|
|
11396
11456
|
resolveHaivePaths as resolveHaivePaths23,
|
|
11397
|
-
saveUsageIndex as
|
|
11457
|
+
saveUsageIndex as saveUsageIndex4,
|
|
11398
11458
|
serializeMemory as serializeMemory20
|
|
11399
11459
|
} from "@hiveai/core";
|
|
11400
11460
|
function registerMemoryReject(memory2) {
|
|
@@ -11427,7 +11487,7 @@ function registerMemoryReject(memory2) {
|
|
|
11427
11487
|
);
|
|
11428
11488
|
const idx = await loadUsageIndex18(paths);
|
|
11429
11489
|
recordRejection3(idx, id, opts.reason ?? null);
|
|
11430
|
-
await
|
|
11490
|
+
await saveUsageIndex4(paths, idx);
|
|
11431
11491
|
const u = idx.by_id[id];
|
|
11432
11492
|
ui.success(
|
|
11433
11493
|
`Rejected ${id} (status=rejected, ${u.rejected_count} rejection${u.rejected_count === 1 ? "" : "s"})`
|
|
@@ -11446,7 +11506,7 @@ import {
|
|
|
11446
11506
|
findProjectRoot as findProjectRoot27,
|
|
11447
11507
|
loadUsageIndex as loadUsageIndex19,
|
|
11448
11508
|
resolveHaivePaths as resolveHaivePaths24,
|
|
11449
|
-
saveUsageIndex as
|
|
11509
|
+
saveUsageIndex as saveUsageIndex5
|
|
11450
11510
|
} from "@hiveai/core";
|
|
11451
11511
|
function registerMemoryRm(memory2) {
|
|
11452
11512
|
memory2.command("delete <id>").alias("rm").description("Delete a memory file (and its usage entry by default). Mirrors MCP mem_delete. Alias: rm").option("-y, --yes", "skip the confirmation prompt").option("--keep-usage", "do not remove the usage.json entry").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
@@ -11480,7 +11540,7 @@ function registerMemoryRm(memory2) {
|
|
|
11480
11540
|
const idx = await loadUsageIndex19(paths);
|
|
11481
11541
|
if (idx.by_id[id]) {
|
|
11482
11542
|
delete idx.by_id[id];
|
|
11483
|
-
await
|
|
11543
|
+
await saveUsageIndex5(paths, idx);
|
|
11484
11544
|
ui.info("Removed usage entry");
|
|
11485
11545
|
}
|
|
11486
11546
|
}
|
|
@@ -11693,7 +11753,7 @@ import {
|
|
|
11693
11753
|
recordRejection as recordRejection4,
|
|
11694
11754
|
recommendFeedbackAdjustment as recommendFeedbackAdjustment2,
|
|
11695
11755
|
resolveHaivePaths as resolveHaivePaths28,
|
|
11696
|
-
saveUsageIndex as
|
|
11756
|
+
saveUsageIndex as saveUsageIndex6,
|
|
11697
11757
|
serializeMemory as serializeMemory21
|
|
11698
11758
|
} from "@hiveai/core";
|
|
11699
11759
|
function registerMemoryFeedback(memory2) {
|
|
@@ -11723,7 +11783,7 @@ function registerMemoryFeedback(memory2) {
|
|
|
11723
11783
|
const outcome = opts.applied ? "applied" : "rejected";
|
|
11724
11784
|
if (opts.applied) recordApplied2(index, id);
|
|
11725
11785
|
else recordRejection4(index, id, opts.reason ?? null);
|
|
11726
|
-
await
|
|
11786
|
+
await saveUsageIndex6(paths, index);
|
|
11727
11787
|
const usage = getUsage19(index, id);
|
|
11728
11788
|
const adjustment = opts.rejected ? recommendFeedbackAdjustment2(target.memory.frontmatter, usage) : { action: "none", reason: "No automatic adjustment needed." };
|
|
11729
11789
|
const adjustedFrontmatter = applyFeedbackAdjustment2(target.memory.frontmatter, adjustment);
|
|
@@ -13554,6 +13614,7 @@ function registerEval(program2) {
|
|
|
13554
13614
|
root,
|
|
13555
13615
|
k,
|
|
13556
13616
|
spec_source: resolvedSpec.source,
|
|
13617
|
+
provenance: { synthesized: resolvedSpec.synthesized, authored: resolvedSpec.authored },
|
|
13557
13618
|
report,
|
|
13558
13619
|
gate_precision: gatePrecision,
|
|
13559
13620
|
...delta ? { delta } : {},
|
|
@@ -13562,13 +13623,18 @@ function registerEval(program2) {
|
|
|
13562
13623
|
applyExitGates(opts, report, delta, gatePrecision, gateDelta);
|
|
13563
13624
|
return;
|
|
13564
13625
|
}
|
|
13626
|
+
if (resolvedSpec.authored === 0 && resolvedSpec.synthesized > 0) {
|
|
13627
|
+
ui.warn(
|
|
13628
|
+
`All ${resolvedSpec.synthesized} case(s) are self-synthesized from your own memories (self-referential). Add hand-labeled cases in .ai/eval/spec.json, or run \`haive eval --spec <file>\`, for an independent score.`
|
|
13629
|
+
);
|
|
13630
|
+
}
|
|
13565
13631
|
if (delta) {
|
|
13566
13632
|
console.log(renderDelta(delta));
|
|
13567
13633
|
}
|
|
13568
13634
|
if (gateDelta) {
|
|
13569
13635
|
console.log(renderGateDelta(gateDelta));
|
|
13570
13636
|
}
|
|
13571
|
-
const md = renderMarkdown2(root, k, resolvedSpec
|
|
13637
|
+
const md = renderMarkdown2(root, k, resolvedSpec, report, gatePrecision);
|
|
13572
13638
|
if (opts.out) {
|
|
13573
13639
|
const outFile = path44.isAbsolute(opts.out) ? opts.out : path44.join(root, opts.out);
|
|
13574
13640
|
await writeFile33(outFile, md, "utf8");
|
|
@@ -13651,30 +13717,39 @@ function renderGateDelta(delta) {
|
|
|
13651
13717
|
lines.push(` rejections ${delta.rejections.baseline ?? "n/a"} \u2192 ${delta.rejections.current ?? "n/a"} ${ui.dim(`(${rejectionDelta})`)}`);
|
|
13652
13718
|
return lines.join("\n");
|
|
13653
13719
|
}
|
|
13720
|
+
function countCases(spec) {
|
|
13721
|
+
return (spec.retrieval?.length ?? 0) + (spec.sensors?.length ?? 0);
|
|
13722
|
+
}
|
|
13654
13723
|
async function resolveSpec(opts, root, memoriesDir) {
|
|
13655
13724
|
if (opts.spec) {
|
|
13656
13725
|
const file = path44.resolve(opts.spec);
|
|
13657
13726
|
const raw = await readFile21(file, "utf8");
|
|
13658
|
-
|
|
13727
|
+
const spec = JSON.parse(raw);
|
|
13728
|
+
return { spec, source: file, synthesized: 0, authored: countCases(spec) };
|
|
13659
13729
|
}
|
|
13660
13730
|
const defaultSpec = path44.join(root, ".ai", "eval", "spec.json");
|
|
13661
13731
|
if (existsSync65(defaultSpec)) {
|
|
13662
13732
|
const raw = await readFile21(defaultSpec, "utf8");
|
|
13663
13733
|
const explicit = JSON.parse(raw);
|
|
13664
13734
|
const memories2 = await loadMemoriesFromDir27(memoriesDir);
|
|
13665
|
-
const
|
|
13735
|
+
const synthesized2 = synthesizeSelfEvalCases(memories2, { includeFiles: !opts.semanticOnly });
|
|
13666
13736
|
return {
|
|
13667
13737
|
spec: {
|
|
13668
|
-
retrieval: [...
|
|
13738
|
+
retrieval: [...synthesized2, ...explicit.retrieval ?? []],
|
|
13669
13739
|
sensors: explicit.sensors ?? []
|
|
13670
13740
|
},
|
|
13671
|
-
source: ".ai/eval/spec.json + synthesized anchored retrieval"
|
|
13741
|
+
source: ".ai/eval/spec.json + synthesized anchored retrieval",
|
|
13742
|
+
synthesized: synthesized2.length,
|
|
13743
|
+
authored: countCases(explicit)
|
|
13672
13744
|
};
|
|
13673
13745
|
}
|
|
13674
13746
|
const memories = await loadMemoriesFromDir27(memoriesDir);
|
|
13747
|
+
const synthesized = synthesizeSelfEvalCases(memories, { includeFiles: !opts.semanticOnly });
|
|
13675
13748
|
return {
|
|
13676
|
-
spec: { retrieval:
|
|
13677
|
-
source: "synthesized anchored retrieval"
|
|
13749
|
+
spec: { retrieval: synthesized },
|
|
13750
|
+
source: "synthesized anchored retrieval",
|
|
13751
|
+
synthesized: synthesized.length,
|
|
13752
|
+
authored: 0
|
|
13678
13753
|
};
|
|
13679
13754
|
}
|
|
13680
13755
|
async function runRetrieval(c, k, ctx) {
|
|
@@ -13707,12 +13782,14 @@ async function runSensorCase(c, ctx) {
|
|
|
13707
13782
|
function pct(n) {
|
|
13708
13783
|
return `${Math.round(n * 100)}%`;
|
|
13709
13784
|
}
|
|
13710
|
-
function renderMarkdown2(root, k,
|
|
13785
|
+
function renderMarkdown2(root, k, resolved, report, gatePrecision) {
|
|
13786
|
+
const provenance = resolved.authored === 0 ? `${resolved.synthesized} synthesized (self-referential \u2014 sanity floor, not ground truth)` : resolved.synthesized === 0 ? `${resolved.authored} authored (independent ground truth)` : `${resolved.authored} authored (independent) + ${resolved.synthesized} synthesized (self-referential)`;
|
|
13711
13787
|
const lines = [
|
|
13712
13788
|
"# hAIve eval report",
|
|
13713
13789
|
"",
|
|
13714
13790
|
`Project: \`${root}\` \xB7 top-k: ${k}`,
|
|
13715
|
-
`Spec: ${source}`,
|
|
13791
|
+
`Spec: ${resolved.source}`,
|
|
13792
|
+
`Cases: ${provenance}`,
|
|
13716
13793
|
"",
|
|
13717
13794
|
`## Overall score: ${report.score}/100`,
|
|
13718
13795
|
""
|
|
@@ -14153,8 +14230,8 @@ function registerDoctor(program2) {
|
|
|
14153
14230
|
fix: "haive init"
|
|
14154
14231
|
});
|
|
14155
14232
|
} else {
|
|
14156
|
-
const { readFile:
|
|
14157
|
-
const content = await
|
|
14233
|
+
const { readFile: readFile29 } = await import("fs/promises");
|
|
14234
|
+
const content = await readFile29(paths.projectContext, "utf8");
|
|
14158
14235
|
const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
|
|
14159
14236
|
if (isTemplate) {
|
|
14160
14237
|
findings.push({
|
|
@@ -14328,8 +14405,8 @@ function registerDoctor(program2) {
|
|
|
14328
14405
|
let hasClaudeEnforcement = false;
|
|
14329
14406
|
if (existsSync68(claudeSettings)) {
|
|
14330
14407
|
try {
|
|
14331
|
-
const { readFile:
|
|
14332
|
-
const raw = await
|
|
14408
|
+
const { readFile: readFile29 } = await import("fs/promises");
|
|
14409
|
+
const raw = await readFile29(claudeSettings, "utf8");
|
|
14333
14410
|
hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
|
|
14334
14411
|
} catch {
|
|
14335
14412
|
hasClaudeEnforcement = false;
|
|
@@ -14352,7 +14429,7 @@ function registerDoctor(program2) {
|
|
|
14352
14429
|
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
|
|
14353
14430
|
});
|
|
14354
14431
|
}
|
|
14355
|
-
findings.push(...await collectInstallFindings(root, "0.
|
|
14432
|
+
findings.push(...await collectInstallFindings(root, "0.23.0"));
|
|
14356
14433
|
findings.push(...await collectToolchainFindings(root));
|
|
14357
14434
|
try {
|
|
14358
14435
|
const legacyRaw = execSync3("haive-mcp --version", {
|
|
@@ -14360,7 +14437,7 @@ function registerDoctor(program2) {
|
|
|
14360
14437
|
timeout: 3e3,
|
|
14361
14438
|
stdio: ["ignore", "pipe", "ignore"]
|
|
14362
14439
|
}).trim();
|
|
14363
|
-
const cliVersion = "0.
|
|
14440
|
+
const cliVersion = "0.23.0";
|
|
14364
14441
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
14365
14442
|
findings.push({
|
|
14366
14443
|
severity: "warn",
|
|
@@ -15433,10 +15510,11 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
15433
15510
|
}
|
|
15434
15511
|
|
|
15435
15512
|
// src/commands/enforce.ts
|
|
15436
|
-
import { execFileSync as execFileSync2, spawn as spawn6 } from "child_process";
|
|
15513
|
+
import { execFile as execFile3, execFileSync as execFileSync2, spawn as spawn6 } from "child_process";
|
|
15437
15514
|
import { existsSync as existsSync75, statSync as statSync3 } from "fs";
|
|
15438
15515
|
import { chmod as chmod2, mkdir as mkdir21, readFile as readFile24, readdir as readdir6, rm as rm3, writeFile as writeFile37 } from "fs/promises";
|
|
15439
15516
|
import path53 from "path";
|
|
15517
|
+
import { promisify as promisify3 } from "util";
|
|
15440
15518
|
import "commander";
|
|
15441
15519
|
import {
|
|
15442
15520
|
antiPatternGateParams as antiPatternGateParams2,
|
|
@@ -15450,15 +15528,18 @@ import {
|
|
|
15450
15528
|
loadMemoriesFromDir as loadMemoriesFromDir38,
|
|
15451
15529
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths6,
|
|
15452
15530
|
readRecentBriefingMarker,
|
|
15531
|
+
recordPreventionHits as recordPreventionHits2,
|
|
15453
15532
|
resolveBriefingBudget as resolveBriefingBudget3,
|
|
15454
15533
|
resolveHaivePaths as resolveHaivePaths48,
|
|
15455
15534
|
runSensors as runSensors2,
|
|
15456
15535
|
saveConfig as saveConfig4,
|
|
15536
|
+
selectCommandSensors,
|
|
15457
15537
|
sensorTargetsFromDiff as sensorTargetsFromDiff2,
|
|
15458
15538
|
SESSION_RECAP_TTL_MS,
|
|
15459
15539
|
verifyAnchor as verifyAnchor4,
|
|
15460
15540
|
writeBriefingMarker as writeBriefingMarker3
|
|
15461
15541
|
} from "@hiveai/core";
|
|
15542
|
+
var execFileAsync2 = promisify3(execFile3);
|
|
15462
15543
|
var MAX_STDIN_BYTES2 = 256 * 1024;
|
|
15463
15544
|
var ENFORCE_HOOK_MARKER = "# hAIve enforcement hook";
|
|
15464
15545
|
function registerEnforce(program2) {
|
|
@@ -16060,7 +16141,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
16060
16141
|
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
16061
16142
|
});
|
|
16062
16143
|
}
|
|
16063
|
-
findings.push(...await inspectIntegrationVersions(root, "0.
|
|
16144
|
+
findings.push(...await inspectIntegrationVersions(root, "0.23.0"));
|
|
16064
16145
|
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
16065
16146
|
const hasBriefing = await hasRecentBriefingMarker2(paths, sessionId);
|
|
16066
16147
|
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
|
|
@@ -16221,6 +16302,26 @@ async function verifyDecisionCoverage(paths, stage, sessionId) {
|
|
|
16221
16302
|
message: `Relevant decisions/policies were surfaced for ${changedFiles.length} changed file(s): ${relevant.length}/${relevant.length}.`
|
|
16222
16303
|
}];
|
|
16223
16304
|
}
|
|
16305
|
+
if (stage === "pre-commit" || stage === "pre-push") {
|
|
16306
|
+
const cfg = await loadConfig14(paths).catch(() => ({}));
|
|
16307
|
+
if (cfg.enforcement?.autoBrief !== false) {
|
|
16308
|
+
await writeBriefingMarker3(paths, {
|
|
16309
|
+
sessionId,
|
|
16310
|
+
source: "haive-autobrief",
|
|
16311
|
+
task: "decision-coverage auto-surfaced at commit",
|
|
16312
|
+
memoryIds: relevant.map(({ memory: memory2 }) => memory2.frontmatter.id),
|
|
16313
|
+
files: changedFiles
|
|
16314
|
+
}).catch(() => {
|
|
16315
|
+
});
|
|
16316
|
+
return [{
|
|
16317
|
+
severity: "ok",
|
|
16318
|
+
code: "decision-coverage-autosurfaced",
|
|
16319
|
+
message: `Surfaced ${relevant.length} relevant decision/policy memor${relevant.length === 1 ? "y" : "ies"} for ${changedFiles.length} changed file(s) at commit time` + (missing.length > 0 ? ` (${missing.length} not previously briefed \u2014 now recorded)` : "") + ". Set enforcement.autoBrief=false to require a manual briefing first.",
|
|
16320
|
+
memory_ids: relevant.slice(0, 12).map(({ memory: memory2 }) => memory2.frontmatter.id),
|
|
16321
|
+
affected_files: changedFiles.slice(0, 10)
|
|
16322
|
+
}];
|
|
16323
|
+
}
|
|
16324
|
+
}
|
|
16224
16325
|
return [{
|
|
16225
16326
|
severity: stage === "local" ? "warn" : "error",
|
|
16226
16327
|
code: "decision-coverage-missing",
|
|
@@ -16292,19 +16393,19 @@ async function runSensorGate(paths, diff) {
|
|
|
16292
16393
|
if (!diff || !existsSync75(paths.memoriesDir)) return [];
|
|
16293
16394
|
try {
|
|
16294
16395
|
const loaded = await loadMemoriesFromDir38(paths.memoriesDir);
|
|
16295
|
-
const
|
|
16296
|
-
|
|
16297
|
-
return Boolean(s) && s.kind === "regex" && !isRetiredMemory3(m.frontmatter, m.body);
|
|
16298
|
-
});
|
|
16299
|
-
if (sensorMemories.length === 0) return [];
|
|
16396
|
+
const scannable = loaded.map((l) => l.memory).filter((m) => Boolean(m.frontmatter.sensor) && !isRetiredMemory3(m.frontmatter, m.body));
|
|
16397
|
+
if (scannable.length === 0) return [];
|
|
16300
16398
|
const targets = sensorTargetsFromDiff2(diff).filter((t) => isSensorScannablePath(t.path));
|
|
16301
16399
|
if (targets.length === 0) return [];
|
|
16302
|
-
const hits = runSensors2(sensorMemories, targets);
|
|
16303
16400
|
const findings = [];
|
|
16304
16401
|
const seen = /* @__PURE__ */ new Set();
|
|
16402
|
+
const firedIds = /* @__PURE__ */ new Set();
|
|
16403
|
+
const regexSensorMemories = scannable.filter((m) => m.frontmatter.sensor.kind === "regex");
|
|
16404
|
+
const hits = regexSensorMemories.length > 0 ? runSensors2(regexSensorMemories, targets) : [];
|
|
16305
16405
|
for (const hit of hits) {
|
|
16306
16406
|
if (seen.has(hit.memory_id)) continue;
|
|
16307
16407
|
seen.add(hit.memory_id);
|
|
16408
|
+
firedIds.add(hit.memory_id);
|
|
16308
16409
|
const where = hit.file ? ` (${hit.file})` : "";
|
|
16309
16410
|
if (hit.severity === "block") {
|
|
16310
16411
|
findings.push({
|
|
@@ -16324,11 +16425,51 @@ async function runSensorGate(paths, diff) {
|
|
|
16324
16425
|
});
|
|
16325
16426
|
}
|
|
16326
16427
|
}
|
|
16428
|
+
const config = await loadConfig14(paths).catch(() => null);
|
|
16429
|
+
if (config?.enforcement?.runCommandSensors === true) {
|
|
16430
|
+
const changedPaths = targets.map((t) => t.path).filter(Boolean);
|
|
16431
|
+
for (const spec of selectCommandSensors(scannable, changedPaths)) {
|
|
16432
|
+
if (seen.has(spec.memory_id)) continue;
|
|
16433
|
+
const failed = await runGateCommandSensor(spec, paths.root);
|
|
16434
|
+
if (!failed) continue;
|
|
16435
|
+
seen.add(spec.memory_id);
|
|
16436
|
+
firedIds.add(spec.memory_id);
|
|
16437
|
+
if (spec.severity === "block") {
|
|
16438
|
+
findings.push({
|
|
16439
|
+
severity: "error",
|
|
16440
|
+
code: "sensor-block",
|
|
16441
|
+
message: `Block command sensor fired \u2014 ${spec.memory_id}: ${spec.message} (command: ${spec.command})`,
|
|
16442
|
+
fix: "Fix the condition the command checks, or run `haive sensors check --commands` to inspect it.",
|
|
16443
|
+
impact: 45
|
|
16444
|
+
});
|
|
16445
|
+
} else {
|
|
16446
|
+
findings.push({
|
|
16447
|
+
severity: "warn",
|
|
16448
|
+
code: "sensor-warn",
|
|
16449
|
+
message: `Command sensor flagged ${spec.memory_id}: ${spec.message}`,
|
|
16450
|
+
fix: "Review the failing command; `haive sensors check --commands` re-runs it.",
|
|
16451
|
+
impact: 5
|
|
16452
|
+
});
|
|
16453
|
+
}
|
|
16454
|
+
}
|
|
16455
|
+
}
|
|
16456
|
+
if (firedIds.size > 0) {
|
|
16457
|
+
await recordPreventionHits2(paths, [...firedIds], "sensor").catch(() => {
|
|
16458
|
+
});
|
|
16459
|
+
}
|
|
16327
16460
|
return findings;
|
|
16328
16461
|
} catch {
|
|
16329
16462
|
return [];
|
|
16330
16463
|
}
|
|
16331
16464
|
}
|
|
16465
|
+
async function runGateCommandSensor(spec, root) {
|
|
16466
|
+
try {
|
|
16467
|
+
await execFileAsync2("bash", ["-c", spec.command], { cwd: root, timeout: 12e4, maxBuffer: 8 * 1024 * 1024 });
|
|
16468
|
+
return false;
|
|
16469
|
+
} catch {
|
|
16470
|
+
return true;
|
|
16471
|
+
}
|
|
16472
|
+
}
|
|
16332
16473
|
async function findGeneratedArtifacts(paths) {
|
|
16333
16474
|
const dirty = await runCommand4("git", ["status", "--short", "--untracked-files=all"], paths.root).catch(() => "");
|
|
16334
16475
|
const generated = dirty.split("\n").map((line) => line.trim()).filter(Boolean).filter(
|
|
@@ -17114,28 +17255,25 @@ function registerRun(program2) {
|
|
|
17114
17255
|
}
|
|
17115
17256
|
|
|
17116
17257
|
// src/commands/sensors.ts
|
|
17117
|
-
import { execFile as
|
|
17258
|
+
import { execFile as execFile4 } from "child_process";
|
|
17118
17259
|
import { existsSync as existsSync76 } from "fs";
|
|
17119
17260
|
import { chmod as chmod3, mkdir as mkdir23, readFile as readFile25, writeFile as writeFile38 } from "fs/promises";
|
|
17120
17261
|
import path54 from "path";
|
|
17121
|
-
import { promisify as
|
|
17262
|
+
import { promisify as promisify4 } from "util";
|
|
17122
17263
|
import "commander";
|
|
17123
17264
|
import {
|
|
17124
|
-
appendPreventionEvent as appendPreventionEvent2,
|
|
17125
17265
|
findProjectRoot as findProjectRoot53,
|
|
17126
17266
|
isRetiredMemory as isRetiredMemory4,
|
|
17127
17267
|
loadConfig as loadConfig15,
|
|
17128
17268
|
loadMemoriesFromDir as loadMemoriesFromDir39,
|
|
17129
|
-
|
|
17130
|
-
recordPrevention as recordPrevention2,
|
|
17269
|
+
recordPreventionHits as recordPreventionHits3,
|
|
17131
17270
|
resolveHaivePaths as resolveHaivePaths49,
|
|
17132
17271
|
runSensors as runSensors3,
|
|
17133
|
-
|
|
17134
|
-
selectCommandSensors,
|
|
17272
|
+
selectCommandSensors as selectCommandSensors2,
|
|
17135
17273
|
sensorTargetsFromDiff as sensorTargetsFromDiff3,
|
|
17136
17274
|
serializeMemory as serializeMemory29
|
|
17137
17275
|
} from "@hiveai/core";
|
|
17138
|
-
var exec2 =
|
|
17276
|
+
var exec2 = promisify4(execFile4);
|
|
17139
17277
|
function registerSensors(program2) {
|
|
17140
17278
|
const sensors = program2.command("sensors").description("Operate executable sensors derived from hAIve memories");
|
|
17141
17279
|
sensors.command("list").description("List memories carrying executable sensors").option("--json", "emit JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
@@ -17172,7 +17310,7 @@ function registerSensors(program2) {
|
|
|
17172
17310
|
const runCommands = opts.commands || config.enforcement?.runCommandSensors === true;
|
|
17173
17311
|
const changedPaths = targets.map((t) => t.path).filter(Boolean);
|
|
17174
17312
|
const allSensorMemories = await runnableSensorMemories(paths, false);
|
|
17175
|
-
const commandSpecs =
|
|
17313
|
+
const commandSpecs = selectCommandSensors2(allSensorMemories, changedPaths);
|
|
17176
17314
|
const commandHits = [];
|
|
17177
17315
|
const commandSkipped = [];
|
|
17178
17316
|
if (commandSpecs.length > 0 && runCommands) {
|
|
@@ -17191,20 +17329,7 @@ function registerSensors(program2) {
|
|
|
17191
17329
|
for (const spec of commandSpecs) commandSkipped.push(spec.memory_id);
|
|
17192
17330
|
}
|
|
17193
17331
|
const firedIds = [...new Set([...hits, ...commandHits].map((hit) => hit.memory_id))];
|
|
17194
|
-
|
|
17195
|
-
const usage = await loadUsageIndex31(paths);
|
|
17196
|
-
const recordedIds = [];
|
|
17197
|
-
for (const id of firedIds) if (recordPrevention2(usage, id)) recordedIds.push(id);
|
|
17198
|
-
if (recordedIds.length > 0) {
|
|
17199
|
-
await saveUsageIndex8(paths, usage).catch(() => {
|
|
17200
|
-
});
|
|
17201
|
-
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
17202
|
-
for (const id of recordedIds) {
|
|
17203
|
-
await appendPreventionEvent2(paths, { at, id, source: "sensor" }).catch(() => {
|
|
17204
|
-
});
|
|
17205
|
-
}
|
|
17206
|
-
}
|
|
17207
|
-
}
|
|
17332
|
+
await recordPreventionHits3(paths, firedIds, "sensor");
|
|
17208
17333
|
const output = {
|
|
17209
17334
|
scanned: memories.length,
|
|
17210
17335
|
hits: hits.map((hit) => ({
|
|
@@ -17571,7 +17696,7 @@ import {
|
|
|
17571
17696
|
loadConfig as loadConfig16,
|
|
17572
17697
|
loadMemoriesFromDir as loadMemoriesFromDir41,
|
|
17573
17698
|
loadPreventionEvents as loadPreventionEvents4,
|
|
17574
|
-
loadUsageIndex as
|
|
17699
|
+
loadUsageIndex as loadUsageIndex31,
|
|
17575
17700
|
resolveHaivePaths as resolveHaivePaths51
|
|
17576
17701
|
} from "@hiveai/core";
|
|
17577
17702
|
function registerDashboard(program2) {
|
|
@@ -17586,7 +17711,7 @@ function registerDashboard(program2) {
|
|
|
17586
17711
|
return;
|
|
17587
17712
|
}
|
|
17588
17713
|
const memories = existsSync78(paths.memoriesDir) ? await loadMemoriesFromDir41(paths.memoriesDir) : [];
|
|
17589
|
-
const usage = await
|
|
17714
|
+
const usage = await loadUsageIndex31(paths);
|
|
17590
17715
|
const preventionEvents = await loadPreventionEvents4(paths);
|
|
17591
17716
|
const config = await loadConfig16(paths);
|
|
17592
17717
|
const top = Math.max(1, Number.parseInt(opts.top ?? "10", 10) || 10);
|
|
@@ -17697,14 +17822,14 @@ function warnNum(n) {
|
|
|
17697
17822
|
}
|
|
17698
17823
|
|
|
17699
17824
|
// src/commands/dev-link.ts
|
|
17700
|
-
import { execFile as
|
|
17825
|
+
import { execFile as execFile5 } from "child_process";
|
|
17701
17826
|
import { cp, readFile as readFile27 } from "fs/promises";
|
|
17702
17827
|
import { existsSync as existsSync79 } from "fs";
|
|
17703
17828
|
import path56 from "path";
|
|
17704
|
-
import { promisify as
|
|
17829
|
+
import { promisify as promisify5 } from "util";
|
|
17705
17830
|
import "commander";
|
|
17706
17831
|
import { findProjectRoot as findProjectRoot56 } from "@hiveai/core";
|
|
17707
|
-
var exec3 =
|
|
17832
|
+
var exec3 = promisify5(execFile5);
|
|
17708
17833
|
function registerDevLink(program2) {
|
|
17709
17834
|
const dev = program2.commands.find((c) => c.name() === "dev") ?? program2.command("dev").description("Developer utilities for working on hAIve itself.");
|
|
17710
17835
|
dev.command("link").description("Hot-swap this repo's built dist into the global @hiveai install so `haive` runs your local code.").option("-d, --dir <dir>", "repo root (default: discovered from cwd)").option("--json", "emit a machine-readable summary", false).action(async (opts) => {
|
|
@@ -17760,8 +17885,41 @@ function registerDevLink(program2) {
|
|
|
17760
17885
|
}
|
|
17761
17886
|
|
|
17762
17887
|
// src/commands/coverage.ts
|
|
17888
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
17889
|
+
import { existsSync as existsSync80 } from "fs";
|
|
17890
|
+
import path57 from "path";
|
|
17763
17891
|
import "commander";
|
|
17764
|
-
import {
|
|
17892
|
+
import {
|
|
17893
|
+
findCoverageGaps,
|
|
17894
|
+
findProjectRoot as findProjectRoot57,
|
|
17895
|
+
mergeHotFiles,
|
|
17896
|
+
resolveHaivePaths as resolveHaivePaths52,
|
|
17897
|
+
tallyHotFiles
|
|
17898
|
+
} from "@hiveai/core";
|
|
17899
|
+
async function readAgentHotFiles(root, cacheFile, sinceMs) {
|
|
17900
|
+
if (!existsSync80(cacheFile)) return [];
|
|
17901
|
+
const raw = await readFile28(cacheFile, "utf8").catch(() => "");
|
|
17902
|
+
const files = [];
|
|
17903
|
+
for (const line of raw.split("\n")) {
|
|
17904
|
+
const trimmed = line.trim();
|
|
17905
|
+
if (!trimmed) continue;
|
|
17906
|
+
try {
|
|
17907
|
+
const obs = JSON.parse(trimmed);
|
|
17908
|
+
if (sinceMs > 0 && obs.ts) {
|
|
17909
|
+
const t = Date.parse(obs.ts);
|
|
17910
|
+
if (Number.isFinite(t) && t < sinceMs) continue;
|
|
17911
|
+
}
|
|
17912
|
+
for (const f of obs.files ?? []) {
|
|
17913
|
+
if (typeof f !== "string" || !f) continue;
|
|
17914
|
+
const rel = path57.isAbsolute(f) ? path57.relative(root, f) : f;
|
|
17915
|
+
if (rel.startsWith("..")) continue;
|
|
17916
|
+
files.push(rel);
|
|
17917
|
+
}
|
|
17918
|
+
} catch {
|
|
17919
|
+
}
|
|
17920
|
+
}
|
|
17921
|
+
return tallyHotFiles(files, "agent");
|
|
17922
|
+
}
|
|
17765
17923
|
function isNoisePath(p) {
|
|
17766
17924
|
if (/(^|\/)(node_modules|dist|build|coverage|\.next)\//.test(p)) return true;
|
|
17767
17925
|
if (p.startsWith(".ai/")) return true;
|
|
@@ -17773,29 +17931,47 @@ function isNoisePath(p) {
|
|
|
17773
17931
|
function registerCoverage(program2) {
|
|
17774
17932
|
program2.command("coverage").description(
|
|
17775
17933
|
"Coverage-gap report: frequently-edited files with no covering team memory (blind spots)."
|
|
17776
|
-
).option("--json", "emit JSON", false).option("--min-changes <n>", "minimum
|
|
17934
|
+
).option("--json", "emit JSON", false).option("--min-changes <n>", "minimum churn count to flag a file", "3").option("--limit <n>", "max gaps to report", "20").option("--days <n>", "lookback window in days (git history + agent edits)", "90").option("--source <which>", "heat source: git | agent | both", "both").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
17777
17935
|
const root = findProjectRoot57(opts.dir);
|
|
17778
17936
|
const paths = resolveHaivePaths52(root);
|
|
17779
17937
|
const minChanges = Math.max(1, parseInt(opts.minChanges ?? "3", 10) || 3);
|
|
17780
17938
|
const limit = Math.max(1, parseInt(opts.limit ?? "20", 10) || 20);
|
|
17781
17939
|
const days = Math.max(1, parseInt(opts.days ?? "90", 10) || 90);
|
|
17782
|
-
const
|
|
17940
|
+
const source = (opts.source ?? "both").toLowerCase();
|
|
17941
|
+
if (!["git", "agent", "both"].includes(source)) {
|
|
17942
|
+
ui.error("--source must be one of: git | agent | both");
|
|
17943
|
+
process.exitCode = 1;
|
|
17944
|
+
return;
|
|
17945
|
+
}
|
|
17946
|
+
const useGit = source === "git" || source === "both";
|
|
17947
|
+
const useAgent = source === "agent" || source === "both";
|
|
17948
|
+
const radar = useGit ? await buildRadar({
|
|
17783
17949
|
root,
|
|
17784
17950
|
taskTokens: null,
|
|
17785
17951
|
filePaths: [],
|
|
17786
17952
|
daysBack: Math.ceil(days / 6),
|
|
17787
17953
|
// getHotFiles multiplies daysBack by 6
|
|
17788
17954
|
maxHotFiles: 500
|
|
17789
|
-
});
|
|
17790
|
-
const
|
|
17955
|
+
}) : null;
|
|
17956
|
+
const gitHotFiles = (radar?.hotFiles ?? []).filter((h) => !isNoisePath(h.path)).map((h) => ({ path: h.path, changes: h.changes, source: "git" }));
|
|
17957
|
+
const sinceMs = Date.now() - days * 864e5;
|
|
17958
|
+
const agentHotFiles = useAgent ? (await readAgentHotFiles(root, path57.join(paths.haiveDir, ".cache", "observations.jsonl"), sinceMs)).filter((h) => !isNoisePath(h.path)) : [];
|
|
17959
|
+
const hotFiles = mergeHotFiles(gitHotFiles, agentHotFiles);
|
|
17791
17960
|
const memories = await loadMemoriesFromDir27(paths.memoriesDir);
|
|
17792
17961
|
const gaps = findCoverageGaps(hotFiles, memories, { minChanges, limit });
|
|
17793
17962
|
if (opts.json) {
|
|
17794
|
-
console.log(JSON.stringify({
|
|
17963
|
+
console.log(JSON.stringify({
|
|
17964
|
+
root,
|
|
17965
|
+
source,
|
|
17966
|
+
scanned_hot_files: hotFiles.length,
|
|
17967
|
+
git_hot_files: gitHotFiles.length,
|
|
17968
|
+
agent_hot_files: agentHotFiles.length,
|
|
17969
|
+
gaps
|
|
17970
|
+
}, null, 2));
|
|
17795
17971
|
return;
|
|
17796
17972
|
}
|
|
17797
|
-
if (!radar.insideGitRepo) {
|
|
17798
|
-
ui.warn("Not a git repository
|
|
17973
|
+
if (useGit && radar && !radar.insideGitRepo && agentHotFiles.length === 0) {
|
|
17974
|
+
ui.warn("Not a git repository and no agent-edit history \u2014 nothing to cross-check.");
|
|
17799
17975
|
return;
|
|
17800
17976
|
}
|
|
17801
17977
|
if (gaps.length === 0) {
|
|
@@ -17804,7 +17980,8 @@ function registerCoverage(program2) {
|
|
|
17804
17980
|
}
|
|
17805
17981
|
console.log(ui.bold(`hAIve coverage \u2014 ${gaps.length} blind spot(s) (hot files with no covering memory)`));
|
|
17806
17982
|
for (const gap of gaps) {
|
|
17807
|
-
|
|
17983
|
+
const src = gap.source ? ui.dim(` [${gap.source}]`) : "";
|
|
17984
|
+
console.log(` ${ui.yellow("\u25CB")} ${gap.path} ${ui.dim(`(${gap.changes} change${gap.changes === 1 ? "" : "s"})`)}${src}`);
|
|
17808
17985
|
}
|
|
17809
17986
|
console.log(
|
|
17810
17987
|
ui.dim(
|
|
@@ -17816,8 +17993,8 @@ function registerCoverage(program2) {
|
|
|
17816
17993
|
|
|
17817
17994
|
// src/commands/merge-driver.ts
|
|
17818
17995
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
17819
|
-
import { readFileSync, writeFileSync, existsSync as
|
|
17820
|
-
import
|
|
17996
|
+
import { readFileSync, writeFileSync, existsSync as existsSync81 } from "fs";
|
|
17997
|
+
import path58 from "path";
|
|
17821
17998
|
import "commander";
|
|
17822
17999
|
import { findProjectRoot as findProjectRoot58, mergeMemoryVersions } from "@hiveai/core";
|
|
17823
18000
|
var GITATTRIBUTES_MARK = "# hAIve merge driver";
|
|
@@ -17849,8 +18026,8 @@ function registerMergeDriver(program2) {
|
|
|
17849
18026
|
process.exitCode = 1;
|
|
17850
18027
|
return;
|
|
17851
18028
|
}
|
|
17852
|
-
const gaPath =
|
|
17853
|
-
let content =
|
|
18029
|
+
const gaPath = path58.join(root, ".gitattributes");
|
|
18030
|
+
let content = existsSync81(gaPath) ? readFileSync(gaPath, "utf8") : "";
|
|
17854
18031
|
if (!content.includes(GITATTRIBUTES_MARK)) {
|
|
17855
18032
|
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
17856
18033
|
content += GITATTRIBUTES_BLOCK + "\n";
|
|
@@ -17865,11 +18042,12 @@ function registerMergeDriver(program2) {
|
|
|
17865
18042
|
|
|
17866
18043
|
// src/commands/memory-resolve-conflict.ts
|
|
17867
18044
|
import { writeFile as writeFile40 } from "fs/promises";
|
|
17868
|
-
import { existsSync as
|
|
18045
|
+
import { existsSync as existsSync83 } from "fs";
|
|
17869
18046
|
import "commander";
|
|
17870
18047
|
import {
|
|
18048
|
+
applyConflictResolution,
|
|
17871
18049
|
findProjectRoot as findProjectRoot59,
|
|
17872
|
-
planConflictResolution,
|
|
18050
|
+
planConflictResolution as planConflictResolution2,
|
|
17873
18051
|
resolveHaivePaths as resolveHaivePaths53,
|
|
17874
18052
|
serializeMemory as serializeMemory31
|
|
17875
18053
|
} from "@hiveai/core";
|
|
@@ -17877,7 +18055,7 @@ function registerMemoryResolveConflict(memory2) {
|
|
|
17877
18055
|
memory2.command("resolve-conflict <id_a> <id_b>").description("Resolve a contradiction: keep the stronger memory, deprecate (supersede) the other").option("--yes", "apply the resolution (without this, only previews it)", false).option("--json", "emit JSON", false).option("-d, --dir <dir>", "project root").action(async (idA, idB, opts) => {
|
|
17878
18056
|
const root = findProjectRoot59(opts.dir);
|
|
17879
18057
|
const paths = resolveHaivePaths53(root);
|
|
17880
|
-
if (!
|
|
18058
|
+
if (!existsSync83(paths.memoriesDir)) {
|
|
17881
18059
|
ui.error(`No .ai/memories at ${root}.`);
|
|
17882
18060
|
process.exitCode = 1;
|
|
17883
18061
|
return;
|
|
@@ -17890,43 +18068,53 @@ function registerMemoryResolveConflict(memory2) {
|
|
|
17890
18068
|
process.exitCode = 1;
|
|
17891
18069
|
return;
|
|
17892
18070
|
}
|
|
17893
|
-
const plan =
|
|
18071
|
+
const plan = planConflictResolution2(a, b);
|
|
18072
|
+
const winner = plan.keep_id === idA ? a : b;
|
|
17894
18073
|
const loser = plan.supersede_id === idA ? a : b;
|
|
18074
|
+
const applied = applyConflictResolution(winner, loser, plan);
|
|
17895
18075
|
if (opts.json) {
|
|
17896
|
-
console.log(JSON.stringify({
|
|
18076
|
+
console.log(JSON.stringify({
|
|
18077
|
+
...plan,
|
|
18078
|
+
winner_revision_count: applied.winner.revision_count,
|
|
18079
|
+
topic: applied.topic,
|
|
18080
|
+
topic_adopted: applied.topic_adopted,
|
|
18081
|
+
applied: Boolean(opts.yes)
|
|
18082
|
+
}, null, 2));
|
|
17897
18083
|
} else {
|
|
17898
18084
|
console.log(ui.bold("Conflict resolution"));
|
|
17899
|
-
console.log(` keep: ${ui.green(plan.keep_id)}`);
|
|
18085
|
+
console.log(` keep: ${ui.green(plan.keep_id)} ${ui.dim(`(rev ${winner.memory.frontmatter.revision_count}\u2192${applied.winner.revision_count})`)}`);
|
|
17900
18086
|
console.log(` supersede: ${ui.red(plan.supersede_id)} ${ui.dim(`\u2192 deprecated`)}`);
|
|
17901
18087
|
console.log(` reason: ${plan.reason}`);
|
|
18088
|
+
if (applied.topic) {
|
|
18089
|
+
console.log(` topic: ${applied.topic}${applied.topic_adopted ? ui.dim(" (adopted from superseded \u2014 future captures upsert into the winner)") : ""}`);
|
|
18090
|
+
}
|
|
17902
18091
|
}
|
|
17903
18092
|
if (!opts.yes) {
|
|
17904
18093
|
if (!opts.json) ui.info("Preview only \u2014 re-run with --yes to apply.");
|
|
17905
18094
|
return;
|
|
17906
18095
|
}
|
|
18096
|
+
await writeFile40(
|
|
18097
|
+
winner.filePath,
|
|
18098
|
+
serializeMemory31({ frontmatter: applied.winner, body: winner.memory.body }),
|
|
18099
|
+
"utf8"
|
|
18100
|
+
);
|
|
17907
18101
|
await writeFile40(
|
|
17908
18102
|
loser.filePath,
|
|
17909
|
-
serializeMemory31({
|
|
17910
|
-
frontmatter: {
|
|
17911
|
-
...loser.memory.frontmatter,
|
|
17912
|
-
status: "deprecated",
|
|
17913
|
-
stale_reason: plan.stale_reason,
|
|
17914
|
-
related_ids: [.../* @__PURE__ */ new Set([...loser.memory.frontmatter.related_ids, plan.keep_id])]
|
|
17915
|
-
},
|
|
17916
|
-
body: loser.memory.body
|
|
17917
|
-
}),
|
|
18103
|
+
serializeMemory31({ frontmatter: applied.loser, body: loser.memory.body }),
|
|
17918
18104
|
"utf8"
|
|
17919
18105
|
);
|
|
17920
|
-
if (!opts.json)
|
|
18106
|
+
if (!opts.json) {
|
|
18107
|
+
ui.success(`Deprecated ${plan.supersede_id}; promoted ${plan.keep_id} (rev ${applied.winner.revision_count}${applied.topic ? `, topic=${applied.topic}` : ""}).`);
|
|
18108
|
+
}
|
|
17921
18109
|
});
|
|
17922
18110
|
}
|
|
17923
18111
|
|
|
17924
18112
|
// src/commands/memory-seed-git.ts
|
|
17925
|
-
import { execFile as
|
|
18113
|
+
import { execFile as execFile6 } from "child_process";
|
|
17926
18114
|
import { mkdir as mkdir25, writeFile as writeFile41 } from "fs/promises";
|
|
17927
|
-
import { existsSync as
|
|
17928
|
-
import
|
|
17929
|
-
import { promisify as
|
|
18115
|
+
import { existsSync as existsSync84 } from "fs";
|
|
18116
|
+
import path59 from "path";
|
|
18117
|
+
import { promisify as promisify6 } from "util";
|
|
17930
18118
|
import "commander";
|
|
17931
18119
|
import {
|
|
17932
18120
|
buildFrontmatter as buildFrontmatter12,
|
|
@@ -17936,12 +18124,12 @@ import {
|
|
|
17936
18124
|
resolveHaivePaths as resolveHaivePaths54,
|
|
17937
18125
|
serializeMemory as serializeMemory33
|
|
17938
18126
|
} from "@hiveai/core";
|
|
17939
|
-
var exec4 =
|
|
18127
|
+
var exec4 = promisify6(execFile6);
|
|
17940
18128
|
function registerMemorySeedGit(memory2) {
|
|
17941
18129
|
memory2.command("seed-git").description("Propose draft `attempt` seeds from revert/hotfix commits in git history (cold-start)").option("--apply", "write the proposed seeds as draft memories (default: preview only)", false).option("--limit <n>", "max seeds to propose", "20").option("--days <n>", "git-history lookback window in days", "365").option("--scope <scope>", "personal | team", "team").option("--json", "emit JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
17942
18130
|
const root = findProjectRoot60(opts.dir);
|
|
17943
18131
|
const paths = resolveHaivePaths54(root);
|
|
17944
|
-
if (!
|
|
18132
|
+
if (!existsSync84(paths.haiveDir)) {
|
|
17945
18133
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
17946
18134
|
process.exitCode = 1;
|
|
17947
18135
|
return;
|
|
@@ -17986,8 +18174,8 @@ function registerMemorySeedGit(memory2) {
|
|
|
17986
18174
|
_Seeded from git ${p.kind} commit ${p.source_sha}. Review and validate (or delete) \u2014 not yet authoritative._
|
|
17987
18175
|
`;
|
|
17988
18176
|
const file = memoryFilePath13(paths, fm.scope, fm.id, fm.module);
|
|
17989
|
-
if (
|
|
17990
|
-
await mkdir25(
|
|
18177
|
+
if (existsSync84(file)) continue;
|
|
18178
|
+
await mkdir25(path59.dirname(file), { recursive: true });
|
|
17991
18179
|
await writeFile41(file, serializeMemory33({ frontmatter: fm, body }), "utf8");
|
|
17992
18180
|
written += 1;
|
|
17993
18181
|
}
|
|
@@ -18019,8 +18207,8 @@ async function readCommits(root, days) {
|
|
|
18019
18207
|
}
|
|
18020
18208
|
|
|
18021
18209
|
// src/commands/bridges.ts
|
|
18022
|
-
import { existsSync as
|
|
18023
|
-
import
|
|
18210
|
+
import { existsSync as existsSync85 } from "fs";
|
|
18211
|
+
import path60 from "path";
|
|
18024
18212
|
import "commander";
|
|
18025
18213
|
import {
|
|
18026
18214
|
findProjectRoot as findProjectRoot61,
|
|
@@ -18041,7 +18229,7 @@ function registerBridges(program2) {
|
|
|
18041
18229
|
const root = findProjectRoot61(opts.dir);
|
|
18042
18230
|
const paths = resolveHaivePaths55(root);
|
|
18043
18231
|
const dryRun = opts.dryRun === true;
|
|
18044
|
-
if (!
|
|
18232
|
+
if (!existsSync85(paths.memoriesDir)) {
|
|
18045
18233
|
ui.warn(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
18046
18234
|
process.exitCode = 1;
|
|
18047
18235
|
return;
|
|
@@ -18060,7 +18248,7 @@ function registerBridges(program2) {
|
|
|
18060
18248
|
targets = BRIDGE_TARGETS3;
|
|
18061
18249
|
} else {
|
|
18062
18250
|
targets = BRIDGE_TARGETS3.filter(
|
|
18063
|
-
(t) =>
|
|
18251
|
+
(t) => existsSync85(path60.join(root, BRIDGE_TARGET_PATH3[t]))
|
|
18064
18252
|
);
|
|
18065
18253
|
if (targets.length === 0) {
|
|
18066
18254
|
ui.info(
|
|
@@ -18089,7 +18277,7 @@ function registerBridges(program2) {
|
|
|
18089
18277
|
console.log(ui.bold("hAIve bridge targets:"));
|
|
18090
18278
|
for (const target of BRIDGE_TARGETS3) {
|
|
18091
18279
|
const relPath = BRIDGE_TARGET_PATH3[target];
|
|
18092
|
-
const exists =
|
|
18280
|
+
const exists = existsSync85(path60.join(root, relPath));
|
|
18093
18281
|
const marker = exists ? ui.dim("\u2713") : ui.dim("\xB7");
|
|
18094
18282
|
console.log(` ${marker} ${target.padEnd(10)} ${relPath}${exists ? "" : " (not present)"}`);
|
|
18095
18283
|
}
|
|
@@ -18100,7 +18288,7 @@ function registerBridges(program2) {
|
|
|
18100
18288
|
|
|
18101
18289
|
// src/index.ts
|
|
18102
18290
|
var program = new Command64();
|
|
18103
|
-
program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.
|
|
18291
|
+
program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.23.0").option("--advanced", "show maintenance and experimental commands in help");
|
|
18104
18292
|
registerInit(program);
|
|
18105
18293
|
registerWelcome(program);
|
|
18106
18294
|
registerResolveProject(program);
|