@hivelore/mcp 0.31.0 → 0.34.1
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 +279 -1065
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +46 -201
- package/dist/server.js +281 -1072
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1147,7 +1147,12 @@ import {
|
|
|
1147
1147
|
import { z as z15 } from "zod";
|
|
1148
1148
|
var ProposeSensorInputSchema = {
|
|
1149
1149
|
memory_id: z15.string().min(1).describe("Id of the gotcha/attempt memory this sensor protects."),
|
|
1150
|
-
|
|
1150
|
+
kind: z15.enum(["regex", "shell", "test"]).default("regex").describe(
|
|
1151
|
+
"regex = pattern matched on added diff lines (default). shell|test = a COMMAND the gate runs when the diff touches the sensor's paths \u2014 routes the team's own oracle (an existing test, an invariant script) to this lesson. Command sensors only execute where enforcement.runCommandSensors=true."
|
|
1152
|
+
),
|
|
1153
|
+
pattern: z15.string().optional().describe("kind=regex: regex matching the FAULTY usage (the risky call/token), e.g. 'stripe\\.paymentIntents\\.create'."),
|
|
1154
|
+
command: z15.string().optional().describe("kind=shell|test: command to execute (e.g. 'npx vitest run tests/payments/refund.spec.ts'). Non-zero exit = the lesson fires."),
|
|
1155
|
+
timeout_ms: z15.number().int().positive().optional().describe("kind=shell|test: max runtime before the executor kills the command (default 120000)."),
|
|
1151
1156
|
absent: z15.string().optional().describe(
|
|
1152
1157
|
"Regex for the CORRECT-usage marker (e.g. 'idempotencyKey'). When it appears in the window around a match, the catch is suppressed \u2014 this is what makes the sensor discriminate the faulty call from the correct one. STRONGLY recommended for 'X without Y' lessons."
|
|
1153
1158
|
),
|
|
@@ -1189,20 +1194,62 @@ async function readPresumedCorrectTargets(root, relPaths) {
|
|
|
1189
1194
|
}
|
|
1190
1195
|
return targets;
|
|
1191
1196
|
}
|
|
1197
|
+
function runCommandForValidation(command, root, timeoutMs = 12e4) {
|
|
1198
|
+
try {
|
|
1199
|
+
execSync(`bash -c ${JSON.stringify(command)}`, {
|
|
1200
|
+
cwd: root,
|
|
1201
|
+
timeout: timeoutMs,
|
|
1202
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
1203
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1204
|
+
});
|
|
1205
|
+
return { status: "passed", detail: "exit 0" };
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
const e = err;
|
|
1208
|
+
const out = `${e.stdout?.toString() ?? ""}
|
|
1209
|
+
${e.stderr?.toString() ?? ""}`.split("\n").filter(Boolean).slice(-8).join("\n");
|
|
1210
|
+
if (e.killed) return { status: "unrunnable", detail: `timed out after ${timeoutMs}ms` };
|
|
1211
|
+
if (e.status === 127 || e.status === 126 || e.code === "ENOENT") {
|
|
1212
|
+
return { status: "unrunnable", detail: e.status === 126 ? "command found but not executable" : "command not found" };
|
|
1213
|
+
}
|
|
1214
|
+
return { status: "failed", detail: out || `exit ${e.status ?? "?"}` };
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1192
1217
|
async function proposeSensor(input, ctx) {
|
|
1193
1218
|
if (!existsSync15(ctx.paths.memoriesDir)) {
|
|
1194
1219
|
throw new Error(`No .ai/memories at ${ctx.paths.root}. Run 'hivelore init' first.`);
|
|
1195
1220
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
if (input.
|
|
1199
|
-
|
|
1221
|
+
const kind = input.kind ?? "regex";
|
|
1222
|
+
if (kind === "regex") {
|
|
1223
|
+
if (!input.pattern) {
|
|
1224
|
+
return {
|
|
1225
|
+
accepted: false,
|
|
1226
|
+
memory_id: input.memory_id,
|
|
1227
|
+
severity: input.severity,
|
|
1228
|
+
reason: "invalid-regex",
|
|
1229
|
+
guidance: "kind=regex requires a `pattern`.",
|
|
1230
|
+
self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
try {
|
|
1234
|
+
new RegExp(input.pattern, input.flags ?? "");
|
|
1235
|
+
if (input.absent) new RegExp(input.absent, input.flags ?? "");
|
|
1236
|
+
} catch (err) {
|
|
1237
|
+
return {
|
|
1238
|
+
accepted: false,
|
|
1239
|
+
memory_id: input.memory_id,
|
|
1240
|
+
severity: input.severity,
|
|
1241
|
+
reason: "invalid-regex",
|
|
1242
|
+
guidance: `The pattern or absent regex does not compile: ${String(err)}`,
|
|
1243
|
+
self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
} else if (!input.command?.trim()) {
|
|
1200
1247
|
return {
|
|
1201
1248
|
accepted: false,
|
|
1202
1249
|
memory_id: input.memory_id,
|
|
1203
1250
|
severity: input.severity,
|
|
1204
|
-
reason: "invalid-
|
|
1205
|
-
guidance:
|
|
1251
|
+
reason: "invalid-command",
|
|
1252
|
+
guidance: "kind=shell|test requires a `command` (the check the gate will execute).",
|
|
1206
1253
|
self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
|
|
1207
1254
|
};
|
|
1208
1255
|
}
|
|
@@ -1211,6 +1258,43 @@ async function proposeSensor(input, ctx) {
|
|
|
1211
1258
|
if (!found) {
|
|
1212
1259
|
throw new Error(`No memory found with id ${input.memory_id}`);
|
|
1213
1260
|
}
|
|
1261
|
+
if (kind !== "regex") {
|
|
1262
|
+
const verdictCmd = runCommandForValidation(input.command.trim(), ctx.paths.root, input.timeout_ms);
|
|
1263
|
+
const anchorPathsCmd = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
|
|
1264
|
+
if (verdictCmd.status !== "passed" && input.severity === "block") {
|
|
1265
|
+
return {
|
|
1266
|
+
accepted: false,
|
|
1267
|
+
memory_id: input.memory_id,
|
|
1268
|
+
severity: input.severity,
|
|
1269
|
+
reason: verdictCmd.status === "unrunnable" ? "command-unrunnable" : "fails-on-current",
|
|
1270
|
+
guidance: verdictCmd.status === "unrunnable" ? `The command could not run (${verdictCmd.detail}). Fix the command (or timeout_ms), then re-propose.` : `The command FAILS on the current tree \u2014 the presumed-correct state must pass, or the gate would block every commit. Revert the faulty diff (or fix the check), then re-propose. Output tail:
|
|
1271
|
+
${verdictCmd.detail}`,
|
|
1272
|
+
self_check: { silent_on_current: false, fires_on_bad: null, fired_on: anchorPathsCmd }
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
const sensorCmd = {
|
|
1276
|
+
kind,
|
|
1277
|
+
command: input.command.trim(),
|
|
1278
|
+
...input.timeout_ms ? { timeout_ms: input.timeout_ms } : {},
|
|
1279
|
+
paths: anchorPathsCmd,
|
|
1280
|
+
message: input.message?.trim() || deriveMessage(found.memory.body, input.command.trim(), void 0),
|
|
1281
|
+
severity: input.severity,
|
|
1282
|
+
autogen: false,
|
|
1283
|
+
last_fired: null
|
|
1284
|
+
};
|
|
1285
|
+
const nextCmd = {
|
|
1286
|
+
frontmatter: { ...found.memory.frontmatter, sensor: sensorCmd },
|
|
1287
|
+
body: found.memory.body
|
|
1288
|
+
};
|
|
1289
|
+
await writeFile8(found.filePath, serializeMemory7(nextCmd), "utf8");
|
|
1290
|
+
return {
|
|
1291
|
+
accepted: true,
|
|
1292
|
+
memory_id: input.memory_id,
|
|
1293
|
+
severity: input.severity,
|
|
1294
|
+
guidance: verdictCmd.status === "passed" ? "Command oracle passes on the current tree; the gate now runs it when the diff touches the sensor's paths (requires enforcement.runCommandSensors=true)." : `Accepted at warn severity, but note: ${verdictCmd.status} on the current tree (${verdictCmd.detail}).`,
|
|
1295
|
+
self_check: { silent_on_current: verdictCmd.status === "passed", fires_on_bad: null, fired_on: [] }
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1214
1298
|
const anchorPaths = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
|
|
1215
1299
|
const currentTargets = await readPresumedCorrectTargets(ctx.paths.root, anchorPaths);
|
|
1216
1300
|
const badExamples = [
|
|
@@ -1271,11 +1355,14 @@ var MemTriedInputSchema = {
|
|
|
1271
1355
|
paths: z16.array(z16.string()).default([]).describe("Anchor file paths this applies to"),
|
|
1272
1356
|
author: z16.string().optional().describe("Author handle or email"),
|
|
1273
1357
|
sensor: z16.object({
|
|
1274
|
-
|
|
1275
|
-
|
|
1358
|
+
kind: z16.enum(["regex", "shell", "test"]).default("regex").describe("regex pattern, or a shell/test COMMAND the gate executes (behaviour bridge)"),
|
|
1359
|
+
pattern: z16.string().optional().describe("kind=regex: regex matching the FAULTY usage (added diff lines)"),
|
|
1360
|
+
command: z16.string().optional().describe("kind=shell|test: command the gate runs when the diff touches the sensor's paths (non-zero exit = lesson fires)"),
|
|
1361
|
+
timeout_ms: z16.number().int().positive().optional().describe("kind=shell|test: max runtime (default 120000)"),
|
|
1362
|
+
absent: z16.string().optional().describe("kind=regex: regex marking CORRECT usage nearby \u2014 excludes it from firing"),
|
|
1276
1363
|
severity: z16.enum(["warn", "block"]).default("block").describe("block = deterministic gate refusal"),
|
|
1277
1364
|
message: z16.string().optional().describe("Self-correction message shown when the sensor fires"),
|
|
1278
|
-
bad_example: z16.string().optional().describe("
|
|
1365
|
+
bad_example: z16.string().optional().describe("kind=regex: code snippet the sensor MUST fire on (validation)")
|
|
1279
1366
|
}).optional().describe(
|
|
1280
1367
|
"ONE-SHOT loop close: validate and attach a sensor in the same call (equivalent to a follow-up propose_sensor). Validated against HEAD \u2014 silent on current code, fires on the bad example. If rejected, the attempt is still saved and the verdict tells you how to revise."
|
|
1281
1368
|
)
|
|
@@ -1311,7 +1398,10 @@ async function memTried(input, ctx) {
|
|
|
1311
1398
|
const verdict = await proposeSensor(
|
|
1312
1399
|
{
|
|
1313
1400
|
memory_id: frontmatter.id,
|
|
1401
|
+
kind: input.sensor.kind ?? "regex",
|
|
1314
1402
|
pattern: input.sensor.pattern,
|
|
1403
|
+
command: input.sensor.command,
|
|
1404
|
+
timeout_ms: input.sensor.timeout_ms,
|
|
1315
1405
|
absent: input.sensor.absent,
|
|
1316
1406
|
severity: input.sensor.severity ?? "block",
|
|
1317
1407
|
message: input.sensor.message,
|
|
@@ -1446,83 +1536,17 @@ async function writeDraft(ctx, draft) {
|
|
|
1446
1536
|
return file;
|
|
1447
1537
|
}
|
|
1448
1538
|
|
|
1449
|
-
// src/tools/mem-
|
|
1450
|
-
import {
|
|
1451
|
-
import { existsSync as
|
|
1452
|
-
import
|
|
1539
|
+
// src/tools/mem-session-end.ts
|
|
1540
|
+
import { writeFile as writeFile12, mkdir as mkdir6 } from "fs/promises";
|
|
1541
|
+
import { existsSync as existsSync19 } from "fs";
|
|
1542
|
+
import path9 from "path";
|
|
1453
1543
|
import {
|
|
1454
1544
|
buildFrontmatter as buildFrontmatter3,
|
|
1455
|
-
|
|
1545
|
+
loadMemoriesFromDir as loadMemoriesFromDir15,
|
|
1456
1546
|
memoryFilePath as memoryFilePath4,
|
|
1457
1547
|
serializeMemory as serializeMemory10
|
|
1458
1548
|
} from "@hivelore/core";
|
|
1459
1549
|
import { z as z18 } from "zod";
|
|
1460
|
-
var MemObserveInputSchema = {
|
|
1461
|
-
what: z18.string().min(1).describe("Short title: what did you observe? (e.g. 'MobilePaymentController has two @RequestBody on handleWebhook')"),
|
|
1462
|
-
where: z18.string().min(1).describe("File path(s) where the issue lives \u2014 be specific"),
|
|
1463
|
-
impact: z18.string().min(1).describe("What breaks or could break because of this (e.g. 'Spring MVC rejects the handler at startup')"),
|
|
1464
|
-
fix: z18.string().optional().describe("Suggested fix or workaround (optional \u2014 leave empty if unknown)"),
|
|
1465
|
-
scope: z18.enum(["personal", "team", "module"]).default("team").describe("Visibility scope \u2014 defaults to team since discoveries benefit everyone"),
|
|
1466
|
-
module: z18.string().optional().describe("Module name (required when scope=module)"),
|
|
1467
|
-
tags: z18.array(z18.string()).default([]).describe("Tags for filtering"),
|
|
1468
|
-
author: z18.string().optional().describe("Author handle or email"),
|
|
1469
|
-
force: z18.boolean().default(false).describe(
|
|
1470
|
-
"Save even if the observation looks like generic, guessable knowledge. By default, low-specificity observations (things a capable model already knows) are SKIPPED to keep the corpus high-signal \u2014 only unguessable, team-specific discoveries are worth storing."
|
|
1471
|
-
)
|
|
1472
|
-
};
|
|
1473
|
-
async function memObserve(input, ctx) {
|
|
1474
|
-
if (!existsSync18(ctx.paths.haiveDir)) {
|
|
1475
|
-
throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'hivelore init' first.`);
|
|
1476
|
-
}
|
|
1477
|
-
const signalText = [input.what, input.impact, input.fix ?? ""].join(" ");
|
|
1478
|
-
if (!input.force && isLikelyGuessable(signalText)) {
|
|
1479
|
-
return {
|
|
1480
|
-
id: "",
|
|
1481
|
-
scope: input.scope,
|
|
1482
|
-
file_path: "",
|
|
1483
|
-
skipped: true,
|
|
1484
|
-
reason: "Observation looks like generic, guessable knowledge (low specificity) \u2014 not saved. Capture only arbitrary, team-specific facts (exact names, values, formats). Pass force=true to override."
|
|
1485
|
-
};
|
|
1486
|
-
}
|
|
1487
|
-
const slug = input.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 6).join("-");
|
|
1488
|
-
const anchorPaths = input.where.split(/[,\n]/).map((s) => s.trim()).filter(Boolean);
|
|
1489
|
-
const baseFm = buildFrontmatter3({
|
|
1490
|
-
type: "gotcha",
|
|
1491
|
-
slug,
|
|
1492
|
-
scope: input.scope,
|
|
1493
|
-
module: input.module,
|
|
1494
|
-
tags: input.tags,
|
|
1495
|
-
paths: anchorPaths,
|
|
1496
|
-
author: input.author
|
|
1497
|
-
});
|
|
1498
|
-
const frontmatter = { ...baseFm, status: "validated" };
|
|
1499
|
-
const lines = [`# ${input.what}`, ""];
|
|
1500
|
-
lines.push(`**Where:** \`${input.where}\``);
|
|
1501
|
-
lines.push("", `**Impact:** ${input.impact}`);
|
|
1502
|
-
if (input.fix) {
|
|
1503
|
-
lines.push("", `**Fix/workaround:** ${input.fix}`);
|
|
1504
|
-
}
|
|
1505
|
-
const body = lines.join("\n") + "\n";
|
|
1506
|
-
const file = memoryFilePath4(ctx.paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
1507
|
-
await mkdir5(path8.dirname(file), { recursive: true });
|
|
1508
|
-
if (existsSync18(file)) {
|
|
1509
|
-
throw new Error(`Memory already exists at ${file}`);
|
|
1510
|
-
}
|
|
1511
|
-
await writeFile11(file, serializeMemory10({ frontmatter, body }), "utf8");
|
|
1512
|
-
return { id: frontmatter.id, scope: frontmatter.scope, file_path: file };
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
// src/tools/mem-session-end.ts
|
|
1516
|
-
import { writeFile as writeFile13, mkdir as mkdir7 } from "fs/promises";
|
|
1517
|
-
import { existsSync as existsSync20 } from "fs";
|
|
1518
|
-
import path10 from "path";
|
|
1519
|
-
import {
|
|
1520
|
-
buildFrontmatter as buildFrontmatter4,
|
|
1521
|
-
loadMemoriesFromDir as loadMemoriesFromDir15,
|
|
1522
|
-
memoryFilePath as memoryFilePath5,
|
|
1523
|
-
serializeMemory as serializeMemory11
|
|
1524
|
-
} from "@hivelore/core";
|
|
1525
|
-
import { z as z19 } from "zod";
|
|
1526
1550
|
|
|
1527
1551
|
// src/session-tracker.ts
|
|
1528
1552
|
import {
|
|
@@ -1531,12 +1555,12 @@ import {
|
|
|
1531
1555
|
loadConfig as loadConfig2,
|
|
1532
1556
|
writeSessionHandoff
|
|
1533
1557
|
} from "@hivelore/core";
|
|
1534
|
-
import { mkdir as
|
|
1535
|
-
import { existsSync as
|
|
1536
|
-
import
|
|
1558
|
+
import { mkdir as mkdir5, writeFile as writeFile11, rm } from "fs/promises";
|
|
1559
|
+
import { existsSync as existsSync18 } from "fs";
|
|
1560
|
+
import path8 from "path";
|
|
1537
1561
|
import { execSync as execSync2 } from "child_process";
|
|
1538
1562
|
function pendingDistillPath(ctx) {
|
|
1539
|
-
return
|
|
1563
|
+
return path8.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
|
|
1540
1564
|
}
|
|
1541
1565
|
var SessionTracker = class {
|
|
1542
1566
|
events = [];
|
|
@@ -1563,7 +1587,7 @@ var SessionTracker = class {
|
|
|
1563
1587
|
this.shutdownRegistered = true;
|
|
1564
1588
|
const save = async () => {
|
|
1565
1589
|
const writingTools = this.events.filter(
|
|
1566
|
-
(e) => ["mem_save", "mem_tried", "
|
|
1590
|
+
(e) => ["mem_save", "mem_tried", "mem_update", "bootstrap_project_save"].includes(e.tool)
|
|
1567
1591
|
);
|
|
1568
1592
|
const totalCalls = this.events.length;
|
|
1569
1593
|
if (totalCalls === 0) return;
|
|
@@ -1640,7 +1664,7 @@ var SessionTracker = class {
|
|
|
1640
1664
|
(e) => e.tool === "mem_session_end" && !e.summary?.startsWith("Auto-captured")
|
|
1641
1665
|
);
|
|
1642
1666
|
const isSubstantialSession = totalCalls >= 3 || writingTools.length > 0;
|
|
1643
|
-
if (!ranPostTask && isSubstantialSession &&
|
|
1667
|
+
if (!ranPostTask && isSubstantialSession && existsSync18(this.ctx.paths.haiveDir)) {
|
|
1644
1668
|
try {
|
|
1645
1669
|
const memoriesSaved = writingTools.map((e) => e.summary ?? "").filter(Boolean).slice(0, 20);
|
|
1646
1670
|
const payload = {
|
|
@@ -1653,9 +1677,9 @@ var SessionTracker = class {
|
|
|
1653
1677
|
...gitDiff ? { git_diff: gitDiff } : {},
|
|
1654
1678
|
...recapId ? { recap_id: recapId } : {}
|
|
1655
1679
|
};
|
|
1656
|
-
const cacheDir =
|
|
1657
|
-
await
|
|
1658
|
-
await
|
|
1680
|
+
const cacheDir = path8.join(this.ctx.paths.haiveDir, ".cache");
|
|
1681
|
+
await mkdir5(cacheDir, { recursive: true });
|
|
1682
|
+
await writeFile11(
|
|
1659
1683
|
pendingDistillPath(this.ctx),
|
|
1660
1684
|
JSON.stringify(payload, null, 2) + "\n",
|
|
1661
1685
|
"utf8"
|
|
@@ -1674,7 +1698,7 @@ var SessionTracker = class {
|
|
|
1674
1698
|
};
|
|
1675
1699
|
async function clearPendingDistill(ctx) {
|
|
1676
1700
|
const p = pendingDistillPath(ctx);
|
|
1677
|
-
if (
|
|
1701
|
+
if (existsSync18(p)) {
|
|
1678
1702
|
try {
|
|
1679
1703
|
await rm(p);
|
|
1680
1704
|
} catch {
|
|
@@ -1691,15 +1715,15 @@ function summarizeTools(events) {
|
|
|
1691
1715
|
|
|
1692
1716
|
// src/tools/mem-session-end.ts
|
|
1693
1717
|
var MemSessionEndInputSchema = {
|
|
1694
|
-
goal:
|
|
1695
|
-
accomplished:
|
|
1696
|
-
discoveries:
|
|
1718
|
+
goal: z18.string().min(1).describe("What you were trying to accomplish this session (1\u20132 sentences)"),
|
|
1719
|
+
accomplished: z18.string().describe("What was actually done \u2014 bullet list recommended"),
|
|
1720
|
+
discoveries: z18.string().default("").describe(
|
|
1697
1721
|
"Any bugs, inconsistencies, surprises, or missing knowledge found during this session. Empty if nothing surprising was found."
|
|
1698
1722
|
),
|
|
1699
|
-
files_touched:
|
|
1700
|
-
next_steps:
|
|
1701
|
-
scope:
|
|
1702
|
-
module:
|
|
1723
|
+
files_touched: z18.array(z18.string()).default([]).describe("Key files that were read or modified \u2014 used as anchor paths"),
|
|
1724
|
+
next_steps: z18.string().default("").describe("What should happen next (for the next session or a teammate)"),
|
|
1725
|
+
scope: z18.enum(["personal", "team", "module"]).default("personal").describe("Visibility: personal = private to you, team = shared with the team"),
|
|
1726
|
+
module: z18.string().optional().describe("Module name (required when scope=module)")
|
|
1703
1727
|
};
|
|
1704
1728
|
function recapTopic(scope, module) {
|
|
1705
1729
|
return module ? `session-recap-${scope}-${module}` : `session-recap-${scope}`;
|
|
@@ -1729,23 +1753,23 @@ ${input.next_steps}`);
|
|
|
1729
1753
|
return lines.join("\n");
|
|
1730
1754
|
}
|
|
1731
1755
|
async function memSessionEnd(input, ctx) {
|
|
1732
|
-
if (!
|
|
1756
|
+
if (!existsSync19(ctx.paths.haiveDir)) {
|
|
1733
1757
|
throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'hivelore init' first.`);
|
|
1734
1758
|
}
|
|
1735
1759
|
const body = buildBody(input);
|
|
1736
1760
|
const topic = recapTopic(input.scope, input.module);
|
|
1737
1761
|
const normalizedFiles = input.files_touched.map((p) => {
|
|
1738
|
-
if (!p || !
|
|
1739
|
-
const rel =
|
|
1762
|
+
if (!p || !path9.isAbsolute(p)) return p;
|
|
1763
|
+
const rel = path9.relative(ctx.paths.root, p);
|
|
1740
1764
|
return rel.startsWith("..") ? p : rel;
|
|
1741
1765
|
});
|
|
1742
1766
|
const invalidPaths = normalizedFiles.filter(
|
|
1743
|
-
(p) => !
|
|
1767
|
+
(p) => !existsSync19(path9.resolve(ctx.paths.root, p))
|
|
1744
1768
|
);
|
|
1745
1769
|
if (invalidPaths.length > 0) {
|
|
1746
1770
|
console.warn(`[haive] session end: anchor path(s) not found: ${invalidPaths.join(", ")}`);
|
|
1747
1771
|
}
|
|
1748
|
-
const existing =
|
|
1772
|
+
const existing = existsSync19(ctx.paths.memoriesDir) ? await loadMemoriesFromDir15(ctx.paths.memoriesDir) : [];
|
|
1749
1773
|
const topicMatch = existing.find(
|
|
1750
1774
|
({ memory }) => memory.frontmatter.topic === topic && memory.frontmatter.scope === input.scope && (!input.module || memory.frontmatter.module === input.module)
|
|
1751
1775
|
);
|
|
@@ -1761,9 +1785,9 @@ async function memSessionEnd(input, ctx) {
|
|
|
1761
1785
|
paths: normalizedFiles.length ? normalizedFiles : fm.anchor.paths
|
|
1762
1786
|
}
|
|
1763
1787
|
};
|
|
1764
|
-
await
|
|
1788
|
+
await writeFile12(
|
|
1765
1789
|
topicMatch.filePath,
|
|
1766
|
-
|
|
1790
|
+
serializeMemory10({ frontmatter: newFrontmatter, body }),
|
|
1767
1791
|
"utf8"
|
|
1768
1792
|
);
|
|
1769
1793
|
await clearPendingDistill(ctx);
|
|
@@ -1775,7 +1799,7 @@ async function memSessionEnd(input, ctx) {
|
|
|
1775
1799
|
revision_count: revisionCount
|
|
1776
1800
|
};
|
|
1777
1801
|
}
|
|
1778
|
-
const frontmatter =
|
|
1802
|
+
const frontmatter = buildFrontmatter3({
|
|
1779
1803
|
type: "session_recap",
|
|
1780
1804
|
slug: "recap",
|
|
1781
1805
|
scope: input.scope,
|
|
@@ -1785,14 +1809,14 @@ async function memSessionEnd(input, ctx) {
|
|
|
1785
1809
|
topic,
|
|
1786
1810
|
status: "validated"
|
|
1787
1811
|
});
|
|
1788
|
-
const file =
|
|
1812
|
+
const file = memoryFilePath4(
|
|
1789
1813
|
ctx.paths,
|
|
1790
1814
|
frontmatter.scope,
|
|
1791
1815
|
frontmatter.id,
|
|
1792
1816
|
frontmatter.module
|
|
1793
1817
|
);
|
|
1794
|
-
await
|
|
1795
|
-
await
|
|
1818
|
+
await mkdir6(path9.dirname(file), { recursive: true });
|
|
1819
|
+
await writeFile12(file, serializeMemory10({ frontmatter, body }), "utf8");
|
|
1796
1820
|
await clearPendingDistill(ctx);
|
|
1797
1821
|
return {
|
|
1798
1822
|
id: frontmatter.id,
|
|
@@ -1804,9 +1828,9 @@ async function memSessionEnd(input, ctx) {
|
|
|
1804
1828
|
}
|
|
1805
1829
|
|
|
1806
1830
|
// src/tools/get-briefing.ts
|
|
1807
|
-
import { readFile as readFile6, writeFile as
|
|
1808
|
-
import { existsSync as
|
|
1809
|
-
import
|
|
1831
|
+
import { readFile as readFile6, writeFile as writeFile13, readdir as readdir4 } from "fs/promises";
|
|
1832
|
+
import { existsSync as existsSync21 } from "fs";
|
|
1833
|
+
import path11 from "path";
|
|
1810
1834
|
import {
|
|
1811
1835
|
allocateBudget,
|
|
1812
1836
|
assessBootstrapState,
|
|
@@ -1840,7 +1864,7 @@ import {
|
|
|
1840
1864
|
queryCodeMap,
|
|
1841
1865
|
readSessionHandoff,
|
|
1842
1866
|
resolveBriefingBudget,
|
|
1843
|
-
serializeMemory as
|
|
1867
|
+
serializeMemory as serializeMemory11,
|
|
1844
1868
|
specificityScore,
|
|
1845
1869
|
GUESSABLE_THRESHOLD,
|
|
1846
1870
|
tokenizeQuery as tokenizeQuery2,
|
|
@@ -1848,12 +1872,12 @@ import {
|
|
|
1848
1872
|
truncateToTokens,
|
|
1849
1873
|
writeBriefingMarker
|
|
1850
1874
|
} from "@hivelore/core";
|
|
1851
|
-
import { z as
|
|
1875
|
+
import { z as z19 } from "zod";
|
|
1852
1876
|
|
|
1853
1877
|
// src/tools/briefing-helpers.ts
|
|
1854
1878
|
import { readdir as readdir3, readFile as readFile5 } from "fs/promises";
|
|
1855
|
-
import { existsSync as
|
|
1856
|
-
import
|
|
1879
|
+
import { existsSync as existsSync20 } from "fs";
|
|
1880
|
+
import path10 from "path";
|
|
1857
1881
|
import {
|
|
1858
1882
|
classifyMemoryPriority as coreClassifyPriority,
|
|
1859
1883
|
isGlobPath,
|
|
@@ -1986,15 +2010,15 @@ async function trySemanticHits(ctx, task, limit) {
|
|
|
1986
2010
|
}
|
|
1987
2011
|
async function loadModuleContexts2(ctx, modules) {
|
|
1988
2012
|
if (modules.length === 0) return [];
|
|
1989
|
-
if (!
|
|
2013
|
+
if (!existsSync20(ctx.paths.modulesContextDir)) return [];
|
|
1990
2014
|
const available = new Set(
|
|
1991
2015
|
(await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
|
|
1992
2016
|
);
|
|
1993
2017
|
const out = [];
|
|
1994
2018
|
for (const m of modules) {
|
|
1995
2019
|
if (!available.has(m)) continue;
|
|
1996
|
-
const file =
|
|
1997
|
-
if (
|
|
2020
|
+
const file = path10.join(ctx.paths.modulesContextDir, m, "context.md");
|
|
2021
|
+
if (existsSync20(file)) {
|
|
1998
2022
|
out.push({ name: m, content: await readFile5(file, "utf8") });
|
|
1999
2023
|
}
|
|
2000
2024
|
}
|
|
@@ -2003,38 +2027,38 @@ async function loadModuleContexts2(ctx, modules) {
|
|
|
2003
2027
|
|
|
2004
2028
|
// src/tools/get-briefing.ts
|
|
2005
2029
|
var GetBriefingInputSchema = {
|
|
2006
|
-
task:
|
|
2030
|
+
task: z19.string().optional().describe(
|
|
2007
2031
|
"What you are about to do, in 1\u20132 sentences. Used to rank relevant memories semantically."
|
|
2008
2032
|
),
|
|
2009
|
-
files:
|
|
2010
|
-
max_tokens:
|
|
2033
|
+
files: z19.array(z19.string()).default([]).describe("Project-relative file paths the agent is currently looking at or about to edit"),
|
|
2034
|
+
max_tokens: z19.number().int().positive().default(8e3).describe(
|
|
2011
2035
|
"Approximate token budget for the entire briefing. Each section is allocated a share and truncated to fit."
|
|
2012
2036
|
),
|
|
2013
|
-
max_memories:
|
|
2014
|
-
include_project_context:
|
|
2015
|
-
dedupe_project_context:
|
|
2037
|
+
max_memories: z19.number().int().positive().default(8).describe("Cap on memories surfaced regardless of token budget"),
|
|
2038
|
+
include_project_context: z19.boolean().default(true),
|
|
2039
|
+
dedupe_project_context: z19.boolean().optional().describe(
|
|
2016
2040
|
"Token saver (default ON): skip re-emitting the project-context body if an identical copy was already sent within the last few minutes this session (the agent still has it). Set false to always include it."
|
|
2017
2041
|
),
|
|
2018
|
-
include_module_contexts:
|
|
2019
|
-
semantic:
|
|
2042
|
+
include_module_contexts: z19.boolean().default(true),
|
|
2043
|
+
semantic: z19.boolean().default(true).describe(
|
|
2020
2044
|
"Use semantic ranking when a task is provided (requires `hivelore embeddings index`)."
|
|
2021
2045
|
),
|
|
2022
|
-
include_stale:
|
|
2023
|
-
track:
|
|
2024
|
-
format:
|
|
2046
|
+
include_stale: z19.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
|
|
2047
|
+
track: z19.boolean().default(true).describe("Increment read_count on returned memories"),
|
|
2048
|
+
format: z19.enum(["full", "compact", "actions"]).default("full").describe(
|
|
2025
2049
|
"Output format: 'full' returns memory bodies (honors token budget via truncation); 'compact' returns a 1-line summary per memory (call mem_get for detail); 'actions' squeezes bodies to actionable bullet lines \u2014 fewer tokens vs full."
|
|
2026
2050
|
),
|
|
2027
|
-
symbols:
|
|
2051
|
+
symbols: z19.array(z19.string()).default([]).describe(
|
|
2028
2052
|
"Symbol names to look up in the code-map (e.g. ['PaymentService', 'TenantFilter']). Returns the file(s) exporting each symbol so agents don't need to grep. Requires `hivelore index code` to have been run."
|
|
2029
2053
|
),
|
|
2030
|
-
min_semantic_score:
|
|
2054
|
+
min_semantic_score: z19.number().min(0).max(1).default(0).describe(
|
|
2031
2055
|
"Drop semantic-only memory hits whose cosine score is below this threshold. Useful to avoid weakly-related noise when the task is short or the corpus is broad. Has no effect on memories matched via anchor/module/literal \u2014 those are always kept. Try 0.25\u20130.4 for stricter matching."
|
|
2032
2056
|
),
|
|
2033
|
-
budget_preset:
|
|
2057
|
+
budget_preset: z19.enum(["quick", "balanced", "deep"]).optional().describe(
|
|
2034
2058
|
"Shortcut token budget: 'quick' minimizes tokens/skip module CONTEXT slices; 'balanced' mirrors historical defaults; 'deep' uses a larger briefing. When set, overrides max_tokens, max_memories, and include_module_contexts."
|
|
2035
2059
|
)
|
|
2036
2060
|
};
|
|
2037
|
-
var GetBriefingZod =
|
|
2061
|
+
var GetBriefingZod = z19.object(GetBriefingInputSchema);
|
|
2038
2062
|
async function getBriefing(input, ctx) {
|
|
2039
2063
|
const resolvedBudget = resolveBriefingBudget(input.budget_preset, {
|
|
2040
2064
|
max_tokens: input.max_tokens,
|
|
@@ -2050,7 +2074,7 @@ async function getBriefing(input, ctx) {
|
|
|
2050
2074
|
let usage = { version: 1, updated_at: "", by_id: {} };
|
|
2051
2075
|
let byId = /* @__PURE__ */ new Map();
|
|
2052
2076
|
let lastSession;
|
|
2053
|
-
if (
|
|
2077
|
+
if (existsSync21(ctx.paths.memoriesDir)) {
|
|
2054
2078
|
const allLoaded = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
|
|
2055
2079
|
const recaps = allLoaded.filter(({ memory }) => memory.frontmatter.type === "session_recap").sort(
|
|
2056
2080
|
(a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
|
|
@@ -2225,7 +2249,7 @@ async function getBriefing(input, ctx) {
|
|
|
2225
2249
|
if (!isAutoPromoteEligible(loaded.memory.frontmatter, u, rule)) continue;
|
|
2226
2250
|
const newFm = { ...loaded.memory.frontmatter, status: "validated", validated_by: "auto" };
|
|
2227
2251
|
try {
|
|
2228
|
-
await
|
|
2252
|
+
await writeFile13(loaded.filePath, serializeMemory11({ frontmatter: newFm, body: loaded.memory.body }), "utf8");
|
|
2229
2253
|
m.status = "validated";
|
|
2230
2254
|
m.confidence = "trusted";
|
|
2231
2255
|
} catch {
|
|
@@ -2233,7 +2257,7 @@ async function getBriefing(input, ctx) {
|
|
|
2233
2257
|
}
|
|
2234
2258
|
}
|
|
2235
2259
|
}
|
|
2236
|
-
let projectContextRaw = input.include_project_context &&
|
|
2260
|
+
let projectContextRaw = input.include_project_context && existsSync21(ctx.paths.projectContext) ? await readFile6(ctx.paths.projectContext, "utf8") : "";
|
|
2237
2261
|
let contextOmittedRecent = false;
|
|
2238
2262
|
if (projectContextRaw && input.dedupe_project_context !== false) {
|
|
2239
2263
|
const ctxHash = hashProjectContext(projectContextRaw);
|
|
@@ -2248,7 +2272,7 @@ async function getBriefing(input, ctx) {
|
|
|
2248
2272
|
const setupWarnings = [];
|
|
2249
2273
|
let autoContextGenerated = false;
|
|
2250
2274
|
let projectContext = isTemplateContext ? "" : projectContextRaw;
|
|
2251
|
-
if ((isTemplateContext || !
|
|
2275
|
+
if ((isTemplateContext || !existsSync21(ctx.paths.projectContext)) && input.include_project_context) {
|
|
2252
2276
|
const haiveConfig = await loadConfig3(ctx.paths);
|
|
2253
2277
|
if (haiveConfig.autoContext) {
|
|
2254
2278
|
const codeMap = await loadCodeMap(ctx.paths);
|
|
@@ -2422,7 +2446,7 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
2422
2446
|
actionRequired.push(extractActionItem(m.id, loaded.memory.body));
|
|
2423
2447
|
}
|
|
2424
2448
|
}
|
|
2425
|
-
if (
|
|
2449
|
+
if (existsSync21(ctx.paths.memoriesDir)) {
|
|
2426
2450
|
const allMems = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
|
|
2427
2451
|
for (const { memory } of allMems) {
|
|
2428
2452
|
const fm = memory.frontmatter;
|
|
@@ -2433,7 +2457,7 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
2433
2457
|
}
|
|
2434
2458
|
}
|
|
2435
2459
|
const pendingDistillFile = pendingDistillPath(ctx);
|
|
2436
|
-
if (
|
|
2460
|
+
if (existsSync21(pendingDistillFile)) {
|
|
2437
2461
|
try {
|
|
2438
2462
|
const raw = await readFile6(pendingDistillFile, "utf8");
|
|
2439
2463
|
const pd = JSON.parse(raw);
|
|
@@ -2462,7 +2486,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
2462
2486
|
}
|
|
2463
2487
|
}
|
|
2464
2488
|
const memoriesEmpty = outputMemories.length === 0;
|
|
2465
|
-
const hasMemoriesDir =
|
|
2489
|
+
const hasMemoriesDir = existsSync21(ctx.paths.memoriesDir);
|
|
2466
2490
|
const isColdStart = isTemplateContext && memoriesEmpty && !lastSession && !autoContextGenerated;
|
|
2467
2491
|
const hasUnguessableSignal = outputMemories.some(
|
|
2468
2492
|
(m) => (m.priority === "must_read" || m.priority === "useful") && specificityScore(m.body) >= GUESSABLE_THRESHOLD
|
|
@@ -2480,7 +2504,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
2480
2504
|
pcRaw = await readFile6(ctx.paths.projectContext, "utf8");
|
|
2481
2505
|
} catch {
|
|
2482
2506
|
}
|
|
2483
|
-
const allForBootstrap =
|
|
2507
|
+
const allForBootstrap = existsSync21(ctx.paths.memoriesDir) ? await loadMemoriesFromDir16(ctx.paths.memoriesDir) : [];
|
|
2484
2508
|
const cmForBootstrap = await loadCodeMap(ctx.paths);
|
|
2485
2509
|
let existingModules = [];
|
|
2486
2510
|
try {
|
|
@@ -2530,7 +2554,7 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
|
|
|
2530
2554
|
}
|
|
2531
2555
|
if (input.task && outputMemories.length > 0 && actionRequired.length === 0) {
|
|
2532
2556
|
hints.push(
|
|
2533
|
-
"After completing the task: capture
|
|
2557
|
+
"After completing the task: capture failed approaches with mem_tried, new gotchas and validated patterns with mem_save."
|
|
2534
2558
|
);
|
|
2535
2559
|
}
|
|
2536
2560
|
if (outputMemories.length > 2 && !input.budget_preset && input.task && !hints.some((h) => h.includes("budget_preset"))) {
|
|
@@ -2544,7 +2568,7 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
|
|
|
2544
2568
|
"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."
|
|
2545
2569
|
);
|
|
2546
2570
|
}
|
|
2547
|
-
if (outputMemories.length > 0 &&
|
|
2571
|
+
if (outputMemories.length > 0 && existsSync21(ctx.paths.haiveDir)) {
|
|
2548
2572
|
const proof = briefingProofLine(await loadPreventionEvents(ctx.paths));
|
|
2549
2573
|
if (proof) hints.push(proof);
|
|
2550
2574
|
}
|
|
@@ -2558,7 +2582,7 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
|
|
|
2558
2582
|
adaptiveTrim
|
|
2559
2583
|
});
|
|
2560
2584
|
const breadcrumbTokens = breadcrumbs ? estimateTokens([...breadcrumbs.start_here, ...breadcrumbs.drill_down, breadcrumbs.note ?? ""].join("\n")) : 0;
|
|
2561
|
-
if (
|
|
2585
|
+
if (existsSync21(ctx.paths.haiveDir)) {
|
|
2562
2586
|
await writeBriefingMarker(ctx.paths, {
|
|
2563
2587
|
sessionId: process.env.HAIVE_SESSION_ID,
|
|
2564
2588
|
...input.task ? { task: input.task } : {},
|
|
@@ -2614,8 +2638,8 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
|
|
|
2614
2638
|
};
|
|
2615
2639
|
}
|
|
2616
2640
|
async function detectRunCommands(root) {
|
|
2617
|
-
const pkgPath =
|
|
2618
|
-
if (!
|
|
2641
|
+
const pkgPath = path11.join(root, "package.json");
|
|
2642
|
+
if (!existsSync21(pkgPath)) return null;
|
|
2619
2643
|
try {
|
|
2620
2644
|
const pkg = JSON.parse(await readFile6(pkgPath, "utf8"));
|
|
2621
2645
|
const scripts = pkg.scripts ?? {};
|
|
@@ -2682,24 +2706,24 @@ function oneLine(value) {
|
|
|
2682
2706
|
return value.replace(/\s+/g, " ").replace(/"/g, '\\"').trim().slice(0, 120);
|
|
2683
2707
|
}
|
|
2684
2708
|
function serverVersion() {
|
|
2685
|
-
return true ? "0.
|
|
2709
|
+
return true ? "0.34.1" : "dev";
|
|
2686
2710
|
}
|
|
2687
2711
|
|
|
2688
2712
|
// src/tools/code-map.ts
|
|
2689
2713
|
import { estimateTokens as estimateTokens2, loadCodeMap as loadCodeMap2, queryCodeMap as queryCodeMap2 } from "@hivelore/core";
|
|
2690
|
-
import { z as
|
|
2714
|
+
import { z as z20 } from "zod";
|
|
2691
2715
|
var CodeMapInputSchema = {
|
|
2692
|
-
file:
|
|
2693
|
-
symbol:
|
|
2694
|
-
paths:
|
|
2716
|
+
file: z20.string().optional().describe("Filter to files whose path contains this substring"),
|
|
2717
|
+
symbol: z20.string().optional().describe("Filter to files exporting a symbol whose name contains this substring"),
|
|
2718
|
+
paths: z20.array(z20.string()).default([]).describe(
|
|
2695
2719
|
"Filter to files under any of these path prefixes (e.g. ['packages/mcp/src/tools/', 'src/auth/']). OR-joined with `file` substring; useful to get a focused view of one module."
|
|
2696
2720
|
),
|
|
2697
|
-
max_files:
|
|
2698
|
-
max_tokens:
|
|
2721
|
+
max_files: z20.number().int().positive().default(40).describe("Cap on returned files (hard limit, applied after token budget)"),
|
|
2722
|
+
max_tokens: z20.number().int().positive().optional().describe(
|
|
2699
2723
|
"Approximate token budget for the response. When the matching set exceeds it, files are ranked by export density (exports per LOC) and the highest-signal ones are kept first. Omit to disable budgeting (legacy behavior)."
|
|
2700
2724
|
)
|
|
2701
2725
|
};
|
|
2702
|
-
var CodeMapInputZod =
|
|
2726
|
+
var CodeMapInputZod = z20.object(CodeMapInputSchema);
|
|
2703
2727
|
async function codeMapTool(input, ctx) {
|
|
2704
2728
|
const map = await loadCodeMap2(ctx.paths);
|
|
2705
2729
|
if (!map) {
|
|
@@ -2768,15 +2792,15 @@ function estimateFileEntryTokens(f) {
|
|
|
2768
2792
|
}
|
|
2769
2793
|
|
|
2770
2794
|
// src/tools/mem-diff.ts
|
|
2771
|
-
import { existsSync as
|
|
2795
|
+
import { existsSync as existsSync22 } from "fs";
|
|
2772
2796
|
import { loadMemoriesFromDir as loadMemoriesFromDir17 } from "@hivelore/core";
|
|
2773
|
-
import { z as
|
|
2797
|
+
import { z as z21 } from "zod";
|
|
2774
2798
|
var MemDiffInputSchema = {
|
|
2775
|
-
id_a:
|
|
2776
|
-
id_b:
|
|
2799
|
+
id_a: z21.string().min(1).describe("First memory id"),
|
|
2800
|
+
id_b: z21.string().min(1).describe("Second memory id")
|
|
2777
2801
|
};
|
|
2778
2802
|
async function memDiff(input, ctx) {
|
|
2779
|
-
if (!
|
|
2803
|
+
if (!existsSync22(ctx.paths.memoriesDir)) {
|
|
2780
2804
|
throw new Error(`No .ai/memories at ${ctx.paths.root}.`);
|
|
2781
2805
|
}
|
|
2782
2806
|
const all = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
|
|
@@ -2813,16 +2837,16 @@ async function memDiff(input, ctx) {
|
|
|
2813
2837
|
}
|
|
2814
2838
|
|
|
2815
2839
|
// src/tools/get-recap.ts
|
|
2816
|
-
import { existsSync as
|
|
2840
|
+
import { existsSync as existsSync23 } from "fs";
|
|
2817
2841
|
import { loadMemoriesFromDir as loadMemoriesFromDir18 } from "@hivelore/core";
|
|
2818
|
-
import { z as
|
|
2842
|
+
import { z as z22 } from "zod";
|
|
2819
2843
|
var GetRecapInputSchema = {
|
|
2820
|
-
scope:
|
|
2844
|
+
scope: z22.enum(["personal", "team", "any"]).default("any").describe(
|
|
2821
2845
|
"Limit to a specific scope's recap. Default 'any' returns the most recent recap across both personal and team scopes."
|
|
2822
2846
|
)
|
|
2823
2847
|
};
|
|
2824
2848
|
async function getRecap(input, ctx) {
|
|
2825
|
-
if (!
|
|
2849
|
+
if (!existsSync23(ctx.paths.memoriesDir)) {
|
|
2826
2850
|
return { recap: null, notice: "No .ai/memories directory \u2014 haive not initialized here." };
|
|
2827
2851
|
}
|
|
2828
2852
|
const all = await loadMemoriesFromDir18(ctx.paths.memoriesDir);
|
|
@@ -2849,13 +2873,13 @@ async function getRecap(input, ctx) {
|
|
|
2849
2873
|
}
|
|
2850
2874
|
|
|
2851
2875
|
// src/tools/mem-relevant-to.ts
|
|
2852
|
-
import { z as
|
|
2876
|
+
import { z as z23 } from "zod";
|
|
2853
2877
|
var MemRelevantToInputSchema = {
|
|
2854
|
-
task:
|
|
2855
|
-
files:
|
|
2856
|
-
limit:
|
|
2857
|
-
min_semantic_score:
|
|
2858
|
-
format:
|
|
2878
|
+
task: z23.string().min(1).describe("What you are about to do, in 1\u20132 sentences. Used to rank relevant memories."),
|
|
2879
|
+
files: z23.array(z23.string()).default([]).describe("Optional: files you are about to edit \u2014 surfaces anchored memories."),
|
|
2880
|
+
limit: z23.number().int().positive().max(30).default(8).describe("Cap on returned memories."),
|
|
2881
|
+
min_semantic_score: z23.number().min(0).max(1).default(0.25).describe("Drop weakly-related semantic hits below this cosine threshold."),
|
|
2882
|
+
format: z23.enum(["full", "compact", "actions"]).default("full").describe("'compact' = id + 1-line summary; 'full' = complete bodies; 'actions' = bullet-first excerpts.")
|
|
2859
2883
|
};
|
|
2860
2884
|
async function memRelevantTo(input, ctx) {
|
|
2861
2885
|
const briefingInput = {
|
|
@@ -2885,14 +2909,14 @@ async function memRelevantTo(input, ctx) {
|
|
|
2885
2909
|
}
|
|
2886
2910
|
|
|
2887
2911
|
// src/tools/code-search.ts
|
|
2888
|
-
import { z as
|
|
2912
|
+
import { z as z24 } from "zod";
|
|
2889
2913
|
import { loadCodeMap as loadCodeMap3 } from "@hivelore/core";
|
|
2890
2914
|
var CodeSearchInputSchema = {
|
|
2891
|
-
query:
|
|
2915
|
+
query: z24.string().min(1).describe(
|
|
2892
2916
|
"Natural-language description of what you are looking for in the codebase (e.g. 'function that hashes passwords', 'JWT signing logic', 'route registration')."
|
|
2893
2917
|
),
|
|
2894
|
-
k:
|
|
2895
|
-
min_score:
|
|
2918
|
+
k: z24.number().int().positive().max(50).default(5).describe("Number of top hits to return."),
|
|
2919
|
+
min_score: z24.number().min(0).max(1).default(0.2).describe(
|
|
2896
2920
|
"Minimum cosine similarity. Hits below this threshold are dropped to avoid noise. Try 0.3+ for stricter matching."
|
|
2897
2921
|
)
|
|
2898
2922
|
};
|
|
@@ -2934,155 +2958,39 @@ async function codeSearch(input, ctx) {
|
|
|
2934
2958
|
};
|
|
2935
2959
|
}
|
|
2936
2960
|
|
|
2937
|
-
// src/tools/why-this-file.ts
|
|
2938
|
-
import { existsSync as existsSync25 } from "fs";
|
|
2939
|
-
import { spawn } from "child_process";
|
|
2940
|
-
import path13 from "path";
|
|
2941
|
-
import {
|
|
2942
|
-
deriveConfidence as deriveConfidence5,
|
|
2943
|
-
getUsage as getUsage7,
|
|
2944
|
-
loadCodeMap as loadCodeMap4,
|
|
2945
|
-
loadMemoriesFromDir as loadMemoriesFromDir19,
|
|
2946
|
-
loadUsageIndex as loadUsageIndex9,
|
|
2947
|
-
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths3
|
|
2948
|
-
} from "@hivelore/core";
|
|
2949
|
-
import { z as z26 } from "zod";
|
|
2950
|
-
var WhyThisFileInputSchema = {
|
|
2951
|
-
path: z26.string().min(1).describe(
|
|
2952
|
-
"Project-relative path to the file you want context on (e.g. 'packages/mcp/src/tools/mem-save.ts')."
|
|
2953
|
-
),
|
|
2954
|
-
git_log_limit: z26.number().int().positive().max(20).default(5).describe("How many recent commits touching this file to include."),
|
|
2955
|
-
memory_limit: z26.number().int().positive().max(20).default(5).describe("Cap on memories anchored to this path.")
|
|
2956
|
-
};
|
|
2957
|
-
async function whyThisFile(input, ctx) {
|
|
2958
|
-
const fileExists = existsSync25(path13.join(ctx.paths.root, input.path));
|
|
2959
|
-
const [commits, memories, codeMap] = await Promise.all([
|
|
2960
|
-
runGitLog(ctx.paths.root, input.path, input.git_log_limit).catch(() => []),
|
|
2961
|
-
collectAnchoredMemories(ctx, input.path, input.memory_limit),
|
|
2962
|
-
loadCodeMap4(ctx.paths)
|
|
2963
|
-
]);
|
|
2964
|
-
const codeMapEntry = codeMap?.files[input.path];
|
|
2965
|
-
const hints = [];
|
|
2966
|
-
if (!fileExists) {
|
|
2967
|
-
hints.push(`File '${input.path}' does not exist on disk \u2014 path may be wrong or file removed.`);
|
|
2968
|
-
}
|
|
2969
|
-
if (commits.length === 0 && fileExists) {
|
|
2970
|
-
hints.push("No git history found \u2014 file may be untracked or git not initialized.");
|
|
2971
|
-
}
|
|
2972
|
-
if (memories.length === 0 && fileExists) {
|
|
2973
|
-
hints.push(
|
|
2974
|
-
"No memories anchored here. If you discover something non-obvious while editing, use mem_observe (with where=" + input.path + ") to capture it."
|
|
2975
|
-
);
|
|
2976
|
-
}
|
|
2977
|
-
if (memories.some((m) => m.type === "attempt" || m.type === "gotcha")) {
|
|
2978
|
-
hints.push("\u26A0\uFE0F attempt/gotcha memories anchored to this file \u2014 read them BEFORE editing.");
|
|
2979
|
-
}
|
|
2980
|
-
return {
|
|
2981
|
-
file: input.path,
|
|
2982
|
-
exists: fileExists,
|
|
2983
|
-
recent_commits: commits,
|
|
2984
|
-
memories,
|
|
2985
|
-
code_map_entry: codeMapEntry ? {
|
|
2986
|
-
...codeMapEntry.summary ? { summary: codeMapEntry.summary } : {},
|
|
2987
|
-
loc: codeMapEntry.loc,
|
|
2988
|
-
exports: codeMapEntry.exports.map((e) => ({
|
|
2989
|
-
name: e.name,
|
|
2990
|
-
kind: e.kind,
|
|
2991
|
-
line: e.line,
|
|
2992
|
-
...e.description ? { description: e.description } : {}
|
|
2993
|
-
}))
|
|
2994
|
-
} : null,
|
|
2995
|
-
...hints.length > 0 ? { hints } : {}
|
|
2996
|
-
};
|
|
2997
|
-
}
|
|
2998
|
-
async function collectAnchoredMemories(ctx, filePath, limit) {
|
|
2999
|
-
if (!existsSync25(ctx.paths.memoriesDir)) return [];
|
|
3000
|
-
const all = await loadMemoriesFromDir19(ctx.paths.memoriesDir);
|
|
3001
|
-
const usage = await loadUsageIndex9(ctx.paths);
|
|
3002
|
-
const out = [];
|
|
3003
|
-
for (const { memory } of all) {
|
|
3004
|
-
const fm = memory.frontmatter;
|
|
3005
|
-
if (fm.status === "rejected" || fm.status === "deprecated") continue;
|
|
3006
|
-
if (fm.type === "session_recap") continue;
|
|
3007
|
-
if (!memoryMatchesAnchorPaths3(memory, [filePath])) continue;
|
|
3008
|
-
const u = getUsage7(usage, fm.id);
|
|
3009
|
-
out.push({
|
|
3010
|
-
id: fm.id,
|
|
3011
|
-
type: fm.type,
|
|
3012
|
-
scope: fm.scope,
|
|
3013
|
-
confidence: deriveConfidence5(fm, u),
|
|
3014
|
-
body_preview: memory.body.split("\n").slice(0, 6).join("\n")
|
|
3015
|
-
});
|
|
3016
|
-
if (out.length >= limit) break;
|
|
3017
|
-
}
|
|
3018
|
-
return out;
|
|
3019
|
-
}
|
|
3020
|
-
async function runGitLog(cwd, filePath, limit) {
|
|
3021
|
-
const sep = "<<HV>>";
|
|
3022
|
-
const fmt = `%h${sep}%an${sep}%ar${sep}%s`;
|
|
3023
|
-
const output = await runCommand(
|
|
3024
|
-
"git",
|
|
3025
|
-
["log", `-n`, String(limit), `--pretty=format:${fmt}`, "--", filePath],
|
|
3026
|
-
cwd
|
|
3027
|
-
);
|
|
3028
|
-
if (!output.trim()) return [];
|
|
3029
|
-
return output.split("\n").map((line) => {
|
|
3030
|
-
const [sha = "", author = "", relative_date = "", subject = ""] = line.split(sep);
|
|
3031
|
-
return { sha, author, relative_date, subject };
|
|
3032
|
-
}).filter((c) => c.sha);
|
|
3033
|
-
}
|
|
3034
|
-
function runCommand(cmd, args, cwd) {
|
|
3035
|
-
return new Promise((resolve, reject) => {
|
|
3036
|
-
const proc = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
3037
|
-
let stdout = "";
|
|
3038
|
-
let stderr = "";
|
|
3039
|
-
proc.stdout.on("data", (chunk) => {
|
|
3040
|
-
stdout += chunk.toString();
|
|
3041
|
-
});
|
|
3042
|
-
proc.stderr.on("data", (chunk) => {
|
|
3043
|
-
stderr += chunk.toString();
|
|
3044
|
-
});
|
|
3045
|
-
proc.on("error", reject);
|
|
3046
|
-
proc.on("close", (code) => {
|
|
3047
|
-
if (code === 0) resolve(stdout);
|
|
3048
|
-
else reject(new Error(stderr || `${cmd} exited with code ${code}`));
|
|
3049
|
-
});
|
|
3050
|
-
});
|
|
3051
|
-
}
|
|
3052
|
-
|
|
3053
2961
|
// src/tools/anti-patterns-check.ts
|
|
3054
|
-
import { existsSync as
|
|
2962
|
+
import { existsSync as existsSync24 } from "fs";
|
|
3055
2963
|
import {
|
|
3056
2964
|
addedLinesFromDiff,
|
|
3057
2965
|
BRIDGE_TARGET_PATH,
|
|
3058
2966
|
buildDocFrequency,
|
|
3059
2967
|
CODE_STOPWORDS,
|
|
3060
|
-
deriveConfidence as
|
|
2968
|
+
deriveConfidence as deriveConfidence5,
|
|
3061
2969
|
diffHasDistinctiveOverlap,
|
|
3062
|
-
getUsage as
|
|
2970
|
+
getUsage as getUsage7,
|
|
3063
2971
|
isRetiredMemory as isRetiredMemory2,
|
|
3064
|
-
loadMemoriesFromDir as
|
|
3065
|
-
loadUsageIndex as
|
|
2972
|
+
loadMemoriesFromDir as loadMemoriesFromDir19,
|
|
2973
|
+
loadUsageIndex as loadUsageIndex9,
|
|
3066
2974
|
literalMatchesAnyToken as literalMatchesAnyToken3,
|
|
3067
|
-
memoryMatchesAnchorPaths as
|
|
2975
|
+
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths3,
|
|
3068
2976
|
recordPreventionHits,
|
|
3069
2977
|
runSensors,
|
|
3070
2978
|
sensorTargetsFromDiff,
|
|
3071
2979
|
tokenizeQuery as tokenizeQuery3
|
|
3072
2980
|
} from "@hivelore/core";
|
|
3073
|
-
import { z as
|
|
2981
|
+
import { z as z25 } from "zod";
|
|
3074
2982
|
var AntiPatternsCheckInputSchema = {
|
|
3075
|
-
diff:
|
|
2983
|
+
diff: z25.string().optional().describe(
|
|
3076
2984
|
"Raw unified diff text (or any code/text snippet) to scan for previously documented anti-patterns. Tokens from the diff are used to match memory bodies and the embeddings index."
|
|
3077
2985
|
),
|
|
3078
|
-
paths:
|
|
2986
|
+
paths: z25.array(z25.string()).default([]).describe(
|
|
3079
2987
|
"File paths affected by the change. Memories anchored to any of these paths are surfaced regardless of the diff content."
|
|
3080
2988
|
),
|
|
3081
|
-
limit:
|
|
3082
|
-
semantic:
|
|
2989
|
+
limit: z25.number().int().positive().max(20).default(8).describe("Cap on returned warnings."),
|
|
2990
|
+
semantic: z25.boolean().default(true).describe(
|
|
3083
2991
|
"When true, also use semantic search (requires @hivelore/embeddings + memory index) to find related anti-patterns."
|
|
3084
2992
|
),
|
|
3085
|
-
min_semantic_score:
|
|
2993
|
+
min_semantic_score: z25.number().min(0).max(1).default(0.45).describe(
|
|
3086
2994
|
"Minimum cosine score for semantic-only anti-pattern hits. Anchor/literal matches still surface. Default 0.45 keeps broad, weakly-related memories out of review noise."
|
|
3087
2995
|
)
|
|
3088
2996
|
};
|
|
@@ -3166,10 +3074,10 @@ async function antiPatternsCheck(input, ctx) {
|
|
|
3166
3074
|
notice: "Nothing to check \u2014 provide either `diff` text or `paths`."
|
|
3167
3075
|
};
|
|
3168
3076
|
}
|
|
3169
|
-
if (!
|
|
3077
|
+
if (!existsSync24(ctx.paths.memoriesDir)) {
|
|
3170
3078
|
return { scanned: 0, warnings: [], notice: "No .ai/memories directory \u2014 nothing to check against." };
|
|
3171
3079
|
}
|
|
3172
|
-
const all = await
|
|
3080
|
+
const all = await loadMemoriesFromDir19(ctx.paths.memoriesDir);
|
|
3173
3081
|
const minSemanticScore = input.min_semantic_score ?? 0.45;
|
|
3174
3082
|
const negative = all.filter(({ memory }) => {
|
|
3175
3083
|
const t = memory.frontmatter.type;
|
|
@@ -3180,7 +3088,7 @@ async function antiPatternsCheck(input, ctx) {
|
|
|
3180
3088
|
if (negative.length === 0) {
|
|
3181
3089
|
return { scanned: 0, warnings: [], notice: "No attempt/gotcha memories found yet." };
|
|
3182
3090
|
}
|
|
3183
|
-
const usage = await
|
|
3091
|
+
const usage = await loadUsageIndex9(ctx.paths);
|
|
3184
3092
|
const docFreq = buildDocFrequency(negative.map(({ memory }) => memory.body));
|
|
3185
3093
|
const seen = /* @__PURE__ */ new Map();
|
|
3186
3094
|
const upsert = (fm, body, reason, score) => {
|
|
@@ -3192,12 +3100,12 @@ async function antiPatternsCheck(input, ctx) {
|
|
|
3192
3100
|
}
|
|
3193
3101
|
return;
|
|
3194
3102
|
}
|
|
3195
|
-
const u =
|
|
3103
|
+
const u = getUsage7(usage, fm.id);
|
|
3196
3104
|
seen.set(fm.id, {
|
|
3197
3105
|
id: fm.id,
|
|
3198
3106
|
type: fm.type,
|
|
3199
3107
|
scope: fm.scope,
|
|
3200
|
-
confidence:
|
|
3108
|
+
confidence: deriveConfidence5(fm, u),
|
|
3201
3109
|
body_preview: body.split("\n").slice(0, 5).join("\n").slice(0, 400),
|
|
3202
3110
|
reasons: [reason],
|
|
3203
3111
|
tags: fm.tags ?? [],
|
|
@@ -3208,7 +3116,7 @@ async function antiPatternsCheck(input, ctx) {
|
|
|
3208
3116
|
};
|
|
3209
3117
|
if (input.paths.length > 0) {
|
|
3210
3118
|
for (const { memory } of negative) {
|
|
3211
|
-
if (
|
|
3119
|
+
if (memoryMatchesAnchorPaths3(memory, input.paths)) {
|
|
3212
3120
|
upsert(memory.frontmatter, memory.body, "anchor");
|
|
3213
3121
|
}
|
|
3214
3122
|
}
|
|
@@ -3285,19 +3193,19 @@ async function antiPatternsCheck(input, ctx) {
|
|
|
3285
3193
|
}
|
|
3286
3194
|
|
|
3287
3195
|
// src/tools/mem-distill.ts
|
|
3288
|
-
import { existsSync as
|
|
3196
|
+
import { existsSync as existsSync25 } from "fs";
|
|
3289
3197
|
import {
|
|
3290
|
-
loadMemoriesFromDir as
|
|
3198
|
+
loadMemoriesFromDir as loadMemoriesFromDir20,
|
|
3291
3199
|
tokenizeQuery as tokenizeQuery4
|
|
3292
3200
|
} from "@hivelore/core";
|
|
3293
|
-
import { z as
|
|
3201
|
+
import { z as z26 } from "zod";
|
|
3294
3202
|
var MemDistillInputSchema = {
|
|
3295
|
-
since_days:
|
|
3296
|
-
min_cluster:
|
|
3297
|
-
type_filter:
|
|
3203
|
+
since_days: z26.number().int().positive().default(30).describe("Only consider memories created in the last N days."),
|
|
3204
|
+
min_cluster: z26.number().int().min(2).default(3).describe("Minimum cluster size to surface."),
|
|
3205
|
+
type_filter: z26.enum(["gotcha", "attempt", "all"]).default("gotcha").describe(
|
|
3298
3206
|
"Memory type to scan. 'gotcha' targets observe-style discoveries that recur, 'attempt' surfaces failed approaches that repeat, 'all' considers both."
|
|
3299
3207
|
),
|
|
3300
|
-
scope:
|
|
3208
|
+
scope: z26.enum(["personal", "team", "module", "any"]).default("any").describe("Restrict to a specific scope.")
|
|
3301
3209
|
};
|
|
3302
3210
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
3303
3211
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
@@ -3337,11 +3245,11 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
3337
3245
|
"error"
|
|
3338
3246
|
]);
|
|
3339
3247
|
async function memDistill(input, ctx) {
|
|
3340
|
-
if (!
|
|
3248
|
+
if (!existsSync25(ctx.paths.memoriesDir)) {
|
|
3341
3249
|
return { scanned: 0, singletons: 0, clusters: [], notice: "No .ai/memories directory." };
|
|
3342
3250
|
}
|
|
3343
3251
|
const cutoff = Date.now() - input.since_days * MS_PER_DAY;
|
|
3344
|
-
const all = await
|
|
3252
|
+
const all = await loadMemoriesFromDir20(ctx.paths.memoriesDir);
|
|
3345
3253
|
const candidates = all.filter(({ memory }) => {
|
|
3346
3254
|
const fm = memory.frontmatter;
|
|
3347
3255
|
if (fm.status === "rejected" || fm.status === "deprecated" || fm.status === "stale") return false;
|
|
@@ -3444,299 +3352,19 @@ function firstHeading(body) {
|
|
|
3444
3352
|
return void 0;
|
|
3445
3353
|
}
|
|
3446
3354
|
|
|
3447
|
-
// src/tools/why-this-decision.ts
|
|
3448
|
-
import { existsSync as existsSync28 } from "fs";
|
|
3449
|
-
import { spawn as spawn2 } from "child_process";
|
|
3450
|
-
import {
|
|
3451
|
-
deriveConfidence as deriveConfidence7,
|
|
3452
|
-
getUsage as getUsage9,
|
|
3453
|
-
loadMemoriesFromDir as loadMemoriesFromDir22,
|
|
3454
|
-
loadUsageIndex as loadUsageIndex11,
|
|
3455
|
-
pathsOverlap as singlePathsOverlap
|
|
3456
|
-
} from "@hivelore/core";
|
|
3457
|
-
import { z as z29 } from "zod";
|
|
3458
|
-
var WhyThisDecisionInputSchema = {
|
|
3459
|
-
id: z29.string().min(1).describe("Memory id to inspect (e.g. '2026-04-25-decision-esm-only')."),
|
|
3460
|
-
git_log_limit: z29.number().int().positive().max(20).default(5).describe("How many recent commits per anchor path to surface.")
|
|
3461
|
-
};
|
|
3462
|
-
async function whyThisDecision(input, ctx) {
|
|
3463
|
-
if (!existsSync28(ctx.paths.memoriesDir)) {
|
|
3464
|
-
return {
|
|
3465
|
-
found: false,
|
|
3466
|
-
related: [],
|
|
3467
|
-
path_neighbors: [],
|
|
3468
|
-
recent_commits: [],
|
|
3469
|
-
notice: "No .ai/memories directory."
|
|
3470
|
-
};
|
|
3471
|
-
}
|
|
3472
|
-
const all = await loadMemoriesFromDir22(ctx.paths.memoriesDir);
|
|
3473
|
-
const usage = await loadUsageIndex11(ctx.paths);
|
|
3474
|
-
const target = all.find(({ memory }) => memory.frontmatter.id === input.id);
|
|
3475
|
-
if (!target) {
|
|
3476
|
-
return {
|
|
3477
|
-
found: false,
|
|
3478
|
-
related: [],
|
|
3479
|
-
path_neighbors: [],
|
|
3480
|
-
recent_commits: [],
|
|
3481
|
-
notice: `Memory '${input.id}' not found.`
|
|
3482
|
-
};
|
|
3483
|
-
}
|
|
3484
|
-
const fm = target.memory.frontmatter;
|
|
3485
|
-
const targetUsage = getUsage9(usage, fm.id);
|
|
3486
|
-
const decision = {
|
|
3487
|
-
id: fm.id,
|
|
3488
|
-
type: fm.type,
|
|
3489
|
-
scope: fm.scope,
|
|
3490
|
-
status: fm.status,
|
|
3491
|
-
confidence: deriveConfidence7(fm, targetUsage),
|
|
3492
|
-
body: target.memory.body,
|
|
3493
|
-
created_at: fm.created_at
|
|
3494
|
-
};
|
|
3495
|
-
const relatedSet = new Set(fm.related_ids ?? []);
|
|
3496
|
-
const related = [];
|
|
3497
|
-
for (const { memory } of all) {
|
|
3498
|
-
if (memory.frontmatter.id === fm.id) continue;
|
|
3499
|
-
const isExplicit = relatedSet.has(memory.frontmatter.id);
|
|
3500
|
-
const isBackLink = (memory.frontmatter.related_ids ?? []).includes(fm.id);
|
|
3501
|
-
if (!isExplicit && !isBackLink) continue;
|
|
3502
|
-
const u = getUsage9(usage, memory.frontmatter.id);
|
|
3503
|
-
related.push({
|
|
3504
|
-
id: memory.frontmatter.id,
|
|
3505
|
-
type: memory.frontmatter.type,
|
|
3506
|
-
scope: memory.frontmatter.scope,
|
|
3507
|
-
confidence: deriveConfidence7(memory.frontmatter, u),
|
|
3508
|
-
body_preview: memory.body.split("\n").slice(0, 4).join("\n").slice(0, 300),
|
|
3509
|
-
relation: isExplicit ? "explicit" : "back-link"
|
|
3510
|
-
});
|
|
3511
|
-
}
|
|
3512
|
-
const targetPaths = fm.anchor.paths;
|
|
3513
|
-
const path_neighbors = [];
|
|
3514
|
-
if (targetPaths.length > 0) {
|
|
3515
|
-
for (const { memory } of all) {
|
|
3516
|
-
if (memory.frontmatter.id === fm.id) continue;
|
|
3517
|
-
if (relatedSet.has(memory.frontmatter.id)) continue;
|
|
3518
|
-
const overlappingPaths = memory.frontmatter.anchor.paths.filter(
|
|
3519
|
-
(p) => targetPaths.some((tp) => singlePathsOverlap(p, tp))
|
|
3520
|
-
);
|
|
3521
|
-
if (overlappingPaths.length === 0) continue;
|
|
3522
|
-
const u = getUsage9(usage, memory.frontmatter.id);
|
|
3523
|
-
path_neighbors.push({
|
|
3524
|
-
id: memory.frontmatter.id,
|
|
3525
|
-
type: memory.frontmatter.type,
|
|
3526
|
-
scope: memory.frontmatter.scope,
|
|
3527
|
-
confidence: deriveConfidence7(memory.frontmatter, u),
|
|
3528
|
-
overlap: overlappingPaths,
|
|
3529
|
-
body_preview: memory.body.split("\n").slice(0, 3).join("\n").slice(0, 200)
|
|
3530
|
-
});
|
|
3531
|
-
if (path_neighbors.length >= 10) break;
|
|
3532
|
-
}
|
|
3533
|
-
}
|
|
3534
|
-
const recent_commits = [];
|
|
3535
|
-
for (const p of targetPaths.slice(0, 5)) {
|
|
3536
|
-
try {
|
|
3537
|
-
const commits = await runGitLog2(ctx.paths.root, p, input.git_log_limit);
|
|
3538
|
-
for (const c of commits) recent_commits.push({ path: p, ...c });
|
|
3539
|
-
} catch {
|
|
3540
|
-
}
|
|
3541
|
-
}
|
|
3542
|
-
const hints = [];
|
|
3543
|
-
if (decision.confidence === "low" || decision.confidence === "stale") {
|
|
3544
|
-
hints.push(`\u26A0\uFE0F Confidence is ${decision.confidence}. Verify this decision still applies before quoting it.`);
|
|
3545
|
-
}
|
|
3546
|
-
if (related.length === 0 && path_neighbors.length === 0 && targetPaths.length === 0) {
|
|
3547
|
-
hints.push("No related memories and no anchored paths \u2014 this decision is isolated; consider adding related_ids or paths.");
|
|
3548
|
-
}
|
|
3549
|
-
if (fm.type !== "decision" && fm.type !== "architecture") {
|
|
3550
|
-
hints.push(`Memory type is '${fm.type}', not 'decision'/'architecture' \u2014 output may be less informative.`);
|
|
3551
|
-
}
|
|
3552
|
-
return {
|
|
3553
|
-
found: true,
|
|
3554
|
-
decision,
|
|
3555
|
-
related,
|
|
3556
|
-
path_neighbors,
|
|
3557
|
-
recent_commits,
|
|
3558
|
-
...hints.length > 0 ? { hints } : {}
|
|
3559
|
-
};
|
|
3560
|
-
}
|
|
3561
|
-
async function runGitLog2(cwd, filePath, limit) {
|
|
3562
|
-
const sep = "<<HV>>";
|
|
3563
|
-
const fmt = `%h${sep}%an${sep}%ar${sep}%s`;
|
|
3564
|
-
const output = await runCommand2(
|
|
3565
|
-
"git",
|
|
3566
|
-
["log", "-n", String(limit), `--pretty=format:${fmt}`, "--", filePath],
|
|
3567
|
-
cwd
|
|
3568
|
-
);
|
|
3569
|
-
if (!output.trim()) return [];
|
|
3570
|
-
return output.split("\n").map((line) => {
|
|
3571
|
-
const [sha = "", author = "", relative_date = "", subject = ""] = line.split(sep);
|
|
3572
|
-
return { sha, author, relative_date, subject };
|
|
3573
|
-
}).filter((c) => c.sha);
|
|
3574
|
-
}
|
|
3575
|
-
function runCommand2(cmd, args, cwd) {
|
|
3576
|
-
return new Promise((resolve, reject) => {
|
|
3577
|
-
const proc = spawn2(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
3578
|
-
let stdout = "";
|
|
3579
|
-
let stderr = "";
|
|
3580
|
-
proc.stdout.on("data", (chunk) => {
|
|
3581
|
-
stdout += chunk.toString();
|
|
3582
|
-
});
|
|
3583
|
-
proc.stderr.on("data", (chunk) => {
|
|
3584
|
-
stderr += chunk.toString();
|
|
3585
|
-
});
|
|
3586
|
-
proc.on("error", reject);
|
|
3587
|
-
proc.on("close", (code) => {
|
|
3588
|
-
if (code === 0) resolve(stdout);
|
|
3589
|
-
else reject(new Error(stderr || `${cmd} exited with code ${code}`));
|
|
3590
|
-
});
|
|
3591
|
-
});
|
|
3592
|
-
}
|
|
3593
|
-
|
|
3594
|
-
// src/tools/mem-conflicts.ts
|
|
3595
|
-
import { existsSync as existsSync29 } from "fs";
|
|
3596
|
-
import {
|
|
3597
|
-
deriveConfidence as deriveConfidence8,
|
|
3598
|
-
getUsage as getUsage10,
|
|
3599
|
-
loadMemoriesFromDir as loadMemoriesFromDir23,
|
|
3600
|
-
loadUsageIndex as loadUsageIndex12,
|
|
3601
|
-
pathsOverlap as pathsOverlap2,
|
|
3602
|
-
tokenizeQuery as tokenizeQuery5
|
|
3603
|
-
} from "@hivelore/core";
|
|
3604
|
-
import { z as z30 } from "zod";
|
|
3605
|
-
var MemConflictsInputSchema = {
|
|
3606
|
-
id: z30.string().min(1).describe("Memory id to check for conflicts."),
|
|
3607
|
-
min_score: z30.number().min(0).max(1).default(0.5).describe("Minimum cosine similarity to consider a memory as a potential conflict (semantic mode)."),
|
|
3608
|
-
semantic: z30.boolean().default(true).describe("Use embeddings for similarity. Falls back to keyword overlap when embeddings are not installed.")
|
|
3609
|
-
};
|
|
3610
|
-
var POSITIVE_PATTERNS = /\b(use|prefer|always|should use|do this|recommended|ok to)\b/i;
|
|
3611
|
-
var NEGATIVE_PATTERNS = /\b(do not use|don'?t use|never|avoid|forbidden|deprecated|stop using|do NOT|❌)\b/i;
|
|
3612
|
-
async function memConflicts(input, ctx) {
|
|
3613
|
-
if (!existsSync29(ctx.paths.memoriesDir)) {
|
|
3614
|
-
return { found: false, scanned: 0, conflicts: [], notice: "No .ai/memories directory." };
|
|
3615
|
-
}
|
|
3616
|
-
const all = await loadMemoriesFromDir23(ctx.paths.memoriesDir);
|
|
3617
|
-
const target = all.find(({ memory }) => memory.frontmatter.id === input.id);
|
|
3618
|
-
if (!target) {
|
|
3619
|
-
return { found: false, scanned: 0, conflicts: [], notice: `Memory '${input.id}' not found.` };
|
|
3620
|
-
}
|
|
3621
|
-
const usage = await loadUsageIndex12(ctx.paths);
|
|
3622
|
-
const others = all.filter(
|
|
3623
|
-
({ memory }) => memory.frontmatter.id !== input.id && memory.frontmatter.type !== "session_recap"
|
|
3624
|
-
);
|
|
3625
|
-
const simScores = input.semantic ? await trySemanticSimilarities(ctx, target, others) : null;
|
|
3626
|
-
const targetText = (target.memory.body + " " + target.memory.frontmatter.tags.join(" ")).toLowerCase();
|
|
3627
|
-
const targetTokens = new Set(tokenizeQuery5(targetText));
|
|
3628
|
-
const targetPolarity = polarity(targetText);
|
|
3629
|
-
const targetPaths = target.memory.frontmatter.anchor.paths;
|
|
3630
|
-
const explicitContradicts = extractContradictsTags(target.memory.body);
|
|
3631
|
-
const conflicts = [];
|
|
3632
|
-
for (const other of others) {
|
|
3633
|
-
const fm = other.memory.frontmatter;
|
|
3634
|
-
const otherText = (other.memory.body + " " + fm.tags.join(" ")).toLowerCase();
|
|
3635
|
-
const reasons = [];
|
|
3636
|
-
const sim = simScores?.get(fm.id) ?? null;
|
|
3637
|
-
const hasPathOverlap = fm.anchor.paths.some((p) => targetPaths.some((tp) => pathsOverlap2(p, tp)));
|
|
3638
|
-
const otherTokens = new Set(tokenizeQuery5(otherText));
|
|
3639
|
-
const tokenOverlap = countIntersection(targetTokens, otherTokens);
|
|
3640
|
-
const isSemanticNeighbor = sim !== null && sim >= input.min_score;
|
|
3641
|
-
if (!hasPathOverlap && tokenOverlap < 4 && !isSemanticNeighbor) continue;
|
|
3642
|
-
const otherContradicts = extractContradictsTags(other.memory.body);
|
|
3643
|
-
if (explicitContradicts.has(fm.id) || otherContradicts.has(input.id)) {
|
|
3644
|
-
reasons.push("explicit-contradiction-tag");
|
|
3645
|
-
}
|
|
3646
|
-
if (target.memory.frontmatter.status === "validated" && fm.status === "rejected" || target.memory.frontmatter.status === "rejected" && fm.status === "validated") {
|
|
3647
|
-
if (tokenOverlap >= 4 || isSemanticNeighbor) reasons.push("opposite-status");
|
|
3648
|
-
}
|
|
3649
|
-
if (hasPathOverlap) {
|
|
3650
|
-
const tType = target.memory.frontmatter.type;
|
|
3651
|
-
const oType = fm.type;
|
|
3652
|
-
const isAttemptVsRule = tType === "attempt" && (oType === "convention" || oType === "decision") || oType === "attempt" && (tType === "convention" || tType === "decision");
|
|
3653
|
-
if (isAttemptVsRule) reasons.push("attempt-vs-convention-same-paths");
|
|
3654
|
-
}
|
|
3655
|
-
if (isSemanticNeighbor) {
|
|
3656
|
-
const otherPolarity = polarity(otherText);
|
|
3657
|
-
if (targetPolarity === "positive" && otherPolarity === "negative" || targetPolarity === "negative" && otherPolarity === "positive") {
|
|
3658
|
-
reasons.push("polarity-keywords");
|
|
3659
|
-
}
|
|
3660
|
-
}
|
|
3661
|
-
if (reasons.length === 0) continue;
|
|
3662
|
-
const u = getUsage10(usage, fm.id);
|
|
3663
|
-
conflicts.push({
|
|
3664
|
-
id: fm.id,
|
|
3665
|
-
type: fm.type,
|
|
3666
|
-
scope: fm.scope,
|
|
3667
|
-
status: fm.status,
|
|
3668
|
-
confidence: deriveConfidence8(fm, u),
|
|
3669
|
-
body_preview: other.memory.body.split("\n").slice(0, 4).join("\n").slice(0, 300),
|
|
3670
|
-
similarity: sim,
|
|
3671
|
-
reasons,
|
|
3672
|
-
shared_paths: fm.anchor.paths.filter((p) => targetPaths.some((tp) => pathsOverlap2(p, tp)))
|
|
3673
|
-
});
|
|
3674
|
-
}
|
|
3675
|
-
conflicts.sort((a, b) => {
|
|
3676
|
-
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;
|
|
3677
|
-
return score(b) - score(a);
|
|
3678
|
-
});
|
|
3679
|
-
return {
|
|
3680
|
-
found: true,
|
|
3681
|
-
target: {
|
|
3682
|
-
id: target.memory.frontmatter.id,
|
|
3683
|
-
type: target.memory.frontmatter.type,
|
|
3684
|
-
status: target.memory.frontmatter.status
|
|
3685
|
-
},
|
|
3686
|
-
scanned: others.length,
|
|
3687
|
-
conflicts: conflicts.slice(0, 10)
|
|
3688
|
-
};
|
|
3689
|
-
}
|
|
3690
|
-
function polarity(text) {
|
|
3691
|
-
const neg = NEGATIVE_PATTERNS.test(text);
|
|
3692
|
-
const pos = POSITIVE_PATTERNS.test(text);
|
|
3693
|
-
if (neg && !pos) return "negative";
|
|
3694
|
-
if (pos && !neg) return "positive";
|
|
3695
|
-
return "neutral";
|
|
3696
|
-
}
|
|
3697
|
-
function extractContradictsTags(body) {
|
|
3698
|
-
const out = /* @__PURE__ */ new Set();
|
|
3699
|
-
for (const m of body.matchAll(/#contradicts:([\w-]+)/g)) {
|
|
3700
|
-
if (m[1]) out.add(m[1]);
|
|
3701
|
-
}
|
|
3702
|
-
return out;
|
|
3703
|
-
}
|
|
3704
|
-
function countIntersection(a, b) {
|
|
3705
|
-
let n = 0;
|
|
3706
|
-
for (const x of a) if (b.has(x)) n++;
|
|
3707
|
-
return n;
|
|
3708
|
-
}
|
|
3709
|
-
async function trySemanticSimilarities(ctx, target, others) {
|
|
3710
|
-
let mod;
|
|
3711
|
-
try {
|
|
3712
|
-
mod = await import("@hivelore/embeddings");
|
|
3713
|
-
} catch {
|
|
3714
|
-
return null;
|
|
3715
|
-
}
|
|
3716
|
-
const result = await mod.semanticSearch(
|
|
3717
|
-
ctx.paths,
|
|
3718
|
-
target.memory.body,
|
|
3719
|
-
{ limit: others.length }
|
|
3720
|
-
);
|
|
3721
|
-
if (!result) return null;
|
|
3722
|
-
const map = /* @__PURE__ */ new Map();
|
|
3723
|
-
for (const hit of result.hits) map.set(hit.id, hit.score);
|
|
3724
|
-
return map;
|
|
3725
|
-
}
|
|
3726
|
-
|
|
3727
3355
|
// src/tools/precommit-check.ts
|
|
3728
|
-
import { pathsOverlap as
|
|
3729
|
-
import { z as
|
|
3356
|
+
import { pathsOverlap as pathsOverlap2 } from "@hivelore/core";
|
|
3357
|
+
import { z as z27 } from "zod";
|
|
3730
3358
|
var PreCommitCheckInputSchema = {
|
|
3731
|
-
diff:
|
|
3359
|
+
diff: z27.string().optional().describe(
|
|
3732
3360
|
"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`."
|
|
3733
3361
|
),
|
|
3734
|
-
paths:
|
|
3735
|
-
block_on:
|
|
3362
|
+
paths: z27.array(z27.string()).default([]).describe("Project-relative paths affected by the change. At least one of `diff` or `paths` should be provided."),
|
|
3363
|
+
block_on: z27.enum(["any", "high-confidence", "never"]).default("high-confidence").describe(
|
|
3736
3364
|
"When to set should_block=true: 'any' = any warning blocks; 'high-confidence' = only warnings from authoritative/trusted memories block; 'never' = report only, never block."
|
|
3737
3365
|
),
|
|
3738
|
-
semantic:
|
|
3739
|
-
anchored_blocks:
|
|
3366
|
+
semantic: z27.boolean().default(true).describe("Enable semantic search in anti_patterns_check (requires embeddings index)."),
|
|
3367
|
+
anchored_blocks: z27.boolean().default(false).describe(
|
|
3740
3368
|
"When true, ALSO block a high-confidence anti-pattern (attempt/gotcha) that is anchored to a touched file AND corroborated by the diff (literal token overlap, or semantic >= 0.45) \u2014 not just very strong semantic matches. Powers the 'anchored' enforcement gate. Config/docs-only commits are still downgraded. Default false preserves the soft, semantic-only blocking behavior."
|
|
3741
3369
|
)
|
|
3742
3370
|
};
|
|
@@ -3805,7 +3433,7 @@ async function preCommitCheck(input, ctx) {
|
|
|
3805
3433
|
}
|
|
3806
3434
|
function classifyWarning(warning, paths, anchoredBlocks = false) {
|
|
3807
3435
|
const codeFiles = paths.filter((p) => !p.startsWith(".ai/") && !isHaiveOwnedPath(p));
|
|
3808
|
-
const anchorHits = (warning.anchor_paths ?? []).length > 0 ? codeFiles.filter((p) => (warning.anchor_paths ?? []).some((ap) =>
|
|
3436
|
+
const anchorHits = (warning.anchor_paths ?? []).length > 0 ? codeFiles.filter((p) => (warning.anchor_paths ?? []).some((ap) => pathsOverlap2(ap, p))) : [];
|
|
3809
3437
|
const affectedFiles = anchorHits.length > 0 ? anchorHits : codeFiles;
|
|
3810
3438
|
const repairCommand = repairCommandForWarning(warning, affectedFiles);
|
|
3811
3439
|
const fileDowngrade = fileTypeDowngradeReason(warning, affectedFiles);
|
|
@@ -4041,228 +3669,15 @@ function repairTargetPathForWarning(warning, paths) {
|
|
|
4041
3669
|
return usablePaths[0];
|
|
4042
3670
|
}
|
|
4043
3671
|
|
|
4044
|
-
// src/tools/pattern-detect.ts
|
|
4045
|
-
import { mkdir as mkdir8, writeFile as writeFile15 } from "fs/promises";
|
|
4046
|
-
import { existsSync as existsSync30 } from "fs";
|
|
4047
|
-
import path14 from "path";
|
|
4048
|
-
import { execSync as execSync3 } from "child_process";
|
|
4049
|
-
import {
|
|
4050
|
-
buildFrontmatter as buildFrontmatter5,
|
|
4051
|
-
memoryFilePath as memoryFilePath6,
|
|
4052
|
-
readUsageEvents,
|
|
4053
|
-
serializeMemory as serializeMemory13
|
|
4054
|
-
} from "@hivelore/core";
|
|
4055
|
-
import { z as z32 } from "zod";
|
|
4056
|
-
var CONFIG_PATTERNS = [
|
|
4057
|
-
".eslintrc",
|
|
4058
|
-
"eslint.config",
|
|
4059
|
-
"prettier.config",
|
|
4060
|
-
".prettierrc",
|
|
4061
|
-
"tsconfig",
|
|
4062
|
-
"jsconfig",
|
|
4063
|
-
"vitest.config",
|
|
4064
|
-
"jest.config",
|
|
4065
|
-
".env.example",
|
|
4066
|
-
".env.defaults",
|
|
4067
|
-
"tailwind.config",
|
|
4068
|
-
"vite.config",
|
|
4069
|
-
"next.config",
|
|
4070
|
-
"babel.config",
|
|
4071
|
-
"postcss.config",
|
|
4072
|
-
"renovate.json",
|
|
4073
|
-
"dependabot.yml"
|
|
4074
|
-
];
|
|
4075
|
-
var MAX_DIFF_BYTES = 4096;
|
|
4076
|
-
var HOT_FILE_MIN = 3;
|
|
4077
|
-
var PatternDetectInputSchema = {
|
|
4078
|
-
since_days: z32.number().int().min(1).default(7).describe("Look-back window in days for both git history and usage log."),
|
|
4079
|
-
dry_run: z32.boolean().default(false).describe("When true, report matches without writing any memory files."),
|
|
4080
|
-
scope: z32.enum(["personal", "team"]).default("team").describe("Scope for proposed memories.")
|
|
4081
|
-
};
|
|
4082
|
-
async function patternDetect(input, ctx) {
|
|
4083
|
-
if (!existsSync30(ctx.paths.haiveDir)) {
|
|
4084
|
-
return {
|
|
4085
|
-
scanned_events: 0,
|
|
4086
|
-
matches: [],
|
|
4087
|
-
saved: 0,
|
|
4088
|
-
saved_ids: [],
|
|
4089
|
-
notice: "No .ai/ directory found. Run 'hivelore init' first."
|
|
4090
|
-
};
|
|
4091
|
-
}
|
|
4092
|
-
const matches = [];
|
|
4093
|
-
try {
|
|
4094
|
-
const changedFiles = gitChangedFiles(ctx.paths.root, input.since_days);
|
|
4095
|
-
const configFiles = changedFiles.filter(
|
|
4096
|
-
(f) => CONFIG_PATTERNS.some((p) => path14.basename(f.toLowerCase()).includes(p))
|
|
4097
|
-
);
|
|
4098
|
-
for (const file of configFiles.slice(0, 5)) {
|
|
4099
|
-
const diff = gitFileDiff(ctx.paths.root, file, input.since_days);
|
|
4100
|
-
if (!diff) continue;
|
|
4101
|
-
const parentDir = path14.basename(path14.dirname(file));
|
|
4102
|
-
const baseName = path14.basename(file).replace(/\.[^.]+$/, "");
|
|
4103
|
-
const slug = `${parentDir}-${baseName}`.replace(/[^a-z0-9]/gi, "-").toLowerCase().slice(0, 40);
|
|
4104
|
-
matches.push({
|
|
4105
|
-
kind: "config_change",
|
|
4106
|
-
signal: `Config file modified: ${file}`,
|
|
4107
|
-
proposed_type: "convention",
|
|
4108
|
-
proposed_slug: `config-change-${slug}`,
|
|
4109
|
-
proposed_body: [
|
|
4110
|
-
`# Config change: \`${file}\``,
|
|
4111
|
-
"",
|
|
4112
|
-
"This configuration file was recently modified. The diff below captures the intent.",
|
|
4113
|
-
"Review and update this memory with the **reason** for the change if known.",
|
|
4114
|
-
"",
|
|
4115
|
-
"```diff",
|
|
4116
|
-
diff.slice(0, MAX_DIFF_BYTES),
|
|
4117
|
-
"```"
|
|
4118
|
-
].join("\n"),
|
|
4119
|
-
anchor_paths: [file]
|
|
4120
|
-
});
|
|
4121
|
-
}
|
|
4122
|
-
} catch {
|
|
4123
|
-
}
|
|
4124
|
-
const events = await readUsageEvents(ctx.paths);
|
|
4125
|
-
const cutoff = Date.now() - input.since_days * 24 * 60 * 60 * 1e3;
|
|
4126
|
-
const recent = events.filter((e) => Date.parse(e.at) >= cutoff);
|
|
4127
|
-
const pathCounts = /* @__PURE__ */ new Map();
|
|
4128
|
-
for (const e of recent) {
|
|
4129
|
-
if (!["mem_tried", "mem_observe", "mem_save"].includes(e.tool)) continue;
|
|
4130
|
-
if (!e.summary) continue;
|
|
4131
|
-
const tokens = e.summary.match(/[^\s"'`,;()[\]{}]+\.[a-zA-Z]{1,6}/g) ?? [];
|
|
4132
|
-
for (const t of tokens) {
|
|
4133
|
-
const key = t.toLowerCase();
|
|
4134
|
-
const existing = pathCounts.get(key);
|
|
4135
|
-
if (existing) {
|
|
4136
|
-
existing.count++;
|
|
4137
|
-
existing.tools.add(e.tool);
|
|
4138
|
-
} else {
|
|
4139
|
-
pathCounts.set(key, { count: 1, tools: /* @__PURE__ */ new Set([e.tool]) });
|
|
4140
|
-
}
|
|
4141
|
-
}
|
|
4142
|
-
}
|
|
4143
|
-
for (const [p, { count, tools }] of pathCounts) {
|
|
4144
|
-
if (count < HOT_FILE_MIN) continue;
|
|
4145
|
-
const isGotchaSignal = tools.has("mem_tried") || tools.has("mem_observe");
|
|
4146
|
-
if (!isGotchaSignal) continue;
|
|
4147
|
-
const slug = p.replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 40);
|
|
4148
|
-
matches.push({
|
|
4149
|
-
kind: "repeated_path",
|
|
4150
|
-
signal: `Path '${p}' appears ${count}\xD7 in mem_tried/mem_observe events`,
|
|
4151
|
-
proposed_type: "gotcha",
|
|
4152
|
-
proposed_slug: `repeated-issue-${slug}`,
|
|
4153
|
-
proposed_body: [
|
|
4154
|
-
`# Recurring issue near \`${p}\``,
|
|
4155
|
-
"",
|
|
4156
|
-
`This file appeared ${count} times in failed-approach or observation events over the last ${input.since_days} days. Review the related attempt/gotcha memories and consolidate them into a single authoritative gotcha.`,
|
|
4157
|
-
"",
|
|
4158
|
-
`**Source signals:** ${[...tools].join(", ")} (${count} events)`
|
|
4159
|
-
].join("\n"),
|
|
4160
|
-
anchor_paths: [p]
|
|
4161
|
-
});
|
|
4162
|
-
}
|
|
4163
|
-
for (const [p, { count, tools }] of pathCounts) {
|
|
4164
|
-
if (count < HOT_FILE_MIN) continue;
|
|
4165
|
-
if (tools.has("mem_tried") || tools.has("mem_observe")) continue;
|
|
4166
|
-
if (CONFIG_PATTERNS.some((cp) => path14.basename(p).includes(cp))) continue;
|
|
4167
|
-
const slug = p.replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 40);
|
|
4168
|
-
matches.push({
|
|
4169
|
-
kind: "hot_file",
|
|
4170
|
-
signal: `Path '${p}' referenced ${count}\xD7 across mem_save events`,
|
|
4171
|
-
proposed_type: "convention",
|
|
4172
|
-
proposed_slug: `hot-file-${slug}`,
|
|
4173
|
-
proposed_body: [
|
|
4174
|
-
`# Frequent edits to \`${p}\``,
|
|
4175
|
-
"",
|
|
4176
|
-
`This file was referenced ${count} times in memory-saving events over the last ${input.since_days} days \u2014 a signal that a recurring pattern or convention applies here.`,
|
|
4177
|
-
"",
|
|
4178
|
-
"**Suggested action:** review recent memories anchored to this path and extract the common pattern as a named convention."
|
|
4179
|
-
].join("\n"),
|
|
4180
|
-
anchor_paths: [p]
|
|
4181
|
-
});
|
|
4182
|
-
}
|
|
4183
|
-
if (matches.length === 0) {
|
|
4184
|
-
return {
|
|
4185
|
-
scanned_events: recent.length,
|
|
4186
|
-
matches: [],
|
|
4187
|
-
saved: 0,
|
|
4188
|
-
saved_ids: [],
|
|
4189
|
-
notice: `No patterns detected in the last ${input.since_days} days (${recent.length} events scanned).`
|
|
4190
|
-
};
|
|
4191
|
-
}
|
|
4192
|
-
if (input.dry_run) {
|
|
4193
|
-
return { scanned_events: recent.length, matches, saved: 0, saved_ids: [] };
|
|
4194
|
-
}
|
|
4195
|
-
const savedIds = [];
|
|
4196
|
-
for (const match of matches) {
|
|
4197
|
-
try {
|
|
4198
|
-
const fm = buildFrontmatter5({
|
|
4199
|
-
type: match.proposed_type,
|
|
4200
|
-
slug: match.proposed_slug,
|
|
4201
|
-
scope: input.scope,
|
|
4202
|
-
tags: ["pattern-detect", match.kind],
|
|
4203
|
-
paths: match.anchor_paths,
|
|
4204
|
-
status: "proposed"
|
|
4205
|
-
});
|
|
4206
|
-
const file = memoryFilePath6(
|
|
4207
|
-
ctx.paths,
|
|
4208
|
-
fm.scope === "shared" ? "team" : fm.scope,
|
|
4209
|
-
fm.id,
|
|
4210
|
-
void 0
|
|
4211
|
-
);
|
|
4212
|
-
if (existsSync30(file)) continue;
|
|
4213
|
-
await mkdir8(path14.dirname(file), { recursive: true });
|
|
4214
|
-
await writeFile15(
|
|
4215
|
-
file,
|
|
4216
|
-
serializeMemory13({ frontmatter: fm, body: match.proposed_body }),
|
|
4217
|
-
"utf8"
|
|
4218
|
-
);
|
|
4219
|
-
savedIds.push(fm.id);
|
|
4220
|
-
} catch {
|
|
4221
|
-
}
|
|
4222
|
-
}
|
|
4223
|
-
return {
|
|
4224
|
-
scanned_events: recent.length,
|
|
4225
|
-
matches,
|
|
4226
|
-
saved: savedIds.length,
|
|
4227
|
-
saved_ids: savedIds
|
|
4228
|
-
};
|
|
4229
|
-
}
|
|
4230
|
-
function gitChangedFiles(root, sinceDays) {
|
|
4231
|
-
try {
|
|
4232
|
-
const out = execSync3(
|
|
4233
|
-
`git log --name-only --pretty="" --diff-filter=AM --since="${sinceDays} days ago"`,
|
|
4234
|
-
{ cwd: root, encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
|
|
4235
|
-
);
|
|
4236
|
-
return [...new Set(out.split("\n").map((l) => l.trim()).filter(Boolean))];
|
|
4237
|
-
} catch {
|
|
4238
|
-
return [];
|
|
4239
|
-
}
|
|
4240
|
-
}
|
|
4241
|
-
function gitFileDiff(root, file, sinceDays) {
|
|
4242
|
-
try {
|
|
4243
|
-
const out = execSync3(
|
|
4244
|
-
`git log -p --follow --since="${sinceDays} days ago" -- "${file}"`,
|
|
4245
|
-
{ cwd: root, encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
|
|
4246
|
-
);
|
|
4247
|
-
if (!out.trim()) return null;
|
|
4248
|
-
const diffLines = out.split("\n").filter(
|
|
4249
|
-
(l) => l.startsWith("+") || l.startsWith("-") || l.startsWith("@@") || l.startsWith("diff")
|
|
4250
|
-
);
|
|
4251
|
-
return diffLines.join("\n").slice(0, MAX_DIFF_BYTES) || null;
|
|
4252
|
-
} catch {
|
|
4253
|
-
return null;
|
|
4254
|
-
}
|
|
4255
|
-
}
|
|
4256
|
-
|
|
4257
3672
|
// src/tools/mem-conflict-candidates.ts
|
|
4258
|
-
import { existsSync as
|
|
3673
|
+
import { existsSync as existsSync26 } from "fs";
|
|
4259
3674
|
import {
|
|
4260
3675
|
findLexicalConflictPairs,
|
|
4261
3676
|
findTopicStatusConflictPairs,
|
|
4262
|
-
loadMemoriesFromDir as
|
|
3677
|
+
loadMemoriesFromDir as loadMemoriesFromDir21,
|
|
4263
3678
|
planConflictResolution
|
|
4264
3679
|
} from "@hivelore/core";
|
|
4265
|
-
import { z as
|
|
3680
|
+
import { z as z28 } from "zod";
|
|
4266
3681
|
function suggestResolution(byId, idA, idB) {
|
|
4267
3682
|
const a = byId.get(idA);
|
|
4268
3683
|
const b = byId.get(idB);
|
|
@@ -4276,17 +3691,17 @@ function suggestResolution(byId, idA, idB) {
|
|
|
4276
3691
|
};
|
|
4277
3692
|
}
|
|
4278
3693
|
var MemConflictCandidatesInputSchema = {
|
|
4279
|
-
since_days:
|
|
4280
|
-
types:
|
|
4281
|
-
min_jaccard:
|
|
4282
|
-
max_pairs:
|
|
4283
|
-
max_scan:
|
|
4284
|
-
max_topic_pairs:
|
|
3694
|
+
since_days: z28.number().int().positive().max(3650).default(365).describe("Only memories created since N days ago"),
|
|
3695
|
+
types: z28.array(z28.enum(["decision", "architecture", "convention", "gotcha"])).default(["decision", "architecture"]).describe("Memory types scanned for pairwise lexical overlap"),
|
|
3696
|
+
min_jaccard: z28.number().min(0).max(1).default(0.45).describe("Minimum Jaccard token similarity to surface as a candidate pair"),
|
|
3697
|
+
max_pairs: z28.number().int().positive().max(100).default(20).describe("Cap pairs returned"),
|
|
3698
|
+
max_scan: z28.number().int().positive().max(2e3).default(500).describe("Maximum memories sampled for O(n\xB2) scan \u2014 excess dropped after chronological sort."),
|
|
3699
|
+
max_topic_pairs: z28.number().int().positive().max(100).default(20).describe(
|
|
4285
3700
|
"Cap for extra signal: memories sharing the same topic with validated vs rejected status."
|
|
4286
3701
|
)
|
|
4287
3702
|
};
|
|
4288
3703
|
async function memConflictCandidates(input, ctx) {
|
|
4289
|
-
if (!
|
|
3704
|
+
if (!existsSync26(ctx.paths.memoriesDir)) {
|
|
4290
3705
|
return {
|
|
4291
3706
|
pairs: [],
|
|
4292
3707
|
topic_status_pairs: [],
|
|
@@ -4295,7 +3710,7 @@ async function memConflictCandidates(input, ctx) {
|
|
|
4295
3710
|
notice: "No .ai/memories directory."
|
|
4296
3711
|
};
|
|
4297
3712
|
}
|
|
4298
|
-
const all = await
|
|
3713
|
+
const all = await loadMemoriesFromDir21(ctx.paths.memoriesDir);
|
|
4299
3714
|
const byId = new Map(all.map((m) => [m.memory.frontmatter.id, m]));
|
|
4300
3715
|
const { pairs, scanned, truncated } = findLexicalConflictPairs(all, {
|
|
4301
3716
|
sinceDays: input.since_days,
|
|
@@ -4325,9 +3740,9 @@ async function memConflictCandidates(input, ctx) {
|
|
|
4325
3740
|
|
|
4326
3741
|
// src/tools/mem-resolve-project.ts
|
|
4327
3742
|
import { resolveProjectInfo } from "@hivelore/core";
|
|
4328
|
-
import { z as
|
|
3743
|
+
import { z as z29 } from "zod";
|
|
4329
3744
|
var MemResolveProjectInputSchema = {
|
|
4330
|
-
cwd:
|
|
3745
|
+
cwd: z29.string().optional().describe("Directory used for root discovery when HAIVE_PROJECT_ROOT is unset.")
|
|
4331
3746
|
};
|
|
4332
3747
|
async function memResolveProject(input, _ctx) {
|
|
4333
3748
|
void _ctx;
|
|
@@ -4341,10 +3756,10 @@ async function memResolveProject(input, _ctx) {
|
|
|
4341
3756
|
|
|
4342
3757
|
// src/tools/mem-suggest-topic.ts
|
|
4343
3758
|
import { MemoryTypeSchema, suggestTopicKey } from "@hivelore/core";
|
|
4344
|
-
import { z as
|
|
3759
|
+
import { z as z30 } from "zod";
|
|
4345
3760
|
var MemSuggestTopicInputSchema = {
|
|
4346
3761
|
type: MemoryTypeSchema.describe("Memory kind \u2014 drives the suggested topic family."),
|
|
4347
|
-
title:
|
|
3762
|
+
title: z30.string().min(1).describe("Short title or phrase (headers, headings) \u2014 turned into slug")
|
|
4348
3763
|
};
|
|
4349
3764
|
async function memSuggestTopic(input, _ctx) {
|
|
4350
3765
|
void _ctx;
|
|
@@ -4353,19 +3768,19 @@ async function memSuggestTopic(input, _ctx) {
|
|
|
4353
3768
|
}
|
|
4354
3769
|
|
|
4355
3770
|
// src/tools/mem-timeline.ts
|
|
4356
|
-
import { existsSync as
|
|
4357
|
-
import { collectTimelineEntries, loadMemoriesFromDir as
|
|
4358
|
-
import { z as
|
|
3771
|
+
import { existsSync as existsSync27 } from "fs";
|
|
3772
|
+
import { collectTimelineEntries, loadMemoriesFromDir as loadMemoriesFromDir22 } from "@hivelore/core";
|
|
3773
|
+
import { z as z31 } from "zod";
|
|
4359
3774
|
var MemTimelineInputSchema = {
|
|
4360
|
-
memory_id:
|
|
4361
|
-
topic:
|
|
4362
|
-
limit:
|
|
3775
|
+
memory_id: z31.string().optional().describe("Seed id \u2014 expands via related_ids, topic, anchors"),
|
|
3776
|
+
topic: z31.string().optional().describe("Frontmatter.topic value \u2014 chronological list when memory_id omitted"),
|
|
3777
|
+
limit: z31.number().int().positive().max(100).default(30).describe("Max timeline entries returned")
|
|
4363
3778
|
};
|
|
4364
3779
|
async function memTimeline(input, ctx) {
|
|
4365
|
-
if (!
|
|
3780
|
+
if (!existsSync27(ctx.paths.memoriesDir)) {
|
|
4366
3781
|
return { entries: [], total: 0, notice: "No .ai/memories directory." };
|
|
4367
3782
|
}
|
|
4368
|
-
const all = await
|
|
3783
|
+
const all = await loadMemoriesFromDir22(ctx.paths.memoriesDir);
|
|
4369
3784
|
const { entries, notice } = collectTimelineEntries(all, {
|
|
4370
3785
|
memoryId: input.memory_id,
|
|
4371
3786
|
topic: input.topic,
|
|
@@ -4374,47 +3789,13 @@ async function memTimeline(input, ctx) {
|
|
|
4374
3789
|
return { entries, total: entries.length, notice };
|
|
4375
3790
|
}
|
|
4376
3791
|
|
|
4377
|
-
// src/tools/runtime-journal-append.ts
|
|
4378
|
-
import { appendRuntimeJournalEntry as appendRuntimeJournalEntry2 } from "@hivelore/core";
|
|
4379
|
-
import { z as z37 } from "zod";
|
|
4380
|
-
var RuntimeJournalAppendInputSchema = {
|
|
4381
|
-
message: z37.string().min(1).describe("Short line to append to the runtime session journal"),
|
|
4382
|
-
kind: z37.enum(["note", "session_end", "mcp"]).default("note"),
|
|
4383
|
-
tool: z37.string().optional().describe("When kind=mcp, which tool name (optional)")
|
|
4384
|
-
};
|
|
4385
|
-
async function runtimeJournalAppend(input, ctx) {
|
|
4386
|
-
await appendRuntimeJournalEntry2(ctx.paths, {
|
|
4387
|
-
kind: input.kind,
|
|
4388
|
-
message: input.message,
|
|
4389
|
-
...input.tool ? { tool: input.tool } : {}
|
|
4390
|
-
});
|
|
4391
|
-
return {
|
|
4392
|
-
ok: true,
|
|
4393
|
-
path_hint: `${ctx.paths.runtimeDir}/session-journal.ndjson`
|
|
4394
|
-
};
|
|
4395
|
-
}
|
|
4396
|
-
|
|
4397
|
-
// src/tools/runtime-journal-tail.ts
|
|
4398
|
-
import { readRuntimeJournalTail } from "@hivelore/core";
|
|
4399
|
-
import { z as z38 } from "zod";
|
|
4400
|
-
var RuntimeJournalTailInputSchema = {
|
|
4401
|
-
limit: z38.number().int().positive().max(500).default(30).describe("Last N journal entries to return")
|
|
4402
|
-
};
|
|
4403
|
-
async function runtimeJournalTail(input, ctx) {
|
|
4404
|
-
const entries = await readRuntimeJournalTail(ctx.paths, input.limit);
|
|
4405
|
-
if (entries.length === 0) {
|
|
4406
|
-
return { entries: [], empty: true };
|
|
4407
|
-
}
|
|
4408
|
-
return { entries };
|
|
4409
|
-
}
|
|
4410
|
-
|
|
4411
3792
|
// src/prompts/bootstrap-project.ts
|
|
4412
|
-
import { z as
|
|
3793
|
+
import { z as z32 } from "zod";
|
|
4413
3794
|
var BootstrapProjectArgsSchema = {
|
|
4414
|
-
module:
|
|
3795
|
+
module: z32.string().optional().describe(
|
|
4415
3796
|
"Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
|
|
4416
3797
|
),
|
|
4417
|
-
focus:
|
|
3798
|
+
focus: z32.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
|
|
4418
3799
|
};
|
|
4419
3800
|
var ROOT_TEMPLATE = `# Project context
|
|
4420
3801
|
|
|
@@ -4497,16 +3878,16 @@ ${template}\`\`\`
|
|
|
4497
3878
|
|
|
4498
3879
|
// src/prompts/bootstrap-repo.ts
|
|
4499
3880
|
import { readFile as readFile7, readdir as readdir5 } from "fs/promises";
|
|
4500
|
-
import { existsSync as
|
|
3881
|
+
import { existsSync as existsSync28 } from "fs";
|
|
4501
3882
|
import {
|
|
4502
3883
|
assessBootstrapState as assessBootstrapState2,
|
|
4503
|
-
loadCodeMap as
|
|
4504
|
-
loadMemoriesFromDir as
|
|
3884
|
+
loadCodeMap as loadCodeMap4,
|
|
3885
|
+
loadMemoriesFromDir as loadMemoriesFromDir23,
|
|
4505
3886
|
renderBootstrapChecklist as renderBootstrapChecklist2
|
|
4506
3887
|
} from "@hivelore/core";
|
|
4507
|
-
import { z as
|
|
3888
|
+
import { z as z33 } from "zod";
|
|
4508
3889
|
var BootstrapRepoArgsSchema = {
|
|
4509
|
-
focus:
|
|
3890
|
+
focus: z33.string().optional().describe("Optional area to emphasize first (e.g. 'payments', 'auth').")
|
|
4510
3891
|
};
|
|
4511
3892
|
async function currentAssessment(ctx) {
|
|
4512
3893
|
let projectContextRaw = "";
|
|
@@ -4514,8 +3895,8 @@ async function currentAssessment(ctx) {
|
|
|
4514
3895
|
projectContextRaw = await readFile7(ctx.paths.projectContext, "utf8");
|
|
4515
3896
|
} catch {
|
|
4516
3897
|
}
|
|
4517
|
-
const memories =
|
|
4518
|
-
const codeMap = await
|
|
3898
|
+
const memories = existsSync28(ctx.paths.memoriesDir) ? await loadMemoriesFromDir23(ctx.paths.memoriesDir) : [];
|
|
3899
|
+
const codeMap = await loadCodeMap4(ctx.paths);
|
|
4519
3900
|
let existingModules = [];
|
|
4520
3901
|
try {
|
|
4521
3902
|
const entries = await readdir5(ctx.paths.modulesContextDir, { withFileTypes: true });
|
|
@@ -4582,10 +3963,10 @@ Main code areas detected: ${areas}
|
|
|
4582
3963
|
}
|
|
4583
3964
|
|
|
4584
3965
|
// src/prompts/post-task.ts
|
|
4585
|
-
import { z as
|
|
3966
|
+
import { z as z34 } from "zod";
|
|
4586
3967
|
var PostTaskArgsSchema = {
|
|
4587
|
-
task_summary:
|
|
4588
|
-
files_touched:
|
|
3968
|
+
task_summary: z34.string().optional().describe("One sentence describing what you just did"),
|
|
3969
|
+
files_touched: z34.array(z34.string()).optional().describe("Files you created or modified during the task")
|
|
4589
3970
|
};
|
|
4590
3971
|
function postTaskPrompt(args, ctx) {
|
|
4591
3972
|
const taskLine = args.task_summary ? `
|
|
@@ -4682,12 +4063,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
|
|
|
4682
4063
|
}
|
|
4683
4064
|
|
|
4684
4065
|
// src/prompts/import-docs.ts
|
|
4685
|
-
import { z as
|
|
4066
|
+
import { z as z35 } from "zod";
|
|
4686
4067
|
var ImportDocsArgsSchema = {
|
|
4687
|
-
content:
|
|
4688
|
-
source:
|
|
4689
|
-
scope:
|
|
4690
|
-
dry_run:
|
|
4068
|
+
content: z35.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
|
|
4069
|
+
source: z35.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
|
|
4070
|
+
scope: z35.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
|
|
4071
|
+
dry_run: z35.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
|
|
4691
4072
|
};
|
|
4692
4073
|
function importDocsPrompt(args, ctx) {
|
|
4693
4074
|
const sourceLine = args.source ? `
|
|
@@ -4753,7 +4134,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
4753
4134
|
// src/server.ts
|
|
4754
4135
|
import { hasRecentBriefingMarker, loadConfigSync } from "@hivelore/core";
|
|
4755
4136
|
var SERVER_NAME = "hivelore";
|
|
4756
|
-
var SERVER_VERSION = "0.
|
|
4137
|
+
var SERVER_VERSION = "0.34.1";
|
|
4757
4138
|
function jsonResult(data) {
|
|
4758
4139
|
return {
|
|
4759
4140
|
content: [
|
|
@@ -4801,14 +4182,7 @@ var MAINTENANCE_PROFILE_TOOLS = [
|
|
|
4801
4182
|
"ingest_findings"
|
|
4802
4183
|
];
|
|
4803
4184
|
var EXPERIMENTAL_PROFILE_TOOLS = [
|
|
4804
|
-
...MAINTENANCE_PROFILE_TOOLS
|
|
4805
|
-
"mem_observe",
|
|
4806
|
-
"why_this_file",
|
|
4807
|
-
"why_this_decision",
|
|
4808
|
-
"mem_conflicts_with",
|
|
4809
|
-
"pattern_detect",
|
|
4810
|
-
"runtime_journal_append",
|
|
4811
|
-
"runtime_journal_tail"
|
|
4185
|
+
...MAINTENANCE_PROFILE_TOOLS
|
|
4812
4186
|
];
|
|
4813
4187
|
var TOOL_PROFILES = {
|
|
4814
4188
|
enforcement: new Set(ENFORCEMENT_PROFILE_TOOLS),
|
|
@@ -4823,7 +4197,6 @@ var BRIEFING_TOOLS = /* @__PURE__ */ new Set(["get_briefing", "mem_relevant_to"]
|
|
|
4823
4197
|
var MUTATING_TOOLS = /* @__PURE__ */ new Set([
|
|
4824
4198
|
"mem_save",
|
|
4825
4199
|
"mem_tried",
|
|
4826
|
-
"mem_observe",
|
|
4827
4200
|
"mem_session_end",
|
|
4828
4201
|
"bootstrap_project_save",
|
|
4829
4202
|
"mem_update",
|
|
@@ -4831,8 +4204,6 @@ var MUTATING_TOOLS = /* @__PURE__ */ new Set([
|
|
|
4831
4204
|
"mem_reject",
|
|
4832
4205
|
"mem_delete",
|
|
4833
4206
|
"mem_feedback",
|
|
4834
|
-
"runtime_journal_append",
|
|
4835
|
-
"pattern_detect",
|
|
4836
4207
|
"ingest_findings",
|
|
4837
4208
|
"propose_sensor"
|
|
4838
4209
|
]);
|
|
@@ -4895,7 +4266,7 @@ function createHaiveServer(options = {}) {
|
|
|
4895
4266
|
" - A domain term and what it means in this codebase",
|
|
4896
4267
|
"",
|
|
4897
4268
|
"DO NOT USE for failed approaches \u2192 use mem_tried instead (better structure).",
|
|
4898
|
-
"
|
|
4269
|
+
"For reactive code discoveries during exploration, prefer a compact gotcha via mem_save.",
|
|
4899
4270
|
"",
|
|
4900
4271
|
"PARAMETERS:",
|
|
4901
4272
|
" type \u2014 convention | decision | gotcha | architecture | glossary | attempt",
|
|
@@ -5022,38 +4393,6 @@ function createHaiveServer(options = {}) {
|
|
|
5022
4393
|
return jsonResult(await ingestFindings(input, context));
|
|
5023
4394
|
}
|
|
5024
4395
|
);
|
|
5025
|
-
registerTool(
|
|
5026
|
-
"mem_observe",
|
|
5027
|
-
[
|
|
5028
|
-
"Capture a code-level discovery made WHILE READING existing code.",
|
|
5029
|
-
"",
|
|
5030
|
-
"USE THIS when you read a file and spot something the team may not know about:",
|
|
5031
|
-
" - A bug or race condition hiding in the code",
|
|
5032
|
-
" - A security gap or missing validation",
|
|
5033
|
-
" - An inconsistency between two files",
|
|
5034
|
-
" - A missing configuration or environment variable",
|
|
5035
|
-
" - Anything that could silently break in production",
|
|
5036
|
-
"",
|
|
5037
|
-
"DIFFERENCE from mem_save: mem_observe is for REACTIVE discoveries during code",
|
|
5038
|
-
"reading. mem_save is for deliberate knowledge capture (conventions, decisions).",
|
|
5039
|
-
"",
|
|
5040
|
-
"Auto-validated, anchored to file paths for staleness detection.",
|
|
5041
|
-
"",
|
|
5042
|
-
"PARAMETERS:",
|
|
5043
|
-
" what \u2014 one-line title (e.g. 'MobilePaymentController: duplicate @RequestBody')",
|
|
5044
|
-
" where \u2014 file path(s) where the issue lives",
|
|
5045
|
-
" impact \u2014 what breaks or could break because of this",
|
|
5046
|
-
" fix \u2014 suggested fix (optional)",
|
|
5047
|
-
" scope \u2014 team (default, since discoveries benefit everyone)",
|
|
5048
|
-
"",
|
|
5049
|
-
"RETURNS: { id, file_path }"
|
|
5050
|
-
].join("\n"),
|
|
5051
|
-
MemObserveInputSchema,
|
|
5052
|
-
async (input) => {
|
|
5053
|
-
tracker.record("mem_observe", input.where);
|
|
5054
|
-
return jsonResult(await memObserve(input, context));
|
|
5055
|
-
}
|
|
5056
|
-
);
|
|
5057
4396
|
registerTool(
|
|
5058
4397
|
"mem_session_end",
|
|
5059
4398
|
[
|
|
@@ -5513,26 +4852,6 @@ function createHaiveServer(options = {}) {
|
|
|
5513
4852
|
return jsonResult(await codeSearch(input, context));
|
|
5514
4853
|
}
|
|
5515
4854
|
);
|
|
5516
|
-
registerTool(
|
|
5517
|
-
"why_this_file",
|
|
5518
|
-
[
|
|
5519
|
-
"One-shot file-context lookup: combines recent git history, memories anchored",
|
|
5520
|
-
"to the path, and the code-map entry. Answers 'why is this file the way it is?'",
|
|
5521
|
-
"in a single call instead of 3-4 manual ones.",
|
|
5522
|
-
"",
|
|
5523
|
-
"PARAMETERS:",
|
|
5524
|
-
" path \u2014 project-relative path (required)",
|
|
5525
|
-
" git_log_limit \u2014 recent commits to include (default 5)",
|
|
5526
|
-
" memory_limit \u2014 anchored memories cap (default 5)",
|
|
5527
|
-
"",
|
|
5528
|
-
"RETURNS: { file, exists, recent_commits: [...], memories: [...], code_map_entry, hints? }"
|
|
5529
|
-
].join("\n"),
|
|
5530
|
-
WhyThisFileInputSchema,
|
|
5531
|
-
async (input) => {
|
|
5532
|
-
tracker.record("why_this_file", input.path);
|
|
5533
|
-
return jsonResult(await whyThisFile(input, context));
|
|
5534
|
-
}
|
|
5535
|
-
);
|
|
5536
4855
|
registerTool(
|
|
5537
4856
|
"anti_patterns_check",
|
|
5538
4857
|
[
|
|
@@ -5582,54 +4901,6 @@ function createHaiveServer(options = {}) {
|
|
|
5582
4901
|
return jsonResult(await memDistill(input, context));
|
|
5583
4902
|
}
|
|
5584
4903
|
);
|
|
5585
|
-
registerTool(
|
|
5586
|
-
"why_this_decision",
|
|
5587
|
-
[
|
|
5588
|
-
"Trace the genealogy of a memory (especially decision/architecture):",
|
|
5589
|
-
"the memory itself + memories explicitly linked via related_ids + memories",
|
|
5590
|
-
"anchored to overlapping paths + recent commits touching those paths.",
|
|
5591
|
-
"",
|
|
5592
|
-
"USE WHEN you find a memory and need to understand WHY it was made and",
|
|
5593
|
-
"what surrounds it. One call instead of 4-5 manual lookups.",
|
|
5594
|
-
"",
|
|
5595
|
-
"PARAMETERS:",
|
|
5596
|
-
" id \u2014 memory id (required)",
|
|
5597
|
-
" git_log_limit \u2014 how many recent commits per anchor path (default 5)",
|
|
5598
|
-
"",
|
|
5599
|
-
"RETURNS: { decision, related: [...], path_neighbors: [...], recent_commits: [...] }"
|
|
5600
|
-
].join("\n"),
|
|
5601
|
-
WhyThisDecisionInputSchema,
|
|
5602
|
-
async (input) => {
|
|
5603
|
-
tracker.record("why_this_decision", input.id);
|
|
5604
|
-
return jsonResult(await whyThisDecision(input, context));
|
|
5605
|
-
}
|
|
5606
|
-
);
|
|
5607
|
-
registerTool(
|
|
5608
|
-
"mem_conflicts_with",
|
|
5609
|
-
[
|
|
5610
|
-
"Detect memories that potentially CONTRADICT a given memory.",
|
|
5611
|
-
"",
|
|
5612
|
-
"USE BEFORE relying on a memory's advice \u2014 surfaces 'another memory says",
|
|
5613
|
-
"the opposite'. Detection uses several heuristics layered together:",
|
|
5614
|
-
"",
|
|
5615
|
-
" 1. Opposite status \u2014 validated vs rejected on overlapping topic",
|
|
5616
|
-
" 2. attempt-vs-convention on overlapping anchor paths",
|
|
5617
|
-
" 3. Polarity keywords \u2014 'use X' vs 'do not use X' among semantic neighbors",
|
|
5618
|
-
" 4. Explicit #contradicts:<id> tags in either body",
|
|
5619
|
-
"",
|
|
5620
|
-
"PARAMETERS:",
|
|
5621
|
-
" id \u2014 memory id to check (required)",
|
|
5622
|
-
" min_score \u2014 minimum cosine similarity for semantic neighbors (default 0.5)",
|
|
5623
|
-
" semantic \u2014 use embeddings (default true)",
|
|
5624
|
-
"",
|
|
5625
|
-
"RETURNS: { found, target, scanned, conflicts: [{ id, reasons, similarity, ... }] }"
|
|
5626
|
-
].join("\n"),
|
|
5627
|
-
MemConflictsInputSchema,
|
|
5628
|
-
async (input) => {
|
|
5629
|
-
tracker.record("mem_conflicts_with", input.id);
|
|
5630
|
-
return jsonResult(await memConflicts(input, context));
|
|
5631
|
-
}
|
|
5632
|
-
);
|
|
5633
4904
|
registerTool(
|
|
5634
4905
|
"mem_conflict_candidates",
|
|
5635
4906
|
[
|
|
@@ -5638,7 +4909,7 @@ function createHaiveServer(options = {}) {
|
|
|
5638
4909
|
" 1. Lexical similarity (Jaccard) on decision/architecture-like pairs",
|
|
5639
4910
|
" 2. Same frontmatter.topic with validated vs rejected \u2014 quick human-review signal",
|
|
5640
4911
|
"",
|
|
5641
|
-
"Advisory only \u2014
|
|
4912
|
+
"Advisory only \u2014 review the listed candidate ids, then resolve with mem_update/mem_delete.",
|
|
5642
4913
|
"",
|
|
5643
4914
|
"PARAMETERS:",
|
|
5644
4915
|
" since_days, types, min_jaccard, max_pairs, max_scan, max_topic_pairs",
|
|
@@ -5651,32 +4922,6 @@ function createHaiveServer(options = {}) {
|
|
|
5651
4922
|
return jsonResult(await memConflictCandidates(input, context));
|
|
5652
4923
|
}
|
|
5653
4924
|
);
|
|
5654
|
-
registerTool(
|
|
5655
|
-
"runtime_journal_append",
|
|
5656
|
-
[
|
|
5657
|
-
"Append one line to `.ai/.runtime/session-journal.ndjson` \u2014 machine-local session continuity.",
|
|
5658
|
-
"",
|
|
5659
|
-
"Does NOT replace team memories; complements mem_session_end recaps for local traces.",
|
|
5660
|
-
"",
|
|
5661
|
-
"PARAMETERS: message, kind (note|session_end|mcp), optional tool",
|
|
5662
|
-
"",
|
|
5663
|
-
"RETURNS: { ok, path_hint }"
|
|
5664
|
-
].join("\n"),
|
|
5665
|
-
RuntimeJournalAppendInputSchema,
|
|
5666
|
-
async (input) => jsonResult(await runtimeJournalAppend(input, context))
|
|
5667
|
-
);
|
|
5668
|
-
registerTool(
|
|
5669
|
-
"runtime_journal_tail",
|
|
5670
|
-
[
|
|
5671
|
-
"Read the last N entries from the runtime session journal (parsed JSON lines).",
|
|
5672
|
-
"",
|
|
5673
|
-
"PARAMETERS: limit (default 30, max 500)",
|
|
5674
|
-
"",
|
|
5675
|
-
"RETURNS: { entries: [...], empty?: true }"
|
|
5676
|
-
].join("\n"),
|
|
5677
|
-
RuntimeJournalTailInputSchema,
|
|
5678
|
-
async (input) => jsonResult(await runtimeJournalTail(input, context))
|
|
5679
|
-
);
|
|
5680
4925
|
registerTool(
|
|
5681
4926
|
"pre_commit_check",
|
|
5682
4927
|
[
|
|
@@ -5703,37 +4948,6 @@ function createHaiveServer(options = {}) {
|
|
|
5703
4948
|
return jsonResult(await preCommitCheck(input, context));
|
|
5704
4949
|
}
|
|
5705
4950
|
);
|
|
5706
|
-
registerTool(
|
|
5707
|
-
"pattern_detect",
|
|
5708
|
-
[
|
|
5709
|
-
"Heuristic memory detector \u2014 finds knowledge worth saving WITHOUT calling an LLM.",
|
|
5710
|
-
"",
|
|
5711
|
-
"Runs three signals over local git history and the tool-usage log:",
|
|
5712
|
-
" 1. CONFIG_CHANGE \u2014 config files modified recently (tsconfig, eslint, prettier, \u2026)",
|
|
5713
|
-
" \u2192 proposes a convention memory with the git diff as body.",
|
|
5714
|
-
" 2. REPEATED_PATH \u2014 same file appears \u22653\xD7 in mem_tried/mem_observe events",
|
|
5715
|
-
" \u2192 proposes a gotcha memory anchored to that path.",
|
|
5716
|
-
" 3. HOT_FILE \u2014 source file referenced \u22653\xD7 in writing-tool events",
|
|
5717
|
-
" \u2192 proposes a convention memory (frequent edits = pattern emerging).",
|
|
5718
|
-
"",
|
|
5719
|
-
"Saves memories with status='proposed'. They feed into auto-promote (Phase 4)",
|
|
5720
|
-
"or are surfaced in the next post_task distillation for LLM review.",
|
|
5721
|
-
"",
|
|
5722
|
-
"USE periodically (e.g. end of sprint) or trigger from post-commit hook.",
|
|
5723
|
-
"",
|
|
5724
|
-
"PARAMETERS:",
|
|
5725
|
-
" since_days \u2014 look-back window in days (default 7)",
|
|
5726
|
-
" dry_run \u2014 report matches without saving (default false)",
|
|
5727
|
-
" scope \u2014 'team' (default) | 'personal'",
|
|
5728
|
-
"",
|
|
5729
|
-
"RETURNS: { scanned_events, matches: [{kind, signal, proposed_type, \u2026}], saved, saved_ids }"
|
|
5730
|
-
].join("\n"),
|
|
5731
|
-
PatternDetectInputSchema,
|
|
5732
|
-
async (input) => {
|
|
5733
|
-
tracker.record("pattern_detect", `since=${input.since_days}d/dry_run=${input.dry_run}`);
|
|
5734
|
-
return jsonResult(await patternDetect(input, context));
|
|
5735
|
-
}
|
|
5736
|
-
);
|
|
5737
4951
|
registerTool(
|
|
5738
4952
|
"mem_diff",
|
|
5739
4953
|
[
|
|
@@ -5784,7 +4998,7 @@ function createHaiveServer(options = {}) {
|
|
|
5784
4998
|
[
|
|
5785
4999
|
"\u2B50 Post-task reflection \u2014 run at the end of every session to capture what you learned:",
|
|
5786
5000
|
"failed approaches (mem_tried), new conventions/decisions/gotchas (mem_save),",
|
|
5787
|
-
"
|
|
5001
|
+
"failed approaches (mem_tried), and an end-of-session recap (mem_session_end).",
|
|
5788
5002
|
"In autopilot mode a minimal recap saves automatically; calling this produces a richer one."
|
|
5789
5003
|
].join(" "),
|
|
5790
5004
|
PostTaskArgsSchema,
|