@hivelore/mcp 0.30.1 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +398 -1136
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +100 -196
- package/dist/server.js +401 -1143
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1122,94 +1122,45 @@ async function memApprove(input, ctx) {
|
|
|
1122
1122
|
}
|
|
1123
1123
|
|
|
1124
1124
|
// src/tools/mem-tried.ts
|
|
1125
|
-
import { mkdir as mkdir3, writeFile as
|
|
1126
|
-
import { existsSync as
|
|
1127
|
-
import
|
|
1125
|
+
import { mkdir as mkdir3, writeFile as writeFile9 } from "fs/promises";
|
|
1126
|
+
import { existsSync as existsSync16 } from "fs";
|
|
1127
|
+
import path6 from "path";
|
|
1128
1128
|
import {
|
|
1129
1129
|
buildFrontmatter as buildFrontmatter2,
|
|
1130
1130
|
memoryFilePath as memoryFilePath2,
|
|
1131
|
-
serializeMemory as
|
|
1131
|
+
serializeMemory as serializeMemory8,
|
|
1132
1132
|
suggestSensorSeed as suggestSensorSeed2
|
|
1133
1133
|
} from "@hivelore/core";
|
|
1134
|
-
import { z as
|
|
1135
|
-
var MemTriedInputSchema = {
|
|
1136
|
-
what: z15.string().min(1).describe("Brief description of the approach that was tried"),
|
|
1137
|
-
why_failed: z15.string().min(1).describe("Why it failed or why it should NOT be used"),
|
|
1138
|
-
instead: z15.string().optional().describe("What to use or do instead (recommended alternative)"),
|
|
1139
|
-
scope: z15.enum(["personal", "team", "module"]).default("personal").describe("Visibility scope"),
|
|
1140
|
-
module: z15.string().optional().describe("Module name (required when scope=module)"),
|
|
1141
|
-
tags: z15.array(z15.string()).default([]).describe("Tags for filtering"),
|
|
1142
|
-
paths: z15.array(z15.string()).default([]).describe("Anchor file paths this applies to"),
|
|
1143
|
-
author: z15.string().optional().describe("Author handle or email")
|
|
1144
|
-
};
|
|
1145
|
-
async function memTried(input, ctx) {
|
|
1146
|
-
if (!existsSync15(ctx.paths.haiveDir)) {
|
|
1147
|
-
throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'hivelore init' first.`);
|
|
1148
|
-
}
|
|
1149
|
-
const slug = input.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 5).join("-");
|
|
1150
|
-
const baseFm = buildFrontmatter2({
|
|
1151
|
-
type: "attempt",
|
|
1152
|
-
slug,
|
|
1153
|
-
scope: input.scope,
|
|
1154
|
-
module: input.module,
|
|
1155
|
-
tags: input.tags,
|
|
1156
|
-
paths: input.paths,
|
|
1157
|
-
author: input.author
|
|
1158
|
-
});
|
|
1159
|
-
const frontmatter = { ...baseFm, status: "validated" };
|
|
1160
|
-
const lines = [`# ${input.what}`, ""];
|
|
1161
|
-
lines.push(`**Why it failed / do NOT use:** ${input.why_failed}`);
|
|
1162
|
-
if (input.instead) {
|
|
1163
|
-
lines.push("", `**Instead, use:** ${input.instead}`);
|
|
1164
|
-
}
|
|
1165
|
-
const body = lines.join("\n") + "\n";
|
|
1166
|
-
const file = memoryFilePath2(ctx.paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
1167
|
-
await mkdir3(path5.dirname(file), { recursive: true });
|
|
1168
|
-
if (existsSync15(file)) {
|
|
1169
|
-
throw new Error(`Memory already exists at ${file}`);
|
|
1170
|
-
}
|
|
1171
|
-
await writeFile8(file, serializeMemory7({ frontmatter, body }), "utf8");
|
|
1172
|
-
const seed = input.paths.length > 0 ? suggestSensorSeed2(body, input.paths) : null;
|
|
1173
|
-
const hint = input.paths.length === 0 ? "No `paths` given, so this attempt is feedforward-only \u2014 it will be briefed but the gate cannot block the repeat. Re-run with `paths` set to the file(s) where the mistake lives, then call propose_sensor to close the loop." : seed ? "This attempt is NOT yet enforced. Call propose_sensor to turn it into a reliable block \u2014 a candidate is pre-filled in proposed_sensor_seed (refine it: pattern = the faulty usage, absent = the correct-usage marker). Hivelore validates the proposal (silent on current code, fires on the bad example) before trusting it to block." : "This attempt is NOT yet enforced and no candidate pattern could be derived from the wording. Call propose_sensor with a discriminating pattern (pattern = faulty usage, absent = correct-usage marker) to close the loop.";
|
|
1174
|
-
return {
|
|
1175
|
-
id: frontmatter.id,
|
|
1176
|
-
scope: frontmatter.scope,
|
|
1177
|
-
file_path: file,
|
|
1178
|
-
loop_open: true,
|
|
1179
|
-
...seed ? {
|
|
1180
|
-
proposed_sensor_seed: {
|
|
1181
|
-
pattern: seed.pattern,
|
|
1182
|
-
...seed.absent ? { absent: seed.absent } : {},
|
|
1183
|
-
message: seed.message
|
|
1184
|
-
}
|
|
1185
|
-
} : {},
|
|
1186
|
-
hint
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1134
|
+
import { z as z16 } from "zod";
|
|
1189
1135
|
|
|
1190
1136
|
// src/tools/propose-sensor.ts
|
|
1191
1137
|
import { execSync } from "child_process";
|
|
1192
|
-
import { readFile as readFile3, writeFile as
|
|
1193
|
-
import { existsSync as
|
|
1194
|
-
import
|
|
1138
|
+
import { readFile as readFile3, writeFile as writeFile8 } from "fs/promises";
|
|
1139
|
+
import { existsSync as existsSync15 } from "fs";
|
|
1140
|
+
import path5 from "path";
|
|
1195
1141
|
import {
|
|
1196
1142
|
extractSensorExamples,
|
|
1197
1143
|
judgeProposedSensor,
|
|
1198
1144
|
loadMemoriesFromDir as loadMemoriesFromDir13,
|
|
1199
|
-
serializeMemory as
|
|
1145
|
+
serializeMemory as serializeMemory7
|
|
1200
1146
|
} from "@hivelore/core";
|
|
1201
|
-
import { z as
|
|
1147
|
+
import { z as z15 } from "zod";
|
|
1202
1148
|
var ProposeSensorInputSchema = {
|
|
1203
|
-
memory_id:
|
|
1204
|
-
|
|
1205
|
-
|
|
1149
|
+
memory_id: z15.string().min(1).describe("Id of the gotcha/attempt memory this sensor protects."),
|
|
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)."),
|
|
1156
|
+
absent: z15.string().optional().describe(
|
|
1206
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."
|
|
1207
1158
|
),
|
|
1208
|
-
bad_example:
|
|
1209
|
-
severity:
|
|
1210
|
-
message:
|
|
1211
|
-
flags:
|
|
1212
|
-
paths:
|
|
1159
|
+
bad_example: z15.string().optional().describe("A code snippet that SHOULD match \u2014 proves the sensor catches the mistake. If omitted, examples are read from the lesson body."),
|
|
1160
|
+
severity: z15.enum(["warn", "block"]).default("block").describe("block = hard-fail the gate (accepted ONLY if it passes self-validation). warn = advisory."),
|
|
1161
|
+
message: z15.string().optional().describe("LLM-facing fix message shown when it fires. Defaults to one derived from the lesson."),
|
|
1162
|
+
flags: z15.string().optional().describe("Optional regex flags (e.g. 'i' for case-insensitive)."),
|
|
1163
|
+
paths: z15.array(z15.string()).default([]).describe("Override scope paths. Defaults to the memory's anchor paths.")
|
|
1213
1164
|
};
|
|
1214
1165
|
function deriveMessage(body, pattern, absent) {
|
|
1215
1166
|
const instead = body.match(/\*\*Instead,\s*use:\*\*\s*([^\n]+)/i)?.[1]?.trim();
|
|
@@ -1234,8 +1185,8 @@ async function readPresumedCorrectTargets(root, relPaths) {
|
|
|
1234
1185
|
continue;
|
|
1235
1186
|
} catch {
|
|
1236
1187
|
}
|
|
1237
|
-
const abs =
|
|
1238
|
-
if (!
|
|
1188
|
+
const abs = path5.resolve(root, rel);
|
|
1189
|
+
if (!existsSync15(abs)) continue;
|
|
1239
1190
|
try {
|
|
1240
1191
|
targets.push({ path: rel, content: await readFile3(abs, "utf8") });
|
|
1241
1192
|
} catch {
|
|
@@ -1243,20 +1194,62 @@ async function readPresumedCorrectTargets(root, relPaths) {
|
|
|
1243
1194
|
}
|
|
1244
1195
|
return targets;
|
|
1245
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
|
+
}
|
|
1246
1217
|
async function proposeSensor(input, ctx) {
|
|
1247
|
-
if (!
|
|
1218
|
+
if (!existsSync15(ctx.paths.memoriesDir)) {
|
|
1248
1219
|
throw new Error(`No .ai/memories at ${ctx.paths.root}. Run 'hivelore init' first.`);
|
|
1249
1220
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
if (input.
|
|
1253
|
-
|
|
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()) {
|
|
1254
1247
|
return {
|
|
1255
1248
|
accepted: false,
|
|
1256
1249
|
memory_id: input.memory_id,
|
|
1257
1250
|
severity: input.severity,
|
|
1258
|
-
reason: "invalid-
|
|
1259
|
-
guidance:
|
|
1251
|
+
reason: "invalid-command",
|
|
1252
|
+
guidance: "kind=shell|test requires a `command` (the check the gate will execute).",
|
|
1260
1253
|
self_check: { silent_on_current: false, fires_on_bad: null, fired_on: [] }
|
|
1261
1254
|
};
|
|
1262
1255
|
}
|
|
@@ -1265,6 +1258,43 @@ async function proposeSensor(input, ctx) {
|
|
|
1265
1258
|
if (!found) {
|
|
1266
1259
|
throw new Error(`No memory found with id ${input.memory_id}`);
|
|
1267
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
|
+
}
|
|
1268
1298
|
const anchorPaths = input.paths.length > 0 ? input.paths : found.memory.frontmatter.anchor.paths;
|
|
1269
1299
|
const currentTargets = await readPresumedCorrectTargets(ctx.paths.root, anchorPaths);
|
|
1270
1300
|
const badExamples = [
|
|
@@ -1304,7 +1334,7 @@ async function proposeSensor(input, ctx) {
|
|
|
1304
1334
|
frontmatter: { ...found.memory.frontmatter, sensor },
|
|
1305
1335
|
body: found.memory.body
|
|
1306
1336
|
};
|
|
1307
|
-
await
|
|
1337
|
+
await writeFile8(found.filePath, serializeMemory7(next), "utf8");
|
|
1308
1338
|
return {
|
|
1309
1339
|
accepted: true,
|
|
1310
1340
|
memory_id: input.memory_id,
|
|
@@ -1314,6 +1344,105 @@ async function proposeSensor(input, ctx) {
|
|
|
1314
1344
|
};
|
|
1315
1345
|
}
|
|
1316
1346
|
|
|
1347
|
+
// src/tools/mem-tried.ts
|
|
1348
|
+
var MemTriedInputSchema = {
|
|
1349
|
+
what: z16.string().min(1).describe("Brief description of the approach that was tried"),
|
|
1350
|
+
why_failed: z16.string().min(1).describe("Why it failed or why it should NOT be used"),
|
|
1351
|
+
instead: z16.string().optional().describe("What to use or do instead (recommended alternative)"),
|
|
1352
|
+
scope: z16.enum(["personal", "team", "module"]).default("personal").describe("Visibility scope"),
|
|
1353
|
+
module: z16.string().optional().describe("Module name (required when scope=module)"),
|
|
1354
|
+
tags: z16.array(z16.string()).default([]).describe("Tags for filtering"),
|
|
1355
|
+
paths: z16.array(z16.string()).default([]).describe("Anchor file paths this applies to"),
|
|
1356
|
+
author: z16.string().optional().describe("Author handle or email"),
|
|
1357
|
+
sensor: z16.object({
|
|
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"),
|
|
1363
|
+
severity: z16.enum(["warn", "block"]).default("block").describe("block = deterministic gate refusal"),
|
|
1364
|
+
message: z16.string().optional().describe("Self-correction message shown when the sensor fires"),
|
|
1365
|
+
bad_example: z16.string().optional().describe("kind=regex: code snippet the sensor MUST fire on (validation)")
|
|
1366
|
+
}).optional().describe(
|
|
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."
|
|
1368
|
+
)
|
|
1369
|
+
};
|
|
1370
|
+
async function memTried(input, ctx) {
|
|
1371
|
+
if (!existsSync16(ctx.paths.haiveDir)) {
|
|
1372
|
+
throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'hivelore init' first.`);
|
|
1373
|
+
}
|
|
1374
|
+
const slug = input.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 5).join("-");
|
|
1375
|
+
const baseFm = buildFrontmatter2({
|
|
1376
|
+
type: "attempt",
|
|
1377
|
+
slug,
|
|
1378
|
+
scope: input.scope,
|
|
1379
|
+
module: input.module,
|
|
1380
|
+
tags: input.tags,
|
|
1381
|
+
paths: input.paths,
|
|
1382
|
+
author: input.author
|
|
1383
|
+
});
|
|
1384
|
+
const frontmatter = { ...baseFm, status: "validated" };
|
|
1385
|
+
const lines = [`# ${input.what}`, ""];
|
|
1386
|
+
lines.push(`**Why it failed / do NOT use:** ${input.why_failed}`);
|
|
1387
|
+
if (input.instead) {
|
|
1388
|
+
lines.push("", `**Instead, use:** ${input.instead}`);
|
|
1389
|
+
}
|
|
1390
|
+
const body = lines.join("\n") + "\n";
|
|
1391
|
+
const file = memoryFilePath2(ctx.paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
1392
|
+
await mkdir3(path6.dirname(file), { recursive: true });
|
|
1393
|
+
if (existsSync16(file)) {
|
|
1394
|
+
throw new Error(`Memory already exists at ${file}`);
|
|
1395
|
+
}
|
|
1396
|
+
await writeFile9(file, serializeMemory8({ frontmatter, body }), "utf8");
|
|
1397
|
+
if (input.sensor) {
|
|
1398
|
+
const verdict = await proposeSensor(
|
|
1399
|
+
{
|
|
1400
|
+
memory_id: frontmatter.id,
|
|
1401
|
+
kind: input.sensor.kind ?? "regex",
|
|
1402
|
+
pattern: input.sensor.pattern,
|
|
1403
|
+
command: input.sensor.command,
|
|
1404
|
+
timeout_ms: input.sensor.timeout_ms,
|
|
1405
|
+
absent: input.sensor.absent,
|
|
1406
|
+
severity: input.sensor.severity ?? "block",
|
|
1407
|
+
message: input.sensor.message,
|
|
1408
|
+
bad_example: input.sensor.bad_example,
|
|
1409
|
+
flags: void 0,
|
|
1410
|
+
paths: []
|
|
1411
|
+
},
|
|
1412
|
+
ctx
|
|
1413
|
+
);
|
|
1414
|
+
return {
|
|
1415
|
+
id: frontmatter.id,
|
|
1416
|
+
scope: frontmatter.scope,
|
|
1417
|
+
file_path: file,
|
|
1418
|
+
loop_open: !verdict.accepted,
|
|
1419
|
+
sensor_result: {
|
|
1420
|
+
accepted: verdict.accepted,
|
|
1421
|
+
severity: input.sensor.severity ?? "block",
|
|
1422
|
+
...verdict.reason ? { reason: verdict.reason } : {},
|
|
1423
|
+
...verdict.guidance ? { guidance: verdict.guidance } : {}
|
|
1424
|
+
},
|
|
1425
|
+
hint: verdict.accepted ? "Loop closed: the attempt is saved AND enforced \u2014 the gate now refuses a repeat deterministically." : `Attempt saved, but the sensor was rejected (${verdict.reason}). Revise per the guidance and re-propose with propose_sensor.`
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
const seed = input.paths.length > 0 ? suggestSensorSeed2(body, input.paths) : null;
|
|
1429
|
+
const hint = input.paths.length === 0 ? "No `paths` given, so this attempt is feedforward-only \u2014 it will be briefed but the gate cannot block the repeat. Re-run with `paths` set to the file(s) where the mistake lives, then call propose_sensor to close the loop." : seed ? "This attempt is NOT yet enforced. Call propose_sensor (or re-run mem_tried with the one-shot `sensor` parameter) to turn it into a reliable block \u2014 a candidate is pre-filled in proposed_sensor_seed (refine it: pattern = the faulty usage, absent = the correct-usage marker). Hivelore validates the proposal (silent on current code, fires on the bad example) before trusting it to block." : "This attempt is NOT yet enforced and no candidate pattern could be derived from the wording. Call propose_sensor with a discriminating pattern (pattern = faulty usage, absent = correct-usage marker) to close the loop.";
|
|
1430
|
+
return {
|
|
1431
|
+
id: frontmatter.id,
|
|
1432
|
+
scope: frontmatter.scope,
|
|
1433
|
+
file_path: file,
|
|
1434
|
+
loop_open: true,
|
|
1435
|
+
...seed ? {
|
|
1436
|
+
proposed_sensor_seed: {
|
|
1437
|
+
pattern: seed.pattern,
|
|
1438
|
+
...seed.absent ? { absent: seed.absent } : {},
|
|
1439
|
+
message: seed.message
|
|
1440
|
+
}
|
|
1441
|
+
} : {},
|
|
1442
|
+
hint
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1317
1446
|
// src/tools/ingest-findings.ts
|
|
1318
1447
|
import { existsSync as existsSync17 } from "fs";
|
|
1319
1448
|
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile10 } from "fs/promises";
|
|
@@ -1407,83 +1536,17 @@ async function writeDraft(ctx, draft) {
|
|
|
1407
1536
|
return file;
|
|
1408
1537
|
}
|
|
1409
1538
|
|
|
1410
|
-
// src/tools/mem-
|
|
1411
|
-
import {
|
|
1412
|
-
import { existsSync as
|
|
1413
|
-
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";
|
|
1414
1543
|
import {
|
|
1415
1544
|
buildFrontmatter as buildFrontmatter3,
|
|
1416
|
-
|
|
1545
|
+
loadMemoriesFromDir as loadMemoriesFromDir15,
|
|
1417
1546
|
memoryFilePath as memoryFilePath4,
|
|
1418
1547
|
serializeMemory as serializeMemory10
|
|
1419
1548
|
} from "@hivelore/core";
|
|
1420
1549
|
import { z as z18 } from "zod";
|
|
1421
|
-
var MemObserveInputSchema = {
|
|
1422
|
-
what: z18.string().min(1).describe("Short title: what did you observe? (e.g. 'MobilePaymentController has two @RequestBody on handleWebhook')"),
|
|
1423
|
-
where: z18.string().min(1).describe("File path(s) where the issue lives \u2014 be specific"),
|
|
1424
|
-
impact: z18.string().min(1).describe("What breaks or could break because of this (e.g. 'Spring MVC rejects the handler at startup')"),
|
|
1425
|
-
fix: z18.string().optional().describe("Suggested fix or workaround (optional \u2014 leave empty if unknown)"),
|
|
1426
|
-
scope: z18.enum(["personal", "team", "module"]).default("team").describe("Visibility scope \u2014 defaults to team since discoveries benefit everyone"),
|
|
1427
|
-
module: z18.string().optional().describe("Module name (required when scope=module)"),
|
|
1428
|
-
tags: z18.array(z18.string()).default([]).describe("Tags for filtering"),
|
|
1429
|
-
author: z18.string().optional().describe("Author handle or email"),
|
|
1430
|
-
force: z18.boolean().default(false).describe(
|
|
1431
|
-
"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."
|
|
1432
|
-
)
|
|
1433
|
-
};
|
|
1434
|
-
async function memObserve(input, ctx) {
|
|
1435
|
-
if (!existsSync18(ctx.paths.haiveDir)) {
|
|
1436
|
-
throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'hivelore init' first.`);
|
|
1437
|
-
}
|
|
1438
|
-
const signalText = [input.what, input.impact, input.fix ?? ""].join(" ");
|
|
1439
|
-
if (!input.force && isLikelyGuessable(signalText)) {
|
|
1440
|
-
return {
|
|
1441
|
-
id: "",
|
|
1442
|
-
scope: input.scope,
|
|
1443
|
-
file_path: "",
|
|
1444
|
-
skipped: true,
|
|
1445
|
-
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."
|
|
1446
|
-
};
|
|
1447
|
-
}
|
|
1448
|
-
const slug = input.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 6).join("-");
|
|
1449
|
-
const anchorPaths = input.where.split(/[,\n]/).map((s) => s.trim()).filter(Boolean);
|
|
1450
|
-
const baseFm = buildFrontmatter3({
|
|
1451
|
-
type: "gotcha",
|
|
1452
|
-
slug,
|
|
1453
|
-
scope: input.scope,
|
|
1454
|
-
module: input.module,
|
|
1455
|
-
tags: input.tags,
|
|
1456
|
-
paths: anchorPaths,
|
|
1457
|
-
author: input.author
|
|
1458
|
-
});
|
|
1459
|
-
const frontmatter = { ...baseFm, status: "validated" };
|
|
1460
|
-
const lines = [`# ${input.what}`, ""];
|
|
1461
|
-
lines.push(`**Where:** \`${input.where}\``);
|
|
1462
|
-
lines.push("", `**Impact:** ${input.impact}`);
|
|
1463
|
-
if (input.fix) {
|
|
1464
|
-
lines.push("", `**Fix/workaround:** ${input.fix}`);
|
|
1465
|
-
}
|
|
1466
|
-
const body = lines.join("\n") + "\n";
|
|
1467
|
-
const file = memoryFilePath4(ctx.paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
1468
|
-
await mkdir5(path8.dirname(file), { recursive: true });
|
|
1469
|
-
if (existsSync18(file)) {
|
|
1470
|
-
throw new Error(`Memory already exists at ${file}`);
|
|
1471
|
-
}
|
|
1472
|
-
await writeFile11(file, serializeMemory10({ frontmatter, body }), "utf8");
|
|
1473
|
-
return { id: frontmatter.id, scope: frontmatter.scope, file_path: file };
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
// src/tools/mem-session-end.ts
|
|
1477
|
-
import { writeFile as writeFile13, mkdir as mkdir7 } from "fs/promises";
|
|
1478
|
-
import { existsSync as existsSync20 } from "fs";
|
|
1479
|
-
import path10 from "path";
|
|
1480
|
-
import {
|
|
1481
|
-
buildFrontmatter as buildFrontmatter4,
|
|
1482
|
-
loadMemoriesFromDir as loadMemoriesFromDir15,
|
|
1483
|
-
memoryFilePath as memoryFilePath5,
|
|
1484
|
-
serializeMemory as serializeMemory11
|
|
1485
|
-
} from "@hivelore/core";
|
|
1486
|
-
import { z as z19 } from "zod";
|
|
1487
1550
|
|
|
1488
1551
|
// src/session-tracker.ts
|
|
1489
1552
|
import {
|
|
@@ -1492,12 +1555,12 @@ import {
|
|
|
1492
1555
|
loadConfig as loadConfig2,
|
|
1493
1556
|
writeSessionHandoff
|
|
1494
1557
|
} from "@hivelore/core";
|
|
1495
|
-
import { mkdir as
|
|
1496
|
-
import { existsSync as
|
|
1497
|
-
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";
|
|
1498
1561
|
import { execSync as execSync2 } from "child_process";
|
|
1499
1562
|
function pendingDistillPath(ctx) {
|
|
1500
|
-
return
|
|
1563
|
+
return path8.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
|
|
1501
1564
|
}
|
|
1502
1565
|
var SessionTracker = class {
|
|
1503
1566
|
events = [];
|
|
@@ -1601,7 +1664,7 @@ var SessionTracker = class {
|
|
|
1601
1664
|
(e) => e.tool === "mem_session_end" && !e.summary?.startsWith("Auto-captured")
|
|
1602
1665
|
);
|
|
1603
1666
|
const isSubstantialSession = totalCalls >= 3 || writingTools.length > 0;
|
|
1604
|
-
if (!ranPostTask && isSubstantialSession &&
|
|
1667
|
+
if (!ranPostTask && isSubstantialSession && existsSync18(this.ctx.paths.haiveDir)) {
|
|
1605
1668
|
try {
|
|
1606
1669
|
const memoriesSaved = writingTools.map((e) => e.summary ?? "").filter(Boolean).slice(0, 20);
|
|
1607
1670
|
const payload = {
|
|
@@ -1614,9 +1677,9 @@ var SessionTracker = class {
|
|
|
1614
1677
|
...gitDiff ? { git_diff: gitDiff } : {},
|
|
1615
1678
|
...recapId ? { recap_id: recapId } : {}
|
|
1616
1679
|
};
|
|
1617
|
-
const cacheDir =
|
|
1618
|
-
await
|
|
1619
|
-
await
|
|
1680
|
+
const cacheDir = path8.join(this.ctx.paths.haiveDir, ".cache");
|
|
1681
|
+
await mkdir5(cacheDir, { recursive: true });
|
|
1682
|
+
await writeFile11(
|
|
1620
1683
|
pendingDistillPath(this.ctx),
|
|
1621
1684
|
JSON.stringify(payload, null, 2) + "\n",
|
|
1622
1685
|
"utf8"
|
|
@@ -1635,7 +1698,7 @@ var SessionTracker = class {
|
|
|
1635
1698
|
};
|
|
1636
1699
|
async function clearPendingDistill(ctx) {
|
|
1637
1700
|
const p = pendingDistillPath(ctx);
|
|
1638
|
-
if (
|
|
1701
|
+
if (existsSync18(p)) {
|
|
1639
1702
|
try {
|
|
1640
1703
|
await rm(p);
|
|
1641
1704
|
} catch {
|
|
@@ -1652,15 +1715,15 @@ function summarizeTools(events) {
|
|
|
1652
1715
|
|
|
1653
1716
|
// src/tools/mem-session-end.ts
|
|
1654
1717
|
var MemSessionEndInputSchema = {
|
|
1655
|
-
goal:
|
|
1656
|
-
accomplished:
|
|
1657
|
-
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(
|
|
1658
1721
|
"Any bugs, inconsistencies, surprises, or missing knowledge found during this session. Empty if nothing surprising was found."
|
|
1659
1722
|
),
|
|
1660
|
-
files_touched:
|
|
1661
|
-
next_steps:
|
|
1662
|
-
scope:
|
|
1663
|
-
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)")
|
|
1664
1727
|
};
|
|
1665
1728
|
function recapTopic(scope, module) {
|
|
1666
1729
|
return module ? `session-recap-${scope}-${module}` : `session-recap-${scope}`;
|
|
@@ -1690,23 +1753,23 @@ ${input.next_steps}`);
|
|
|
1690
1753
|
return lines.join("\n");
|
|
1691
1754
|
}
|
|
1692
1755
|
async function memSessionEnd(input, ctx) {
|
|
1693
|
-
if (!
|
|
1756
|
+
if (!existsSync19(ctx.paths.haiveDir)) {
|
|
1694
1757
|
throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'hivelore init' first.`);
|
|
1695
1758
|
}
|
|
1696
1759
|
const body = buildBody(input);
|
|
1697
1760
|
const topic = recapTopic(input.scope, input.module);
|
|
1698
1761
|
const normalizedFiles = input.files_touched.map((p) => {
|
|
1699
|
-
if (!p || !
|
|
1700
|
-
const rel =
|
|
1762
|
+
if (!p || !path9.isAbsolute(p)) return p;
|
|
1763
|
+
const rel = path9.relative(ctx.paths.root, p);
|
|
1701
1764
|
return rel.startsWith("..") ? p : rel;
|
|
1702
1765
|
});
|
|
1703
1766
|
const invalidPaths = normalizedFiles.filter(
|
|
1704
|
-
(p) => !
|
|
1767
|
+
(p) => !existsSync19(path9.resolve(ctx.paths.root, p))
|
|
1705
1768
|
);
|
|
1706
1769
|
if (invalidPaths.length > 0) {
|
|
1707
1770
|
console.warn(`[haive] session end: anchor path(s) not found: ${invalidPaths.join(", ")}`);
|
|
1708
1771
|
}
|
|
1709
|
-
const existing =
|
|
1772
|
+
const existing = existsSync19(ctx.paths.memoriesDir) ? await loadMemoriesFromDir15(ctx.paths.memoriesDir) : [];
|
|
1710
1773
|
const topicMatch = existing.find(
|
|
1711
1774
|
({ memory }) => memory.frontmatter.topic === topic && memory.frontmatter.scope === input.scope && (!input.module || memory.frontmatter.module === input.module)
|
|
1712
1775
|
);
|
|
@@ -1722,9 +1785,9 @@ async function memSessionEnd(input, ctx) {
|
|
|
1722
1785
|
paths: normalizedFiles.length ? normalizedFiles : fm.anchor.paths
|
|
1723
1786
|
}
|
|
1724
1787
|
};
|
|
1725
|
-
await
|
|
1788
|
+
await writeFile12(
|
|
1726
1789
|
topicMatch.filePath,
|
|
1727
|
-
|
|
1790
|
+
serializeMemory10({ frontmatter: newFrontmatter, body }),
|
|
1728
1791
|
"utf8"
|
|
1729
1792
|
);
|
|
1730
1793
|
await clearPendingDistill(ctx);
|
|
@@ -1736,7 +1799,7 @@ async function memSessionEnd(input, ctx) {
|
|
|
1736
1799
|
revision_count: revisionCount
|
|
1737
1800
|
};
|
|
1738
1801
|
}
|
|
1739
|
-
const frontmatter =
|
|
1802
|
+
const frontmatter = buildFrontmatter3({
|
|
1740
1803
|
type: "session_recap",
|
|
1741
1804
|
slug: "recap",
|
|
1742
1805
|
scope: input.scope,
|
|
@@ -1746,14 +1809,14 @@ async function memSessionEnd(input, ctx) {
|
|
|
1746
1809
|
topic,
|
|
1747
1810
|
status: "validated"
|
|
1748
1811
|
});
|
|
1749
|
-
const file =
|
|
1812
|
+
const file = memoryFilePath4(
|
|
1750
1813
|
ctx.paths,
|
|
1751
1814
|
frontmatter.scope,
|
|
1752
1815
|
frontmatter.id,
|
|
1753
1816
|
frontmatter.module
|
|
1754
1817
|
);
|
|
1755
|
-
await
|
|
1756
|
-
await
|
|
1818
|
+
await mkdir6(path9.dirname(file), { recursive: true });
|
|
1819
|
+
await writeFile12(file, serializeMemory10({ frontmatter, body }), "utf8");
|
|
1757
1820
|
await clearPendingDistill(ctx);
|
|
1758
1821
|
return {
|
|
1759
1822
|
id: frontmatter.id,
|
|
@@ -1765,9 +1828,9 @@ async function memSessionEnd(input, ctx) {
|
|
|
1765
1828
|
}
|
|
1766
1829
|
|
|
1767
1830
|
// src/tools/get-briefing.ts
|
|
1768
|
-
import { readFile as readFile6, writeFile as
|
|
1769
|
-
import { existsSync as
|
|
1770
|
-
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";
|
|
1771
1834
|
import {
|
|
1772
1835
|
allocateBudget,
|
|
1773
1836
|
assessBootstrapState,
|
|
@@ -1801,7 +1864,7 @@ import {
|
|
|
1801
1864
|
queryCodeMap,
|
|
1802
1865
|
readSessionHandoff,
|
|
1803
1866
|
resolveBriefingBudget,
|
|
1804
|
-
serializeMemory as
|
|
1867
|
+
serializeMemory as serializeMemory11,
|
|
1805
1868
|
specificityScore,
|
|
1806
1869
|
GUESSABLE_THRESHOLD,
|
|
1807
1870
|
tokenizeQuery as tokenizeQuery2,
|
|
@@ -1809,12 +1872,12 @@ import {
|
|
|
1809
1872
|
truncateToTokens,
|
|
1810
1873
|
writeBriefingMarker
|
|
1811
1874
|
} from "@hivelore/core";
|
|
1812
|
-
import { z as
|
|
1875
|
+
import { z as z19 } from "zod";
|
|
1813
1876
|
|
|
1814
1877
|
// src/tools/briefing-helpers.ts
|
|
1815
1878
|
import { readdir as readdir3, readFile as readFile5 } from "fs/promises";
|
|
1816
|
-
import { existsSync as
|
|
1817
|
-
import
|
|
1879
|
+
import { existsSync as existsSync20 } from "fs";
|
|
1880
|
+
import path10 from "path";
|
|
1818
1881
|
import {
|
|
1819
1882
|
classifyMemoryPriority as coreClassifyPriority,
|
|
1820
1883
|
isGlobPath,
|
|
@@ -1947,15 +2010,15 @@ async function trySemanticHits(ctx, task, limit) {
|
|
|
1947
2010
|
}
|
|
1948
2011
|
async function loadModuleContexts2(ctx, modules) {
|
|
1949
2012
|
if (modules.length === 0) return [];
|
|
1950
|
-
if (!
|
|
2013
|
+
if (!existsSync20(ctx.paths.modulesContextDir)) return [];
|
|
1951
2014
|
const available = new Set(
|
|
1952
2015
|
(await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
|
|
1953
2016
|
);
|
|
1954
2017
|
const out = [];
|
|
1955
2018
|
for (const m of modules) {
|
|
1956
2019
|
if (!available.has(m)) continue;
|
|
1957
|
-
const file =
|
|
1958
|
-
if (
|
|
2020
|
+
const file = path10.join(ctx.paths.modulesContextDir, m, "context.md");
|
|
2021
|
+
if (existsSync20(file)) {
|
|
1959
2022
|
out.push({ name: m, content: await readFile5(file, "utf8") });
|
|
1960
2023
|
}
|
|
1961
2024
|
}
|
|
@@ -1964,38 +2027,38 @@ async function loadModuleContexts2(ctx, modules) {
|
|
|
1964
2027
|
|
|
1965
2028
|
// src/tools/get-briefing.ts
|
|
1966
2029
|
var GetBriefingInputSchema = {
|
|
1967
|
-
task:
|
|
2030
|
+
task: z19.string().optional().describe(
|
|
1968
2031
|
"What you are about to do, in 1\u20132 sentences. Used to rank relevant memories semantically."
|
|
1969
2032
|
),
|
|
1970
|
-
files:
|
|
1971
|
-
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(
|
|
1972
2035
|
"Approximate token budget for the entire briefing. Each section is allocated a share and truncated to fit."
|
|
1973
2036
|
),
|
|
1974
|
-
max_memories:
|
|
1975
|
-
include_project_context:
|
|
1976
|
-
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(
|
|
1977
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."
|
|
1978
2041
|
),
|
|
1979
|
-
include_module_contexts:
|
|
1980
|
-
semantic:
|
|
2042
|
+
include_module_contexts: z19.boolean().default(true),
|
|
2043
|
+
semantic: z19.boolean().default(true).describe(
|
|
1981
2044
|
"Use semantic ranking when a task is provided (requires `hivelore embeddings index`)."
|
|
1982
2045
|
),
|
|
1983
|
-
include_stale:
|
|
1984
|
-
track:
|
|
1985
|
-
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(
|
|
1986
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."
|
|
1987
2050
|
),
|
|
1988
|
-
symbols:
|
|
2051
|
+
symbols: z19.array(z19.string()).default([]).describe(
|
|
1989
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."
|
|
1990
2053
|
),
|
|
1991
|
-
min_semantic_score:
|
|
2054
|
+
min_semantic_score: z19.number().min(0).max(1).default(0).describe(
|
|
1992
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."
|
|
1993
2056
|
),
|
|
1994
|
-
budget_preset:
|
|
2057
|
+
budget_preset: z19.enum(["quick", "balanced", "deep"]).optional().describe(
|
|
1995
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."
|
|
1996
2059
|
)
|
|
1997
2060
|
};
|
|
1998
|
-
var GetBriefingZod =
|
|
2061
|
+
var GetBriefingZod = z19.object(GetBriefingInputSchema);
|
|
1999
2062
|
async function getBriefing(input, ctx) {
|
|
2000
2063
|
const resolvedBudget = resolveBriefingBudget(input.budget_preset, {
|
|
2001
2064
|
max_tokens: input.max_tokens,
|
|
@@ -2011,7 +2074,7 @@ async function getBriefing(input, ctx) {
|
|
|
2011
2074
|
let usage = { version: 1, updated_at: "", by_id: {} };
|
|
2012
2075
|
let byId = /* @__PURE__ */ new Map();
|
|
2013
2076
|
let lastSession;
|
|
2014
|
-
if (
|
|
2077
|
+
if (existsSync21(ctx.paths.memoriesDir)) {
|
|
2015
2078
|
const allLoaded = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
|
|
2016
2079
|
const recaps = allLoaded.filter(({ memory }) => memory.frontmatter.type === "session_recap").sort(
|
|
2017
2080
|
(a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
|
|
@@ -2186,7 +2249,7 @@ async function getBriefing(input, ctx) {
|
|
|
2186
2249
|
if (!isAutoPromoteEligible(loaded.memory.frontmatter, u, rule)) continue;
|
|
2187
2250
|
const newFm = { ...loaded.memory.frontmatter, status: "validated", validated_by: "auto" };
|
|
2188
2251
|
try {
|
|
2189
|
-
await
|
|
2252
|
+
await writeFile13(loaded.filePath, serializeMemory11({ frontmatter: newFm, body: loaded.memory.body }), "utf8");
|
|
2190
2253
|
m.status = "validated";
|
|
2191
2254
|
m.confidence = "trusted";
|
|
2192
2255
|
} catch {
|
|
@@ -2194,7 +2257,7 @@ async function getBriefing(input, ctx) {
|
|
|
2194
2257
|
}
|
|
2195
2258
|
}
|
|
2196
2259
|
}
|
|
2197
|
-
let projectContextRaw = input.include_project_context &&
|
|
2260
|
+
let projectContextRaw = input.include_project_context && existsSync21(ctx.paths.projectContext) ? await readFile6(ctx.paths.projectContext, "utf8") : "";
|
|
2198
2261
|
let contextOmittedRecent = false;
|
|
2199
2262
|
if (projectContextRaw && input.dedupe_project_context !== false) {
|
|
2200
2263
|
const ctxHash = hashProjectContext(projectContextRaw);
|
|
@@ -2209,7 +2272,7 @@ async function getBriefing(input, ctx) {
|
|
|
2209
2272
|
const setupWarnings = [];
|
|
2210
2273
|
let autoContextGenerated = false;
|
|
2211
2274
|
let projectContext = isTemplateContext ? "" : projectContextRaw;
|
|
2212
|
-
if ((isTemplateContext || !
|
|
2275
|
+
if ((isTemplateContext || !existsSync21(ctx.paths.projectContext)) && input.include_project_context) {
|
|
2213
2276
|
const haiveConfig = await loadConfig3(ctx.paths);
|
|
2214
2277
|
if (haiveConfig.autoContext) {
|
|
2215
2278
|
const codeMap = await loadCodeMap(ctx.paths);
|
|
@@ -2315,11 +2378,20 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
2315
2378
|
if (isDecaying(u, createdAt)) decayWarnings.push(m.id);
|
|
2316
2379
|
}
|
|
2317
2380
|
const formattedMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : input.format === "actions" ? trimmedMemories.map((m) => ({ ...m, body: extractActionsBriefBody(m.body) })) : trimmedMemories;
|
|
2318
|
-
|
|
2381
|
+
let outputMemories = formattedMemories.map((m) => ({
|
|
2319
2382
|
...m,
|
|
2320
2383
|
priority: classifyMemoryPriority(m, byId.get(m.id), input.files, input.symbols),
|
|
2321
2384
|
why: explainWhySurfaced(m, byId.get(m.id), input.files, inferred)
|
|
2322
2385
|
}));
|
|
2386
|
+
if (input.format === "full") {
|
|
2387
|
+
const hasDirectHits = outputMemories.some((m) => m.priority === "must_read" || m.priority === "useful");
|
|
2388
|
+
if (hasDirectHits) {
|
|
2389
|
+
outputMemories = outputMemories.map(
|
|
2390
|
+
(m) => m.priority === "background" ? { ...m, body: `${compactSummary(m.body)}
|
|
2391
|
+
(background \u2014 full body: mem_get("${m.id}"))` } : m
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2323
2395
|
const briefingQuality = classifyBriefingQuality(outputMemories, {
|
|
2324
2396
|
isTemplateContext,
|
|
2325
2397
|
autoContextGenerated,
|
|
@@ -2374,7 +2446,7 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
2374
2446
|
actionRequired.push(extractActionItem(m.id, loaded.memory.body));
|
|
2375
2447
|
}
|
|
2376
2448
|
}
|
|
2377
|
-
if (
|
|
2449
|
+
if (existsSync21(ctx.paths.memoriesDir)) {
|
|
2378
2450
|
const allMems = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
|
|
2379
2451
|
for (const { memory } of allMems) {
|
|
2380
2452
|
const fm = memory.frontmatter;
|
|
@@ -2385,7 +2457,7 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
2385
2457
|
}
|
|
2386
2458
|
}
|
|
2387
2459
|
const pendingDistillFile = pendingDistillPath(ctx);
|
|
2388
|
-
if (
|
|
2460
|
+
if (existsSync21(pendingDistillFile)) {
|
|
2389
2461
|
try {
|
|
2390
2462
|
const raw = await readFile6(pendingDistillFile, "utf8");
|
|
2391
2463
|
const pd = JSON.parse(raw);
|
|
@@ -2414,7 +2486,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
2414
2486
|
}
|
|
2415
2487
|
}
|
|
2416
2488
|
const memoriesEmpty = outputMemories.length === 0;
|
|
2417
|
-
const hasMemoriesDir =
|
|
2489
|
+
const hasMemoriesDir = existsSync21(ctx.paths.memoriesDir);
|
|
2418
2490
|
const isColdStart = isTemplateContext && memoriesEmpty && !lastSession && !autoContextGenerated;
|
|
2419
2491
|
const hasUnguessableSignal = outputMemories.some(
|
|
2420
2492
|
(m) => (m.priority === "must_read" || m.priority === "useful") && specificityScore(m.body) >= GUESSABLE_THRESHOLD
|
|
@@ -2432,7 +2504,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
2432
2504
|
pcRaw = await readFile6(ctx.paths.projectContext, "utf8");
|
|
2433
2505
|
} catch {
|
|
2434
2506
|
}
|
|
2435
|
-
const allForBootstrap =
|
|
2507
|
+
const allForBootstrap = existsSync21(ctx.paths.memoriesDir) ? await loadMemoriesFromDir16(ctx.paths.memoriesDir) : [];
|
|
2436
2508
|
const cmForBootstrap = await loadCodeMap(ctx.paths);
|
|
2437
2509
|
let existingModules = [];
|
|
2438
2510
|
try {
|
|
@@ -2496,7 +2568,7 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
|
|
|
2496
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."
|
|
2497
2569
|
);
|
|
2498
2570
|
}
|
|
2499
|
-
if (outputMemories.length > 0 &&
|
|
2571
|
+
if (outputMemories.length > 0 && existsSync21(ctx.paths.haiveDir)) {
|
|
2500
2572
|
const proof = briefingProofLine(await loadPreventionEvents(ctx.paths));
|
|
2501
2573
|
if (proof) hints.push(proof);
|
|
2502
2574
|
}
|
|
@@ -2510,7 +2582,7 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
|
|
|
2510
2582
|
adaptiveTrim
|
|
2511
2583
|
});
|
|
2512
2584
|
const breadcrumbTokens = breadcrumbs ? estimateTokens([...breadcrumbs.start_here, ...breadcrumbs.drill_down, breadcrumbs.note ?? ""].join("\n")) : 0;
|
|
2513
|
-
if (
|
|
2585
|
+
if (existsSync21(ctx.paths.haiveDir)) {
|
|
2514
2586
|
await writeBriefingMarker(ctx.paths, {
|
|
2515
2587
|
sessionId: process.env.HAIVE_SESSION_ID,
|
|
2516
2588
|
...input.task ? { task: input.task } : {},
|
|
@@ -2566,8 +2638,8 @@ Invoke the \`bootstrap_repo\` MCP prompt, or close these gaps directly:
|
|
|
2566
2638
|
};
|
|
2567
2639
|
}
|
|
2568
2640
|
async function detectRunCommands(root) {
|
|
2569
|
-
const pkgPath =
|
|
2570
|
-
if (!
|
|
2641
|
+
const pkgPath = path11.join(root, "package.json");
|
|
2642
|
+
if (!existsSync21(pkgPath)) return null;
|
|
2571
2643
|
try {
|
|
2572
2644
|
const pkg = JSON.parse(await readFile6(pkgPath, "utf8"));
|
|
2573
2645
|
const scripts = pkg.scripts ?? {};
|
|
@@ -2634,24 +2706,24 @@ function oneLine(value) {
|
|
|
2634
2706
|
return value.replace(/\s+/g, " ").replace(/"/g, '\\"').trim().slice(0, 120);
|
|
2635
2707
|
}
|
|
2636
2708
|
function serverVersion() {
|
|
2637
|
-
return true ? "0.
|
|
2709
|
+
return true ? "0.33.0" : "dev";
|
|
2638
2710
|
}
|
|
2639
2711
|
|
|
2640
2712
|
// src/tools/code-map.ts
|
|
2641
2713
|
import { estimateTokens as estimateTokens2, loadCodeMap as loadCodeMap2, queryCodeMap as queryCodeMap2 } from "@hivelore/core";
|
|
2642
|
-
import { z as
|
|
2714
|
+
import { z as z20 } from "zod";
|
|
2643
2715
|
var CodeMapInputSchema = {
|
|
2644
|
-
file:
|
|
2645
|
-
symbol:
|
|
2646
|
-
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(
|
|
2647
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."
|
|
2648
2720
|
),
|
|
2649
|
-
max_files:
|
|
2650
|
-
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(
|
|
2651
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)."
|
|
2652
2724
|
)
|
|
2653
2725
|
};
|
|
2654
|
-
var CodeMapInputZod =
|
|
2726
|
+
var CodeMapInputZod = z20.object(CodeMapInputSchema);
|
|
2655
2727
|
async function codeMapTool(input, ctx) {
|
|
2656
2728
|
const map = await loadCodeMap2(ctx.paths);
|
|
2657
2729
|
if (!map) {
|
|
@@ -2720,15 +2792,15 @@ function estimateFileEntryTokens(f) {
|
|
|
2720
2792
|
}
|
|
2721
2793
|
|
|
2722
2794
|
// src/tools/mem-diff.ts
|
|
2723
|
-
import { existsSync as
|
|
2795
|
+
import { existsSync as existsSync22 } from "fs";
|
|
2724
2796
|
import { loadMemoriesFromDir as loadMemoriesFromDir17 } from "@hivelore/core";
|
|
2725
|
-
import { z as
|
|
2797
|
+
import { z as z21 } from "zod";
|
|
2726
2798
|
var MemDiffInputSchema = {
|
|
2727
|
-
id_a:
|
|
2728
|
-
id_b:
|
|
2799
|
+
id_a: z21.string().min(1).describe("First memory id"),
|
|
2800
|
+
id_b: z21.string().min(1).describe("Second memory id")
|
|
2729
2801
|
};
|
|
2730
2802
|
async function memDiff(input, ctx) {
|
|
2731
|
-
if (!
|
|
2803
|
+
if (!existsSync22(ctx.paths.memoriesDir)) {
|
|
2732
2804
|
throw new Error(`No .ai/memories at ${ctx.paths.root}.`);
|
|
2733
2805
|
}
|
|
2734
2806
|
const all = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
|
|
@@ -2765,16 +2837,16 @@ async function memDiff(input, ctx) {
|
|
|
2765
2837
|
}
|
|
2766
2838
|
|
|
2767
2839
|
// src/tools/get-recap.ts
|
|
2768
|
-
import { existsSync as
|
|
2840
|
+
import { existsSync as existsSync23 } from "fs";
|
|
2769
2841
|
import { loadMemoriesFromDir as loadMemoriesFromDir18 } from "@hivelore/core";
|
|
2770
|
-
import { z as
|
|
2842
|
+
import { z as z22 } from "zod";
|
|
2771
2843
|
var GetRecapInputSchema = {
|
|
2772
|
-
scope:
|
|
2844
|
+
scope: z22.enum(["personal", "team", "any"]).default("any").describe(
|
|
2773
2845
|
"Limit to a specific scope's recap. Default 'any' returns the most recent recap across both personal and team scopes."
|
|
2774
2846
|
)
|
|
2775
2847
|
};
|
|
2776
2848
|
async function getRecap(input, ctx) {
|
|
2777
|
-
if (!
|
|
2849
|
+
if (!existsSync23(ctx.paths.memoriesDir)) {
|
|
2778
2850
|
return { recap: null, notice: "No .ai/memories directory \u2014 haive not initialized here." };
|
|
2779
2851
|
}
|
|
2780
2852
|
const all = await loadMemoriesFromDir18(ctx.paths.memoriesDir);
|
|
@@ -2801,13 +2873,13 @@ async function getRecap(input, ctx) {
|
|
|
2801
2873
|
}
|
|
2802
2874
|
|
|
2803
2875
|
// src/tools/mem-relevant-to.ts
|
|
2804
|
-
import { z as
|
|
2876
|
+
import { z as z23 } from "zod";
|
|
2805
2877
|
var MemRelevantToInputSchema = {
|
|
2806
|
-
task:
|
|
2807
|
-
files:
|
|
2808
|
-
limit:
|
|
2809
|
-
min_semantic_score:
|
|
2810
|
-
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.")
|
|
2811
2883
|
};
|
|
2812
2884
|
async function memRelevantTo(input, ctx) {
|
|
2813
2885
|
const briefingInput = {
|
|
@@ -2837,14 +2909,14 @@ async function memRelevantTo(input, ctx) {
|
|
|
2837
2909
|
}
|
|
2838
2910
|
|
|
2839
2911
|
// src/tools/code-search.ts
|
|
2840
|
-
import { z as
|
|
2912
|
+
import { z as z24 } from "zod";
|
|
2841
2913
|
import { loadCodeMap as loadCodeMap3 } from "@hivelore/core";
|
|
2842
2914
|
var CodeSearchInputSchema = {
|
|
2843
|
-
query:
|
|
2915
|
+
query: z24.string().min(1).describe(
|
|
2844
2916
|
"Natural-language description of what you are looking for in the codebase (e.g. 'function that hashes passwords', 'JWT signing logic', 'route registration')."
|
|
2845
2917
|
),
|
|
2846
|
-
k:
|
|
2847
|
-
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(
|
|
2848
2920
|
"Minimum cosine similarity. Hits below this threshold are dropped to avoid noise. Try 0.3+ for stricter matching."
|
|
2849
2921
|
)
|
|
2850
2922
|
};
|
|
@@ -2886,155 +2958,39 @@ async function codeSearch(input, ctx) {
|
|
|
2886
2958
|
};
|
|
2887
2959
|
}
|
|
2888
2960
|
|
|
2889
|
-
// src/tools/why-this-file.ts
|
|
2890
|
-
import { existsSync as existsSync25 } from "fs";
|
|
2891
|
-
import { spawn } from "child_process";
|
|
2892
|
-
import path13 from "path";
|
|
2893
|
-
import {
|
|
2894
|
-
deriveConfidence as deriveConfidence5,
|
|
2895
|
-
getUsage as getUsage7,
|
|
2896
|
-
loadCodeMap as loadCodeMap4,
|
|
2897
|
-
loadMemoriesFromDir as loadMemoriesFromDir19,
|
|
2898
|
-
loadUsageIndex as loadUsageIndex9,
|
|
2899
|
-
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths3
|
|
2900
|
-
} from "@hivelore/core";
|
|
2901
|
-
import { z as z26 } from "zod";
|
|
2902
|
-
var WhyThisFileInputSchema = {
|
|
2903
|
-
path: z26.string().min(1).describe(
|
|
2904
|
-
"Project-relative path to the file you want context on (e.g. 'packages/mcp/src/tools/mem-save.ts')."
|
|
2905
|
-
),
|
|
2906
|
-
git_log_limit: z26.number().int().positive().max(20).default(5).describe("How many recent commits touching this file to include."),
|
|
2907
|
-
memory_limit: z26.number().int().positive().max(20).default(5).describe("Cap on memories anchored to this path.")
|
|
2908
|
-
};
|
|
2909
|
-
async function whyThisFile(input, ctx) {
|
|
2910
|
-
const fileExists = existsSync25(path13.join(ctx.paths.root, input.path));
|
|
2911
|
-
const [commits, memories, codeMap] = await Promise.all([
|
|
2912
|
-
runGitLog(ctx.paths.root, input.path, input.git_log_limit).catch(() => []),
|
|
2913
|
-
collectAnchoredMemories(ctx, input.path, input.memory_limit),
|
|
2914
|
-
loadCodeMap4(ctx.paths)
|
|
2915
|
-
]);
|
|
2916
|
-
const codeMapEntry = codeMap?.files[input.path];
|
|
2917
|
-
const hints = [];
|
|
2918
|
-
if (!fileExists) {
|
|
2919
|
-
hints.push(`File '${input.path}' does not exist on disk \u2014 path may be wrong or file removed.`);
|
|
2920
|
-
}
|
|
2921
|
-
if (commits.length === 0 && fileExists) {
|
|
2922
|
-
hints.push("No git history found \u2014 file may be untracked or git not initialized.");
|
|
2923
|
-
}
|
|
2924
|
-
if (memories.length === 0 && fileExists) {
|
|
2925
|
-
hints.push(
|
|
2926
|
-
"No memories anchored here. If you discover something non-obvious while editing, use mem_observe (with where=" + input.path + ") to capture it."
|
|
2927
|
-
);
|
|
2928
|
-
}
|
|
2929
|
-
if (memories.some((m) => m.type === "attempt" || m.type === "gotcha")) {
|
|
2930
|
-
hints.push("\u26A0\uFE0F attempt/gotcha memories anchored to this file \u2014 read them BEFORE editing.");
|
|
2931
|
-
}
|
|
2932
|
-
return {
|
|
2933
|
-
file: input.path,
|
|
2934
|
-
exists: fileExists,
|
|
2935
|
-
recent_commits: commits,
|
|
2936
|
-
memories,
|
|
2937
|
-
code_map_entry: codeMapEntry ? {
|
|
2938
|
-
...codeMapEntry.summary ? { summary: codeMapEntry.summary } : {},
|
|
2939
|
-
loc: codeMapEntry.loc,
|
|
2940
|
-
exports: codeMapEntry.exports.map((e) => ({
|
|
2941
|
-
name: e.name,
|
|
2942
|
-
kind: e.kind,
|
|
2943
|
-
line: e.line,
|
|
2944
|
-
...e.description ? { description: e.description } : {}
|
|
2945
|
-
}))
|
|
2946
|
-
} : null,
|
|
2947
|
-
...hints.length > 0 ? { hints } : {}
|
|
2948
|
-
};
|
|
2949
|
-
}
|
|
2950
|
-
async function collectAnchoredMemories(ctx, filePath, limit) {
|
|
2951
|
-
if (!existsSync25(ctx.paths.memoriesDir)) return [];
|
|
2952
|
-
const all = await loadMemoriesFromDir19(ctx.paths.memoriesDir);
|
|
2953
|
-
const usage = await loadUsageIndex9(ctx.paths);
|
|
2954
|
-
const out = [];
|
|
2955
|
-
for (const { memory } of all) {
|
|
2956
|
-
const fm = memory.frontmatter;
|
|
2957
|
-
if (fm.status === "rejected" || fm.status === "deprecated") continue;
|
|
2958
|
-
if (fm.type === "session_recap") continue;
|
|
2959
|
-
if (!memoryMatchesAnchorPaths3(memory, [filePath])) continue;
|
|
2960
|
-
const u = getUsage7(usage, fm.id);
|
|
2961
|
-
out.push({
|
|
2962
|
-
id: fm.id,
|
|
2963
|
-
type: fm.type,
|
|
2964
|
-
scope: fm.scope,
|
|
2965
|
-
confidence: deriveConfidence5(fm, u),
|
|
2966
|
-
body_preview: memory.body.split("\n").slice(0, 6).join("\n")
|
|
2967
|
-
});
|
|
2968
|
-
if (out.length >= limit) break;
|
|
2969
|
-
}
|
|
2970
|
-
return out;
|
|
2971
|
-
}
|
|
2972
|
-
async function runGitLog(cwd, filePath, limit) {
|
|
2973
|
-
const sep = "<<HV>>";
|
|
2974
|
-
const fmt = `%h${sep}%an${sep}%ar${sep}%s`;
|
|
2975
|
-
const output = await runCommand(
|
|
2976
|
-
"git",
|
|
2977
|
-
["log", `-n`, String(limit), `--pretty=format:${fmt}`, "--", filePath],
|
|
2978
|
-
cwd
|
|
2979
|
-
);
|
|
2980
|
-
if (!output.trim()) return [];
|
|
2981
|
-
return output.split("\n").map((line) => {
|
|
2982
|
-
const [sha = "", author = "", relative_date = "", subject = ""] = line.split(sep);
|
|
2983
|
-
return { sha, author, relative_date, subject };
|
|
2984
|
-
}).filter((c) => c.sha);
|
|
2985
|
-
}
|
|
2986
|
-
function runCommand(cmd, args, cwd) {
|
|
2987
|
-
return new Promise((resolve, reject) => {
|
|
2988
|
-
const proc = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
2989
|
-
let stdout = "";
|
|
2990
|
-
let stderr = "";
|
|
2991
|
-
proc.stdout.on("data", (chunk) => {
|
|
2992
|
-
stdout += chunk.toString();
|
|
2993
|
-
});
|
|
2994
|
-
proc.stderr.on("data", (chunk) => {
|
|
2995
|
-
stderr += chunk.toString();
|
|
2996
|
-
});
|
|
2997
|
-
proc.on("error", reject);
|
|
2998
|
-
proc.on("close", (code) => {
|
|
2999
|
-
if (code === 0) resolve(stdout);
|
|
3000
|
-
else reject(new Error(stderr || `${cmd} exited with code ${code}`));
|
|
3001
|
-
});
|
|
3002
|
-
});
|
|
3003
|
-
}
|
|
3004
|
-
|
|
3005
2961
|
// src/tools/anti-patterns-check.ts
|
|
3006
|
-
import { existsSync as
|
|
2962
|
+
import { existsSync as existsSync24 } from "fs";
|
|
3007
2963
|
import {
|
|
3008
2964
|
addedLinesFromDiff,
|
|
3009
2965
|
BRIDGE_TARGET_PATH,
|
|
3010
2966
|
buildDocFrequency,
|
|
3011
2967
|
CODE_STOPWORDS,
|
|
3012
|
-
deriveConfidence as
|
|
2968
|
+
deriveConfidence as deriveConfidence5,
|
|
3013
2969
|
diffHasDistinctiveOverlap,
|
|
3014
|
-
getUsage as
|
|
2970
|
+
getUsage as getUsage7,
|
|
3015
2971
|
isRetiredMemory as isRetiredMemory2,
|
|
3016
|
-
loadMemoriesFromDir as
|
|
3017
|
-
loadUsageIndex as
|
|
2972
|
+
loadMemoriesFromDir as loadMemoriesFromDir19,
|
|
2973
|
+
loadUsageIndex as loadUsageIndex9,
|
|
3018
2974
|
literalMatchesAnyToken as literalMatchesAnyToken3,
|
|
3019
|
-
memoryMatchesAnchorPaths as
|
|
2975
|
+
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths3,
|
|
3020
2976
|
recordPreventionHits,
|
|
3021
2977
|
runSensors,
|
|
3022
2978
|
sensorTargetsFromDiff,
|
|
3023
2979
|
tokenizeQuery as tokenizeQuery3
|
|
3024
2980
|
} from "@hivelore/core";
|
|
3025
|
-
import { z as
|
|
2981
|
+
import { z as z25 } from "zod";
|
|
3026
2982
|
var AntiPatternsCheckInputSchema = {
|
|
3027
|
-
diff:
|
|
2983
|
+
diff: z25.string().optional().describe(
|
|
3028
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."
|
|
3029
2985
|
),
|
|
3030
|
-
paths:
|
|
2986
|
+
paths: z25.array(z25.string()).default([]).describe(
|
|
3031
2987
|
"File paths affected by the change. Memories anchored to any of these paths are surfaced regardless of the diff content."
|
|
3032
2988
|
),
|
|
3033
|
-
limit:
|
|
3034
|
-
semantic:
|
|
2989
|
+
limit: z25.number().int().positive().max(20).default(8).describe("Cap on returned warnings."),
|
|
2990
|
+
semantic: z25.boolean().default(true).describe(
|
|
3035
2991
|
"When true, also use semantic search (requires @hivelore/embeddings + memory index) to find related anti-patterns."
|
|
3036
2992
|
),
|
|
3037
|
-
min_semantic_score:
|
|
2993
|
+
min_semantic_score: z25.number().min(0).max(1).default(0.45).describe(
|
|
3038
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."
|
|
3039
2995
|
)
|
|
3040
2996
|
};
|
|
@@ -3118,10 +3074,10 @@ async function antiPatternsCheck(input, ctx) {
|
|
|
3118
3074
|
notice: "Nothing to check \u2014 provide either `diff` text or `paths`."
|
|
3119
3075
|
};
|
|
3120
3076
|
}
|
|
3121
|
-
if (!
|
|
3077
|
+
if (!existsSync24(ctx.paths.memoriesDir)) {
|
|
3122
3078
|
return { scanned: 0, warnings: [], notice: "No .ai/memories directory \u2014 nothing to check against." };
|
|
3123
3079
|
}
|
|
3124
|
-
const all = await
|
|
3080
|
+
const all = await loadMemoriesFromDir19(ctx.paths.memoriesDir);
|
|
3125
3081
|
const minSemanticScore = input.min_semantic_score ?? 0.45;
|
|
3126
3082
|
const negative = all.filter(({ memory }) => {
|
|
3127
3083
|
const t = memory.frontmatter.type;
|
|
@@ -3132,7 +3088,7 @@ async function antiPatternsCheck(input, ctx) {
|
|
|
3132
3088
|
if (negative.length === 0) {
|
|
3133
3089
|
return { scanned: 0, warnings: [], notice: "No attempt/gotcha memories found yet." };
|
|
3134
3090
|
}
|
|
3135
|
-
const usage = await
|
|
3091
|
+
const usage = await loadUsageIndex9(ctx.paths);
|
|
3136
3092
|
const docFreq = buildDocFrequency(negative.map(({ memory }) => memory.body));
|
|
3137
3093
|
const seen = /* @__PURE__ */ new Map();
|
|
3138
3094
|
const upsert = (fm, body, reason, score) => {
|
|
@@ -3144,12 +3100,12 @@ async function antiPatternsCheck(input, ctx) {
|
|
|
3144
3100
|
}
|
|
3145
3101
|
return;
|
|
3146
3102
|
}
|
|
3147
|
-
const u =
|
|
3103
|
+
const u = getUsage7(usage, fm.id);
|
|
3148
3104
|
seen.set(fm.id, {
|
|
3149
3105
|
id: fm.id,
|
|
3150
3106
|
type: fm.type,
|
|
3151
3107
|
scope: fm.scope,
|
|
3152
|
-
confidence:
|
|
3108
|
+
confidence: deriveConfidence5(fm, u),
|
|
3153
3109
|
body_preview: body.split("\n").slice(0, 5).join("\n").slice(0, 400),
|
|
3154
3110
|
reasons: [reason],
|
|
3155
3111
|
tags: fm.tags ?? [],
|
|
@@ -3160,7 +3116,7 @@ async function antiPatternsCheck(input, ctx) {
|
|
|
3160
3116
|
};
|
|
3161
3117
|
if (input.paths.length > 0) {
|
|
3162
3118
|
for (const { memory } of negative) {
|
|
3163
|
-
if (
|
|
3119
|
+
if (memoryMatchesAnchorPaths3(memory, input.paths)) {
|
|
3164
3120
|
upsert(memory.frontmatter, memory.body, "anchor");
|
|
3165
3121
|
}
|
|
3166
3122
|
}
|
|
@@ -3237,19 +3193,19 @@ async function antiPatternsCheck(input, ctx) {
|
|
|
3237
3193
|
}
|
|
3238
3194
|
|
|
3239
3195
|
// src/tools/mem-distill.ts
|
|
3240
|
-
import { existsSync as
|
|
3196
|
+
import { existsSync as existsSync25 } from "fs";
|
|
3241
3197
|
import {
|
|
3242
|
-
loadMemoriesFromDir as
|
|
3198
|
+
loadMemoriesFromDir as loadMemoriesFromDir20,
|
|
3243
3199
|
tokenizeQuery as tokenizeQuery4
|
|
3244
3200
|
} from "@hivelore/core";
|
|
3245
|
-
import { z as
|
|
3201
|
+
import { z as z26 } from "zod";
|
|
3246
3202
|
var MemDistillInputSchema = {
|
|
3247
|
-
since_days:
|
|
3248
|
-
min_cluster:
|
|
3249
|
-
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(
|
|
3250
3206
|
"Memory type to scan. 'gotcha' targets observe-style discoveries that recur, 'attempt' surfaces failed approaches that repeat, 'all' considers both."
|
|
3251
3207
|
),
|
|
3252
|
-
scope:
|
|
3208
|
+
scope: z26.enum(["personal", "team", "module", "any"]).default("any").describe("Restrict to a specific scope.")
|
|
3253
3209
|
};
|
|
3254
3210
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
3255
3211
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
@@ -3289,11 +3245,11 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
3289
3245
|
"error"
|
|
3290
3246
|
]);
|
|
3291
3247
|
async function memDistill(input, ctx) {
|
|
3292
|
-
if (!
|
|
3248
|
+
if (!existsSync25(ctx.paths.memoriesDir)) {
|
|
3293
3249
|
return { scanned: 0, singletons: 0, clusters: [], notice: "No .ai/memories directory." };
|
|
3294
3250
|
}
|
|
3295
3251
|
const cutoff = Date.now() - input.since_days * MS_PER_DAY;
|
|
3296
|
-
const all = await
|
|
3252
|
+
const all = await loadMemoriesFromDir20(ctx.paths.memoriesDir);
|
|
3297
3253
|
const candidates = all.filter(({ memory }) => {
|
|
3298
3254
|
const fm = memory.frontmatter;
|
|
3299
3255
|
if (fm.status === "rejected" || fm.status === "deprecated" || fm.status === "stale") return false;
|
|
@@ -3396,299 +3352,19 @@ function firstHeading(body) {
|
|
|
3396
3352
|
return void 0;
|
|
3397
3353
|
}
|
|
3398
3354
|
|
|
3399
|
-
// src/tools/why-this-decision.ts
|
|
3400
|
-
import { existsSync as existsSync28 } from "fs";
|
|
3401
|
-
import { spawn as spawn2 } from "child_process";
|
|
3402
|
-
import {
|
|
3403
|
-
deriveConfidence as deriveConfidence7,
|
|
3404
|
-
getUsage as getUsage9,
|
|
3405
|
-
loadMemoriesFromDir as loadMemoriesFromDir22,
|
|
3406
|
-
loadUsageIndex as loadUsageIndex11,
|
|
3407
|
-
pathsOverlap as singlePathsOverlap
|
|
3408
|
-
} from "@hivelore/core";
|
|
3409
|
-
import { z as z29 } from "zod";
|
|
3410
|
-
var WhyThisDecisionInputSchema = {
|
|
3411
|
-
id: z29.string().min(1).describe("Memory id to inspect (e.g. '2026-04-25-decision-esm-only')."),
|
|
3412
|
-
git_log_limit: z29.number().int().positive().max(20).default(5).describe("How many recent commits per anchor path to surface.")
|
|
3413
|
-
};
|
|
3414
|
-
async function whyThisDecision(input, ctx) {
|
|
3415
|
-
if (!existsSync28(ctx.paths.memoriesDir)) {
|
|
3416
|
-
return {
|
|
3417
|
-
found: false,
|
|
3418
|
-
related: [],
|
|
3419
|
-
path_neighbors: [],
|
|
3420
|
-
recent_commits: [],
|
|
3421
|
-
notice: "No .ai/memories directory."
|
|
3422
|
-
};
|
|
3423
|
-
}
|
|
3424
|
-
const all = await loadMemoriesFromDir22(ctx.paths.memoriesDir);
|
|
3425
|
-
const usage = await loadUsageIndex11(ctx.paths);
|
|
3426
|
-
const target = all.find(({ memory }) => memory.frontmatter.id === input.id);
|
|
3427
|
-
if (!target) {
|
|
3428
|
-
return {
|
|
3429
|
-
found: false,
|
|
3430
|
-
related: [],
|
|
3431
|
-
path_neighbors: [],
|
|
3432
|
-
recent_commits: [],
|
|
3433
|
-
notice: `Memory '${input.id}' not found.`
|
|
3434
|
-
};
|
|
3435
|
-
}
|
|
3436
|
-
const fm = target.memory.frontmatter;
|
|
3437
|
-
const targetUsage = getUsage9(usage, fm.id);
|
|
3438
|
-
const decision = {
|
|
3439
|
-
id: fm.id,
|
|
3440
|
-
type: fm.type,
|
|
3441
|
-
scope: fm.scope,
|
|
3442
|
-
status: fm.status,
|
|
3443
|
-
confidence: deriveConfidence7(fm, targetUsage),
|
|
3444
|
-
body: target.memory.body,
|
|
3445
|
-
created_at: fm.created_at
|
|
3446
|
-
};
|
|
3447
|
-
const relatedSet = new Set(fm.related_ids ?? []);
|
|
3448
|
-
const related = [];
|
|
3449
|
-
for (const { memory } of all) {
|
|
3450
|
-
if (memory.frontmatter.id === fm.id) continue;
|
|
3451
|
-
const isExplicit = relatedSet.has(memory.frontmatter.id);
|
|
3452
|
-
const isBackLink = (memory.frontmatter.related_ids ?? []).includes(fm.id);
|
|
3453
|
-
if (!isExplicit && !isBackLink) continue;
|
|
3454
|
-
const u = getUsage9(usage, memory.frontmatter.id);
|
|
3455
|
-
related.push({
|
|
3456
|
-
id: memory.frontmatter.id,
|
|
3457
|
-
type: memory.frontmatter.type,
|
|
3458
|
-
scope: memory.frontmatter.scope,
|
|
3459
|
-
confidence: deriveConfidence7(memory.frontmatter, u),
|
|
3460
|
-
body_preview: memory.body.split("\n").slice(0, 4).join("\n").slice(0, 300),
|
|
3461
|
-
relation: isExplicit ? "explicit" : "back-link"
|
|
3462
|
-
});
|
|
3463
|
-
}
|
|
3464
|
-
const targetPaths = fm.anchor.paths;
|
|
3465
|
-
const path_neighbors = [];
|
|
3466
|
-
if (targetPaths.length > 0) {
|
|
3467
|
-
for (const { memory } of all) {
|
|
3468
|
-
if (memory.frontmatter.id === fm.id) continue;
|
|
3469
|
-
if (relatedSet.has(memory.frontmatter.id)) continue;
|
|
3470
|
-
const overlappingPaths = memory.frontmatter.anchor.paths.filter(
|
|
3471
|
-
(p) => targetPaths.some((tp) => singlePathsOverlap(p, tp))
|
|
3472
|
-
);
|
|
3473
|
-
if (overlappingPaths.length === 0) continue;
|
|
3474
|
-
const u = getUsage9(usage, memory.frontmatter.id);
|
|
3475
|
-
path_neighbors.push({
|
|
3476
|
-
id: memory.frontmatter.id,
|
|
3477
|
-
type: memory.frontmatter.type,
|
|
3478
|
-
scope: memory.frontmatter.scope,
|
|
3479
|
-
confidence: deriveConfidence7(memory.frontmatter, u),
|
|
3480
|
-
overlap: overlappingPaths,
|
|
3481
|
-
body_preview: memory.body.split("\n").slice(0, 3).join("\n").slice(0, 200)
|
|
3482
|
-
});
|
|
3483
|
-
if (path_neighbors.length >= 10) break;
|
|
3484
|
-
}
|
|
3485
|
-
}
|
|
3486
|
-
const recent_commits = [];
|
|
3487
|
-
for (const p of targetPaths.slice(0, 5)) {
|
|
3488
|
-
try {
|
|
3489
|
-
const commits = await runGitLog2(ctx.paths.root, p, input.git_log_limit);
|
|
3490
|
-
for (const c of commits) recent_commits.push({ path: p, ...c });
|
|
3491
|
-
} catch {
|
|
3492
|
-
}
|
|
3493
|
-
}
|
|
3494
|
-
const hints = [];
|
|
3495
|
-
if (decision.confidence === "low" || decision.confidence === "stale") {
|
|
3496
|
-
hints.push(`\u26A0\uFE0F Confidence is ${decision.confidence}. Verify this decision still applies before quoting it.`);
|
|
3497
|
-
}
|
|
3498
|
-
if (related.length === 0 && path_neighbors.length === 0 && targetPaths.length === 0) {
|
|
3499
|
-
hints.push("No related memories and no anchored paths \u2014 this decision is isolated; consider adding related_ids or paths.");
|
|
3500
|
-
}
|
|
3501
|
-
if (fm.type !== "decision" && fm.type !== "architecture") {
|
|
3502
|
-
hints.push(`Memory type is '${fm.type}', not 'decision'/'architecture' \u2014 output may be less informative.`);
|
|
3503
|
-
}
|
|
3504
|
-
return {
|
|
3505
|
-
found: true,
|
|
3506
|
-
decision,
|
|
3507
|
-
related,
|
|
3508
|
-
path_neighbors,
|
|
3509
|
-
recent_commits,
|
|
3510
|
-
...hints.length > 0 ? { hints } : {}
|
|
3511
|
-
};
|
|
3512
|
-
}
|
|
3513
|
-
async function runGitLog2(cwd, filePath, limit) {
|
|
3514
|
-
const sep = "<<HV>>";
|
|
3515
|
-
const fmt = `%h${sep}%an${sep}%ar${sep}%s`;
|
|
3516
|
-
const output = await runCommand2(
|
|
3517
|
-
"git",
|
|
3518
|
-
["log", "-n", String(limit), `--pretty=format:${fmt}`, "--", filePath],
|
|
3519
|
-
cwd
|
|
3520
|
-
);
|
|
3521
|
-
if (!output.trim()) return [];
|
|
3522
|
-
return output.split("\n").map((line) => {
|
|
3523
|
-
const [sha = "", author = "", relative_date = "", subject = ""] = line.split(sep);
|
|
3524
|
-
return { sha, author, relative_date, subject };
|
|
3525
|
-
}).filter((c) => c.sha);
|
|
3526
|
-
}
|
|
3527
|
-
function runCommand2(cmd, args, cwd) {
|
|
3528
|
-
return new Promise((resolve, reject) => {
|
|
3529
|
-
const proc = spawn2(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
3530
|
-
let stdout = "";
|
|
3531
|
-
let stderr = "";
|
|
3532
|
-
proc.stdout.on("data", (chunk) => {
|
|
3533
|
-
stdout += chunk.toString();
|
|
3534
|
-
});
|
|
3535
|
-
proc.stderr.on("data", (chunk) => {
|
|
3536
|
-
stderr += chunk.toString();
|
|
3537
|
-
});
|
|
3538
|
-
proc.on("error", reject);
|
|
3539
|
-
proc.on("close", (code) => {
|
|
3540
|
-
if (code === 0) resolve(stdout);
|
|
3541
|
-
else reject(new Error(stderr || `${cmd} exited with code ${code}`));
|
|
3542
|
-
});
|
|
3543
|
-
});
|
|
3544
|
-
}
|
|
3545
|
-
|
|
3546
|
-
// src/tools/mem-conflicts.ts
|
|
3547
|
-
import { existsSync as existsSync29 } from "fs";
|
|
3548
|
-
import {
|
|
3549
|
-
deriveConfidence as deriveConfidence8,
|
|
3550
|
-
getUsage as getUsage10,
|
|
3551
|
-
loadMemoriesFromDir as loadMemoriesFromDir23,
|
|
3552
|
-
loadUsageIndex as loadUsageIndex12,
|
|
3553
|
-
pathsOverlap as pathsOverlap2,
|
|
3554
|
-
tokenizeQuery as tokenizeQuery5
|
|
3555
|
-
} from "@hivelore/core";
|
|
3556
|
-
import { z as z30 } from "zod";
|
|
3557
|
-
var MemConflictsInputSchema = {
|
|
3558
|
-
id: z30.string().min(1).describe("Memory id to check for conflicts."),
|
|
3559
|
-
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)."),
|
|
3560
|
-
semantic: z30.boolean().default(true).describe("Use embeddings for similarity. Falls back to keyword overlap when embeddings are not installed.")
|
|
3561
|
-
};
|
|
3562
|
-
var POSITIVE_PATTERNS = /\b(use|prefer|always|should use|do this|recommended|ok to)\b/i;
|
|
3563
|
-
var NEGATIVE_PATTERNS = /\b(do not use|don'?t use|never|avoid|forbidden|deprecated|stop using|do NOT|❌)\b/i;
|
|
3564
|
-
async function memConflicts(input, ctx) {
|
|
3565
|
-
if (!existsSync29(ctx.paths.memoriesDir)) {
|
|
3566
|
-
return { found: false, scanned: 0, conflicts: [], notice: "No .ai/memories directory." };
|
|
3567
|
-
}
|
|
3568
|
-
const all = await loadMemoriesFromDir23(ctx.paths.memoriesDir);
|
|
3569
|
-
const target = all.find(({ memory }) => memory.frontmatter.id === input.id);
|
|
3570
|
-
if (!target) {
|
|
3571
|
-
return { found: false, scanned: 0, conflicts: [], notice: `Memory '${input.id}' not found.` };
|
|
3572
|
-
}
|
|
3573
|
-
const usage = await loadUsageIndex12(ctx.paths);
|
|
3574
|
-
const others = all.filter(
|
|
3575
|
-
({ memory }) => memory.frontmatter.id !== input.id && memory.frontmatter.type !== "session_recap"
|
|
3576
|
-
);
|
|
3577
|
-
const simScores = input.semantic ? await trySemanticSimilarities(ctx, target, others) : null;
|
|
3578
|
-
const targetText = (target.memory.body + " " + target.memory.frontmatter.tags.join(" ")).toLowerCase();
|
|
3579
|
-
const targetTokens = new Set(tokenizeQuery5(targetText));
|
|
3580
|
-
const targetPolarity = polarity(targetText);
|
|
3581
|
-
const targetPaths = target.memory.frontmatter.anchor.paths;
|
|
3582
|
-
const explicitContradicts = extractContradictsTags(target.memory.body);
|
|
3583
|
-
const conflicts = [];
|
|
3584
|
-
for (const other of others) {
|
|
3585
|
-
const fm = other.memory.frontmatter;
|
|
3586
|
-
const otherText = (other.memory.body + " " + fm.tags.join(" ")).toLowerCase();
|
|
3587
|
-
const reasons = [];
|
|
3588
|
-
const sim = simScores?.get(fm.id) ?? null;
|
|
3589
|
-
const hasPathOverlap = fm.anchor.paths.some((p) => targetPaths.some((tp) => pathsOverlap2(p, tp)));
|
|
3590
|
-
const otherTokens = new Set(tokenizeQuery5(otherText));
|
|
3591
|
-
const tokenOverlap = countIntersection(targetTokens, otherTokens);
|
|
3592
|
-
const isSemanticNeighbor = sim !== null && sim >= input.min_score;
|
|
3593
|
-
if (!hasPathOverlap && tokenOverlap < 4 && !isSemanticNeighbor) continue;
|
|
3594
|
-
const otherContradicts = extractContradictsTags(other.memory.body);
|
|
3595
|
-
if (explicitContradicts.has(fm.id) || otherContradicts.has(input.id)) {
|
|
3596
|
-
reasons.push("explicit-contradiction-tag");
|
|
3597
|
-
}
|
|
3598
|
-
if (target.memory.frontmatter.status === "validated" && fm.status === "rejected" || target.memory.frontmatter.status === "rejected" && fm.status === "validated") {
|
|
3599
|
-
if (tokenOverlap >= 4 || isSemanticNeighbor) reasons.push("opposite-status");
|
|
3600
|
-
}
|
|
3601
|
-
if (hasPathOverlap) {
|
|
3602
|
-
const tType = target.memory.frontmatter.type;
|
|
3603
|
-
const oType = fm.type;
|
|
3604
|
-
const isAttemptVsRule = tType === "attempt" && (oType === "convention" || oType === "decision") || oType === "attempt" && (tType === "convention" || tType === "decision");
|
|
3605
|
-
if (isAttemptVsRule) reasons.push("attempt-vs-convention-same-paths");
|
|
3606
|
-
}
|
|
3607
|
-
if (isSemanticNeighbor) {
|
|
3608
|
-
const otherPolarity = polarity(otherText);
|
|
3609
|
-
if (targetPolarity === "positive" && otherPolarity === "negative" || targetPolarity === "negative" && otherPolarity === "positive") {
|
|
3610
|
-
reasons.push("polarity-keywords");
|
|
3611
|
-
}
|
|
3612
|
-
}
|
|
3613
|
-
if (reasons.length === 0) continue;
|
|
3614
|
-
const u = getUsage10(usage, fm.id);
|
|
3615
|
-
conflicts.push({
|
|
3616
|
-
id: fm.id,
|
|
3617
|
-
type: fm.type,
|
|
3618
|
-
scope: fm.scope,
|
|
3619
|
-
status: fm.status,
|
|
3620
|
-
confidence: deriveConfidence8(fm, u),
|
|
3621
|
-
body_preview: other.memory.body.split("\n").slice(0, 4).join("\n").slice(0, 300),
|
|
3622
|
-
similarity: sim,
|
|
3623
|
-
reasons,
|
|
3624
|
-
shared_paths: fm.anchor.paths.filter((p) => targetPaths.some((tp) => pathsOverlap2(p, tp)))
|
|
3625
|
-
});
|
|
3626
|
-
}
|
|
3627
|
-
conflicts.sort((a, b) => {
|
|
3628
|
-
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;
|
|
3629
|
-
return score(b) - score(a);
|
|
3630
|
-
});
|
|
3631
|
-
return {
|
|
3632
|
-
found: true,
|
|
3633
|
-
target: {
|
|
3634
|
-
id: target.memory.frontmatter.id,
|
|
3635
|
-
type: target.memory.frontmatter.type,
|
|
3636
|
-
status: target.memory.frontmatter.status
|
|
3637
|
-
},
|
|
3638
|
-
scanned: others.length,
|
|
3639
|
-
conflicts: conflicts.slice(0, 10)
|
|
3640
|
-
};
|
|
3641
|
-
}
|
|
3642
|
-
function polarity(text) {
|
|
3643
|
-
const neg = NEGATIVE_PATTERNS.test(text);
|
|
3644
|
-
const pos = POSITIVE_PATTERNS.test(text);
|
|
3645
|
-
if (neg && !pos) return "negative";
|
|
3646
|
-
if (pos && !neg) return "positive";
|
|
3647
|
-
return "neutral";
|
|
3648
|
-
}
|
|
3649
|
-
function extractContradictsTags(body) {
|
|
3650
|
-
const out = /* @__PURE__ */ new Set();
|
|
3651
|
-
for (const m of body.matchAll(/#contradicts:([\w-]+)/g)) {
|
|
3652
|
-
if (m[1]) out.add(m[1]);
|
|
3653
|
-
}
|
|
3654
|
-
return out;
|
|
3655
|
-
}
|
|
3656
|
-
function countIntersection(a, b) {
|
|
3657
|
-
let n = 0;
|
|
3658
|
-
for (const x of a) if (b.has(x)) n++;
|
|
3659
|
-
return n;
|
|
3660
|
-
}
|
|
3661
|
-
async function trySemanticSimilarities(ctx, target, others) {
|
|
3662
|
-
let mod;
|
|
3663
|
-
try {
|
|
3664
|
-
mod = await import("@hivelore/embeddings");
|
|
3665
|
-
} catch {
|
|
3666
|
-
return null;
|
|
3667
|
-
}
|
|
3668
|
-
const result = await mod.semanticSearch(
|
|
3669
|
-
ctx.paths,
|
|
3670
|
-
target.memory.body,
|
|
3671
|
-
{ limit: others.length }
|
|
3672
|
-
);
|
|
3673
|
-
if (!result) return null;
|
|
3674
|
-
const map = /* @__PURE__ */ new Map();
|
|
3675
|
-
for (const hit of result.hits) map.set(hit.id, hit.score);
|
|
3676
|
-
return map;
|
|
3677
|
-
}
|
|
3678
|
-
|
|
3679
3355
|
// src/tools/precommit-check.ts
|
|
3680
|
-
import { pathsOverlap as
|
|
3681
|
-
import { z as
|
|
3356
|
+
import { pathsOverlap as pathsOverlap2 } from "@hivelore/core";
|
|
3357
|
+
import { z as z27 } from "zod";
|
|
3682
3358
|
var PreCommitCheckInputSchema = {
|
|
3683
|
-
diff:
|
|
3359
|
+
diff: z27.string().optional().describe(
|
|
3684
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`."
|
|
3685
3361
|
),
|
|
3686
|
-
paths:
|
|
3687
|
-
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(
|
|
3688
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."
|
|
3689
3365
|
),
|
|
3690
|
-
semantic:
|
|
3691
|
-
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(
|
|
3692
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."
|
|
3693
3369
|
)
|
|
3694
3370
|
};
|
|
@@ -3757,7 +3433,7 @@ async function preCommitCheck(input, ctx) {
|
|
|
3757
3433
|
}
|
|
3758
3434
|
function classifyWarning(warning, paths, anchoredBlocks = false) {
|
|
3759
3435
|
const codeFiles = paths.filter((p) => !p.startsWith(".ai/") && !isHaiveOwnedPath(p));
|
|
3760
|
-
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))) : [];
|
|
3761
3437
|
const affectedFiles = anchorHits.length > 0 ? anchorHits : codeFiles;
|
|
3762
3438
|
const repairCommand = repairCommandForWarning(warning, affectedFiles);
|
|
3763
3439
|
const fileDowngrade = fileTypeDowngradeReason(warning, affectedFiles);
|
|
@@ -3993,228 +3669,15 @@ function repairTargetPathForWarning(warning, paths) {
|
|
|
3993
3669
|
return usablePaths[0];
|
|
3994
3670
|
}
|
|
3995
3671
|
|
|
3996
|
-
// src/tools/pattern-detect.ts
|
|
3997
|
-
import { mkdir as mkdir8, writeFile as writeFile15 } from "fs/promises";
|
|
3998
|
-
import { existsSync as existsSync30 } from "fs";
|
|
3999
|
-
import path14 from "path";
|
|
4000
|
-
import { execSync as execSync3 } from "child_process";
|
|
4001
|
-
import {
|
|
4002
|
-
buildFrontmatter as buildFrontmatter5,
|
|
4003
|
-
memoryFilePath as memoryFilePath6,
|
|
4004
|
-
readUsageEvents,
|
|
4005
|
-
serializeMemory as serializeMemory13
|
|
4006
|
-
} from "@hivelore/core";
|
|
4007
|
-
import { z as z32 } from "zod";
|
|
4008
|
-
var CONFIG_PATTERNS = [
|
|
4009
|
-
".eslintrc",
|
|
4010
|
-
"eslint.config",
|
|
4011
|
-
"prettier.config",
|
|
4012
|
-
".prettierrc",
|
|
4013
|
-
"tsconfig",
|
|
4014
|
-
"jsconfig",
|
|
4015
|
-
"vitest.config",
|
|
4016
|
-
"jest.config",
|
|
4017
|
-
".env.example",
|
|
4018
|
-
".env.defaults",
|
|
4019
|
-
"tailwind.config",
|
|
4020
|
-
"vite.config",
|
|
4021
|
-
"next.config",
|
|
4022
|
-
"babel.config",
|
|
4023
|
-
"postcss.config",
|
|
4024
|
-
"renovate.json",
|
|
4025
|
-
"dependabot.yml"
|
|
4026
|
-
];
|
|
4027
|
-
var MAX_DIFF_BYTES = 4096;
|
|
4028
|
-
var HOT_FILE_MIN = 3;
|
|
4029
|
-
var PatternDetectInputSchema = {
|
|
4030
|
-
since_days: z32.number().int().min(1).default(7).describe("Look-back window in days for both git history and usage log."),
|
|
4031
|
-
dry_run: z32.boolean().default(false).describe("When true, report matches without writing any memory files."),
|
|
4032
|
-
scope: z32.enum(["personal", "team"]).default("team").describe("Scope for proposed memories.")
|
|
4033
|
-
};
|
|
4034
|
-
async function patternDetect(input, ctx) {
|
|
4035
|
-
if (!existsSync30(ctx.paths.haiveDir)) {
|
|
4036
|
-
return {
|
|
4037
|
-
scanned_events: 0,
|
|
4038
|
-
matches: [],
|
|
4039
|
-
saved: 0,
|
|
4040
|
-
saved_ids: [],
|
|
4041
|
-
notice: "No .ai/ directory found. Run 'hivelore init' first."
|
|
4042
|
-
};
|
|
4043
|
-
}
|
|
4044
|
-
const matches = [];
|
|
4045
|
-
try {
|
|
4046
|
-
const changedFiles = gitChangedFiles(ctx.paths.root, input.since_days);
|
|
4047
|
-
const configFiles = changedFiles.filter(
|
|
4048
|
-
(f) => CONFIG_PATTERNS.some((p) => path14.basename(f.toLowerCase()).includes(p))
|
|
4049
|
-
);
|
|
4050
|
-
for (const file of configFiles.slice(0, 5)) {
|
|
4051
|
-
const diff = gitFileDiff(ctx.paths.root, file, input.since_days);
|
|
4052
|
-
if (!diff) continue;
|
|
4053
|
-
const parentDir = path14.basename(path14.dirname(file));
|
|
4054
|
-
const baseName = path14.basename(file).replace(/\.[^.]+$/, "");
|
|
4055
|
-
const slug = `${parentDir}-${baseName}`.replace(/[^a-z0-9]/gi, "-").toLowerCase().slice(0, 40);
|
|
4056
|
-
matches.push({
|
|
4057
|
-
kind: "config_change",
|
|
4058
|
-
signal: `Config file modified: ${file}`,
|
|
4059
|
-
proposed_type: "convention",
|
|
4060
|
-
proposed_slug: `config-change-${slug}`,
|
|
4061
|
-
proposed_body: [
|
|
4062
|
-
`# Config change: \`${file}\``,
|
|
4063
|
-
"",
|
|
4064
|
-
"This configuration file was recently modified. The diff below captures the intent.",
|
|
4065
|
-
"Review and update this memory with the **reason** for the change if known.",
|
|
4066
|
-
"",
|
|
4067
|
-
"```diff",
|
|
4068
|
-
diff.slice(0, MAX_DIFF_BYTES),
|
|
4069
|
-
"```"
|
|
4070
|
-
].join("\n"),
|
|
4071
|
-
anchor_paths: [file]
|
|
4072
|
-
});
|
|
4073
|
-
}
|
|
4074
|
-
} catch {
|
|
4075
|
-
}
|
|
4076
|
-
const events = await readUsageEvents(ctx.paths);
|
|
4077
|
-
const cutoff = Date.now() - input.since_days * 24 * 60 * 60 * 1e3;
|
|
4078
|
-
const recent = events.filter((e) => Date.parse(e.at) >= cutoff);
|
|
4079
|
-
const pathCounts = /* @__PURE__ */ new Map();
|
|
4080
|
-
for (const e of recent) {
|
|
4081
|
-
if (!["mem_tried", "mem_observe", "mem_save"].includes(e.tool)) continue;
|
|
4082
|
-
if (!e.summary) continue;
|
|
4083
|
-
const tokens = e.summary.match(/[^\s"'`,;()[\]{}]+\.[a-zA-Z]{1,6}/g) ?? [];
|
|
4084
|
-
for (const t of tokens) {
|
|
4085
|
-
const key = t.toLowerCase();
|
|
4086
|
-
const existing = pathCounts.get(key);
|
|
4087
|
-
if (existing) {
|
|
4088
|
-
existing.count++;
|
|
4089
|
-
existing.tools.add(e.tool);
|
|
4090
|
-
} else {
|
|
4091
|
-
pathCounts.set(key, { count: 1, tools: /* @__PURE__ */ new Set([e.tool]) });
|
|
4092
|
-
}
|
|
4093
|
-
}
|
|
4094
|
-
}
|
|
4095
|
-
for (const [p, { count, tools }] of pathCounts) {
|
|
4096
|
-
if (count < HOT_FILE_MIN) continue;
|
|
4097
|
-
const isGotchaSignal = tools.has("mem_tried") || tools.has("mem_observe");
|
|
4098
|
-
if (!isGotchaSignal) continue;
|
|
4099
|
-
const slug = p.replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 40);
|
|
4100
|
-
matches.push({
|
|
4101
|
-
kind: "repeated_path",
|
|
4102
|
-
signal: `Path '${p}' appears ${count}\xD7 in mem_tried/mem_observe events`,
|
|
4103
|
-
proposed_type: "gotcha",
|
|
4104
|
-
proposed_slug: `repeated-issue-${slug}`,
|
|
4105
|
-
proposed_body: [
|
|
4106
|
-
`# Recurring issue near \`${p}\``,
|
|
4107
|
-
"",
|
|
4108
|
-
`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.`,
|
|
4109
|
-
"",
|
|
4110
|
-
`**Source signals:** ${[...tools].join(", ")} (${count} events)`
|
|
4111
|
-
].join("\n"),
|
|
4112
|
-
anchor_paths: [p]
|
|
4113
|
-
});
|
|
4114
|
-
}
|
|
4115
|
-
for (const [p, { count, tools }] of pathCounts) {
|
|
4116
|
-
if (count < HOT_FILE_MIN) continue;
|
|
4117
|
-
if (tools.has("mem_tried") || tools.has("mem_observe")) continue;
|
|
4118
|
-
if (CONFIG_PATTERNS.some((cp) => path14.basename(p).includes(cp))) continue;
|
|
4119
|
-
const slug = p.replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 40);
|
|
4120
|
-
matches.push({
|
|
4121
|
-
kind: "hot_file",
|
|
4122
|
-
signal: `Path '${p}' referenced ${count}\xD7 across mem_save events`,
|
|
4123
|
-
proposed_type: "convention",
|
|
4124
|
-
proposed_slug: `hot-file-${slug}`,
|
|
4125
|
-
proposed_body: [
|
|
4126
|
-
`# Frequent edits to \`${p}\``,
|
|
4127
|
-
"",
|
|
4128
|
-
`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.`,
|
|
4129
|
-
"",
|
|
4130
|
-
"**Suggested action:** review recent memories anchored to this path and extract the common pattern as a named convention."
|
|
4131
|
-
].join("\n"),
|
|
4132
|
-
anchor_paths: [p]
|
|
4133
|
-
});
|
|
4134
|
-
}
|
|
4135
|
-
if (matches.length === 0) {
|
|
4136
|
-
return {
|
|
4137
|
-
scanned_events: recent.length,
|
|
4138
|
-
matches: [],
|
|
4139
|
-
saved: 0,
|
|
4140
|
-
saved_ids: [],
|
|
4141
|
-
notice: `No patterns detected in the last ${input.since_days} days (${recent.length} events scanned).`
|
|
4142
|
-
};
|
|
4143
|
-
}
|
|
4144
|
-
if (input.dry_run) {
|
|
4145
|
-
return { scanned_events: recent.length, matches, saved: 0, saved_ids: [] };
|
|
4146
|
-
}
|
|
4147
|
-
const savedIds = [];
|
|
4148
|
-
for (const match of matches) {
|
|
4149
|
-
try {
|
|
4150
|
-
const fm = buildFrontmatter5({
|
|
4151
|
-
type: match.proposed_type,
|
|
4152
|
-
slug: match.proposed_slug,
|
|
4153
|
-
scope: input.scope,
|
|
4154
|
-
tags: ["pattern-detect", match.kind],
|
|
4155
|
-
paths: match.anchor_paths,
|
|
4156
|
-
status: "proposed"
|
|
4157
|
-
});
|
|
4158
|
-
const file = memoryFilePath6(
|
|
4159
|
-
ctx.paths,
|
|
4160
|
-
fm.scope === "shared" ? "team" : fm.scope,
|
|
4161
|
-
fm.id,
|
|
4162
|
-
void 0
|
|
4163
|
-
);
|
|
4164
|
-
if (existsSync30(file)) continue;
|
|
4165
|
-
await mkdir8(path14.dirname(file), { recursive: true });
|
|
4166
|
-
await writeFile15(
|
|
4167
|
-
file,
|
|
4168
|
-
serializeMemory13({ frontmatter: fm, body: match.proposed_body }),
|
|
4169
|
-
"utf8"
|
|
4170
|
-
);
|
|
4171
|
-
savedIds.push(fm.id);
|
|
4172
|
-
} catch {
|
|
4173
|
-
}
|
|
4174
|
-
}
|
|
4175
|
-
return {
|
|
4176
|
-
scanned_events: recent.length,
|
|
4177
|
-
matches,
|
|
4178
|
-
saved: savedIds.length,
|
|
4179
|
-
saved_ids: savedIds
|
|
4180
|
-
};
|
|
4181
|
-
}
|
|
4182
|
-
function gitChangedFiles(root, sinceDays) {
|
|
4183
|
-
try {
|
|
4184
|
-
const out = execSync3(
|
|
4185
|
-
`git log --name-only --pretty="" --diff-filter=AM --since="${sinceDays} days ago"`,
|
|
4186
|
-
{ cwd: root, encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
|
|
4187
|
-
);
|
|
4188
|
-
return [...new Set(out.split("\n").map((l) => l.trim()).filter(Boolean))];
|
|
4189
|
-
} catch {
|
|
4190
|
-
return [];
|
|
4191
|
-
}
|
|
4192
|
-
}
|
|
4193
|
-
function gitFileDiff(root, file, sinceDays) {
|
|
4194
|
-
try {
|
|
4195
|
-
const out = execSync3(
|
|
4196
|
-
`git log -p --follow --since="${sinceDays} days ago" -- "${file}"`,
|
|
4197
|
-
{ cwd: root, encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
|
|
4198
|
-
);
|
|
4199
|
-
if (!out.trim()) return null;
|
|
4200
|
-
const diffLines = out.split("\n").filter(
|
|
4201
|
-
(l) => l.startsWith("+") || l.startsWith("-") || l.startsWith("@@") || l.startsWith("diff")
|
|
4202
|
-
);
|
|
4203
|
-
return diffLines.join("\n").slice(0, MAX_DIFF_BYTES) || null;
|
|
4204
|
-
} catch {
|
|
4205
|
-
return null;
|
|
4206
|
-
}
|
|
4207
|
-
}
|
|
4208
|
-
|
|
4209
3672
|
// src/tools/mem-conflict-candidates.ts
|
|
4210
|
-
import { existsSync as
|
|
3673
|
+
import { existsSync as existsSync26 } from "fs";
|
|
4211
3674
|
import {
|
|
4212
3675
|
findLexicalConflictPairs,
|
|
4213
3676
|
findTopicStatusConflictPairs,
|
|
4214
|
-
loadMemoriesFromDir as
|
|
3677
|
+
loadMemoriesFromDir as loadMemoriesFromDir21,
|
|
4215
3678
|
planConflictResolution
|
|
4216
3679
|
} from "@hivelore/core";
|
|
4217
|
-
import { z as
|
|
3680
|
+
import { z as z28 } from "zod";
|
|
4218
3681
|
function suggestResolution(byId, idA, idB) {
|
|
4219
3682
|
const a = byId.get(idA);
|
|
4220
3683
|
const b = byId.get(idB);
|
|
@@ -4228,17 +3691,17 @@ function suggestResolution(byId, idA, idB) {
|
|
|
4228
3691
|
};
|
|
4229
3692
|
}
|
|
4230
3693
|
var MemConflictCandidatesInputSchema = {
|
|
4231
|
-
since_days:
|
|
4232
|
-
types:
|
|
4233
|
-
min_jaccard:
|
|
4234
|
-
max_pairs:
|
|
4235
|
-
max_scan:
|
|
4236
|
-
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(
|
|
4237
3700
|
"Cap for extra signal: memories sharing the same topic with validated vs rejected status."
|
|
4238
3701
|
)
|
|
4239
3702
|
};
|
|
4240
3703
|
async function memConflictCandidates(input, ctx) {
|
|
4241
|
-
if (!
|
|
3704
|
+
if (!existsSync26(ctx.paths.memoriesDir)) {
|
|
4242
3705
|
return {
|
|
4243
3706
|
pairs: [],
|
|
4244
3707
|
topic_status_pairs: [],
|
|
@@ -4247,7 +3710,7 @@ async function memConflictCandidates(input, ctx) {
|
|
|
4247
3710
|
notice: "No .ai/memories directory."
|
|
4248
3711
|
};
|
|
4249
3712
|
}
|
|
4250
|
-
const all = await
|
|
3713
|
+
const all = await loadMemoriesFromDir21(ctx.paths.memoriesDir);
|
|
4251
3714
|
const byId = new Map(all.map((m) => [m.memory.frontmatter.id, m]));
|
|
4252
3715
|
const { pairs, scanned, truncated } = findLexicalConflictPairs(all, {
|
|
4253
3716
|
sinceDays: input.since_days,
|
|
@@ -4277,9 +3740,9 @@ async function memConflictCandidates(input, ctx) {
|
|
|
4277
3740
|
|
|
4278
3741
|
// src/tools/mem-resolve-project.ts
|
|
4279
3742
|
import { resolveProjectInfo } from "@hivelore/core";
|
|
4280
|
-
import { z as
|
|
3743
|
+
import { z as z29 } from "zod";
|
|
4281
3744
|
var MemResolveProjectInputSchema = {
|
|
4282
|
-
cwd:
|
|
3745
|
+
cwd: z29.string().optional().describe("Directory used for root discovery when HAIVE_PROJECT_ROOT is unset.")
|
|
4283
3746
|
};
|
|
4284
3747
|
async function memResolveProject(input, _ctx) {
|
|
4285
3748
|
void _ctx;
|
|
@@ -4293,10 +3756,10 @@ async function memResolveProject(input, _ctx) {
|
|
|
4293
3756
|
|
|
4294
3757
|
// src/tools/mem-suggest-topic.ts
|
|
4295
3758
|
import { MemoryTypeSchema, suggestTopicKey } from "@hivelore/core";
|
|
4296
|
-
import { z as
|
|
3759
|
+
import { z as z30 } from "zod";
|
|
4297
3760
|
var MemSuggestTopicInputSchema = {
|
|
4298
3761
|
type: MemoryTypeSchema.describe("Memory kind \u2014 drives the suggested topic family."),
|
|
4299
|
-
title:
|
|
3762
|
+
title: z30.string().min(1).describe("Short title or phrase (headers, headings) \u2014 turned into slug")
|
|
4300
3763
|
};
|
|
4301
3764
|
async function memSuggestTopic(input, _ctx) {
|
|
4302
3765
|
void _ctx;
|
|
@@ -4305,19 +3768,19 @@ async function memSuggestTopic(input, _ctx) {
|
|
|
4305
3768
|
}
|
|
4306
3769
|
|
|
4307
3770
|
// src/tools/mem-timeline.ts
|
|
4308
|
-
import { existsSync as
|
|
4309
|
-
import { collectTimelineEntries, loadMemoriesFromDir as
|
|
4310
|
-
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";
|
|
4311
3774
|
var MemTimelineInputSchema = {
|
|
4312
|
-
memory_id:
|
|
4313
|
-
topic:
|
|
4314
|
-
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")
|
|
4315
3778
|
};
|
|
4316
3779
|
async function memTimeline(input, ctx) {
|
|
4317
|
-
if (!
|
|
3780
|
+
if (!existsSync27(ctx.paths.memoriesDir)) {
|
|
4318
3781
|
return { entries: [], total: 0, notice: "No .ai/memories directory." };
|
|
4319
3782
|
}
|
|
4320
|
-
const all = await
|
|
3783
|
+
const all = await loadMemoriesFromDir22(ctx.paths.memoriesDir);
|
|
4321
3784
|
const { entries, notice } = collectTimelineEntries(all, {
|
|
4322
3785
|
memoryId: input.memory_id,
|
|
4323
3786
|
topic: input.topic,
|
|
@@ -4326,47 +3789,13 @@ async function memTimeline(input, ctx) {
|
|
|
4326
3789
|
return { entries, total: entries.length, notice };
|
|
4327
3790
|
}
|
|
4328
3791
|
|
|
4329
|
-
// src/tools/runtime-journal-append.ts
|
|
4330
|
-
import { appendRuntimeJournalEntry as appendRuntimeJournalEntry2 } from "@hivelore/core";
|
|
4331
|
-
import { z as z37 } from "zod";
|
|
4332
|
-
var RuntimeJournalAppendInputSchema = {
|
|
4333
|
-
message: z37.string().min(1).describe("Short line to append to the runtime session journal"),
|
|
4334
|
-
kind: z37.enum(["note", "session_end", "mcp"]).default("note"),
|
|
4335
|
-
tool: z37.string().optional().describe("When kind=mcp, which tool name (optional)")
|
|
4336
|
-
};
|
|
4337
|
-
async function runtimeJournalAppend(input, ctx) {
|
|
4338
|
-
await appendRuntimeJournalEntry2(ctx.paths, {
|
|
4339
|
-
kind: input.kind,
|
|
4340
|
-
message: input.message,
|
|
4341
|
-
...input.tool ? { tool: input.tool } : {}
|
|
4342
|
-
});
|
|
4343
|
-
return {
|
|
4344
|
-
ok: true,
|
|
4345
|
-
path_hint: `${ctx.paths.runtimeDir}/session-journal.ndjson`
|
|
4346
|
-
};
|
|
4347
|
-
}
|
|
4348
|
-
|
|
4349
|
-
// src/tools/runtime-journal-tail.ts
|
|
4350
|
-
import { readRuntimeJournalTail } from "@hivelore/core";
|
|
4351
|
-
import { z as z38 } from "zod";
|
|
4352
|
-
var RuntimeJournalTailInputSchema = {
|
|
4353
|
-
limit: z38.number().int().positive().max(500).default(30).describe("Last N journal entries to return")
|
|
4354
|
-
};
|
|
4355
|
-
async function runtimeJournalTail(input, ctx) {
|
|
4356
|
-
const entries = await readRuntimeJournalTail(ctx.paths, input.limit);
|
|
4357
|
-
if (entries.length === 0) {
|
|
4358
|
-
return { entries: [], empty: true };
|
|
4359
|
-
}
|
|
4360
|
-
return { entries };
|
|
4361
|
-
}
|
|
4362
|
-
|
|
4363
3792
|
// src/prompts/bootstrap-project.ts
|
|
4364
|
-
import { z as
|
|
3793
|
+
import { z as z32 } from "zod";
|
|
4365
3794
|
var BootstrapProjectArgsSchema = {
|
|
4366
|
-
module:
|
|
3795
|
+
module: z32.string().optional().describe(
|
|
4367
3796
|
"Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
|
|
4368
3797
|
),
|
|
4369
|
-
focus:
|
|
3798
|
+
focus: z32.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
|
|
4370
3799
|
};
|
|
4371
3800
|
var ROOT_TEMPLATE = `# Project context
|
|
4372
3801
|
|
|
@@ -4449,16 +3878,16 @@ ${template}\`\`\`
|
|
|
4449
3878
|
|
|
4450
3879
|
// src/prompts/bootstrap-repo.ts
|
|
4451
3880
|
import { readFile as readFile7, readdir as readdir5 } from "fs/promises";
|
|
4452
|
-
import { existsSync as
|
|
3881
|
+
import { existsSync as existsSync28 } from "fs";
|
|
4453
3882
|
import {
|
|
4454
3883
|
assessBootstrapState as assessBootstrapState2,
|
|
4455
|
-
loadCodeMap as
|
|
4456
|
-
loadMemoriesFromDir as
|
|
3884
|
+
loadCodeMap as loadCodeMap4,
|
|
3885
|
+
loadMemoriesFromDir as loadMemoriesFromDir23,
|
|
4457
3886
|
renderBootstrapChecklist as renderBootstrapChecklist2
|
|
4458
3887
|
} from "@hivelore/core";
|
|
4459
|
-
import { z as
|
|
3888
|
+
import { z as z33 } from "zod";
|
|
4460
3889
|
var BootstrapRepoArgsSchema = {
|
|
4461
|
-
focus:
|
|
3890
|
+
focus: z33.string().optional().describe("Optional area to emphasize first (e.g. 'payments', 'auth').")
|
|
4462
3891
|
};
|
|
4463
3892
|
async function currentAssessment(ctx) {
|
|
4464
3893
|
let projectContextRaw = "";
|
|
@@ -4466,8 +3895,8 @@ async function currentAssessment(ctx) {
|
|
|
4466
3895
|
projectContextRaw = await readFile7(ctx.paths.projectContext, "utf8");
|
|
4467
3896
|
} catch {
|
|
4468
3897
|
}
|
|
4469
|
-
const memories =
|
|
4470
|
-
const codeMap = await
|
|
3898
|
+
const memories = existsSync28(ctx.paths.memoriesDir) ? await loadMemoriesFromDir23(ctx.paths.memoriesDir) : [];
|
|
3899
|
+
const codeMap = await loadCodeMap4(ctx.paths);
|
|
4471
3900
|
let existingModules = [];
|
|
4472
3901
|
try {
|
|
4473
3902
|
const entries = await readdir5(ctx.paths.modulesContextDir, { withFileTypes: true });
|
|
@@ -4534,10 +3963,10 @@ Main code areas detected: ${areas}
|
|
|
4534
3963
|
}
|
|
4535
3964
|
|
|
4536
3965
|
// src/prompts/post-task.ts
|
|
4537
|
-
import { z as
|
|
3966
|
+
import { z as z34 } from "zod";
|
|
4538
3967
|
var PostTaskArgsSchema = {
|
|
4539
|
-
task_summary:
|
|
4540
|
-
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")
|
|
4541
3970
|
};
|
|
4542
3971
|
function postTaskPrompt(args, ctx) {
|
|
4543
3972
|
const taskLine = args.task_summary ? `
|
|
@@ -4634,12 +4063,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
|
|
|
4634
4063
|
}
|
|
4635
4064
|
|
|
4636
4065
|
// src/prompts/import-docs.ts
|
|
4637
|
-
import { z as
|
|
4066
|
+
import { z as z35 } from "zod";
|
|
4638
4067
|
var ImportDocsArgsSchema = {
|
|
4639
|
-
content:
|
|
4640
|
-
source:
|
|
4641
|
-
scope:
|
|
4642
|
-
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")
|
|
4643
4072
|
};
|
|
4644
4073
|
function importDocsPrompt(args, ctx) {
|
|
4645
4074
|
const sourceLine = args.source ? `
|
|
@@ -4705,7 +4134,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
4705
4134
|
// src/server.ts
|
|
4706
4135
|
import { hasRecentBriefingMarker, loadConfigSync } from "@hivelore/core";
|
|
4707
4136
|
var SERVER_NAME = "hivelore";
|
|
4708
|
-
var SERVER_VERSION = "0.
|
|
4137
|
+
var SERVER_VERSION = "0.33.0";
|
|
4709
4138
|
function jsonResult(data) {
|
|
4710
4139
|
return {
|
|
4711
4140
|
content: [
|
|
@@ -4753,14 +4182,7 @@ var MAINTENANCE_PROFILE_TOOLS = [
|
|
|
4753
4182
|
"ingest_findings"
|
|
4754
4183
|
];
|
|
4755
4184
|
var EXPERIMENTAL_PROFILE_TOOLS = [
|
|
4756
|
-
...MAINTENANCE_PROFILE_TOOLS
|
|
4757
|
-
"mem_observe",
|
|
4758
|
-
"why_this_file",
|
|
4759
|
-
"why_this_decision",
|
|
4760
|
-
"mem_conflicts_with",
|
|
4761
|
-
"pattern_detect",
|
|
4762
|
-
"runtime_journal_append",
|
|
4763
|
-
"runtime_journal_tail"
|
|
4185
|
+
...MAINTENANCE_PROFILE_TOOLS
|
|
4764
4186
|
];
|
|
4765
4187
|
var TOOL_PROFILES = {
|
|
4766
4188
|
enforcement: new Set(ENFORCEMENT_PROFILE_TOOLS),
|
|
@@ -4775,7 +4197,6 @@ var BRIEFING_TOOLS = /* @__PURE__ */ new Set(["get_briefing", "mem_relevant_to"]
|
|
|
4775
4197
|
var MUTATING_TOOLS = /* @__PURE__ */ new Set([
|
|
4776
4198
|
"mem_save",
|
|
4777
4199
|
"mem_tried",
|
|
4778
|
-
"mem_observe",
|
|
4779
4200
|
"mem_session_end",
|
|
4780
4201
|
"bootstrap_project_save",
|
|
4781
4202
|
"mem_update",
|
|
@@ -4783,8 +4204,6 @@ var MUTATING_TOOLS = /* @__PURE__ */ new Set([
|
|
|
4783
4204
|
"mem_reject",
|
|
4784
4205
|
"mem_delete",
|
|
4785
4206
|
"mem_feedback",
|
|
4786
|
-
"runtime_journal_append",
|
|
4787
|
-
"pattern_detect",
|
|
4788
4207
|
"ingest_findings",
|
|
4789
4208
|
"propose_sensor"
|
|
4790
4209
|
]);
|
|
@@ -4847,7 +4266,7 @@ function createHaiveServer(options = {}) {
|
|
|
4847
4266
|
" - A domain term and what it means in this codebase",
|
|
4848
4267
|
"",
|
|
4849
4268
|
"DO NOT USE for failed approaches \u2192 use mem_tried instead (better structure).",
|
|
4850
|
-
"
|
|
4269
|
+
"For reactive code discoveries during exploration, prefer a compact gotcha via mem_save.",
|
|
4851
4270
|
"",
|
|
4852
4271
|
"PARAMETERS:",
|
|
4853
4272
|
" type \u2014 convention | decision | gotcha | architecture | glossary | attempt",
|
|
@@ -4974,38 +4393,6 @@ function createHaiveServer(options = {}) {
|
|
|
4974
4393
|
return jsonResult(await ingestFindings(input, context));
|
|
4975
4394
|
}
|
|
4976
4395
|
);
|
|
4977
|
-
registerTool(
|
|
4978
|
-
"mem_observe",
|
|
4979
|
-
[
|
|
4980
|
-
"Capture a code-level discovery made WHILE READING existing code.",
|
|
4981
|
-
"",
|
|
4982
|
-
"USE THIS when you read a file and spot something the team may not know about:",
|
|
4983
|
-
" - A bug or race condition hiding in the code",
|
|
4984
|
-
" - A security gap or missing validation",
|
|
4985
|
-
" - An inconsistency between two files",
|
|
4986
|
-
" - A missing configuration or environment variable",
|
|
4987
|
-
" - Anything that could silently break in production",
|
|
4988
|
-
"",
|
|
4989
|
-
"DIFFERENCE from mem_save: mem_observe is for REACTIVE discoveries during code",
|
|
4990
|
-
"reading. mem_save is for deliberate knowledge capture (conventions, decisions).",
|
|
4991
|
-
"",
|
|
4992
|
-
"Auto-validated, anchored to file paths for staleness detection.",
|
|
4993
|
-
"",
|
|
4994
|
-
"PARAMETERS:",
|
|
4995
|
-
" what \u2014 one-line title (e.g. 'MobilePaymentController: duplicate @RequestBody')",
|
|
4996
|
-
" where \u2014 file path(s) where the issue lives",
|
|
4997
|
-
" impact \u2014 what breaks or could break because of this",
|
|
4998
|
-
" fix \u2014 suggested fix (optional)",
|
|
4999
|
-
" scope \u2014 team (default, since discoveries benefit everyone)",
|
|
5000
|
-
"",
|
|
5001
|
-
"RETURNS: { id, file_path }"
|
|
5002
|
-
].join("\n"),
|
|
5003
|
-
MemObserveInputSchema,
|
|
5004
|
-
async (input) => {
|
|
5005
|
-
tracker.record("mem_observe", input.where);
|
|
5006
|
-
return jsonResult(await memObserve(input, context));
|
|
5007
|
-
}
|
|
5008
|
-
);
|
|
5009
4396
|
registerTool(
|
|
5010
4397
|
"mem_session_end",
|
|
5011
4398
|
[
|
|
@@ -5465,26 +4852,6 @@ function createHaiveServer(options = {}) {
|
|
|
5465
4852
|
return jsonResult(await codeSearch(input, context));
|
|
5466
4853
|
}
|
|
5467
4854
|
);
|
|
5468
|
-
registerTool(
|
|
5469
|
-
"why_this_file",
|
|
5470
|
-
[
|
|
5471
|
-
"One-shot file-context lookup: combines recent git history, memories anchored",
|
|
5472
|
-
"to the path, and the code-map entry. Answers 'why is this file the way it is?'",
|
|
5473
|
-
"in a single call instead of 3-4 manual ones.",
|
|
5474
|
-
"",
|
|
5475
|
-
"PARAMETERS:",
|
|
5476
|
-
" path \u2014 project-relative path (required)",
|
|
5477
|
-
" git_log_limit \u2014 recent commits to include (default 5)",
|
|
5478
|
-
" memory_limit \u2014 anchored memories cap (default 5)",
|
|
5479
|
-
"",
|
|
5480
|
-
"RETURNS: { file, exists, recent_commits: [...], memories: [...], code_map_entry, hints? }"
|
|
5481
|
-
].join("\n"),
|
|
5482
|
-
WhyThisFileInputSchema,
|
|
5483
|
-
async (input) => {
|
|
5484
|
-
tracker.record("why_this_file", input.path);
|
|
5485
|
-
return jsonResult(await whyThisFile(input, context));
|
|
5486
|
-
}
|
|
5487
|
-
);
|
|
5488
4855
|
registerTool(
|
|
5489
4856
|
"anti_patterns_check",
|
|
5490
4857
|
[
|
|
@@ -5534,54 +4901,6 @@ function createHaiveServer(options = {}) {
|
|
|
5534
4901
|
return jsonResult(await memDistill(input, context));
|
|
5535
4902
|
}
|
|
5536
4903
|
);
|
|
5537
|
-
registerTool(
|
|
5538
|
-
"why_this_decision",
|
|
5539
|
-
[
|
|
5540
|
-
"Trace the genealogy of a memory (especially decision/architecture):",
|
|
5541
|
-
"the memory itself + memories explicitly linked via related_ids + memories",
|
|
5542
|
-
"anchored to overlapping paths + recent commits touching those paths.",
|
|
5543
|
-
"",
|
|
5544
|
-
"USE WHEN you find a memory and need to understand WHY it was made and",
|
|
5545
|
-
"what surrounds it. One call instead of 4-5 manual lookups.",
|
|
5546
|
-
"",
|
|
5547
|
-
"PARAMETERS:",
|
|
5548
|
-
" id \u2014 memory id (required)",
|
|
5549
|
-
" git_log_limit \u2014 how many recent commits per anchor path (default 5)",
|
|
5550
|
-
"",
|
|
5551
|
-
"RETURNS: { decision, related: [...], path_neighbors: [...], recent_commits: [...] }"
|
|
5552
|
-
].join("\n"),
|
|
5553
|
-
WhyThisDecisionInputSchema,
|
|
5554
|
-
async (input) => {
|
|
5555
|
-
tracker.record("why_this_decision", input.id);
|
|
5556
|
-
return jsonResult(await whyThisDecision(input, context));
|
|
5557
|
-
}
|
|
5558
|
-
);
|
|
5559
|
-
registerTool(
|
|
5560
|
-
"mem_conflicts_with",
|
|
5561
|
-
[
|
|
5562
|
-
"Detect memories that potentially CONTRADICT a given memory.",
|
|
5563
|
-
"",
|
|
5564
|
-
"USE BEFORE relying on a memory's advice \u2014 surfaces 'another memory says",
|
|
5565
|
-
"the opposite'. Detection uses several heuristics layered together:",
|
|
5566
|
-
"",
|
|
5567
|
-
" 1. Opposite status \u2014 validated vs rejected on overlapping topic",
|
|
5568
|
-
" 2. attempt-vs-convention on overlapping anchor paths",
|
|
5569
|
-
" 3. Polarity keywords \u2014 'use X' vs 'do not use X' among semantic neighbors",
|
|
5570
|
-
" 4. Explicit #contradicts:<id> tags in either body",
|
|
5571
|
-
"",
|
|
5572
|
-
"PARAMETERS:",
|
|
5573
|
-
" id \u2014 memory id to check (required)",
|
|
5574
|
-
" min_score \u2014 minimum cosine similarity for semantic neighbors (default 0.5)",
|
|
5575
|
-
" semantic \u2014 use embeddings (default true)",
|
|
5576
|
-
"",
|
|
5577
|
-
"RETURNS: { found, target, scanned, conflicts: [{ id, reasons, similarity, ... }] }"
|
|
5578
|
-
].join("\n"),
|
|
5579
|
-
MemConflictsInputSchema,
|
|
5580
|
-
async (input) => {
|
|
5581
|
-
tracker.record("mem_conflicts_with", input.id);
|
|
5582
|
-
return jsonResult(await memConflicts(input, context));
|
|
5583
|
-
}
|
|
5584
|
-
);
|
|
5585
4904
|
registerTool(
|
|
5586
4905
|
"mem_conflict_candidates",
|
|
5587
4906
|
[
|
|
@@ -5590,7 +4909,7 @@ function createHaiveServer(options = {}) {
|
|
|
5590
4909
|
" 1. Lexical similarity (Jaccard) on decision/architecture-like pairs",
|
|
5591
4910
|
" 2. Same frontmatter.topic with validated vs rejected \u2014 quick human-review signal",
|
|
5592
4911
|
"",
|
|
5593
|
-
"Advisory only \u2014
|
|
4912
|
+
"Advisory only \u2014 review the listed candidate ids, then resolve with mem_update/mem_delete.",
|
|
5594
4913
|
"",
|
|
5595
4914
|
"PARAMETERS:",
|
|
5596
4915
|
" since_days, types, min_jaccard, max_pairs, max_scan, max_topic_pairs",
|
|
@@ -5603,32 +4922,6 @@ function createHaiveServer(options = {}) {
|
|
|
5603
4922
|
return jsonResult(await memConflictCandidates(input, context));
|
|
5604
4923
|
}
|
|
5605
4924
|
);
|
|
5606
|
-
registerTool(
|
|
5607
|
-
"runtime_journal_append",
|
|
5608
|
-
[
|
|
5609
|
-
"Append one line to `.ai/.runtime/session-journal.ndjson` \u2014 machine-local session continuity.",
|
|
5610
|
-
"",
|
|
5611
|
-
"Does NOT replace team memories; complements mem_session_end recaps for local traces.",
|
|
5612
|
-
"",
|
|
5613
|
-
"PARAMETERS: message, kind (note|session_end|mcp), optional tool",
|
|
5614
|
-
"",
|
|
5615
|
-
"RETURNS: { ok, path_hint }"
|
|
5616
|
-
].join("\n"),
|
|
5617
|
-
RuntimeJournalAppendInputSchema,
|
|
5618
|
-
async (input) => jsonResult(await runtimeJournalAppend(input, context))
|
|
5619
|
-
);
|
|
5620
|
-
registerTool(
|
|
5621
|
-
"runtime_journal_tail",
|
|
5622
|
-
[
|
|
5623
|
-
"Read the last N entries from the runtime session journal (parsed JSON lines).",
|
|
5624
|
-
"",
|
|
5625
|
-
"PARAMETERS: limit (default 30, max 500)",
|
|
5626
|
-
"",
|
|
5627
|
-
"RETURNS: { entries: [...], empty?: true }"
|
|
5628
|
-
].join("\n"),
|
|
5629
|
-
RuntimeJournalTailInputSchema,
|
|
5630
|
-
async (input) => jsonResult(await runtimeJournalTail(input, context))
|
|
5631
|
-
);
|
|
5632
4925
|
registerTool(
|
|
5633
4926
|
"pre_commit_check",
|
|
5634
4927
|
[
|
|
@@ -5655,37 +4948,6 @@ function createHaiveServer(options = {}) {
|
|
|
5655
4948
|
return jsonResult(await preCommitCheck(input, context));
|
|
5656
4949
|
}
|
|
5657
4950
|
);
|
|
5658
|
-
registerTool(
|
|
5659
|
-
"pattern_detect",
|
|
5660
|
-
[
|
|
5661
|
-
"Heuristic memory detector \u2014 finds knowledge worth saving WITHOUT calling an LLM.",
|
|
5662
|
-
"",
|
|
5663
|
-
"Runs three signals over local git history and the tool-usage log:",
|
|
5664
|
-
" 1. CONFIG_CHANGE \u2014 config files modified recently (tsconfig, eslint, prettier, \u2026)",
|
|
5665
|
-
" \u2192 proposes a convention memory with the git diff as body.",
|
|
5666
|
-
" 2. REPEATED_PATH \u2014 same file appears \u22653\xD7 in mem_tried/mem_observe events",
|
|
5667
|
-
" \u2192 proposes a gotcha memory anchored to that path.",
|
|
5668
|
-
" 3. HOT_FILE \u2014 source file referenced \u22653\xD7 in writing-tool events",
|
|
5669
|
-
" \u2192 proposes a convention memory (frequent edits = pattern emerging).",
|
|
5670
|
-
"",
|
|
5671
|
-
"Saves memories with status='proposed'. They feed into auto-promote (Phase 4)",
|
|
5672
|
-
"or are surfaced in the next post_task distillation for LLM review.",
|
|
5673
|
-
"",
|
|
5674
|
-
"USE periodically (e.g. end of sprint) or trigger from post-commit hook.",
|
|
5675
|
-
"",
|
|
5676
|
-
"PARAMETERS:",
|
|
5677
|
-
" since_days \u2014 look-back window in days (default 7)",
|
|
5678
|
-
" dry_run \u2014 report matches without saving (default false)",
|
|
5679
|
-
" scope \u2014 'team' (default) | 'personal'",
|
|
5680
|
-
"",
|
|
5681
|
-
"RETURNS: { scanned_events, matches: [{kind, signal, proposed_type, \u2026}], saved, saved_ids }"
|
|
5682
|
-
].join("\n"),
|
|
5683
|
-
PatternDetectInputSchema,
|
|
5684
|
-
async (input) => {
|
|
5685
|
-
tracker.record("pattern_detect", `since=${input.since_days}d/dry_run=${input.dry_run}`);
|
|
5686
|
-
return jsonResult(await patternDetect(input, context));
|
|
5687
|
-
}
|
|
5688
|
-
);
|
|
5689
4951
|
registerTool(
|
|
5690
4952
|
"mem_diff",
|
|
5691
4953
|
[
|
|
@@ -5736,7 +4998,7 @@ function createHaiveServer(options = {}) {
|
|
|
5736
4998
|
[
|
|
5737
4999
|
"\u2B50 Post-task reflection \u2014 run at the end of every session to capture what you learned:",
|
|
5738
5000
|
"failed approaches (mem_tried), new conventions/decisions/gotchas (mem_save),",
|
|
5739
|
-
"
|
|
5001
|
+
"failed approaches (mem_tried), and an end-of-session recap (mem_session_end).",
|
|
5740
5002
|
"In autopilot mode a minimal recap saves automatically; calling this produces a richer one."
|
|
5741
5003
|
].join(" "),
|
|
5742
5004
|
PostTaskArgsSchema,
|