@hiveai/cli 0.9.14 → 0.9.16
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/README.md +16 -1
- package/dist/index.js +465 -33
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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.16";
|
|
6277
6349
|
function jsonResult(data) {
|
|
6278
6350
|
return {
|
|
6279
6351
|
content: [
|
|
@@ -6284,15 +6356,58 @@ function jsonResult(data) {
|
|
|
6284
6356
|
]
|
|
6285
6357
|
};
|
|
6286
6358
|
}
|
|
6287
|
-
var ENFORCEMENT_PROFILE_TOOLS =
|
|
6359
|
+
var ENFORCEMENT_PROFILE_TOOLS = [
|
|
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
|
+
];
|
|
6371
|
+
var MAINTENANCE_PROFILE_TOOLS = [
|
|
6372
|
+
...ENFORCEMENT_PROFILE_TOOLS,
|
|
6373
|
+
"mem_suggest_topic",
|
|
6374
|
+
"mem_for_files",
|
|
6375
|
+
"mem_list",
|
|
6376
|
+
"get_project_context",
|
|
6377
|
+
"bootstrap_project_save",
|
|
6378
|
+
"mem_resolve_project",
|
|
6379
|
+
"mem_update",
|
|
6380
|
+
"mem_approve",
|
|
6381
|
+
"mem_reject",
|
|
6382
|
+
"mem_pending",
|
|
6383
|
+
"mem_delete",
|
|
6384
|
+
"mem_diff",
|
|
6385
|
+
"get_recap",
|
|
6386
|
+
"code_search",
|
|
6387
|
+
"anti_patterns_check",
|
|
6388
|
+
"mem_distill",
|
|
6389
|
+
"mem_timeline",
|
|
6390
|
+
"mem_conflict_candidates"
|
|
6391
|
+
];
|
|
6392
|
+
var EXPERIMENTAL_PROFILE_TOOLS = [
|
|
6393
|
+
...MAINTENANCE_PROFILE_TOOLS,
|
|
6394
|
+
"mem_observe",
|
|
6395
|
+
"why_this_file",
|
|
6396
|
+
"why_this_decision",
|
|
6397
|
+
"mem_conflicts_with",
|
|
6398
|
+
"pattern_detect",
|
|
6399
|
+
"runtime_journal_append",
|
|
6400
|
+
"runtime_journal_tail"
|
|
6401
|
+
];
|
|
6402
|
+
var TOOL_PROFILES = {
|
|
6403
|
+
enforcement: new Set(ENFORCEMENT_PROFILE_TOOLS),
|
|
6404
|
+
maintenance: new Set(MAINTENANCE_PROFILE_TOOLS),
|
|
6405
|
+
experimental: new Set(EXPERIMENTAL_PROFILE_TOOLS)
|
|
6406
|
+
};
|
|
6407
|
+
function getAllowedToolsForProfile(profile) {
|
|
6408
|
+
if (profile === "full") return TOOL_PROFILES.experimental;
|
|
6409
|
+
return TOOL_PROFILES[profile] ?? TOOL_PROFILES.enforcement;
|
|
6410
|
+
}
|
|
6296
6411
|
var BRIEFING_TOOLS = /* @__PURE__ */ new Set(["get_briefing", "mem_relevant_to"]);
|
|
6297
6412
|
var MUTATING_TOOLS = /* @__PURE__ */ new Set([
|
|
6298
6413
|
"mem_save",
|
|
@@ -6319,7 +6434,8 @@ function createHaiveServer(options = {}) {
|
|
|
6319
6434
|
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
6320
6435
|
{ capabilities: { tools: {}, prompts: {} } }
|
|
6321
6436
|
);
|
|
6322
|
-
const
|
|
6437
|
+
const allowedTools = getAllowedToolsForProfile(toolProfile);
|
|
6438
|
+
const shouldRegisterTool = (name) => allowedTools.has(name);
|
|
6323
6439
|
const registerTool = (name, description, schema, handler) => {
|
|
6324
6440
|
if (!shouldRegisterTool(name)) return;
|
|
6325
6441
|
const tool = server.tool.bind(server);
|
|
@@ -6343,7 +6459,11 @@ function createHaiveServer(options = {}) {
|
|
|
6343
6459
|
}
|
|
6344
6460
|
);
|
|
6345
6461
|
};
|
|
6346
|
-
const shouldRegisterPrompt = (name) =>
|
|
6462
|
+
const shouldRegisterPrompt = (name) => {
|
|
6463
|
+
if (name === "bootstrap_project" || name === "post_task") return true;
|
|
6464
|
+
if (name === "import_docs") return toolProfile !== "enforcement";
|
|
6465
|
+
return toolProfile === "experimental" || toolProfile === "full";
|
|
6466
|
+
};
|
|
6347
6467
|
registerTool(
|
|
6348
6468
|
"mem_save",
|
|
6349
6469
|
[
|
|
@@ -10641,9 +10761,9 @@ function parseDays(input) {
|
|
|
10641
10761
|
|
|
10642
10762
|
// src/commands/doctor.ts
|
|
10643
10763
|
import { existsSync as existsSync60 } from "fs";
|
|
10644
|
-
import { stat } from "fs/promises";
|
|
10764
|
+
import { readFile as readFile17, stat } from "fs/promises";
|
|
10645
10765
|
import path41 from "path";
|
|
10646
|
-
import { execSync as execSync3 } from "child_process";
|
|
10766
|
+
import { execFileSync, execSync as execSync3 } from "child_process";
|
|
10647
10767
|
import "commander";
|
|
10648
10768
|
import {
|
|
10649
10769
|
codeMapPath as codeMapPath2,
|
|
@@ -10681,8 +10801,8 @@ function registerDoctor(program2) {
|
|
|
10681
10801
|
fix: "haive init"
|
|
10682
10802
|
});
|
|
10683
10803
|
} else {
|
|
10684
|
-
const { readFile:
|
|
10685
|
-
const content = await
|
|
10804
|
+
const { readFile: readFile19 } = await import("fs/promises");
|
|
10805
|
+
const content = await readFile19(paths.projectContext, "utf8");
|
|
10686
10806
|
const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
|
|
10687
10807
|
if (isTemplate) {
|
|
10688
10808
|
findings.push({
|
|
@@ -10808,8 +10928,8 @@ function registerDoctor(program2) {
|
|
|
10808
10928
|
let hasClaudeEnforcement = false;
|
|
10809
10929
|
if (existsSync60(claudeSettings)) {
|
|
10810
10930
|
try {
|
|
10811
|
-
const { readFile:
|
|
10812
|
-
const raw = await
|
|
10931
|
+
const { readFile: readFile19 } = await import("fs/promises");
|
|
10932
|
+
const raw = await readFile19(claudeSettings, "utf8");
|
|
10813
10933
|
hasClaudeEnforcement = raw.includes("haive enforce session-start") && raw.includes("haive enforce pre-tool-use");
|
|
10814
10934
|
} catch {
|
|
10815
10935
|
hasClaudeEnforcement = false;
|
|
@@ -10832,13 +10952,14 @@ function registerDoctor(program2) {
|
|
|
10832
10952
|
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
|
|
10833
10953
|
});
|
|
10834
10954
|
}
|
|
10955
|
+
findings.push(...await collectInstallFindings(root, "0.9.16"));
|
|
10835
10956
|
try {
|
|
10836
10957
|
const legacyRaw = execSync3("haive-mcp --version", {
|
|
10837
10958
|
encoding: "utf8",
|
|
10838
10959
|
timeout: 3e3,
|
|
10839
10960
|
stdio: ["ignore", "pipe", "ignore"]
|
|
10840
10961
|
}).trim();
|
|
10841
|
-
const cliVersion = "0.9.
|
|
10962
|
+
const cliVersion = "0.9.16";
|
|
10842
10963
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
10843
10964
|
findings.push({
|
|
10844
10965
|
severity: "warn",
|
|
@@ -10885,6 +11006,103 @@ function emit(findings, opts) {
|
|
|
10885
11006
|
function isSearchTool(name) {
|
|
10886
11007
|
return ["mem_search", "code_search", "mem_relevant_to", "get_briefing"].includes(name);
|
|
10887
11008
|
}
|
|
11009
|
+
async function collectInstallFindings(root, expectedVersion) {
|
|
11010
|
+
const findings = [];
|
|
11011
|
+
const haiveBins = listHaiveBins();
|
|
11012
|
+
if (haiveBins.length === 0) {
|
|
11013
|
+
findings.push({
|
|
11014
|
+
severity: "warn",
|
|
11015
|
+
code: "haive-not-on-path",
|
|
11016
|
+
message: "No `haive` binary was found on PATH. Hooks and MCP configs that call `haive` may fail.",
|
|
11017
|
+
fix: `npm install -g @hiveai/cli@${expectedVersion}`
|
|
11018
|
+
});
|
|
11019
|
+
} else {
|
|
11020
|
+
const first = haiveBins[0];
|
|
11021
|
+
const firstVersion = versionForBinary(first);
|
|
11022
|
+
if (firstVersion && firstVersion !== expectedVersion) {
|
|
11023
|
+
findings.push({
|
|
11024
|
+
severity: "warn",
|
|
11025
|
+
code: "path-haive-version-mismatch",
|
|
11026
|
+
message: `PATH resolves haive to ${first} (${firstVersion}), but this build expects ${expectedVersion}.`,
|
|
11027
|
+
fix: `npm install -g @hiveai/cli@${expectedVersion}
|
|
11028
|
+
which -a haive`
|
|
11029
|
+
});
|
|
11030
|
+
}
|
|
11031
|
+
const skewed = haiveBins.map((bin) => ({ bin, version: versionForBinary(bin) })).filter((item) => item.version && item.version !== expectedVersion);
|
|
11032
|
+
if (skewed.length > 0) {
|
|
11033
|
+
findings.push({
|
|
11034
|
+
severity: "info",
|
|
11035
|
+
code: "multiple-haive-binaries",
|
|
11036
|
+
message: `Found ${haiveBins.length} haive binar${haiveBins.length === 1 ? "y" : "ies"} on PATH; ${skewed.length} do not match ${expectedVersion}.`,
|
|
11037
|
+
fix: "Remove stale global installs or ensure hooks call the intended `haive` binary."
|
|
11038
|
+
});
|
|
11039
|
+
}
|
|
11040
|
+
}
|
|
11041
|
+
const integrationFiles = [
|
|
11042
|
+
".git/hooks/pre-commit",
|
|
11043
|
+
".git/hooks/pre-push",
|
|
11044
|
+
".claude/settings.local.json",
|
|
11045
|
+
".mcp.json",
|
|
11046
|
+
".cursor/mcp.json",
|
|
11047
|
+
".vscode/mcp.json"
|
|
11048
|
+
];
|
|
11049
|
+
for (const rel of integrationFiles) {
|
|
11050
|
+
const file = path41.join(root, rel);
|
|
11051
|
+
if (!existsSync60(file)) continue;
|
|
11052
|
+
const text = await readFile17(file, "utf8").catch(() => "");
|
|
11053
|
+
for (const bin of extractAbsoluteHaiveBins(text)) {
|
|
11054
|
+
const version = versionForBinary(bin);
|
|
11055
|
+
if (!version) {
|
|
11056
|
+
findings.push({
|
|
11057
|
+
severity: "warn",
|
|
11058
|
+
code: "integration-haive-binary-missing",
|
|
11059
|
+
message: `${rel} references ${bin}, but it could not be executed.`,
|
|
11060
|
+
fix: "Run `haive agent setup --no-global` or `haive enforce install` to rewrite project integrations."
|
|
11061
|
+
});
|
|
11062
|
+
} else if (version !== expectedVersion) {
|
|
11063
|
+
findings.push({
|
|
11064
|
+
severity: "warn",
|
|
11065
|
+
code: "integration-haive-version-mismatch",
|
|
11066
|
+
message: `${rel} references ${bin} (${version}), but current hAIve is ${expectedVersion}.`,
|
|
11067
|
+
fix: "Run `haive agent setup --no-global` and `haive enforce install` to refresh stale paths."
|
|
11068
|
+
});
|
|
11069
|
+
}
|
|
11070
|
+
}
|
|
11071
|
+
}
|
|
11072
|
+
return findings;
|
|
11073
|
+
}
|
|
11074
|
+
function listHaiveBins() {
|
|
11075
|
+
try {
|
|
11076
|
+
return execSync3("which -a haive", {
|
|
11077
|
+
encoding: "utf8",
|
|
11078
|
+
timeout: 3e3,
|
|
11079
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
11080
|
+
}).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
11081
|
+
} catch {
|
|
11082
|
+
return [];
|
|
11083
|
+
}
|
|
11084
|
+
}
|
|
11085
|
+
function versionForBinary(bin) {
|
|
11086
|
+
try {
|
|
11087
|
+
const out = execFileSync(bin, ["--version"], {
|
|
11088
|
+
encoding: "utf8",
|
|
11089
|
+
timeout: 3e3,
|
|
11090
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
11091
|
+
}).trim();
|
|
11092
|
+
return out.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
|
|
11093
|
+
} catch {
|
|
11094
|
+
return null;
|
|
11095
|
+
}
|
|
11096
|
+
}
|
|
11097
|
+
function extractAbsoluteHaiveBins(text) {
|
|
11098
|
+
const out = /* @__PURE__ */ new Set();
|
|
11099
|
+
const re = /(["'\s])((?:\/[^"'\s]+)*\/haive)\b/g;
|
|
11100
|
+
let match;
|
|
11101
|
+
while (match = re.exec(text)) {
|
|
11102
|
+
if (match[2]) out.add(match[2]);
|
|
11103
|
+
}
|
|
11104
|
+
return [...out].sort();
|
|
11105
|
+
}
|
|
10888
11106
|
|
|
10889
11107
|
// src/commands/playback.ts
|
|
10890
11108
|
import { existsSync as existsSync61 } from "fs";
|
|
@@ -11050,19 +11268,21 @@ function registerPrecommit(program2) {
|
|
|
11050
11268
|
console.log(ui.bold(`hAIve precommit \u2014 ${touchedPaths.length} file(s)`));
|
|
11051
11269
|
console.log(
|
|
11052
11270
|
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}`
|
|
11271
|
+
` 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
11272
|
)
|
|
11055
11273
|
);
|
|
11056
11274
|
console.log();
|
|
11057
|
-
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
|
|
11275
|
+
const blocking = result.warnings.filter((w) => w.level === "blocking");
|
|
11276
|
+
const review = result.warnings.filter((w) => w.level === "review");
|
|
11277
|
+
const info = result.warnings.filter((w) => w.level === "info");
|
|
11278
|
+
printWarnings("Blocking anti-patterns", blocking, "error");
|
|
11279
|
+
printWarnings("Review anti-patterns", review.slice(0, 8), "warn");
|
|
11280
|
+
if (info.length > 0) {
|
|
11281
|
+
console.log(
|
|
11282
|
+
ui.dim(
|
|
11283
|
+
`${info.length} weak anti-pattern signal${info.length === 1 ? "" : "s"} hidden. Use --json to inspect FYI matches.`
|
|
11284
|
+
)
|
|
11285
|
+
);
|
|
11066
11286
|
console.log();
|
|
11067
11287
|
}
|
|
11068
11288
|
if (result.relevant_memories.length > 0) {
|
|
@@ -11091,6 +11311,20 @@ function registerPrecommit(program2) {
|
|
|
11091
11311
|
}
|
|
11092
11312
|
});
|
|
11093
11313
|
}
|
|
11314
|
+
function printWarnings(title, warnings, tone) {
|
|
11315
|
+
if (warnings.length === 0) return;
|
|
11316
|
+
console.log(ui.bold(tone === "error" ? `\u2717 ${title}:` : `\u26A0 ${title}:`));
|
|
11317
|
+
for (const w of warnings) {
|
|
11318
|
+
const marker = tone === "error" ? ui.red("\u2717") : ui.yellow("\u26A0");
|
|
11319
|
+
console.log(` ${marker} ${w.id} ${ui.dim(`(${w.type}, ${w.confidence})`)}`);
|
|
11320
|
+
for (const line of w.body_preview.split("\n").slice(0, 3)) {
|
|
11321
|
+
console.log(` ${ui.dim(line)}`);
|
|
11322
|
+
}
|
|
11323
|
+
console.log(` ${ui.dim("reasons:")} ${w.reasons.join(", ")}`);
|
|
11324
|
+
if (w.rationale) console.log(` ${ui.dim("why shown:")} ${w.rationale}`);
|
|
11325
|
+
}
|
|
11326
|
+
console.log();
|
|
11327
|
+
}
|
|
11094
11328
|
function runCommand3(cmd, args, cwd) {
|
|
11095
11329
|
return new Promise((resolve, reject) => {
|
|
11096
11330
|
const proc = spawn4(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -11177,7 +11411,9 @@ import { existsSync as existsSync64 } from "fs";
|
|
|
11177
11411
|
import "commander";
|
|
11178
11412
|
import {
|
|
11179
11413
|
findProjectRoot as findProjectRoot44,
|
|
11414
|
+
getUsage as getUsage20,
|
|
11180
11415
|
loadMemoriesFromDir as loadMemoriesFromDir35,
|
|
11416
|
+
loadUsageIndex as loadUsageIndex26,
|
|
11181
11417
|
resolveHaivePaths as resolveHaivePaths40
|
|
11182
11418
|
} from "@hiveai/core";
|
|
11183
11419
|
async function lintMemoriesAsync(root) {
|
|
@@ -11185,7 +11421,9 @@ async function lintMemoriesAsync(root) {
|
|
|
11185
11421
|
const out = [];
|
|
11186
11422
|
if (!existsSync64(paths.memoriesDir)) return out;
|
|
11187
11423
|
const loaded = await loadMemoriesFromDir35(paths.memoriesDir);
|
|
11424
|
+
const usage = await loadUsageIndex26(paths);
|
|
11188
11425
|
const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
|
|
11426
|
+
const actionableWords = /\b(always|never|prefer|use|avoid|because|instead|why|rationale|do not|must|should)\b/i;
|
|
11189
11427
|
for (const { filePath, memory: memory2 } of loaded) {
|
|
11190
11428
|
const fm = memory2.frontmatter;
|
|
11191
11429
|
if (fm.type === "session_recap") continue;
|
|
@@ -11200,6 +11438,15 @@ async function lintMemoriesAsync(root) {
|
|
|
11200
11438
|
message: "Body looks very short (< ~40 chars of prose after headings). Prefer actionable detail."
|
|
11201
11439
|
});
|
|
11202
11440
|
}
|
|
11441
|
+
if (["decision", "gotcha", "convention", "architecture", "attempt"].includes(fm.type) && fm.status !== "rejected" && !actionableWords.test(naked)) {
|
|
11442
|
+
out.push({
|
|
11443
|
+
file: filePath,
|
|
11444
|
+
id: fm.id,
|
|
11445
|
+
severity: "info",
|
|
11446
|
+
code: "LOW_ACTIONABILITY",
|
|
11447
|
+
message: "Record does not contain obvious action/rationale words. Add the concrete rule, why it exists, and what to do instead."
|
|
11448
|
+
});
|
|
11449
|
+
}
|
|
11203
11450
|
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated") {
|
|
11204
11451
|
out.push({
|
|
11205
11452
|
file: filePath,
|
|
@@ -11237,9 +11484,64 @@ async function lintMemoriesAsync(root) {
|
|
|
11237
11484
|
message: "No Markdown heading (#/##/###) \u2014 add one so humans and auditors can skim the memo quickly."
|
|
11238
11485
|
});
|
|
11239
11486
|
}
|
|
11487
|
+
const u = getUsage20(usage, fm.id);
|
|
11488
|
+
if (fm.status === "validated" && u.read_count === 0) {
|
|
11489
|
+
out.push({
|
|
11490
|
+
file: filePath,
|
|
11491
|
+
id: fm.id,
|
|
11492
|
+
severity: "info",
|
|
11493
|
+
code: "NEVER_READ",
|
|
11494
|
+
message: "Validated record has never been surfaced/read. Consider improving tags/anchors or archiving it if it is not useful."
|
|
11495
|
+
});
|
|
11496
|
+
}
|
|
11497
|
+
}
|
|
11498
|
+
for (const dup of nearDuplicatePairs(loaded)) {
|
|
11499
|
+
out.push({
|
|
11500
|
+
file: dup.file,
|
|
11501
|
+
id: dup.id,
|
|
11502
|
+
severity: "warn",
|
|
11503
|
+
code: "NEAR_DUPLICATE",
|
|
11504
|
+
message: `Body overlaps ~${Math.round(dup.score * 100)}% with ${dup.otherId}. Merge or deprecate one record to reduce briefing noise.`
|
|
11505
|
+
});
|
|
11506
|
+
}
|
|
11507
|
+
return out;
|
|
11508
|
+
}
|
|
11509
|
+
function nearDuplicatePairs(loaded) {
|
|
11510
|
+
const out = [];
|
|
11511
|
+
const candidates = loaded.filter(({ memory: memory2 }) => {
|
|
11512
|
+
const fm = memory2.frontmatter;
|
|
11513
|
+
return fm.type !== "session_recap" && fm.status !== "rejected" && fm.status !== "deprecated";
|
|
11514
|
+
});
|
|
11515
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
11516
|
+
for (let j = i + 1; j < candidates.length; j++) {
|
|
11517
|
+
const a = candidates[i];
|
|
11518
|
+
const b = candidates[j];
|
|
11519
|
+
if (a.memory.frontmatter.scope !== b.memory.frontmatter.scope) continue;
|
|
11520
|
+
if (a.memory.frontmatter.type !== b.memory.frontmatter.type) continue;
|
|
11521
|
+
const score = jaccard2(tokenSet(a.memory.body), tokenSet(b.memory.body));
|
|
11522
|
+
if (score >= 0.72) {
|
|
11523
|
+
out.push({
|
|
11524
|
+
id: a.memory.frontmatter.id,
|
|
11525
|
+
otherId: b.memory.frontmatter.id,
|
|
11526
|
+
file: a.filePath,
|
|
11527
|
+
score
|
|
11528
|
+
});
|
|
11529
|
+
}
|
|
11530
|
+
}
|
|
11240
11531
|
}
|
|
11241
11532
|
return out;
|
|
11242
11533
|
}
|
|
11534
|
+
function tokenSet(body) {
|
|
11535
|
+
return new Set(
|
|
11536
|
+
(body.toLowerCase().match(/\b[a-z0-9]{4,}\b/g) ?? []).filter((word) => !["this", "that", "with", "from", "have"].includes(word))
|
|
11537
|
+
);
|
|
11538
|
+
}
|
|
11539
|
+
function jaccard2(a, b) {
|
|
11540
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
11541
|
+
let inter = 0;
|
|
11542
|
+
for (const item of a) if (b.has(item)) inter++;
|
|
11543
|
+
return inter / (a.size + b.size - inter);
|
|
11544
|
+
}
|
|
11243
11545
|
function registerMemoryLint(parent) {
|
|
11244
11546
|
parent.command("lint").description(
|
|
11245
11547
|
"Heuristic corpus checks (anchors on key types, headings, verbosity). Static analysis only."
|
|
@@ -11441,9 +11743,9 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
11441
11743
|
}
|
|
11442
11744
|
|
|
11443
11745
|
// src/commands/enforce.ts
|
|
11444
|
-
import { spawn as spawn5 } from "child_process";
|
|
11746
|
+
import { execFileSync as execFileSync2, spawn as spawn5 } from "child_process";
|
|
11445
11747
|
import { existsSync as existsSync68 } from "fs";
|
|
11446
|
-
import { chmod as chmod2, mkdir as mkdir19, readFile as
|
|
11748
|
+
import { chmod as chmod2, mkdir as mkdir19, readFile as readFile18, rm as rm3, writeFile as writeFile31 } from "fs/promises";
|
|
11447
11749
|
import path47 from "path";
|
|
11448
11750
|
import "commander";
|
|
11449
11751
|
import {
|
|
@@ -11749,6 +12051,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
11749
12051
|
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
11750
12052
|
};
|
|
11751
12053
|
}
|
|
12054
|
+
findings.push(...await inspectIntegrationVersions(root, "0.9.16"));
|
|
11752
12055
|
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
11753
12056
|
const hasBriefing = await hasRecentBriefingMarker(paths, sessionId);
|
|
11754
12057
|
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
|
|
@@ -11937,6 +12240,71 @@ async function findGeneratedArtifacts(paths) {
|
|
|
11937
12240
|
impact: 10
|
|
11938
12241
|
}];
|
|
11939
12242
|
}
|
|
12243
|
+
async function inspectIntegrationVersions(root, expectedVersion) {
|
|
12244
|
+
const files = [
|
|
12245
|
+
".git/hooks/pre-commit",
|
|
12246
|
+
".git/hooks/pre-push",
|
|
12247
|
+
".claude/settings.local.json",
|
|
12248
|
+
".mcp.json",
|
|
12249
|
+
".cursor/mcp.json",
|
|
12250
|
+
".vscode/mcp.json"
|
|
12251
|
+
];
|
|
12252
|
+
const findings = [];
|
|
12253
|
+
for (const rel of files) {
|
|
12254
|
+
const file = path47.join(root, rel);
|
|
12255
|
+
if (!existsSync68(file)) continue;
|
|
12256
|
+
const text = await readFile18(file, "utf8").catch(() => "");
|
|
12257
|
+
for (const bin of extractAbsoluteHaiveBins2(text)) {
|
|
12258
|
+
const version = versionForBinary2(bin);
|
|
12259
|
+
if (!version) {
|
|
12260
|
+
findings.push({
|
|
12261
|
+
severity: "warn",
|
|
12262
|
+
code: "integration-haive-binary-missing",
|
|
12263
|
+
message: `${rel} references ${bin}, but that binary could not be executed.`,
|
|
12264
|
+
fix: "Run `haive agent setup --no-global` or `haive enforce install` to refresh project integrations.",
|
|
12265
|
+
impact: 0
|
|
12266
|
+
});
|
|
12267
|
+
} else if (version !== expectedVersion) {
|
|
12268
|
+
findings.push({
|
|
12269
|
+
severity: "warn",
|
|
12270
|
+
code: "integration-haive-version-mismatch",
|
|
12271
|
+
message: `${rel} references hAIve ${version} at ${bin}; current hAIve is ${expectedVersion}.`,
|
|
12272
|
+
fix: "Run `haive agent setup --no-global` and `haive enforce install` to repair stale hooks/configs.",
|
|
12273
|
+
impact: 0
|
|
12274
|
+
});
|
|
12275
|
+
}
|
|
12276
|
+
}
|
|
12277
|
+
}
|
|
12278
|
+
if (findings.length === 0) {
|
|
12279
|
+
return [{
|
|
12280
|
+
severity: "ok",
|
|
12281
|
+
code: "integration-version-check",
|
|
12282
|
+
message: "No stale absolute hAIve binary paths were found in project hooks/MCP configs."
|
|
12283
|
+
}];
|
|
12284
|
+
}
|
|
12285
|
+
return findings;
|
|
12286
|
+
}
|
|
12287
|
+
function extractAbsoluteHaiveBins2(text) {
|
|
12288
|
+
const out = /* @__PURE__ */ new Set();
|
|
12289
|
+
const re = /(["'\s])((?:\/[^"'\s]+)*\/haive)\b/g;
|
|
12290
|
+
let match;
|
|
12291
|
+
while (match = re.exec(text)) {
|
|
12292
|
+
if (match[2]) out.add(match[2]);
|
|
12293
|
+
}
|
|
12294
|
+
return [...out].sort();
|
|
12295
|
+
}
|
|
12296
|
+
function versionForBinary2(bin) {
|
|
12297
|
+
try {
|
|
12298
|
+
const out = execFileSync2(bin, ["--version"], {
|
|
12299
|
+
encoding: "utf8",
|
|
12300
|
+
timeout: 3e3,
|
|
12301
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
12302
|
+
}).trim();
|
|
12303
|
+
return out.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
|
|
12304
|
+
} catch {
|
|
12305
|
+
return null;
|
|
12306
|
+
}
|
|
12307
|
+
}
|
|
11940
12308
|
async function getChangedFiles(root, stage) {
|
|
11941
12309
|
const commands = stage === "pre-commit" ? [["diff", "--cached", "--name-only"]] : [
|
|
11942
12310
|
["diff", "--cached", "--name-only"],
|
|
@@ -11996,7 +12364,7 @@ haive enforce check --stage pre-push --dir . || exit $?
|
|
|
11996
12364
|
for (const hook of hooks) {
|
|
11997
12365
|
const file = path47.join(hooksDir, hook.name);
|
|
11998
12366
|
if (existsSync68(file)) {
|
|
11999
|
-
const current = await
|
|
12367
|
+
const current = await readFile18(file, "utf8").catch(() => "");
|
|
12000
12368
|
if (current.includes(ENFORCE_HOOK_MARKER)) {
|
|
12001
12369
|
await writeFile31(file, hook.body, "utf8");
|
|
12002
12370
|
} else {
|
|
@@ -12141,7 +12509,7 @@ function registerRun(program2) {
|
|
|
12141
12509
|
|
|
12142
12510
|
// src/index.ts
|
|
12143
12511
|
var program = new Command51();
|
|
12144
|
-
program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.
|
|
12512
|
+
program.name("haive").description("hAIve \u2014 policy enforcement layer for AI coding agents").version("0.9.16").option("--advanced", "show maintenance and experimental commands in help");
|
|
12145
12513
|
registerInit(program);
|
|
12146
12514
|
registerWelcome(program);
|
|
12147
12515
|
registerResolveProject(program);
|
|
@@ -12194,6 +12562,32 @@ registerBenchmark(program);
|
|
|
12194
12562
|
registerDoctor(program);
|
|
12195
12563
|
registerPlayback(program);
|
|
12196
12564
|
registerPrecommit(program);
|
|
12565
|
+
var CORE_ROOT_COMMANDS = /* @__PURE__ */ new Set([
|
|
12566
|
+
"init",
|
|
12567
|
+
"doctor",
|
|
12568
|
+
"agent",
|
|
12569
|
+
"enforce",
|
|
12570
|
+
"run",
|
|
12571
|
+
"briefing",
|
|
12572
|
+
"sync",
|
|
12573
|
+
"mcp",
|
|
12574
|
+
"memory",
|
|
12575
|
+
"session",
|
|
12576
|
+
"precommit",
|
|
12577
|
+
"welcome"
|
|
12578
|
+
]);
|
|
12579
|
+
var CORE_MEMORY_COMMANDS = /* @__PURE__ */ new Set([
|
|
12580
|
+
"add",
|
|
12581
|
+
"list",
|
|
12582
|
+
"query",
|
|
12583
|
+
"show",
|
|
12584
|
+
"verify",
|
|
12585
|
+
"lint",
|
|
12586
|
+
"tried",
|
|
12587
|
+
"rm"
|
|
12588
|
+
]);
|
|
12589
|
+
var CORE_SESSION_COMMANDS = /* @__PURE__ */ new Set(["end"]);
|
|
12590
|
+
applySurfaceVisibility(program);
|
|
12197
12591
|
program.parseAsync(process.argv).catch((err) => {
|
|
12198
12592
|
if (isZodError(err)) {
|
|
12199
12593
|
for (const issue of err.issues) {
|
|
@@ -12205,6 +12599,44 @@ program.parseAsync(process.argv).catch((err) => {
|
|
|
12205
12599
|
}
|
|
12206
12600
|
process.exit(1);
|
|
12207
12601
|
});
|
|
12602
|
+
function applySurfaceVisibility(root) {
|
|
12603
|
+
const showAdvanced = process.argv.includes("--advanced") || process.env.HAIVE_SHOW_ADVANCED === "1";
|
|
12604
|
+
if (!showAdvanced) hideNonCoreCommands(root);
|
|
12605
|
+
root.addHelpText(
|
|
12606
|
+
"after",
|
|
12607
|
+
[
|
|
12608
|
+
"",
|
|
12609
|
+
"Default help shows the core hAIve harness: init, doctor, agent setup, briefing, enforcement,",
|
|
12610
|
+
"sync, session recaps, and high-signal memory commands.",
|
|
12611
|
+
"Run `haive --advanced --help` or set HAIVE_SHOW_ADVANCED=1 to show maintenance and experimental commands."
|
|
12612
|
+
].join("\n")
|
|
12613
|
+
);
|
|
12614
|
+
const memoryCommand = root.commands.find((cmd) => cmd.name() === "memory");
|
|
12615
|
+
memoryCommand?.addHelpText(
|
|
12616
|
+
"after",
|
|
12617
|
+
[
|
|
12618
|
+
"",
|
|
12619
|
+
"Default help shows the memory commands that support the core harness workflow.",
|
|
12620
|
+
"Run `haive --advanced memory --help` or set HAIVE_SHOW_ADVANCED=1 to show review, import, digest, timeline, and conflict tools."
|
|
12621
|
+
].join("\n")
|
|
12622
|
+
);
|
|
12623
|
+
}
|
|
12624
|
+
function hideNonCoreCommands(command) {
|
|
12625
|
+
for (const child of command.commands) {
|
|
12626
|
+
if (!isCoreCommand(command, child)) {
|
|
12627
|
+
child._hidden = true;
|
|
12628
|
+
}
|
|
12629
|
+
hideNonCoreCommands(child);
|
|
12630
|
+
}
|
|
12631
|
+
}
|
|
12632
|
+
function isCoreCommand(parent, child) {
|
|
12633
|
+
const parentName = parent.name();
|
|
12634
|
+
const childName = child.name();
|
|
12635
|
+
if (parentName === "haive") return CORE_ROOT_COMMANDS.has(childName);
|
|
12636
|
+
if (parentName === "memory") return CORE_MEMORY_COMMANDS.has(childName);
|
|
12637
|
+
if (parentName === "session") return CORE_SESSION_COMMANDS.has(childName);
|
|
12638
|
+
return true;
|
|
12639
|
+
}
|
|
12208
12640
|
function isZodError(err) {
|
|
12209
12641
|
return err !== null && typeof err === "object" && "issues" in err && Array.isArray(err.issues);
|
|
12210
12642
|
}
|