@hiveai/cli 0.14.0 → 0.17.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 +503 -60
- 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";
|
|
@@ -9,9 +9,10 @@ import { mkdir, readFile as readFile2 } from "fs/promises";
|
|
|
9
9
|
import path3 from "path";
|
|
10
10
|
import "commander";
|
|
11
11
|
import {
|
|
12
|
+
classifyMemoryPriority,
|
|
13
|
+
compactAutoRecapBody,
|
|
12
14
|
extractActionsBriefBody,
|
|
13
15
|
findProjectRoot as findProjectRoot2,
|
|
14
|
-
isStackPackSeed,
|
|
15
16
|
literalMatchesAllTokens,
|
|
16
17
|
literalMatchesAnyToken,
|
|
17
18
|
loadCodeMap as loadCodeMap3,
|
|
@@ -199,7 +200,7 @@ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
|
|
|
199
200
|
if (!f) continue;
|
|
200
201
|
counts.set(f, (counts.get(f) ?? 0) + 1);
|
|
201
202
|
}
|
|
202
|
-
let entries = [...counts.entries()].map(([
|
|
203
|
+
let entries = [...counts.entries()].map(([path58, changes]) => ({ path: path58, changes }));
|
|
203
204
|
const lowerPaths = filePaths.map((p) => p.toLowerCase());
|
|
204
205
|
if (lowerPaths.length > 0) {
|
|
205
206
|
entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
|
|
@@ -954,7 +955,7 @@ function registerBriefing(program2) {
|
|
|
954
955
|
out(`${ui.bold("=== Last Session Recap ===")}
|
|
955
956
|
`);
|
|
956
957
|
out(ui.dim(`${fm.id} (${fm.scope}${rev})`));
|
|
957
|
-
out(recap.memory.body.trim());
|
|
958
|
+
out(compactAutoRecapBody(recap.memory.body).trim());
|
|
958
959
|
out("");
|
|
959
960
|
}
|
|
960
961
|
if (existsSync3(paths.projectContext) && !stopped()) {
|
|
@@ -1124,12 +1125,19 @@ ${ui.bold("=== Symbol Locations ===")}
|
|
|
1124
1125
|
function classifyCliPriority(item, filePaths, tokens, exactTaskHit, partialTaskHit) {
|
|
1125
1126
|
const fm = item.memory.frontmatter;
|
|
1126
1127
|
const anchored = filePaths.length > 0 && memoryMatchesAnchorPaths(item.memory, filePaths);
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1128
|
+
return classifyMemoryPriority({
|
|
1129
|
+
type: fm.type,
|
|
1130
|
+
tags: fm.tags,
|
|
1131
|
+
requiresHumanApproval: Boolean(fm.requires_human_approval),
|
|
1132
|
+
directAnchor: anchored,
|
|
1133
|
+
directSymbol: false,
|
|
1134
|
+
// symbol lookup is rendered separately in the CLI, not via anchor priority
|
|
1135
|
+
exactTaskMatch: exactTaskHit,
|
|
1136
|
+
strongSemantic: false,
|
|
1137
|
+
usefulSemantic: partialTaskHit || item.score >= 4,
|
|
1138
|
+
moduleOrDomainMatch: false,
|
|
1139
|
+
tagTaskMatch: Boolean(tokens && fm.tags.some((tag) => tokens.includes(tag)))
|
|
1140
|
+
});
|
|
1133
1141
|
}
|
|
1134
1142
|
function priorityBadge(priority) {
|
|
1135
1143
|
if (priority === "must_read") return ui.red("[must_read]");
|
|
@@ -3019,7 +3027,7 @@ ${SEED_FOOTER(stack)}` });
|
|
|
3019
3027
|
}
|
|
3020
3028
|
|
|
3021
3029
|
// src/commands/init.ts
|
|
3022
|
-
var HAIVE_GITHUB_ACTION_REF = `v${"0.
|
|
3030
|
+
var HAIVE_GITHUB_ACTION_REF = `v${"0.17.0"}`;
|
|
3023
3031
|
var PROJECT_CONTEXT_TEMPLATE = `# Project context
|
|
3024
3032
|
|
|
3025
3033
|
> Generated by \`haive init\`. Run \`haive init --bootstrap\` to auto-fill from your codebase,
|
|
@@ -3202,7 +3210,11 @@ jobs:
|
|
|
3202
3210
|
run: npm install -g @hiveai/cli
|
|
3203
3211
|
|
|
3204
3212
|
- name: harness quality regression gate
|
|
3205
|
-
run: haive eval --regression-gate
|
|
3213
|
+
run: haive eval --regression-gate --record --ref "\${{ github.sha }}"
|
|
3214
|
+
|
|
3215
|
+
- name: harness quality trend
|
|
3216
|
+
if: always()
|
|
3217
|
+
run: haive eval --trend || true
|
|
3206
3218
|
|
|
3207
3219
|
# On push to main: push shared memories to the hub (if hubPath is configured)
|
|
3208
3220
|
# Uncomment and configure hubPath in .ai/haive.config.json to enable.
|
|
@@ -3830,14 +3842,21 @@ async function readStdin(maxBytes) {
|
|
|
3830
3842
|
setTimeout(finish, 2e3);
|
|
3831
3843
|
});
|
|
3832
3844
|
}
|
|
3845
|
+
function isExpectedNonzeroExit(command) {
|
|
3846
|
+
if (!command) return false;
|
|
3847
|
+
if (command.includes("|")) return true;
|
|
3848
|
+
if (/\|\|\s*true\b/.test(command)) return true;
|
|
3849
|
+
return /(^|\s|;|&&)(grep|egrep|fgrep|rg|ag|find|test|diff|\[\[?)\b/.test(command);
|
|
3850
|
+
}
|
|
3833
3851
|
function detectFailure(payload) {
|
|
3834
3852
|
const response = payload.tool_response;
|
|
3835
3853
|
if (!response) return false;
|
|
3836
3854
|
const responseText = typeof response === "string" ? response : JSON.stringify(response);
|
|
3837
3855
|
if (payload.tool_name === "Bash") {
|
|
3856
|
+
const command = typeof payload.tool_input?.["command"] === "string" ? payload.tool_input["command"] : "";
|
|
3838
3857
|
if (typeof response === "object") {
|
|
3839
3858
|
const code = response["exit_code"] ?? response["exitCode"];
|
|
3840
|
-
if (typeof code === "number" && code !== 0) return true;
|
|
3859
|
+
if (typeof code === "number" && code !== 0 && !isExpectedNonzeroExit(command)) return true;
|
|
3841
3860
|
}
|
|
3842
3861
|
if (/\b(command not found|No such file or directory|ERR_MODULE_NOT_FOUND|ENOENT|EACCES)\b/.test(responseText)) return true;
|
|
3843
3862
|
if (/\berror TS\d+:/i.test(responseText)) return true;
|
|
@@ -4079,6 +4098,7 @@ import {
|
|
|
4079
4098
|
deriveConfidence as deriveConfidence4,
|
|
4080
4099
|
estimateTokens,
|
|
4081
4100
|
evaluateSkillActivation,
|
|
4101
|
+
compactAutoRecapBody as compactAutoRecapBody2,
|
|
4082
4102
|
extractActionsBriefBody as extractActionsBriefBody2,
|
|
4083
4103
|
getUsage as getUsage6,
|
|
4084
4104
|
inferModulesFromPaths as inferModulesFromPaths2,
|
|
@@ -4110,7 +4130,12 @@ import { z as z19 } from "zod";
|
|
|
4110
4130
|
import { readdir as readdir3, readFile as readFile42 } from "fs/promises";
|
|
4111
4131
|
import { existsSync as existsSync20 } from "fs";
|
|
4112
4132
|
import path102 from "path";
|
|
4113
|
-
import {
|
|
4133
|
+
import {
|
|
4134
|
+
classifyMemoryPriority as coreClassifyPriority,
|
|
4135
|
+
isGlobPath,
|
|
4136
|
+
pathsOverlap,
|
|
4137
|
+
priorityRank as corePriorityRank
|
|
4138
|
+
} from "@hiveai/core";
|
|
4114
4139
|
import { estimateTokens as estimateTokens2, loadCodeMap as loadCodeMap22, queryCodeMap as queryCodeMap22 } from "@hiveai/core";
|
|
4115
4140
|
import { z as z20 } from "zod";
|
|
4116
4141
|
import { existsSync as existsSync222 } from "fs";
|
|
@@ -5573,7 +5598,7 @@ function compactSummary(body) {
|
|
|
5573
5598
|
}
|
|
5574
5599
|
return body.slice(0, 120);
|
|
5575
5600
|
}
|
|
5576
|
-
function
|
|
5601
|
+
function classifyMemoryPriority2(memory2, loaded, inputFiles, inputSymbols) {
|
|
5577
5602
|
const fm = loaded?.memory.frontmatter;
|
|
5578
5603
|
const directAnchor = Boolean(
|
|
5579
5604
|
fm && inputFiles.length > 0 && fm.anchor.paths.some((p) => inputFiles.some((file) => pathsOverlap(p, file)))
|
|
@@ -5583,21 +5608,23 @@ function classifyMemoryPriority(memory2, loaded, inputFiles, inputSymbols) {
|
|
|
5583
5608
|
(sym) => inputSymbols.some((wanted) => wanted.toLowerCase() === sym.toLowerCase())
|
|
5584
5609
|
)
|
|
5585
5610
|
);
|
|
5586
|
-
const
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5611
|
+
const semantic = memory2.semantic_score ?? 0;
|
|
5612
|
+
return coreClassifyPriority({
|
|
5613
|
+
type: memory2.type,
|
|
5614
|
+
tags: fm?.tags ?? memory2.tags ?? [],
|
|
5615
|
+
requiresHumanApproval: Boolean(fm?.requires_human_approval),
|
|
5616
|
+
directAnchor,
|
|
5617
|
+
directSymbol,
|
|
5618
|
+
exactTaskMatch: memory2.match_quality === "exact",
|
|
5619
|
+
strongSemantic: semantic >= 0.65,
|
|
5620
|
+
usefulSemantic: semantic >= 0.35,
|
|
5621
|
+
moduleOrDomainMatch: memory2.reasons.includes("module") || memory2.reasons.includes("domain"),
|
|
5622
|
+
tagTaskMatch: false
|
|
5623
|
+
// MCP ranking doesn't use a separate tag-token signal
|
|
5624
|
+
});
|
|
5598
5625
|
}
|
|
5599
5626
|
function priorityRank(priority) {
|
|
5600
|
-
return priority
|
|
5627
|
+
return corePriorityRank(priority);
|
|
5601
5628
|
}
|
|
5602
5629
|
function classifyBriefingQuality(memories, context) {
|
|
5603
5630
|
const mustRead = memories.filter((m) => m.priority === "must_read").length;
|
|
@@ -5764,7 +5791,9 @@ async function getBriefing(input, ctx) {
|
|
|
5764
5791
|
id: fm.id,
|
|
5765
5792
|
scope: fm.scope,
|
|
5766
5793
|
revision_count: fm.revision_count ?? 0,
|
|
5767
|
-
|
|
5794
|
+
// Auto-generated recaps are low-signal tool dumps — compact them so they inform without
|
|
5795
|
+
// dominating the briefing head. Human/post_task recaps pass through unchanged.
|
|
5796
|
+
body: compactAutoRecapBody2(r.memory.body)
|
|
5768
5797
|
};
|
|
5769
5798
|
}
|
|
5770
5799
|
const allMemories = allLoaded.filter(({ memory: memory2 }) => {
|
|
@@ -5886,8 +5915,8 @@ async function getBriefing(input, ctx) {
|
|
|
5886
5915
|
const impactScore = (m) => (m.impact_score ?? 0) * 3;
|
|
5887
5916
|
const activationBoost = (m) => activatedSkills.has(m.id) ? 5 : 0;
|
|
5888
5917
|
const lexScore = (m) => 12 * (lexNorm.get(m.id) ?? 0);
|
|
5889
|
-
const sa = priorityRank(
|
|
5890
|
-
const sb = priorityRank(
|
|
5918
|
+
const sa = priorityRank(classifyMemoryPriority2(a, byId.get(a.id), input.files, input.symbols)) * 100 + reasonScore(a) + confidenceScore(a) + impactScore(a) + activationBoost(a) + lexScore(a) + (a.semantic_score ?? 0);
|
|
5919
|
+
const sb = priorityRank(classifyMemoryPriority2(b, byId.get(b.id), input.files, input.symbols)) * 100 + reasonScore(b) + confidenceScore(b) + impactScore(b) + activationBoost(b) + lexScore(b) + (b.semantic_score ?? 0);
|
|
5891
5920
|
return sb - sa;
|
|
5892
5921
|
});
|
|
5893
5922
|
for (const mem of ranked.slice(0, briefingMaxMemories)) {
|
|
@@ -6043,7 +6072,7 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
6043
6072
|
const formattedMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : input.format === "actions" ? trimmedMemories.map((m) => ({ ...m, body: extractActionsBriefBody2(m.body) })) : trimmedMemories;
|
|
6044
6073
|
const outputMemories = formattedMemories.map((m) => ({
|
|
6045
6074
|
...m,
|
|
6046
|
-
priority:
|
|
6075
|
+
priority: classifyMemoryPriority2(m, byId.get(m.id), input.files, input.symbols),
|
|
6047
6076
|
why: explainWhySurfaced(m, byId.get(m.id), input.files, inferred)
|
|
6048
6077
|
}));
|
|
6049
6078
|
const briefingQuality = classifyBriefingQuality(outputMemories, {
|
|
@@ -6580,6 +6609,27 @@ function tokenizeDiffForLiteral(diff) {
|
|
|
6580
6609
|
const wordTokens = source.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 4 && !CODE_STOPWORDS.has(t));
|
|
6581
6610
|
return [.../* @__PURE__ */ new Set([...wsTokens, ...wordTokens])];
|
|
6582
6611
|
}
|
|
6612
|
+
function stripAiDirHunks(diff) {
|
|
6613
|
+
if (!diff.includes("diff --git")) return diff;
|
|
6614
|
+
const out = [];
|
|
6615
|
+
let block = [];
|
|
6616
|
+
let keep = true;
|
|
6617
|
+
const flush = () => {
|
|
6618
|
+
if (keep) out.push(...block);
|
|
6619
|
+
block = [];
|
|
6620
|
+
keep = true;
|
|
6621
|
+
};
|
|
6622
|
+
for (const line of diff.split("\n")) {
|
|
6623
|
+
if (line.startsWith("diff --git ")) {
|
|
6624
|
+
flush();
|
|
6625
|
+
const target = line.match(/ b\/(.+)$/)?.[1] ?? "";
|
|
6626
|
+
keep = !target.startsWith(".ai/");
|
|
6627
|
+
}
|
|
6628
|
+
block.push(line);
|
|
6629
|
+
}
|
|
6630
|
+
flush();
|
|
6631
|
+
return out.join("\n");
|
|
6632
|
+
}
|
|
6583
6633
|
async function antiPatternsCheck(input, ctx) {
|
|
6584
6634
|
if (!input.diff && input.paths.length === 0) {
|
|
6585
6635
|
return {
|
|
@@ -6635,10 +6685,11 @@ async function antiPatternsCheck(input, ctx) {
|
|
|
6635
6685
|
}
|
|
6636
6686
|
}
|
|
6637
6687
|
}
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
const
|
|
6641
|
-
const
|
|
6688
|
+
const scanDiff = input.diff ? stripAiDirHunks(input.diff) : input.diff;
|
|
6689
|
+
if (scanDiff) {
|
|
6690
|
+
const tokens = tokenizeDiffForLiteral(scanDiff);
|
|
6691
|
+
const added = addedLinesFromDiff(scanDiff);
|
|
6692
|
+
const addedText = added.trim().length > 0 ? added : scanDiff;
|
|
6642
6693
|
if (tokens.length > 0) {
|
|
6643
6694
|
for (const { memory: memory2 } of negative) {
|
|
6644
6695
|
if (literalMatchesAnyToken3(memory2, tokens)) {
|
|
@@ -6668,10 +6719,10 @@ async function antiPatternsCheck(input, ctx) {
|
|
|
6668
6719
|
}
|
|
6669
6720
|
}
|
|
6670
6721
|
}
|
|
6671
|
-
if (input.semantic &&
|
|
6722
|
+
if (input.semantic && scanDiff) {
|
|
6672
6723
|
try {
|
|
6673
6724
|
const mod = await import("@hiveai/embeddings");
|
|
6674
|
-
const result = await mod.semanticSearch(ctx.paths,
|
|
6725
|
+
const result = await mod.semanticSearch(ctx.paths, scanDiff, { limit: input.limit * 2 });
|
|
6675
6726
|
if (result) {
|
|
6676
6727
|
const negativeIds = new Set(negative.map(({ memory: memory2 }) => memory2.frontmatter.id));
|
|
6677
6728
|
for (const hit of result.hits) {
|
|
@@ -7958,7 +8009,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
7958
8009
|
};
|
|
7959
8010
|
}
|
|
7960
8011
|
var SERVER_NAME = "haive";
|
|
7961
|
-
var SERVER_VERSION = "0.
|
|
8012
|
+
var SERVER_VERSION = "0.17.0";
|
|
7962
8013
|
function jsonResult(data) {
|
|
7963
8014
|
return {
|
|
7964
8015
|
content: [
|
|
@@ -9001,7 +9052,7 @@ import {
|
|
|
9001
9052
|
getUsage as getUsage11,
|
|
9002
9053
|
isAutoPromoteEligible as isAutoPromoteEligible2,
|
|
9003
9054
|
isDecaying as isDecaying2,
|
|
9004
|
-
isStackPackSeed
|
|
9055
|
+
isStackPackSeed,
|
|
9005
9056
|
loadCodeMap as loadCodeMap6,
|
|
9006
9057
|
loadConfig as loadConfig4,
|
|
9007
9058
|
loadMemoriesFromDir as loadMemoriesFromDir25,
|
|
@@ -9466,7 +9517,7 @@ async function injectBridge(bridgeFile, memoriesDir, maxMemories, root, quiet) {
|
|
|
9466
9517
|
const top = all.filter(({ memory: memory2 }) => {
|
|
9467
9518
|
const s = memory2.frontmatter.status;
|
|
9468
9519
|
if (memory2.frontmatter.type === "session_recap") return false;
|
|
9469
|
-
if (
|
|
9520
|
+
if (isStackPackSeed(memory2.frontmatter)) return false;
|
|
9470
9521
|
return s === "validated" || s === "proposed";
|
|
9471
9522
|
}).sort((a, b) => {
|
|
9472
9523
|
const score = (m) => {
|
|
@@ -12709,9 +12760,12 @@ import "commander";
|
|
|
12709
12760
|
import {
|
|
12710
12761
|
aggregateRetrieval,
|
|
12711
12762
|
aggregateSensors,
|
|
12763
|
+
appendEvalHistory,
|
|
12712
12764
|
buildReport,
|
|
12713
12765
|
compareEvalReports,
|
|
12766
|
+
computeEvalTrend,
|
|
12714
12767
|
findProjectRoot as findProjectRoot42,
|
|
12768
|
+
loadEvalHistory,
|
|
12715
12769
|
resolveHaivePaths as resolveHaivePaths38,
|
|
12716
12770
|
scoreRetrievalCase,
|
|
12717
12771
|
scoreSensorCase,
|
|
@@ -12720,7 +12774,7 @@ import {
|
|
|
12720
12774
|
function registerEval(program2) {
|
|
12721
12775
|
program2.command("eval").description(
|
|
12722
12776
|
"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."
|
|
12723
|
-
).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) => {
|
|
12777
|
+
).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) => {
|
|
12724
12778
|
const root = findProjectRoot42(opts.dir);
|
|
12725
12779
|
const paths = resolveHaivePaths38(root);
|
|
12726
12780
|
if (!existsSync65(paths.memoriesDir)) {
|
|
@@ -12728,6 +12782,22 @@ function registerEval(program2) {
|
|
|
12728
12782
|
process.exitCode = 1;
|
|
12729
12783
|
return;
|
|
12730
12784
|
}
|
|
12785
|
+
if (opts.trend) {
|
|
12786
|
+
const trend = computeEvalTrend(await loadEvalHistory(paths));
|
|
12787
|
+
if (opts.json) {
|
|
12788
|
+
console.log(JSON.stringify(trend, null, 2));
|
|
12789
|
+
return;
|
|
12790
|
+
}
|
|
12791
|
+
if (trend.runs === 0) {
|
|
12792
|
+
ui.info("No eval history yet. Run `haive eval --record` to start trending the harness.");
|
|
12793
|
+
return;
|
|
12794
|
+
}
|
|
12795
|
+
const spark = trend.recent.map((s) => "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588"[Math.min(7, Math.round(s / 100 * 7))]).join("");
|
|
12796
|
+
const arrow = trend.regressed ? ui.red("\u25BC") : (trend.delta ?? 0) > 0 ? ui.green("\u25B2") : ui.dim("=");
|
|
12797
|
+
console.log(ui.bold("hAIve eval trend"));
|
|
12798
|
+
console.log(` ${spark} latest ${arrow} ${trend.latest}/100 ${ui.dim(`(best ${trend.best}, ${trend.runs} run${trend.runs === 1 ? "" : "s"})`)}`);
|
|
12799
|
+
return;
|
|
12800
|
+
}
|
|
12731
12801
|
const k = Math.max(1, parseInt(opts.top ?? "8", 10) || 8);
|
|
12732
12802
|
const ctx = { paths };
|
|
12733
12803
|
const resolvedSpec = await resolveSpec(opts, root, paths.memoriesDir);
|
|
@@ -12755,6 +12825,17 @@ function registerEval(program2) {
|
|
|
12755
12825
|
sensorAgg = aggregateSensors(results);
|
|
12756
12826
|
}
|
|
12757
12827
|
const report = buildReport(retrievalAgg, sensorAgg);
|
|
12828
|
+
if (opts.record) {
|
|
12829
|
+
await appendEvalHistory(paths, {
|
|
12830
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12831
|
+
score: report.score,
|
|
12832
|
+
...report.retrieval ? { mean_recall: report.retrieval.mean_recall, mrr: report.retrieval.mrr } : {},
|
|
12833
|
+
...report.sensors ? { catch_rate: report.sensors.catch_rate } : {},
|
|
12834
|
+
...opts.ref ? { ref: opts.ref } : {}
|
|
12835
|
+
}).catch(() => {
|
|
12836
|
+
});
|
|
12837
|
+
if (!opts.json) ui.success(`Recorded eval score ${report.score}/100 to history.`);
|
|
12838
|
+
}
|
|
12758
12839
|
const baselineFile = opts.baselineFile ? path43.isAbsolute(opts.baselineFile) ? opts.baselineFile : path43.join(root, opts.baselineFile) : path43.join(root, ".ai", "eval", "baseline.json");
|
|
12759
12840
|
if (opts.baseline) {
|
|
12760
12841
|
const snapshot = {
|
|
@@ -13283,7 +13364,7 @@ import {
|
|
|
13283
13364
|
codeMapPath as codeMapPath2,
|
|
13284
13365
|
findProjectRoot as findProjectRoot45,
|
|
13285
13366
|
getUsage as getUsage23,
|
|
13286
|
-
isStackPackSeed as
|
|
13367
|
+
isStackPackSeed as isStackPackSeed2,
|
|
13287
13368
|
loadCodeMap as loadCodeMap7,
|
|
13288
13369
|
loadConfig as loadConfig11,
|
|
13289
13370
|
loadMemoriesFromDir as loadMemoriesFromDir35,
|
|
@@ -13395,11 +13476,11 @@ function registerDoctor(program2) {
|
|
|
13395
13476
|
fix: "haive memory approve <id> # activate\nhaive memory delete <id> # or delete if obsolete"
|
|
13396
13477
|
});
|
|
13397
13478
|
}
|
|
13398
|
-
const policyMemories = memories.filter((m) => !
|
|
13479
|
+
const policyMemories = memories.filter((m) => !isStackPackSeed2(m.memory.frontmatter));
|
|
13399
13480
|
const anchorless = policyMemories.filter(
|
|
13400
13481
|
(m) => m.memory.frontmatter.anchor.paths.length === 0 && m.memory.frontmatter.anchor.symbols.length === 0 && m.memory.frontmatter.type !== "session_recap" && m.memory.frontmatter.type !== "glossary" && m.memory.frontmatter.type !== "skill"
|
|
13401
13482
|
);
|
|
13402
|
-
const stackSeeds = memories.filter((m) =>
|
|
13483
|
+
const stackSeeds = memories.filter((m) => isStackPackSeed2(m.memory.frontmatter));
|
|
13403
13484
|
if (anchorless.length / Math.max(policyMemories.length, 1) > 0.3) {
|
|
13404
13485
|
findings.push({
|
|
13405
13486
|
severity: "warn",
|
|
@@ -13529,7 +13610,7 @@ function registerDoctor(program2) {
|
|
|
13529
13610
|
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
|
|
13530
13611
|
});
|
|
13531
13612
|
}
|
|
13532
|
-
findings.push(...await collectInstallFindings(root, "0.
|
|
13613
|
+
findings.push(...await collectInstallFindings(root, "0.17.0"));
|
|
13533
13614
|
findings.push(...await collectToolchainFindings(root));
|
|
13534
13615
|
try {
|
|
13535
13616
|
const legacyRaw = execSync3("haive-mcp --version", {
|
|
@@ -13537,7 +13618,7 @@ function registerDoctor(program2) {
|
|
|
13537
13618
|
timeout: 3e3,
|
|
13538
13619
|
stdio: ["ignore", "pipe", "ignore"]
|
|
13539
13620
|
}).trim();
|
|
13540
|
-
const cliVersion = "0.
|
|
13621
|
+
const cliVersion = "0.17.0";
|
|
13541
13622
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
13542
13623
|
findings.push({
|
|
13543
13624
|
severity: "warn",
|
|
@@ -14599,6 +14680,7 @@ import "commander";
|
|
|
14599
14680
|
import {
|
|
14600
14681
|
antiPatternGateParams as antiPatternGateParams2,
|
|
14601
14682
|
findProjectRoot as findProjectRoot52,
|
|
14683
|
+
findUncapturedFailures,
|
|
14602
14684
|
hasRecentBriefingMarker as hasRecentBriefingMarker2,
|
|
14603
14685
|
isFreshIsoDate,
|
|
14604
14686
|
loadConfig as loadConfig13,
|
|
@@ -14860,6 +14942,7 @@ async function buildFinishReport(dir) {
|
|
|
14860
14942
|
}]
|
|
14861
14943
|
});
|
|
14862
14944
|
}
|
|
14945
|
+
findings.push(...await checkFailureCapture(paths, config));
|
|
14863
14946
|
const status = await getGitSyncStatus(root);
|
|
14864
14947
|
if (!status.available) {
|
|
14865
14948
|
findings.push({
|
|
@@ -14877,7 +14960,7 @@ async function buildFinishReport(dir) {
|
|
|
14877
14960
|
severity: "error",
|
|
14878
14961
|
code: shippableDirty.length > 0 ? "git-sync-uncommitted-shippable" : "git-sync-uncommitted-changes",
|
|
14879
14962
|
message: shippableDirty.length > 0 ? `${shippableDirty.length} shippable file(s) are modified but not committed.` : `${status.dirtyFiles.length} file(s) are modified but not committed.`,
|
|
14880
|
-
fix: shippableDirty.length > 0 ? "Bump the lockstep package version if needed, then `git add`, `git commit`, `git tag vX.Y.Z`, `git push && git push
|
|
14963
|
+
fix: shippableDirty.length > 0 ? "Bump the lockstep package version if needed, then `git add`, `git commit`, `git tag vX.Y.Z`, `git push && git push origin vX.Y.Z` (not `--tags`)." : "Commit and push these changes before reporting the task done.",
|
|
14881
14964
|
reason: "The multi-agent git-sync decision requires agents to leave completed work committed and pushed, not as a local diff.",
|
|
14882
14965
|
affected_files: status.dirtyFiles.slice(0, 12),
|
|
14883
14966
|
impact: 100
|
|
@@ -15002,7 +15085,7 @@ async function buildFinishReport(dir) {
|
|
|
15002
15085
|
severity: "error",
|
|
15003
15086
|
code: "release-tag-unpushed",
|
|
15004
15087
|
message: `Tag ${tag} is not present on the remote.`,
|
|
15005
|
-
fix:
|
|
15088
|
+
fix: `Run \`git push origin ${tag}\` (avoid \`git push --tags\` \u2014 it fails on pre-existing divergent tags).`,
|
|
15006
15089
|
impact: 50
|
|
15007
15090
|
});
|
|
15008
15091
|
} else if (remoteTag === true) {
|
|
@@ -15016,13 +15099,54 @@ async function buildFinishReport(dir) {
|
|
|
15016
15099
|
severity: "warn",
|
|
15017
15100
|
code: "release-tag-remote-unverified",
|
|
15018
15101
|
message: `Could not verify whether tag ${tag} exists on the remote.`,
|
|
15019
|
-
fix:
|
|
15102
|
+
fix: `Run \`git push origin ${tag}\` if you have not already (avoid \`git push --tags\`).`,
|
|
15020
15103
|
impact: 10
|
|
15021
15104
|
});
|
|
15022
15105
|
}
|
|
15023
15106
|
findings.push(...await verifyGithubActionsForHead(root, status));
|
|
15024
15107
|
return finishReport(root, initialized, mode, findings, config);
|
|
15025
15108
|
}
|
|
15109
|
+
async function checkFailureCapture(paths, config) {
|
|
15110
|
+
const gate = config.enforcement?.failureCaptureGate ?? "warn";
|
|
15111
|
+
if (gate === "off") return [];
|
|
15112
|
+
const obsFile = path51.join(paths.haiveDir, ".cache", "observations.jsonl");
|
|
15113
|
+
if (!existsSync75(obsFile)) return [];
|
|
15114
|
+
const failures = [];
|
|
15115
|
+
try {
|
|
15116
|
+
const raw = await readFile23(obsFile, "utf8");
|
|
15117
|
+
for (const line of raw.split("\n")) {
|
|
15118
|
+
const trimmed = line.trim();
|
|
15119
|
+
if (!trimmed) continue;
|
|
15120
|
+
try {
|
|
15121
|
+
const o = JSON.parse(trimmed);
|
|
15122
|
+
if (o.failure_hint && o.ts) failures.push({ ts: o.ts, tool: o.tool ?? "?", summary: o.summary ?? "" });
|
|
15123
|
+
} catch {
|
|
15124
|
+
}
|
|
15125
|
+
}
|
|
15126
|
+
} catch {
|
|
15127
|
+
return [];
|
|
15128
|
+
}
|
|
15129
|
+
if (failures.length === 0) return [];
|
|
15130
|
+
const memories = existsSync75(paths.memoriesDir) ? await loadMemoriesFromDir38(paths.memoriesDir) : [];
|
|
15131
|
+
const captureTimes = memories.filter(({ memory: memory2 }) => ["attempt", "gotcha"].includes(memory2.frontmatter.type)).map(({ memory: memory2 }) => memory2.frontmatter.created_at);
|
|
15132
|
+
const uncaptured = findUncapturedFailures(failures, captureTimes);
|
|
15133
|
+
if (uncaptured.length === 0) {
|
|
15134
|
+
return [{
|
|
15135
|
+
severity: "ok",
|
|
15136
|
+
code: "failure-capture-clean",
|
|
15137
|
+
message: "No uncaptured hard failures from this session."
|
|
15138
|
+
}];
|
|
15139
|
+
}
|
|
15140
|
+
return [{
|
|
15141
|
+
severity: gate === "block" ? "error" : "info",
|
|
15142
|
+
code: "uncaptured-failures",
|
|
15143
|
+
message: `${uncaptured.length} hard failure(s) this session were never captured as a lesson (mem_tried).`,
|
|
15144
|
+
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.",
|
|
15145
|
+
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.",
|
|
15146
|
+
affected_files: uncaptured.slice(0, 8).map((f) => `${f.tool}: ${f.summary}`.slice(0, 100)),
|
|
15147
|
+
...gate === "block" ? { impact: 30 } : {}
|
|
15148
|
+
}];
|
|
15149
|
+
}
|
|
15026
15150
|
function finishReport(root, initialized, mode, findings, config) {
|
|
15027
15151
|
const score = buildScore(findings, config.enforcement?.scoreThreshold);
|
|
15028
15152
|
const hasErrors = findings.some((f) => f.severity === "error");
|
|
@@ -15167,7 +15291,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
15167
15291
|
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
15168
15292
|
});
|
|
15169
15293
|
}
|
|
15170
|
-
findings.push(...await inspectIntegrationVersions(root, "0.
|
|
15294
|
+
findings.push(...await inspectIntegrationVersions(root, "0.17.0"));
|
|
15171
15295
|
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
15172
15296
|
const hasBriefing = await hasRecentBriefingMarker2(paths, sessionId);
|
|
15173
15297
|
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
|
|
@@ -15327,7 +15451,7 @@ async function verifyDecisionCoverage(paths, stage, sessionId) {
|
|
|
15327
15451
|
severity: stage === "local" ? "warn" : "error",
|
|
15328
15452
|
code: "decision-coverage-missing",
|
|
15329
15453
|
message: `${missing.length}/${relevant.length} relevant anchored decisions/policies were not present in the latest briefing: ${missing.slice(0, 6).map((m) => m.frontmatter.id).join(", ")}`,
|
|
15330
|
-
fix: `Run \`haive briefing --files "${changedFiles.slice(0,
|
|
15454
|
+
fix: `Run \`haive briefing --files "${changedFiles.slice(0, 12).join(",")}" --max-memories 60 --task "..."\` before committing (briefings now accumulate, so several smaller briefings also work).`,
|
|
15331
15455
|
reason: "Changed files overlap validated anchored policy memories that were not recorded in the latest briefing marker.",
|
|
15332
15456
|
affected_files: changedFiles.slice(0, 10),
|
|
15333
15457
|
memory_ids: missing.slice(0, 10).map((m) => m.frontmatter.id),
|
|
@@ -16163,12 +16287,14 @@ import {
|
|
|
16163
16287
|
appendPreventionEvent as appendPreventionEvent2,
|
|
16164
16288
|
findProjectRoot as findProjectRoot53,
|
|
16165
16289
|
isRetiredMemory as isRetiredMemory3,
|
|
16290
|
+
loadConfig as loadConfig14,
|
|
16166
16291
|
loadMemoriesFromDir as loadMemoriesFromDir39,
|
|
16167
16292
|
loadUsageIndex as loadUsageIndex29,
|
|
16168
16293
|
recordPrevention as recordPrevention2,
|
|
16169
16294
|
resolveHaivePaths as resolveHaivePaths49,
|
|
16170
16295
|
runSensors as runSensors2,
|
|
16171
16296
|
saveUsageIndex as saveUsageIndex8,
|
|
16297
|
+
selectCommandSensors,
|
|
16172
16298
|
sensorTargetsFromDiff as sensorTargetsFromDiff2,
|
|
16173
16299
|
serializeMemory as serializeMemory27
|
|
16174
16300
|
} from "@hiveai/core";
|
|
@@ -16198,14 +16324,36 @@ function registerSensors(program2) {
|
|
|
16198
16324
|
});
|
|
16199
16325
|
sensors.command("check").description(
|
|
16200
16326
|
"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."
|
|
16201
|
-
).option("--diff-file <path>", "read unified diff from a file instead of staged changes").option("--json", "emit JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
16327
|
+
).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) => {
|
|
16202
16328
|
const root = findProjectRoot53(opts.dir);
|
|
16203
16329
|
const paths = resolveHaivePaths49(root);
|
|
16204
16330
|
const memories = await runnableSensorMemories(paths);
|
|
16205
16331
|
const diff = opts.diffFile ? await readFile24(path53.resolve(root, opts.diffFile), "utf8") : await stagedDiff(root);
|
|
16206
16332
|
const targets = sensorTargetsFromDiff2(diff);
|
|
16207
16333
|
const hits = runSensors2(memories, targets.length > 0 ? targets : [{ path: "", content: diff }]);
|
|
16208
|
-
const
|
|
16334
|
+
const config = await loadConfig14(paths);
|
|
16335
|
+
const runCommands = opts.commands || config.enforcement?.runCommandSensors === true;
|
|
16336
|
+
const changedPaths = targets.map((t) => t.path).filter(Boolean);
|
|
16337
|
+
const allSensorMemories = await runnableSensorMemories(paths, false);
|
|
16338
|
+
const commandSpecs = selectCommandSensors(allSensorMemories, changedPaths);
|
|
16339
|
+
const commandHits = [];
|
|
16340
|
+
const commandSkipped = [];
|
|
16341
|
+
if (commandSpecs.length > 0 && runCommands) {
|
|
16342
|
+
for (const spec of commandSpecs) {
|
|
16343
|
+
const failed = await runCommandSensor(spec, root);
|
|
16344
|
+
if (failed) {
|
|
16345
|
+
commandHits.push({
|
|
16346
|
+
memory_id: spec.memory_id,
|
|
16347
|
+
severity: spec.severity,
|
|
16348
|
+
message: spec.message,
|
|
16349
|
+
matched_line: `command failed: ${spec.command}`
|
|
16350
|
+
});
|
|
16351
|
+
}
|
|
16352
|
+
}
|
|
16353
|
+
} else if (commandSpecs.length > 0) {
|
|
16354
|
+
for (const spec of commandSpecs) commandSkipped.push(spec.memory_id);
|
|
16355
|
+
}
|
|
16356
|
+
const firedIds = [...new Set([...hits, ...commandHits].map((hit) => hit.memory_id))];
|
|
16209
16357
|
if (firedIds.length > 0) {
|
|
16210
16358
|
const usage = await loadUsageIndex29(paths);
|
|
16211
16359
|
const recordedIds = [];
|
|
@@ -16228,12 +16376,15 @@ function registerSensors(program2) {
|
|
|
16228
16376
|
severity: hit.severity,
|
|
16229
16377
|
message: hit.message,
|
|
16230
16378
|
matched_line: hit.matched_line
|
|
16231
|
-
}))
|
|
16379
|
+
})),
|
|
16380
|
+
command_hits: commandHits,
|
|
16381
|
+
command_skipped: commandSkipped
|
|
16232
16382
|
};
|
|
16233
16383
|
if (opts.json) {
|
|
16234
16384
|
console.log(JSON.stringify(output, null, 2));
|
|
16235
16385
|
} else {
|
|
16236
|
-
|
|
16386
|
+
const total = hits.length + commandHits.length;
|
|
16387
|
+
console.log(ui.bold(`hAIve sensors check \u2014 ${total} hit(s), ${memories.length} regex + ${commandSpecs.length} command sensor(s)`));
|
|
16237
16388
|
for (const hit of hits) {
|
|
16238
16389
|
const marker = hit.severity === "block" ? ui.red("\u2717") : ui.yellow("\u26A0");
|
|
16239
16390
|
console.log(` ${marker} ${hit.memory_id} ${ui.dim(`(${hit.severity})`)}`);
|
|
@@ -16241,8 +16392,17 @@ function registerSensors(program2) {
|
|
|
16241
16392
|
console.log(` ${hit.message}`);
|
|
16242
16393
|
if (hit.matched_line) console.log(` ${ui.dim(hit.matched_line)}`);
|
|
16243
16394
|
}
|
|
16395
|
+
for (const hit of commandHits) {
|
|
16396
|
+
const marker = hit.severity === "block" ? ui.red("\u2717") : ui.yellow("\u26A0");
|
|
16397
|
+
console.log(` ${marker} ${hit.memory_id} ${ui.dim(`(${hit.severity}, command)`)}`);
|
|
16398
|
+
console.log(` ${hit.message}`);
|
|
16399
|
+
console.log(` ${ui.dim(hit.matched_line)}`);
|
|
16400
|
+
}
|
|
16401
|
+
if (commandSkipped.length > 0) {
|
|
16402
|
+
console.log(ui.dim(` ${commandSkipped.length} command sensor(s) not run \u2014 pass --commands or set enforcement.runCommandSensors.`));
|
|
16403
|
+
}
|
|
16244
16404
|
}
|
|
16245
|
-
if (hits.some((hit) => hit.severity === "block")) process.exitCode = 1;
|
|
16405
|
+
if ([...hits, ...commandHits].some((hit) => hit.severity === "block")) process.exitCode = 1;
|
|
16246
16406
|
});
|
|
16247
16407
|
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) => {
|
|
16248
16408
|
const severity = opts.severity ?? "block";
|
|
@@ -16329,6 +16489,14 @@ async function runnableSensorMemories(paths, regexOnly = true) {
|
|
|
16329
16489
|
return !isRetiredMemory3(memory2.frontmatter, memory2.body);
|
|
16330
16490
|
});
|
|
16331
16491
|
}
|
|
16492
|
+
async function runCommandSensor(spec, root) {
|
|
16493
|
+
try {
|
|
16494
|
+
await exec2("bash", ["-c", spec.command], { cwd: root, timeout: 12e4, maxBuffer: 8 * 1024 * 1024 });
|
|
16495
|
+
return false;
|
|
16496
|
+
} catch {
|
|
16497
|
+
return true;
|
|
16498
|
+
}
|
|
16499
|
+
}
|
|
16332
16500
|
async function stagedDiff(root) {
|
|
16333
16501
|
try {
|
|
16334
16502
|
const { stdout } = await exec2("git", ["diff", "--cached"], { cwd: root });
|
|
@@ -16562,6 +16730,7 @@ import "commander";
|
|
|
16562
16730
|
import {
|
|
16563
16731
|
buildDashboard,
|
|
16564
16732
|
findProjectRoot as findProjectRoot55,
|
|
16733
|
+
loadConfig as loadConfig15,
|
|
16565
16734
|
loadMemoriesFromDir as loadMemoriesFromDir41,
|
|
16566
16735
|
loadPreventionEvents,
|
|
16567
16736
|
loadUsageIndex as loadUsageIndex30,
|
|
@@ -16581,11 +16750,13 @@ function registerDashboard(program2) {
|
|
|
16581
16750
|
const memories = existsSync78(paths.memoriesDir) ? await loadMemoriesFromDir41(paths.memoriesDir) : [];
|
|
16582
16751
|
const usage = await loadUsageIndex30(paths);
|
|
16583
16752
|
const preventionEvents = await loadPreventionEvents(paths);
|
|
16753
|
+
const config = await loadConfig15(paths);
|
|
16584
16754
|
const top = Math.max(1, Number.parseInt(opts.top ?? "10", 10) || 10);
|
|
16585
16755
|
const dormantDays = opts.dormantDays ? Number.parseInt(opts.dormantDays, 10) : void 0;
|
|
16586
16756
|
const report = buildDashboard(memories, usage, {
|
|
16587
16757
|
top,
|
|
16588
16758
|
preventionEvents,
|
|
16759
|
+
antiPatternGate: config.enforcement?.antiPatternGate ?? "anchored",
|
|
16589
16760
|
...dormantDays !== void 0 && Number.isFinite(dormantDays) ? { dormantDays } : {}
|
|
16590
16761
|
});
|
|
16591
16762
|
if (opts.json) {
|
|
@@ -16596,7 +16767,7 @@ function registerDashboard(program2) {
|
|
|
16596
16767
|
});
|
|
16597
16768
|
}
|
|
16598
16769
|
function renderDashboard(r) {
|
|
16599
|
-
const { inventory: inv, impact, sensors, health, decay, corpus, prevention } = r;
|
|
16770
|
+
const { inventory: inv, impact, sensors, health, decay, corpus, prevention, gate_precision: gate } = r;
|
|
16600
16771
|
console.log(ui.bold("hAIve dashboard"));
|
|
16601
16772
|
console.log(
|
|
16602
16773
|
` ${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`
|
|
@@ -16647,6 +16818,15 @@ function renderDashboard(r) {
|
|
|
16647
16818
|
}
|
|
16648
16819
|
}
|
|
16649
16820
|
console.log();
|
|
16821
|
+
console.log(ui.bold("Gate precision") + ui.dim(" (is the anti-pattern gate real or noisy?)"));
|
|
16822
|
+
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)}%`);
|
|
16823
|
+
console.log(
|
|
16824
|
+
` ${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"}`
|
|
16825
|
+
);
|
|
16826
|
+
if (gate.suggestion) {
|
|
16827
|
+
ui.info(`Tuning: set enforcement.antiPatternGate="${gate.suggestion.recommended}" \u2014 ${gate.suggestion.reason}`);
|
|
16828
|
+
}
|
|
16829
|
+
console.log();
|
|
16650
16830
|
console.log(ui.bold("Health"));
|
|
16651
16831
|
console.log(
|
|
16652
16832
|
` stale ${warnNum(health.stale)} \xB7 anchorless ${warnNum(health.anchorless)} \xB7 pending ${health.pending} \xB7 prune candidates ${warnNum(health.prune_candidates)}`
|
|
@@ -16744,9 +16924,268 @@ function registerDevLink(program2) {
|
|
|
16744
16924
|
});
|
|
16745
16925
|
}
|
|
16746
16926
|
|
|
16927
|
+
// src/commands/coverage.ts
|
|
16928
|
+
import "commander";
|
|
16929
|
+
import { findCoverageGaps, findProjectRoot as findProjectRoot57, resolveHaivePaths as resolveHaivePaths52 } from "@hiveai/core";
|
|
16930
|
+
function isNoisePath(p) {
|
|
16931
|
+
if (/(^|\/)(node_modules|dist|build|coverage|\.next)\//.test(p)) return true;
|
|
16932
|
+
if (p.startsWith(".ai/")) return true;
|
|
16933
|
+
if (/\.(jsonl|lock|map|snap|min\.js)$/.test(p)) return true;
|
|
16934
|
+
if (/(^|\/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$/.test(p)) return true;
|
|
16935
|
+
if (/(^|\/)(CHANGELOG|LICENSE)(\.md)?$/.test(p)) return true;
|
|
16936
|
+
return false;
|
|
16937
|
+
}
|
|
16938
|
+
function registerCoverage(program2) {
|
|
16939
|
+
program2.command("coverage").description(
|
|
16940
|
+
"Coverage-gap report: frequently-edited files with no covering team memory (blind spots)."
|
|
16941
|
+
).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) => {
|
|
16942
|
+
const root = findProjectRoot57(opts.dir);
|
|
16943
|
+
const paths = resolveHaivePaths52(root);
|
|
16944
|
+
const minChanges = Math.max(1, parseInt(opts.minChanges ?? "3", 10) || 3);
|
|
16945
|
+
const limit = Math.max(1, parseInt(opts.limit ?? "20", 10) || 20);
|
|
16946
|
+
const days = Math.max(1, parseInt(opts.days ?? "90", 10) || 90);
|
|
16947
|
+
const radar = await buildRadar({
|
|
16948
|
+
root,
|
|
16949
|
+
taskTokens: null,
|
|
16950
|
+
filePaths: [],
|
|
16951
|
+
daysBack: Math.ceil(days / 6),
|
|
16952
|
+
// getHotFiles multiplies daysBack by 6
|
|
16953
|
+
maxHotFiles: 500
|
|
16954
|
+
});
|
|
16955
|
+
const hotFiles = radar.hotFiles.filter((h) => !isNoisePath(h.path)).map((h) => ({ path: h.path, changes: h.changes }));
|
|
16956
|
+
const memories = await loadMemoriesFromDir27(paths.memoriesDir);
|
|
16957
|
+
const gaps = findCoverageGaps(hotFiles, memories, { minChanges, limit });
|
|
16958
|
+
if (opts.json) {
|
|
16959
|
+
console.log(JSON.stringify({ root, scanned_hot_files: hotFiles.length, gaps }, null, 2));
|
|
16960
|
+
return;
|
|
16961
|
+
}
|
|
16962
|
+
if (!radar.insideGitRepo) {
|
|
16963
|
+
ui.warn("Not a git repository \u2014 coverage uses git churn to find hot files.");
|
|
16964
|
+
return;
|
|
16965
|
+
}
|
|
16966
|
+
if (gaps.length === 0) {
|
|
16967
|
+
ui.success(`No coverage gaps: every file changed \u2265${minChanges}\xD7 is covered by a team memory.`);
|
|
16968
|
+
return;
|
|
16969
|
+
}
|
|
16970
|
+
console.log(ui.bold(`hAIve coverage \u2014 ${gaps.length} blind spot(s) (hot files with no covering memory)`));
|
|
16971
|
+
for (const gap of gaps) {
|
|
16972
|
+
console.log(` ${ui.yellow("\u25CB")} ${gap.path} ${ui.dim(`(${gap.changes} change${gap.changes === 1 ? "" : "s"})`)}`);
|
|
16973
|
+
}
|
|
16974
|
+
console.log(
|
|
16975
|
+
ui.dim(
|
|
16976
|
+
"\nAdd a decision/convention/gotcha anchored to the top files, or a sensor, to close the gap."
|
|
16977
|
+
)
|
|
16978
|
+
);
|
|
16979
|
+
});
|
|
16980
|
+
}
|
|
16981
|
+
|
|
16982
|
+
// src/commands/merge-driver.ts
|
|
16983
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
16984
|
+
import { readFileSync, writeFileSync, existsSync as existsSync80 } from "fs";
|
|
16985
|
+
import path56 from "path";
|
|
16986
|
+
import "commander";
|
|
16987
|
+
import { findProjectRoot as findProjectRoot58, mergeMemoryVersions } from "@hiveai/core";
|
|
16988
|
+
var GITATTRIBUTES_MARK = "# hAIve merge driver";
|
|
16989
|
+
var GITATTRIBUTES_BLOCK = [
|
|
16990
|
+
GITATTRIBUTES_MARK,
|
|
16991
|
+
".ai/memories/**/*.md merge=haive",
|
|
16992
|
+
"# hAIve merge driver end"
|
|
16993
|
+
].join("\n");
|
|
16994
|
+
function registerMergeDriver(program2) {
|
|
16995
|
+
const cmd = program2.command("merge-driver").description("Deterministic git merge driver for hAIve memory files (kills .ai/ conflict markers)");
|
|
16996
|
+
cmd.command("run <base> <ours> <theirs>").description("Git merge-driver entrypoint: resolve ours/theirs by frontmatter order, write into <ours>").action((base, ours, theirs) => {
|
|
16997
|
+
try {
|
|
16998
|
+
const oursContent = readFileSync(ours, "utf8");
|
|
16999
|
+
const theirsContent = readFileSync(theirs, "utf8");
|
|
17000
|
+
const result = mergeMemoryVersions(oursContent, theirsContent);
|
|
17001
|
+
if (result.content !== oursContent) writeFileSync(ours, result.content, "utf8");
|
|
17002
|
+
process.exit(0);
|
|
17003
|
+
} catch {
|
|
17004
|
+
process.exit(1);
|
|
17005
|
+
}
|
|
17006
|
+
});
|
|
17007
|
+
cmd.command("install").description("Configure git + .gitattributes so memory-file conflicts auto-resolve").option("-d, --dir <dir>", "project root").action((opts) => {
|
|
17008
|
+
const root = findProjectRoot58(opts.dir);
|
|
17009
|
+
try {
|
|
17010
|
+
execFileSync3("git", ["config", "merge.haive.name", "hAIve memory merge driver"], { cwd: root });
|
|
17011
|
+
execFileSync3("git", ["config", "merge.haive.driver", "haive merge-driver run %O %A %B"], { cwd: root });
|
|
17012
|
+
} catch {
|
|
17013
|
+
ui.error("Could not set git config \u2014 is this a git repository?");
|
|
17014
|
+
process.exitCode = 1;
|
|
17015
|
+
return;
|
|
17016
|
+
}
|
|
17017
|
+
const gaPath = path56.join(root, ".gitattributes");
|
|
17018
|
+
let content = existsSync80(gaPath) ? readFileSync(gaPath, "utf8") : "";
|
|
17019
|
+
if (!content.includes(GITATTRIBUTES_MARK)) {
|
|
17020
|
+
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
17021
|
+
content += GITATTRIBUTES_BLOCK + "\n";
|
|
17022
|
+
writeFileSync(gaPath, content, "utf8");
|
|
17023
|
+
ui.success("Installed hAIve merge driver (git config + .gitattributes).");
|
|
17024
|
+
} else {
|
|
17025
|
+
ui.info("hAIve merge driver already present in .gitattributes \u2014 refreshed git config.");
|
|
17026
|
+
}
|
|
17027
|
+
ui.info("Memory-file conflicts under .ai/memories/ now resolve by revision_count \u2192 created_at.");
|
|
17028
|
+
});
|
|
17029
|
+
}
|
|
17030
|
+
|
|
17031
|
+
// src/commands/memory-resolve-conflict.ts
|
|
17032
|
+
import { writeFile as writeFile38 } from "fs/promises";
|
|
17033
|
+
import { existsSync as existsSync81 } from "fs";
|
|
17034
|
+
import "commander";
|
|
17035
|
+
import {
|
|
17036
|
+
findProjectRoot as findProjectRoot59,
|
|
17037
|
+
planConflictResolution,
|
|
17038
|
+
resolveHaivePaths as resolveHaivePaths53,
|
|
17039
|
+
serializeMemory as serializeMemory29
|
|
17040
|
+
} from "@hiveai/core";
|
|
17041
|
+
function registerMemoryResolveConflict(memory2) {
|
|
17042
|
+
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) => {
|
|
17043
|
+
const root = findProjectRoot59(opts.dir);
|
|
17044
|
+
const paths = resolveHaivePaths53(root);
|
|
17045
|
+
if (!existsSync81(paths.memoriesDir)) {
|
|
17046
|
+
ui.error(`No .ai/memories at ${root}.`);
|
|
17047
|
+
process.exitCode = 1;
|
|
17048
|
+
return;
|
|
17049
|
+
}
|
|
17050
|
+
const memories = await loadMemoriesFromDir27(paths.memoriesDir);
|
|
17051
|
+
const a = memories.find((m) => m.memory.frontmatter.id === idA);
|
|
17052
|
+
const b = memories.find((m) => m.memory.frontmatter.id === idB);
|
|
17053
|
+
if (!a || !b) {
|
|
17054
|
+
ui.error(`Memory not found: ${!a ? idA : ""} ${!b ? idB : ""}`.trim());
|
|
17055
|
+
process.exitCode = 1;
|
|
17056
|
+
return;
|
|
17057
|
+
}
|
|
17058
|
+
const plan = planConflictResolution(a, b);
|
|
17059
|
+
const loser = plan.supersede_id === idA ? a : b;
|
|
17060
|
+
if (opts.json) {
|
|
17061
|
+
console.log(JSON.stringify({ ...plan, applied: Boolean(opts.yes) }, null, 2));
|
|
17062
|
+
} else {
|
|
17063
|
+
console.log(ui.bold("Conflict resolution"));
|
|
17064
|
+
console.log(` keep: ${ui.green(plan.keep_id)}`);
|
|
17065
|
+
console.log(` supersede: ${ui.red(plan.supersede_id)} ${ui.dim(`\u2192 deprecated`)}`);
|
|
17066
|
+
console.log(` reason: ${plan.reason}`);
|
|
17067
|
+
}
|
|
17068
|
+
if (!opts.yes) {
|
|
17069
|
+
if (!opts.json) ui.info("Preview only \u2014 re-run with --yes to apply.");
|
|
17070
|
+
return;
|
|
17071
|
+
}
|
|
17072
|
+
await writeFile38(
|
|
17073
|
+
loser.filePath,
|
|
17074
|
+
serializeMemory29({
|
|
17075
|
+
frontmatter: {
|
|
17076
|
+
...loser.memory.frontmatter,
|
|
17077
|
+
status: "deprecated",
|
|
17078
|
+
stale_reason: plan.stale_reason,
|
|
17079
|
+
related_ids: [.../* @__PURE__ */ new Set([...loser.memory.frontmatter.related_ids, plan.keep_id])]
|
|
17080
|
+
},
|
|
17081
|
+
body: loser.memory.body
|
|
17082
|
+
}),
|
|
17083
|
+
"utf8"
|
|
17084
|
+
);
|
|
17085
|
+
if (!opts.json) ui.success(`Deprecated ${plan.supersede_id}; ${plan.keep_id} remains authoritative.`);
|
|
17086
|
+
});
|
|
17087
|
+
}
|
|
17088
|
+
|
|
17089
|
+
// src/commands/memory-seed-git.ts
|
|
17090
|
+
import { execFile as execFile4 } from "child_process";
|
|
17091
|
+
import { mkdir as mkdir24, writeFile as writeFile39 } from "fs/promises";
|
|
17092
|
+
import { existsSync as existsSync83 } from "fs";
|
|
17093
|
+
import path57 from "path";
|
|
17094
|
+
import { promisify as promisify4 } from "util";
|
|
17095
|
+
import "commander";
|
|
17096
|
+
import {
|
|
17097
|
+
buildFrontmatter as buildFrontmatter12,
|
|
17098
|
+
findProjectRoot as findProjectRoot60,
|
|
17099
|
+
memoryFilePath as memoryFilePath13,
|
|
17100
|
+
proposeSeedsFromCommits,
|
|
17101
|
+
resolveHaivePaths as resolveHaivePaths54,
|
|
17102
|
+
serializeMemory as serializeMemory30
|
|
17103
|
+
} from "@hiveai/core";
|
|
17104
|
+
var exec4 = promisify4(execFile4);
|
|
17105
|
+
function registerMemorySeedGit(memory2) {
|
|
17106
|
+
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) => {
|
|
17107
|
+
const root = findProjectRoot60(opts.dir);
|
|
17108
|
+
const paths = resolveHaivePaths54(root);
|
|
17109
|
+
if (!existsSync83(paths.haiveDir)) {
|
|
17110
|
+
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
17111
|
+
process.exitCode = 1;
|
|
17112
|
+
return;
|
|
17113
|
+
}
|
|
17114
|
+
const limit = Math.max(1, parseInt(opts.limit ?? "20", 10) || 20);
|
|
17115
|
+
const days = Math.max(1, parseInt(opts.days ?? "365", 10) || 365);
|
|
17116
|
+
const commits = await readCommits(root, days);
|
|
17117
|
+
const proposals = proposeSeedsFromCommits(commits, limit);
|
|
17118
|
+
if (opts.json) {
|
|
17119
|
+
console.log(JSON.stringify({ scanned_commits: commits.length, proposals, applied: Boolean(opts.apply) }, null, 2));
|
|
17120
|
+
} else if (proposals.length === 0) {
|
|
17121
|
+
ui.info("No revert/hotfix signals found in git history \u2014 nothing to seed.");
|
|
17122
|
+
return;
|
|
17123
|
+
} else {
|
|
17124
|
+
console.log(ui.bold(`hAIve seed-git \u2014 ${proposals.length} proposal(s) from ${commits.length} commit(s)`));
|
|
17125
|
+
for (const p of proposals) {
|
|
17126
|
+
console.log(` ${ui.yellow("\u25C6")} ${ui.dim(`[${p.kind}]`)} ${p.what}`);
|
|
17127
|
+
if (p.paths.length > 0) console.log(` ${ui.dim("paths:")} ${p.paths.join(", ")}`);
|
|
17128
|
+
}
|
|
17129
|
+
}
|
|
17130
|
+
if (!opts.apply) {
|
|
17131
|
+
if (!opts.json) ui.info("Preview only \u2014 re-run with --apply to write these as draft memories.");
|
|
17132
|
+
return;
|
|
17133
|
+
}
|
|
17134
|
+
let written = 0;
|
|
17135
|
+
for (const p of proposals) {
|
|
17136
|
+
const fm = {
|
|
17137
|
+
...buildFrontmatter12({
|
|
17138
|
+
type: "attempt",
|
|
17139
|
+
slug: p.slug,
|
|
17140
|
+
scope: opts.scope ?? "team",
|
|
17141
|
+
tags: ["seed", "git-history", p.kind],
|
|
17142
|
+
paths: p.paths
|
|
17143
|
+
}),
|
|
17144
|
+
status: "draft"
|
|
17145
|
+
// human reviews before it becomes validated
|
|
17146
|
+
};
|
|
17147
|
+
const body = `# ${p.what}
|
|
17148
|
+
|
|
17149
|
+
**Why it failed / do NOT use:** ${p.why_failed}
|
|
17150
|
+
|
|
17151
|
+
_Seeded from git ${p.kind} commit ${p.source_sha}. Review and validate (or delete) \u2014 not yet authoritative._
|
|
17152
|
+
`;
|
|
17153
|
+
const file = memoryFilePath13(paths, fm.scope, fm.id, fm.module);
|
|
17154
|
+
if (existsSync83(file)) continue;
|
|
17155
|
+
await mkdir24(path57.dirname(file), { recursive: true });
|
|
17156
|
+
await writeFile39(file, serializeMemory30({ frontmatter: fm, body }), "utf8");
|
|
17157
|
+
written += 1;
|
|
17158
|
+
}
|
|
17159
|
+
if (!opts.json) {
|
|
17160
|
+
ui.success(`Wrote ${written} draft seed(s). Review them: \`haive memory pending\` \u2192 validate or delete.`);
|
|
17161
|
+
}
|
|
17162
|
+
});
|
|
17163
|
+
}
|
|
17164
|
+
async function readCommits(root, days) {
|
|
17165
|
+
try {
|
|
17166
|
+
const { stdout } = await exec4(
|
|
17167
|
+
"git",
|
|
17168
|
+
["log", `--since=${days}.days.ago`, "--name-only", "--pretty=format:%x1f%h%x1f%s", "-n", "500"],
|
|
17169
|
+
{ cwd: root, maxBuffer: 8 * 1024 * 1024 }
|
|
17170
|
+
);
|
|
17171
|
+
const blocks = stdout.split("").filter((b) => b.length > 0);
|
|
17172
|
+
const commits = [];
|
|
17173
|
+
for (let i = 0; i + 1 < blocks.length; i += 2) {
|
|
17174
|
+
const sha = blocks[i].trim();
|
|
17175
|
+
const tail = blocks[i + 1];
|
|
17176
|
+
const lines = tail.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
17177
|
+
const subject = lines.shift() ?? "";
|
|
17178
|
+
commits.push({ sha, subject, files: lines });
|
|
17179
|
+
}
|
|
17180
|
+
return commits;
|
|
17181
|
+
} catch {
|
|
17182
|
+
return [];
|
|
17183
|
+
}
|
|
17184
|
+
}
|
|
17185
|
+
|
|
16747
17186
|
// src/index.ts
|
|
16748
|
-
var program = new
|
|
16749
|
-
program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.
|
|
17187
|
+
var program = new Command63();
|
|
17188
|
+
program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.17.0").option("--advanced", "show maintenance and experimental commands in help");
|
|
16750
17189
|
registerInit(program);
|
|
16751
17190
|
registerWelcome(program);
|
|
16752
17191
|
registerResolveProject(program);
|
|
@@ -16757,6 +17196,8 @@ registerAgent(program);
|
|
|
16757
17196
|
registerSensors(program);
|
|
16758
17197
|
registerIngest(program);
|
|
16759
17198
|
registerDashboard(program);
|
|
17199
|
+
registerCoverage(program);
|
|
17200
|
+
registerMergeDriver(program);
|
|
16760
17201
|
registerDevLink(program);
|
|
16761
17202
|
registerMcp(program);
|
|
16762
17203
|
registerBriefing(program);
|
|
@@ -16787,6 +17228,8 @@ registerMemoryUpdate(memory);
|
|
|
16787
17228
|
registerMemoryHot(memory);
|
|
16788
17229
|
registerMemoryTried(memory);
|
|
16789
17230
|
registerMemorySeed(memory);
|
|
17231
|
+
registerMemorySeedGit(memory);
|
|
17232
|
+
registerMemoryResolveConflict(memory);
|
|
16790
17233
|
registerMemoryImport(memory);
|
|
16791
17234
|
registerMemoryImportChangelog(memory);
|
|
16792
17235
|
registerMemoryDigest(memory);
|