@hiveai/cli 0.9.13 → 0.9.15
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 +352 -29
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
package/dist/index.js
CHANGED
|
@@ -3063,6 +3063,7 @@ import {
|
|
|
3063
3063
|
loadMemoriesFromDir as loadMemoriesFromDir13,
|
|
3064
3064
|
loadUsageIndex as loadUsageIndex7,
|
|
3065
3065
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths22,
|
|
3066
|
+
pathsOverlap,
|
|
3066
3067
|
queryCodeMap as queryCodeMap2,
|
|
3067
3068
|
resolveBriefingBudget as resolveBriefingBudget2,
|
|
3068
3069
|
serializeMemory as serializeMemory9,
|
|
@@ -3126,7 +3127,7 @@ import {
|
|
|
3126
3127
|
getUsage as getUsage9,
|
|
3127
3128
|
loadMemoriesFromDir as loadMemoriesFromDir20,
|
|
3128
3129
|
loadUsageIndex as loadUsageIndex11,
|
|
3129
|
-
pathsOverlap,
|
|
3130
|
+
pathsOverlap as pathsOverlap2,
|
|
3130
3131
|
tokenizeQuery as tokenizeQuery5
|
|
3131
3132
|
} from "@hiveai/core";
|
|
3132
3133
|
import { z as z27 } from "zod";
|
|
@@ -4654,10 +4655,14 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
4654
4655
|
const createdAt = loaded?.memory.frontmatter.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4655
4656
|
if (isDecaying(u, createdAt)) decayWarnings.push(m.id);
|
|
4656
4657
|
}
|
|
4657
|
-
const
|
|
4658
|
+
const formattedMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : input.format === "actions" ? trimmedMemories.map((m) => ({
|
|
4658
4659
|
...m,
|
|
4659
4660
|
body: extractActionsBriefBody2(m.body)
|
|
4660
4661
|
})) : trimmedMemories;
|
|
4662
|
+
const outputMemories = formattedMemories.map((m) => ({
|
|
4663
|
+
...m,
|
|
4664
|
+
why: explainWhySurfaced(m, byId.get(m.id), input.files, inferred)
|
|
4665
|
+
}));
|
|
4661
4666
|
let symbolLocations;
|
|
4662
4667
|
const symbolsToLookup = new Set(input.symbols);
|
|
4663
4668
|
for (const m of outputMemories) {
|
|
@@ -4823,6 +4828,44 @@ function compactSummary(body) {
|
|
|
4823
4828
|
}
|
|
4824
4829
|
return body.slice(0, 120);
|
|
4825
4830
|
}
|
|
4831
|
+
function explainWhySurfaced(memory2, loaded, inputFiles, inferredModules) {
|
|
4832
|
+
const why = [];
|
|
4833
|
+
const fm = loaded?.memory.frontmatter;
|
|
4834
|
+
if (memory2.reasons.includes("anchor") && fm) {
|
|
4835
|
+
const matching = fm.anchor.paths.filter(
|
|
4836
|
+
(p) => inputFiles.length === 0 || inputFiles.some((file) => pathsOverlap(p, file))
|
|
4837
|
+
);
|
|
4838
|
+
if (matching.length > 0) {
|
|
4839
|
+
why.push(`Anchored to touched path${matching.length === 1 ? "" : "s"}: ${matching.slice(0, 4).join(", ")}`);
|
|
4840
|
+
} else if (fm.anchor.paths.length > 0) {
|
|
4841
|
+
why.push(`Pulled by related anchor: ${fm.anchor.paths.slice(0, 4).join(", ")}`);
|
|
4842
|
+
}
|
|
4843
|
+
if (fm.anchor.symbols.length > 0) {
|
|
4844
|
+
why.push(`Anchor symbol${fm.anchor.symbols.length === 1 ? "" : "s"}: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
if (memory2.reasons.includes("module")) {
|
|
4848
|
+
const moduleHints = [
|
|
4849
|
+
...memory2.module ? [memory2.module] : [],
|
|
4850
|
+
...memory2.tags.filter((tag) => inferredModules.includes(tag))
|
|
4851
|
+
];
|
|
4852
|
+
const shown = moduleHints.length > 0 ? [...new Set(moduleHints)].join(", ") : inferredModules.join(", ");
|
|
4853
|
+
why.push(shown ? `Matched inferred module/tag: ${shown}` : "Matched inferred module context.");
|
|
4854
|
+
}
|
|
4855
|
+
if (memory2.reasons.includes("domain")) {
|
|
4856
|
+
why.push("Matched inferred domain from the target file paths.");
|
|
4857
|
+
}
|
|
4858
|
+
if (memory2.reasons.includes("semantic")) {
|
|
4859
|
+
const score = memory2.semantic_score !== void 0 ? ` score=${Math.round(memory2.semantic_score * 100) / 100}` : "";
|
|
4860
|
+
why.push(`${memory2.match_quality === "exact" ? "Literal task match" : "Semantic/task relevance"}${score}.`);
|
|
4861
|
+
}
|
|
4862
|
+
why.push(`Confidence: ${memory2.confidence}; read ${memory2.read_count} time${memory2.read_count === 1 ? "" : "s"}.`);
|
|
4863
|
+
if (memory2.type === "attempt") why.push("Failed-approach record; read before repeating the same path.");
|
|
4864
|
+
if (memory2.status === "proposed" || memory2.status === "draft") {
|
|
4865
|
+
why.push("Unvalidated record; use cautiously or ask a human before treating it as policy.");
|
|
4866
|
+
}
|
|
4867
|
+
return why;
|
|
4868
|
+
}
|
|
4826
4869
|
async function trySemanticHits(ctx, task, limit) {
|
|
4827
4870
|
let mod;
|
|
4828
4871
|
try {
|
|
@@ -5585,7 +5628,7 @@ async function memConflicts(input, ctx) {
|
|
|
5585
5628
|
const otherText = (other.memory.body + " " + fm.tags.join(" ")).toLowerCase();
|
|
5586
5629
|
const reasons = [];
|
|
5587
5630
|
const sim = simScores?.get(fm.id) ?? null;
|
|
5588
|
-
const hasPathOverlap = fm.anchor.paths.some((p) => targetPaths.some((tp) =>
|
|
5631
|
+
const hasPathOverlap = fm.anchor.paths.some((p) => targetPaths.some((tp) => pathsOverlap2(p, tp)));
|
|
5589
5632
|
const otherTokens = new Set(tokenizeQuery5(otherText));
|
|
5590
5633
|
const tokenOverlap = countIntersection(targetTokens, otherTokens);
|
|
5591
5634
|
const isSemanticNeighbor = sim !== null && sim >= input.min_score;
|
|
@@ -5620,7 +5663,7 @@ async function memConflicts(input, ctx) {
|
|
|
5620
5663
|
body_preview: other.memory.body.split("\n").slice(0, 4).join("\n").slice(0, 300),
|
|
5621
5664
|
similarity: sim,
|
|
5622
5665
|
reasons,
|
|
5623
|
-
shared_paths: fm.anchor.paths.filter((p) => targetPaths.some((tp) =>
|
|
5666
|
+
shared_paths: fm.anchor.paths.filter((p) => targetPaths.some((tp) => pathsOverlap2(p, tp)))
|
|
5624
5667
|
});
|
|
5625
5668
|
}
|
|
5626
5669
|
conflicts.sort((a, b) => {
|
|
@@ -5707,10 +5750,13 @@ async function preCommitCheck(input, ctx) {
|
|
|
5707
5750
|
const filesTouching = new Set(relevantMatches.map((m) => m.id));
|
|
5708
5751
|
const staleHits = verifyResult.results.filter((r) => r.stale && filesTouching.has(r.id));
|
|
5709
5752
|
const blockOn = input.block_on;
|
|
5710
|
-
const
|
|
5753
|
+
const classifiedWarnings = apResult.warnings.map(classifyWarning);
|
|
5754
|
+
const blockingWarnings = classifiedWarnings.filter((w) => w.level === "blocking");
|
|
5755
|
+
const reviewWarnings = classifiedWarnings.filter((w) => w.level === "review");
|
|
5756
|
+
const infoWarnings = classifiedWarnings.filter((w) => w.level === "info");
|
|
5711
5757
|
let should_block = false;
|
|
5712
5758
|
if (blockOn !== "never") {
|
|
5713
|
-
if (blockOn === "any" && (
|
|
5759
|
+
if (blockOn === "any" && (blockingWarnings.length > 0 || reviewWarnings.length > 0 || staleHits.length > 0)) should_block = true;
|
|
5714
5760
|
if (blockOn === "high-confidence" && (blockingWarnings.length > 0 || staleHits.length > 0)) should_block = true;
|
|
5715
5761
|
}
|
|
5716
5762
|
const relevant_memories = relevantMatches.slice(0, 8).map((m) => ({
|
|
@@ -5724,10 +5770,12 @@ async function preCommitCheck(input, ctx) {
|
|
|
5724
5770
|
summary: {
|
|
5725
5771
|
anti_patterns: apResult.warnings.length,
|
|
5726
5772
|
blocking_warnings: blockingWarnings.length,
|
|
5773
|
+
review_warnings: reviewWarnings.length,
|
|
5774
|
+
info_warnings: infoWarnings.length,
|
|
5727
5775
|
relevant_memories: relevant_memories.length,
|
|
5728
5776
|
stale_anchors: staleHits.length
|
|
5729
5777
|
},
|
|
5730
|
-
warnings:
|
|
5778
|
+
warnings: classifiedWarnings,
|
|
5731
5779
|
relevant_memories,
|
|
5732
5780
|
stale_anchors: staleHits.map((r) => ({
|
|
5733
5781
|
id: r.id,
|
|
@@ -5737,6 +5785,30 @@ async function preCommitCheck(input, ctx) {
|
|
|
5737
5785
|
}))
|
|
5738
5786
|
};
|
|
5739
5787
|
}
|
|
5788
|
+
function classifyWarning(warning) {
|
|
5789
|
+
if (isBlockingWarning(warning)) {
|
|
5790
|
+
return {
|
|
5791
|
+
...warning,
|
|
5792
|
+
level: "blocking",
|
|
5793
|
+
rationale: "authoritative/trusted memory plus strong semantic match to the diff (score >= 0.65)"
|
|
5794
|
+
};
|
|
5795
|
+
}
|
|
5796
|
+
const hasSemantic = warning.reasons.includes("semantic");
|
|
5797
|
+
const semanticScore = warning.semantic_score ?? 0;
|
|
5798
|
+
const highConfidence = warning.confidence === "authoritative" || warning.confidence === "trusted";
|
|
5799
|
+
if (hasSemantic && semanticScore >= 0.45 || highConfidence && warning.reasons.includes("anchor") && warning.reasons.includes("literal")) {
|
|
5800
|
+
return {
|
|
5801
|
+
...warning,
|
|
5802
|
+
level: "review",
|
|
5803
|
+
rationale: hasSemantic ? "semantic match is plausible but below blocking threshold" : "anchored high-confidence memory also matched diff tokens, but no strong semantic proof"
|
|
5804
|
+
};
|
|
5805
|
+
}
|
|
5806
|
+
return {
|
|
5807
|
+
...warning,
|
|
5808
|
+
level: "info",
|
|
5809
|
+
rationale: "weak signal only (literal/anchor/low semantic evidence); surfaced for audit, hidden in concise CLI output"
|
|
5810
|
+
};
|
|
5811
|
+
}
|
|
5740
5812
|
function isBlockingWarning(warning) {
|
|
5741
5813
|
const highConfidence = warning.confidence === "authoritative" || warning.confidence === "trusted";
|
|
5742
5814
|
if (!highConfidence) return false;
|
|
@@ -6273,7 +6345,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
6273
6345
|
};
|
|
6274
6346
|
}
|
|
6275
6347
|
var SERVER_NAME = "haive";
|
|
6276
|
-
var SERVER_VERSION = "0.9.
|
|
6348
|
+
var SERVER_VERSION = "0.9.15";
|
|
6277
6349
|
function jsonResult(data) {
|
|
6278
6350
|
return {
|
|
6279
6351
|
content: [
|
|
@@ -6287,9 +6359,12 @@ function jsonResult(data) {
|
|
|
6287
6359
|
var ENFORCEMENT_PROFILE_TOOLS = /* @__PURE__ */ new Set([
|
|
6288
6360
|
"get_briefing",
|
|
6289
6361
|
"mem_save",
|
|
6362
|
+
"mem_tried",
|
|
6290
6363
|
"mem_search",
|
|
6364
|
+
"mem_get",
|
|
6291
6365
|
"mem_verify",
|
|
6292
6366
|
"mem_relevant_to",
|
|
6367
|
+
"code_map",
|
|
6293
6368
|
"pre_commit_check",
|
|
6294
6369
|
"mem_session_end"
|
|
6295
6370
|
]);
|
|
@@ -10641,9 +10716,9 @@ function parseDays(input) {
|
|
|
10641
10716
|
|
|
10642
10717
|
// src/commands/doctor.ts
|
|
10643
10718
|
import { existsSync as existsSync60 } from "fs";
|
|
10644
|
-
import { stat } from "fs/promises";
|
|
10719
|
+
import { readFile as readFile17, stat } from "fs/promises";
|
|
10645
10720
|
import path41 from "path";
|
|
10646
|
-
import { execSync as execSync3 } from "child_process";
|
|
10721
|
+
import { execFileSync, execSync as execSync3 } from "child_process";
|
|
10647
10722
|
import "commander";
|
|
10648
10723
|
import {
|
|
10649
10724
|
codeMapPath as codeMapPath2,
|
|
@@ -10681,8 +10756,8 @@ function registerDoctor(program2) {
|
|
|
10681
10756
|
fix: "haive init"
|
|
10682
10757
|
});
|
|
10683
10758
|
} else {
|
|
10684
|
-
const { readFile:
|
|
10685
|
-
const content = await
|
|
10759
|
+
const { readFile: readFile19 } = await import("fs/promises");
|
|
10760
|
+
const content = await readFile19(paths.projectContext, "utf8");
|
|
10686
10761
|
const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
|
|
10687
10762
|
if (isTemplate) {
|
|
10688
10763
|
findings.push({
|
|
@@ -10808,8 +10883,8 @@ function registerDoctor(program2) {
|
|
|
10808
10883
|
let hasClaudeEnforcement = false;
|
|
10809
10884
|
if (existsSync60(claudeSettings)) {
|
|
10810
10885
|
try {
|
|
10811
|
-
const { readFile:
|
|
10812
|
-
const raw = await
|
|
10886
|
+
const { readFile: readFile19 } = await import("fs/promises");
|
|
10887
|
+
const raw = await readFile19(claudeSettings, "utf8");
|
|
10813
10888
|
hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
|
|
10814
10889
|
} catch {
|
|
10815
10890
|
hasClaudeEnforcement = false;
|
|
@@ -10832,13 +10907,14 @@ function registerDoctor(program2) {
|
|
|
10832
10907
|
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
|
|
10833
10908
|
});
|
|
10834
10909
|
}
|
|
10910
|
+
findings.push(...await collectInstallFindings(root, "0.9.15"));
|
|
10835
10911
|
try {
|
|
10836
10912
|
const legacyRaw = execSync3("haive-mcp --version", {
|
|
10837
10913
|
encoding: "utf8",
|
|
10838
10914
|
timeout: 3e3,
|
|
10839
10915
|
stdio: ["ignore", "pipe", "ignore"]
|
|
10840
10916
|
}).trim();
|
|
10841
|
-
const cliVersion = "0.9.
|
|
10917
|
+
const cliVersion = "0.9.15";
|
|
10842
10918
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
10843
10919
|
findings.push({
|
|
10844
10920
|
severity: "warn",
|
|
@@ -10885,6 +10961,103 @@ function emit(findings, opts) {
|
|
|
10885
10961
|
function isSearchTool(name) {
|
|
10886
10962
|
return ["mem_search", "code_search", "mem_relevant_to", "get_briefing"].includes(name);
|
|
10887
10963
|
}
|
|
10964
|
+
async function collectInstallFindings(root, expectedVersion) {
|
|
10965
|
+
const findings = [];
|
|
10966
|
+
const haiveBins = listHaiveBins();
|
|
10967
|
+
if (haiveBins.length === 0) {
|
|
10968
|
+
findings.push({
|
|
10969
|
+
severity: "warn",
|
|
10970
|
+
code: "haive-not-on-path",
|
|
10971
|
+
message: "No `haive` binary was found on PATH. Hooks and MCP configs that call `haive` may fail.",
|
|
10972
|
+
fix: `npm install -g @hiveai/cli@${expectedVersion}`
|
|
10973
|
+
});
|
|
10974
|
+
} else {
|
|
10975
|
+
const first = haiveBins[0];
|
|
10976
|
+
const firstVersion = versionForBinary(first);
|
|
10977
|
+
if (firstVersion && firstVersion !== expectedVersion) {
|
|
10978
|
+
findings.push({
|
|
10979
|
+
severity: "warn",
|
|
10980
|
+
code: "path-haive-version-mismatch",
|
|
10981
|
+
message: `PATH resolves haive to ${first} (${firstVersion}), but this build expects ${expectedVersion}.`,
|
|
10982
|
+
fix: `npm install -g @hiveai/cli@${expectedVersion}
|
|
10983
|
+
which -a haive`
|
|
10984
|
+
});
|
|
10985
|
+
}
|
|
10986
|
+
const skewed = haiveBins.map((bin) => ({ bin, version: versionForBinary(bin) })).filter((item) => item.version && item.version !== expectedVersion);
|
|
10987
|
+
if (skewed.length > 0) {
|
|
10988
|
+
findings.push({
|
|
10989
|
+
severity: "info",
|
|
10990
|
+
code: "multiple-haive-binaries",
|
|
10991
|
+
message: `Found ${haiveBins.length} haive binar${haiveBins.length === 1 ? "y" : "ies"} on PATH; ${skewed.length} do not match ${expectedVersion}.`,
|
|
10992
|
+
fix: "Remove stale global installs or ensure hooks call the intended `haive` binary."
|
|
10993
|
+
});
|
|
10994
|
+
}
|
|
10995
|
+
}
|
|
10996
|
+
const integrationFiles = [
|
|
10997
|
+
".git/hooks/pre-commit",
|
|
10998
|
+
".git/hooks/pre-push",
|
|
10999
|
+
".claude/settings.local.json",
|
|
11000
|
+
".mcp.json",
|
|
11001
|
+
".cursor/mcp.json",
|
|
11002
|
+
".vscode/mcp.json"
|
|
11003
|
+
];
|
|
11004
|
+
for (const rel of integrationFiles) {
|
|
11005
|
+
const file = path41.join(root, rel);
|
|
11006
|
+
if (!existsSync60(file)) continue;
|
|
11007
|
+
const text = await readFile17(file, "utf8").catch(() => "");
|
|
11008
|
+
for (const bin of extractAbsoluteHaiveBins(text)) {
|
|
11009
|
+
const version = versionForBinary(bin);
|
|
11010
|
+
if (!version) {
|
|
11011
|
+
findings.push({
|
|
11012
|
+
severity: "warn",
|
|
11013
|
+
code: "integration-haive-binary-missing",
|
|
11014
|
+
message: `${rel} references ${bin}, but it could not be executed.`,
|
|
11015
|
+
fix: "Run `haive agent setup --no-global` or `haive enforce install` to rewrite project integrations."
|
|
11016
|
+
});
|
|
11017
|
+
} else if (version !== expectedVersion) {
|
|
11018
|
+
findings.push({
|
|
11019
|
+
severity: "warn",
|
|
11020
|
+
code: "integration-haive-version-mismatch",
|
|
11021
|
+
message: `${rel} references ${bin} (${version}), but current hAIve is ${expectedVersion}.`,
|
|
11022
|
+
fix: "Run `haive agent setup --no-global` and `haive enforce install` to refresh stale paths."
|
|
11023
|
+
});
|
|
11024
|
+
}
|
|
11025
|
+
}
|
|
11026
|
+
}
|
|
11027
|
+
return findings;
|
|
11028
|
+
}
|
|
11029
|
+
function listHaiveBins() {
|
|
11030
|
+
try {
|
|
11031
|
+
return execSync3("which -a haive", {
|
|
11032
|
+
encoding: "utf8",
|
|
11033
|
+
timeout: 3e3,
|
|
11034
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
11035
|
+
}).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
11036
|
+
} catch {
|
|
11037
|
+
return [];
|
|
11038
|
+
}
|
|
11039
|
+
}
|
|
11040
|
+
function versionForBinary(bin) {
|
|
11041
|
+
try {
|
|
11042
|
+
const out = execFileSync(bin, ["--version"], {
|
|
11043
|
+
encoding: "utf8",
|
|
11044
|
+
timeout: 3e3,
|
|
11045
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
11046
|
+
}).trim();
|
|
11047
|
+
return out.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
|
|
11048
|
+
} catch {
|
|
11049
|
+
return null;
|
|
11050
|
+
}
|
|
11051
|
+
}
|
|
11052
|
+
function extractAbsoluteHaiveBins(text) {
|
|
11053
|
+
const out = /* @__PURE__ */ new Set();
|
|
11054
|
+
const re = /(["'\s])((?:\/[^"'\s]+)*\/haive)\b/g;
|
|
11055
|
+
let match;
|
|
11056
|
+
while (match = re.exec(text)) {
|
|
11057
|
+
if (match[2]) out.add(match[2]);
|
|
11058
|
+
}
|
|
11059
|
+
return [...out].sort();
|
|
11060
|
+
}
|
|
10888
11061
|
|
|
10889
11062
|
// src/commands/playback.ts
|
|
10890
11063
|
import { existsSync as existsSync61 } from "fs";
|
|
@@ -11050,19 +11223,21 @@ function registerPrecommit(program2) {
|
|
|
11050
11223
|
console.log(ui.bold(`hAIve precommit \u2014 ${touchedPaths.length} file(s)`));
|
|
11051
11224
|
console.log(
|
|
11052
11225
|
ui.dim(
|
|
11053
|
-
` anti-patterns: ${result.summary.anti_patterns} blocking: ${result.summary.blocking_warnings ?? result.summary.anti_patterns} relevant memories: ${result.summary.relevant_memories} stale anchors: ${result.summary.stale_anchors}`
|
|
11226
|
+
` anti-patterns: ${result.summary.anti_patterns} blocking: ${result.summary.blocking_warnings ?? result.summary.anti_patterns} review: ${result.summary.review_warnings ?? 0} info: ${result.summary.info_warnings ?? 0} relevant memories: ${result.summary.relevant_memories} stale anchors: ${result.summary.stale_anchors}`
|
|
11054
11227
|
)
|
|
11055
11228
|
);
|
|
11056
11229
|
console.log();
|
|
11057
|
-
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
|
|
11230
|
+
const blocking = result.warnings.filter((w) => w.level === "blocking");
|
|
11231
|
+
const review = result.warnings.filter((w) => w.level === "review");
|
|
11232
|
+
const info = result.warnings.filter((w) => w.level === "info");
|
|
11233
|
+
printWarnings("Blocking anti-patterns", blocking, "error");
|
|
11234
|
+
printWarnings("Review anti-patterns", review.slice(0, 8), "warn");
|
|
11235
|
+
if (info.length > 0) {
|
|
11236
|
+
console.log(
|
|
11237
|
+
ui.dim(
|
|
11238
|
+
`${info.length} weak anti-pattern signal${info.length === 1 ? "" : "s"} hidden. Use --json to inspect FYI matches.`
|
|
11239
|
+
)
|
|
11240
|
+
);
|
|
11066
11241
|
console.log();
|
|
11067
11242
|
}
|
|
11068
11243
|
if (result.relevant_memories.length > 0) {
|
|
@@ -11091,6 +11266,20 @@ function registerPrecommit(program2) {
|
|
|
11091
11266
|
}
|
|
11092
11267
|
});
|
|
11093
11268
|
}
|
|
11269
|
+
function printWarnings(title, warnings, tone) {
|
|
11270
|
+
if (warnings.length === 0) return;
|
|
11271
|
+
console.log(ui.bold(tone === "error" ? `\u2717 ${title}:` : `\u26A0 ${title}:`));
|
|
11272
|
+
for (const w of warnings) {
|
|
11273
|
+
const marker = tone === "error" ? ui.red("\u2717") : ui.yellow("\u26A0");
|
|
11274
|
+
console.log(` ${marker} ${w.id} ${ui.dim(`(${w.type}, ${w.confidence})`)}`);
|
|
11275
|
+
for (const line of w.body_preview.split("\n").slice(0, 3)) {
|
|
11276
|
+
console.log(` ${ui.dim(line)}`);
|
|
11277
|
+
}
|
|
11278
|
+
console.log(` ${ui.dim("reasons:")} ${w.reasons.join(", ")}`);
|
|
11279
|
+
if (w.rationale) console.log(` ${ui.dim("why shown:")} ${w.rationale}`);
|
|
11280
|
+
}
|
|
11281
|
+
console.log();
|
|
11282
|
+
}
|
|
11094
11283
|
function runCommand3(cmd, args, cwd) {
|
|
11095
11284
|
return new Promise((resolve, reject) => {
|
|
11096
11285
|
const proc = spawn4(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -11177,7 +11366,9 @@ import { existsSync as existsSync64 } from "fs";
|
|
|
11177
11366
|
import "commander";
|
|
11178
11367
|
import {
|
|
11179
11368
|
findProjectRoot as findProjectRoot44,
|
|
11369
|
+
getUsage as getUsage20,
|
|
11180
11370
|
loadMemoriesFromDir as loadMemoriesFromDir35,
|
|
11371
|
+
loadUsageIndex as loadUsageIndex26,
|
|
11181
11372
|
resolveHaivePaths as resolveHaivePaths40
|
|
11182
11373
|
} from "@hiveai/core";
|
|
11183
11374
|
async function lintMemoriesAsync(root) {
|
|
@@ -11185,7 +11376,9 @@ async function lintMemoriesAsync(root) {
|
|
|
11185
11376
|
const out = [];
|
|
11186
11377
|
if (!existsSync64(paths.memoriesDir)) return out;
|
|
11187
11378
|
const loaded = await loadMemoriesFromDir35(paths.memoriesDir);
|
|
11379
|
+
const usage = await loadUsageIndex26(paths);
|
|
11188
11380
|
const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
|
|
11381
|
+
const actionableWords = /\b(always|never|prefer|use|avoid|because|instead|why|rationale|do not|must|should)\b/i;
|
|
11189
11382
|
for (const { filePath, memory: memory2 } of loaded) {
|
|
11190
11383
|
const fm = memory2.frontmatter;
|
|
11191
11384
|
if (fm.type === "session_recap") continue;
|
|
@@ -11200,6 +11393,15 @@ async function lintMemoriesAsync(root) {
|
|
|
11200
11393
|
message: "Body looks very short (< ~40 chars of prose after headings). Prefer actionable detail."
|
|
11201
11394
|
});
|
|
11202
11395
|
}
|
|
11396
|
+
if (["decision", "gotcha", "convention", "architecture", "attempt"].includes(fm.type) && fm.status !== "rejected" && !actionableWords.test(naked)) {
|
|
11397
|
+
out.push({
|
|
11398
|
+
file: filePath,
|
|
11399
|
+
id: fm.id,
|
|
11400
|
+
severity: "info",
|
|
11401
|
+
code: "LOW_ACTIONABILITY",
|
|
11402
|
+
message: "Record does not contain obvious action/rationale words. Add the concrete rule, why it exists, and what to do instead."
|
|
11403
|
+
});
|
|
11404
|
+
}
|
|
11203
11405
|
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated") {
|
|
11204
11406
|
out.push({
|
|
11205
11407
|
file: filePath,
|
|
@@ -11237,9 +11439,64 @@ async function lintMemoriesAsync(root) {
|
|
|
11237
11439
|
message: "No Markdown heading (#/##/###) \u2014 add one so humans and auditors can skim the memo quickly."
|
|
11238
11440
|
});
|
|
11239
11441
|
}
|
|
11442
|
+
const u = getUsage20(usage, fm.id);
|
|
11443
|
+
if (fm.status === "validated" && u.read_count === 0) {
|
|
11444
|
+
out.push({
|
|
11445
|
+
file: filePath,
|
|
11446
|
+
id: fm.id,
|
|
11447
|
+
severity: "info",
|
|
11448
|
+
code: "NEVER_READ",
|
|
11449
|
+
message: "Validated record has never been surfaced/read. Consider improving tags/anchors or archiving it if it is not useful."
|
|
11450
|
+
});
|
|
11451
|
+
}
|
|
11452
|
+
}
|
|
11453
|
+
for (const dup of nearDuplicatePairs(loaded)) {
|
|
11454
|
+
out.push({
|
|
11455
|
+
file: dup.file,
|
|
11456
|
+
id: dup.id,
|
|
11457
|
+
severity: "warn",
|
|
11458
|
+
code: "NEAR_DUPLICATE",
|
|
11459
|
+
message: `Body overlaps ~${Math.round(dup.score * 100)}% with ${dup.otherId}. Merge or deprecate one record to reduce briefing noise.`
|
|
11460
|
+
});
|
|
11461
|
+
}
|
|
11462
|
+
return out;
|
|
11463
|
+
}
|
|
11464
|
+
function nearDuplicatePairs(loaded) {
|
|
11465
|
+
const out = [];
|
|
11466
|
+
const candidates = loaded.filter(({ memory: memory2 }) => {
|
|
11467
|
+
const fm = memory2.frontmatter;
|
|
11468
|
+
return fm.type !== "session_recap" && fm.status !== "rejected" && fm.status !== "deprecated";
|
|
11469
|
+
});
|
|
11470
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
11471
|
+
for (let j = i + 1; j < candidates.length; j++) {
|
|
11472
|
+
const a = candidates[i];
|
|
11473
|
+
const b = candidates[j];
|
|
11474
|
+
if (a.memory.frontmatter.scope !== b.memory.frontmatter.scope) continue;
|
|
11475
|
+
if (a.memory.frontmatter.type !== b.memory.frontmatter.type) continue;
|
|
11476
|
+
const score = jaccard2(tokenSet(a.memory.body), tokenSet(b.memory.body));
|
|
11477
|
+
if (score >= 0.72) {
|
|
11478
|
+
out.push({
|
|
11479
|
+
id: a.memory.frontmatter.id,
|
|
11480
|
+
otherId: b.memory.frontmatter.id,
|
|
11481
|
+
file: a.filePath,
|
|
11482
|
+
score
|
|
11483
|
+
});
|
|
11484
|
+
}
|
|
11485
|
+
}
|
|
11240
11486
|
}
|
|
11241
11487
|
return out;
|
|
11242
11488
|
}
|
|
11489
|
+
function tokenSet(body) {
|
|
11490
|
+
return new Set(
|
|
11491
|
+
(body.toLowerCase().match(/\b[a-z0-9]{4,}\b/g) ?? []).filter((word) => !["this", "that", "with", "from", "have"].includes(word))
|
|
11492
|
+
);
|
|
11493
|
+
}
|
|
11494
|
+
function jaccard2(a, b) {
|
|
11495
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
11496
|
+
let inter = 0;
|
|
11497
|
+
for (const item of a) if (b.has(item)) inter++;
|
|
11498
|
+
return inter / (a.size + b.size - inter);
|
|
11499
|
+
}
|
|
11243
11500
|
function registerMemoryLint(parent) {
|
|
11244
11501
|
parent.command("lint").description(
|
|
11245
11502
|
"Heuristic corpus checks (anchors on key types, headings, verbosity). Static analysis only."
|
|
@@ -11441,9 +11698,9 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
11441
11698
|
}
|
|
11442
11699
|
|
|
11443
11700
|
// src/commands/enforce.ts
|
|
11444
|
-
import { spawn as spawn5 } from "child_process";
|
|
11701
|
+
import { execFileSync as execFileSync2, spawn as spawn5 } from "child_process";
|
|
11445
11702
|
import { existsSync as existsSync68 } from "fs";
|
|
11446
|
-
import { chmod as chmod2, mkdir as mkdir19, readFile as
|
|
11703
|
+
import { chmod as chmod2, mkdir as mkdir19, readFile as readFile18, rm as rm3, writeFile as writeFile31 } from "fs/promises";
|
|
11447
11704
|
import path47 from "path";
|
|
11448
11705
|
import "commander";
|
|
11449
11706
|
import {
|
|
@@ -11749,6 +12006,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
11749
12006
|
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
11750
12007
|
};
|
|
11751
12008
|
}
|
|
12009
|
+
findings.push(...await inspectIntegrationVersions(root, "0.9.15"));
|
|
11752
12010
|
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
11753
12011
|
const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
|
|
11754
12012
|
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
|
|
@@ -11937,6 +12195,71 @@ async function findGeneratedArtifacts(paths) {
|
|
|
11937
12195
|
impact: 10
|
|
11938
12196
|
}];
|
|
11939
12197
|
}
|
|
12198
|
+
async function inspectIntegrationVersions(root, expectedVersion) {
|
|
12199
|
+
const files = [
|
|
12200
|
+
".git/hooks/pre-commit",
|
|
12201
|
+
".git/hooks/pre-push",
|
|
12202
|
+
".claude/settings.local.json",
|
|
12203
|
+
".mcp.json",
|
|
12204
|
+
".cursor/mcp.json",
|
|
12205
|
+
".vscode/mcp.json"
|
|
12206
|
+
];
|
|
12207
|
+
const findings = [];
|
|
12208
|
+
for (const rel of files) {
|
|
12209
|
+
const file = path47.join(root, rel);
|
|
12210
|
+
if (!existsSync68(file)) continue;
|
|
12211
|
+
const text = await readFile18(file, "utf8").catch(() => "");
|
|
12212
|
+
for (const bin of extractAbsoluteHaiveBins2(text)) {
|
|
12213
|
+
const version = versionForBinary2(bin);
|
|
12214
|
+
if (!version) {
|
|
12215
|
+
findings.push({
|
|
12216
|
+
severity: "warn",
|
|
12217
|
+
code: "integration-haive-binary-missing",
|
|
12218
|
+
message: `${rel} references ${bin}, but that binary could not be executed.`,
|
|
12219
|
+
fix: "Run `haive agent setup --no-global` or `haive enforce install` to refresh project integrations.",
|
|
12220
|
+
impact: 0
|
|
12221
|
+
});
|
|
12222
|
+
} else if (version !== expectedVersion) {
|
|
12223
|
+
findings.push({
|
|
12224
|
+
severity: "warn",
|
|
12225
|
+
code: "integration-haive-version-mismatch",
|
|
12226
|
+
message: `${rel} references hAIve ${version} at ${bin}; current hAIve is ${expectedVersion}.`,
|
|
12227
|
+
fix: "Run `haive agent setup --no-global` and `haive enforce install` to repair stale hooks/configs.",
|
|
12228
|
+
impact: 0
|
|
12229
|
+
});
|
|
12230
|
+
}
|
|
12231
|
+
}
|
|
12232
|
+
}
|
|
12233
|
+
if (findings.length === 0) {
|
|
12234
|
+
return [{
|
|
12235
|
+
severity: "ok",
|
|
12236
|
+
code: "integration-version-check",
|
|
12237
|
+
message: "No stale absolute hAIve binary paths were found in project hooks/MCP configs."
|
|
12238
|
+
}];
|
|
12239
|
+
}
|
|
12240
|
+
return findings;
|
|
12241
|
+
}
|
|
12242
|
+
function extractAbsoluteHaiveBins2(text) {
|
|
12243
|
+
const out = /* @__PURE__ */ new Set();
|
|
12244
|
+
const re = /(["'\s])((?:\/[^"'\s]+)*\/haive)\b/g;
|
|
12245
|
+
let match;
|
|
12246
|
+
while (match = re.exec(text)) {
|
|
12247
|
+
if (match[2]) out.add(match[2]);
|
|
12248
|
+
}
|
|
12249
|
+
return [...out].sort();
|
|
12250
|
+
}
|
|
12251
|
+
function versionForBinary2(bin) {
|
|
12252
|
+
try {
|
|
12253
|
+
const out = execFileSync2(bin, ["--version"], {
|
|
12254
|
+
encoding: "utf8",
|
|
12255
|
+
timeout: 3e3,
|
|
12256
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
12257
|
+
}).trim();
|
|
12258
|
+
return out.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
|
|
12259
|
+
} catch {
|
|
12260
|
+
return null;
|
|
12261
|
+
}
|
|
12262
|
+
}
|
|
11940
12263
|
async function getChangedFiles(root, stage) {
|
|
11941
12264
|
const commands = stage === "pre-commit" ? [["diff", "--cached", "--name-only"]] : [
|
|
11942
12265
|
["diff", "--cached", "--name-only"],
|
|
@@ -11996,7 +12319,7 @@ haive enforce check --stage pre-push --dir . || exit $?
|
|
|
11996
12319
|
for (const hook of hooks) {
|
|
11997
12320
|
const file = path47.join(hooksDir, hook.name);
|
|
11998
12321
|
if (existsSync68(file)) {
|
|
11999
|
-
const current = await
|
|
12322
|
+
const current = await readFile18(file, "utf8").catch(() => "");
|
|
12000
12323
|
if (current.includes(ENFORCE_HOOK_MARKER)) {
|
|
12001
12324
|
await writeFile31(file, hook.body, "utf8");
|
|
12002
12325
|
} else {
|
|
@@ -12141,7 +12464,7 @@ function registerRun(program2) {
|
|
|
12141
12464
|
|
|
12142
12465
|
// src/index.ts
|
|
12143
12466
|
var program = new Command51();
|
|
12144
|
-
program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.
|
|
12467
|
+
program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.15");
|
|
12145
12468
|
registerInit(program);
|
|
12146
12469
|
registerWelcome(program);
|
|
12147
12470
|
registerResolveProject(program);
|