@hiveai/cli 0.13.9 → 0.15.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 +568 -61
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command63 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/briefing.ts
|
|
7
7
|
import { existsSync as existsSync3 } from "fs";
|
|
@@ -199,7 +199,7 @@ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
|
|
|
199
199
|
if (!f) continue;
|
|
200
200
|
counts.set(f, (counts.get(f) ?? 0) + 1);
|
|
201
201
|
}
|
|
202
|
-
let entries = [...counts.entries()].map(([
|
|
202
|
+
let entries = [...counts.entries()].map(([path58, changes]) => ({ path: path58, changes }));
|
|
203
203
|
const lowerPaths = filePaths.map((p) => p.toLowerCase());
|
|
204
204
|
if (lowerPaths.length > 0) {
|
|
205
205
|
entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
|
|
@@ -3019,7 +3019,7 @@ ${SEED_FOOTER(stack)}` });
|
|
|
3019
3019
|
}
|
|
3020
3020
|
|
|
3021
3021
|
// src/commands/init.ts
|
|
3022
|
-
var HAIVE_GITHUB_ACTION_REF = `v${"0.
|
|
3022
|
+
var HAIVE_GITHUB_ACTION_REF = `v${"0.15.0"}`;
|
|
3023
3023
|
var PROJECT_CONTEXT_TEMPLATE = `# Project context
|
|
3024
3024
|
|
|
3025
3025
|
> Generated by \`haive init\`. Run \`haive init --bootstrap\` to auto-fill from your codebase,
|
|
@@ -3202,7 +3202,11 @@ jobs:
|
|
|
3202
3202
|
run: npm install -g @hiveai/cli
|
|
3203
3203
|
|
|
3204
3204
|
- name: harness quality regression gate
|
|
3205
|
-
run: haive eval --regression-gate
|
|
3205
|
+
run: haive eval --regression-gate --record --ref "\${{ github.sha }}"
|
|
3206
|
+
|
|
3207
|
+
- name: harness quality trend
|
|
3208
|
+
if: always()
|
|
3209
|
+
run: haive eval --trend || true
|
|
3206
3210
|
|
|
3207
3211
|
# On push to main: push shared memories to the hub (if hubPath is configured)
|
|
3208
3212
|
# Uncomment and configure hubPath in .ai/haive.config.json to enable.
|
|
@@ -4089,10 +4093,13 @@ import {
|
|
|
4089
4093
|
literalMatchesAnyToken as literalMatchesAnyToken22,
|
|
4090
4094
|
loadCodeMap as loadCodeMap5,
|
|
4091
4095
|
loadConfig as loadConfig3,
|
|
4096
|
+
hashProjectContext,
|
|
4092
4097
|
loadMemoriesFromDir as loadMemoriesFromDir15,
|
|
4093
4098
|
loadUsageIndex as loadUsageIndex8,
|
|
4094
4099
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths22,
|
|
4100
|
+
projectContextRecentlyEmitted,
|
|
4095
4101
|
rankMemoriesLexical as rankMemoriesLexical2,
|
|
4102
|
+
recordProjectContextEmission,
|
|
4096
4103
|
queryCodeMap as queryCodeMap2,
|
|
4097
4104
|
resolveBriefingBudget as resolveBriefingBudget2,
|
|
4098
4105
|
serializeMemory as serializeMemory10,
|
|
@@ -5711,6 +5718,9 @@ var GetBriefingInputSchema = {
|
|
|
5711
5718
|
),
|
|
5712
5719
|
max_memories: z19.number().int().positive().default(8).describe("Cap on memories surfaced regardless of token budget"),
|
|
5713
5720
|
include_project_context: z19.boolean().default(true),
|
|
5721
|
+
dedupe_project_context: z19.boolean().optional().describe(
|
|
5722
|
+
"Token saver (default ON): skip re-emitting the project-context body if an identical copy was already sent within the last few minutes this session (the agent still has it). Set false to always include it."
|
|
5723
|
+
),
|
|
5714
5724
|
include_module_contexts: z19.boolean().default(true),
|
|
5715
5725
|
semantic: z19.boolean().default(true).describe(
|
|
5716
5726
|
"Use semantic ranking when a task is provided (requires `haive embeddings index`)."
|
|
@@ -5918,7 +5928,17 @@ async function getBriefing(input, ctx) {
|
|
|
5918
5928
|
}
|
|
5919
5929
|
}
|
|
5920
5930
|
}
|
|
5921
|
-
|
|
5931
|
+
let projectContextRaw = input.include_project_context && existsSync21(ctx.paths.projectContext) ? await readFile52(ctx.paths.projectContext, "utf8") : "";
|
|
5932
|
+
let contextOmittedRecent = false;
|
|
5933
|
+
if (projectContextRaw && input.dedupe_project_context !== false) {
|
|
5934
|
+
const ctxHash = hashProjectContext(projectContextRaw);
|
|
5935
|
+
if (await projectContextRecentlyEmitted(ctx.paths, ctxHash)) {
|
|
5936
|
+
contextOmittedRecent = true;
|
|
5937
|
+
projectContextRaw = "";
|
|
5938
|
+
} else {
|
|
5939
|
+
await recordProjectContextEmission(ctx.paths, ctxHash);
|
|
5940
|
+
}
|
|
5941
|
+
}
|
|
5922
5942
|
const isTemplateContext = projectContextRaw.includes("TODO \u2014 high-level overview") || projectContextRaw.includes("Generated by `haive init`");
|
|
5923
5943
|
const setupWarnings = [];
|
|
5924
5944
|
let autoContextGenerated = false;
|
|
@@ -6186,7 +6206,11 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
6186
6206
|
search_mode: searchMode,
|
|
6187
6207
|
inferred_modules: inferred,
|
|
6188
6208
|
...lastSession ? { last_session: lastSession } : {},
|
|
6189
|
-
project_context:
|
|
6209
|
+
project_context: contextOmittedRecent ? {
|
|
6210
|
+
content: "(project context unchanged \u2014 omitted to save tokens; it was provided earlier this session. Pass dedupe_project_context:false to force a full copy.)",
|
|
6211
|
+
truncated: false,
|
|
6212
|
+
omitted_recent: true
|
|
6213
|
+
} : adaptiveTrim ? {
|
|
6190
6214
|
content: "(adaptive briefing: auto-generated context omitted \u2014 no team-specific policy matched, so a capable model needs nothing extra here)",
|
|
6191
6215
|
truncated: false,
|
|
6192
6216
|
...isTemplateContext && !autoContextGenerated ? { is_template: true } : {},
|
|
@@ -7504,7 +7528,7 @@ async function patternDetect(input, ctx) {
|
|
|
7504
7528
|
for (const [p, { count, tools }] of pathCounts) {
|
|
7505
7529
|
if (count < HOT_FILE_MIN) continue;
|
|
7506
7530
|
if (tools.has("mem_tried") || tools.has("mem_observe")) continue;
|
|
7507
|
-
if (CONFIG_PATTERNS.some((
|
|
7531
|
+
if (CONFIG_PATTERNS.some((cp2) => path122.basename(p).includes(cp2))) continue;
|
|
7508
7532
|
const slug = p.replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 40);
|
|
7509
7533
|
matches.push({
|
|
7510
7534
|
kind: "hot_file",
|
|
@@ -7938,7 +7962,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
7938
7962
|
};
|
|
7939
7963
|
}
|
|
7940
7964
|
var SERVER_NAME = "haive";
|
|
7941
|
-
var SERVER_VERSION = "0.
|
|
7965
|
+
var SERVER_VERSION = "0.15.0";
|
|
7942
7966
|
function jsonResult(data) {
|
|
7943
7967
|
return {
|
|
7944
7968
|
content: [
|
|
@@ -8687,6 +8711,7 @@ function createHaiveServer(options = {}) {
|
|
|
8687
8711
|
"anti_patterns_check",
|
|
8688
8712
|
[
|
|
8689
8713
|
"Scan a diff (or set of paths) against documented attempt/gotcha memories.",
|
|
8714
|
+
"[Diff-scan layer: the MEMORY-MATCH component. `pre_commit_check` combines this with sensors + stale checks; `haive enforce check` is the gate.]",
|
|
8690
8715
|
"Surfaces 'you are about to repeat a known mistake' warnings BEFORE you commit.",
|
|
8691
8716
|
"",
|
|
8692
8717
|
"USE BEFORE finalizing a non-trivial change. Cheap and high-signal: the only",
|
|
@@ -8830,6 +8855,7 @@ function createHaiveServer(options = {}) {
|
|
|
8830
8855
|
"pre_commit_check",
|
|
8831
8856
|
[
|
|
8832
8857
|
"One-shot 'should I block this commit?' check. Combines three signals:",
|
|
8858
|
+
"[Diff-scan layer: the COMBINED check (sensors + anti-patterns + stale). `haive enforce check` is the gate that runs this at commit time.]",
|
|
8833
8859
|
"",
|
|
8834
8860
|
" 1. anti_patterns_check \u2014 known gotchas/attempts that match the diff",
|
|
8835
8861
|
" 2. mem_for_files \u2014 conventions/decisions anchored to touched files",
|
|
@@ -12687,9 +12713,12 @@ import "commander";
|
|
|
12687
12713
|
import {
|
|
12688
12714
|
aggregateRetrieval,
|
|
12689
12715
|
aggregateSensors,
|
|
12716
|
+
appendEvalHistory,
|
|
12690
12717
|
buildReport,
|
|
12691
12718
|
compareEvalReports,
|
|
12719
|
+
computeEvalTrend,
|
|
12692
12720
|
findProjectRoot as findProjectRoot42,
|
|
12721
|
+
loadEvalHistory,
|
|
12693
12722
|
resolveHaivePaths as resolveHaivePaths38,
|
|
12694
12723
|
scoreRetrievalCase,
|
|
12695
12724
|
scoreSensorCase,
|
|
@@ -12698,7 +12727,7 @@ import {
|
|
|
12698
12727
|
function registerEval(program2) {
|
|
12699
12728
|
program2.command("eval").description(
|
|
12700
12729
|
"Rigorous, repeatable quality eval: do the right memories surface (retrieval) and do the right sensors fire (catch-rate)? Emits a numeric 0\u2013100 score. Uses .ai/eval cases via --spec, or auto-synthesizes cases from anchored memories."
|
|
12701
|
-
).option("--spec <file>", "JSON eval spec ({ retrieval: [...], sensors: [...] })").option("--semantic-only", "self-eval probes by title alone (no anchor files) \u2014 harder retrieval", false).option("-k, --top <n>", "briefing top-k considered a hit", "8").option("--json", "emit JSON", false).option("--out <file>", "write a Markdown report").option("--fail-under <score>", "exit non-zero if the overall score is below this (0\u2013100) \u2014 for CI gates").option("--baseline", "save this run as the baseline (.ai/eval/baseline.json) for future --compare", false).option("--compare", "diff this run against the saved baseline and print the delta", false).option("--baseline-file <path>", "baseline file to read/write (default: .ai/eval/baseline.json)").option("--fail-on-regression", "with --compare, exit non-zero if the score dropped vs the baseline", false).option("--regression-gate", "CI-safe gate: compare against the baseline IF one exists (fail on regression), else no-op", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
12730
|
+
).option("--spec <file>", "JSON eval spec ({ retrieval: [...], sensors: [...] })").option("--semantic-only", "self-eval probes by title alone (no anchor files) \u2014 harder retrieval", false).option("-k, --top <n>", "briefing top-k considered a hit", "8").option("--json", "emit JSON", false).option("--out <file>", "write a Markdown report").option("--fail-under <score>", "exit non-zero if the overall score is below this (0\u2013100) \u2014 for CI gates").option("--baseline", "save this run as the baseline (.ai/eval/baseline.json) for future --compare", false).option("--compare", "diff this run against the saved baseline and print the delta", false).option("--baseline-file <path>", "baseline file to read/write (default: .ai/eval/baseline.json)").option("--fail-on-regression", "with --compare, exit non-zero if the score dropped vs the baseline", false).option("--regression-gate", "CI-safe gate: compare against the baseline IF one exists (fail on regression), else no-op", false).option("--record", "append this run's score to .ai/.cache/eval-history.jsonl (trend the harness over time)", false).option("--trend", "print the recorded score trend (sparkline + latest/best/delta) and exit", false).option("--ref <ref>", "version/commit label stored with a --record run").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
12702
12731
|
const root = findProjectRoot42(opts.dir);
|
|
12703
12732
|
const paths = resolveHaivePaths38(root);
|
|
12704
12733
|
if (!existsSync65(paths.memoriesDir)) {
|
|
@@ -12706,6 +12735,22 @@ function registerEval(program2) {
|
|
|
12706
12735
|
process.exitCode = 1;
|
|
12707
12736
|
return;
|
|
12708
12737
|
}
|
|
12738
|
+
if (opts.trend) {
|
|
12739
|
+
const trend = computeEvalTrend(await loadEvalHistory(paths));
|
|
12740
|
+
if (opts.json) {
|
|
12741
|
+
console.log(JSON.stringify(trend, null, 2));
|
|
12742
|
+
return;
|
|
12743
|
+
}
|
|
12744
|
+
if (trend.runs === 0) {
|
|
12745
|
+
ui.info("No eval history yet. Run `haive eval --record` to start trending the harness.");
|
|
12746
|
+
return;
|
|
12747
|
+
}
|
|
12748
|
+
const spark = trend.recent.map((s) => "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588"[Math.min(7, Math.round(s / 100 * 7))]).join("");
|
|
12749
|
+
const arrow = trend.regressed ? ui.red("\u25BC") : (trend.delta ?? 0) > 0 ? ui.green("\u25B2") : ui.dim("=");
|
|
12750
|
+
console.log(ui.bold("hAIve eval trend"));
|
|
12751
|
+
console.log(` ${spark} latest ${arrow} ${trend.latest}/100 ${ui.dim(`(best ${trend.best}, ${trend.runs} run${trend.runs === 1 ? "" : "s"})`)}`);
|
|
12752
|
+
return;
|
|
12753
|
+
}
|
|
12709
12754
|
const k = Math.max(1, parseInt(opts.top ?? "8", 10) || 8);
|
|
12710
12755
|
const ctx = { paths };
|
|
12711
12756
|
const resolvedSpec = await resolveSpec(opts, root, paths.memoriesDir);
|
|
@@ -12733,6 +12778,17 @@ function registerEval(program2) {
|
|
|
12733
12778
|
sensorAgg = aggregateSensors(results);
|
|
12734
12779
|
}
|
|
12735
12780
|
const report = buildReport(retrievalAgg, sensorAgg);
|
|
12781
|
+
if (opts.record) {
|
|
12782
|
+
await appendEvalHistory(paths, {
|
|
12783
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12784
|
+
score: report.score,
|
|
12785
|
+
...report.retrieval ? { mean_recall: report.retrieval.mean_recall, mrr: report.retrieval.mrr } : {},
|
|
12786
|
+
...report.sensors ? { catch_rate: report.sensors.catch_rate } : {},
|
|
12787
|
+
...opts.ref ? { ref: opts.ref } : {}
|
|
12788
|
+
}).catch(() => {
|
|
12789
|
+
});
|
|
12790
|
+
if (!opts.json) ui.success(`Recorded eval score ${report.score}/100 to history.`);
|
|
12791
|
+
}
|
|
12736
12792
|
const baselineFile = opts.baselineFile ? path43.isAbsolute(opts.baselineFile) ? opts.baselineFile : path43.join(root, opts.baselineFile) : path43.join(root, ".ai", "eval", "baseline.json");
|
|
12737
12793
|
if (opts.baseline) {
|
|
12738
12794
|
const snapshot = {
|
|
@@ -13308,8 +13364,8 @@ function registerDoctor(program2) {
|
|
|
13308
13364
|
fix: "haive init"
|
|
13309
13365
|
});
|
|
13310
13366
|
} else {
|
|
13311
|
-
const { readFile:
|
|
13312
|
-
const content = await
|
|
13367
|
+
const { readFile: readFile27 } = await import("fs/promises");
|
|
13368
|
+
const content = await readFile27(paths.projectContext, "utf8");
|
|
13313
13369
|
const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
|
|
13314
13370
|
if (isTemplate) {
|
|
13315
13371
|
findings.push({
|
|
@@ -13483,8 +13539,8 @@ function registerDoctor(program2) {
|
|
|
13483
13539
|
let hasClaudeEnforcement = false;
|
|
13484
13540
|
if (existsSync68(claudeSettings)) {
|
|
13485
13541
|
try {
|
|
13486
|
-
const { readFile:
|
|
13487
|
-
const raw = await
|
|
13542
|
+
const { readFile: readFile27 } = await import("fs/promises");
|
|
13543
|
+
const raw = await readFile27(claudeSettings, "utf8");
|
|
13488
13544
|
hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
|
|
13489
13545
|
} catch {
|
|
13490
13546
|
hasClaudeEnforcement = false;
|
|
@@ -13507,7 +13563,7 @@ function registerDoctor(program2) {
|
|
|
13507
13563
|
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
|
|
13508
13564
|
});
|
|
13509
13565
|
}
|
|
13510
|
-
findings.push(...await collectInstallFindings(root, "0.
|
|
13566
|
+
findings.push(...await collectInstallFindings(root, "0.15.0"));
|
|
13511
13567
|
findings.push(...await collectToolchainFindings(root));
|
|
13512
13568
|
try {
|
|
13513
13569
|
const legacyRaw = execSync3("haive-mcp --version", {
|
|
@@ -13515,7 +13571,7 @@ function registerDoctor(program2) {
|
|
|
13515
13571
|
timeout: 3e3,
|
|
13516
13572
|
stdio: ["ignore", "pipe", "ignore"]
|
|
13517
13573
|
}).trim();
|
|
13518
|
-
const cliVersion = "0.
|
|
13574
|
+
const cliVersion = "0.15.0";
|
|
13519
13575
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
13520
13576
|
findings.push({
|
|
13521
13577
|
severity: "warn",
|
|
@@ -14577,6 +14633,7 @@ import "commander";
|
|
|
14577
14633
|
import {
|
|
14578
14634
|
antiPatternGateParams as antiPatternGateParams2,
|
|
14579
14635
|
findProjectRoot as findProjectRoot52,
|
|
14636
|
+
findUncapturedFailures,
|
|
14580
14637
|
hasRecentBriefingMarker as hasRecentBriefingMarker2,
|
|
14581
14638
|
isFreshIsoDate,
|
|
14582
14639
|
loadConfig as loadConfig13,
|
|
@@ -14748,52 +14805,73 @@ ${briefing.project_context.content.slice(0, 1800)}`);
|
|
|
14748
14805
|
[setup warning] ${warning}`);
|
|
14749
14806
|
}
|
|
14750
14807
|
});
|
|
14751
|
-
enforce.command("pre-tool-use").description("Claude Code PreToolUse hook:
|
|
14808
|
+
enforce.command("pre-tool-use").description("Claude Code PreToolUse hook: surface the relevant team policy for the edited file (advise; configurable to block).").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
14752
14809
|
const payload = await readHookPayload();
|
|
14753
14810
|
const root = resolveRoot(opts.dir, payload);
|
|
14754
14811
|
if (!root) return;
|
|
14755
14812
|
const paths = resolveHaivePaths48(root);
|
|
14756
14813
|
if (!existsSync75(paths.haiveDir)) return;
|
|
14757
14814
|
if (!isWriteLikeTool(payload)) return;
|
|
14758
|
-
const
|
|
14759
|
-
if (
|
|
14760
|
-
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
|
|
14764
|
-
|
|
14815
|
+
const config = await loadConfig13(paths);
|
|
14816
|
+
if (config.enforcement?.requireBriefingFirst === false) return;
|
|
14817
|
+
const gate = config.enforcement?.preEditGate ?? "advise";
|
|
14818
|
+
const targetFiles = extractToolPaths(payload, root);
|
|
14819
|
+
const hasMarker = await hasRecentBriefingMarker2(paths, payload.session_id);
|
|
14820
|
+
const missing = targetFiles.length > 0 ? await missingRequiredMemoriesForFiles(paths, targetFiles, payload.session_id) : [];
|
|
14821
|
+
if (hasMarker && missing.length === 0) return;
|
|
14822
|
+
if (targetFiles.length > 0) {
|
|
14823
|
+
await recordFilesIntoBriefingMarker(paths, targetFiles, missing, payload.session_id).catch(() => {
|
|
14824
|
+
});
|
|
14825
|
+
}
|
|
14826
|
+
const contextText = buildPreEditContext(payload.tool_name ?? "write tool", targetFiles, missing, hasMarker);
|
|
14827
|
+
if (gate === "block") {
|
|
14765
14828
|
console.error(
|
|
14766
|
-
|
|
14767
|
-
"hAIve enforcement blocked this action.",
|
|
14768
|
-
`Tool: ${payload.tool_name ?? "write tool"}`,
|
|
14769
|
-
`Files: ${targetFiles.slice(0, 6).join(", ")}`,
|
|
14770
|
-
"",
|
|
14771
|
-
"These files have required hAIve context that was not in the current briefing:",
|
|
14772
|
-
...ids.map((id) => ` - ${id}`),
|
|
14773
|
-
"",
|
|
14774
|
-
"Load the targeted briefing before editing:",
|
|
14775
|
-
` ${briefingCommandForFiles(targetFiles)}`
|
|
14776
|
-
].join("\n")
|
|
14829
|
+
contextText + '\n\nThe relevant context is now recorded \u2014 re-issue the same edit to proceed (no `haive briefing` command needed). To make this advisory instead of blocking, set `{ "enforcement": { "preEditGate": "advise" } }` in .ai/haive.config.json.'
|
|
14777
14830
|
);
|
|
14778
14831
|
process.exit(2);
|
|
14779
14832
|
}
|
|
14780
|
-
|
|
14781
|
-
console.error(
|
|
14782
|
-
[
|
|
14783
|
-
"hAIve enforcement blocked this action.",
|
|
14784
|
-
`Tool: ${tool}`,
|
|
14785
|
-
"",
|
|
14786
|
-
"This project is initialized with hAIve. Load the team briefing before editing:",
|
|
14787
|
-
" haive enforce session-start",
|
|
14788
|
-
"or call MCP get_briefing / mem_relevant_to from your AI client.",
|
|
14789
|
-
"",
|
|
14790
|
-
"If this is intentional, a human can disable enforcement in .ai/haive.config.json:",
|
|
14791
|
-
' { "enforcement": { "requireBriefingFirst": false } }'
|
|
14792
|
-
].join("\n")
|
|
14793
|
-
);
|
|
14794
|
-
process.exit(2);
|
|
14833
|
+
emitPreToolUseContext(contextText);
|
|
14795
14834
|
});
|
|
14796
14835
|
}
|
|
14836
|
+
async function recordFilesIntoBriefingMarker(paths, files, missing, sessionId) {
|
|
14837
|
+
const existing = await readRecentBriefingMarker(paths, sessionId);
|
|
14838
|
+
const ids = new Set(existing?.memory_ids ?? []);
|
|
14839
|
+
for (const { memory: memory2 } of missing) ids.add(memory2.frontmatter.id);
|
|
14840
|
+
await writeBriefingMarker3(paths, {
|
|
14841
|
+
sessionId,
|
|
14842
|
+
task: existing?.task ?? "pre-edit auto-briefing",
|
|
14843
|
+
source: "haive-pre-edit",
|
|
14844
|
+
files,
|
|
14845
|
+
memoryIds: [...ids]
|
|
14846
|
+
});
|
|
14847
|
+
}
|
|
14848
|
+
function buildPreEditContext(tool, files, missing, hasMarker) {
|
|
14849
|
+
const lines = ["hAIve \u2014 relevant team policy for this edit", `Tool: ${tool}`];
|
|
14850
|
+
if (files.length > 0) lines.push(`Files: ${files.slice(0, 6).join(", ")}`);
|
|
14851
|
+
if (missing.length > 0) {
|
|
14852
|
+
lines.push("", "Consult these before editing (anchored to the files you are touching):");
|
|
14853
|
+
for (const { memory: memory2 } of missing.slice(0, 5)) {
|
|
14854
|
+
const fm = memory2.frontmatter;
|
|
14855
|
+
lines.push("", `### ${fm.id} (${fm.scope}/${fm.type})`, memory2.body.trim().slice(0, 900));
|
|
14856
|
+
}
|
|
14857
|
+
} else if (!hasMarker) {
|
|
14858
|
+
lines.push(
|
|
14859
|
+
"",
|
|
14860
|
+
"No team briefing was loaded yet this session. Proceeding \u2014 but for substantive work call get_briefing / mem_relevant_to for richer context."
|
|
14861
|
+
);
|
|
14862
|
+
}
|
|
14863
|
+
return lines.join("\n");
|
|
14864
|
+
}
|
|
14865
|
+
function emitPreToolUseContext(text) {
|
|
14866
|
+
console.log(
|
|
14867
|
+
JSON.stringify({
|
|
14868
|
+
hookSpecificOutput: {
|
|
14869
|
+
hookEventName: "PreToolUse",
|
|
14870
|
+
additionalContext: text
|
|
14871
|
+
}
|
|
14872
|
+
})
|
|
14873
|
+
);
|
|
14874
|
+
}
|
|
14797
14875
|
async function buildFinishReport(dir) {
|
|
14798
14876
|
const root = findProjectRoot52(dir);
|
|
14799
14877
|
const paths = resolveHaivePaths48(root);
|
|
@@ -14817,6 +14895,7 @@ async function buildFinishReport(dir) {
|
|
|
14817
14895
|
}]
|
|
14818
14896
|
});
|
|
14819
14897
|
}
|
|
14898
|
+
findings.push(...await checkFailureCapture(paths, config));
|
|
14820
14899
|
const status = await getGitSyncStatus(root);
|
|
14821
14900
|
if (!status.available) {
|
|
14822
14901
|
findings.push({
|
|
@@ -14980,6 +15059,47 @@ async function buildFinishReport(dir) {
|
|
|
14980
15059
|
findings.push(...await verifyGithubActionsForHead(root, status));
|
|
14981
15060
|
return finishReport(root, initialized, mode, findings, config);
|
|
14982
15061
|
}
|
|
15062
|
+
async function checkFailureCapture(paths, config) {
|
|
15063
|
+
const gate = config.enforcement?.failureCaptureGate ?? "warn";
|
|
15064
|
+
if (gate === "off") return [];
|
|
15065
|
+
const obsFile = path51.join(paths.haiveDir, ".cache", "observations.jsonl");
|
|
15066
|
+
if (!existsSync75(obsFile)) return [];
|
|
15067
|
+
const failures = [];
|
|
15068
|
+
try {
|
|
15069
|
+
const raw = await readFile23(obsFile, "utf8");
|
|
15070
|
+
for (const line of raw.split("\n")) {
|
|
15071
|
+
const trimmed = line.trim();
|
|
15072
|
+
if (!trimmed) continue;
|
|
15073
|
+
try {
|
|
15074
|
+
const o = JSON.parse(trimmed);
|
|
15075
|
+
if (o.failure_hint && o.ts) failures.push({ ts: o.ts, tool: o.tool ?? "?", summary: o.summary ?? "" });
|
|
15076
|
+
} catch {
|
|
15077
|
+
}
|
|
15078
|
+
}
|
|
15079
|
+
} catch {
|
|
15080
|
+
return [];
|
|
15081
|
+
}
|
|
15082
|
+
if (failures.length === 0) return [];
|
|
15083
|
+
const memories = existsSync75(paths.memoriesDir) ? await loadMemoriesFromDir38(paths.memoriesDir) : [];
|
|
15084
|
+
const captureTimes = memories.filter(({ memory: memory2 }) => ["attempt", "gotcha"].includes(memory2.frontmatter.type)).map(({ memory: memory2 }) => memory2.frontmatter.created_at);
|
|
15085
|
+
const uncaptured = findUncapturedFailures(failures, captureTimes);
|
|
15086
|
+
if (uncaptured.length === 0) {
|
|
15087
|
+
return [{
|
|
15088
|
+
severity: "ok",
|
|
15089
|
+
code: "failure-capture-clean",
|
|
15090
|
+
message: "No uncaptured hard failures from this session."
|
|
15091
|
+
}];
|
|
15092
|
+
}
|
|
15093
|
+
return [{
|
|
15094
|
+
severity: gate === "block" ? "error" : "info",
|
|
15095
|
+
code: "uncaptured-failures",
|
|
15096
|
+
message: `${uncaptured.length} hard failure(s) this session were never captured as a lesson (mem_tried).`,
|
|
15097
|
+
fix: "Call `mem_tried` (or `haive memory tried`) for each real failure so the next session doesn't repeat it. False positives (e.g. a grep that found nothing) can be ignored.",
|
|
15098
|
+
reason: "Harness ratchet: a mistake that isn't written down gets re-introduced. Set enforcement.failureCaptureGate to 'off' to disable, or 'block' to hard-fail.",
|
|
15099
|
+
affected_files: uncaptured.slice(0, 8).map((f) => `${f.tool}: ${f.summary}`.slice(0, 100)),
|
|
15100
|
+
...gate === "block" ? { impact: 30 } : {}
|
|
15101
|
+
}];
|
|
15102
|
+
}
|
|
14983
15103
|
function finishReport(root, initialized, mode, findings, config) {
|
|
14984
15104
|
const score = buildScore(findings, config.enforcement?.scoreThreshold);
|
|
14985
15105
|
const hasErrors = findings.some((f) => f.severity === "error");
|
|
@@ -15124,7 +15244,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
15124
15244
|
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
15125
15245
|
});
|
|
15126
15246
|
}
|
|
15127
|
-
findings.push(...await inspectIntegrationVersions(root, "0.
|
|
15247
|
+
findings.push(...await inspectIntegrationVersions(root, "0.15.0"));
|
|
15128
15248
|
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
15129
15249
|
const hasBriefing = await hasRecentBriefingMarker2(paths, sessionId);
|
|
15130
15250
|
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
|
|
@@ -15242,7 +15362,7 @@ async function verifyMemoryPolicy(paths, config) {
|
|
|
15242
15362
|
}
|
|
15243
15363
|
async function verifyDecisionCoverage(paths, stage, sessionId) {
|
|
15244
15364
|
if (!existsSync75(paths.memoriesDir)) return [];
|
|
15245
|
-
const changedFiles = await getChangedFiles(paths.root, stage);
|
|
15365
|
+
const changedFiles = (await getChangedFiles(paths.root, stage)).filter((f) => !isGeneratedArtifact(f));
|
|
15246
15366
|
if (changedFiles.length === 0) {
|
|
15247
15367
|
return [{ severity: "info", code: "decision-coverage-no-changes", message: "No changed files to match against policy memories." }];
|
|
15248
15368
|
}
|
|
@@ -16039,8 +16159,10 @@ async function missingRequiredMemoriesForFiles(paths, files, sessionId) {
|
|
|
16039
16159
|
return memoryMatchesAnchorPaths6(memory2, files);
|
|
16040
16160
|
}).map(({ memory: memory2, filePath }) => ({ memory: memory2, filePath }));
|
|
16041
16161
|
}
|
|
16042
|
-
function
|
|
16043
|
-
|
|
16162
|
+
function isGeneratedArtifact(file) {
|
|
16163
|
+
if (file === ".ai/project-context.md" || file === ".ai/code-map.json") return true;
|
|
16164
|
+
if (file.startsWith(".ai/.cache/") || file.startsWith(".ai/.runtime/") || file.startsWith(".ai/.usage/")) return true;
|
|
16165
|
+
return false;
|
|
16044
16166
|
}
|
|
16045
16167
|
async function readStdin2(maxBytes) {
|
|
16046
16168
|
if (process.stdin.isTTY) return "";
|
|
@@ -16118,12 +16240,14 @@ import {
|
|
|
16118
16240
|
appendPreventionEvent as appendPreventionEvent2,
|
|
16119
16241
|
findProjectRoot as findProjectRoot53,
|
|
16120
16242
|
isRetiredMemory as isRetiredMemory3,
|
|
16243
|
+
loadConfig as loadConfig14,
|
|
16121
16244
|
loadMemoriesFromDir as loadMemoriesFromDir39,
|
|
16122
16245
|
loadUsageIndex as loadUsageIndex29,
|
|
16123
16246
|
recordPrevention as recordPrevention2,
|
|
16124
16247
|
resolveHaivePaths as resolveHaivePaths49,
|
|
16125
16248
|
runSensors as runSensors2,
|
|
16126
16249
|
saveUsageIndex as saveUsageIndex8,
|
|
16250
|
+
selectCommandSensors,
|
|
16127
16251
|
sensorTargetsFromDiff as sensorTargetsFromDiff2,
|
|
16128
16252
|
serializeMemory as serializeMemory27
|
|
16129
16253
|
} from "@hiveai/core";
|
|
@@ -16151,14 +16275,38 @@ function registerSensors(program2) {
|
|
|
16151
16275
|
if (row.last_fired) console.log(` ${ui.dim("last fired:")} ${row.last_fired}`);
|
|
16152
16276
|
}
|
|
16153
16277
|
});
|
|
16154
|
-
sensors.command("check").description(
|
|
16278
|
+
sensors.command("check").description(
|
|
16279
|
+
"Run regex sensors against a diff (the deterministic/computational layer); defaults to `git diff --cached`.\n Diff-scan layers: `sensors check` (regex) and `anti_patterns_check` (memory match) are components;\n `pre_commit_check` combines them; `haive enforce check` is THE gate that runs at commit."
|
|
16280
|
+
).option("--diff-file <path>", "read unified diff from a file instead of staged changes").option("--json", "emit JSON", false).option("--commands", "ALSO execute shell/test sensors (runs repo-authored commands)", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
16155
16281
|
const root = findProjectRoot53(opts.dir);
|
|
16156
16282
|
const paths = resolveHaivePaths49(root);
|
|
16157
16283
|
const memories = await runnableSensorMemories(paths);
|
|
16158
16284
|
const diff = opts.diffFile ? await readFile24(path53.resolve(root, opts.diffFile), "utf8") : await stagedDiff(root);
|
|
16159
16285
|
const targets = sensorTargetsFromDiff2(diff);
|
|
16160
16286
|
const hits = runSensors2(memories, targets.length > 0 ? targets : [{ path: "", content: diff }]);
|
|
16161
|
-
const
|
|
16287
|
+
const config = await loadConfig14(paths);
|
|
16288
|
+
const runCommands = opts.commands || config.enforcement?.runCommandSensors === true;
|
|
16289
|
+
const changedPaths = targets.map((t) => t.path).filter(Boolean);
|
|
16290
|
+
const allSensorMemories = await runnableSensorMemories(paths, false);
|
|
16291
|
+
const commandSpecs = selectCommandSensors(allSensorMemories, changedPaths);
|
|
16292
|
+
const commandHits = [];
|
|
16293
|
+
const commandSkipped = [];
|
|
16294
|
+
if (commandSpecs.length > 0 && runCommands) {
|
|
16295
|
+
for (const spec of commandSpecs) {
|
|
16296
|
+
const failed = await runCommandSensor(spec, root);
|
|
16297
|
+
if (failed) {
|
|
16298
|
+
commandHits.push({
|
|
16299
|
+
memory_id: spec.memory_id,
|
|
16300
|
+
severity: spec.severity,
|
|
16301
|
+
message: spec.message,
|
|
16302
|
+
matched_line: `command failed: ${spec.command}`
|
|
16303
|
+
});
|
|
16304
|
+
}
|
|
16305
|
+
}
|
|
16306
|
+
} else if (commandSpecs.length > 0) {
|
|
16307
|
+
for (const spec of commandSpecs) commandSkipped.push(spec.memory_id);
|
|
16308
|
+
}
|
|
16309
|
+
const firedIds = [...new Set([...hits, ...commandHits].map((hit) => hit.memory_id))];
|
|
16162
16310
|
if (firedIds.length > 0) {
|
|
16163
16311
|
const usage = await loadUsageIndex29(paths);
|
|
16164
16312
|
const recordedIds = [];
|
|
@@ -16181,12 +16329,15 @@ function registerSensors(program2) {
|
|
|
16181
16329
|
severity: hit.severity,
|
|
16182
16330
|
message: hit.message,
|
|
16183
16331
|
matched_line: hit.matched_line
|
|
16184
|
-
}))
|
|
16332
|
+
})),
|
|
16333
|
+
command_hits: commandHits,
|
|
16334
|
+
command_skipped: commandSkipped
|
|
16185
16335
|
};
|
|
16186
16336
|
if (opts.json) {
|
|
16187
16337
|
console.log(JSON.stringify(output, null, 2));
|
|
16188
16338
|
} else {
|
|
16189
|
-
|
|
16339
|
+
const total = hits.length + commandHits.length;
|
|
16340
|
+
console.log(ui.bold(`hAIve sensors check \u2014 ${total} hit(s), ${memories.length} regex + ${commandSpecs.length} command sensor(s)`));
|
|
16190
16341
|
for (const hit of hits) {
|
|
16191
16342
|
const marker = hit.severity === "block" ? ui.red("\u2717") : ui.yellow("\u26A0");
|
|
16192
16343
|
console.log(` ${marker} ${hit.memory_id} ${ui.dim(`(${hit.severity})`)}`);
|
|
@@ -16194,8 +16345,17 @@ function registerSensors(program2) {
|
|
|
16194
16345
|
console.log(` ${hit.message}`);
|
|
16195
16346
|
if (hit.matched_line) console.log(` ${ui.dim(hit.matched_line)}`);
|
|
16196
16347
|
}
|
|
16348
|
+
for (const hit of commandHits) {
|
|
16349
|
+
const marker = hit.severity === "block" ? ui.red("\u2717") : ui.yellow("\u26A0");
|
|
16350
|
+
console.log(` ${marker} ${hit.memory_id} ${ui.dim(`(${hit.severity}, command)`)}`);
|
|
16351
|
+
console.log(` ${hit.message}`);
|
|
16352
|
+
console.log(` ${ui.dim(hit.matched_line)}`);
|
|
16353
|
+
}
|
|
16354
|
+
if (commandSkipped.length > 0) {
|
|
16355
|
+
console.log(ui.dim(` ${commandSkipped.length} command sensor(s) not run \u2014 pass --commands or set enforcement.runCommandSensors.`));
|
|
16356
|
+
}
|
|
16197
16357
|
}
|
|
16198
|
-
if (hits.some((hit) => hit.severity === "block")) process.exitCode = 1;
|
|
16358
|
+
if ([...hits, ...commandHits].some((hit) => hit.severity === "block")) process.exitCode = 1;
|
|
16199
16359
|
});
|
|
16200
16360
|
sensors.command("promote").description("Promote or demote an existing memory sensor severity").argument("<memory-id>", "memory id carrying the sensor").option("--severity <severity>", "block | warn", "block").option("--yes", "confirm promotion to block severity", false).option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
16201
16361
|
const severity = opts.severity ?? "block";
|
|
@@ -16282,6 +16442,14 @@ async function runnableSensorMemories(paths, regexOnly = true) {
|
|
|
16282
16442
|
return !isRetiredMemory3(memory2.frontmatter, memory2.body);
|
|
16283
16443
|
});
|
|
16284
16444
|
}
|
|
16445
|
+
async function runCommandSensor(spec, root) {
|
|
16446
|
+
try {
|
|
16447
|
+
await exec2("bash", ["-c", spec.command], { cwd: root, timeout: 12e4, maxBuffer: 8 * 1024 * 1024 });
|
|
16448
|
+
return false;
|
|
16449
|
+
} catch {
|
|
16450
|
+
return true;
|
|
16451
|
+
}
|
|
16452
|
+
}
|
|
16285
16453
|
async function stagedDiff(root) {
|
|
16286
16454
|
try {
|
|
16287
16455
|
const { stdout } = await exec2("git", ["diff", "--cached"], { cwd: root });
|
|
@@ -16515,6 +16683,7 @@ import "commander";
|
|
|
16515
16683
|
import {
|
|
16516
16684
|
buildDashboard,
|
|
16517
16685
|
findProjectRoot as findProjectRoot55,
|
|
16686
|
+
loadConfig as loadConfig15,
|
|
16518
16687
|
loadMemoriesFromDir as loadMemoriesFromDir41,
|
|
16519
16688
|
loadPreventionEvents,
|
|
16520
16689
|
loadUsageIndex as loadUsageIndex30,
|
|
@@ -16534,11 +16703,13 @@ function registerDashboard(program2) {
|
|
|
16534
16703
|
const memories = existsSync78(paths.memoriesDir) ? await loadMemoriesFromDir41(paths.memoriesDir) : [];
|
|
16535
16704
|
const usage = await loadUsageIndex30(paths);
|
|
16536
16705
|
const preventionEvents = await loadPreventionEvents(paths);
|
|
16706
|
+
const config = await loadConfig15(paths);
|
|
16537
16707
|
const top = Math.max(1, Number.parseInt(opts.top ?? "10", 10) || 10);
|
|
16538
16708
|
const dormantDays = opts.dormantDays ? Number.parseInt(opts.dormantDays, 10) : void 0;
|
|
16539
16709
|
const report = buildDashboard(memories, usage, {
|
|
16540
16710
|
top,
|
|
16541
16711
|
preventionEvents,
|
|
16712
|
+
antiPatternGate: config.enforcement?.antiPatternGate ?? "anchored",
|
|
16542
16713
|
...dormantDays !== void 0 && Number.isFinite(dormantDays) ? { dormantDays } : {}
|
|
16543
16714
|
});
|
|
16544
16715
|
if (opts.json) {
|
|
@@ -16549,7 +16720,7 @@ function registerDashboard(program2) {
|
|
|
16549
16720
|
});
|
|
16550
16721
|
}
|
|
16551
16722
|
function renderDashboard(r) {
|
|
16552
|
-
const { inventory: inv, impact, sensors, health, decay, corpus, prevention } = r;
|
|
16723
|
+
const { inventory: inv, impact, sensors, health, decay, corpus, prevention, gate_precision: gate } = r;
|
|
16553
16724
|
console.log(ui.bold("hAIve dashboard"));
|
|
16554
16725
|
console.log(
|
|
16555
16726
|
` ${ui.dim("corpus:")} ${inv.total} policy memor${inv.total === 1 ? "y" : "ies"} (${inv.active} active, ${inv.retired} retired) \xB7 ${inv.session_recaps} recap(s) \xB7 ~${corpus.est_tokens.toLocaleString()} tokens`
|
|
@@ -16600,6 +16771,15 @@ function renderDashboard(r) {
|
|
|
16600
16771
|
}
|
|
16601
16772
|
}
|
|
16602
16773
|
console.log();
|
|
16774
|
+
console.log(ui.bold("Gate precision") + ui.dim(" (is the anti-pattern gate real or noisy?)"));
|
|
16775
|
+
const precisionLabel = gate.precision === null ? ui.dim("no signal yet") : gate.precision >= 0.7 ? ui.green(`${Math.round(gate.precision * 100)}%`) : ui.yellow(`${Math.round(gate.precision * 100)}%`);
|
|
16776
|
+
console.log(
|
|
16777
|
+
` ${precisionLabel} precision \xB7 ${gate.useful} useful (sensor ${gate.sensor_catches} \xB7 anti-pattern ${gate.anti_pattern_catches}) \xB7 ${gate.rejections > 0 ? ui.yellow(`${gate.rejections} rejected`) : "0 rejected"}`
|
|
16778
|
+
);
|
|
16779
|
+
if (gate.suggestion) {
|
|
16780
|
+
ui.info(`Tuning: set enforcement.antiPatternGate="${gate.suggestion.recommended}" \u2014 ${gate.suggestion.reason}`);
|
|
16781
|
+
}
|
|
16782
|
+
console.log();
|
|
16603
16783
|
console.log(ui.bold("Health"));
|
|
16604
16784
|
console.log(
|
|
16605
16785
|
` stale ${warnNum(health.stale)} \xB7 anchorless ${warnNum(health.anchorless)} \xB7 pending ${health.pending} \xB7 prune candidates ${warnNum(health.prune_candidates)}`
|
|
@@ -16634,9 +16814,331 @@ function warnNum(n) {
|
|
|
16634
16814
|
return n > 0 ? ui.yellow(String(n)) : String(n);
|
|
16635
16815
|
}
|
|
16636
16816
|
|
|
16817
|
+
// src/commands/dev-link.ts
|
|
16818
|
+
import { execFile as execFile3 } from "child_process";
|
|
16819
|
+
import { cp, readFile as readFile26 } from "fs/promises";
|
|
16820
|
+
import { existsSync as existsSync79 } from "fs";
|
|
16821
|
+
import path55 from "path";
|
|
16822
|
+
import { promisify as promisify3 } from "util";
|
|
16823
|
+
import "commander";
|
|
16824
|
+
import { findProjectRoot as findProjectRoot56 } from "@hiveai/core";
|
|
16825
|
+
var exec3 = promisify3(execFile3);
|
|
16826
|
+
function registerDevLink(program2) {
|
|
16827
|
+
const dev = program2.commands.find((c) => c.name() === "dev") ?? program2.command("dev").description("Developer utilities for working on hAIve itself.");
|
|
16828
|
+
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) => {
|
|
16829
|
+
const root = findProjectRoot56(opts.dir);
|
|
16830
|
+
if (!existsSync79(path55.join(root, "packages", "cli", "dist", "index.js"))) {
|
|
16831
|
+
ui.error(`Not the hAIve monorepo (no packages/cli/dist) at ${root}. Run \`pnpm -r build\` first, or pass --dir.`);
|
|
16832
|
+
process.exitCode = 1;
|
|
16833
|
+
return;
|
|
16834
|
+
}
|
|
16835
|
+
let globalModules;
|
|
16836
|
+
try {
|
|
16837
|
+
globalModules = (await exec3("npm", ["root", "-g"])).stdout.trim();
|
|
16838
|
+
} catch {
|
|
16839
|
+
globalModules = path55.join(path55.dirname(path55.dirname(process.execPath)), "lib", "node_modules");
|
|
16840
|
+
}
|
|
16841
|
+
const globalHive = path55.join(globalModules, "@hiveai");
|
|
16842
|
+
if (!existsSync79(globalHive)) {
|
|
16843
|
+
ui.error(`No global @hiveai install at ${globalHive}. Install once with \`npm i -g @hiveai/cli\`, then re-run.`);
|
|
16844
|
+
process.exitCode = 1;
|
|
16845
|
+
return;
|
|
16846
|
+
}
|
|
16847
|
+
const linked = [];
|
|
16848
|
+
const copyDist = async (fromPkg, toDistDir) => {
|
|
16849
|
+
const from = path55.join(root, "packages", fromPkg, "dist");
|
|
16850
|
+
if (!existsSync79(from) || !existsSync79(path55.dirname(toDistDir))) return;
|
|
16851
|
+
await cp(from, toDistDir, { recursive: true });
|
|
16852
|
+
linked.push(path55.relative(globalModules, toDistDir));
|
|
16853
|
+
};
|
|
16854
|
+
for (const pkg of ["cli", "mcp"]) {
|
|
16855
|
+
await copyDist(pkg, path55.join(globalHive, pkg, "dist"));
|
|
16856
|
+
for (const nested of ["core", "embeddings"]) {
|
|
16857
|
+
await copyDist(nested, path55.join(globalHive, pkg, "node_modules", "@hiveai", nested, "dist"));
|
|
16858
|
+
}
|
|
16859
|
+
}
|
|
16860
|
+
await copyDist("core", path55.join(globalHive, "core", "dist"));
|
|
16861
|
+
let version = "unknown";
|
|
16862
|
+
try {
|
|
16863
|
+
version = JSON.parse(await readFile26(path55.join(root, "package.json"), "utf8")).version ?? "unknown";
|
|
16864
|
+
} catch {
|
|
16865
|
+
}
|
|
16866
|
+
if (opts.json) {
|
|
16867
|
+
console.log(JSON.stringify({ ok: linked.length > 0, version, global_root: globalHive, linked }, null, 2));
|
|
16868
|
+
return;
|
|
16869
|
+
}
|
|
16870
|
+
if (linked.length === 0) {
|
|
16871
|
+
ui.warn("Nothing linked \u2014 no matching dist targets were found in the global install.");
|
|
16872
|
+
return;
|
|
16873
|
+
}
|
|
16874
|
+
ui.success(`Linked local dist (v${version}) into the global @hiveai install:`);
|
|
16875
|
+
for (const t of linked) console.log(` ${ui.dim("\u2192")} ${t}`);
|
|
16876
|
+
console.log(ui.dim("The global `haive` now runs your local build (git hooks + MCP included)."));
|
|
16877
|
+
});
|
|
16878
|
+
}
|
|
16879
|
+
|
|
16880
|
+
// src/commands/coverage.ts
|
|
16881
|
+
import "commander";
|
|
16882
|
+
import { findCoverageGaps, findProjectRoot as findProjectRoot57, resolveHaivePaths as resolveHaivePaths52 } from "@hiveai/core";
|
|
16883
|
+
function isNoisePath(p) {
|
|
16884
|
+
if (/(^|\/)(node_modules|dist|build|coverage|\.next)\//.test(p)) return true;
|
|
16885
|
+
if (p.startsWith(".ai/")) return true;
|
|
16886
|
+
if (/\.(jsonl|lock|map|snap|min\.js)$/.test(p)) return true;
|
|
16887
|
+
if (/(^|\/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$/.test(p)) return true;
|
|
16888
|
+
if (/(^|\/)(CHANGELOG|LICENSE)(\.md)?$/.test(p)) return true;
|
|
16889
|
+
return false;
|
|
16890
|
+
}
|
|
16891
|
+
function registerCoverage(program2) {
|
|
16892
|
+
program2.command("coverage").description(
|
|
16893
|
+
"Coverage-gap report: frequently-edited files with no covering team memory (blind spots)."
|
|
16894
|
+
).option("--json", "emit JSON", false).option("--min-changes <n>", "minimum git-churn count to flag a file", "3").option("--limit <n>", "max gaps to report", "20").option("--days <n>", "git-history lookback window in days", "90").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
16895
|
+
const root = findProjectRoot57(opts.dir);
|
|
16896
|
+
const paths = resolveHaivePaths52(root);
|
|
16897
|
+
const minChanges = Math.max(1, parseInt(opts.minChanges ?? "3", 10) || 3);
|
|
16898
|
+
const limit = Math.max(1, parseInt(opts.limit ?? "20", 10) || 20);
|
|
16899
|
+
const days = Math.max(1, parseInt(opts.days ?? "90", 10) || 90);
|
|
16900
|
+
const radar = await buildRadar({
|
|
16901
|
+
root,
|
|
16902
|
+
taskTokens: null,
|
|
16903
|
+
filePaths: [],
|
|
16904
|
+
daysBack: Math.ceil(days / 6),
|
|
16905
|
+
// getHotFiles multiplies daysBack by 6
|
|
16906
|
+
maxHotFiles: 500
|
|
16907
|
+
});
|
|
16908
|
+
const hotFiles = radar.hotFiles.filter((h) => !isNoisePath(h.path)).map((h) => ({ path: h.path, changes: h.changes }));
|
|
16909
|
+
const memories = await loadMemoriesFromDir27(paths.memoriesDir);
|
|
16910
|
+
const gaps = findCoverageGaps(hotFiles, memories, { minChanges, limit });
|
|
16911
|
+
if (opts.json) {
|
|
16912
|
+
console.log(JSON.stringify({ root, scanned_hot_files: hotFiles.length, gaps }, null, 2));
|
|
16913
|
+
return;
|
|
16914
|
+
}
|
|
16915
|
+
if (!radar.insideGitRepo) {
|
|
16916
|
+
ui.warn("Not a git repository \u2014 coverage uses git churn to find hot files.");
|
|
16917
|
+
return;
|
|
16918
|
+
}
|
|
16919
|
+
if (gaps.length === 0) {
|
|
16920
|
+
ui.success(`No coverage gaps: every file changed \u2265${minChanges}\xD7 is covered by a team memory.`);
|
|
16921
|
+
return;
|
|
16922
|
+
}
|
|
16923
|
+
console.log(ui.bold(`hAIve coverage \u2014 ${gaps.length} blind spot(s) (hot files with no covering memory)`));
|
|
16924
|
+
for (const gap of gaps) {
|
|
16925
|
+
console.log(` ${ui.yellow("\u25CB")} ${gap.path} ${ui.dim(`(${gap.changes} change${gap.changes === 1 ? "" : "s"})`)}`);
|
|
16926
|
+
}
|
|
16927
|
+
console.log(
|
|
16928
|
+
ui.dim(
|
|
16929
|
+
"\nAdd a decision/convention/gotcha anchored to the top files, or a sensor, to close the gap."
|
|
16930
|
+
)
|
|
16931
|
+
);
|
|
16932
|
+
});
|
|
16933
|
+
}
|
|
16934
|
+
|
|
16935
|
+
// src/commands/merge-driver.ts
|
|
16936
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
16937
|
+
import { readFileSync, writeFileSync, existsSync as existsSync80 } from "fs";
|
|
16938
|
+
import path56 from "path";
|
|
16939
|
+
import "commander";
|
|
16940
|
+
import { findProjectRoot as findProjectRoot58, mergeMemoryVersions } from "@hiveai/core";
|
|
16941
|
+
var GITATTRIBUTES_MARK = "# hAIve merge driver";
|
|
16942
|
+
var GITATTRIBUTES_BLOCK = [
|
|
16943
|
+
GITATTRIBUTES_MARK,
|
|
16944
|
+
".ai/memories/**/*.md merge=haive",
|
|
16945
|
+
"# hAIve merge driver end"
|
|
16946
|
+
].join("\n");
|
|
16947
|
+
function registerMergeDriver(program2) {
|
|
16948
|
+
const cmd = program2.command("merge-driver").description("Deterministic git merge driver for hAIve memory files (kills .ai/ conflict markers)");
|
|
16949
|
+
cmd.command("run <base> <ours> <theirs>").description("Git merge-driver entrypoint: resolve ours/theirs by frontmatter order, write into <ours>").action((base, ours, theirs) => {
|
|
16950
|
+
try {
|
|
16951
|
+
const oursContent = readFileSync(ours, "utf8");
|
|
16952
|
+
const theirsContent = readFileSync(theirs, "utf8");
|
|
16953
|
+
const result = mergeMemoryVersions(oursContent, theirsContent);
|
|
16954
|
+
if (result.content !== oursContent) writeFileSync(ours, result.content, "utf8");
|
|
16955
|
+
process.exit(0);
|
|
16956
|
+
} catch {
|
|
16957
|
+
process.exit(1);
|
|
16958
|
+
}
|
|
16959
|
+
});
|
|
16960
|
+
cmd.command("install").description("Configure git + .gitattributes so memory-file conflicts auto-resolve").option("-d, --dir <dir>", "project root").action((opts) => {
|
|
16961
|
+
const root = findProjectRoot58(opts.dir);
|
|
16962
|
+
try {
|
|
16963
|
+
execFileSync3("git", ["config", "merge.haive.name", "hAIve memory merge driver"], { cwd: root });
|
|
16964
|
+
execFileSync3("git", ["config", "merge.haive.driver", "haive merge-driver run %O %A %B"], { cwd: root });
|
|
16965
|
+
} catch {
|
|
16966
|
+
ui.error("Could not set git config \u2014 is this a git repository?");
|
|
16967
|
+
process.exitCode = 1;
|
|
16968
|
+
return;
|
|
16969
|
+
}
|
|
16970
|
+
const gaPath = path56.join(root, ".gitattributes");
|
|
16971
|
+
let content = existsSync80(gaPath) ? readFileSync(gaPath, "utf8") : "";
|
|
16972
|
+
if (!content.includes(GITATTRIBUTES_MARK)) {
|
|
16973
|
+
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
16974
|
+
content += GITATTRIBUTES_BLOCK + "\n";
|
|
16975
|
+
writeFileSync(gaPath, content, "utf8");
|
|
16976
|
+
ui.success("Installed hAIve merge driver (git config + .gitattributes).");
|
|
16977
|
+
} else {
|
|
16978
|
+
ui.info("hAIve merge driver already present in .gitattributes \u2014 refreshed git config.");
|
|
16979
|
+
}
|
|
16980
|
+
ui.info("Memory-file conflicts under .ai/memories/ now resolve by revision_count \u2192 created_at.");
|
|
16981
|
+
});
|
|
16982
|
+
}
|
|
16983
|
+
|
|
16984
|
+
// src/commands/memory-resolve-conflict.ts
|
|
16985
|
+
import { writeFile as writeFile38 } from "fs/promises";
|
|
16986
|
+
import { existsSync as existsSync81 } from "fs";
|
|
16987
|
+
import "commander";
|
|
16988
|
+
import {
|
|
16989
|
+
findProjectRoot as findProjectRoot59,
|
|
16990
|
+
planConflictResolution,
|
|
16991
|
+
resolveHaivePaths as resolveHaivePaths53,
|
|
16992
|
+
serializeMemory as serializeMemory29
|
|
16993
|
+
} from "@hiveai/core";
|
|
16994
|
+
function registerMemoryResolveConflict(memory2) {
|
|
16995
|
+
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) => {
|
|
16996
|
+
const root = findProjectRoot59(opts.dir);
|
|
16997
|
+
const paths = resolveHaivePaths53(root);
|
|
16998
|
+
if (!existsSync81(paths.memoriesDir)) {
|
|
16999
|
+
ui.error(`No .ai/memories at ${root}.`);
|
|
17000
|
+
process.exitCode = 1;
|
|
17001
|
+
return;
|
|
17002
|
+
}
|
|
17003
|
+
const memories = await loadMemoriesFromDir27(paths.memoriesDir);
|
|
17004
|
+
const a = memories.find((m) => m.memory.frontmatter.id === idA);
|
|
17005
|
+
const b = memories.find((m) => m.memory.frontmatter.id === idB);
|
|
17006
|
+
if (!a || !b) {
|
|
17007
|
+
ui.error(`Memory not found: ${!a ? idA : ""} ${!b ? idB : ""}`.trim());
|
|
17008
|
+
process.exitCode = 1;
|
|
17009
|
+
return;
|
|
17010
|
+
}
|
|
17011
|
+
const plan = planConflictResolution(a, b);
|
|
17012
|
+
const loser = plan.supersede_id === idA ? a : b;
|
|
17013
|
+
if (opts.json) {
|
|
17014
|
+
console.log(JSON.stringify({ ...plan, applied: Boolean(opts.yes) }, null, 2));
|
|
17015
|
+
} else {
|
|
17016
|
+
console.log(ui.bold("Conflict resolution"));
|
|
17017
|
+
console.log(` keep: ${ui.green(plan.keep_id)}`);
|
|
17018
|
+
console.log(` supersede: ${ui.red(plan.supersede_id)} ${ui.dim(`\u2192 deprecated`)}`);
|
|
17019
|
+
console.log(` reason: ${plan.reason}`);
|
|
17020
|
+
}
|
|
17021
|
+
if (!opts.yes) {
|
|
17022
|
+
if (!opts.json) ui.info("Preview only \u2014 re-run with --yes to apply.");
|
|
17023
|
+
return;
|
|
17024
|
+
}
|
|
17025
|
+
await writeFile38(
|
|
17026
|
+
loser.filePath,
|
|
17027
|
+
serializeMemory29({
|
|
17028
|
+
frontmatter: {
|
|
17029
|
+
...loser.memory.frontmatter,
|
|
17030
|
+
status: "deprecated",
|
|
17031
|
+
stale_reason: plan.stale_reason,
|
|
17032
|
+
related_ids: [.../* @__PURE__ */ new Set([...loser.memory.frontmatter.related_ids, plan.keep_id])]
|
|
17033
|
+
},
|
|
17034
|
+
body: loser.memory.body
|
|
17035
|
+
}),
|
|
17036
|
+
"utf8"
|
|
17037
|
+
);
|
|
17038
|
+
if (!opts.json) ui.success(`Deprecated ${plan.supersede_id}; ${plan.keep_id} remains authoritative.`);
|
|
17039
|
+
});
|
|
17040
|
+
}
|
|
17041
|
+
|
|
17042
|
+
// src/commands/memory-seed-git.ts
|
|
17043
|
+
import { execFile as execFile4 } from "child_process";
|
|
17044
|
+
import { mkdir as mkdir24, writeFile as writeFile39 } from "fs/promises";
|
|
17045
|
+
import { existsSync as existsSync83 } from "fs";
|
|
17046
|
+
import path57 from "path";
|
|
17047
|
+
import { promisify as promisify4 } from "util";
|
|
17048
|
+
import "commander";
|
|
17049
|
+
import {
|
|
17050
|
+
buildFrontmatter as buildFrontmatter12,
|
|
17051
|
+
findProjectRoot as findProjectRoot60,
|
|
17052
|
+
memoryFilePath as memoryFilePath13,
|
|
17053
|
+
proposeSeedsFromCommits,
|
|
17054
|
+
resolveHaivePaths as resolveHaivePaths54,
|
|
17055
|
+
serializeMemory as serializeMemory30
|
|
17056
|
+
} from "@hiveai/core";
|
|
17057
|
+
var exec4 = promisify4(execFile4);
|
|
17058
|
+
function registerMemorySeedGit(memory2) {
|
|
17059
|
+
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) => {
|
|
17060
|
+
const root = findProjectRoot60(opts.dir);
|
|
17061
|
+
const paths = resolveHaivePaths54(root);
|
|
17062
|
+
if (!existsSync83(paths.haiveDir)) {
|
|
17063
|
+
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
17064
|
+
process.exitCode = 1;
|
|
17065
|
+
return;
|
|
17066
|
+
}
|
|
17067
|
+
const limit = Math.max(1, parseInt(opts.limit ?? "20", 10) || 20);
|
|
17068
|
+
const days = Math.max(1, parseInt(opts.days ?? "365", 10) || 365);
|
|
17069
|
+
const commits = await readCommits(root, days);
|
|
17070
|
+
const proposals = proposeSeedsFromCommits(commits, limit);
|
|
17071
|
+
if (opts.json) {
|
|
17072
|
+
console.log(JSON.stringify({ scanned_commits: commits.length, proposals, applied: Boolean(opts.apply) }, null, 2));
|
|
17073
|
+
} else if (proposals.length === 0) {
|
|
17074
|
+
ui.info("No revert/hotfix signals found in git history \u2014 nothing to seed.");
|
|
17075
|
+
return;
|
|
17076
|
+
} else {
|
|
17077
|
+
console.log(ui.bold(`hAIve seed-git \u2014 ${proposals.length} proposal(s) from ${commits.length} commit(s)`));
|
|
17078
|
+
for (const p of proposals) {
|
|
17079
|
+
console.log(` ${ui.yellow("\u25C6")} ${ui.dim(`[${p.kind}]`)} ${p.what}`);
|
|
17080
|
+
if (p.paths.length > 0) console.log(` ${ui.dim("paths:")} ${p.paths.join(", ")}`);
|
|
17081
|
+
}
|
|
17082
|
+
}
|
|
17083
|
+
if (!opts.apply) {
|
|
17084
|
+
if (!opts.json) ui.info("Preview only \u2014 re-run with --apply to write these as draft memories.");
|
|
17085
|
+
return;
|
|
17086
|
+
}
|
|
17087
|
+
let written = 0;
|
|
17088
|
+
for (const p of proposals) {
|
|
17089
|
+
const fm = {
|
|
17090
|
+
...buildFrontmatter12({
|
|
17091
|
+
type: "attempt",
|
|
17092
|
+
slug: p.slug,
|
|
17093
|
+
scope: opts.scope ?? "team",
|
|
17094
|
+
tags: ["seed", "git-history", p.kind],
|
|
17095
|
+
paths: p.paths
|
|
17096
|
+
}),
|
|
17097
|
+
status: "draft"
|
|
17098
|
+
// human reviews before it becomes validated
|
|
17099
|
+
};
|
|
17100
|
+
const body = `# ${p.what}
|
|
17101
|
+
|
|
17102
|
+
**Why it failed / do NOT use:** ${p.why_failed}
|
|
17103
|
+
|
|
17104
|
+
_Seeded from git ${p.kind} commit ${p.source_sha}. Review and validate (or delete) \u2014 not yet authoritative._
|
|
17105
|
+
`;
|
|
17106
|
+
const file = memoryFilePath13(paths, fm.scope, fm.id, fm.module);
|
|
17107
|
+
if (existsSync83(file)) continue;
|
|
17108
|
+
await mkdir24(path57.dirname(file), { recursive: true });
|
|
17109
|
+
await writeFile39(file, serializeMemory30({ frontmatter: fm, body }), "utf8");
|
|
17110
|
+
written += 1;
|
|
17111
|
+
}
|
|
17112
|
+
if (!opts.json) {
|
|
17113
|
+
ui.success(`Wrote ${written} draft seed(s). Review them: \`haive memory pending\` \u2192 validate or delete.`);
|
|
17114
|
+
}
|
|
17115
|
+
});
|
|
17116
|
+
}
|
|
17117
|
+
async function readCommits(root, days) {
|
|
17118
|
+
try {
|
|
17119
|
+
const { stdout } = await exec4(
|
|
17120
|
+
"git",
|
|
17121
|
+
["log", `--since=${days}.days.ago`, "--name-only", "--pretty=format:%x1f%h%x1f%s", "-n", "500"],
|
|
17122
|
+
{ cwd: root, maxBuffer: 8 * 1024 * 1024 }
|
|
17123
|
+
);
|
|
17124
|
+
const blocks = stdout.split("").filter((b) => b.length > 0);
|
|
17125
|
+
const commits = [];
|
|
17126
|
+
for (let i = 0; i + 1 < blocks.length; i += 2) {
|
|
17127
|
+
const sha = blocks[i].trim();
|
|
17128
|
+
const tail = blocks[i + 1];
|
|
17129
|
+
const lines = tail.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
17130
|
+
const subject = lines.shift() ?? "";
|
|
17131
|
+
commits.push({ sha, subject, files: lines });
|
|
17132
|
+
}
|
|
17133
|
+
return commits;
|
|
17134
|
+
} catch {
|
|
17135
|
+
return [];
|
|
17136
|
+
}
|
|
17137
|
+
}
|
|
17138
|
+
|
|
16637
17139
|
// src/index.ts
|
|
16638
|
-
var program = new
|
|
16639
|
-
program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.
|
|
17140
|
+
var program = new Command63();
|
|
17141
|
+
program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.15.0").option("--advanced", "show maintenance and experimental commands in help");
|
|
16640
17142
|
registerInit(program);
|
|
16641
17143
|
registerWelcome(program);
|
|
16642
17144
|
registerResolveProject(program);
|
|
@@ -16647,6 +17149,9 @@ registerAgent(program);
|
|
|
16647
17149
|
registerSensors(program);
|
|
16648
17150
|
registerIngest(program);
|
|
16649
17151
|
registerDashboard(program);
|
|
17152
|
+
registerCoverage(program);
|
|
17153
|
+
registerMergeDriver(program);
|
|
17154
|
+
registerDevLink(program);
|
|
16650
17155
|
registerMcp(program);
|
|
16651
17156
|
registerBriefing(program);
|
|
16652
17157
|
registerTui(program);
|
|
@@ -16676,6 +17181,8 @@ registerMemoryUpdate(memory);
|
|
|
16676
17181
|
registerMemoryHot(memory);
|
|
16677
17182
|
registerMemoryTried(memory);
|
|
16678
17183
|
registerMemorySeed(memory);
|
|
17184
|
+
registerMemorySeedGit(memory);
|
|
17185
|
+
registerMemoryResolveConflict(memory);
|
|
16679
17186
|
registerMemoryImport(memory);
|
|
16680
17187
|
registerMemoryImportChangelog(memory);
|
|
16681
17188
|
registerMemoryDigest(memory);
|