@hiveai/cli 0.10.5 → 0.10.9
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 +2 -0
- package/dist/index.js +833 -386
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -2743,6 +2743,7 @@ ${SEED_FOOTER(stack)}` });
|
|
|
2743
2743
|
}
|
|
2744
2744
|
|
|
2745
2745
|
// src/commands/init.ts
|
|
2746
|
+
var HAIVE_GITHUB_ACTION_REF = `v${"0.10.9"}`;
|
|
2746
2747
|
var PROJECT_CONTEXT_TEMPLATE = `# Project context
|
|
2747
2748
|
|
|
2748
2749
|
> Generated by \`haive init\`. Run \`haive init --bootstrap\` to auto-fill from your codebase,
|
|
@@ -2777,6 +2778,7 @@ This repo uses **hAIve** for shared context. The map:
|
|
|
2777
2778
|
1. **Before editing** for a goal, call \`get_briefing\` (task + files/symbols) to load ranked context \u2014 or \`mem_relevant_to\` if project context is already loaded this session.
|
|
2778
2779
|
2. **When an approach fails**, call \`mem_tried\` right away so the next agent skips the dead end.
|
|
2779
2780
|
3. **Before closing** a substantive session, run the \`post_task\` prompt to capture what was learned.
|
|
2781
|
+
4. **Before final response**, run \`haive enforce finish\`. If it blocks, commit/push, bump/tag shippable releases, then rerun it.
|
|
2780
2782
|
|
|
2781
2783
|
If the haive MCP server is not available, tell the developer rather than silently skipping it.
|
|
2782
2784
|
|
|
@@ -2803,6 +2805,7 @@ This repository uses **hAIve**. Running \`haive init\` means the team expects ag
|
|
|
2803
2805
|
|
|
2804
2806
|
- On failure: **\`mem_tried\`** immediately.
|
|
2805
2807
|
- Before closing a substantive session: MCP prompt **\`post_task\`** when there is something worth capturing.
|
|
2808
|
+
- Before final response: **\`haive enforce finish\`** must pass; it checks commit/push and release version/tag protocol.
|
|
2806
2809
|
|
|
2807
2810
|
## If haive MCP is missing
|
|
2808
2811
|
|
|
@@ -2898,7 +2901,7 @@ jobs:
|
|
|
2898
2901
|
steps:
|
|
2899
2902
|
- uses: actions/checkout@v4
|
|
2900
2903
|
|
|
2901
|
-
- uses: Doucs91/hAIve/packages/github-action
|
|
2904
|
+
- uses: Doucs91/hAIve/packages/github-action@${HAIVE_GITHUB_ACTION_REF}
|
|
2902
2905
|
with:
|
|
2903
2906
|
github-token: \${{ secrets.GITHUB_TOKEN }}
|
|
2904
2907
|
# post-if-empty: 'true' # uncomment to always post (even when no memories found)
|
|
@@ -3746,9 +3749,8 @@ import { mkdir as mkdir52, writeFile as writeFile92, rm } from "fs/promises";
|
|
|
3746
3749
|
import { existsSync as existsSync16 } from "fs";
|
|
3747
3750
|
import path72 from "path";
|
|
3748
3751
|
import { execSync } from "child_process";
|
|
3749
|
-
import { readFile as
|
|
3750
|
-
import { existsSync as
|
|
3751
|
-
import path92 from "path";
|
|
3752
|
+
import { readFile as readFile42, writeFile as writeFile11 } from "fs/promises";
|
|
3753
|
+
import { existsSync as existsSync19 } from "fs";
|
|
3752
3754
|
import {
|
|
3753
3755
|
allocateBudget,
|
|
3754
3756
|
DEFAULT_AUTO_PROMOTE_RULE,
|
|
@@ -3757,11 +3759,9 @@ import {
|
|
|
3757
3759
|
extractActionsBriefBody as extractActionsBriefBody2,
|
|
3758
3760
|
getUsage as getUsage5,
|
|
3759
3761
|
inferModulesFromPaths as inferModulesFromPaths2,
|
|
3760
|
-
isGlobPath,
|
|
3761
|
-
isRetiredMemory,
|
|
3762
3762
|
isAutoPromoteEligible,
|
|
3763
3763
|
isDecaying,
|
|
3764
|
-
|
|
3764
|
+
isRetiredMemory,
|
|
3765
3765
|
literalMatchesAllTokens as literalMatchesAllTokens22,
|
|
3766
3766
|
literalMatchesAnyToken as literalMatchesAnyToken22,
|
|
3767
3767
|
loadCodeMap as loadCodeMap5,
|
|
@@ -3769,7 +3769,6 @@ import {
|
|
|
3769
3769
|
loadMemoriesFromDir as loadMemoriesFromDir13,
|
|
3770
3770
|
loadUsageIndex as loadUsageIndex7,
|
|
3771
3771
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths22,
|
|
3772
|
-
pathsOverlap,
|
|
3773
3772
|
queryCodeMap as queryCodeMap2,
|
|
3774
3773
|
resolveBriefingBudget as resolveBriefingBudget2,
|
|
3775
3774
|
serializeMemory as serializeMemory9,
|
|
@@ -3781,17 +3780,21 @@ import {
|
|
|
3781
3780
|
writeBriefingMarker as writeBriefingMarker2
|
|
3782
3781
|
} from "@hiveai/core";
|
|
3783
3782
|
import { z as z17 } from "zod";
|
|
3783
|
+
import { readdir as readdir3, readFile as readFile32 } from "fs/promises";
|
|
3784
|
+
import { existsSync as existsSync18 } from "fs";
|
|
3785
|
+
import path92 from "path";
|
|
3786
|
+
import { isGlobPath, isStackPackSeed as isStackPackSeed2, pathsOverlap } from "@hiveai/core";
|
|
3784
3787
|
import { estimateTokens as estimateTokens2, loadCodeMap as loadCodeMap22, queryCodeMap as queryCodeMap22 } from "@hiveai/core";
|
|
3785
3788
|
import { z as z18 } from "zod";
|
|
3786
|
-
import { existsSync as
|
|
3789
|
+
import { existsSync as existsSync20 } from "fs";
|
|
3787
3790
|
import { loadMemoriesFromDir as loadMemoriesFromDir14 } from "@hiveai/core";
|
|
3788
3791
|
import { z as z19 } from "zod";
|
|
3789
|
-
import { existsSync as
|
|
3792
|
+
import { existsSync as existsSync21 } from "fs";
|
|
3790
3793
|
import { loadMemoriesFromDir as loadMemoriesFromDir15 } from "@hiveai/core";
|
|
3791
3794
|
import { z as z20 } from "zod";
|
|
3792
3795
|
import { z as z21 } from "zod";
|
|
3793
3796
|
import { z as z22 } from "zod";
|
|
3794
|
-
import { existsSync as
|
|
3797
|
+
import { existsSync as existsSync222 } from "fs";
|
|
3795
3798
|
import { spawn } from "child_process";
|
|
3796
3799
|
import path102 from "path";
|
|
3797
3800
|
import {
|
|
@@ -3803,7 +3806,7 @@ import {
|
|
|
3803
3806
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths3
|
|
3804
3807
|
} from "@hiveai/core";
|
|
3805
3808
|
import { z as z23 } from "zod";
|
|
3806
|
-
import { existsSync as
|
|
3809
|
+
import { existsSync as existsSync23 } from "fs";
|
|
3807
3810
|
import {
|
|
3808
3811
|
addedLinesFromDiff,
|
|
3809
3812
|
deriveConfidence as deriveConfidence6,
|
|
@@ -3818,13 +3821,13 @@ import {
|
|
|
3818
3821
|
tokenizeQuery as tokenizeQuery3
|
|
3819
3822
|
} from "@hiveai/core";
|
|
3820
3823
|
import { z as z24 } from "zod";
|
|
3821
|
-
import { existsSync as
|
|
3824
|
+
import { existsSync as existsSync24 } from "fs";
|
|
3822
3825
|
import {
|
|
3823
3826
|
loadMemoriesFromDir as loadMemoriesFromDir18,
|
|
3824
3827
|
tokenizeQuery as tokenizeQuery4
|
|
3825
3828
|
} from "@hiveai/core";
|
|
3826
3829
|
import { z as z25 } from "zod";
|
|
3827
|
-
import { existsSync as
|
|
3830
|
+
import { existsSync as existsSync25 } from "fs";
|
|
3828
3831
|
import { spawn as spawn2 } from "child_process";
|
|
3829
3832
|
import {
|
|
3830
3833
|
deriveConfidence as deriveConfidence7,
|
|
@@ -3834,7 +3837,7 @@ import {
|
|
|
3834
3837
|
pathsOverlap as singlePathsOverlap
|
|
3835
3838
|
} from "@hiveai/core";
|
|
3836
3839
|
import { z as z26 } from "zod";
|
|
3837
|
-
import { existsSync as
|
|
3840
|
+
import { existsSync as existsSync26 } from "fs";
|
|
3838
3841
|
import {
|
|
3839
3842
|
deriveConfidence as deriveConfidence8,
|
|
3840
3843
|
getUsage as getUsage9,
|
|
@@ -3846,7 +3849,7 @@ import {
|
|
|
3846
3849
|
import { z as z27 } from "zod";
|
|
3847
3850
|
import { z as z28 } from "zod";
|
|
3848
3851
|
import { mkdir as mkdir72, writeFile as writeFile12 } from "fs/promises";
|
|
3849
|
-
import { existsSync as
|
|
3852
|
+
import { existsSync as existsSync27 } from "fs";
|
|
3850
3853
|
import path112 from "path";
|
|
3851
3854
|
import { execSync as execSync2 } from "child_process";
|
|
3852
3855
|
import {
|
|
@@ -3856,7 +3859,7 @@ import {
|
|
|
3856
3859
|
serializeMemory as serializeMemory10
|
|
3857
3860
|
} from "@hiveai/core";
|
|
3858
3861
|
import { z as z29 } from "zod";
|
|
3859
|
-
import { existsSync as
|
|
3862
|
+
import { existsSync as existsSync28 } from "fs";
|
|
3860
3863
|
import {
|
|
3861
3864
|
findLexicalConflictPairs,
|
|
3862
3865
|
findTopicStatusConflictPairs,
|
|
@@ -3867,7 +3870,7 @@ import { resolveProjectInfo } from "@hiveai/core";
|
|
|
3867
3870
|
import { z as z31 } from "zod";
|
|
3868
3871
|
import { MemoryTypeSchema, suggestTopicKey } from "@hiveai/core";
|
|
3869
3872
|
import { z as z32 } from "zod";
|
|
3870
|
-
import { existsSync as
|
|
3873
|
+
import { existsSync as existsSync29 } from "fs";
|
|
3871
3874
|
import { collectTimelineEntries, loadMemoriesFromDir as loadMemoriesFromDir222 } from "@hiveai/core";
|
|
3872
3875
|
import { z as z33 } from "zod";
|
|
3873
3876
|
import { appendRuntimeJournalEntry as appendRuntimeJournalEntry2 } from "@hiveai/core";
|
|
@@ -3877,7 +3880,7 @@ import { z as z35 } from "zod";
|
|
|
3877
3880
|
import { z as z36 } from "zod";
|
|
3878
3881
|
import { z as z37 } from "zod";
|
|
3879
3882
|
import { z as z38 } from "zod";
|
|
3880
|
-
import { loadConfigSync } from "@hiveai/core";
|
|
3883
|
+
import { hasRecentBriefingMarker, loadConfigSync } from "@hiveai/core";
|
|
3881
3884
|
function createContext(options = {}) {
|
|
3882
3885
|
const env = options.env ?? process.env;
|
|
3883
3886
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -5108,6 +5111,144 @@ async function memSessionEnd(input, ctx) {
|
|
|
5108
5111
|
revision_count: 0
|
|
5109
5112
|
};
|
|
5110
5113
|
}
|
|
5114
|
+
function compactSummary(body) {
|
|
5115
|
+
for (const line of body.split("\n")) {
|
|
5116
|
+
const trimmed = line.replace(/^#+\s*/, "").trim();
|
|
5117
|
+
if (trimmed.length > 0) return trimmed.slice(0, 120);
|
|
5118
|
+
}
|
|
5119
|
+
return body.slice(0, 120);
|
|
5120
|
+
}
|
|
5121
|
+
function classifyMemoryPriority(memory2, loaded, inputFiles, inputSymbols) {
|
|
5122
|
+
const fm = loaded?.memory.frontmatter;
|
|
5123
|
+
const directAnchor = Boolean(
|
|
5124
|
+
fm && inputFiles.length > 0 && fm.anchor.paths.some((p) => inputFiles.some((file) => pathsOverlap(p, file)))
|
|
5125
|
+
);
|
|
5126
|
+
const directSymbol = Boolean(
|
|
5127
|
+
fm && inputSymbols.length > 0 && fm.anchor.symbols.some(
|
|
5128
|
+
(sym) => inputSymbols.some((wanted) => wanted.toLowerCase() === sym.toLowerCase())
|
|
5129
|
+
)
|
|
5130
|
+
);
|
|
5131
|
+
const strongSemantic = (memory2.semantic_score ?? 0) >= 0.65;
|
|
5132
|
+
const usefulSemantic = (memory2.semantic_score ?? 0) >= 0.35;
|
|
5133
|
+
if (fm?.requires_human_approval || directAnchor || directSymbol || memory2.type === "attempt" && (memory2.match_quality === "exact" || strongSemantic) || memory2.type === "skill" && (memory2.match_quality === "exact" || strongSemantic)) {
|
|
5134
|
+
return "must_read";
|
|
5135
|
+
}
|
|
5136
|
+
if (isStackPackSeed2(fm)) {
|
|
5137
|
+
return "background";
|
|
5138
|
+
}
|
|
5139
|
+
if (memory2.type === "skill" || memory2.reasons.includes("module") || memory2.reasons.includes("domain") || memory2.match_quality === "exact" || usefulSemantic) {
|
|
5140
|
+
return "useful";
|
|
5141
|
+
}
|
|
5142
|
+
return "background";
|
|
5143
|
+
}
|
|
5144
|
+
function priorityRank(priority) {
|
|
5145
|
+
return priority === "must_read" ? 3 : priority === "useful" ? 2 : 1;
|
|
5146
|
+
}
|
|
5147
|
+
function classifyBriefingQuality(memories, context) {
|
|
5148
|
+
const mustRead = memories.filter((m) => m.priority === "must_read").length;
|
|
5149
|
+
const useful = memories.filter((m) => m.priority === "useful").length;
|
|
5150
|
+
const background = memories.filter((m) => m.priority === "background").length;
|
|
5151
|
+
const weakSemantic = memories.filter(
|
|
5152
|
+
(m) => m.reasons.length === 1 && m.reasons.includes("semantic") && (m.semantic_score ?? 0) > 0 && (m.semantic_score ?? 0) < 0.35
|
|
5153
|
+
).length;
|
|
5154
|
+
const reasons = [];
|
|
5155
|
+
if (memories.length === 0) reasons.push("no memories matched the task or files");
|
|
5156
|
+
if (context.isTemplateContext && !context.autoContextGenerated) reasons.push("project context is still a template");
|
|
5157
|
+
if (!context.hasLastSession) reasons.push("no previous session recap");
|
|
5158
|
+
if (mustRead > 0) reasons.push(`${mustRead} must_read memor${mustRead === 1 ? "y" : "ies"} matched directly`);
|
|
5159
|
+
if (useful > 0) reasons.push(`${useful} useful memor${useful === 1 ? "y" : "ies"} matched`);
|
|
5160
|
+
if (background > useful + mustRead && background > 2) reasons.push(`${background} background memories dominate the result`);
|
|
5161
|
+
if (weakSemantic > 0) reasons.push(`${weakSemantic} weak semantic-only match${weakSemantic === 1 ? "" : "es"}`);
|
|
5162
|
+
if (context.searchMode === "literal_fallback") reasons.push("semantic index unavailable or empty; literal fallback used");
|
|
5163
|
+
if (memories.length === 0 || mustRead === 0 && useful === 0) {
|
|
5164
|
+
return { level: "thin", reasons };
|
|
5165
|
+
}
|
|
5166
|
+
if (background > useful + mustRead && background > 2) {
|
|
5167
|
+
return { level: "noisy", reasons };
|
|
5168
|
+
}
|
|
5169
|
+
return { level: "strong", reasons };
|
|
5170
|
+
}
|
|
5171
|
+
function explainWhySurfaced(memory2, loaded, inputFiles, inferredModules) {
|
|
5172
|
+
const why = [];
|
|
5173
|
+
const fm = loaded?.memory.frontmatter;
|
|
5174
|
+
if (memory2.reasons.includes("anchor") && fm) {
|
|
5175
|
+
const matching = fm.anchor.paths.filter(
|
|
5176
|
+
(p) => inputFiles.length === 0 || inputFiles.some((file) => pathsOverlap(p, file))
|
|
5177
|
+
);
|
|
5178
|
+
if (matching.length > 0) {
|
|
5179
|
+
const exact = matching.filter(
|
|
5180
|
+
(p) => !isGlobPath(p) && inputFiles.some((file) => p === file || pathsOverlap(p, file))
|
|
5181
|
+
);
|
|
5182
|
+
const glob = matching.filter((p) => isGlobPath(p));
|
|
5183
|
+
if (exact.length > 0) {
|
|
5184
|
+
why.push(`Exact/file anchor match: ${exact.slice(0, 4).join(", ")}`);
|
|
5185
|
+
}
|
|
5186
|
+
if (glob.length > 0) {
|
|
5187
|
+
why.push(`Glob anchor match: ${glob.slice(0, 4).join(", ")}`);
|
|
5188
|
+
}
|
|
5189
|
+
if (exact.length === 0 && glob.length === 0) {
|
|
5190
|
+
why.push(`Anchored to touched path${matching.length === 1 ? "" : "s"}: ${matching.slice(0, 4).join(", ")}`);
|
|
5191
|
+
}
|
|
5192
|
+
} else if (fm.anchor.paths.length > 0) {
|
|
5193
|
+
why.push(`Pulled by related anchor: ${fm.anchor.paths.slice(0, 4).join(", ")}`);
|
|
5194
|
+
}
|
|
5195
|
+
if (fm.anchor.symbols.length > 0) {
|
|
5196
|
+
why.push(`Anchor symbol${fm.anchor.symbols.length === 1 ? "" : "s"}: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
|
|
5197
|
+
}
|
|
5198
|
+
}
|
|
5199
|
+
if (memory2.reasons.includes("symbol") && fm) {
|
|
5200
|
+
why.push(`Explicit symbol match: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
|
|
5201
|
+
}
|
|
5202
|
+
if (memory2.reasons.includes("module")) {
|
|
5203
|
+
const moduleHints = [
|
|
5204
|
+
...memory2.module ? [memory2.module] : [],
|
|
5205
|
+
...memory2.tags.filter((tag) => inferredModules.includes(tag))
|
|
5206
|
+
];
|
|
5207
|
+
const shown = moduleHints.length > 0 ? [...new Set(moduleHints)].join(", ") : inferredModules.join(", ");
|
|
5208
|
+
why.push(shown ? `Matched inferred module/tag: ${shown}` : "Matched inferred module context.");
|
|
5209
|
+
}
|
|
5210
|
+
if (memory2.reasons.includes("domain")) {
|
|
5211
|
+
why.push("Matched inferred domain from the target file paths.");
|
|
5212
|
+
}
|
|
5213
|
+
if (memory2.reasons.includes("semantic")) {
|
|
5214
|
+
const score = memory2.semantic_score !== void 0 ? ` score=${Math.round(memory2.semantic_score * 100) / 100}` : "";
|
|
5215
|
+
why.push(`${memory2.match_quality === "exact" ? "Literal task match" : "Semantic/task relevance"}${score}.`);
|
|
5216
|
+
}
|
|
5217
|
+
why.push(`Confidence: ${memory2.confidence}; read ${memory2.read_count} time${memory2.read_count === 1 ? "" : "s"}.`);
|
|
5218
|
+
if (memory2.type === "attempt") why.push("Failed-approach record; read before repeating the same path.");
|
|
5219
|
+
if (memory2.type === "skill") why.push("Skill (reusable procedure/playbook) \u2014 follow the steps described when doing this type of task.");
|
|
5220
|
+
if (memory2.status === "proposed" || memory2.status === "draft") {
|
|
5221
|
+
why.push("Unvalidated record; use cautiously or ask a human before treating it as policy.");
|
|
5222
|
+
}
|
|
5223
|
+
return why;
|
|
5224
|
+
}
|
|
5225
|
+
async function trySemanticHits(ctx, task, limit) {
|
|
5226
|
+
let mod;
|
|
5227
|
+
try {
|
|
5228
|
+
mod = await import("@hiveai/embeddings");
|
|
5229
|
+
} catch {
|
|
5230
|
+
return null;
|
|
5231
|
+
}
|
|
5232
|
+
const result = await mod.semanticSearch(ctx.paths, task, { limit });
|
|
5233
|
+
if (!result) return null;
|
|
5234
|
+
return result.hits.map((h) => ({ id: h.id, score: h.score }));
|
|
5235
|
+
}
|
|
5236
|
+
async function loadModuleContexts2(ctx, modules) {
|
|
5237
|
+
if (modules.length === 0) return [];
|
|
5238
|
+
if (!existsSync18(ctx.paths.modulesContextDir)) return [];
|
|
5239
|
+
const available = new Set(
|
|
5240
|
+
(await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
|
|
5241
|
+
);
|
|
5242
|
+
const out = [];
|
|
5243
|
+
for (const m of modules) {
|
|
5244
|
+
if (!available.has(m)) continue;
|
|
5245
|
+
const file = path92.join(ctx.paths.modulesContextDir, m, "context.md");
|
|
5246
|
+
if (existsSync18(file)) {
|
|
5247
|
+
out.push({ name: m, content: await readFile32(file, "utf8") });
|
|
5248
|
+
}
|
|
5249
|
+
}
|
|
5250
|
+
return out;
|
|
5251
|
+
}
|
|
5111
5252
|
var GetBriefingInputSchema = {
|
|
5112
5253
|
task: z17.string().optional().describe(
|
|
5113
5254
|
"What you are about to do, in 1\u20132 sentences. Used to rank relevant memories semantically."
|
|
@@ -5153,7 +5294,7 @@ async function getBriefing(input, ctx) {
|
|
|
5153
5294
|
let usage = { version: 1, updated_at: "", by_id: {} };
|
|
5154
5295
|
let byId = /* @__PURE__ */ new Map();
|
|
5155
5296
|
let lastSession;
|
|
5156
|
-
if (
|
|
5297
|
+
if (existsSync19(ctx.paths.memoriesDir)) {
|
|
5157
5298
|
const allLoaded = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
|
|
5158
5299
|
const recaps = allLoaded.filter(({ memory: memory2 }) => memory2.frontmatter.type === "session_recap").sort(
|
|
5159
5300
|
(a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
|
|
@@ -5238,34 +5379,25 @@ async function getBriefing(input, ctx) {
|
|
|
5238
5379
|
if (input.task) {
|
|
5239
5380
|
const tokens = tokenizeQuery22(input.task);
|
|
5240
5381
|
const andHits = allMemories.filter((m) => literalMatchesAllTokens22(m.memory, tokens));
|
|
5241
|
-
for (const loaded of andHits)
|
|
5242
|
-
addOrUpdate(loaded, "semantic", void 0, "exact");
|
|
5243
|
-
}
|
|
5382
|
+
for (const loaded of andHits) addOrUpdate(loaded, "semantic", void 0, "exact");
|
|
5244
5383
|
if (andHits.length === 0 && tokens.length > 1) {
|
|
5245
5384
|
for (const loaded of allMemories) {
|
|
5246
|
-
if (literalMatchesAnyToken22(loaded.memory, tokens))
|
|
5247
|
-
addOrUpdate(loaded, "semantic", void 0, "partial");
|
|
5248
|
-
}
|
|
5385
|
+
if (literalMatchesAnyToken22(loaded.memory, tokens)) addOrUpdate(loaded, "semantic", void 0, "partial");
|
|
5249
5386
|
}
|
|
5250
5387
|
}
|
|
5251
5388
|
if (semanticHits) {
|
|
5252
5389
|
for (const hit of semanticHits) {
|
|
5253
|
-
if (hit.score < input.min_semantic_score)
|
|
5254
|
-
const existing = seen.get(hit.id);
|
|
5255
|
-
if (!existing) continue;
|
|
5256
|
-
}
|
|
5390
|
+
if (hit.score < input.min_semantic_score && !seen.has(hit.id)) continue;
|
|
5257
5391
|
const loaded = byId.get(hit.id);
|
|
5258
5392
|
if (loaded) addOrUpdate(loaded, "semantic", hit.score, "semantic");
|
|
5259
5393
|
}
|
|
5260
5394
|
}
|
|
5261
5395
|
}
|
|
5262
5396
|
const ranked = [...seen.values()].sort((a, b) => {
|
|
5263
|
-
const
|
|
5264
|
-
const reasonScore = (m) => (m.type === "attempt" ? 3 : 0) + // attempt = negative knowledge, surface first to prevent repeating mistakes
|
|
5265
|
-
(m.reasons.includes("anchor") ? 4 : 0) + (m.reasons.includes("symbol") ? 4 : 0) + (m.reasons.includes("module") ? 2 : 0) + (m.reasons.includes("semantic") ? 2 : 0) + (m.reasons.includes("domain") ? 1 : 0);
|
|
5397
|
+
const reasonScore = (m) => (m.type === "attempt" ? 3 : 0) + (m.reasons.includes("anchor") ? 4 : 0) + (m.reasons.includes("symbol") ? 4 : 0) + (m.reasons.includes("module") ? 2 : 0) + (m.reasons.includes("semantic") ? 2 : 0) + (m.reasons.includes("domain") ? 1 : 0);
|
|
5266
5398
|
const confidenceScore = (m) => m.confidence === "authoritative" ? 4 : m.confidence === "trusted" ? 3 : m.confidence === "low" ? 1 : m.confidence === "stale" ? -2 : 0;
|
|
5267
|
-
const sa =
|
|
5268
|
-
const sb =
|
|
5399
|
+
const sa = priorityRank(classifyMemoryPriority(a, byId.get(a.id), input.files, input.symbols)) * 100 + reasonScore(a) + confidenceScore(a) + (a.semantic_score ?? 0);
|
|
5400
|
+
const sb = priorityRank(classifyMemoryPriority(b, byId.get(b.id), input.files, input.symbols)) * 100 + reasonScore(b) + confidenceScore(b) + (b.semantic_score ?? 0);
|
|
5269
5401
|
return sb - sa;
|
|
5270
5402
|
});
|
|
5271
5403
|
for (const mem of ranked.slice(0, briefingMaxMemories)) {
|
|
@@ -5294,11 +5426,7 @@ async function getBriefing(input, ctx) {
|
|
|
5294
5426
|
if (!isAutoPromoteEligible(loaded.memory.frontmatter, u, rule)) continue;
|
|
5295
5427
|
const newFm = { ...loaded.memory.frontmatter, status: "validated" };
|
|
5296
5428
|
try {
|
|
5297
|
-
await writeFile11(
|
|
5298
|
-
loaded.filePath,
|
|
5299
|
-
serializeMemory9({ frontmatter: newFm, body: loaded.memory.body }),
|
|
5300
|
-
"utf8"
|
|
5301
|
-
);
|
|
5429
|
+
await writeFile11(loaded.filePath, serializeMemory9({ frontmatter: newFm, body: loaded.memory.body }), "utf8");
|
|
5302
5430
|
m.status = "validated";
|
|
5303
5431
|
m.confidence = "trusted";
|
|
5304
5432
|
} catch {
|
|
@@ -5306,12 +5434,12 @@ async function getBriefing(input, ctx) {
|
|
|
5306
5434
|
}
|
|
5307
5435
|
}
|
|
5308
5436
|
}
|
|
5309
|
-
const projectContextRaw = input.include_project_context &&
|
|
5437
|
+
const projectContextRaw = input.include_project_context && existsSync19(ctx.paths.projectContext) ? await readFile42(ctx.paths.projectContext, "utf8") : "";
|
|
5310
5438
|
const isTemplateContext = projectContextRaw.includes("TODO \u2014 high-level overview") || projectContextRaw.includes("Generated by `haive init`");
|
|
5311
5439
|
const setupWarnings = [];
|
|
5312
5440
|
let autoContextGenerated = false;
|
|
5313
5441
|
let projectContext = isTemplateContext ? "" : projectContextRaw;
|
|
5314
|
-
if ((isTemplateContext || !
|
|
5442
|
+
if ((isTemplateContext || !existsSync19(ctx.paths.projectContext)) && input.include_project_context) {
|
|
5315
5443
|
const haiveConfig = await loadConfig3(ctx.paths);
|
|
5316
5444
|
if (haiveConfig.autoContext) {
|
|
5317
5445
|
const codeMap = await loadCodeMap5(ctx.paths);
|
|
@@ -5347,15 +5475,9 @@ async function getBriefing(input, ctx) {
|
|
|
5347
5475
|
);
|
|
5348
5476
|
}
|
|
5349
5477
|
} else {
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
);
|
|
5354
|
-
} else {
|
|
5355
|
-
setupWarnings.push(
|
|
5356
|
-
"No project-context.md found. Run `haive init` then invoke the bootstrap_project MCP prompt."
|
|
5357
|
-
);
|
|
5358
|
-
}
|
|
5478
|
+
setupWarnings.push(
|
|
5479
|
+
isTemplateContext ? "project-context.md still contains the default template. Invoke the bootstrap_project MCP prompt to auto-fill it from your codebase. Until then, get_briefing returns no project context." : "No project-context.md found. Run `haive init` then invoke the bootstrap_project MCP prompt."
|
|
5480
|
+
);
|
|
5359
5481
|
}
|
|
5360
5482
|
}
|
|
5361
5483
|
const moduleContents = briefingIncludeModules ? await loadModuleContexts2(ctx, inferred) : [];
|
|
@@ -5418,10 +5540,7 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
5418
5540
|
const createdAt = loaded?.memory.frontmatter.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
5419
5541
|
if (isDecaying(u, createdAt)) decayWarnings.push(m.id);
|
|
5420
5542
|
}
|
|
5421
|
-
const formattedMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : input.format === "actions" ? trimmedMemories.map((m) => ({
|
|
5422
|
-
...m,
|
|
5423
|
-
body: extractActionsBriefBody2(m.body)
|
|
5424
|
-
})) : trimmedMemories;
|
|
5543
|
+
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;
|
|
5425
5544
|
const outputMemories = formattedMemories.map((m) => ({
|
|
5426
5545
|
...m,
|
|
5427
5546
|
priority: classifyMemoryPriority(m, byId.get(m.id), input.files, input.symbols),
|
|
@@ -5436,8 +5555,7 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
5436
5555
|
let symbolLocations;
|
|
5437
5556
|
const symbolsToLookup = new Set(input.symbols);
|
|
5438
5557
|
for (const m of outputMemories) {
|
|
5439
|
-
const
|
|
5440
|
-
for (const sym of loaded?.memory.frontmatter.anchor.symbols ?? []) {
|
|
5558
|
+
for (const sym of byId.get(m.id)?.memory.frontmatter.anchor.symbols ?? []) {
|
|
5441
5559
|
symbolsToLookup.add(sym);
|
|
5442
5560
|
}
|
|
5443
5561
|
}
|
|
@@ -5465,41 +5583,37 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
5465
5583
|
}
|
|
5466
5584
|
}
|
|
5467
5585
|
const actionRequired = [];
|
|
5468
|
-
|
|
5469
|
-
const
|
|
5470
|
-
if (!loaded?.memory.frontmatter.requires_human_approval) continue;
|
|
5471
|
-
const bodyLines = loaded.memory.body.split("\n");
|
|
5586
|
+
const extractActionItem = (id, body) => {
|
|
5587
|
+
const bodyLines = body.split("\n");
|
|
5472
5588
|
const quoteBlock = bodyLines.filter((l) => l.startsWith("> ")).map((l) => l.slice(2)).join(" ").replace(/^\*«\s*/, "").replace(/\s*»\*$/, "").trim();
|
|
5473
5589
|
const headingLine = bodyLines.find((l) => l.startsWith("## "));
|
|
5474
|
-
const summary = headingLine?.replace(/^##\s*/, "").trim() ??
|
|
5475
|
-
|
|
5476
|
-
id
|
|
5590
|
+
const summary = headingLine?.replace(/^##\s*/, "").trim() ?? id;
|
|
5591
|
+
return {
|
|
5592
|
+
id,
|
|
5477
5593
|
summary,
|
|
5478
|
-
developer_message: quoteBlock || `Une modification externe potentiellement incompatible a \xE9t\xE9 d\xE9tect\xE9e (${
|
|
5479
|
-
}
|
|
5594
|
+
developer_message: quoteBlock || `Une modification externe potentiellement incompatible a \xE9t\xE9 d\xE9tect\xE9e (${id}). Veux-tu que j'analyse l'impact et que je propose des mises \xE0 jour ?`
|
|
5595
|
+
};
|
|
5596
|
+
};
|
|
5597
|
+
for (const m of outputMemories) {
|
|
5598
|
+
const loaded = byId.get(m.id);
|
|
5599
|
+
if (loaded?.memory.frontmatter.requires_human_approval) {
|
|
5600
|
+
actionRequired.push(extractActionItem(m.id, loaded.memory.body));
|
|
5601
|
+
}
|
|
5480
5602
|
}
|
|
5481
|
-
if (
|
|
5603
|
+
if (existsSync19(ctx.paths.memoriesDir)) {
|
|
5482
5604
|
const allMems = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
|
|
5483
5605
|
for (const { memory: memory2 } of allMems) {
|
|
5484
5606
|
const fm = memory2.frontmatter;
|
|
5485
5607
|
if (!fm.requires_human_approval) continue;
|
|
5486
5608
|
if (fm.status === "rejected" || fm.status === "deprecated") continue;
|
|
5487
5609
|
if (actionRequired.some((a) => a.id === fm.id)) continue;
|
|
5488
|
-
|
|
5489
|
-
const quoteBlock = bodyLines.filter((l) => l.startsWith("> ")).map((l) => l.slice(2)).join(" ").replace(/^\*«\s*/, "").replace(/\s*»\*$/, "").trim();
|
|
5490
|
-
const headingLine = bodyLines.find((l) => l.startsWith("## "));
|
|
5491
|
-
const summary = headingLine?.replace(/^##\s*/, "").trim() ?? fm.id;
|
|
5492
|
-
actionRequired.push({
|
|
5493
|
-
id: fm.id,
|
|
5494
|
-
summary,
|
|
5495
|
-
developer_message: quoteBlock || `Une modification externe potentiellement incompatible a \xE9t\xE9 d\xE9tect\xE9e (${fm.id}). Veux-tu que j'analyse l'impact et que je propose des mises \xE0 jour ?`
|
|
5496
|
-
});
|
|
5610
|
+
actionRequired.push(extractActionItem(fm.id, memory2.body));
|
|
5497
5611
|
}
|
|
5498
5612
|
}
|
|
5499
5613
|
const pendingDistillFile = pendingDistillPath(ctx);
|
|
5500
|
-
if (
|
|
5614
|
+
if (existsSync19(pendingDistillFile)) {
|
|
5501
5615
|
try {
|
|
5502
|
-
const raw = await
|
|
5616
|
+
const raw = await readFile42(pendingDistillFile, "utf8");
|
|
5503
5617
|
const pd = JSON.parse(raw);
|
|
5504
5618
|
const ageMs = Date.now() - new Date(pd.session_end).getTime();
|
|
5505
5619
|
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1e3;
|
|
@@ -5526,7 +5640,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
5526
5640
|
}
|
|
5527
5641
|
}
|
|
5528
5642
|
const memoriesEmpty = outputMemories.length === 0;
|
|
5529
|
-
const hasMemoriesDir =
|
|
5643
|
+
const hasMemoriesDir = existsSync19(ctx.paths.memoriesDir);
|
|
5530
5644
|
const isColdStart = isTemplateContext && memoriesEmpty && !lastSession && !autoContextGenerated;
|
|
5531
5645
|
const hasUnguessableSignal = outputMemories.some(
|
|
5532
5646
|
(m) => (m.priority === "must_read" || m.priority === "useful") && specificityScore2(m.body) >= GUESSABLE_THRESHOLD
|
|
@@ -5573,7 +5687,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
5573
5687
|
"No team-specific policy matched these files/task \u2014 nothing here a capable model can't infer. The auto-generated project context was trimmed to keep this briefing near-zero-cost; proceed with normal Read/Grep."
|
|
5574
5688
|
);
|
|
5575
5689
|
}
|
|
5576
|
-
if (
|
|
5690
|
+
if (existsSync19(ctx.paths.haiveDir)) {
|
|
5577
5691
|
await writeBriefingMarker2(ctx.paths, {
|
|
5578
5692
|
sessionId: process.env.HAIVE_SESSION_ID,
|
|
5579
5693
|
...input.task ? { task: input.task } : {},
|
|
@@ -5621,144 +5735,6 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
5621
5735
|
}
|
|
5622
5736
|
};
|
|
5623
5737
|
}
|
|
5624
|
-
function compactSummary(body) {
|
|
5625
|
-
for (const line of body.split("\n")) {
|
|
5626
|
-
const trimmed = line.replace(/^#+\s*/, "").trim();
|
|
5627
|
-
if (trimmed.length > 0) return trimmed.slice(0, 120);
|
|
5628
|
-
}
|
|
5629
|
-
return body.slice(0, 120);
|
|
5630
|
-
}
|
|
5631
|
-
function classifyMemoryPriority(memory2, loaded, inputFiles, inputSymbols) {
|
|
5632
|
-
const fm = loaded?.memory.frontmatter;
|
|
5633
|
-
const directAnchor = Boolean(
|
|
5634
|
-
fm && inputFiles.length > 0 && fm.anchor.paths.some((p) => inputFiles.some((file) => pathsOverlap(p, file)))
|
|
5635
|
-
);
|
|
5636
|
-
const directSymbol = Boolean(
|
|
5637
|
-
fm && inputSymbols.length > 0 && fm.anchor.symbols.some(
|
|
5638
|
-
(sym) => inputSymbols.some((wanted) => wanted.toLowerCase() === sym.toLowerCase())
|
|
5639
|
-
)
|
|
5640
|
-
);
|
|
5641
|
-
const strongSemantic = (memory2.semantic_score ?? 0) >= 0.65;
|
|
5642
|
-
const usefulSemantic = (memory2.semantic_score ?? 0) >= 0.35;
|
|
5643
|
-
if (fm?.requires_human_approval || directAnchor || directSymbol || memory2.type === "attempt" && (memory2.match_quality === "exact" || strongSemantic) || memory2.type === "skill" && (memory2.match_quality === "exact" || strongSemantic)) {
|
|
5644
|
-
return "must_read";
|
|
5645
|
-
}
|
|
5646
|
-
if (isStackPackSeed2(fm)) {
|
|
5647
|
-
return "background";
|
|
5648
|
-
}
|
|
5649
|
-
if (memory2.type === "skill" || memory2.reasons.includes("module") || memory2.reasons.includes("domain") || memory2.match_quality === "exact" || usefulSemantic) {
|
|
5650
|
-
return "useful";
|
|
5651
|
-
}
|
|
5652
|
-
return "background";
|
|
5653
|
-
}
|
|
5654
|
-
function priorityRank(priority) {
|
|
5655
|
-
return priority === "must_read" ? 3 : priority === "useful" ? 2 : 1;
|
|
5656
|
-
}
|
|
5657
|
-
function classifyBriefingQuality(memories, context) {
|
|
5658
|
-
const mustRead = memories.filter((m) => m.priority === "must_read").length;
|
|
5659
|
-
const useful = memories.filter((m) => m.priority === "useful").length;
|
|
5660
|
-
const background = memories.filter((m) => m.priority === "background").length;
|
|
5661
|
-
const weakSemantic = memories.filter(
|
|
5662
|
-
(m) => m.reasons.length === 1 && m.reasons.includes("semantic") && (m.semantic_score ?? 0) > 0 && (m.semantic_score ?? 0) < 0.35
|
|
5663
|
-
).length;
|
|
5664
|
-
const reasons = [];
|
|
5665
|
-
if (memories.length === 0) reasons.push("no memories matched the task or files");
|
|
5666
|
-
if (context.isTemplateContext && !context.autoContextGenerated) reasons.push("project context is still a template");
|
|
5667
|
-
if (!context.hasLastSession) reasons.push("no previous session recap");
|
|
5668
|
-
if (mustRead > 0) reasons.push(`${mustRead} must_read memor${mustRead === 1 ? "y" : "ies"} matched directly`);
|
|
5669
|
-
if (useful > 0) reasons.push(`${useful} useful memor${useful === 1 ? "y" : "ies"} matched`);
|
|
5670
|
-
if (background > useful + mustRead && background > 2) reasons.push(`${background} background memories dominate the result`);
|
|
5671
|
-
if (weakSemantic > 0) reasons.push(`${weakSemantic} weak semantic-only match${weakSemantic === 1 ? "" : "es"}`);
|
|
5672
|
-
if (context.searchMode === "literal_fallback") reasons.push("semantic index unavailable or empty; literal fallback used");
|
|
5673
|
-
if (memories.length === 0 || mustRead === 0 && useful === 0) {
|
|
5674
|
-
return { level: "thin", reasons };
|
|
5675
|
-
}
|
|
5676
|
-
if (background > useful + mustRead && background > 2) {
|
|
5677
|
-
return { level: "noisy", reasons };
|
|
5678
|
-
}
|
|
5679
|
-
return { level: "strong", reasons };
|
|
5680
|
-
}
|
|
5681
|
-
function explainWhySurfaced(memory2, loaded, inputFiles, inferredModules) {
|
|
5682
|
-
const why = [];
|
|
5683
|
-
const fm = loaded?.memory.frontmatter;
|
|
5684
|
-
if (memory2.reasons.includes("anchor") && fm) {
|
|
5685
|
-
const matching = fm.anchor.paths.filter(
|
|
5686
|
-
(p) => inputFiles.length === 0 || inputFiles.some((file) => pathsOverlap(p, file))
|
|
5687
|
-
);
|
|
5688
|
-
if (matching.length > 0) {
|
|
5689
|
-
const exact = matching.filter(
|
|
5690
|
-
(p) => !isGlobPath(p) && inputFiles.some((file) => p === file || pathsOverlap(p, file))
|
|
5691
|
-
);
|
|
5692
|
-
const glob = matching.filter((p) => isGlobPath(p));
|
|
5693
|
-
if (exact.length > 0) {
|
|
5694
|
-
why.push(`Exact/file anchor match: ${exact.slice(0, 4).join(", ")}`);
|
|
5695
|
-
}
|
|
5696
|
-
if (glob.length > 0) {
|
|
5697
|
-
why.push(`Glob anchor match: ${glob.slice(0, 4).join(", ")}`);
|
|
5698
|
-
}
|
|
5699
|
-
if (exact.length === 0 && glob.length === 0) {
|
|
5700
|
-
why.push(`Anchored to touched path${matching.length === 1 ? "" : "s"}: ${matching.slice(0, 4).join(", ")}`);
|
|
5701
|
-
}
|
|
5702
|
-
} else if (fm.anchor.paths.length > 0) {
|
|
5703
|
-
why.push(`Pulled by related anchor: ${fm.anchor.paths.slice(0, 4).join(", ")}`);
|
|
5704
|
-
}
|
|
5705
|
-
if (fm.anchor.symbols.length > 0) {
|
|
5706
|
-
why.push(`Anchor symbol${fm.anchor.symbols.length === 1 ? "" : "s"}: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
|
|
5707
|
-
}
|
|
5708
|
-
}
|
|
5709
|
-
if (memory2.reasons.includes("symbol") && fm) {
|
|
5710
|
-
why.push(`Explicit symbol match: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
|
|
5711
|
-
}
|
|
5712
|
-
if (memory2.reasons.includes("module")) {
|
|
5713
|
-
const moduleHints = [
|
|
5714
|
-
...memory2.module ? [memory2.module] : [],
|
|
5715
|
-
...memory2.tags.filter((tag) => inferredModules.includes(tag))
|
|
5716
|
-
];
|
|
5717
|
-
const shown = moduleHints.length > 0 ? [...new Set(moduleHints)].join(", ") : inferredModules.join(", ");
|
|
5718
|
-
why.push(shown ? `Matched inferred module/tag: ${shown}` : "Matched inferred module context.");
|
|
5719
|
-
}
|
|
5720
|
-
if (memory2.reasons.includes("domain")) {
|
|
5721
|
-
why.push("Matched inferred domain from the target file paths.");
|
|
5722
|
-
}
|
|
5723
|
-
if (memory2.reasons.includes("semantic")) {
|
|
5724
|
-
const score = memory2.semantic_score !== void 0 ? ` score=${Math.round(memory2.semantic_score * 100) / 100}` : "";
|
|
5725
|
-
why.push(`${memory2.match_quality === "exact" ? "Literal task match" : "Semantic/task relevance"}${score}.`);
|
|
5726
|
-
}
|
|
5727
|
-
why.push(`Confidence: ${memory2.confidence}; read ${memory2.read_count} time${memory2.read_count === 1 ? "" : "s"}.`);
|
|
5728
|
-
if (memory2.type === "attempt") why.push("Failed-approach record; read before repeating the same path.");
|
|
5729
|
-
if (memory2.type === "skill") why.push("Skill (reusable procedure/playbook) \u2014 follow the steps described when doing this type of task.");
|
|
5730
|
-
if (memory2.status === "proposed" || memory2.status === "draft") {
|
|
5731
|
-
why.push("Unvalidated record; use cautiously or ask a human before treating it as policy.");
|
|
5732
|
-
}
|
|
5733
|
-
return why;
|
|
5734
|
-
}
|
|
5735
|
-
async function trySemanticHits(ctx, task, limit) {
|
|
5736
|
-
let mod;
|
|
5737
|
-
try {
|
|
5738
|
-
mod = await import("@hiveai/embeddings");
|
|
5739
|
-
} catch {
|
|
5740
|
-
return null;
|
|
5741
|
-
}
|
|
5742
|
-
const result = await mod.semanticSearch(ctx.paths, task, { limit });
|
|
5743
|
-
if (!result) return null;
|
|
5744
|
-
return result.hits.map((h) => ({ id: h.id, score: h.score }));
|
|
5745
|
-
}
|
|
5746
|
-
async function loadModuleContexts2(ctx, modules) {
|
|
5747
|
-
if (modules.length === 0) return [];
|
|
5748
|
-
if (!existsSync18(ctx.paths.modulesContextDir)) return [];
|
|
5749
|
-
const available = new Set(
|
|
5750
|
-
(await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
|
|
5751
|
-
);
|
|
5752
|
-
const out = [];
|
|
5753
|
-
for (const m of modules) {
|
|
5754
|
-
if (!available.has(m)) continue;
|
|
5755
|
-
const file = path92.join(ctx.paths.modulesContextDir, m, "context.md");
|
|
5756
|
-
if (existsSync18(file)) {
|
|
5757
|
-
out.push({ name: m, content: await readFile32(file, "utf8") });
|
|
5758
|
-
}
|
|
5759
|
-
}
|
|
5760
|
-
return out;
|
|
5761
|
-
}
|
|
5762
5738
|
var CodeMapInputSchema = {
|
|
5763
5739
|
file: z18.string().optional().describe("Filter to files whose path contains this substring"),
|
|
5764
5740
|
symbol: z18.string().optional().describe("Filter to files exporting a symbol whose name contains this substring"),
|
|
@@ -5842,7 +5818,7 @@ var MemDiffInputSchema = {
|
|
|
5842
5818
|
id_b: z19.string().min(1).describe("Second memory id")
|
|
5843
5819
|
};
|
|
5844
5820
|
async function memDiff(input, ctx) {
|
|
5845
|
-
if (!
|
|
5821
|
+
if (!existsSync20(ctx.paths.memoriesDir)) {
|
|
5846
5822
|
throw new Error(`No .ai/memories at ${ctx.paths.root}.`);
|
|
5847
5823
|
}
|
|
5848
5824
|
const all = await loadMemoriesFromDir14(ctx.paths.memoriesDir);
|
|
@@ -5883,7 +5859,7 @@ var GetRecapInputSchema = {
|
|
|
5883
5859
|
)
|
|
5884
5860
|
};
|
|
5885
5861
|
async function getRecap(input, ctx) {
|
|
5886
|
-
if (!
|
|
5862
|
+
if (!existsSync21(ctx.paths.memoriesDir)) {
|
|
5887
5863
|
return { recap: null, notice: "No .ai/memories directory \u2014 haive not initialized here." };
|
|
5888
5864
|
}
|
|
5889
5865
|
const all = await loadMemoriesFromDir15(ctx.paths.memoriesDir);
|
|
@@ -5982,7 +5958,7 @@ var WhyThisFileInputSchema = {
|
|
|
5982
5958
|
memory_limit: z23.number().int().positive().max(20).default(5).describe("Cap on memories anchored to this path.")
|
|
5983
5959
|
};
|
|
5984
5960
|
async function whyThisFile(input, ctx) {
|
|
5985
|
-
const fileExists =
|
|
5961
|
+
const fileExists = existsSync222(path102.join(ctx.paths.root, input.path));
|
|
5986
5962
|
const [commits, memories, codeMap] = await Promise.all([
|
|
5987
5963
|
runGitLog(ctx.paths.root, input.path, input.git_log_limit).catch(() => []),
|
|
5988
5964
|
collectAnchoredMemories(ctx, input.path, input.memory_limit),
|
|
@@ -6023,7 +5999,7 @@ async function whyThisFile(input, ctx) {
|
|
|
6023
5999
|
};
|
|
6024
6000
|
}
|
|
6025
6001
|
async function collectAnchoredMemories(ctx, filePath, limit) {
|
|
6026
|
-
if (!
|
|
6002
|
+
if (!existsSync222(ctx.paths.memoriesDir)) return [];
|
|
6027
6003
|
const all = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
|
|
6028
6004
|
const usage = await loadUsageIndex8(ctx.paths);
|
|
6029
6005
|
const out = [];
|
|
@@ -6155,7 +6131,7 @@ async function antiPatternsCheck(input, ctx) {
|
|
|
6155
6131
|
notice: "Nothing to check \u2014 provide either `diff` text or `paths`."
|
|
6156
6132
|
};
|
|
6157
6133
|
}
|
|
6158
|
-
if (!
|
|
6134
|
+
if (!existsSync23(ctx.paths.memoriesDir)) {
|
|
6159
6135
|
return { scanned: 0, warnings: [], notice: "No .ai/memories directory \u2014 nothing to check against." };
|
|
6160
6136
|
}
|
|
6161
6137
|
const all = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
|
|
@@ -6190,6 +6166,7 @@ async function antiPatternsCheck(input, ctx) {
|
|
|
6190
6166
|
reasons: [reason],
|
|
6191
6167
|
tags: fm.tags ?? [],
|
|
6192
6168
|
anchor_paths: fm.anchor?.paths ?? [],
|
|
6169
|
+
...fm.sensor != null ? { has_sensor: true } : {},
|
|
6193
6170
|
...score !== void 0 ? { semantic_score: score } : {}
|
|
6194
6171
|
});
|
|
6195
6172
|
};
|
|
@@ -6302,7 +6279,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
6302
6279
|
"error"
|
|
6303
6280
|
]);
|
|
6304
6281
|
async function memDistill(input, ctx) {
|
|
6305
|
-
if (!
|
|
6282
|
+
if (!existsSync24(ctx.paths.memoriesDir)) {
|
|
6306
6283
|
return { scanned: 0, singletons: 0, clusters: [], notice: "No .ai/memories directory." };
|
|
6307
6284
|
}
|
|
6308
6285
|
const cutoff = Date.now() - input.since_days * MS_PER_DAY;
|
|
@@ -6413,7 +6390,7 @@ var WhyThisDecisionInputSchema = {
|
|
|
6413
6390
|
git_log_limit: z26.number().int().positive().max(20).default(5).describe("How many recent commits per anchor path to surface.")
|
|
6414
6391
|
};
|
|
6415
6392
|
async function whyThisDecision(input, ctx) {
|
|
6416
|
-
if (!
|
|
6393
|
+
if (!existsSync25(ctx.paths.memoriesDir)) {
|
|
6417
6394
|
return {
|
|
6418
6395
|
found: false,
|
|
6419
6396
|
related: [],
|
|
@@ -6551,7 +6528,7 @@ var MemConflictsInputSchema = {
|
|
|
6551
6528
|
var POSITIVE_PATTERNS = /\b(use|prefer|always|should use|do this|recommended|ok to)\b/i;
|
|
6552
6529
|
var NEGATIVE_PATTERNS = /\b(do not use|don'?t use|never|avoid|forbidden|deprecated|stop using|do NOT|❌)\b/i;
|
|
6553
6530
|
async function memConflicts(input, ctx) {
|
|
6554
|
-
if (!
|
|
6531
|
+
if (!existsSync26(ctx.paths.memoriesDir)) {
|
|
6555
6532
|
return { found: false, scanned: 0, conflicts: [], notice: "No .ai/memories directory." };
|
|
6556
6533
|
}
|
|
6557
6534
|
const all = await loadMemoriesFromDir20(ctx.paths.memoriesDir);
|
|
@@ -6741,7 +6718,9 @@ async function preCommitCheck(input, ctx) {
|
|
|
6741
6718
|
};
|
|
6742
6719
|
}
|
|
6743
6720
|
function classifyWarning(warning, paths, anchoredBlocks = false) {
|
|
6744
|
-
const affectedFiles = paths.filter(
|
|
6721
|
+
const affectedFiles = paths.filter(
|
|
6722
|
+
(p) => !p.startsWith(".ai/.usage/") && !p.startsWith(".ai/.cache/") && !p.startsWith(".ai/.runtime/")
|
|
6723
|
+
);
|
|
6745
6724
|
const repairCommand = repairCommandForWarning(warning, affectedFiles);
|
|
6746
6725
|
const fileDowngrade = fileTypeDowngradeReason(warning, affectedFiles);
|
|
6747
6726
|
if (fileDowngrade) {
|
|
@@ -6772,6 +6751,24 @@ function classifyWarning(warning, paths, anchoredBlocks = false) {
|
|
|
6772
6751
|
};
|
|
6773
6752
|
}
|
|
6774
6753
|
if (isBlockingWarning(warning)) {
|
|
6754
|
+
if (warning.scope === "personal") {
|
|
6755
|
+
return {
|
|
6756
|
+
...warning,
|
|
6757
|
+
level: "review",
|
|
6758
|
+
rationale: "personal anti-pattern memories are review guidance unless a deterministic block-severity sensor fires",
|
|
6759
|
+
affected_files: affectedFiles,
|
|
6760
|
+
repair_command: repairCommand
|
|
6761
|
+
};
|
|
6762
|
+
}
|
|
6763
|
+
if (warning.has_sensor && !warning.reasons.includes("sensor")) {
|
|
6764
|
+
return {
|
|
6765
|
+
...warning,
|
|
6766
|
+
level: "review",
|
|
6767
|
+
rationale: "memory has a sensor that did not fire \u2014 sensor is the authoritative check; strong semantic match alone is insufficient to block",
|
|
6768
|
+
affected_files: affectedFiles,
|
|
6769
|
+
repair_command: repairCommand
|
|
6770
|
+
};
|
|
6771
|
+
}
|
|
6775
6772
|
return {
|
|
6776
6773
|
...warning,
|
|
6777
6774
|
level: "blocking",
|
|
@@ -6783,7 +6780,16 @@ function classifyWarning(warning, paths, anchoredBlocks = false) {
|
|
|
6783
6780
|
const hasSemantic = warning.reasons.includes("semantic");
|
|
6784
6781
|
const semanticScore = warning.semantic_score ?? 0;
|
|
6785
6782
|
const highConfidence = warning.confidence === "authoritative" || warning.confidence === "trusted";
|
|
6786
|
-
if (anchoredBlocks && highConfidence && warning.reasons.includes("anchor") && (warning.reasons.includes("literal") || hasSemantic && semanticScore >= 0.45)) {
|
|
6783
|
+
if (anchoredBlocks && highConfidence && warning.scope !== "personal" && warning.reasons.includes("anchor") && (warning.reasons.includes("literal") || hasSemantic && semanticScore >= 0.45)) {
|
|
6784
|
+
if (warning.has_sensor && !warning.reasons.includes("sensor")) {
|
|
6785
|
+
return {
|
|
6786
|
+
...warning,
|
|
6787
|
+
level: "review",
|
|
6788
|
+
rationale: "memory has a sensor that did not fire \u2014 literal match alone is insufficient to block; sensor is the authoritative check",
|
|
6789
|
+
affected_files: affectedFiles,
|
|
6790
|
+
repair_command: repairCommand
|
|
6791
|
+
};
|
|
6792
|
+
}
|
|
6787
6793
|
return {
|
|
6788
6794
|
...warning,
|
|
6789
6795
|
level: "blocking",
|
|
@@ -6908,8 +6914,20 @@ function isJsonConfigFile(base) {
|
|
|
6908
6914
|
return false;
|
|
6909
6915
|
}
|
|
6910
6916
|
function repairCommandForWarning(warning, paths) {
|
|
6911
|
-
const
|
|
6912
|
-
return
|
|
6917
|
+
const targetPath = repairTargetPathForWarning(warning, paths);
|
|
6918
|
+
return targetPath ? `haive briefing --files "${targetPath}" --task "review ${warning.id}"` : `haive memory show ${warning.id}`;
|
|
6919
|
+
}
|
|
6920
|
+
function repairTargetPathForWarning(warning, paths) {
|
|
6921
|
+
const usablePaths = paths.filter(
|
|
6922
|
+
(p) => !p.startsWith(".ai/.usage/") && !p.startsWith(".ai/.cache/") && !p.startsWith(".ai/.runtime/")
|
|
6923
|
+
);
|
|
6924
|
+
const anchors = warning.anchor_paths ?? [];
|
|
6925
|
+
for (const file of usablePaths) {
|
|
6926
|
+
if (anchors.some((anchor) => anchor === file || file.startsWith(`${anchor}/`) || anchor.startsWith(`${file}/`))) {
|
|
6927
|
+
return file;
|
|
6928
|
+
}
|
|
6929
|
+
}
|
|
6930
|
+
return usablePaths[0];
|
|
6913
6931
|
}
|
|
6914
6932
|
var CONFIG_PATTERNS = [
|
|
6915
6933
|
".eslintrc",
|
|
@@ -6938,7 +6956,7 @@ var PatternDetectInputSchema = {
|
|
|
6938
6956
|
scope: z29.enum(["personal", "team"]).default("team").describe("Scope for proposed memories.")
|
|
6939
6957
|
};
|
|
6940
6958
|
async function patternDetect(input, ctx) {
|
|
6941
|
-
if (!
|
|
6959
|
+
if (!existsSync27(ctx.paths.haiveDir)) {
|
|
6942
6960
|
return {
|
|
6943
6961
|
scanned_events: 0,
|
|
6944
6962
|
matches: [],
|
|
@@ -7067,7 +7085,7 @@ async function patternDetect(input, ctx) {
|
|
|
7067
7085
|
fm.id,
|
|
7068
7086
|
void 0
|
|
7069
7087
|
);
|
|
7070
|
-
if (
|
|
7088
|
+
if (existsSync27(file)) continue;
|
|
7071
7089
|
await mkdir72(path112.dirname(file), { recursive: true });
|
|
7072
7090
|
await writeFile12(
|
|
7073
7091
|
file,
|
|
@@ -7122,7 +7140,7 @@ var MemConflictCandidatesInputSchema = {
|
|
|
7122
7140
|
)
|
|
7123
7141
|
};
|
|
7124
7142
|
async function memConflictCandidates(input, ctx) {
|
|
7125
|
-
if (!
|
|
7143
|
+
if (!existsSync28(ctx.paths.memoriesDir)) {
|
|
7126
7144
|
return {
|
|
7127
7145
|
pairs: [],
|
|
7128
7146
|
topic_status_pairs: [],
|
|
@@ -7170,7 +7188,7 @@ var MemTimelineInputSchema = {
|
|
|
7170
7188
|
limit: z33.number().int().positive().max(100).default(30).describe("Max timeline entries returned")
|
|
7171
7189
|
};
|
|
7172
7190
|
async function memTimeline(input, ctx) {
|
|
7173
|
-
if (!
|
|
7191
|
+
if (!existsSync29(ctx.paths.memoriesDir)) {
|
|
7174
7192
|
return { entries: [], total: 0, notice: "No .ai/memories directory." };
|
|
7175
7193
|
}
|
|
7176
7194
|
const all = await loadMemoriesFromDir222(ctx.paths.memoriesDir);
|
|
@@ -7363,7 +7381,19 @@ This creates/updates a single rolling recap that **get_briefing automatically su
|
|
|
7363
7381
|
|
|
7364
7382
|
Calling \`mem_session_end\` also **clears the pending-distill marker** (if any), confirming that this session's learnings have been properly captured rather than left as an auto-recap skeleton.
|
|
7365
7383
|
|
|
7366
|
-
|
|
7384
|
+
### 7. Verify the git/release exit protocol \u2014 always
|
|
7385
|
+
Run **\`haive enforce finish\`** before your final response.
|
|
7386
|
+
|
|
7387
|
+
This executable gate checks the multi-agent git-sync decision:
|
|
7388
|
+
- no completed work is left as an uncommitted local diff
|
|
7389
|
+
- shippable package changes have a lockstep version bump
|
|
7390
|
+
- the release tag \`vX.Y.Z\` exists when a version was bumped
|
|
7391
|
+
- commits and tags have been pushed
|
|
7392
|
+
- agents never run \`npm publish\` (publication remains human-owned)
|
|
7393
|
+
|
|
7394
|
+
If it blocks, fix the reported Git/version/tag/push issue before telling the developer the task is done.
|
|
7395
|
+
|
|
7396
|
+
When done, respond with a brief summary: "Saved N memories: [list of IDs]. Session recap saved. hAIve finish gate passed."
|
|
7367
7397
|
`;
|
|
7368
7398
|
return {
|
|
7369
7399
|
description: "Post-task reflection: capture what you learned before closing the session",
|
|
@@ -7442,7 +7472,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
7442
7472
|
};
|
|
7443
7473
|
}
|
|
7444
7474
|
var SERVER_NAME = "haive";
|
|
7445
|
-
var SERVER_VERSION = "0.10.
|
|
7475
|
+
var SERVER_VERSION = "0.10.9";
|
|
7446
7476
|
function jsonResult(data) {
|
|
7447
7477
|
return {
|
|
7448
7478
|
content: [
|
|
@@ -7546,11 +7576,16 @@ function createHaiveServer(options = {}) {
|
|
|
7546
7576
|
return await handler(input);
|
|
7547
7577
|
}
|
|
7548
7578
|
if (requireBriefingFirst && MUTATING_TOOLS.has(name) && !briefingLoaded) {
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7579
|
+
const hasDiskMarker = await hasRecentBriefingMarker(context.paths).catch(() => false);
|
|
7580
|
+
if (hasDiskMarker) {
|
|
7581
|
+
briefingLoaded = true;
|
|
7582
|
+
} else {
|
|
7583
|
+
return jsonResult({
|
|
7584
|
+
error: "haive_briefing_required",
|
|
7585
|
+
message: "This hAIve project requires get_briefing or mem_relevant_to before state-changing hAIve tools. Call get_briefing({ task: '...' }) first.",
|
|
7586
|
+
tool: name
|
|
7587
|
+
});
|
|
7588
|
+
}
|
|
7554
7589
|
}
|
|
7555
7590
|
return await handler(input);
|
|
7556
7591
|
}
|
|
@@ -8412,7 +8447,7 @@ function registerMcp(program2) {
|
|
|
8412
8447
|
// src/commands/sync.ts
|
|
8413
8448
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
8414
8449
|
import { readFile as readFile9, writeFile as writeFile13, mkdir as mkdir10 } from "fs/promises";
|
|
8415
|
-
import { existsSync as
|
|
8450
|
+
import { existsSync as existsSync30 } from "fs";
|
|
8416
8451
|
import path15 from "path";
|
|
8417
8452
|
import "commander";
|
|
8418
8453
|
import {
|
|
@@ -8449,7 +8484,7 @@ function registerSync(program2) {
|
|
|
8449
8484
|
).option("--bridge-file <path>", "bridge file to inject into (default: CLAUDE.md)").option("--bridge-max-memories <n>", "max memories to inject into bridge file", "5").option("--embed", "rebuild embeddings index after sync (requires @haive/embeddings)").option("--no-cross-repo", "skip cross-repo memory pull even if crossRepoSources is configured").option("--no-deps", "skip dependency version tracking").option("--no-contracts", "skip contract file diff checking").option("--dry-run", "report what would change without writing any files").action(async (opts) => {
|
|
8450
8485
|
const root = findProjectRoot12(opts.dir);
|
|
8451
8486
|
const paths = resolveHaivePaths9(root);
|
|
8452
|
-
if (!
|
|
8487
|
+
if (!existsSync30(paths.memoriesDir)) {
|
|
8453
8488
|
if (!opts.quiet) ui.warn(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
8454
8489
|
process.exitCode = 1;
|
|
8455
8490
|
return;
|
|
@@ -8873,7 +8908,7 @@ function bridgeSummaryLine(body) {
|
|
|
8873
8908
|
return oneLine.length > 140 ? oneLine.slice(0, 137) + "\u2026" : oneLine;
|
|
8874
8909
|
}
|
|
8875
8910
|
async function injectBridge(bridgeFile, memoriesDir, maxMemories, root, quiet) {
|
|
8876
|
-
if (!
|
|
8911
|
+
if (!existsSync30(memoriesDir)) return;
|
|
8877
8912
|
const all = await loadMemoriesFromDir23(memoriesDir);
|
|
8878
8913
|
const top = all.filter(({ memory: memory2 }) => {
|
|
8879
8914
|
const s = memory2.frontmatter.status;
|
|
@@ -8899,7 +8934,7 @@ async function injectBridge(bridgeFile, memoriesDir, maxMemories, root, quiet) {
|
|
|
8899
8934
|
` + block + `
|
|
8900
8935
|
|
|
8901
8936
|
${BRIDGE_END}`;
|
|
8902
|
-
const fileExists =
|
|
8937
|
+
const fileExists = existsSync30(bridgeFile);
|
|
8903
8938
|
let existing = fileExists ? await readFile9(bridgeFile, "utf8") : "";
|
|
8904
8939
|
existing = existing.replace(/\r\n/g, "\n");
|
|
8905
8940
|
const startIdx = existing.indexOf(BRIDGE_START);
|
|
@@ -8950,7 +8985,7 @@ function collectSinceChanges(root, ref) {
|
|
|
8950
8985
|
// src/commands/memory-add.ts
|
|
8951
8986
|
import { createHash as createHash2 } from "crypto";
|
|
8952
8987
|
import { mkdir as mkdir11, readFile as readFile10, writeFile as writeFile14 } from "fs/promises";
|
|
8953
|
-
import { existsSync as
|
|
8988
|
+
import { existsSync as existsSync31 } from "fs";
|
|
8954
8989
|
import path16 from "path";
|
|
8955
8990
|
import "commander";
|
|
8956
8991
|
import {
|
|
@@ -8992,7 +9027,7 @@ function registerMemoryAdd(memory2) {
|
|
|
8992
9027
|
).requiredOption("--type <type>", "skill | convention | decision | gotcha | architecture | glossary | attempt").option("--slug <slug>", "short kebab-case identifier used in the file name (auto-derived from --title/--body when omitted)").option("--title <text>", "memory title \u2014 becomes the first heading of the body").option("--scope <scope>", "personal | team | module (default: config default; team in autopilot)").option("--module <name>", "module name (required when scope=module)").option("--tags <csv>", "comma-separated tags for easier retrieval").option("--domain <domain>", "domain (e.g. transactions)").option("--author <author>", "author email or handle").option("--paths <csv>", "anchor to source files \u2014 used for staleness detection by haive sync").option("--files <csv>", "alias for --paths (matches the MCP `files` parameter)").option("--symbols <csv>", "anchor to specific symbols (class/function names)").option("--commit <sha>", "anchor to a specific commit SHA").option("--body <text>", "memory body content (Markdown) \u2014 overrides --title default body").option("--body-file <path>", "read memory body from a Markdown file \u2014 for long content").option("--no-auto-tag", "disable automatic tag suggestions inferred from anchor paths").option("--topic <key>", "stable key for upsert: if a memory with this topic+scope already exists, update it in-place (revision_count++)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
8993
9028
|
const root = findProjectRoot13(opts.dir);
|
|
8994
9029
|
const paths = resolveHaivePaths10(root);
|
|
8995
|
-
if (!
|
|
9030
|
+
if (!existsSync31(paths.haiveDir)) {
|
|
8996
9031
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
8997
9032
|
process.exitCode = 1;
|
|
8998
9033
|
return;
|
|
@@ -9004,7 +9039,7 @@ function registerMemoryAdd(memory2) {
|
|
|
9004
9039
|
const inferredTags = autoTagsEnabled ? inferModulesFromPaths3(anchorPaths) : [];
|
|
9005
9040
|
const mergedTags = Array.from(/* @__PURE__ */ new Set([...userTags, ...inferredTags]));
|
|
9006
9041
|
if (anchorPaths.length > 0) {
|
|
9007
|
-
const missing = anchorPaths.filter((p) => !
|
|
9042
|
+
const missing = anchorPaths.filter((p) => !existsSync31(path16.resolve(root, p)));
|
|
9008
9043
|
if (missing.length > 0) {
|
|
9009
9044
|
ui.warn(`Anchor path${missing.length > 1 ? "s" : ""} not found in project:`);
|
|
9010
9045
|
for (const p of missing) ui.warn(` \u2717 ${p}`);
|
|
@@ -9017,7 +9052,7 @@ function registerMemoryAdd(memory2) {
|
|
|
9017
9052
|
const slug = slugify(opts.slug ?? opts.title ?? opts.topic ?? opts.body ?? `${opts.type}-memory`);
|
|
9018
9053
|
let body;
|
|
9019
9054
|
if (opts.bodyFile !== void 0) {
|
|
9020
|
-
if (!
|
|
9055
|
+
if (!existsSync31(opts.bodyFile)) {
|
|
9021
9056
|
ui.error(`--body-file not found: ${opts.bodyFile}`);
|
|
9022
9057
|
process.exitCode = 1;
|
|
9023
9058
|
return;
|
|
@@ -9033,7 +9068,7 @@ TODO \u2014 write the memory body.
|
|
|
9033
9068
|
`;
|
|
9034
9069
|
}
|
|
9035
9070
|
const scope = opts.scope ?? config.defaultScope ?? "personal";
|
|
9036
|
-
if (
|
|
9071
|
+
if (existsSync31(paths.memoriesDir)) {
|
|
9037
9072
|
const incomingHash = createHash2("sha256").update(body.trim()).digest("hex").slice(0, 12);
|
|
9038
9073
|
const allForHash = await loadMemoriesFromDir24(paths.memoriesDir);
|
|
9039
9074
|
const hashDup = allForHash.find(
|
|
@@ -9046,7 +9081,7 @@ TODO \u2014 write the memory body.
|
|
|
9046
9081
|
return;
|
|
9047
9082
|
}
|
|
9048
9083
|
}
|
|
9049
|
-
if (opts.topic &&
|
|
9084
|
+
if (opts.topic && existsSync31(paths.memoriesDir)) {
|
|
9050
9085
|
const existing = await loadMemoriesFromDir24(paths.memoriesDir);
|
|
9051
9086
|
const topicMatch = existing.find(
|
|
9052
9087
|
({ memory: memory3 }) => memory3.frontmatter.topic === opts.topic && memory3.frontmatter.scope === scope && (!opts.module || memory3.frontmatter.module === opts.module)
|
|
@@ -9091,12 +9126,12 @@ TODO \u2014 write the memory body.
|
|
|
9091
9126
|
});
|
|
9092
9127
|
const file = memoryFilePath6(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
9093
9128
|
await mkdir11(path16.dirname(file), { recursive: true });
|
|
9094
|
-
if (
|
|
9129
|
+
if (existsSync31(file)) {
|
|
9095
9130
|
ui.error(`Memory already exists at ${file}`);
|
|
9096
9131
|
process.exitCode = 1;
|
|
9097
9132
|
return;
|
|
9098
9133
|
}
|
|
9099
|
-
if (
|
|
9134
|
+
if (existsSync31(paths.memoriesDir)) {
|
|
9100
9135
|
const existing = await loadMemoriesFromDir24(paths.memoriesDir);
|
|
9101
9136
|
const slugTokens = slug.toLowerCase().split(/[-_\s]+/).filter(Boolean);
|
|
9102
9137
|
const similar = existing.filter(({ memory: memory3 }) => {
|
|
@@ -9188,7 +9223,7 @@ function slugify(value) {
|
|
|
9188
9223
|
}
|
|
9189
9224
|
|
|
9190
9225
|
// src/commands/memory-list.ts
|
|
9191
|
-
import { existsSync as
|
|
9226
|
+
import { existsSync as existsSync33 } from "fs";
|
|
9192
9227
|
import path17 from "path";
|
|
9193
9228
|
import "commander";
|
|
9194
9229
|
import { findProjectRoot as findProjectRoot14, resolveHaivePaths as resolveHaivePaths11 } from "@hiveai/core";
|
|
@@ -9205,7 +9240,7 @@ function registerMemoryList(memory2) {
|
|
|
9205
9240
|
memory2.command("list").description("List memories with optional filters").option("--scope <scope>", "personal | team | module").option("--type <type>", "filter by type").option("--tag <tag>", "filter by tag").option("--module <name>", "filter by module name").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected,deprecated)").option("--show-rejected", "include rejected memories (hidden by default)").option("--limit <n>", "max memories to display").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9206
9241
|
const root = findProjectRoot14(opts.dir);
|
|
9207
9242
|
const paths = resolveHaivePaths11(root);
|
|
9208
|
-
if (!
|
|
9243
|
+
if (!existsSync33(paths.memoriesDir)) {
|
|
9209
9244
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
9210
9245
|
process.exitCode = 1;
|
|
9211
9246
|
return;
|
|
@@ -9280,7 +9315,7 @@ function matchesFilters(loaded, opts) {
|
|
|
9280
9315
|
|
|
9281
9316
|
// src/commands/memory-promote.ts
|
|
9282
9317
|
import { mkdir as mkdir12, unlink as unlink2, writeFile as writeFile15 } from "fs/promises";
|
|
9283
|
-
import { existsSync as
|
|
9318
|
+
import { existsSync as existsSync34 } from "fs";
|
|
9284
9319
|
import path18 from "path";
|
|
9285
9320
|
import "commander";
|
|
9286
9321
|
import {
|
|
@@ -9293,7 +9328,7 @@ function registerMemoryPromote(memory2) {
|
|
|
9293
9328
|
memory2.command("promote <id>").description("Promote a personal memory to team scope (status -> proposed)").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
9294
9329
|
const root = findProjectRoot15(opts.dir);
|
|
9295
9330
|
const paths = resolveHaivePaths12(root);
|
|
9296
|
-
if (!
|
|
9331
|
+
if (!existsSync34(paths.memoriesDir)) {
|
|
9297
9332
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
9298
9333
|
process.exitCode = 1;
|
|
9299
9334
|
return;
|
|
@@ -9338,7 +9373,7 @@ function registerMemoryPromote(memory2) {
|
|
|
9338
9373
|
}
|
|
9339
9374
|
|
|
9340
9375
|
// src/commands/memory-approve.ts
|
|
9341
|
-
import { existsSync as
|
|
9376
|
+
import { existsSync as existsSync35 } from "fs";
|
|
9342
9377
|
import { writeFile as writeFile16 } from "fs/promises";
|
|
9343
9378
|
import path19 from "path";
|
|
9344
9379
|
import "commander";
|
|
@@ -9351,7 +9386,7 @@ function registerMemoryApprove(memory2) {
|
|
|
9351
9386
|
memory2.command("approve [id]").description("Mark a memory as 'validated'. Use --all to bulk-approve all proposed/draft memories.").option("--all", "approve all proposed and draft memories at once").option("--pending", "approve all memories with status 'proposed'").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
9352
9387
|
const root = findProjectRoot16(opts.dir);
|
|
9353
9388
|
const paths = resolveHaivePaths13(root);
|
|
9354
|
-
if (!
|
|
9389
|
+
if (!existsSync35(paths.memoriesDir)) {
|
|
9355
9390
|
ui.error(`No .ai/memories at ${root}.`);
|
|
9356
9391
|
process.exitCode = 1;
|
|
9357
9392
|
return;
|
|
@@ -9410,7 +9445,7 @@ function registerMemoryApprove(memory2) {
|
|
|
9410
9445
|
|
|
9411
9446
|
// src/commands/memory-update.ts
|
|
9412
9447
|
import { readFile as readFile11, writeFile as writeFile17 } from "fs/promises";
|
|
9413
|
-
import { existsSync as
|
|
9448
|
+
import { existsSync as existsSync36 } from "fs";
|
|
9414
9449
|
import path20 from "path";
|
|
9415
9450
|
import "commander";
|
|
9416
9451
|
import {
|
|
@@ -9422,7 +9457,7 @@ function registerMemoryUpdate(memory2) {
|
|
|
9422
9457
|
memory2.command("update <id>").description("Update body, tags, or anchor of an existing memory (preserves id and usage history)").option("--type <type>", "change the memory type (convention | decision | gotcha | architecture | glossary | skill | attempt)").option("--title <text>", "new title \u2014 replaces the first heading of the body").option("--body <text>", "new Markdown body \u2014 replaces the existing body").option("--body-file <path>", "read new body from a Markdown file \u2014 for long content").option("--tags <csv>", "new tags, comma-separated \u2014 fully replaces existing tags").option("--paths <csv>", "new anchor paths, comma-separated").option("--files <csv>", "alias for --paths (matches the MCP `files` parameter)").option("--symbols <csv>", "new anchor symbols, comma-separated").option("--commit <sha>", "new anchor commit SHA").option("--domain <domain>", "new domain label").option("--author <author>", "new author handle or email").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
9423
9458
|
const root = findProjectRoot17(opts.dir);
|
|
9424
9459
|
const paths = resolveHaivePaths14(root);
|
|
9425
|
-
if (!
|
|
9460
|
+
if (!existsSync36(paths.memoriesDir)) {
|
|
9426
9461
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
9427
9462
|
process.exitCode = 1;
|
|
9428
9463
|
return;
|
|
@@ -9464,7 +9499,7 @@ function registerMemoryUpdate(memory2) {
|
|
|
9464
9499
|
if (opts.author !== void 0) updated.push("author");
|
|
9465
9500
|
let newBody;
|
|
9466
9501
|
if (opts.bodyFile !== void 0) {
|
|
9467
|
-
if (!
|
|
9502
|
+
if (!existsSync36(opts.bodyFile)) {
|
|
9468
9503
|
ui.error(`--body-file not found: ${opts.bodyFile}`);
|
|
9469
9504
|
process.exitCode = 1;
|
|
9470
9505
|
return;
|
|
@@ -9510,7 +9545,7 @@ function parseCsv3(value) {
|
|
|
9510
9545
|
|
|
9511
9546
|
// src/commands/memory-auto-promote.ts
|
|
9512
9547
|
import { writeFile as writeFile18 } from "fs/promises";
|
|
9513
|
-
import { existsSync as
|
|
9548
|
+
import { existsSync as existsSync37 } from "fs";
|
|
9514
9549
|
import path21 from "path";
|
|
9515
9550
|
import "commander";
|
|
9516
9551
|
import {
|
|
@@ -9530,7 +9565,7 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
9530
9565
|
).option("--apply", "actually write status=validated to disk (default: dry-run)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9531
9566
|
const root = findProjectRoot18(opts.dir);
|
|
9532
9567
|
const paths = resolveHaivePaths15(root);
|
|
9533
|
-
if (!
|
|
9568
|
+
if (!existsSync37(paths.memoriesDir)) {
|
|
9534
9569
|
ui.error(`No .ai/memories at ${root}.`);
|
|
9535
9570
|
process.exitCode = 1;
|
|
9536
9571
|
return;
|
|
@@ -9573,7 +9608,7 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
9573
9608
|
|
|
9574
9609
|
// src/commands/memory-edit.ts
|
|
9575
9610
|
import { spawn as spawn3 } from "child_process";
|
|
9576
|
-
import { existsSync as
|
|
9611
|
+
import { existsSync as existsSync38 } from "fs";
|
|
9577
9612
|
import { readFile as readFile12 } from "fs/promises";
|
|
9578
9613
|
import path23 from "path";
|
|
9579
9614
|
import "commander";
|
|
@@ -9586,7 +9621,7 @@ function registerMemoryEdit(memory2) {
|
|
|
9586
9621
|
memory2.command("edit <id>").description("Open a memory in $EDITOR and re-validate when you save").option("-e, --editor <cmd>", "editor command (defaults to $EDITOR or 'vi')").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
9587
9622
|
const root = findProjectRoot19(opts.dir);
|
|
9588
9623
|
const paths = resolveHaivePaths16(root);
|
|
9589
|
-
if (!
|
|
9624
|
+
if (!existsSync38(paths.memoriesDir)) {
|
|
9590
9625
|
ui.error(`No .ai/memories at ${root}.`);
|
|
9591
9626
|
process.exitCode = 1;
|
|
9592
9627
|
return;
|
|
@@ -9626,7 +9661,7 @@ function runEditor(editor, file) {
|
|
|
9626
9661
|
}
|
|
9627
9662
|
|
|
9628
9663
|
// src/commands/memory-for-files.ts
|
|
9629
|
-
import { existsSync as
|
|
9664
|
+
import { existsSync as existsSync39 } from "fs";
|
|
9630
9665
|
import path24 from "path";
|
|
9631
9666
|
import "commander";
|
|
9632
9667
|
import {
|
|
@@ -9642,7 +9677,7 @@ function registerMemoryForFiles(memory2) {
|
|
|
9642
9677
|
memory2.command("for-files <files...>").description("Show memories relevant to the given files (anchor overlap, module, domain)").option("-d, --dir <dir>", "project root").action(async (files, opts) => {
|
|
9643
9678
|
const root = findProjectRoot20(opts.dir);
|
|
9644
9679
|
const paths = resolveHaivePaths17(root);
|
|
9645
|
-
if (!
|
|
9680
|
+
if (!existsSync39(paths.memoriesDir)) {
|
|
9646
9681
|
ui.error(`No .ai/memories at ${root}.`);
|
|
9647
9682
|
process.exitCode = 1;
|
|
9648
9683
|
return;
|
|
@@ -9754,7 +9789,7 @@ function printGroup(root, label, loaded, usage) {
|
|
|
9754
9789
|
}
|
|
9755
9790
|
|
|
9756
9791
|
// src/commands/memory-hot.ts
|
|
9757
|
-
import { existsSync as
|
|
9792
|
+
import { existsSync as existsSync40 } from "fs";
|
|
9758
9793
|
import path25 from "path";
|
|
9759
9794
|
import "commander";
|
|
9760
9795
|
import {
|
|
@@ -9769,7 +9804,7 @@ function registerMemoryHot(memory2) {
|
|
|
9769
9804
|
).option("--threshold <n>", "minimum read_count to qualify (default: 3)", "3").option("--status <status>", "limit to one status (default: draft + proposed)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9770
9805
|
const root = findProjectRoot21(opts.dir);
|
|
9771
9806
|
const paths = resolveHaivePaths18(root);
|
|
9772
|
-
if (!
|
|
9807
|
+
if (!existsSync40(paths.memoriesDir)) {
|
|
9773
9808
|
ui.error(`No .ai/memories at ${root}.`);
|
|
9774
9809
|
process.exitCode = 1;
|
|
9775
9810
|
return;
|
|
@@ -9808,7 +9843,7 @@ function registerMemoryHot(memory2) {
|
|
|
9808
9843
|
|
|
9809
9844
|
// src/commands/memory-tried.ts
|
|
9810
9845
|
import { mkdir as mkdir13, writeFile as writeFile19 } from "fs/promises";
|
|
9811
|
-
import { existsSync as
|
|
9846
|
+
import { existsSync as existsSync41 } from "fs";
|
|
9812
9847
|
import path26 from "path";
|
|
9813
9848
|
import "commander";
|
|
9814
9849
|
import {
|
|
@@ -9838,7 +9873,7 @@ function registerMemoryTried(memory2) {
|
|
|
9838
9873
|
).requiredOption("--what <text>", "what approach was tried (short, descriptive title)").requiredOption("--why-failed <text>", "why it failed or should NOT be used (include the exact error if possible)").option("--instead <text>", "the correct approach to use instead").option("--scope <scope>", "personal | team | module", "personal").option("--module <name>", "module name (required when scope=module)").option("--tags <csv>", "comma-separated tags").option("--paths <csv>", "anchor paths, comma-separated").option("--files <csv>", "alias for --paths (matches the MCP `files` parameter)").option("--author <author>", "author email or handle").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9839
9874
|
const root = findProjectRoot22(opts.dir);
|
|
9840
9875
|
const paths = resolveHaivePaths19(root);
|
|
9841
|
-
if (!
|
|
9876
|
+
if (!existsSync41(paths.haiveDir)) {
|
|
9842
9877
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
9843
9878
|
process.exitCode = 1;
|
|
9844
9879
|
return;
|
|
@@ -9866,7 +9901,7 @@ function registerMemoryTried(memory2) {
|
|
|
9866
9901
|
}
|
|
9867
9902
|
const file = memoryFilePath8(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
9868
9903
|
await mkdir13(path26.dirname(file), { recursive: true });
|
|
9869
|
-
if (
|
|
9904
|
+
if (existsSync41(file)) {
|
|
9870
9905
|
ui.error(`Memory already exists at ${file}`);
|
|
9871
9906
|
process.exitCode = 1;
|
|
9872
9907
|
return;
|
|
@@ -9884,7 +9919,7 @@ function parseCsv4(value) {
|
|
|
9884
9919
|
|
|
9885
9920
|
// src/commands/memory-seed.ts
|
|
9886
9921
|
import { readFile as readFile13 } from "fs/promises";
|
|
9887
|
-
import { existsSync as
|
|
9922
|
+
import { existsSync as existsSync43 } from "fs";
|
|
9888
9923
|
import path27 from "path";
|
|
9889
9924
|
import "commander";
|
|
9890
9925
|
import {
|
|
@@ -9924,7 +9959,7 @@ function registerMemorySeed(memory2) {
|
|
|
9924
9959
|
}
|
|
9925
9960
|
return;
|
|
9926
9961
|
}
|
|
9927
|
-
if (!
|
|
9962
|
+
if (!existsSync43(paths.haiveDir)) {
|
|
9928
9963
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
9929
9964
|
process.exitCode = 1;
|
|
9930
9965
|
return;
|
|
@@ -9980,7 +10015,7 @@ function registerMemorySeed(memory2) {
|
|
|
9980
10015
|
}
|
|
9981
10016
|
|
|
9982
10017
|
// src/commands/memory-pending.ts
|
|
9983
|
-
import { existsSync as
|
|
10018
|
+
import { existsSync as existsSync44 } from "fs";
|
|
9984
10019
|
import path28 from "path";
|
|
9985
10020
|
import "commander";
|
|
9986
10021
|
import {
|
|
@@ -9993,7 +10028,7 @@ function registerMemoryPending(memory2) {
|
|
|
9993
10028
|
memory2.command("pending").description("List draft and proposed memories awaiting review (sorted by reads desc).\n\n draft = created but not yet activated \xB7 proposed = promoted, awaiting team validation").option("--scope <scope>", "filter by scope (personal | team | module)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9994
10029
|
const root = findProjectRoot24(opts.dir);
|
|
9995
10030
|
const paths = resolveHaivePaths21(root);
|
|
9996
|
-
if (!
|
|
10031
|
+
if (!existsSync44(paths.memoriesDir)) {
|
|
9997
10032
|
ui.error(`No .ai/memories at ${root}.`);
|
|
9998
10033
|
process.exitCode = 1;
|
|
9999
10034
|
return;
|
|
@@ -10051,7 +10086,7 @@ function registerMemoryPending(memory2) {
|
|
|
10051
10086
|
}
|
|
10052
10087
|
|
|
10053
10088
|
// src/commands/memory-query.ts
|
|
10054
|
-
import { existsSync as
|
|
10089
|
+
import { existsSync as existsSync45 } from "fs";
|
|
10055
10090
|
import path29 from "path";
|
|
10056
10091
|
import "commander";
|
|
10057
10092
|
import {
|
|
@@ -10068,7 +10103,7 @@ function registerMemoryQuery(memory2) {
|
|
|
10068
10103
|
memory2.command("query <text>").alias("search").description("Search memories by id, tag, or substring (AND, OR fallback). Alias: search").option("-d, --dir <dir>", "project root").option("--limit <n>", "max results", "20").option("--scope <scope>", "personal | team | module").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected)").option("--show-rejected", "include rejected memories (hidden by default)").action(async (text, opts) => {
|
|
10069
10104
|
const root = findProjectRoot25(opts.dir);
|
|
10070
10105
|
const paths = resolveHaivePaths22(root);
|
|
10071
|
-
if (!
|
|
10106
|
+
if (!existsSync45(paths.memoriesDir)) {
|
|
10072
10107
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
10073
10108
|
process.exitCode = 1;
|
|
10074
10109
|
return;
|
|
@@ -10127,7 +10162,7 @@ ${top.length} of ${matches.length} match${matches.length === 1 ? "" : "es"}`)
|
|
|
10127
10162
|
|
|
10128
10163
|
// src/commands/memory-reject.ts
|
|
10129
10164
|
import { writeFile as writeFile20 } from "fs/promises";
|
|
10130
|
-
import { existsSync as
|
|
10165
|
+
import { existsSync as existsSync46 } from "fs";
|
|
10131
10166
|
import "commander";
|
|
10132
10167
|
import {
|
|
10133
10168
|
findProjectRoot as findProjectRoot26,
|
|
@@ -10141,7 +10176,7 @@ function registerMemoryReject(memory2) {
|
|
|
10141
10176
|
memory2.command("reject <id>").description("Record a rejection (blocks auto-promotion and lowers confidence)").option("-r, --reason <reason>", "why this memory is being rejected").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
10142
10177
|
const root = findProjectRoot26(opts.dir);
|
|
10143
10178
|
const paths = resolveHaivePaths23(root);
|
|
10144
|
-
if (!
|
|
10179
|
+
if (!existsSync46(paths.memoriesDir)) {
|
|
10145
10180
|
ui.error(`No .ai/memories at ${root}.`);
|
|
10146
10181
|
process.exitCode = 1;
|
|
10147
10182
|
return;
|
|
@@ -10177,7 +10212,7 @@ function registerMemoryReject(memory2) {
|
|
|
10177
10212
|
}
|
|
10178
10213
|
|
|
10179
10214
|
// src/commands/memory-rm.ts
|
|
10180
|
-
import { existsSync as
|
|
10215
|
+
import { existsSync as existsSync47 } from "fs";
|
|
10181
10216
|
import { unlink as unlink3 } from "fs/promises";
|
|
10182
10217
|
import path30 from "path";
|
|
10183
10218
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
@@ -10192,7 +10227,7 @@ function registerMemoryRm(memory2) {
|
|
|
10192
10227
|
memory2.command("rm <id>").description("Delete a memory file (and its usage entry by default)").option("-y, --yes", "skip the confirmation prompt").option("--keep-usage", "do not remove the usage.json entry").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
10193
10228
|
const root = findProjectRoot27(opts.dir);
|
|
10194
10229
|
const paths = resolveHaivePaths24(root);
|
|
10195
|
-
if (!
|
|
10230
|
+
if (!existsSync47(paths.memoriesDir)) {
|
|
10196
10231
|
ui.error(`No .ai/memories at ${root}.`);
|
|
10197
10232
|
process.exitCode = 1;
|
|
10198
10233
|
return;
|
|
@@ -10228,7 +10263,7 @@ function registerMemoryRm(memory2) {
|
|
|
10228
10263
|
}
|
|
10229
10264
|
|
|
10230
10265
|
// src/commands/memory-show.ts
|
|
10231
|
-
import { existsSync as
|
|
10266
|
+
import { existsSync as existsSync48 } from "fs";
|
|
10232
10267
|
import { readFile as readFile14 } from "fs/promises";
|
|
10233
10268
|
import path31 from "path";
|
|
10234
10269
|
import "commander";
|
|
@@ -10243,7 +10278,7 @@ function registerMemoryShow(memory2) {
|
|
|
10243
10278
|
memory2.command("show <id>").description("Print a memory's frontmatter, body, and confidence/usage").option("--raw", "print the raw file contents instead of a summary").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
10244
10279
|
const root = findProjectRoot28(opts.dir);
|
|
10245
10280
|
const paths = resolveHaivePaths25(root);
|
|
10246
|
-
if (!
|
|
10281
|
+
if (!existsSync48(paths.memoriesDir)) {
|
|
10247
10282
|
ui.error(`No .ai/memories at ${root}.`);
|
|
10248
10283
|
process.exitCode = 1;
|
|
10249
10284
|
return;
|
|
@@ -10287,7 +10322,7 @@ function registerMemoryShow(memory2) {
|
|
|
10287
10322
|
}
|
|
10288
10323
|
|
|
10289
10324
|
// src/commands/memory-stats.ts
|
|
10290
|
-
import { existsSync as
|
|
10325
|
+
import { existsSync as existsSync49 } from "fs";
|
|
10291
10326
|
import path33 from "path";
|
|
10292
10327
|
import "commander";
|
|
10293
10328
|
import {
|
|
@@ -10301,7 +10336,7 @@ function registerMemoryStats(memory2) {
|
|
|
10301
10336
|
memory2.command("stats").description("Show usage stats and confidence levels per memory").option("--id <id>", "show stats for a single memory id").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10302
10337
|
const root = findProjectRoot29(opts.dir);
|
|
10303
10338
|
const paths = resolveHaivePaths26(root);
|
|
10304
|
-
if (!
|
|
10339
|
+
if (!existsSync49(paths.memoriesDir)) {
|
|
10305
10340
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
10306
10341
|
process.exitCode = 1;
|
|
10307
10342
|
return;
|
|
@@ -10333,7 +10368,7 @@ function registerMemoryStats(memory2) {
|
|
|
10333
10368
|
|
|
10334
10369
|
// src/commands/memory-verify.ts
|
|
10335
10370
|
import { writeFile as writeFile21 } from "fs/promises";
|
|
10336
|
-
import { existsSync as
|
|
10371
|
+
import { existsSync as existsSync50 } from "fs";
|
|
10337
10372
|
import path34 from "path";
|
|
10338
10373
|
import "commander";
|
|
10339
10374
|
import {
|
|
@@ -10348,7 +10383,7 @@ function registerMemoryVerify(memory2) {
|
|
|
10348
10383
|
).option("--id <id>", "verify a single memory by id").option("--all", "verify every memory (default if --id is omitted)").option("--update", "write status=stale or status=validated back to disk").option("--json", "emit machine-readable JSON (for CI / agents)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10349
10384
|
const root = findProjectRoot30(opts.dir);
|
|
10350
10385
|
const paths = resolveHaivePaths27(root);
|
|
10351
|
-
if (!
|
|
10386
|
+
if (!existsSync50(paths.memoriesDir)) {
|
|
10352
10387
|
if (opts.json) {
|
|
10353
10388
|
console.log(JSON.stringify({ error: "not-initialized", root }, null, 2));
|
|
10354
10389
|
} else {
|
|
@@ -10469,7 +10504,7 @@ function applyVerification2(mem, result) {
|
|
|
10469
10504
|
|
|
10470
10505
|
// src/commands/memory-import.ts
|
|
10471
10506
|
import { readFile as readFile15 } from "fs/promises";
|
|
10472
|
-
import { existsSync as
|
|
10507
|
+
import { existsSync as existsSync51 } from "fs";
|
|
10473
10508
|
import "commander";
|
|
10474
10509
|
import {
|
|
10475
10510
|
findProjectRoot as findProjectRoot31,
|
|
@@ -10481,12 +10516,12 @@ function registerMemoryImport(memory2) {
|
|
|
10481
10516
|
).requiredOption("--from <file>", "Markdown/text file to import from").option("--scope <scope>", "personal | team (default: team)", "team").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10482
10517
|
const root = findProjectRoot31(opts.dir);
|
|
10483
10518
|
const paths = resolveHaivePaths28(root);
|
|
10484
|
-
if (!
|
|
10519
|
+
if (!existsSync51(paths.haiveDir)) {
|
|
10485
10520
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
10486
10521
|
process.exitCode = 1;
|
|
10487
10522
|
return;
|
|
10488
10523
|
}
|
|
10489
|
-
if (!
|
|
10524
|
+
if (!existsSync51(opts.from)) {
|
|
10490
10525
|
ui.error(`File not found: ${opts.from}`);
|
|
10491
10526
|
process.exitCode = 1;
|
|
10492
10527
|
return;
|
|
@@ -10519,7 +10554,7 @@ function registerMemoryImport(memory2) {
|
|
|
10519
10554
|
}
|
|
10520
10555
|
|
|
10521
10556
|
// src/commands/memory-import-changelog.ts
|
|
10522
|
-
import { existsSync as
|
|
10557
|
+
import { existsSync as existsSync53 } from "fs";
|
|
10523
10558
|
import { readFile as readFile16, mkdir as mkdir14, writeFile as writeFile23 } from "fs/promises";
|
|
10524
10559
|
import path35 from "path";
|
|
10525
10560
|
import "commander";
|
|
@@ -10594,7 +10629,7 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
10594
10629
|
const root = findProjectRoot32(opts.dir);
|
|
10595
10630
|
const paths = resolveHaivePaths29(root);
|
|
10596
10631
|
const changelogPath = path35.resolve(root, opts.fromChangelog);
|
|
10597
|
-
if (!
|
|
10632
|
+
if (!existsSync53(changelogPath)) {
|
|
10598
10633
|
ui.error(`CHANGELOG not found: ${changelogPath}`);
|
|
10599
10634
|
process.exitCode = 1;
|
|
10600
10635
|
return;
|
|
@@ -10681,7 +10716,7 @@ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${p
|
|
|
10681
10716
|
}
|
|
10682
10717
|
|
|
10683
10718
|
// src/commands/memory-digest.ts
|
|
10684
|
-
import { existsSync as
|
|
10719
|
+
import { existsSync as existsSync54 } from "fs";
|
|
10685
10720
|
import { writeFile as writeFile24 } from "fs/promises";
|
|
10686
10721
|
import path36 from "path";
|
|
10687
10722
|
import "commander";
|
|
@@ -10706,7 +10741,7 @@ function registerMemoryDigest(program2) {
|
|
|
10706
10741
|
).option("--days <n>", "look-back window in days (default: 7)", "7").option("--scope <scope>", "personal | team | module | all (default: team)", "team").option("--out <file>", "write digest to a file instead of stdout").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10707
10742
|
const root = findProjectRoot33(opts.dir);
|
|
10708
10743
|
const paths = resolveHaivePaths30(root);
|
|
10709
|
-
if (!
|
|
10744
|
+
if (!existsSync54(paths.memoriesDir)) {
|
|
10710
10745
|
ui.error("No .ai/memories found. Run `haive init` first.");
|
|
10711
10746
|
process.exitCode = 1;
|
|
10712
10747
|
return;
|
|
@@ -10789,7 +10824,7 @@ function registerMemoryDigest(program2) {
|
|
|
10789
10824
|
|
|
10790
10825
|
// src/commands/session-end.ts
|
|
10791
10826
|
import { writeFile as writeFile25, mkdir as mkdir15, readFile as readFile17, rm as rm2 } from "fs/promises";
|
|
10792
|
-
import { existsSync as
|
|
10827
|
+
import { existsSync as existsSync55 } from "fs";
|
|
10793
10828
|
import { spawn as spawn4 } from "child_process";
|
|
10794
10829
|
import path37 from "path";
|
|
10795
10830
|
import "commander";
|
|
@@ -10803,7 +10838,7 @@ import {
|
|
|
10803
10838
|
} from "@hiveai/core";
|
|
10804
10839
|
async function buildAutoRecap(paths) {
|
|
10805
10840
|
const obsFile = path37.join(paths.haiveDir, ".cache", "observations.jsonl");
|
|
10806
|
-
if (!
|
|
10841
|
+
if (!existsSync55(obsFile)) return await buildGitAutoRecap(paths);
|
|
10807
10842
|
const raw = await readFile17(obsFile, "utf8").catch(() => "");
|
|
10808
10843
|
if (!raw.trim()) return await buildGitAutoRecap(paths);
|
|
10809
10844
|
const lines = raw.split("\n").filter(Boolean);
|
|
@@ -10994,7 +11029,7 @@ function registerSessionEnd(session2) {
|
|
|
10994
11029
|
).option("--goal <text>", "what you were trying to accomplish (1\u20132 sentences)").option("--accomplished <text>", "what was actually done (bullet list recommended)").option("--discoveries <text>", "bugs, surprises, or inconsistencies found during this session").option("--files <csv>", "key files touched, comma-separated (used as anchor for staleness detection)").option("--next <text>", "what should happen next (for the next session or a teammate)").option("--scope <scope>", "personal | team | module (default: personal)", "personal").option("--module <name>", "module name (required when scope=module)").option("--auto", "synthesize the recap from .ai/.cache/observations.jsonl (used by Claude Code SessionEnd hook)").option("--quiet", "suppress non-error output (for hook use)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10995
11030
|
const root = findProjectRoot34(opts.dir);
|
|
10996
11031
|
const paths = resolveHaivePaths31(root);
|
|
10997
|
-
if (!
|
|
11032
|
+
if (!existsSync55(paths.haiveDir)) {
|
|
10998
11033
|
if (opts.auto || opts.quiet) return;
|
|
10999
11034
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
11000
11035
|
process.exitCode = 1;
|
|
@@ -11027,7 +11062,7 @@ function registerSessionEnd(session2) {
|
|
|
11027
11062
|
});
|
|
11028
11063
|
const topic = recapTopic2(scope, opts.module);
|
|
11029
11064
|
const filesTouched = parseCsv5(resolvedFiles).map((p) => normalizeAnchorPath(root, p));
|
|
11030
|
-
const missingPaths = filesTouched.filter((p) => !
|
|
11065
|
+
const missingPaths = filesTouched.filter((p) => !existsSync55(path37.resolve(root, p)));
|
|
11031
11066
|
if (missingPaths.length > 0 && !opts.quiet) {
|
|
11032
11067
|
ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
|
|
11033
11068
|
for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
|
|
@@ -11035,10 +11070,10 @@ function registerSessionEnd(session2) {
|
|
|
11035
11070
|
const cleanupObservations = async () => {
|
|
11036
11071
|
if (!opts.auto) return;
|
|
11037
11072
|
const obsFile = path37.join(paths.haiveDir, ".cache", "observations.jsonl");
|
|
11038
|
-
if (
|
|
11073
|
+
if (existsSync55(obsFile)) await rm2(obsFile).catch(() => {
|
|
11039
11074
|
});
|
|
11040
11075
|
};
|
|
11041
|
-
if (
|
|
11076
|
+
if (existsSync55(paths.memoriesDir)) {
|
|
11042
11077
|
const existing = await loadMemoriesFromDir27(paths.memoriesDir);
|
|
11043
11078
|
const topicMatch = existing.find(
|
|
11044
11079
|
({ memory: memory2 }) => memory2.frontmatter.topic === topic && memory2.frontmatter.scope === scope && (!opts.module || memory2.frontmatter.module === opts.module)
|
|
@@ -11100,7 +11135,7 @@ function normalizeAnchorPath(root, filePath) {
|
|
|
11100
11135
|
}
|
|
11101
11136
|
|
|
11102
11137
|
// src/commands/snapshot.ts
|
|
11103
|
-
import { existsSync as
|
|
11138
|
+
import { existsSync as existsSync56 } from "fs";
|
|
11104
11139
|
import { readdir as readdir4 } from "fs/promises";
|
|
11105
11140
|
import path38 from "path";
|
|
11106
11141
|
import "commander";
|
|
@@ -11135,14 +11170,14 @@ function registerSnapshot(program2) {
|
|
|
11135
11170
|
).option("--diff", "compare the contract against its stored snapshot").option("--list", "list all stored contract snapshots").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
11136
11171
|
const root = findProjectRoot35(opts.dir);
|
|
11137
11172
|
const paths = resolveHaivePaths32(root);
|
|
11138
|
-
if (!
|
|
11173
|
+
if (!existsSync56(paths.haiveDir)) {
|
|
11139
11174
|
ui.error("No .ai/ found. Run `haive init` first.");
|
|
11140
11175
|
process.exitCode = 1;
|
|
11141
11176
|
return;
|
|
11142
11177
|
}
|
|
11143
11178
|
if (opts.list) {
|
|
11144
11179
|
const contractsDir = path38.join(paths.haiveDir, "contracts");
|
|
11145
|
-
if (!
|
|
11180
|
+
if (!existsSync56(contractsDir)) {
|
|
11146
11181
|
console.log(ui.dim("No contract snapshots found."));
|
|
11147
11182
|
return;
|
|
11148
11183
|
}
|
|
@@ -11266,7 +11301,7 @@ function detectFormat(filePath) {
|
|
|
11266
11301
|
}
|
|
11267
11302
|
|
|
11268
11303
|
// src/commands/hub.ts
|
|
11269
|
-
import { existsSync as
|
|
11304
|
+
import { existsSync as existsSync57 } from "fs";
|
|
11270
11305
|
import { mkdir as mkdir16, readFile as readFile18, writeFile as writeFile26, copyFile } from "fs/promises";
|
|
11271
11306
|
import path39 from "path";
|
|
11272
11307
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
@@ -11368,7 +11403,7 @@ Next steps:
|
|
|
11368
11403
|
return;
|
|
11369
11404
|
}
|
|
11370
11405
|
const hubRoot = path39.resolve(root, config.hubPath);
|
|
11371
|
-
if (!
|
|
11406
|
+
if (!existsSync57(hubRoot)) {
|
|
11372
11407
|
ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
|
|
11373
11408
|
process.exitCode = 1;
|
|
11374
11409
|
return;
|
|
@@ -11438,7 +11473,7 @@ Next steps:
|
|
|
11438
11473
|
}
|
|
11439
11474
|
const hubRoot = path39.resolve(root, config.hubPath);
|
|
11440
11475
|
const hubSharedDir = path39.join(hubRoot, ".ai", "memories", "shared");
|
|
11441
|
-
if (!
|
|
11476
|
+
if (!existsSync57(hubSharedDir)) {
|
|
11442
11477
|
ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
|
|
11443
11478
|
return;
|
|
11444
11479
|
}
|
|
@@ -11493,7 +11528,7 @@ Next steps:
|
|
|
11493
11528
|
` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
|
|
11494
11529
|
);
|
|
11495
11530
|
const sharedDir = path39.join(paths.memoriesDir, "shared");
|
|
11496
|
-
if (
|
|
11531
|
+
if (existsSync57(sharedDir)) {
|
|
11497
11532
|
const { readdir: readdir7 } = await import("fs/promises");
|
|
11498
11533
|
const sources = (await readdir7(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
11499
11534
|
console.log(`
|
|
@@ -11522,7 +11557,7 @@ Next steps:
|
|
|
11522
11557
|
|
|
11523
11558
|
// src/commands/stats.ts
|
|
11524
11559
|
import "commander";
|
|
11525
|
-
import { existsSync as
|
|
11560
|
+
import { existsSync as existsSync58 } from "fs";
|
|
11526
11561
|
import { mkdir as mkdir17, writeFile as writeFile27 } from "fs/promises";
|
|
11527
11562
|
import path40 from "path";
|
|
11528
11563
|
import {
|
|
@@ -11600,7 +11635,7 @@ async function writeRoiReport(paths, root, sinceRaw, outRelative) {
|
|
|
11600
11635
|
const size = await usageLogSize(paths);
|
|
11601
11636
|
let events = await readUsageEvents2(paths);
|
|
11602
11637
|
let memoryCount = { team: 0, personal: 0, total_skipped_session: 0 };
|
|
11603
|
-
if (
|
|
11638
|
+
if (existsSync58(paths.memoriesDir)) {
|
|
11604
11639
|
const mems = await loadMemoriesFromDir29(paths.memoriesDir);
|
|
11605
11640
|
for (const { memory: memory2 } of mems) {
|
|
11606
11641
|
const fm = memory2.frontmatter;
|
|
@@ -11820,7 +11855,7 @@ function summarize(name, t0, payload, notes) {
|
|
|
11820
11855
|
}
|
|
11821
11856
|
|
|
11822
11857
|
// src/commands/benchmark.ts
|
|
11823
|
-
import { existsSync as
|
|
11858
|
+
import { existsSync as existsSync59 } from "fs";
|
|
11824
11859
|
import { readdir as readdir5, readFile as readFile19, writeFile as writeFile28 } from "fs/promises";
|
|
11825
11860
|
import path41 from "path";
|
|
11826
11861
|
import "commander";
|
|
@@ -11868,14 +11903,14 @@ function resolveBenchmarkRoot(dir) {
|
|
|
11868
11903
|
return path41.join(projectRoot, candidate);
|
|
11869
11904
|
}
|
|
11870
11905
|
async function collectRows(root) {
|
|
11871
|
-
if (!
|
|
11906
|
+
if (!existsSync59(root)) throw new Error(`Benchmark directory not found: ${root}`);
|
|
11872
11907
|
const entries = await readdir5(root, { withFileTypes: true });
|
|
11873
11908
|
const rows = [];
|
|
11874
11909
|
for (const entry of entries) {
|
|
11875
11910
|
if (!entry.isDirectory()) continue;
|
|
11876
11911
|
const fixtureDir = path41.join(root, entry.name);
|
|
11877
11912
|
const reportFile = path41.join(fixtureDir, "BENCHMARK_AGENT_REPORT.md");
|
|
11878
|
-
if (!
|
|
11913
|
+
if (!existsSync59(reportFile)) continue;
|
|
11879
11914
|
const report = await readFile19(reportFile, "utf8");
|
|
11880
11915
|
rows.push(parseAgentReport(entry.name, report));
|
|
11881
11916
|
}
|
|
@@ -11966,7 +12001,7 @@ function escapeRegExp(value) {
|
|
|
11966
12001
|
|
|
11967
12002
|
// src/commands/memory-suggest.ts
|
|
11968
12003
|
import { mkdir as mkdir18, writeFile as writeFile29 } from "fs/promises";
|
|
11969
|
-
import { existsSync as
|
|
12004
|
+
import { existsSync as existsSync60 } from "fs";
|
|
11970
12005
|
import path43 from "path";
|
|
11971
12006
|
import "commander";
|
|
11972
12007
|
import {
|
|
@@ -12045,7 +12080,7 @@ function registerMemorySuggest(memory2) {
|
|
|
12045
12080
|
}
|
|
12046
12081
|
const created = [];
|
|
12047
12082
|
const skipped = [];
|
|
12048
|
-
const existing =
|
|
12083
|
+
const existing = existsSync60(paths.memoriesDir) ? await loadMemoriesFromDir30(paths.memoriesDir) : [];
|
|
12049
12084
|
for (const s of top) {
|
|
12050
12085
|
const slug = slugify2(s.query);
|
|
12051
12086
|
if (!slug) {
|
|
@@ -12069,7 +12104,7 @@ function registerMemorySuggest(memory2) {
|
|
|
12069
12104
|
const body = renderTemplate(s, fm.id, status);
|
|
12070
12105
|
const file = memoryFilePath10(paths, fm.scope, fm.id, fm.module);
|
|
12071
12106
|
await mkdir18(path43.dirname(file), { recursive: true });
|
|
12072
|
-
if (
|
|
12107
|
+
if (existsSync60(file)) {
|
|
12073
12108
|
skipped.push({ query: s.query, reason: `file already exists at ${path43.relative(root, file)}` });
|
|
12074
12109
|
continue;
|
|
12075
12110
|
}
|
|
@@ -12172,7 +12207,7 @@ function truncate2(text, max) {
|
|
|
12172
12207
|
}
|
|
12173
12208
|
|
|
12174
12209
|
// src/commands/memory-archive.ts
|
|
12175
|
-
import { existsSync as
|
|
12210
|
+
import { existsSync as existsSync61 } from "fs";
|
|
12176
12211
|
import { writeFile as writeFile30 } from "fs/promises";
|
|
12177
12212
|
import path44 from "path";
|
|
12178
12213
|
import "commander";
|
|
@@ -12193,7 +12228,7 @@ function registerMemoryArchive(memory2) {
|
|
|
12193
12228
|
).option("--since <window>", "minimum age since last read (e.g. '180d', '6m'). Default: enforcement.decayAfterDays or 180d").option("--type <type>", "limit to a memory type (default 'attempt'). Pass 'all' to scan all types.", "attempt").option("--unread", "decay by unread-age ALONE (ignore anchor status) \u2014 more aggressive corpus hygiene", false).option("--apply", "actually rewrite files (default: dry run)", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
12194
12229
|
const root = findProjectRoot41(opts.dir);
|
|
12195
12230
|
const paths = resolveHaivePaths37(root);
|
|
12196
|
-
if (!
|
|
12231
|
+
if (!existsSync61(paths.memoriesDir)) {
|
|
12197
12232
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
12198
12233
|
process.exitCode = 1;
|
|
12199
12234
|
return;
|
|
@@ -12218,7 +12253,7 @@ function registerMemoryArchive(memory2) {
|
|
|
12218
12253
|
if (fm.status === "deprecated" || fm.status === "rejected") continue;
|
|
12219
12254
|
const retired = retirementSignal2(fm, mem.body);
|
|
12220
12255
|
const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
|
|
12221
|
-
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !
|
|
12256
|
+
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync61(path44.join(paths.root, p)));
|
|
12222
12257
|
const isAnchorless = !hasAnyAnchor;
|
|
12223
12258
|
if (!retired.retired && !opts.unread && !isAnchorless && !allPathsGone) continue;
|
|
12224
12259
|
const u = getUsage18(usage, fm.id);
|
|
@@ -12293,7 +12328,7 @@ function parseDays(input) {
|
|
|
12293
12328
|
}
|
|
12294
12329
|
|
|
12295
12330
|
// src/commands/doctor.ts
|
|
12296
|
-
import { existsSync as
|
|
12331
|
+
import { existsSync as existsSync63, statSync as statSync2 } from "fs";
|
|
12297
12332
|
import { readFile as readFile20, stat, writeFile as writeFile31 } from "fs/promises";
|
|
12298
12333
|
import path45 from "path";
|
|
12299
12334
|
import { execFileSync, execSync as execSync3 } from "child_process";
|
|
@@ -12302,6 +12337,7 @@ import {
|
|
|
12302
12337
|
codeMapPath as codeMapPath2,
|
|
12303
12338
|
findProjectRoot as findProjectRoot42,
|
|
12304
12339
|
getUsage as getUsage19,
|
|
12340
|
+
isStackPackSeed as isStackPackSeed4,
|
|
12305
12341
|
loadCodeMap as loadCodeMap7,
|
|
12306
12342
|
loadConfig as loadConfig11,
|
|
12307
12343
|
loadMemoriesFromDir as loadMemoriesFromDir33,
|
|
@@ -12319,7 +12355,7 @@ function registerDoctor(program2) {
|
|
|
12319
12355
|
const findings = [];
|
|
12320
12356
|
const repairs = [];
|
|
12321
12357
|
const config = await loadConfig11(paths);
|
|
12322
|
-
if (!
|
|
12358
|
+
if (!existsSync63(paths.haiveDir)) {
|
|
12323
12359
|
findings.push({
|
|
12324
12360
|
severity: "error",
|
|
12325
12361
|
code: "not-initialized",
|
|
@@ -12340,7 +12376,7 @@ function registerDoctor(program2) {
|
|
|
12340
12376
|
})
|
|
12341
12377
|
);
|
|
12342
12378
|
}
|
|
12343
|
-
if (!
|
|
12379
|
+
if (!existsSync63(paths.projectContext)) {
|
|
12344
12380
|
findings.push({
|
|
12345
12381
|
severity: "warn",
|
|
12346
12382
|
code: "no-project-context",
|
|
@@ -12369,7 +12405,7 @@ function registerDoctor(program2) {
|
|
|
12369
12405
|
});
|
|
12370
12406
|
}
|
|
12371
12407
|
}
|
|
12372
|
-
const memories =
|
|
12408
|
+
const memories = existsSync63(paths.memoriesDir) ? await loadMemoriesFromDir33(paths.memoriesDir) : [];
|
|
12373
12409
|
const now = Date.now();
|
|
12374
12410
|
if (memories.length === 0) {
|
|
12375
12411
|
findings.push({
|
|
@@ -12413,16 +12449,25 @@ function registerDoctor(program2) {
|
|
|
12413
12449
|
fix: "haive memory approve <id> # activate\nhaive memory rm <id> # or delete if obsolete"
|
|
12414
12450
|
});
|
|
12415
12451
|
}
|
|
12416
|
-
const
|
|
12452
|
+
const policyMemories = memories.filter((m) => !isStackPackSeed4(m.memory.frontmatter));
|
|
12453
|
+
const anchorless = policyMemories.filter(
|
|
12417
12454
|
(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"
|
|
12418
12455
|
);
|
|
12419
|
-
|
|
12456
|
+
const stackSeeds = memories.filter((m) => isStackPackSeed4(m.memory.frontmatter));
|
|
12457
|
+
if (anchorless.length / Math.max(policyMemories.length, 1) > 0.3) {
|
|
12420
12458
|
findings.push({
|
|
12421
12459
|
severity: "warn",
|
|
12422
12460
|
code: "anchorless-majority",
|
|
12423
|
-
message: `${anchorless.length}/${
|
|
12461
|
+
message: `${anchorless.length}/${policyMemories.length} repo-specific memories have no anchor path/symbol \u2014 staleness undetectable.`,
|
|
12424
12462
|
fix: "Add `paths:` + `symbols:` to mem_save calls to enable haive memory verify."
|
|
12425
12463
|
});
|
|
12464
|
+
} else if (stackSeeds.length > 0 && policyMemories.length === 0) {
|
|
12465
|
+
findings.push({
|
|
12466
|
+
severity: "info",
|
|
12467
|
+
code: "stack-pack-seeds",
|
|
12468
|
+
message: `${stackSeeds.length} starter stack memor${stackSeeds.length === 1 ? "y is" : "ies are"} present as generic background guidance.`,
|
|
12469
|
+
fix: "Replace or anchor stack-pack seeds when they become repo-specific policy."
|
|
12470
|
+
});
|
|
12426
12471
|
}
|
|
12427
12472
|
const decayCandidates = memories.filter((m) => {
|
|
12428
12473
|
if (m.memory.frontmatter.status !== "validated") return false;
|
|
@@ -12512,7 +12557,7 @@ function registerDoctor(program2) {
|
|
|
12512
12557
|
if (config.enforcement?.requireBriefingFirst) {
|
|
12513
12558
|
const claudeSettings = path45.join(root, ".claude", "settings.local.json");
|
|
12514
12559
|
let hasClaudeEnforcement = false;
|
|
12515
|
-
if (
|
|
12560
|
+
if (existsSync63(claudeSettings)) {
|
|
12516
12561
|
try {
|
|
12517
12562
|
const { readFile: readFile24 } = await import("fs/promises");
|
|
12518
12563
|
const raw = await readFile24(claudeSettings, "utf8");
|
|
@@ -12538,14 +12583,14 @@ function registerDoctor(program2) {
|
|
|
12538
12583
|
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
|
|
12539
12584
|
});
|
|
12540
12585
|
}
|
|
12541
|
-
findings.push(...await collectInstallFindings(root, "0.10.
|
|
12586
|
+
findings.push(...await collectInstallFindings(root, "0.10.9"));
|
|
12542
12587
|
try {
|
|
12543
12588
|
const legacyRaw = execSync3("haive-mcp --version", {
|
|
12544
12589
|
encoding: "utf8",
|
|
12545
12590
|
timeout: 3e3,
|
|
12546
12591
|
stdio: ["ignore", "pipe", "ignore"]
|
|
12547
12592
|
}).trim();
|
|
12548
|
-
const cliVersion = "0.10.
|
|
12593
|
+
const cliVersion = "0.10.9";
|
|
12549
12594
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
12550
12595
|
findings.push({
|
|
12551
12596
|
severity: "warn",
|
|
@@ -12567,7 +12612,7 @@ npm uninstall -g @hiveai/mcp`
|
|
|
12567
12612
|
];
|
|
12568
12613
|
const staleConfigs = [];
|
|
12569
12614
|
for (const cfgPath of configPaths) {
|
|
12570
|
-
if (!
|
|
12615
|
+
if (!existsSync63(cfgPath)) continue;
|
|
12571
12616
|
try {
|
|
12572
12617
|
const raw = await readFile20(cfgPath, "utf8");
|
|
12573
12618
|
if (raw.includes('"haive-mcp"') || raw.includes("'haive-mcp'")) {
|
|
@@ -12862,7 +12907,7 @@ which -a haive`
|
|
|
12862
12907
|
];
|
|
12863
12908
|
for (const rel of integrationFiles) {
|
|
12864
12909
|
const file = path45.join(root, rel);
|
|
12865
|
-
if (!
|
|
12910
|
+
if (!existsSync63(file)) continue;
|
|
12866
12911
|
const text = await readFile20(file, "utf8").catch(() => "");
|
|
12867
12912
|
for (const bin of extractAbsoluteHaiveBins(text)) {
|
|
12868
12913
|
const version = versionForBinary(bin);
|
|
@@ -12958,7 +13003,7 @@ function collectGlobalHivemoduleFindings(expectedVersion) {
|
|
|
12958
13003
|
}
|
|
12959
13004
|
}
|
|
12960
13005
|
async function readJson(file) {
|
|
12961
|
-
if (!
|
|
13006
|
+
if (!existsSync63(file)) return null;
|
|
12962
13007
|
try {
|
|
12963
13008
|
return JSON.parse(await readFile20(file, "utf8"));
|
|
12964
13009
|
} catch {
|
|
@@ -13005,7 +13050,7 @@ function extractAbsoluteHaiveBins(text) {
|
|
|
13005
13050
|
}
|
|
13006
13051
|
|
|
13007
13052
|
// src/commands/playback.ts
|
|
13008
|
-
import { existsSync as
|
|
13053
|
+
import { existsSync as existsSync64 } from "fs";
|
|
13009
13054
|
import "commander";
|
|
13010
13055
|
import {
|
|
13011
13056
|
findProjectRoot as findProjectRoot43,
|
|
@@ -13035,7 +13080,7 @@ function registerPlayback(program2) {
|
|
|
13035
13080
|
const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
|
|
13036
13081
|
const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
|
|
13037
13082
|
const sessions = bucketSessions(filtered, gapMs);
|
|
13038
|
-
const all =
|
|
13083
|
+
const all = existsSync64(paths.memoriesDir) ? await loadMemoriesFromDir34(paths.memoriesDir) : [];
|
|
13039
13084
|
const memByCreatedAt = all.filter(({ memory: memory2 }) => memory2.frontmatter.type !== "session_recap").map(({ memory: memory2 }) => ({ id: memory2.frontmatter.id, at: Date.parse(memory2.frontmatter.created_at) })).sort((a, b) => a.at - b.at);
|
|
13040
13085
|
const enriched = sessions.map((s, i) => {
|
|
13041
13086
|
const startMs = Date.parse(s.start);
|
|
@@ -13277,7 +13322,7 @@ function runCommand3(cmd, args, cwd) {
|
|
|
13277
13322
|
}
|
|
13278
13323
|
|
|
13279
13324
|
// src/commands/welcome.ts
|
|
13280
|
-
import { existsSync as
|
|
13325
|
+
import { existsSync as existsSync65 } from "fs";
|
|
13281
13326
|
import "commander";
|
|
13282
13327
|
import {
|
|
13283
13328
|
findProjectRoot as findProjectRoot45,
|
|
@@ -13299,7 +13344,7 @@ function registerWelcome(program2) {
|
|
|
13299
13344
|
).option("--limit <n>", "maximum memories listed", "20").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
13300
13345
|
const root = findProjectRoot45(opts.dir);
|
|
13301
13346
|
const paths = resolveHaivePaths41(root);
|
|
13302
|
-
if (!
|
|
13347
|
+
if (!existsSync65(paths.memoriesDir)) {
|
|
13303
13348
|
ui.error(`No memories at ${paths.memoriesDir}. Run 'haive init' first.`);
|
|
13304
13349
|
process.exitCode = 1;
|
|
13305
13350
|
return;
|
|
@@ -13371,7 +13416,7 @@ function registerResolveProject(program2) {
|
|
|
13371
13416
|
}
|
|
13372
13417
|
|
|
13373
13418
|
// src/commands/runtime-journal.ts
|
|
13374
|
-
import { existsSync as
|
|
13419
|
+
import { existsSync as existsSync66 } from "fs";
|
|
13375
13420
|
import path47 from "path";
|
|
13376
13421
|
import "commander";
|
|
13377
13422
|
import {
|
|
@@ -13397,7 +13442,7 @@ function registerRuntime(program2) {
|
|
|
13397
13442
|
const root = path47.resolve(opts.dir ?? process.cwd());
|
|
13398
13443
|
const paths = resolveHaivePaths42(findProjectRoot46(root));
|
|
13399
13444
|
const limit = Math.min(500, Math.max(1, parseInt(opts.limit, 10) || 30));
|
|
13400
|
-
if (!
|
|
13445
|
+
if (!existsSync66(paths.haiveDir)) {
|
|
13401
13446
|
ui.error("No .ai/ \u2014 run `haive init` first.");
|
|
13402
13447
|
process.exitCode = 1;
|
|
13403
13448
|
return;
|
|
@@ -13412,7 +13457,7 @@ function registerRuntime(program2) {
|
|
|
13412
13457
|
}
|
|
13413
13458
|
|
|
13414
13459
|
// src/commands/memory-timeline.ts
|
|
13415
|
-
import { existsSync as
|
|
13460
|
+
import { existsSync as existsSync67 } from "fs";
|
|
13416
13461
|
import path48 from "path";
|
|
13417
13462
|
import "commander";
|
|
13418
13463
|
import {
|
|
@@ -13431,7 +13476,7 @@ function registerMemoryTimeline(memory2) {
|
|
|
13431
13476
|
}
|
|
13432
13477
|
const root = path48.resolve(opts.dir ?? process.cwd());
|
|
13433
13478
|
const paths = resolveHaivePaths43(findProjectRoot47(root));
|
|
13434
|
-
if (!
|
|
13479
|
+
if (!existsSync67(paths.memoriesDir)) {
|
|
13435
13480
|
ui.error("No memories \u2014 run `haive init`.");
|
|
13436
13481
|
process.exitCode = 1;
|
|
13437
13482
|
return;
|
|
@@ -13449,7 +13494,7 @@ function registerMemoryTimeline(memory2) {
|
|
|
13449
13494
|
}
|
|
13450
13495
|
|
|
13451
13496
|
// src/commands/memory-conflict-candidates.ts
|
|
13452
|
-
import { existsSync as
|
|
13497
|
+
import { existsSync as existsSync68 } from "fs";
|
|
13453
13498
|
import path49 from "path";
|
|
13454
13499
|
import "commander";
|
|
13455
13500
|
import {
|
|
@@ -13474,7 +13519,7 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
13474
13519
|
).option("--min-jaccard <x>", "minimum Jaccard for lexical pairs", "0.45").option("--max-pairs <n>", "cap lexical pairs", "20").option("--max-scan <n>", "max memories scanned (lexical)", "500").option("--max-topic-pairs <n>", "cap topic/status pairs", "20").action(async (opts) => {
|
|
13475
13520
|
const root = path49.resolve(opts.dir ?? process.cwd());
|
|
13476
13521
|
const paths = resolveHaivePaths44(findProjectRoot48(root));
|
|
13477
|
-
if (!
|
|
13522
|
+
if (!existsSync68(paths.memoriesDir)) {
|
|
13478
13523
|
ui.error("No memories \u2014 run `haive init`.");
|
|
13479
13524
|
process.exitCode = 1;
|
|
13480
13525
|
return;
|
|
@@ -13510,14 +13555,14 @@ function registerMemoryConflictCandidates(memory2) {
|
|
|
13510
13555
|
|
|
13511
13556
|
// src/commands/enforce.ts
|
|
13512
13557
|
import { execFileSync as execFileSync2, spawn as spawn6 } from "child_process";
|
|
13513
|
-
import { existsSync as
|
|
13558
|
+
import { existsSync as existsSync69, statSync as statSync3 } from "fs";
|
|
13514
13559
|
import { chmod as chmod2, mkdir as mkdir19, readFile as readFile21, readdir as readdir6, rm as rm3, writeFile as writeFile33 } from "fs/promises";
|
|
13515
13560
|
import path50 from "path";
|
|
13516
13561
|
import "commander";
|
|
13517
13562
|
import {
|
|
13518
13563
|
antiPatternGateParams as antiPatternGateParams2,
|
|
13519
13564
|
findProjectRoot as findProjectRoot49,
|
|
13520
|
-
hasRecentBriefingMarker,
|
|
13565
|
+
hasRecentBriefingMarker as hasRecentBriefingMarker2,
|
|
13521
13566
|
isFreshIsoDate,
|
|
13522
13567
|
loadConfig as loadConfig13,
|
|
13523
13568
|
loadMemoriesFromDir as loadMemoriesFromDir36,
|
|
@@ -13585,14 +13630,14 @@ function registerEnforce(program2) {
|
|
|
13585
13630
|
const root = findProjectRoot49(opts.dir);
|
|
13586
13631
|
const paths = resolveHaivePaths45(root);
|
|
13587
13632
|
const cacheDir = path50.join(paths.haiveDir, ".cache");
|
|
13588
|
-
if (
|
|
13633
|
+
if (existsSync69(cacheDir)) {
|
|
13589
13634
|
if (opts.dryRun) ui.info(`would clean ${path50.relative(root, cacheDir)} (preserving .gitignore)`);
|
|
13590
13635
|
else {
|
|
13591
13636
|
const removed = await cleanupCacheDir(cacheDir);
|
|
13592
13637
|
ui.success(`cleaned ${path50.relative(root, cacheDir)}${removed > 0 ? ` (${removed} item${removed === 1 ? "" : "s"} removed)` : ""}`);
|
|
13593
13638
|
}
|
|
13594
13639
|
}
|
|
13595
|
-
if (
|
|
13640
|
+
if (existsSync69(paths.runtimeDir)) {
|
|
13596
13641
|
if (opts.dryRun) ui.info(`would clean ${path50.relative(root, paths.runtimeDir)} (preserving briefing markers)`);
|
|
13597
13642
|
else {
|
|
13598
13643
|
const removed = await cleanupRuntimeDir(paths.runtimeDir);
|
|
@@ -13605,12 +13650,19 @@ function registerEnforce(program2) {
|
|
|
13605
13650
|
printReport(report, Boolean(opts.json), Boolean(opts.explain));
|
|
13606
13651
|
if (report.should_block) process.exit(2);
|
|
13607
13652
|
});
|
|
13653
|
+
enforce.command("finish").alias("completion").description(
|
|
13654
|
+
"Final agent-exit gate: verify the git sync/release protocol before reporting a task done."
|
|
13655
|
+
).option("-d, --dir <dir>", "project root").option("--explain", "group findings by blocking/review/info and show repair commands", false).option("--json", "emit JSON", false).action(async (opts) => {
|
|
13656
|
+
const report = await buildFinishReport(opts.dir);
|
|
13657
|
+
printReport(report, Boolean(opts.json), Boolean(opts.explain));
|
|
13658
|
+
if (report.should_block) process.exit(2);
|
|
13659
|
+
});
|
|
13608
13660
|
enforce.command("session-start").description("Claude Code SessionStart hook: inject briefing and write a local briefing marker.").option("-d, --dir <dir>", "project root").option("--task <text>", "task text to rank memories").option("--source <name>", "marker source", "claude-session-start").option("--session-id <id>", "agent session id").action(async (opts) => {
|
|
13609
13661
|
const payload = await readHookPayload();
|
|
13610
13662
|
const root = resolveRoot(opts.dir, payload);
|
|
13611
13663
|
if (!root) return;
|
|
13612
13664
|
const paths = resolveHaivePaths45(root);
|
|
13613
|
-
if (!
|
|
13665
|
+
if (!existsSync69(paths.haiveDir)) return;
|
|
13614
13666
|
await mkdir19(paths.runtimeDir, { recursive: true });
|
|
13615
13667
|
const sessionId = opts.sessionId ?? payload.session_id;
|
|
13616
13668
|
const task = opts.task ?? payload.prompt ?? "Start an AI coding session in this hAIve-initialized project.";
|
|
@@ -13673,9 +13725,9 @@ ${briefing.project_context.content.slice(0, 1800)}`);
|
|
|
13673
13725
|
const root = resolveRoot(opts.dir, payload);
|
|
13674
13726
|
if (!root) return;
|
|
13675
13727
|
const paths = resolveHaivePaths45(root);
|
|
13676
|
-
if (!
|
|
13728
|
+
if (!existsSync69(paths.haiveDir)) return;
|
|
13677
13729
|
if (!isWriteLikeTool(payload)) return;
|
|
13678
|
-
const ok = await
|
|
13730
|
+
const ok = await hasRecentBriefingMarker2(paths, payload.session_id);
|
|
13679
13731
|
if (ok) {
|
|
13680
13732
|
const targetFiles = extractToolPaths(payload, root);
|
|
13681
13733
|
if (targetFiles.length === 0) return;
|
|
@@ -13714,10 +13766,206 @@ ${briefing.project_context.content.slice(0, 1800)}`);
|
|
|
13714
13766
|
process.exit(2);
|
|
13715
13767
|
});
|
|
13716
13768
|
}
|
|
13769
|
+
async function buildFinishReport(dir) {
|
|
13770
|
+
const root = findProjectRoot49(dir);
|
|
13771
|
+
const paths = resolveHaivePaths45(root);
|
|
13772
|
+
const initialized = existsSync69(paths.haiveDir);
|
|
13773
|
+
const config = initialized ? await loadConfig13(paths) : {};
|
|
13774
|
+
const mode = config.enforcement?.mode ?? "strict";
|
|
13775
|
+
const findings = [];
|
|
13776
|
+
if (!initialized) {
|
|
13777
|
+
return withCategories({
|
|
13778
|
+
root,
|
|
13779
|
+
initialized,
|
|
13780
|
+
mode,
|
|
13781
|
+
score: buildScore([], config.enforcement?.scoreThreshold),
|
|
13782
|
+
should_block: true,
|
|
13783
|
+
findings: [{
|
|
13784
|
+
severity: "error",
|
|
13785
|
+
code: "not-initialized",
|
|
13786
|
+
message: "This repository is not initialized with hAIve.",
|
|
13787
|
+
fix: "Run `haive init` or `haive enforce install`.",
|
|
13788
|
+
impact: 100
|
|
13789
|
+
}]
|
|
13790
|
+
});
|
|
13791
|
+
}
|
|
13792
|
+
const status = await getGitSyncStatus(root);
|
|
13793
|
+
if (!status.available) {
|
|
13794
|
+
findings.push({
|
|
13795
|
+
severity: "error",
|
|
13796
|
+
code: "git-unavailable",
|
|
13797
|
+
message: "Git status could not be inspected, so hAIve cannot verify the exit protocol.",
|
|
13798
|
+
fix: "Run `git status` manually, then commit/push according to the hAIve git-sync protocol.",
|
|
13799
|
+
impact: 100
|
|
13800
|
+
});
|
|
13801
|
+
return finishReport(root, initialized, mode, findings, config);
|
|
13802
|
+
}
|
|
13803
|
+
const shippableDirty = status.dirtyFiles.filter(isShippablePath);
|
|
13804
|
+
if (status.dirtyFiles.length > 0) {
|
|
13805
|
+
findings.push({
|
|
13806
|
+
severity: "error",
|
|
13807
|
+
code: shippableDirty.length > 0 ? "git-sync-uncommitted-shippable" : "git-sync-uncommitted-changes",
|
|
13808
|
+
message: shippableDirty.length > 0 ? `${shippableDirty.length} shippable file(s) are modified but not committed.` : `${status.dirtyFiles.length} file(s) are modified but not committed.`,
|
|
13809
|
+
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 --tags`." : "Commit and push these changes before reporting the task done.",
|
|
13810
|
+
reason: "The multi-agent git-sync decision requires agents to leave completed work committed and pushed, not as a local diff.",
|
|
13811
|
+
affected_files: status.dirtyFiles.slice(0, 12),
|
|
13812
|
+
impact: 100
|
|
13813
|
+
});
|
|
13814
|
+
return finishReport(root, initialized, mode, findings, config);
|
|
13815
|
+
}
|
|
13816
|
+
findings.push({
|
|
13817
|
+
severity: "ok",
|
|
13818
|
+
code: "git-worktree-clean",
|
|
13819
|
+
message: "No uncommitted worktree changes remain."
|
|
13820
|
+
});
|
|
13821
|
+
if (!status.upstream) {
|
|
13822
|
+
findings.push({
|
|
13823
|
+
severity: "warn",
|
|
13824
|
+
code: "git-sync-no-upstream",
|
|
13825
|
+
message: "This branch has no upstream, so hAIve cannot verify that commits/tags were pushed.",
|
|
13826
|
+
fix: "Set an upstream with `git push -u origin <branch>`.",
|
|
13827
|
+
impact: 15
|
|
13828
|
+
});
|
|
13829
|
+
return finishReport(root, initialized, mode, findings, config);
|
|
13830
|
+
}
|
|
13831
|
+
if (status.behind > 0) {
|
|
13832
|
+
findings.push({
|
|
13833
|
+
severity: "error",
|
|
13834
|
+
code: "git-sync-behind-upstream",
|
|
13835
|
+
message: `This branch is ${status.behind} commit(s) behind ${status.upstream}.`,
|
|
13836
|
+
fix: "Run `git pull --ff-only` and resolve any conflicts before finishing.",
|
|
13837
|
+
impact: 40
|
|
13838
|
+
});
|
|
13839
|
+
}
|
|
13840
|
+
if (status.ahead > 0) {
|
|
13841
|
+
findings.push({
|
|
13842
|
+
severity: "error",
|
|
13843
|
+
code: "git-sync-unpushed-commits",
|
|
13844
|
+
message: `This branch is ${status.ahead} commit(s) ahead of ${status.upstream}.`,
|
|
13845
|
+
fix: "Run `git push` before reporting the task done.",
|
|
13846
|
+
reason: "The multi-agent git-sync decision requires agents to push completed commits.",
|
|
13847
|
+
impact: 60
|
|
13848
|
+
});
|
|
13849
|
+
} else {
|
|
13850
|
+
findings.push({
|
|
13851
|
+
severity: "ok",
|
|
13852
|
+
code: "git-sync-pushed",
|
|
13853
|
+
message: `Branch is not ahead of ${status.upstream}.`
|
|
13854
|
+
});
|
|
13855
|
+
}
|
|
13856
|
+
const releaseChangedFiles = status.releaseChangedFiles ?? status.changedSinceUpstream;
|
|
13857
|
+
const releaseBaseRef = status.releaseBaseRef ?? status.upstream;
|
|
13858
|
+
const shippableChanged = releaseChangedFiles.filter(isShippablePath);
|
|
13859
|
+
if (shippableChanged.length === 0) {
|
|
13860
|
+
findings.push({
|
|
13861
|
+
severity: "ok",
|
|
13862
|
+
code: "release-version-not-required",
|
|
13863
|
+
message: "No shippable package code changed since upstream; no version/tag required."
|
|
13864
|
+
});
|
|
13865
|
+
return finishReport(root, initialized, mode, findings, config);
|
|
13866
|
+
}
|
|
13867
|
+
findings.push({
|
|
13868
|
+
severity: "info",
|
|
13869
|
+
code: "release-shippable-changes",
|
|
13870
|
+
message: `${shippableChanged.length} shippable file(s) changed since ${releaseBaseRef}.`,
|
|
13871
|
+
affected_files: shippableChanged.slice(0, 12)
|
|
13872
|
+
});
|
|
13873
|
+
const versionState = await inspectReleaseVersionState(root, releaseBaseRef);
|
|
13874
|
+
if (!versionState.lockstep) {
|
|
13875
|
+
findings.push({
|
|
13876
|
+
severity: "error",
|
|
13877
|
+
code: "release-version-not-lockstep",
|
|
13878
|
+
message: `Publishable package versions are not in lockstep: ${versionState.localVersionsLabel}.`,
|
|
13879
|
+
fix: "Set root, core, cli, mcp, and embeddings package.json versions to the same X.Y.Z.",
|
|
13880
|
+
impact: 60
|
|
13881
|
+
});
|
|
13882
|
+
return finishReport(root, initialized, mode, findings, config);
|
|
13883
|
+
}
|
|
13884
|
+
const version = versionState.version;
|
|
13885
|
+
if (!version) {
|
|
13886
|
+
findings.push({
|
|
13887
|
+
severity: "error",
|
|
13888
|
+
code: "release-version-unreadable",
|
|
13889
|
+
message: "Could not read the lockstep package version.",
|
|
13890
|
+
fix: "Verify package.json files are valid JSON.",
|
|
13891
|
+
impact: 60
|
|
13892
|
+
});
|
|
13893
|
+
return finishReport(root, initialized, mode, findings, config);
|
|
13894
|
+
}
|
|
13895
|
+
if (versionState.baseVersion && compareSemver(version, versionState.baseVersion) <= 0) {
|
|
13896
|
+
findings.push({
|
|
13897
|
+
severity: "error",
|
|
13898
|
+
code: "release-version-missing",
|
|
13899
|
+
message: `Shippable code changed, but version stayed at ${version} (base: ${versionState.baseVersion}).`,
|
|
13900
|
+
fix: "Bump the lockstep package version (patch by default), commit the bump, tag it, then push code and tags.",
|
|
13901
|
+
impact: 70
|
|
13902
|
+
});
|
|
13903
|
+
} else {
|
|
13904
|
+
findings.push({
|
|
13905
|
+
severity: "ok",
|
|
13906
|
+
code: "release-version-bumped",
|
|
13907
|
+
message: versionState.baseVersion ? `Lockstep version bumped from ${versionState.baseVersion} to ${version}.` : `Lockstep version is ${version}.`
|
|
13908
|
+
});
|
|
13909
|
+
}
|
|
13910
|
+
const tag = `v${version}`;
|
|
13911
|
+
const localTagAtHead = await tagPointsAtHead(root, tag);
|
|
13912
|
+
if (!localTagAtHead) {
|
|
13913
|
+
findings.push({
|
|
13914
|
+
severity: "error",
|
|
13915
|
+
code: "release-tag-missing",
|
|
13916
|
+
message: `Expected git tag ${tag} to point at HEAD.`,
|
|
13917
|
+
fix: `Run \`git tag ${tag}\` after committing the version bump.`,
|
|
13918
|
+
impact: 50
|
|
13919
|
+
});
|
|
13920
|
+
} else {
|
|
13921
|
+
findings.push({
|
|
13922
|
+
severity: "ok",
|
|
13923
|
+
code: "release-tag-present",
|
|
13924
|
+
message: `Tag ${tag} points at HEAD.`
|
|
13925
|
+
});
|
|
13926
|
+
}
|
|
13927
|
+
const remoteTag = await remoteTagExists(root, tag);
|
|
13928
|
+
if (remoteTag === false) {
|
|
13929
|
+
findings.push({
|
|
13930
|
+
severity: "error",
|
|
13931
|
+
code: "release-tag-unpushed",
|
|
13932
|
+
message: `Tag ${tag} is not present on the remote.`,
|
|
13933
|
+
fix: "Run `git push --tags`.",
|
|
13934
|
+
impact: 50
|
|
13935
|
+
});
|
|
13936
|
+
} else if (remoteTag === true) {
|
|
13937
|
+
findings.push({
|
|
13938
|
+
severity: "ok",
|
|
13939
|
+
code: "release-tag-pushed",
|
|
13940
|
+
message: `Tag ${tag} exists on the remote.`
|
|
13941
|
+
});
|
|
13942
|
+
} else {
|
|
13943
|
+
findings.push({
|
|
13944
|
+
severity: "warn",
|
|
13945
|
+
code: "release-tag-remote-unverified",
|
|
13946
|
+
message: `Could not verify whether tag ${tag} exists on the remote.`,
|
|
13947
|
+
fix: "Run `git push --tags` if you have not already.",
|
|
13948
|
+
impact: 10
|
|
13949
|
+
});
|
|
13950
|
+
}
|
|
13951
|
+
return finishReport(root, initialized, mode, findings, config);
|
|
13952
|
+
}
|
|
13953
|
+
function finishReport(root, initialized, mode, findings, config) {
|
|
13954
|
+
const score = buildScore(findings, config.enforcement?.scoreThreshold);
|
|
13955
|
+
const hasErrors = findings.some((f) => f.severity === "error");
|
|
13956
|
+
return withCategories({
|
|
13957
|
+
root,
|
|
13958
|
+
initialized,
|
|
13959
|
+
mode,
|
|
13960
|
+
score,
|
|
13961
|
+
should_block: mode === "strict" && hasErrors,
|
|
13962
|
+
findings
|
|
13963
|
+
});
|
|
13964
|
+
}
|
|
13717
13965
|
async function runWithEnforcement(command, args, opts) {
|
|
13718
13966
|
const root = findProjectRoot49(opts.dir);
|
|
13719
13967
|
const paths = resolveHaivePaths45(root);
|
|
13720
|
-
if (!
|
|
13968
|
+
if (!existsSync69(paths.haiveDir)) {
|
|
13721
13969
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
13722
13970
|
process.exit(1);
|
|
13723
13971
|
}
|
|
@@ -13812,7 +14060,7 @@ async function writeWrapperBriefing(paths, sessionId, task) {
|
|
|
13812
14060
|
async function buildEnforcementReport(dir, stage, sessionId) {
|
|
13813
14061
|
const root = findProjectRoot49(dir);
|
|
13814
14062
|
const paths = resolveHaivePaths45(root);
|
|
13815
|
-
const initialized =
|
|
14063
|
+
const initialized = existsSync69(paths.haiveDir);
|
|
13816
14064
|
const config = initialized ? await loadConfig13(paths) : {};
|
|
13817
14065
|
if (initialized) await applyLightweightRepairs(root, paths);
|
|
13818
14066
|
const mode = config.enforcement?.mode ?? "strict";
|
|
@@ -13843,9 +14091,9 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
13843
14091
|
findings: [{ severity: "info", code: "enforcement-off", message: "hAIve enforcement is disabled." }]
|
|
13844
14092
|
});
|
|
13845
14093
|
}
|
|
13846
|
-
findings.push(...await inspectIntegrationVersions(root, "0.10.
|
|
14094
|
+
findings.push(...await inspectIntegrationVersions(root, "0.10.9"));
|
|
13847
14095
|
if (config.enforcement?.requireBriefingFirst !== false && stage !== "ci") {
|
|
13848
|
-
const hasBriefing = await
|
|
14096
|
+
const hasBriefing = await hasRecentBriefingMarker2(paths, sessionId);
|
|
13849
14097
|
findings.push(hasBriefing ? { severity: "ok", code: "briefing-loaded", message: "A recent hAIve briefing marker exists." } : {
|
|
13850
14098
|
severity: "error",
|
|
13851
14099
|
code: "briefing-missing",
|
|
@@ -13877,7 +14125,7 @@ async function buildEnforcementReport(dir, stage, sessionId) {
|
|
|
13877
14125
|
findings.push(...await verifyDecisionCoverage(paths, stage, sessionId));
|
|
13878
14126
|
}
|
|
13879
14127
|
if (stage === "pre-commit" || stage === "ci") {
|
|
13880
|
-
findings.push(...await runPrecommitPolicy(paths, config.enforcement?.antiPatternGate ?? "anchored"));
|
|
14128
|
+
findings.push(...await runPrecommitPolicy(paths, config.enforcement?.antiPatternGate ?? "anchored", stage));
|
|
13881
14129
|
}
|
|
13882
14130
|
if (config.enforcement?.cleanupGeneratedArtifacts !== false) {
|
|
13883
14131
|
findings.push(...await findGeneratedArtifacts(paths));
|
|
@@ -13913,7 +14161,7 @@ function withCategories(report) {
|
|
|
13913
14161
|
};
|
|
13914
14162
|
}
|
|
13915
14163
|
async function hasRecentSessionRecap(paths) {
|
|
13916
|
-
if (!
|
|
14164
|
+
if (!existsSync69(paths.memoriesDir)) return false;
|
|
13917
14165
|
const all = await loadMemoriesFromDir36(paths.memoriesDir);
|
|
13918
14166
|
return all.some(({ memory: memory2 }) => {
|
|
13919
14167
|
const fm = memory2.frontmatter;
|
|
@@ -13922,7 +14170,7 @@ async function hasRecentSessionRecap(paths) {
|
|
|
13922
14170
|
});
|
|
13923
14171
|
}
|
|
13924
14172
|
async function verifyMemoryPolicy(paths, config) {
|
|
13925
|
-
if (!
|
|
14173
|
+
if (!existsSync69(paths.memoriesDir)) return [];
|
|
13926
14174
|
const all = await loadMemoriesFromDir36(paths.memoriesDir);
|
|
13927
14175
|
const findings = [];
|
|
13928
14176
|
const staleImportant = [];
|
|
@@ -13960,7 +14208,7 @@ async function verifyMemoryPolicy(paths, config) {
|
|
|
13960
14208
|
return findings;
|
|
13961
14209
|
}
|
|
13962
14210
|
async function verifyDecisionCoverage(paths, stage, sessionId) {
|
|
13963
|
-
if (!
|
|
14211
|
+
if (!existsSync69(paths.memoriesDir)) return [];
|
|
13964
14212
|
const changedFiles = await getChangedFiles(paths.root, stage);
|
|
13965
14213
|
if (changedFiles.length === 0) {
|
|
13966
14214
|
return [{ severity: "info", code: "decision-coverage-no-changes", message: "No changed files to match against policy memories." }];
|
|
@@ -14001,19 +14249,20 @@ async function verifyDecisionCoverage(paths, stage, sessionId) {
|
|
|
14001
14249
|
impact: Math.min(35, 10 + missing.length * 5)
|
|
14002
14250
|
}];
|
|
14003
14251
|
}
|
|
14004
|
-
async function runPrecommitPolicy(paths, gate) {
|
|
14252
|
+
async function runPrecommitPolicy(paths, gate, stage) {
|
|
14005
14253
|
if (gate === "off") {
|
|
14006
14254
|
return [{ severity: "info", code: "precommit-policy-off", message: "Anti-pattern gate is disabled (enforcement.antiPatternGate=off)." }];
|
|
14007
14255
|
}
|
|
14008
|
-
const
|
|
14009
|
-
const touchedPaths =
|
|
14256
|
+
const snapshot = await getPolicyDiffSnapshot(paths.root, stage);
|
|
14257
|
+
const touchedPaths = snapshot.paths;
|
|
14010
14258
|
if (touchedPaths.length === 0) {
|
|
14011
|
-
|
|
14259
|
+
const code = stage === "ci" ? "no-ci-diff-changes" : "no-staged-changes";
|
|
14260
|
+
const message = stage === "ci" ? "No changed files found for CI policy diff." : "No staged changes found for pre-commit policy.";
|
|
14261
|
+
return [{ severity: "info", code, message }];
|
|
14012
14262
|
}
|
|
14013
|
-
const diff = await runCommand4("git", ["diff", "--cached"], paths.root).catch(() => "");
|
|
14014
14263
|
const { block_on, anchored_blocks } = antiPatternGateParams2(gate);
|
|
14015
14264
|
const result = await preCommitCheck({
|
|
14016
|
-
diff,
|
|
14265
|
+
diff: snapshot.diff,
|
|
14017
14266
|
paths: touchedPaths,
|
|
14018
14267
|
block_on,
|
|
14019
14268
|
anchored_blocks,
|
|
@@ -14023,7 +14272,7 @@ async function runPrecommitPolicy(paths, gate) {
|
|
|
14023
14272
|
return [{
|
|
14024
14273
|
severity: "ok",
|
|
14025
14274
|
code: "precommit-policy-pass",
|
|
14026
|
-
message:
|
|
14275
|
+
message: `${stage === "ci" ? "CI" : "Pre-commit"} policy passed for ${touchedPaths.length} changed file(s).`
|
|
14027
14276
|
}];
|
|
14028
14277
|
}
|
|
14029
14278
|
return [{
|
|
@@ -14071,7 +14320,7 @@ async function cleanupRuntimeDir(runtimeDir) {
|
|
|
14071
14320
|
removed++;
|
|
14072
14321
|
}
|
|
14073
14322
|
await writeFile33(path50.join(runtimeDir, ".gitignore"), "*\n!.gitignore\n!README.md\n", "utf8");
|
|
14074
|
-
if (!
|
|
14323
|
+
if (!existsSync69(path50.join(runtimeDir, "README.md"))) {
|
|
14075
14324
|
await writeFile33(
|
|
14076
14325
|
path50.join(runtimeDir, "README.md"),
|
|
14077
14326
|
"# .ai/.runtime \u2014 disposable local layer\n\nRuntime data is local. hAIve cleanup preserves briefing markers so enforcement state remains valid.\n",
|
|
@@ -14114,7 +14363,7 @@ async function inspectIntegrationVersions(root, expectedVersion) {
|
|
|
14114
14363
|
const findings = [];
|
|
14115
14364
|
for (const rel of files) {
|
|
14116
14365
|
const file = path50.join(root, rel);
|
|
14117
|
-
if (!
|
|
14366
|
+
if (!existsSync69(file)) continue;
|
|
14118
14367
|
const text = await readFile21(file, "utf8").catch(() => "");
|
|
14119
14368
|
for (const bin of extractAbsoluteHaiveBins2(text)) {
|
|
14120
14369
|
const version = versionForBinary2(bin);
|
|
@@ -14174,22 +14423,217 @@ function versionForBinary2(bin) {
|
|
|
14174
14423
|
}
|
|
14175
14424
|
}
|
|
14176
14425
|
async function getChangedFiles(root, stage) {
|
|
14177
|
-
|
|
14178
|
-
|
|
14179
|
-
|
|
14180
|
-
|
|
14426
|
+
if (stage === "ci") {
|
|
14427
|
+
return (await getPolicyDiffSnapshot(root, "ci")).paths;
|
|
14428
|
+
}
|
|
14429
|
+
if (stage === "pre-commit") {
|
|
14430
|
+
return normalizeChangedFileList(
|
|
14431
|
+
await runCommand4("git", ["diff", "--cached", "--name-only"], root).catch(() => "")
|
|
14432
|
+
);
|
|
14433
|
+
}
|
|
14181
14434
|
const files = /* @__PURE__ */ new Set();
|
|
14182
|
-
for (const args of
|
|
14183
|
-
const
|
|
14184
|
-
|
|
14185
|
-
|
|
14186
|
-
|
|
14187
|
-
|
|
14435
|
+
for (const args of [["diff", "--cached", "--name-only"], ["diff", "--name-only"]]) {
|
|
14436
|
+
for (const file of normalizeChangedFileList(await runCommand4("git", args, root).catch(() => ""))) {
|
|
14437
|
+
files.add(file);
|
|
14438
|
+
}
|
|
14439
|
+
}
|
|
14440
|
+
return [...files];
|
|
14441
|
+
}
|
|
14442
|
+
async function getPolicyDiffSnapshot(root, stage) {
|
|
14443
|
+
if (stage === "pre-commit") {
|
|
14444
|
+
const diff = await runCommand4("git", ["diff", "--cached"], root).catch(() => "");
|
|
14445
|
+
const names = await runCommand4("git", ["diff", "--cached", "--name-only"], root).catch(() => "");
|
|
14446
|
+
return { diff, paths: normalizeChangedFileList(names), source: "staged" };
|
|
14447
|
+
}
|
|
14448
|
+
const range = await resolveCiDiffRange(root);
|
|
14449
|
+
if (range) {
|
|
14450
|
+
const diff = await runCommand4("git", ["diff", range], root).catch(() => "");
|
|
14451
|
+
const names = await runCommand4("git", ["diff", "--name-only", range], root).catch(() => "");
|
|
14452
|
+
return { diff, paths: normalizeChangedFileList(names), source: range };
|
|
14453
|
+
}
|
|
14454
|
+
return { diff: "", paths: [], source: "none" };
|
|
14455
|
+
}
|
|
14456
|
+
async function resolveCiDiffRange(root) {
|
|
14457
|
+
const explicitBase = cleanGitSha(process.env.HAIVE_BASE_SHA ?? process.env.HAIVE_BASE_REF);
|
|
14458
|
+
const explicitHead = cleanGitSha(process.env.HAIVE_HEAD_SHA ?? process.env.GITHUB_SHA) ?? "HEAD";
|
|
14459
|
+
if (explicitBase && await gitCommitExists(root, explicitBase)) {
|
|
14460
|
+
return `${explicitBase}...${explicitHead}`;
|
|
14461
|
+
}
|
|
14462
|
+
const eventRange = await resolveGithubEventRange(root);
|
|
14463
|
+
if (eventRange) return eventRange;
|
|
14464
|
+
const baseRef = process.env.GITHUB_BASE_REF?.trim();
|
|
14465
|
+
if (baseRef) {
|
|
14466
|
+
const remoteRef = `origin/${baseRef}`;
|
|
14467
|
+
if (await gitCommitExists(root, remoteRef)) return `${remoteRef}...${explicitHead}`;
|
|
14468
|
+
}
|
|
14469
|
+
if (await gitCommitExists(root, "origin/main")) return `origin/main...${explicitHead}`;
|
|
14470
|
+
if (await gitCommitExists(root, "origin/master")) return `origin/master...${explicitHead}`;
|
|
14471
|
+
if (await gitCommitExists(root, "HEAD^")) return `HEAD^..${explicitHead}`;
|
|
14472
|
+
return null;
|
|
14473
|
+
}
|
|
14474
|
+
async function resolveGithubEventRange(root) {
|
|
14475
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
14476
|
+
if (!eventPath || !existsSync69(eventPath)) return null;
|
|
14477
|
+
try {
|
|
14478
|
+
const event = JSON.parse(await readFile21(eventPath, "utf8"));
|
|
14479
|
+
const prBase = cleanGitSha(event.pull_request?.base?.sha);
|
|
14480
|
+
const prHead = cleanGitSha(event.pull_request?.head?.sha ?? event.after ?? process.env.GITHUB_SHA) ?? "HEAD";
|
|
14481
|
+
if (prBase && await gitCommitExists(root, prBase)) return `${prBase}...${prHead}`;
|
|
14482
|
+
const pushBase = cleanGitSha(event.before);
|
|
14483
|
+
const pushHead = cleanGitSha(event.after ?? process.env.GITHUB_SHA) ?? "HEAD";
|
|
14484
|
+
if (pushBase && await gitCommitExists(root, pushBase)) return `${pushBase}..${pushHead}`;
|
|
14485
|
+
} catch {
|
|
14486
|
+
return null;
|
|
14487
|
+
}
|
|
14488
|
+
return null;
|
|
14489
|
+
}
|
|
14490
|
+
function cleanGitSha(value) {
|
|
14491
|
+
const trimmed = value?.trim();
|
|
14492
|
+
if (!trimmed || /^0+$/.test(trimmed)) return null;
|
|
14493
|
+
return trimmed;
|
|
14494
|
+
}
|
|
14495
|
+
async function gitCommitExists(root, ref) {
|
|
14496
|
+
try {
|
|
14497
|
+
await runCommand4("git", ["rev-parse", "--verify", `${ref}^{commit}`], root);
|
|
14498
|
+
return true;
|
|
14499
|
+
} catch {
|
|
14500
|
+
return false;
|
|
14188
14501
|
}
|
|
14189
|
-
|
|
14502
|
+
}
|
|
14503
|
+
function normalizeChangedFileList(raw) {
|
|
14504
|
+
return raw.split("\n").map((s) => s.trim()).filter(Boolean).filter(
|
|
14190
14505
|
(file) => !file.startsWith(".ai/.runtime/") && !file.startsWith(".ai/.cache/") && !file.startsWith(".ai/.usage/") && file !== ".ai/.usage/tool-usage.jsonl"
|
|
14191
14506
|
);
|
|
14192
14507
|
}
|
|
14508
|
+
async function getGitSyncStatus(root) {
|
|
14509
|
+
const dirty = (await runCommand4("git", ["status", "--short", "--untracked-files=all"], root).catch(() => "")).split("\n").map((line) => statusLineToPath(line.trim())).filter(Boolean).filter((file) => normalizeChangedFileList(file).length > 0);
|
|
14510
|
+
const branch = (await runCommand4("git", ["branch", "--show-current"], root).catch(() => "")).trim() || void 0;
|
|
14511
|
+
const upstream = (await runCommand4("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], root).catch(() => "")).trim() || void 0;
|
|
14512
|
+
if (!branch && !upstream) {
|
|
14513
|
+
const inside = (await runCommand4("git", ["rev-parse", "--is-inside-work-tree"], root).catch(() => "")).trim();
|
|
14514
|
+
if (inside !== "true") return { available: false, ahead: 0, behind: 0, dirtyFiles: [], changedSinceUpstream: [] };
|
|
14515
|
+
}
|
|
14516
|
+
let ahead = 0;
|
|
14517
|
+
let behind = 0;
|
|
14518
|
+
let changedSinceUpstream = [];
|
|
14519
|
+
let releaseBaseRef;
|
|
14520
|
+
let releaseChangedFiles;
|
|
14521
|
+
if (upstream) {
|
|
14522
|
+
const counts = (await runCommand4("git", ["rev-list", "--left-right", "--count", `${upstream}...HEAD`], root).catch(() => "")).trim();
|
|
14523
|
+
const [behindRaw, aheadRaw] = counts.split(/\s+/);
|
|
14524
|
+
behind = Number.parseInt(behindRaw ?? "0", 10) || 0;
|
|
14525
|
+
ahead = Number.parseInt(aheadRaw ?? "0", 10) || 0;
|
|
14526
|
+
changedSinceUpstream = normalizeChangedFileList(
|
|
14527
|
+
await runCommand4("git", ["diff", "--name-only", `${upstream}...HEAD`], root).catch(() => "")
|
|
14528
|
+
);
|
|
14529
|
+
if (changedSinceUpstream.length > 0) {
|
|
14530
|
+
releaseBaseRef = upstream;
|
|
14531
|
+
releaseChangedFiles = changedSinceUpstream;
|
|
14532
|
+
}
|
|
14533
|
+
}
|
|
14534
|
+
if (!releaseChangedFiles || releaseChangedFiles.length === 0) {
|
|
14535
|
+
const hasParent = (await runCommand4("git", ["rev-parse", "--verify", "--quiet", "HEAD^"], root).catch(() => "")).trim().length > 0;
|
|
14536
|
+
if (hasParent) {
|
|
14537
|
+
const changedSinceParent = normalizeChangedFileList(
|
|
14538
|
+
await runCommand4("git", ["diff", "--name-only", "HEAD^..HEAD"], root).catch(() => "")
|
|
14539
|
+
);
|
|
14540
|
+
if (changedSinceParent.length > 0) {
|
|
14541
|
+
releaseBaseRef = "HEAD^";
|
|
14542
|
+
releaseChangedFiles = changedSinceParent;
|
|
14543
|
+
}
|
|
14544
|
+
}
|
|
14545
|
+
}
|
|
14546
|
+
return {
|
|
14547
|
+
available: true,
|
|
14548
|
+
branch,
|
|
14549
|
+
upstream,
|
|
14550
|
+
ahead,
|
|
14551
|
+
behind,
|
|
14552
|
+
dirtyFiles: dirty,
|
|
14553
|
+
changedSinceUpstream,
|
|
14554
|
+
...releaseBaseRef ? { releaseBaseRef } : {},
|
|
14555
|
+
...releaseChangedFiles ? { releaseChangedFiles } : {}
|
|
14556
|
+
};
|
|
14557
|
+
}
|
|
14558
|
+
function statusLineToPath(line) {
|
|
14559
|
+
const body = line.replace(/^[ MADRCU?!]{1,2}\s+/, "").trim();
|
|
14560
|
+
const renamed = body.match(/.+ -> (.+)$/);
|
|
14561
|
+
return renamed?.[1]?.trim() ?? body;
|
|
14562
|
+
}
|
|
14563
|
+
var VERSION_FILES = [
|
|
14564
|
+
"package.json",
|
|
14565
|
+
"packages/core/package.json",
|
|
14566
|
+
"packages/cli/package.json",
|
|
14567
|
+
"packages/mcp/package.json",
|
|
14568
|
+
"packages/embeddings/package.json"
|
|
14569
|
+
];
|
|
14570
|
+
var SHIPPABLE_PATH_PREFIXES = [
|
|
14571
|
+
"packages/core/src/",
|
|
14572
|
+
"packages/cli/src/",
|
|
14573
|
+
"packages/mcp/src/",
|
|
14574
|
+
"packages/embeddings/src/"
|
|
14575
|
+
];
|
|
14576
|
+
function isShippablePath(file) {
|
|
14577
|
+
return SHIPPABLE_PATH_PREFIXES.some((prefix) => file.startsWith(prefix)) || VERSION_FILES.includes(file);
|
|
14578
|
+
}
|
|
14579
|
+
async function inspectReleaseVersionState(root, upstream) {
|
|
14580
|
+
const localEntries = await Promise.all(VERSION_FILES.map(async (file) => [file, await readPackageVersion(root, file)]));
|
|
14581
|
+
const localVersions = new Map(localEntries);
|
|
14582
|
+
const unique = new Set([...localVersions.values()].filter(Boolean));
|
|
14583
|
+
const version = unique.size === 1 ? [...unique][0] : void 0;
|
|
14584
|
+
const localVersionsLabel = VERSION_FILES.map((file) => `${file}=${localVersions.get(file) ?? "unreadable"}`).join(", ");
|
|
14585
|
+
const baseVersion = await readPackageVersionAtRef(root, upstream, "package.json");
|
|
14586
|
+
return {
|
|
14587
|
+
lockstep: unique.size === 1 && localVersions.size === VERSION_FILES.length,
|
|
14588
|
+
...version ? { version } : {},
|
|
14589
|
+
...baseVersion ? { baseVersion } : {},
|
|
14590
|
+
localVersionsLabel
|
|
14591
|
+
};
|
|
14592
|
+
}
|
|
14593
|
+
async function readPackageVersion(root, relPath) {
|
|
14594
|
+
try {
|
|
14595
|
+
const data = JSON.parse(await readFile21(path50.join(root, relPath), "utf8"));
|
|
14596
|
+
return typeof data.version === "string" ? data.version : void 0;
|
|
14597
|
+
} catch {
|
|
14598
|
+
return void 0;
|
|
14599
|
+
}
|
|
14600
|
+
}
|
|
14601
|
+
async function readPackageVersionAtRef(root, ref, relPath) {
|
|
14602
|
+
try {
|
|
14603
|
+
const raw = await runCommand4("git", ["show", `${ref}:${relPath}`], root);
|
|
14604
|
+
const data = JSON.parse(raw);
|
|
14605
|
+
return typeof data.version === "string" ? data.version : void 0;
|
|
14606
|
+
} catch {
|
|
14607
|
+
return void 0;
|
|
14608
|
+
}
|
|
14609
|
+
}
|
|
14610
|
+
function compareSemver(a, b) {
|
|
14611
|
+
const pa = a.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
|
|
14612
|
+
const pb = b.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
|
|
14613
|
+
const len = Math.max(pa.length, pb.length, 3);
|
|
14614
|
+
for (let i = 0; i < len; i++) {
|
|
14615
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
14616
|
+
if (diff !== 0) return diff;
|
|
14617
|
+
}
|
|
14618
|
+
return 0;
|
|
14619
|
+
}
|
|
14620
|
+
async function tagPointsAtHead(root, tag) {
|
|
14621
|
+
const tags = await runCommand4("git", ["tag", "--points-at", "HEAD"], root).catch(() => "");
|
|
14622
|
+
return tags.split("\n").map((line) => line.trim()).includes(tag);
|
|
14623
|
+
}
|
|
14624
|
+
async function remoteTagExists(root, tag) {
|
|
14625
|
+
const branch = (await runCommand4("git", ["branch", "--show-current"], root).catch(() => "")).trim();
|
|
14626
|
+
const branchRemote = branch ? (await runCommand4("git", ["config", "--get", `branch.${branch}.remote`], root).catch(() => "")).trim() : "";
|
|
14627
|
+
const hasOrigin = (await runCommand4("git", ["config", "--get", "remote.origin.url"], root).catch(() => "")).trim().length > 0;
|
|
14628
|
+
const remote = branchRemote || (hasOrigin ? "origin" : "");
|
|
14629
|
+
if (!remote) return null;
|
|
14630
|
+
try {
|
|
14631
|
+
const out = await runCommand4("git", ["ls-remote", "--tags", remote, `refs/tags/${tag}`], root);
|
|
14632
|
+
return out.trim().length > 0;
|
|
14633
|
+
} catch {
|
|
14634
|
+
return null;
|
|
14635
|
+
}
|
|
14636
|
+
}
|
|
14193
14637
|
function buildScore(findings, threshold = 80) {
|
|
14194
14638
|
const checks = {
|
|
14195
14639
|
total: findings.length,
|
|
@@ -14210,7 +14654,7 @@ function buildScore(findings, threshold = 80) {
|
|
|
14210
14654
|
}
|
|
14211
14655
|
async function installGitEnforcement(root) {
|
|
14212
14656
|
const hooksDir = path50.join(root, ".git", "hooks");
|
|
14213
|
-
if (!
|
|
14657
|
+
if (!existsSync69(path50.join(root, ".git"))) {
|
|
14214
14658
|
ui.warn("No .git directory found; git enforcement hooks skipped.");
|
|
14215
14659
|
return;
|
|
14216
14660
|
}
|
|
@@ -14233,7 +14677,7 @@ haive enforce check --stage pre-push --dir . || exit $?
|
|
|
14233
14677
|
];
|
|
14234
14678
|
for (const hook of hooks) {
|
|
14235
14679
|
const file = path50.join(hooksDir, hook.name);
|
|
14236
|
-
if (
|
|
14680
|
+
if (existsSync69(file)) {
|
|
14237
14681
|
const current = await readFile21(file, "utf8").catch(() => "");
|
|
14238
14682
|
if (current.includes(ENFORCE_HOOK_MARKER)) {
|
|
14239
14683
|
await writeFile33(file, hook.body, "utf8");
|
|
@@ -14252,7 +14696,7 @@ ${hook.body}`, "utf8");
|
|
|
14252
14696
|
async function installCiEnforcement(root) {
|
|
14253
14697
|
const workflowPath = path50.join(root, ".github", "workflows", "haive-enforcement.yml");
|
|
14254
14698
|
await mkdir19(path50.dirname(workflowPath), { recursive: true });
|
|
14255
|
-
if (
|
|
14699
|
+
if (existsSync69(workflowPath)) {
|
|
14256
14700
|
ui.info("GitHub Actions enforcement workflow already exists \u2014 skipped");
|
|
14257
14701
|
return;
|
|
14258
14702
|
}
|
|
@@ -14278,6 +14722,9 @@ jobs:
|
|
|
14278
14722
|
- name: Install hAIve
|
|
14279
14723
|
run: npm install -g @hiveai/cli
|
|
14280
14724
|
- name: Enforce hAIve policy
|
|
14725
|
+
env:
|
|
14726
|
+
HAIVE_BASE_SHA: \${{ github.event.pull_request.base.sha || github.event.before }}
|
|
14727
|
+
HAIVE_HEAD_SHA: \${{ github.event.pull_request.head.sha || github.sha }}
|
|
14281
14728
|
run: haive enforce ci
|
|
14282
14729
|
`, "utf8");
|
|
14283
14730
|
ui.success(`Created ${path50.relative(root, workflowPath)}`);
|
|
@@ -14381,7 +14828,7 @@ function normalizeToolPath(file, root) {
|
|
|
14381
14828
|
return path50.relative(root, normalized).replace(/\\/g, "/");
|
|
14382
14829
|
}
|
|
14383
14830
|
async function missingRequiredMemoriesForFiles(paths, files, sessionId) {
|
|
14384
|
-
if (!
|
|
14831
|
+
if (!existsSync69(paths.memoriesDir)) return [];
|
|
14385
14832
|
const marker = await readRecentBriefingMarker(paths, sessionId);
|
|
14386
14833
|
const consulted = new Set(marker?.memory_ids ?? []);
|
|
14387
14834
|
const policyTypes = /* @__PURE__ */ new Set(["decision", "gotcha", "architecture", "convention", "attempt"]);
|
|
@@ -14455,7 +14902,7 @@ function registerRun(program2) {
|
|
|
14455
14902
|
|
|
14456
14903
|
// src/commands/sensors.ts
|
|
14457
14904
|
import { execFile as execFile2 } from "child_process";
|
|
14458
|
-
import { existsSync as
|
|
14905
|
+
import { existsSync as existsSync70 } from "fs";
|
|
14459
14906
|
import { chmod as chmod3, mkdir as mkdir20, readFile as readFile23, writeFile as writeFile34 } from "fs/promises";
|
|
14460
14907
|
import path51 from "path";
|
|
14461
14908
|
import { promisify as promisify2 } from "util";
|
|
@@ -14538,7 +14985,7 @@ function registerSensors(program2) {
|
|
|
14538
14985
|
}
|
|
14539
14986
|
const root = findProjectRoot50(opts.dir);
|
|
14540
14987
|
const paths = resolveHaivePaths46(root);
|
|
14541
|
-
const loaded =
|
|
14988
|
+
const loaded = existsSync70(paths.memoriesDir) ? await loadMemoriesFromDir37(paths.memoriesDir) : [];
|
|
14542
14989
|
const found = loaded.find(({ memory: memory2 }) => memory2.frontmatter.id === id);
|
|
14543
14990
|
if (!found) {
|
|
14544
14991
|
ui.error(`No memory found with id ${id}`);
|
|
@@ -14600,7 +15047,7 @@ async function sensorRows(paths) {
|
|
|
14600
15047
|
});
|
|
14601
15048
|
}
|
|
14602
15049
|
async function runnableSensorMemories(paths, regexOnly = true) {
|
|
14603
|
-
if (!
|
|
15050
|
+
if (!existsSync70(paths.memoriesDir)) return [];
|
|
14604
15051
|
const loaded = await loadMemoriesFromDir37(paths.memoriesDir);
|
|
14605
15052
|
return loaded.map(({ memory: memory2 }) => memory2).filter((memory2) => {
|
|
14606
15053
|
const sensor = memory2.frontmatter.sensor;
|
|
@@ -14643,7 +15090,7 @@ function shellQuote(value) {
|
|
|
14643
15090
|
|
|
14644
15091
|
// src/index.ts
|
|
14645
15092
|
var program = new Command53();
|
|
14646
|
-
program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.10.
|
|
15093
|
+
program.name("haive").description("hAIve - repo-native memory and context policy for coding-agent harnesses").version("0.10.9").option("--advanced", "show maintenance and experimental commands in help");
|
|
14647
15094
|
registerInit(program);
|
|
14648
15095
|
registerWelcome(program);
|
|
14649
15096
|
registerResolveProject(program);
|