@hiveai/mcp 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2065,13 +2065,520 @@ async function antiPatternsCheck(input, ctx) {
2065
2065
  };
2066
2066
  }
2067
2067
 
2068
- // src/prompts/bootstrap-project.ts
2068
+ // src/tools/mem-distill.ts
2069
+ import { existsSync as existsSync22 } from "fs";
2070
+ import {
2071
+ loadMemoriesFromDir as loadMemoriesFromDir18,
2072
+ tokenizeQuery as tokenizeQuery4
2073
+ } from "@hiveai/core";
2069
2074
  import { z as z25 } from "zod";
2075
+ var MemDistillInputSchema = {
2076
+ since_days: z25.number().int().positive().default(30).describe("Only consider memories created in the last N days."),
2077
+ min_cluster: z25.number().int().min(2).default(3).describe("Minimum cluster size to surface."),
2078
+ type_filter: z25.enum(["gotcha", "attempt", "all"]).default("gotcha").describe(
2079
+ "Memory type to scan. 'gotcha' targets observe-style discoveries that recur, 'attempt' surfaces failed approaches that repeat, 'all' considers both."
2080
+ ),
2081
+ scope: z25.enum(["personal", "team", "module", "any"]).default("any").describe("Restrict to a specific scope.")
2082
+ };
2083
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
2084
+ var STOP_WORDS = /* @__PURE__ */ new Set([
2085
+ "the",
2086
+ "and",
2087
+ "for",
2088
+ "with",
2089
+ "that",
2090
+ "this",
2091
+ "from",
2092
+ "into",
2093
+ "when",
2094
+ "then",
2095
+ "also",
2096
+ "must",
2097
+ "have",
2098
+ "does",
2099
+ "not",
2100
+ "but",
2101
+ "you",
2102
+ "your",
2103
+ "its",
2104
+ "because",
2105
+ "why",
2106
+ "how",
2107
+ "what",
2108
+ "use",
2109
+ "using",
2110
+ "used",
2111
+ "add",
2112
+ "added",
2113
+ "make",
2114
+ "made",
2115
+ "fix",
2116
+ "fixed",
2117
+ "bug",
2118
+ "error"
2119
+ ]);
2120
+ async function memDistill(input, ctx) {
2121
+ if (!existsSync22(ctx.paths.memoriesDir)) {
2122
+ return { scanned: 0, singletons: 0, clusters: [], notice: "No .ai/memories directory." };
2123
+ }
2124
+ const cutoff = Date.now() - input.since_days * MS_PER_DAY;
2125
+ const all = await loadMemoriesFromDir18(ctx.paths.memoriesDir);
2126
+ const candidates = all.filter(({ memory }) => {
2127
+ const fm = memory.frontmatter;
2128
+ if (fm.status === "rejected" || fm.status === "deprecated" || fm.status === "stale") return false;
2129
+ if (input.scope !== "any" && fm.scope !== input.scope) return false;
2130
+ if (input.type_filter === "gotcha" && fm.type !== "gotcha") return false;
2131
+ if (input.type_filter === "attempt" && fm.type !== "attempt") return false;
2132
+ if (input.type_filter === "all" && fm.type !== "gotcha" && fm.type !== "attempt") return false;
2133
+ if (Date.parse(fm.created_at) < cutoff) return false;
2134
+ return true;
2135
+ });
2136
+ if (candidates.length < input.min_cluster) {
2137
+ return {
2138
+ scanned: candidates.length,
2139
+ singletons: candidates.length,
2140
+ clusters: [],
2141
+ notice: candidates.length === 0 ? `No matching memories in the last ${input.since_days} days.` : `Only ${candidates.length} candidate${candidates.length === 1 ? "" : "s"} \u2014 below min_cluster=${input.min_cluster}.`
2142
+ };
2143
+ }
2144
+ const features = candidates.map((loaded) => ({
2145
+ loaded,
2146
+ keywords: keywordSet(loaded),
2147
+ paths: new Set(loaded.memory.frontmatter.anchor.paths)
2148
+ }));
2149
+ const parent = features.map((_, i) => i);
2150
+ const find = (i) => parent[i] === i ? i : parent[i] = find(parent[i] ?? 0);
2151
+ const union = (a, b) => {
2152
+ const ra = find(a), rb = find(b);
2153
+ if (ra !== rb) parent[ra] = rb;
2154
+ };
2155
+ for (let i = 0; i < features.length; i++) {
2156
+ for (let j = i + 1; j < features.length; j++) {
2157
+ const fi = features[i], fj = features[j];
2158
+ const pathSim = jaccard(fi.paths, fj.paths);
2159
+ const kwSim = jaccard(fi.keywords, fj.keywords);
2160
+ if (pathSim >= 0.5 || kwSim >= 0.4) union(i, j);
2161
+ }
2162
+ }
2163
+ const groups = /* @__PURE__ */ new Map();
2164
+ for (let i = 0; i < features.length; i++) {
2165
+ const root = find(i);
2166
+ const arr = groups.get(root) ?? [];
2167
+ arr.push(i);
2168
+ groups.set(root, arr);
2169
+ }
2170
+ const clusters = [];
2171
+ let singletons = 0;
2172
+ for (const indices of groups.values()) {
2173
+ if (indices.length < input.min_cluster) {
2174
+ singletons += indices.length;
2175
+ continue;
2176
+ }
2177
+ const members = indices.map((i) => features[i]);
2178
+ const allPaths = /* @__PURE__ */ new Set();
2179
+ const allKeywords = /* @__PURE__ */ new Map();
2180
+ let latest = 0;
2181
+ for (const m of members) {
2182
+ for (const p of m.paths) allPaths.add(p);
2183
+ for (const k of m.keywords) allKeywords.set(k, (allKeywords.get(k) ?? 0) + 1);
2184
+ const t = Date.parse(m.loaded.memory.frontmatter.created_at);
2185
+ if (t > latest) latest = t;
2186
+ }
2187
+ const commonKeywords = [...allKeywords.entries()].filter(([, n]) => n >= Math.max(2, Math.floor(members.length / 2))).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([k]) => k);
2188
+ const titles = members.map((m) => firstHeading(m.loaded.memory.body) ?? m.loaded.memory.frontmatter.id).slice(0, 5);
2189
+ const suggestedType = members.every((m) => m.loaded.memory.frontmatter.type === "attempt") ? "gotcha" : "convention";
2190
+ clusters.push({
2191
+ suggested_topic: commonKeywords.slice(0, 3).join("-") || "merged-observations",
2192
+ suggested_type: suggestedType,
2193
+ member_ids: members.map((m) => m.loaded.memory.frontmatter.id),
2194
+ overlapping_paths: [...allPaths].slice(0, 10),
2195
+ common_keywords: commonKeywords,
2196
+ sample_titles: titles,
2197
+ latest_at: new Date(latest).toISOString()
2198
+ });
2199
+ }
2200
+ clusters.sort((a, b) => b.member_ids.length - a.member_ids.length);
2201
+ return {
2202
+ scanned: candidates.length,
2203
+ singletons,
2204
+ clusters
2205
+ };
2206
+ }
2207
+ function keywordSet(loaded) {
2208
+ const text = (loaded.memory.body + " " + loaded.memory.frontmatter.tags.join(" ")).slice(0, 800);
2209
+ const tokens = tokenizeQuery4(text).filter((t) => t.length >= 4 && !STOP_WORDS.has(t));
2210
+ return new Set(tokens);
2211
+ }
2212
+ function jaccard(a, b) {
2213
+ if (a.size === 0 && b.size === 0) return 0;
2214
+ let intersect = 0;
2215
+ for (const x of a) if (b.has(x)) intersect++;
2216
+ const union = a.size + b.size - intersect;
2217
+ return union === 0 ? 0 : intersect / union;
2218
+ }
2219
+ function firstHeading(body) {
2220
+ for (const line of body.split("\n")) {
2221
+ const t = line.trim();
2222
+ if (t.startsWith("#")) return t.replace(/^#+\s*/, "").slice(0, 80);
2223
+ if (t.length > 0) return t.slice(0, 80);
2224
+ }
2225
+ return void 0;
2226
+ }
2227
+
2228
+ // src/tools/why-this-decision.ts
2229
+ import { existsSync as existsSync23 } from "fs";
2230
+ import { spawn as spawn2 } from "child_process";
2231
+ import {
2232
+ deriveConfidence as deriveConfidence7,
2233
+ getUsage as getUsage8,
2234
+ loadMemoriesFromDir as loadMemoriesFromDir19,
2235
+ loadUsageIndex as loadUsageIndex10,
2236
+ pathsOverlap as singlePathsOverlap
2237
+ } from "@hiveai/core";
2238
+ import { z as z26 } from "zod";
2239
+ var WhyThisDecisionInputSchema = {
2240
+ id: z26.string().min(1).describe("Memory id to inspect (e.g. '2026-04-25-decision-esm-only')."),
2241
+ git_log_limit: z26.number().int().positive().max(20).default(5).describe("How many recent commits per anchor path to surface.")
2242
+ };
2243
+ async function whyThisDecision(input, ctx) {
2244
+ if (!existsSync23(ctx.paths.memoriesDir)) {
2245
+ return {
2246
+ found: false,
2247
+ related: [],
2248
+ path_neighbors: [],
2249
+ recent_commits: [],
2250
+ notice: "No .ai/memories directory."
2251
+ };
2252
+ }
2253
+ const all = await loadMemoriesFromDir19(ctx.paths.memoriesDir);
2254
+ const usage = await loadUsageIndex10(ctx.paths);
2255
+ const target = all.find(({ memory }) => memory.frontmatter.id === input.id);
2256
+ if (!target) {
2257
+ return {
2258
+ found: false,
2259
+ related: [],
2260
+ path_neighbors: [],
2261
+ recent_commits: [],
2262
+ notice: `Memory '${input.id}' not found.`
2263
+ };
2264
+ }
2265
+ const fm = target.memory.frontmatter;
2266
+ const targetUsage = getUsage8(usage, fm.id);
2267
+ const decision = {
2268
+ id: fm.id,
2269
+ type: fm.type,
2270
+ scope: fm.scope,
2271
+ status: fm.status,
2272
+ confidence: deriveConfidence7(fm, targetUsage),
2273
+ body: target.memory.body,
2274
+ created_at: fm.created_at
2275
+ };
2276
+ const relatedSet = new Set(fm.related_ids ?? []);
2277
+ const related = [];
2278
+ for (const { memory } of all) {
2279
+ if (memory.frontmatter.id === fm.id) continue;
2280
+ const isExplicit = relatedSet.has(memory.frontmatter.id);
2281
+ const isBackLink = (memory.frontmatter.related_ids ?? []).includes(fm.id);
2282
+ if (!isExplicit && !isBackLink) continue;
2283
+ const u = getUsage8(usage, memory.frontmatter.id);
2284
+ related.push({
2285
+ id: memory.frontmatter.id,
2286
+ type: memory.frontmatter.type,
2287
+ scope: memory.frontmatter.scope,
2288
+ confidence: deriveConfidence7(memory.frontmatter, u),
2289
+ body_preview: memory.body.split("\n").slice(0, 4).join("\n").slice(0, 300),
2290
+ relation: isExplicit ? "explicit" : "back-link"
2291
+ });
2292
+ }
2293
+ const targetPaths = fm.anchor.paths;
2294
+ const path_neighbors = [];
2295
+ if (targetPaths.length > 0) {
2296
+ for (const { memory } of all) {
2297
+ if (memory.frontmatter.id === fm.id) continue;
2298
+ if (relatedSet.has(memory.frontmatter.id)) continue;
2299
+ const overlappingPaths = memory.frontmatter.anchor.paths.filter(
2300
+ (p) => targetPaths.some((tp) => singlePathsOverlap(p, tp))
2301
+ );
2302
+ if (overlappingPaths.length === 0) continue;
2303
+ const u = getUsage8(usage, memory.frontmatter.id);
2304
+ path_neighbors.push({
2305
+ id: memory.frontmatter.id,
2306
+ type: memory.frontmatter.type,
2307
+ scope: memory.frontmatter.scope,
2308
+ confidence: deriveConfidence7(memory.frontmatter, u),
2309
+ overlap: overlappingPaths,
2310
+ body_preview: memory.body.split("\n").slice(0, 3).join("\n").slice(0, 200)
2311
+ });
2312
+ if (path_neighbors.length >= 10) break;
2313
+ }
2314
+ }
2315
+ const recent_commits = [];
2316
+ for (const p of targetPaths.slice(0, 5)) {
2317
+ try {
2318
+ const commits = await runGitLog2(ctx.paths.root, p, input.git_log_limit);
2319
+ for (const c of commits) recent_commits.push({ path: p, ...c });
2320
+ } catch {
2321
+ }
2322
+ }
2323
+ const hints = [];
2324
+ if (decision.confidence === "low" || decision.confidence === "stale") {
2325
+ hints.push(`\u26A0\uFE0F Confidence is ${decision.confidence}. Verify this decision still applies before quoting it.`);
2326
+ }
2327
+ if (related.length === 0 && path_neighbors.length === 0 && targetPaths.length === 0) {
2328
+ hints.push("No related memories and no anchored paths \u2014 this decision is isolated; consider adding related_ids or paths.");
2329
+ }
2330
+ if (fm.type !== "decision" && fm.type !== "architecture") {
2331
+ hints.push(`Memory type is '${fm.type}', not 'decision'/'architecture' \u2014 output may be less informative.`);
2332
+ }
2333
+ return {
2334
+ found: true,
2335
+ decision,
2336
+ related,
2337
+ path_neighbors,
2338
+ recent_commits,
2339
+ ...hints.length > 0 ? { hints } : {}
2340
+ };
2341
+ }
2342
+ async function runGitLog2(cwd, filePath, limit) {
2343
+ const sep = "<<HV>>";
2344
+ const fmt = `%h${sep}%an${sep}%ar${sep}%s`;
2345
+ const output = await runCommand2(
2346
+ "git",
2347
+ ["log", "-n", String(limit), `--pretty=format:${fmt}`, "--", filePath],
2348
+ cwd
2349
+ );
2350
+ if (!output.trim()) return [];
2351
+ return output.split("\n").map((line) => {
2352
+ const [sha = "", author = "", relative_date = "", subject = ""] = line.split(sep);
2353
+ return { sha, author, relative_date, subject };
2354
+ }).filter((c) => c.sha);
2355
+ }
2356
+ function runCommand2(cmd, args, cwd) {
2357
+ return new Promise((resolve, reject) => {
2358
+ const proc = spawn2(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
2359
+ let stdout = "";
2360
+ let stderr = "";
2361
+ proc.stdout.on("data", (chunk) => {
2362
+ stdout += chunk.toString();
2363
+ });
2364
+ proc.stderr.on("data", (chunk) => {
2365
+ stderr += chunk.toString();
2366
+ });
2367
+ proc.on("error", reject);
2368
+ proc.on("close", (code) => {
2369
+ if (code === 0) resolve(stdout);
2370
+ else reject(new Error(stderr || `${cmd} exited with code ${code}`));
2371
+ });
2372
+ });
2373
+ }
2374
+
2375
+ // src/tools/mem-conflicts.ts
2376
+ import { existsSync as existsSync24 } from "fs";
2377
+ import {
2378
+ deriveConfidence as deriveConfidence8,
2379
+ getUsage as getUsage9,
2380
+ loadMemoriesFromDir as loadMemoriesFromDir20,
2381
+ loadUsageIndex as loadUsageIndex11,
2382
+ pathsOverlap,
2383
+ tokenizeQuery as tokenizeQuery5
2384
+ } from "@hiveai/core";
2385
+ import { z as z27 } from "zod";
2386
+ var MemConflictsInputSchema = {
2387
+ id: z27.string().min(1).describe("Memory id to check for conflicts."),
2388
+ min_score: z27.number().min(0).max(1).default(0.5).describe("Minimum cosine similarity to consider a memory as a potential conflict (semantic mode)."),
2389
+ semantic: z27.boolean().default(true).describe("Use embeddings for similarity. Falls back to keyword overlap when embeddings are not installed.")
2390
+ };
2391
+ var POSITIVE_PATTERNS = /\b(use|prefer|always|should use|do this|recommended|ok to)\b/i;
2392
+ var NEGATIVE_PATTERNS = /\b(do not use|don'?t use|never|avoid|forbidden|deprecated|stop using|do NOT|❌)\b/i;
2393
+ async function memConflicts(input, ctx) {
2394
+ if (!existsSync24(ctx.paths.memoriesDir)) {
2395
+ return { found: false, scanned: 0, conflicts: [], notice: "No .ai/memories directory." };
2396
+ }
2397
+ const all = await loadMemoriesFromDir20(ctx.paths.memoriesDir);
2398
+ const target = all.find(({ memory }) => memory.frontmatter.id === input.id);
2399
+ if (!target) {
2400
+ return { found: false, scanned: 0, conflicts: [], notice: `Memory '${input.id}' not found.` };
2401
+ }
2402
+ const usage = await loadUsageIndex11(ctx.paths);
2403
+ const others = all.filter(
2404
+ ({ memory }) => memory.frontmatter.id !== input.id && memory.frontmatter.type !== "session_recap"
2405
+ );
2406
+ const simScores = input.semantic ? await trySemanticSimilarities(ctx, target, others) : null;
2407
+ const targetText = (target.memory.body + " " + target.memory.frontmatter.tags.join(" ")).toLowerCase();
2408
+ const targetTokens = new Set(tokenizeQuery5(targetText));
2409
+ const targetPolarity = polarity(targetText);
2410
+ const targetPaths = target.memory.frontmatter.anchor.paths;
2411
+ const explicitContradicts = extractContradictsTags(target.memory.body);
2412
+ const conflicts = [];
2413
+ for (const other of others) {
2414
+ const fm = other.memory.frontmatter;
2415
+ const otherText = (other.memory.body + " " + fm.tags.join(" ")).toLowerCase();
2416
+ const reasons = [];
2417
+ const sim = simScores?.get(fm.id) ?? null;
2418
+ const hasPathOverlap = fm.anchor.paths.some((p) => targetPaths.some((tp) => pathsOverlap(p, tp)));
2419
+ const otherTokens = new Set(tokenizeQuery5(otherText));
2420
+ const tokenOverlap = countIntersection(targetTokens, otherTokens);
2421
+ const isSemanticNeighbor = sim !== null && sim >= input.min_score;
2422
+ if (!hasPathOverlap && tokenOverlap < 4 && !isSemanticNeighbor) continue;
2423
+ const otherContradicts = extractContradictsTags(other.memory.body);
2424
+ if (explicitContradicts.has(fm.id) || otherContradicts.has(input.id)) {
2425
+ reasons.push("explicit-contradiction-tag");
2426
+ }
2427
+ if (target.memory.frontmatter.status === "validated" && fm.status === "rejected" || target.memory.frontmatter.status === "rejected" && fm.status === "validated") {
2428
+ if (tokenOverlap >= 4 || isSemanticNeighbor) reasons.push("opposite-status");
2429
+ }
2430
+ if (hasPathOverlap) {
2431
+ const tType = target.memory.frontmatter.type;
2432
+ const oType = fm.type;
2433
+ const isAttemptVsRule = tType === "attempt" && (oType === "convention" || oType === "decision") || oType === "attempt" && (tType === "convention" || tType === "decision");
2434
+ if (isAttemptVsRule) reasons.push("attempt-vs-convention-same-paths");
2435
+ }
2436
+ if (isSemanticNeighbor) {
2437
+ const otherPolarity = polarity(otherText);
2438
+ if (targetPolarity === "positive" && otherPolarity === "negative" || targetPolarity === "negative" && otherPolarity === "positive") {
2439
+ reasons.push("polarity-keywords");
2440
+ }
2441
+ }
2442
+ if (reasons.length === 0) continue;
2443
+ const u = getUsage9(usage, fm.id);
2444
+ conflicts.push({
2445
+ id: fm.id,
2446
+ type: fm.type,
2447
+ scope: fm.scope,
2448
+ status: fm.status,
2449
+ confidence: deriveConfidence8(fm, u),
2450
+ body_preview: other.memory.body.split("\n").slice(0, 4).join("\n").slice(0, 300),
2451
+ similarity: sim,
2452
+ reasons,
2453
+ shared_paths: fm.anchor.paths.filter((p) => targetPaths.some((tp) => pathsOverlap(p, tp)))
2454
+ });
2455
+ }
2456
+ conflicts.sort((a, b) => {
2457
+ const score = (c) => (c.reasons.includes("explicit-contradiction-tag") ? 100 : 0) + (c.reasons.includes("opposite-status") ? 50 : 0) + (c.reasons.includes("attempt-vs-convention-same-paths") ? 25 : 0) + (c.reasons.includes("polarity-keywords") ? 10 : 0) + (c.similarity ?? 0) * 5;
2458
+ return score(b) - score(a);
2459
+ });
2460
+ return {
2461
+ found: true,
2462
+ target: {
2463
+ id: target.memory.frontmatter.id,
2464
+ type: target.memory.frontmatter.type,
2465
+ status: target.memory.frontmatter.status
2466
+ },
2467
+ scanned: others.length,
2468
+ conflicts: conflicts.slice(0, 10)
2469
+ };
2470
+ }
2471
+ function polarity(text) {
2472
+ const neg = NEGATIVE_PATTERNS.test(text);
2473
+ const pos = POSITIVE_PATTERNS.test(text);
2474
+ if (neg && !pos) return "negative";
2475
+ if (pos && !neg) return "positive";
2476
+ return "neutral";
2477
+ }
2478
+ function extractContradictsTags(body) {
2479
+ const out = /* @__PURE__ */ new Set();
2480
+ for (const m of body.matchAll(/#contradicts:([\w-]+)/g)) {
2481
+ if (m[1]) out.add(m[1]);
2482
+ }
2483
+ return out;
2484
+ }
2485
+ function countIntersection(a, b) {
2486
+ let n = 0;
2487
+ for (const x of a) if (b.has(x)) n++;
2488
+ return n;
2489
+ }
2490
+ async function trySemanticSimilarities(ctx, target, others) {
2491
+ let mod;
2492
+ try {
2493
+ mod = await import("@hiveai/embeddings");
2494
+ } catch {
2495
+ return null;
2496
+ }
2497
+ const result = await mod.semanticSearch(
2498
+ ctx.paths,
2499
+ target.memory.body,
2500
+ { limit: others.length }
2501
+ );
2502
+ if (!result) return null;
2503
+ const map = /* @__PURE__ */ new Map();
2504
+ for (const hit of result.hits) map.set(hit.id, hit.score);
2505
+ return map;
2506
+ }
2507
+
2508
+ // src/tools/precommit-check.ts
2509
+ import { z as z28 } from "zod";
2510
+ var PreCommitCheckInputSchema = {
2511
+ diff: z28.string().optional().describe(
2512
+ "Raw unified diff text to scan. If omitted, only `paths` is used. When called from a pre-commit hook, pipe the output of `git diff --cached`."
2513
+ ),
2514
+ paths: z28.array(z28.string()).default([]).describe("Project-relative paths affected by the change. At least one of `diff` or `paths` should be provided."),
2515
+ block_on: z28.enum(["any", "high-confidence", "never"]).default("high-confidence").describe(
2516
+ "When to set should_block=true: 'any' = any warning blocks; 'high-confidence' = only warnings from authoritative/trusted memories block; 'never' = report only, never block."
2517
+ ),
2518
+ semantic: z28.boolean().default(true).describe("Enable semantic search in anti_patterns_check (requires embeddings index).")
2519
+ };
2520
+ async function preCommitCheck(input, ctx) {
2521
+ if (!input.diff && input.paths.length === 0) {
2522
+ return {
2523
+ should_block: false,
2524
+ summary: { anti_patterns: 0, relevant_memories: 0, stale_anchors: 0 },
2525
+ warnings: [],
2526
+ relevant_memories: [],
2527
+ stale_anchors: [],
2528
+ notice: "Nothing to check \u2014 provide either `diff` or `paths`."
2529
+ };
2530
+ }
2531
+ const apResult = await antiPatternsCheck({
2532
+ diff: input.diff,
2533
+ paths: input.paths,
2534
+ limit: 20,
2535
+ semantic: input.semantic
2536
+ }, ctx);
2537
+ const relevant = input.paths.length > 0 ? await memForFiles({ files: input.paths, include_module_contexts: false, track: false }, ctx) : { by_anchor: [], by_module: [], by_domain: [], module_contexts: [], inferred_modules: [] };
2538
+ const relevantMatches = [...relevant.by_anchor, ...relevant.by_module];
2539
+ const verifyResult = input.paths.length > 0 ? await memVerify({ update: false, id: void 0 }, ctx) : { results: [], summary: { checked: 0, fresh: 0, stale: 0, anchorless_skipped: 0, updated: 0 } };
2540
+ const filesTouching = new Set(relevantMatches.map((m) => m.id));
2541
+ const staleHits = verifyResult.results.filter((r) => r.stale && filesTouching.has(r.id));
2542
+ const blockOn = input.block_on;
2543
+ let should_block = false;
2544
+ if (blockOn !== "never") {
2545
+ const high = apResult.warnings.filter(
2546
+ (w) => w.confidence === "authoritative" || w.confidence === "trusted"
2547
+ );
2548
+ if (blockOn === "any" && (apResult.warnings.length > 0 || staleHits.length > 0)) should_block = true;
2549
+ if (blockOn === "high-confidence" && (high.length > 0 || staleHits.length > 0)) should_block = true;
2550
+ }
2551
+ const relevant_memories = relevantMatches.slice(0, 8).map((m) => ({
2552
+ id: m.id,
2553
+ type: m.type,
2554
+ confidence: String(m.confidence),
2555
+ body_preview: (m.body ?? "").split("\n").slice(0, 4).join("\n").slice(0, 250)
2556
+ }));
2557
+ return {
2558
+ should_block,
2559
+ summary: {
2560
+ anti_patterns: apResult.warnings.length,
2561
+ relevant_memories: relevant_memories.length,
2562
+ stale_anchors: staleHits.length
2563
+ },
2564
+ warnings: apResult.warnings,
2565
+ relevant_memories,
2566
+ stale_anchors: staleHits.map((r) => ({
2567
+ id: r.id,
2568
+ // The matching `relevantMatches` entry tells us which paths overlap.
2569
+ paths: relevantMatches.find((m) => m.id === r.id) ? input.paths.filter((p) => relevantMatches.some((m) => m.id === r.id)) : [],
2570
+ body_preview: r.reason ?? "anchored code drifted; verify before relying on this memory"
2571
+ }))
2572
+ };
2573
+ }
2574
+
2575
+ // src/prompts/bootstrap-project.ts
2576
+ import { z as z29 } from "zod";
2070
2577
  var BootstrapProjectArgsSchema = {
2071
- module: z25.string().optional().describe(
2578
+ module: z29.string().optional().describe(
2072
2579
  "Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
2073
2580
  ),
2074
- focus: z25.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
2581
+ focus: z29.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
2075
2582
  };
2076
2583
  var ROOT_TEMPLATE = `# Project context
2077
2584
 
@@ -2153,10 +2660,10 @@ ${template}\`\`\`
2153
2660
  }
2154
2661
 
2155
2662
  // src/prompts/post-task.ts
2156
- import { z as z26 } from "zod";
2663
+ import { z as z30 } from "zod";
2157
2664
  var PostTaskArgsSchema = {
2158
- task_summary: z26.string().optional().describe("One sentence describing what you just did"),
2159
- files_touched: z26.array(z26.string()).optional().describe("Files you created or modified during the task")
2665
+ task_summary: z30.string().optional().describe("One sentence describing what you just did"),
2666
+ files_touched: z30.array(z30.string()).optional().describe("Files you created or modified during the task")
2160
2667
  };
2161
2668
  function postTaskPrompt(args, ctx) {
2162
2669
  const taskLine = args.task_summary ? `
@@ -2238,12 +2745,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
2238
2745
  }
2239
2746
 
2240
2747
  // src/prompts/import-docs.ts
2241
- import { z as z27 } from "zod";
2748
+ import { z as z31 } from "zod";
2242
2749
  var ImportDocsArgsSchema = {
2243
- content: z27.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
2244
- source: z27.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
2245
- scope: z27.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
2246
- dry_run: z27.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
2750
+ content: z31.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
2751
+ source: z31.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
2752
+ scope: z31.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
2753
+ dry_run: z31.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
2247
2754
  };
2248
2755
  function importDocsPrompt(args, ctx) {
2249
2756
  const sourceLine = args.source ? `
@@ -2379,7 +2886,7 @@ function summarizeTools(events) {
2379
2886
 
2380
2887
  // src/server.ts
2381
2888
  var SERVER_NAME = "haive";
2382
- var SERVER_VERSION = "0.5.0";
2889
+ var SERVER_VERSION = "0.6.0";
2383
2890
  function jsonResult(data) {
2384
2891
  return {
2385
2892
  content: [
@@ -2926,6 +3433,104 @@ function createHaiveServer(options = {}) {
2926
3433
  return jsonResult(await antiPatternsCheck(input, context));
2927
3434
  }
2928
3435
  );
3436
+ server.tool(
3437
+ "mem_distill",
3438
+ [
3439
+ "Cluster recurring observations / failed attempts so a human can collapse",
3440
+ "N similar memories into one richer convention/gotcha. Cheap heuristic",
3441
+ "(anchor path overlap + body keyword overlap) \u2014 no embeddings required.",
3442
+ "",
3443
+ "USE periodically (e.g. monthly) to prevent memory pollution from agents",
3444
+ "saving the same observation many times.",
3445
+ "",
3446
+ "PARAMETERS:",
3447
+ " since_days \u2014 only consider memories from the last N days (default 30)",
3448
+ " min_cluster \u2014 minimum cluster size to surface (default 3)",
3449
+ " type_filter \u2014 'gotcha' | 'attempt' | 'all' (default 'gotcha')",
3450
+ " scope \u2014 'personal' | 'team' | 'module' | 'any' (default 'any')",
3451
+ "",
3452
+ "RETURNS: { scanned, singletons, clusters: [{ suggested_topic, member_ids, ... }] }",
3453
+ "Output is advisory \u2014 nothing is written to disk."
3454
+ ].join("\n"),
3455
+ MemDistillInputSchema,
3456
+ async (input) => {
3457
+ tracker.record("mem_distill", `${input.type_filter}/since=${input.since_days}d`);
3458
+ return jsonResult(await memDistill(input, context));
3459
+ }
3460
+ );
3461
+ server.tool(
3462
+ "why_this_decision",
3463
+ [
3464
+ "Trace the genealogy of a memory (especially decision/architecture):",
3465
+ "the memory itself + memories explicitly linked via related_ids + memories",
3466
+ "anchored to overlapping paths + recent commits touching those paths.",
3467
+ "",
3468
+ "USE WHEN you find a memory and need to understand WHY it was made and",
3469
+ "what surrounds it. One call instead of 4-5 manual lookups.",
3470
+ "",
3471
+ "PARAMETERS:",
3472
+ " id \u2014 memory id (required)",
3473
+ " git_log_limit \u2014 how many recent commits per anchor path (default 5)",
3474
+ "",
3475
+ "RETURNS: { decision, related: [...], path_neighbors: [...], recent_commits: [...] }"
3476
+ ].join("\n"),
3477
+ WhyThisDecisionInputSchema,
3478
+ async (input) => {
3479
+ tracker.record("why_this_decision", input.id);
3480
+ return jsonResult(await whyThisDecision(input, context));
3481
+ }
3482
+ );
3483
+ server.tool(
3484
+ "mem_conflicts_with",
3485
+ [
3486
+ "Detect memories that potentially CONTRADICT a given memory.",
3487
+ "",
3488
+ "USE BEFORE relying on a memory's advice \u2014 surfaces 'another memory says",
3489
+ "the opposite'. Detection uses several heuristics layered together:",
3490
+ "",
3491
+ " 1. Opposite status \u2014 validated vs rejected on overlapping topic",
3492
+ " 2. attempt-vs-convention on overlapping anchor paths",
3493
+ " 3. Polarity keywords \u2014 'use X' vs 'do not use X' among semantic neighbors",
3494
+ " 4. Explicit #contradicts:<id> tags in either body",
3495
+ "",
3496
+ "PARAMETERS:",
3497
+ " id \u2014 memory id to check (required)",
3498
+ " min_score \u2014 minimum cosine similarity for semantic neighbors (default 0.5)",
3499
+ " semantic \u2014 use embeddings (default true)",
3500
+ "",
3501
+ "RETURNS: { found, target, scanned, conflicts: [{ id, reasons, similarity, ... }] }"
3502
+ ].join("\n"),
3503
+ MemConflictsInputSchema,
3504
+ async (input) => {
3505
+ tracker.record("mem_conflicts_with", input.id);
3506
+ return jsonResult(await memConflicts(input, context));
3507
+ }
3508
+ );
3509
+ server.tool(
3510
+ "pre_commit_check",
3511
+ [
3512
+ "One-shot 'should I block this commit?' check. Combines three signals:",
3513
+ "",
3514
+ " 1. anti_patterns_check \u2014 known gotchas/attempts that match the diff",
3515
+ " 2. mem_for_files \u2014 conventions/decisions anchored to touched files",
3516
+ " 3. mem_verify \u2014 memories whose anchors are stale (knowledge may be wrong)",
3517
+ "",
3518
+ "USE FROM A GIT HOOK or before finalizing a non-trivial change.",
3519
+ "",
3520
+ "PARAMETERS:",
3521
+ " diff \u2014 raw unified diff text (e.g. `git diff --cached`)",
3522
+ " paths \u2014 affected file paths (project-relative)",
3523
+ " block_on \u2014 'any' | 'high-confidence' (default) | 'never'",
3524
+ " semantic \u2014 use embeddings in anti_patterns_check (default true)",
3525
+ "",
3526
+ "RETURNS: { should_block, summary, warnings, relevant_memories, stale_anchors }"
3527
+ ].join("\n"),
3528
+ PreCommitCheckInputSchema,
3529
+ async (input) => {
3530
+ tracker.record("pre_commit_check", `${input.paths.length}p`);
3531
+ return jsonResult(await preCommitCheck(input, context));
3532
+ }
3533
+ );
2929
3534
  server.tool(
2930
3535
  "mem_diff",
2931
3536
  [