@kage-core/kage-graph-mcp 1.1.12 → 1.1.14

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/kernel.js CHANGED
@@ -40,6 +40,7 @@ exports.pendingDir = pendingDir;
40
40
  exports.publicCandidatesDir = publicCandidatesDir;
41
41
  exports.indexesDir = indexesDir;
42
42
  exports.graphDir = graphDir;
43
+ exports.graphRegistryDir = graphRegistryDir;
43
44
  exports.codeGraphDir = codeGraphDir;
44
45
  exports.branchesDir = branchesDir;
45
46
  exports.reviewDir = reviewDir;
@@ -62,6 +63,7 @@ exports.catalogDomainNodeCount = catalogDomainNodeCount;
62
63
  exports.ensureMemoryDirs = ensureMemoryDirs;
63
64
  exports.loadApprovedPackets = loadApprovedPackets;
64
65
  exports.loadPendingPackets = loadPendingPackets;
66
+ exports.writeLspSymbolIndex = writeLspSymbolIndex;
65
67
  exports.buildCodeGraph = buildCodeGraph;
66
68
  exports.buildKnowledgeGraph = buildKnowledgeGraph;
67
69
  exports.buildIndexes = buildIndexes;
@@ -74,6 +76,8 @@ exports.queryCodeGraph = queryCodeGraph;
74
76
  exports.queryGraph = queryGraph;
75
77
  exports.graphMermaid = graphMermaid;
76
78
  exports.kageMetrics = kageMetrics;
79
+ exports.auditProject = auditProject;
80
+ exports.memoryInbox = memoryInbox;
77
81
  exports.qualityReport = qualityReport;
78
82
  exports.benchmarkProject = benchmarkProject;
79
83
  exports.benchmarkTaskComparison = benchmarkTaskComparison;
@@ -119,11 +123,16 @@ exports.MEMORY_TYPES = [
119
123
  "runbook",
120
124
  "bug_fix",
121
125
  "decision",
126
+ "rationale",
122
127
  "convention",
123
128
  "workflow",
124
129
  "gotcha",
125
130
  "reference",
126
131
  "policy",
132
+ "issue_context",
133
+ "code_explanation",
134
+ "negative_result",
135
+ "constraint",
127
136
  ];
128
137
  exports.SETUP_AGENTS = [
129
138
  "codex",
@@ -174,10 +183,13 @@ Capture examples:
174
183
  - A bug cause and verified fix.
175
184
  - A convention future agents should follow.
176
185
  - A decision and its rationale.
186
+ - Why code, architecture, product, or release behavior ended up this way.
187
+ - A non-obvious issue state, failed approach, or code explanation.
177
188
  - A gotcha that caused rediscovery or wasted time.
178
189
  - A path-specific workflow or dependency relationship.
179
190
 
180
- Keep captures concise and future-facing. Do not store raw transcripts.
191
+ Keep captures concise, source-backed, and useful for future understanding,
192
+ decisions, debugging, explanation, or action. Do not store raw transcripts.
181
193
 
182
194
  ## End-Of-Task Proposal
183
195
 
@@ -264,6 +276,9 @@ function indexesDir(projectDir) {
264
276
  function graphDir(projectDir) {
265
277
  return (0, node_path_1.join)(memoryRoot(projectDir), "graph");
266
278
  }
279
+ function graphRegistryDir(projectDir) {
280
+ return (0, node_path_1.join)(memoryRoot(projectDir), "graph_registry");
281
+ }
267
282
  function codeGraphDir(projectDir) {
268
283
  return (0, node_path_1.join)(memoryRoot(projectDir), "code_graph");
269
284
  }
@@ -526,7 +541,7 @@ function evaluateMemoryQuality(projectDir, packet) {
526
541
  const bodyTokens = tokenize(packet.body);
527
542
  const hasEvidence = packet.source_refs.length > 0;
528
543
  const hasPaths = packet.paths.length > 0;
529
- const highValueType = ["runbook", "bug_fix", "decision", "convention", "workflow", "gotcha", "policy"].includes(packet.type);
544
+ const highValueType = ["runbook", "bug_fix", "decision", "rationale", "convention", "workflow", "gotcha", "policy", "issue_context", "code_explanation", "negative_result", "constraint"].includes(packet.type);
530
545
  if (highValueType) {
531
546
  score += 14;
532
547
  reasons.push("high-value memory type");
@@ -589,7 +604,7 @@ function evaluateMemoryAdmission(projectDir, packet) {
589
604
  const risks = [];
590
605
  const text = `${packet.title}\n${packet.summary}\n${packet.body}`.toLowerCase();
591
606
  let score = 0;
592
- if (["runbook", "bug_fix", "decision", "convention", "workflow", "gotcha", "policy"].includes(packet.type)) {
607
+ if (["runbook", "bug_fix", "decision", "rationale", "convention", "workflow", "gotcha", "policy", "issue_context", "code_explanation", "negative_result", "constraint"].includes(packet.type)) {
593
608
  score += 18;
594
609
  reasons.push("durable memory type");
595
610
  }
@@ -601,9 +616,9 @@ function evaluateMemoryAdmission(projectDir, packet) {
601
616
  score += 12;
602
617
  reasons.push("repo scoped or path grounded");
603
618
  }
604
- if (/(when|after|before|because|requires|must|avoid|prefer|use this|run this|root cause|decision|convention|gotcha|workaround|fix|policy)/i.test(packet.body)) {
619
+ if (/(when|after|before|because|requires|must|avoid|prefer|use this|run this|root cause|rationale|decision|convention|gotcha|workaround|fix|policy|issue|hypothesis|unresolved|explains?|data flow|invariant|coupling|constraint)/i.test(packet.body)) {
605
620
  score += 18;
606
- reasons.push("has future trigger or rationale");
621
+ reasons.push("has durable trigger, rationale, issue context, or explanation");
607
622
  }
608
623
  if (/(verified by|evidence:|test passed|reproduced|root cause)/i.test(packet.body)) {
609
624
  score += 10;
@@ -986,6 +1001,15 @@ function createRepoOverviewPacket(projectDir) {
986
1001
  ...((0, node_fs_1.existsSync)(packagePath) ? [{ kind: "file", path: "package.json" }] : []),
987
1002
  ...((0, node_fs_1.existsSync)(readmePath) ? [{ kind: "file", path: "README.md" }] : []),
988
1003
  ],
1004
+ context: {
1005
+ fact: "Generated repo overview summarizes package metadata and the README as a navigation aid for agent startup.",
1006
+ why: "Agents need fast repo orientation before deeper recall or code graph queries, but generated overview memory should stay separate from human rationale.",
1007
+ trigger: "Recall when an agent needs first-pass repo purpose, scripts, stack, or README context.",
1008
+ action: "Use this as orientation only, then inspect source-backed memory and code graph facts for implementation decisions.",
1009
+ verification: "Generated from package.json and README.md when present.",
1010
+ risk_if_forgotten: "Agents may waste context rediscovering basic repo purpose or treat generated overview text as deeper semantic memory.",
1011
+ stale_when: "package.json or README.md changes enough that the generated overview no longer matches the repo.",
1012
+ },
989
1013
  freshness: {
990
1014
  ttl_days: 90,
991
1015
  last_verified_at: createdAt.slice(0, 10),
@@ -1068,6 +1092,15 @@ function createRepoStructurePacket(projectDir) {
1068
1092
  paths: existing.filter((entry) => pathExistsInRepo(projectDir, entry)),
1069
1093
  stack: [],
1070
1094
  source_refs: existing.map((path) => ({ kind: "file", path })),
1095
+ context: {
1096
+ fact: "Generated repo structure summarizes top-level files, workflows, and test files as a navigation aid.",
1097
+ why: "Agents need a quick map of repo entry points before choosing which files, workflows, or tests to inspect.",
1098
+ trigger: "Recall when orienting to this repo's layout, CI workflows, or test locations.",
1099
+ action: "Use this as a starting map and verify details against the current filesystem or code graph before editing.",
1100
+ verification: "Generated from files present in the repository.",
1101
+ risk_if_forgotten: "Agents may miss important entry points such as AGENTS.md, workflows, or MCP tests during initial orientation.",
1102
+ stale_when: "Top-level repo structure, workflow files, or test files change.",
1103
+ },
1071
1104
  freshness: {
1072
1105
  ttl_days: 30,
1073
1106
  last_verified_at: createdAt.slice(0, 10),
@@ -1102,7 +1135,7 @@ function upsertGeneratedPacket(projectDir, packet) {
1102
1135
  if (existing && existing.quality?.reviewer !== "kage-indexer")
1103
1136
  return;
1104
1137
  if (existing) {
1105
- const comparableFields = ["title", "summary", "body", "tags", "paths", "stack", "source_refs", "freshness"];
1138
+ const comparableFields = ["title", "summary", "body", "tags", "paths", "stack", "source_refs", "context", "freshness"];
1106
1139
  const same = comparableFields.every((field) => JSON.stringify(existing[field]) === JSON.stringify(packet[field]));
1107
1140
  if (same)
1108
1141
  return;
@@ -1170,6 +1203,9 @@ function packetGroundingWarnings(projectDir, packet, source) {
1170
1203
  const hasGroundedSource = packet.source_refs.some((ref) => {
1171
1204
  if (typeof ref.path === "string")
1172
1205
  return !shouldSkipRepoMemoryPath(ref.path) && pathExistsInRepo(projectDir, ref.path);
1206
+ if (Array.isArray(ref.changed_files)) {
1207
+ return ref.changed_files.some((path) => typeof path === "string" && !shouldSkipRepoMemoryPath(path) && pathExistsInRepo(projectDir, path));
1208
+ }
1173
1209
  if (typeof ref.kind === "string" && ["explicit_capture", "local_public_candidate"].includes(ref.kind))
1174
1210
  return true;
1175
1211
  return typeof ref.url === "string";
@@ -1894,6 +1930,54 @@ function parseLspDocumentSymbols(projectDir, path) {
1894
1930
  }
1895
1931
  return { symbols, imports: [], calls: [] };
1896
1932
  }
1933
+ function writeLspSymbolIndex(projectDir) {
1934
+ ensureMemoryDirs(projectDir);
1935
+ const outDir = (0, node_path_1.join)(memoryRoot(projectDir), "code_index");
1936
+ ensureDir(outDir);
1937
+ const outPath = (0, node_path_1.join)(outDir, "lsp-symbols.json");
1938
+ const documents = [];
1939
+ let symbolCount = 0;
1940
+ const errors = [];
1941
+ for (const absolutePath of listCodeFiles(projectDir)) {
1942
+ const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
1943
+ if (!TS_AST_EXTENSIONS.has(extensionOf(rel)))
1944
+ continue;
1945
+ try {
1946
+ const content = (0, node_fs_1.readFileSync)(absolutePath, "utf8");
1947
+ const symbols = extractSymbols(rel, content).map((symbol) => ({
1948
+ name: symbol.name,
1949
+ kind: symbol.kind,
1950
+ detail: symbol.signature,
1951
+ range: {
1952
+ start: { line: Math.max(0, symbol.line - 1), character: 0 },
1953
+ end: { line: Math.max(0, (symbol.end_line ?? symbol.line) - 1), character: 0 },
1954
+ },
1955
+ }));
1956
+ if (!symbols.length)
1957
+ continue;
1958
+ symbolCount += symbols.length;
1959
+ documents.push({ path: rel, symbols });
1960
+ }
1961
+ catch (error) {
1962
+ errors.push(`${rel}: ${error instanceof Error ? error.message : String(error)}`);
1963
+ }
1964
+ }
1965
+ writeJson(outPath, {
1966
+ schema_version: 1,
1967
+ generator: "kage-lsp-symbol-index",
1968
+ generated_at: nowIso(),
1969
+ documents,
1970
+ });
1971
+ return {
1972
+ ok: errors.length === 0,
1973
+ project_dir: projectDir,
1974
+ path: outPath,
1975
+ parser: "lsp",
1976
+ documents: documents.length,
1977
+ symbols: symbolCount,
1978
+ errors,
1979
+ };
1980
+ }
1897
1981
  function parseLsif(projectDir, path) {
1898
1982
  const docs = new Map();
1899
1983
  const ranges = new Map();
@@ -2030,9 +2114,14 @@ function buildCodeGraph(projectDir) {
2030
2114
  const addSymbol = (symbol) => {
2031
2115
  if (!fileByPath.has(symbol.path))
2032
2116
  return;
2033
- if (symbols.some((existing) => existing.id === symbol.id))
2034
- return;
2035
2117
  const file = fileByPath.get(symbol.path);
2118
+ const existing = symbols.find((candidate) => candidate.id === symbol.id);
2119
+ if (existing) {
2120
+ existing.parser = strongerParser(existing.parser, symbol.parser);
2121
+ if (file)
2122
+ file.parser = strongerParser(file.parser, symbol.parser);
2123
+ return;
2124
+ }
2036
2125
  if (file)
2037
2126
  file.parser = strongerParser(file.parser, symbol.parser);
2038
2127
  symbols.push(symbol);
@@ -2105,6 +2194,7 @@ function buildKnowledgeGraph(projectDir) {
2105
2194
  const episodes = [];
2106
2195
  const repoEntityId = graphEntityId("repo", repoKey(projectDir));
2107
2196
  const generatedFrom = packets.map((packet) => packet.updated_at).sort().at(-1) ?? null;
2197
+ const codeGraph = buildCodeGraph(projectDir);
2108
2198
  addEntity(entities, {
2109
2199
  id: repoEntityId,
2110
2200
  type: "repo",
@@ -2270,6 +2360,118 @@ function buildKnowledgeGraph(projectDir) {
2270
2360
  evidence: [episodeId],
2271
2361
  });
2272
2362
  }
2363
+ const context = engineeringContextFor(packet);
2364
+ if (context.verification) {
2365
+ const command = normalizeCommandText(context.verification);
2366
+ if (command) {
2367
+ const commandId = graphEntityId("command", command);
2368
+ addEntity(entities, {
2369
+ id: commandId,
2370
+ type: "command",
2371
+ name: command,
2372
+ summary: `Verification command from structured memory context.`,
2373
+ first_seen_at: packet.created_at,
2374
+ last_seen_at: packet.updated_at,
2375
+ evidence: [episodeId],
2376
+ });
2377
+ addEdge(edges, {
2378
+ from: memoryId,
2379
+ to: commandId,
2380
+ relation: "verified_by",
2381
+ fact: `"${packet.title}" is verified by "${command}".`,
2382
+ confidence: packet.confidence,
2383
+ valid_from: packet.updated_at,
2384
+ invalidated_at: null,
2385
+ branch,
2386
+ commit: head,
2387
+ evidence: [episodeId],
2388
+ });
2389
+ }
2390
+ }
2391
+ const packetTextLower = `${packet.title}\n${packet.summary}\n${packet.body}`.toLowerCase();
2392
+ const packetPathSet = new Set(packet.paths);
2393
+ const symbolRelation = packet.type === "bug_fix"
2394
+ ? "fixes_symbol"
2395
+ : packet.type === "decision" || packet.type === "rationale" || packet.type === "constraint"
2396
+ ? "informs_symbol"
2397
+ : "explains_symbol";
2398
+ for (const symbol of codeGraph.symbols.filter((symbol) => packetPathSet.has(symbol.path))) {
2399
+ if (packet.type !== "code_explanation" && !packetTextLower.includes(symbol.name.toLowerCase()))
2400
+ continue;
2401
+ const symbolEntityId = graphEntityId("symbol", symbol.id);
2402
+ addEntity(entities, {
2403
+ id: symbolEntityId,
2404
+ type: "symbol",
2405
+ name: symbol.name,
2406
+ aliases: [symbol.id, symbol.path],
2407
+ summary: `${symbol.kind} in ${symbol.path}:${symbol.line}`,
2408
+ first_seen_at: packet.created_at,
2409
+ last_seen_at: packet.updated_at,
2410
+ evidence: [episodeId],
2411
+ });
2412
+ addEdge(edges, {
2413
+ from: memoryId,
2414
+ to: symbolEntityId,
2415
+ relation: symbolRelation,
2416
+ fact: `"${packet.title}" ${symbolRelation.replace(/_/g, " ")} ${symbol.name} in ${symbol.path}.`,
2417
+ confidence: packet.confidence,
2418
+ valid_from: packet.updated_at,
2419
+ invalidated_at: null,
2420
+ branch,
2421
+ commit: head,
2422
+ evidence: [episodeId],
2423
+ });
2424
+ }
2425
+ for (const route of codeGraph.routes.filter((route) => packetPathSet.has(route.file_path) && packetTextLower.includes(route.path.toLowerCase()))) {
2426
+ const routeEntityId = graphEntityId("route", route.id);
2427
+ addEntity(entities, {
2428
+ id: routeEntityId,
2429
+ type: "route",
2430
+ name: `${route.method} ${route.path}`,
2431
+ aliases: [route.id, route.file_path],
2432
+ summary: `${route.framework} route in ${route.file_path}:${route.line}`,
2433
+ first_seen_at: packet.created_at,
2434
+ last_seen_at: packet.updated_at,
2435
+ evidence: [episodeId],
2436
+ });
2437
+ addEdge(edges, {
2438
+ from: memoryId,
2439
+ to: routeEntityId,
2440
+ relation: "applies_to_route",
2441
+ fact: `"${packet.title}" applies to route ${route.method} ${route.path}.`,
2442
+ confidence: packet.confidence,
2443
+ valid_from: packet.updated_at,
2444
+ invalidated_at: null,
2445
+ branch,
2446
+ commit: head,
2447
+ evidence: [episodeId],
2448
+ });
2449
+ }
2450
+ for (const test of codeGraph.tests.filter((test) => packetPathSet.has(test.test_path) || Boolean(test.covers_path && packetPathSet.has(test.covers_path)))) {
2451
+ const testEntityId = graphEntityId("test", test.test_symbol);
2452
+ addEntity(entities, {
2453
+ id: testEntityId,
2454
+ type: "test",
2455
+ name: test.title,
2456
+ aliases: [test.test_symbol, test.test_path],
2457
+ summary: `Test in ${test.test_path}:${test.line}${test.covers_symbol ? ` covers ${test.covers_symbol}` : ""}`,
2458
+ first_seen_at: packet.created_at,
2459
+ last_seen_at: packet.updated_at,
2460
+ evidence: [episodeId],
2461
+ });
2462
+ addEdge(edges, {
2463
+ from: memoryId,
2464
+ to: testEntityId,
2465
+ relation: "verified_by_test",
2466
+ fact: `"${packet.title}" is related to test "${test.title}".`,
2467
+ confidence: packet.confidence,
2468
+ valid_from: packet.updated_at,
2469
+ invalidated_at: null,
2470
+ branch,
2471
+ commit: head,
2472
+ evidence: [episodeId],
2473
+ });
2474
+ }
2273
2475
  }
2274
2476
  const manifestCommands = npmScriptCommands(projectDir);
2275
2477
  if (manifestCommands.length) {
@@ -2702,38 +2904,148 @@ function scorePacket(queryTerms, packet) {
2702
2904
  score += 1;
2703
2905
  return { score, why: unique(why).slice(0, 8) };
2704
2906
  }
2907
+ const BM25_K1 = 1.2;
2908
+ const BM25_B = 0.75;
2909
+ const BM25_FIELD_WEIGHTS = {
2910
+ title: 4,
2911
+ summary: 2.4,
2912
+ tag: 2.8,
2913
+ path: 2.4,
2914
+ type: 1.8,
2915
+ body: 1,
2916
+ };
2917
+ function lexicalStem(term) {
2918
+ if (term.length > 5 && term.endsWith("ing"))
2919
+ return term.slice(0, -3);
2920
+ if (term.length > 4 && term.endsWith("ies"))
2921
+ return `${term.slice(0, -3)}y`;
2922
+ if (term.length > 4 && term.endsWith("es"))
2923
+ return term.slice(0, -2);
2924
+ if (term.length > 3 && term.endsWith("s"))
2925
+ return term.slice(0, -1);
2926
+ return term;
2927
+ }
2928
+ function expandQueryTerms(terms) {
2929
+ return unique(terms.flatMap((term) => unique([term, lexicalStem(term)].filter(Boolean))));
2930
+ }
2931
+ function bm25Document(packet) {
2932
+ const termFrequency = new Map();
2933
+ const fieldHits = new Map();
2934
+ let length = 0;
2935
+ const addField = (field, text) => {
2936
+ const weight = BM25_FIELD_WEIGHTS[field];
2937
+ for (const token of tokenize(text)) {
2938
+ termFrequency.set(token, (termFrequency.get(token) ?? 0) + weight);
2939
+ if (!fieldHits.has(token))
2940
+ fieldHits.set(token, new Set());
2941
+ fieldHits.get(token).add(field);
2942
+ length += weight;
2943
+ }
2944
+ };
2945
+ addField("title", packet.title);
2946
+ addField("summary", packet.summary);
2947
+ addField("tag", packet.tags.join(" "));
2948
+ addField("path", packet.paths.join(" "));
2949
+ addField("type", packet.type);
2950
+ addField("body", packet.body);
2951
+ return { packet, termFrequency, fieldHits, length: Math.max(1, length) };
2952
+ }
2953
+ function scorePacketsBm25(queryTerms, packets) {
2954
+ const terms = expandQueryTerms(queryTerms);
2955
+ const documents = packets.map(bm25Document);
2956
+ const result = new Map();
2957
+ if (!terms.length || !documents.length)
2958
+ return result;
2959
+ const averageLength = documents.reduce((sum, document) => sum + document.length, 0) / documents.length || 1;
2960
+ const documentFrequency = new Map();
2961
+ for (const term of terms) {
2962
+ documentFrequency.set(term, documents.filter((document) => document.termFrequency.has(term)).length);
2963
+ }
2964
+ for (const document of documents) {
2965
+ let score = 0;
2966
+ const why = [];
2967
+ for (const term of terms) {
2968
+ const termFrequency = document.termFrequency.get(term) ?? 0;
2969
+ if (termFrequency <= 0)
2970
+ continue;
2971
+ const df = documentFrequency.get(term) ?? 0;
2972
+ const idf = Math.log(1 + (documents.length - df + 0.5) / (df + 0.5));
2973
+ const denominator = termFrequency + BM25_K1 * (1 - BM25_B + BM25_B * (document.length / averageLength));
2974
+ score += idf * ((termFrequency * (BM25_K1 + 1)) / denominator);
2975
+ const fields = Array.from(document.fieldHits.get(term) ?? []).sort();
2976
+ if (fields.length)
2977
+ why.push(`bm25:${fields.join("+")}:${term}`);
2978
+ }
2979
+ if (score > 0)
2980
+ result.set(document.packet.id, { score: Number(score.toFixed(2)), why: unique(why).slice(0, 8) });
2981
+ }
2982
+ return result;
2983
+ }
2984
+ function recallIntentBoost(queryTerms, packet) {
2985
+ const terms = new Set(expandQueryTerms(queryTerms));
2986
+ const commandIntent = ["run", "test", "tests", "build", "command", "commands"].some((term) => terms.has(term));
2987
+ const debugIntent = ["bug", "fix", "error", "fail", "debug"].some((term) => terms.has(term));
2988
+ const gotchaIntent = terms.has("gotcha");
2989
+ const decisionIntent = terms.has("decision");
2990
+ const packetText = `${packet.title}\n${packet.summary}\n${packet.body}\n${packet.tags.join(" ")}`;
2991
+ const hasCommandEvidence = /\b(?:npm|pnpm|yarn|bun|node|python|pytest|vitest|cargo|go)\s+(?:run\s+)?(?:test|tests|build|dev|start)\b|package\.json|scripts?/i.test(packetText);
2992
+ let score = 0;
2993
+ if (commandIntent) {
2994
+ if (packet.type === "runbook")
2995
+ score += hasCommandEvidence ? 22 : 8;
2996
+ if (packet.type === "repo_map" && hasCommandEvidence)
2997
+ score += 34;
2998
+ if (!["runbook", "repo_map", "workflow"].includes(packet.type) && !debugIntent)
2999
+ score -= 8;
3000
+ if (packet.type === "decision" && /release|verified by|passed|published/i.test(`${packet.title}\n${packet.body}`))
3001
+ score -= 3;
3002
+ }
3003
+ if (debugIntent && packet.type === "bug_fix")
3004
+ score += 10;
3005
+ if (gotchaIntent)
3006
+ score += packet.type === "gotcha" ? 18 : -4;
3007
+ if (decisionIntent)
3008
+ score += packet.type === "decision" ? 12 : 0;
3009
+ return score;
3010
+ }
2705
3011
  function recallBreakdown(projectDir, terms, packet, textScore) {
2706
3012
  const graph = buildKnowledgeGraph(projectDir);
2707
3013
  const packetEntityId = graph.entities.find((entity) => entity.type === "memory" && entity.aliases.includes(packet.id))?.id;
2708
- const graphScore = packetEntityId
3014
+ const rawGraphScore = packetEntityId
2709
3015
  ? graph.edges.filter((edge) => edge.from === packetEntityId || edge.to === packetEntityId).reduce((sum, edge) => sum + scoreText(terms, edge.fact), 0)
2710
3016
  : 0;
3017
+ const graphScore = Math.min(rawGraphScore * 0.45, textScore > 0 ? textScore * 1.5 + 12 : 8);
2711
3018
  const pathTypeTag = scoreText(terms, `${packet.type} ${packet.tags.join(" ")} ${packet.paths.join(" ")}`, [packet.type, ...packet.tags, ...packet.paths]);
3019
+ const intent = recallIntentBoost(terms, packet);
2712
3020
  const freshness = packet.status === "approved" ? 2 : packet.status === "pending" ? 0 : -5;
2713
3021
  const quality = Number(packet.quality.score ?? evaluateMemoryQuality(projectDir, packet).score) / 10;
2714
3022
  const feedback = packetFeedbackScore(packet);
2715
3023
  const vector = 0;
2716
- const final = Number((textScore + graphScore * 0.45 + pathTypeTag * 0.8 + vector + freshness + quality + feedback).toFixed(2));
2717
- return { text: textScore, graph: graphScore, path_type_tag: pathTypeTag, vector, freshness, quality: Number(quality.toFixed(2)), feedback, final };
3024
+ const final = Number((textScore + graphScore + pathTypeTag * 0.8 + intent + vector + freshness + quality + feedback).toFixed(2));
3025
+ return { bm25: textScore, text: textScore, graph: Number(graphScore.toFixed(2)), path_type_tag: pathTypeTag, intent, vector, freshness, quality: Number(quality.toFixed(2)), feedback, final };
2718
3026
  }
2719
3027
  function recall(projectDir, query, limit = 5, explain = false) {
2720
3028
  indexProject(projectDir);
2721
3029
  const terms = tokenize(query);
2722
- const scored = loadApprovedPackets(projectDir)
3030
+ const approvedPackets = loadApprovedPackets(projectDir);
3031
+ const lexicalScores = scorePacketsBm25(terms, approvedPackets);
3032
+ const scored = approvedPackets
2723
3033
  .map((packet) => {
2724
- const { score, why } = scorePacket(terms, packet);
3034
+ const { score, why } = lexicalScores.get(packet.id) ?? { score: 0, why: [] };
2725
3035
  const score_breakdown = recallBreakdown(projectDir, terms, packet, score);
2726
- const relevance = score + score_breakdown.graph + score_breakdown.path_type_tag + score_breakdown.vector;
2727
- return { packet, score: explain ? score_breakdown.final : score, relevance, why_matched: why, score_breakdown };
3036
+ const relevance = score + score_breakdown.graph + score_breakdown.path_type_tag + score_breakdown.intent + score_breakdown.vector;
3037
+ return { packet, score: score_breakdown.final, relevance, why_matched: why, score_breakdown };
2728
3038
  })
2729
3039
  .filter((entry) => entry.relevance > 0)
2730
3040
  .sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title))
2731
3041
  .slice(0, limit)
2732
3042
  .map(({ relevance, ...entry }) => entry);
2733
3043
  const pendingSeen = new Set();
2734
- const pendingScored = recallablePendingPackets(projectDir)
3044
+ const pendingPackets = recallablePendingPackets(projectDir);
3045
+ const pendingLexicalScores = scorePacketsBm25(terms, pendingPackets);
3046
+ const pendingScored = pendingPackets
2735
3047
  .map((packet) => {
2736
- const { score, why } = scorePacket(terms, packet);
3048
+ const { score, why } = pendingLexicalScores.get(packet.id) ?? { score: 0, why: [] };
2737
3049
  return { packet, score, why_matched: why };
2738
3050
  })
2739
3051
  .filter((entry) => entry.score > 0)
@@ -2788,7 +3100,7 @@ function recall(projectDir, query, limit = 5, explain = false) {
2788
3100
  ? scored.map((entry) => ({
2789
3101
  packet_id: entry.packet.id,
2790
3102
  title: entry.packet.title,
2791
- provider: "text",
3103
+ provider: "bm25",
2792
3104
  score_breakdown: entry.score_breakdown,
2793
3105
  why_matched: entry.why_matched,
2794
3106
  }))
@@ -3039,6 +3351,206 @@ function kageMetrics(projectDir) {
3039
3351
  },
3040
3352
  };
3041
3353
  }
3354
+ function auditProject(projectDir) {
3355
+ ensureMemoryDirs(projectDir);
3356
+ const validation = validateProject(projectDir);
3357
+ const quality = qualityReport(projectDir);
3358
+ const codeGraph = buildCodeGraph(projectDir);
3359
+ const knowledgeGraph = buildKnowledgeGraph(projectDir);
3360
+ const approved = loadApprovedPackets(projectDir);
3361
+ const pending = loadPendingPackets(projectDir);
3362
+ const structuredPackets = approved.filter(hasStructuredEngineeringContext);
3363
+ const preciseParsers = ["scip", "lsif", "lsp"];
3364
+ const astParsers = ["typescript-ast", "tree-sitter"];
3365
+ const indexableFiles = codeGraph.files.filter((file) => file.parser !== "metadata").length;
3366
+ const preciseFiles = codeGraph.files.filter((file) => preciseParsers.includes(file.parser)).length;
3367
+ const astFiles = codeGraph.files.filter((file) => astParsers.includes(file.parser)).length;
3368
+ const fallbackFiles = codeGraph.files.filter((file) => file.parser === "generic-static" || file.parser === "metadata").length;
3369
+ const memoryCodeEdges = knowledgeGraph.edges.filter((edge) => ["explains_symbol", "informs_symbol", "fixes_symbol", "applies_to_route", "verified_by_test"].includes(edge.relation)).length;
3370
+ const stalePackets = quality.totals.stale;
3371
+ const duplicateCandidatesTotal = quality.totals.duplicate;
3372
+ const structuredCoverage = percent(structuredPackets.length, approved.length);
3373
+ const preciseCoverage = percent(preciseFiles, indexableFiles);
3374
+ const memoryCodeCoverage = percent(Math.min(memoryCodeEdges, approved.length), approved.length);
3375
+ const recommendations = [];
3376
+ if (structuredPackets.length < approved.length) {
3377
+ recommendations.push("Add structured context fields to high-value memories: why, verification, risk_if_forgotten, and stale_when.");
3378
+ }
3379
+ if (pending.length) {
3380
+ recommendations.push("Review pending memory inbox packets and approve, reject, merge, or supersede them before handoff.");
3381
+ }
3382
+ if (stalePackets) {
3383
+ recommendations.push("Run kage gc --dry-run and update or deprecate stale memory before trusting recall.");
3384
+ }
3385
+ if (duplicateCandidatesTotal) {
3386
+ recommendations.push("Merge or supersede duplicate memory packets so agents do not receive conflicting context.");
3387
+ }
3388
+ if (preciseFiles < indexableFiles) {
3389
+ recommendations.push("Add or extend SCIP/LSIF/LSP index artifacts in CI for remaining source files; keep AST/static extraction as fallback.");
3390
+ }
3391
+ if (!memoryCodeEdges && approved.length && codeGraph.symbols.length) {
3392
+ recommendations.push("Link memory packets to symbols, routes, and tests with code_explanation, bug_fix, decision, and verification context.");
3393
+ }
3394
+ if (!validation.ok) {
3395
+ recommendations.push("Fix validation errors before relying on Kage in PR or agent-start workflows.");
3396
+ }
3397
+ const trustScore = Math.max(0, Math.min(100, Math.round((validation.ok ? 25 : 0) +
3398
+ quality.useful_memory_ratio_percent * 0.25 +
3399
+ structuredCoverage * 0.2 +
3400
+ memoryCodeCoverage * 0.15 +
3401
+ Math.max(0, 15 - pending.length * 3 - stalePackets * 5 - duplicateCandidatesTotal * 4))));
3402
+ return {
3403
+ schema_version: 1,
3404
+ project_dir: projectDir,
3405
+ generated_at: nowIso(),
3406
+ ok: validation.ok && stalePackets === 0 && duplicateCandidatesTotal === 0,
3407
+ trust_score: trustScore,
3408
+ checks: {
3409
+ validation,
3410
+ memory_inbox: {
3411
+ approved_packets: approved.length,
3412
+ pending_packets: pending.length,
3413
+ stale_packets: stalePackets,
3414
+ duplicate_candidates: duplicateCandidatesTotal,
3415
+ },
3416
+ structured_memory: {
3417
+ total_packets: approved.length,
3418
+ structured_packets: structuredPackets.length,
3419
+ coverage_percent: structuredCoverage,
3420
+ missing_context_packet_ids: approved.filter((packet) => !structuredPackets.includes(packet)).map((packet) => packet.id),
3421
+ },
3422
+ code_graph: {
3423
+ files: codeGraph.files.length,
3424
+ precise_files: preciseFiles,
3425
+ ast_files: astFiles,
3426
+ fallback_files: fallbackFiles,
3427
+ precise_coverage_percent: preciseCoverage,
3428
+ indexer_coverage_percent: percent(codeGraph.files.filter((file) => file.parser !== "metadata").length, indexableFiles),
3429
+ },
3430
+ graph_links: {
3431
+ memory_code_edges: memoryCodeEdges,
3432
+ evidence_coverage_percent: percent(knowledgeGraph.edges.filter((edge) => edge.evidence.length > 0).length, knowledgeGraph.edges.length),
3433
+ },
3434
+ },
3435
+ recommendations,
3436
+ };
3437
+ }
3438
+ function memoryInbox(projectDir) {
3439
+ ensureMemoryDirs(projectDir);
3440
+ const validation = validateProject(projectDir);
3441
+ const quality = qualityReport(projectDir);
3442
+ const approved = loadApprovedPackets(projectDir);
3443
+ const pending = loadPendingPackets(projectDir);
3444
+ const items = [];
3445
+ for (const packet of pending) {
3446
+ const qualityDetails = evaluateMemoryQuality(projectDir, packet);
3447
+ items.push({
3448
+ kind: "pending",
3449
+ severity: "warning",
3450
+ packet_id: packet.id,
3451
+ title: packet.title,
3452
+ type: packet.type,
3453
+ status: packet.status,
3454
+ paths: packet.paths,
3455
+ summary: packet.summary,
3456
+ reasons: [
3457
+ ...(qualityDetails.risks ?? []),
3458
+ `quality score ${qualityDetails.score}/100`,
3459
+ ],
3460
+ action: "Approve, reject, merge, or keep pending after reviewing source refs and sensitivity.",
3461
+ });
3462
+ }
3463
+ for (const packet of approved) {
3464
+ const reasons = staleMemoryReasons(projectDir, packet);
3465
+ if (reasons.length) {
3466
+ items.push({
3467
+ kind: "stale",
3468
+ severity: "blocker",
3469
+ packet_id: packet.id,
3470
+ title: packet.title,
3471
+ type: packet.type,
3472
+ status: packet.status,
3473
+ paths: packet.paths,
3474
+ summary: packet.summary,
3475
+ reasons,
3476
+ action: `${staleSuggestedAction(reasons)} this packet before trusting recall.`,
3477
+ });
3478
+ }
3479
+ }
3480
+ for (const packet of approved.filter((packet) => !hasStructuredEngineeringContext(packet))) {
3481
+ items.push({
3482
+ kind: "missing_context",
3483
+ severity: "info",
3484
+ packet_id: packet.id,
3485
+ title: packet.title,
3486
+ type: packet.type,
3487
+ status: packet.status,
3488
+ paths: packet.paths,
3489
+ summary: packet.summary,
3490
+ reasons: ["missing explicit why, verification, risk, stale condition, trigger, or action"],
3491
+ action: "Add structured context if this packet carries durable rationale, bug, issue, or code explanation.",
3492
+ });
3493
+ }
3494
+ for (const packet of quality.packets.filter((packet) => packet.classification === "duplicate")) {
3495
+ const source = [...approved, ...pending].find((candidate) => candidate.id === packet.id);
3496
+ items.push({
3497
+ kind: "duplicate",
3498
+ severity: "warning",
3499
+ packet_id: packet.id,
3500
+ title: packet.title,
3501
+ type: packet.type,
3502
+ status: packet.status,
3503
+ paths: source?.paths,
3504
+ summary: source?.summary ?? packet.title,
3505
+ reasons: packet.risks.length ? packet.risks : ["duplicate candidate detected by quality report"],
3506
+ action: "Merge, supersede, or deprecate overlapping memory before handoff.",
3507
+ });
3508
+ }
3509
+ for (const error of validation.errors) {
3510
+ items.push({
3511
+ kind: "validation_error",
3512
+ severity: "blocker",
3513
+ summary: error,
3514
+ reasons: [error],
3515
+ action: "Fix validation errors before relying on Kage in agent or PR workflows.",
3516
+ });
3517
+ }
3518
+ for (const warning of validation.warnings) {
3519
+ items.push({
3520
+ kind: "validation_warning",
3521
+ severity: "warning",
3522
+ summary: warning,
3523
+ reasons: [warning],
3524
+ action: "Review grounding, indexes, generated artifacts, or packet quality.",
3525
+ });
3526
+ }
3527
+ const counts = {
3528
+ approved: approved.length,
3529
+ pending: pending.length,
3530
+ stale: items.filter((item) => item.kind === "stale").length,
3531
+ duplicates: items.filter((item) => item.kind === "duplicate").length,
3532
+ missing_context: items.filter((item) => item.kind === "missing_context").length,
3533
+ validation_errors: validation.errors.length,
3534
+ validation_warnings: validation.warnings.length,
3535
+ };
3536
+ const recommendations = unique([
3537
+ ...(counts.pending ? ["Review pending memory packets before handoff."] : []),
3538
+ ...(counts.stale ? ["Update, verify, supersede, or deprecate stale memory packets."] : []),
3539
+ ...(counts.duplicates ? ["Merge or supersede duplicate memory packets."] : []),
3540
+ ...(counts.missing_context ? ["Add structured why, verification, risk, and stale_when context to high-value packets."] : []),
3541
+ ...(counts.validation_errors ? ["Fix validation errors before trusting recall."] : []),
3542
+ ...(counts.validation_warnings ? ["Review validation warnings so memory remains source-grounded."] : []),
3543
+ ]);
3544
+ return {
3545
+ schema_version: 1,
3546
+ project_dir: projectDir,
3547
+ generated_at: nowIso(),
3548
+ ok: counts.pending === 0 && counts.stale === 0 && counts.duplicates === 0 && counts.validation_errors === 0,
3549
+ counts,
3550
+ items,
3551
+ recommendations,
3552
+ };
3553
+ }
3042
3554
  function qualityReport(projectDir) {
3043
3555
  ensureMemoryDirs(projectDir);
3044
3556
  const packets = [...loadPacketsFromDir(packetsDir(projectDir)), ...loadPacketsFromDir(pendingDir(projectDir))];
@@ -3116,17 +3628,57 @@ function benchmarkProject(projectDir) {
3116
3628
  const metrics = kageMetricsShallow(projectDir);
3117
3629
  const quality = qualityReport(projectDir);
3118
3630
  const typeCoverage = quality.memory_type_coverage;
3631
+ const recallHitRate = percent(scenarios.filter((scenario) => scenario.hit).length, scenarios.length);
3632
+ const codeFlowCoverage = metrics.code_graph.files > 0 && metrics.code_graph.symbols > 0 ? 100 : 0;
3633
+ const gates = [
3634
+ {
3635
+ name: "recall_hit_rate",
3636
+ target: 60,
3637
+ actual: recallHitRate,
3638
+ unit: "percent",
3639
+ pass: recallHitRate >= 60,
3640
+ required: true,
3641
+ },
3642
+ {
3643
+ name: "evidence_coverage",
3644
+ target: 80,
3645
+ actual: quality.evidence_coverage_percent,
3646
+ unit: "percent",
3647
+ pass: quality.evidence_coverage_percent >= 80,
3648
+ required: true,
3649
+ },
3650
+ {
3651
+ name: "useful_memory_ratio",
3652
+ target: 70,
3653
+ actual: quality.useful_memory_ratio_percent,
3654
+ unit: "percent",
3655
+ pass: quality.useful_memory_ratio_percent >= 70,
3656
+ required: true,
3657
+ },
3658
+ {
3659
+ name: "code_flow_coverage",
3660
+ target: 100,
3661
+ actual: codeFlowCoverage,
3662
+ unit: "percent",
3663
+ pass: codeFlowCoverage >= 100,
3664
+ required: true,
3665
+ },
3666
+ ];
3667
+ const gateScore = Math.round(gates.reduce((sum, gate) => sum + Math.min(100, Math.round((gate.actual / Math.max(1, gate.target)) * 100)), 0) / gates.length);
3119
3668
  return {
3120
3669
  schema_version: 1,
3121
3670
  project_dir: projectDir,
3122
3671
  generated_at: nowIso(),
3672
+ ok: gates.filter((gate) => gate.required).every((gate) => gate.pass),
3673
+ overall_score: gateScore,
3674
+ gates,
3123
3675
  scenarios,
3124
3676
  pain_metrics: {
3125
3677
  setup_runbook_coverage_percent: typeCoverage.runbook ? 100 : 0,
3126
3678
  bug_fix_coverage_percent: typeCoverage.bug_fix ? 100 : 0,
3127
3679
  decision_coverage_percent: typeCoverage.decision ? 100 : 0,
3128
- code_flow_coverage_percent: metrics.code_graph.files > 0 && metrics.code_graph.symbols > 0 ? 100 : 0,
3129
- recall_hit_rate_percent: percent(scenarios.filter((scenario) => scenario.hit).length, scenarios.length),
3680
+ code_flow_coverage_percent: codeFlowCoverage,
3681
+ recall_hit_rate_percent: recallHitRate,
3130
3682
  estimated_rediscovery_avoided: scenarios.filter((scenario) => scenario.hit).length,
3131
3683
  estimated_tokens_saved: metrics.savings.estimated_tokens_saved_per_recall,
3132
3684
  time_to_first_use_seconds: metrics.harness.policy_installed ? 30 : 90,
@@ -3296,10 +3848,20 @@ function inferLearningType(input) {
3296
3848
  if (input.type)
3297
3849
  return input.type;
3298
3850
  const text = `${input.title ?? ""} ${input.learning}`.toLowerCase();
3851
+ if (/(issue context|issue|hypothesis|blocked|unresolved|attempted fix)/.test(text))
3852
+ return "issue_context";
3299
3853
  if (/(bug|fix|error|fail|failure|broken|regression)/.test(text))
3300
3854
  return "bug_fix";
3301
- if (/(decided|decision|rationale|tradeoff|chose|choose)/.test(text))
3855
+ if (/(code explanation|explains|data flow|invariant|coupling|module purpose)/.test(text))
3856
+ return "code_explanation";
3857
+ if (/(constraint|external requirement|legal|compliance|performance budget)/.test(text))
3858
+ return "constraint";
3859
+ if (/(negative result|tried|failed because|rejected)/.test(text))
3860
+ return "negative_result";
3861
+ if (/(decided|decision|tradeoff|chose|choose)/.test(text))
3302
3862
  return "decision";
3863
+ if (/(why|rationale|because)/.test(text))
3864
+ return "rationale";
3303
3865
  if (/(run|command|setup|install|build|test|deploy)/.test(text))
3304
3866
  return "runbook";
3305
3867
  if (/(convention|always|prefer|avoid|pattern)/.test(text))
@@ -3312,6 +3874,58 @@ function titleFromLearning(learning) {
3312
3874
  const sentence = learning.split(/[.!?]\s+/)[0]?.trim() || learning.trim();
3313
3875
  return sentence.slice(0, 90) || "Session learning";
3314
3876
  }
3877
+ const MEMORY_CONTEXT_FIELD_LABELS = [
3878
+ "Fact",
3879
+ "Decision",
3880
+ "Why",
3881
+ "Rationale",
3882
+ "Because",
3883
+ "When",
3884
+ "Trigger",
3885
+ "Action",
3886
+ "Do",
3887
+ "Use",
3888
+ "Verified by",
3889
+ "Verification",
3890
+ "Evidence",
3891
+ "Risk if forgotten",
3892
+ "Risk",
3893
+ "Stale when",
3894
+ "Invalid when",
3895
+ "Revisit when",
3896
+ "Rejected alternatives",
3897
+ ];
3898
+ function labeledMemoryField(text, labels) {
3899
+ const escaped = labels.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
3900
+ const allLabels = MEMORY_CONTEXT_FIELD_LABELS.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
3901
+ const match = text.match(new RegExp(`(?:^|\\n|\\b)(?:${escaped})\\s*:\\s*([\\s\\S]*?)(?=(?:\\s|\\n)+(?:${allLabels})\\s*:|$)`, "i"));
3902
+ return match?.[1]?.trim().replace(/\s+$/, "");
3903
+ }
3904
+ function inferEngineeringContext(input) {
3905
+ const body = input.body.trim();
3906
+ const firstParagraph = body
3907
+ .split(/\n\s*\n/)
3908
+ .find((part) => !/^\s*(why|verified by|verification|risk if forgotten|stale when|trigger|action|rejected alternatives)\s*:/i.test(part))
3909
+ ?.trim();
3910
+ const context = {
3911
+ fact: input.context?.fact ?? labeledMemoryField(body, ["Fact", "Decision"]) ?? firstParagraph ?? input.title,
3912
+ why: input.context?.why ?? labeledMemoryField(body, ["Why", "Rationale", "Because"]),
3913
+ trigger: input.context?.trigger ?? labeledMemoryField(body, ["When", "Trigger"]),
3914
+ action: input.context?.action ?? labeledMemoryField(body, ["Action", "Do", "Use"]),
3915
+ verification: input.context?.verification ?? labeledMemoryField(body, ["Verified by", "Verification", "Evidence"]),
3916
+ risk_if_forgotten: input.context?.risk_if_forgotten ?? labeledMemoryField(body, ["Risk if forgotten", "Risk"]),
3917
+ stale_when: input.context?.stale_when ?? labeledMemoryField(body, ["Stale when", "Invalid when", "Revisit when"]),
3918
+ rejected_alternatives: input.context?.rejected_alternatives,
3919
+ };
3920
+ return Object.fromEntries(Object.entries(context).filter(([, value]) => Array.isArray(value) ? value.length : Boolean(value)));
3921
+ }
3922
+ function engineeringContextFor(packet) {
3923
+ return inferEngineeringContext({ title: packet.title, body: packet.body, context: packet.context });
3924
+ }
3925
+ function hasStructuredEngineeringContext(packet) {
3926
+ const context = engineeringContextFor(packet);
3927
+ return Boolean(context.why || context.verification || context.risk_if_forgotten || context.stale_when || context.trigger || context.action);
3928
+ }
3315
3929
  function learn(input) {
3316
3930
  const type = inferLearningType(input);
3317
3931
  const title = input.title?.trim() || titleFromLearning(input.learning);
@@ -3329,6 +3943,7 @@ function learn(input) {
3329
3943
  tags: unique(["session-learning", ...(input.tags ?? [])]),
3330
3944
  paths: input.paths,
3331
3945
  stack: input.stack,
3946
+ context: input.context,
3332
3947
  });
3333
3948
  }
3334
3949
  function capture(input) {
@@ -3366,6 +3981,7 @@ function capture(input) {
3366
3981
  captured_at: createdAt,
3367
3982
  },
3368
3983
  ],
3984
+ context: inferEngineeringContext({ title: input.title, body: input.body, context: input.context }),
3369
3985
  freshness: {
3370
3986
  ttl_days: 365,
3371
3987
  last_verified_at: createdAt,
@@ -3903,6 +4519,15 @@ function reusableFileObservation(event) {
3903
4519
  "dispatch",
3904
4520
  "convention",
3905
4521
  "decision",
4522
+ "rationale",
4523
+ "root cause",
4524
+ "issue",
4525
+ "hypothesis",
4526
+ "unresolved",
4527
+ "code explanation",
4528
+ "explains",
4529
+ "data flow",
4530
+ "invariant",
3906
4531
  "gotcha",
3907
4532
  "workflow",
3908
4533
  "runbook",
@@ -3974,9 +4599,20 @@ function reusablePromptObservation(event) {
3974
4599
  "convention",
3975
4600
  "policy",
3976
4601
  "gotcha",
4602
+ "bug",
4603
+ "issue",
4604
+ "issue context",
4605
+ "hypothesis",
4606
+ "unresolved",
4607
+ "rationale",
4608
+ "why:",
4609
+ "because",
4610
+ "code explanation",
4611
+ "explains",
4612
+ "data flow",
4613
+ "root cause",
3977
4614
  "runbook",
3978
4615
  "workflow",
3979
- "root cause",
3980
4616
  "use this",
3981
4617
  "always",
3982
4618
  "never",
@@ -3985,7 +4621,7 @@ function reusablePromptObservation(event) {
3985
4621
  ];
3986
4622
  if (!durableSignals.some((signal) => lower.includes(signal)))
3987
4623
  return "";
3988
- if (/^(fix|build|create|implement|update|continue|show me|what is|why is|can you)\b/i.test(text) && !/(decision|convention|policy|gotcha|remember|prefer|avoid)/i.test(text))
4624
+ if (/^(fix|build|create|implement|update|continue|show me|what is|why is|can you)\b/i.test(text) && !/(decision|convention|policy|gotcha|remember|prefer|avoid|bug|issue|hypothesis|rationale|because|root cause|code explanation|explains)/i.test(text))
3989
4625
  return "";
3990
4626
  return text;
3991
4627
  }
@@ -4112,7 +4748,8 @@ function createDiffChangeMemory(projectDir, summary) {
4112
4748
  "",
4113
4749
  "Improve this packet when more context is known:",
4114
4750
  "- The actual feature, fix, or refactor rationale.",
4115
- "- The package, API, command, or architectural pattern future agents should reuse.",
4751
+ "- Why the change was made, including relevant bugs, issues, decisions, and code explanations.",
4752
+ "- The package, API, command, or architectural pattern future agents should understand, verify, or reuse.",
4116
4753
  "- Any gotchas, follow-up risks, or branch-specific assumptions.",
4117
4754
  "",
4118
4755
  "Promote beyond this repo only after explicit org/global review.",
@@ -4143,6 +4780,15 @@ function createDiffChangeMemory(projectDir, summary) {
4143
4780
  summary_path: (0, node_path_1.join)(reviewDir(projectDir), `branch-summary-${slugify(branch)}.json`),
4144
4781
  },
4145
4782
  ],
4783
+ context: {
4784
+ fact: `Current branch ${branch} changes ${summary.changed_files.length} repo path${summary.changed_files.length === 1 ? "" : "s"}.`,
4785
+ why: "Branch change memory gives future agents durable context from the git diff when they continue, review, or verify this work.",
4786
+ trigger: "Recall when asking what changed on this branch, preparing a PR review, or resuming this work.",
4787
+ action: "Use the changed file list and diff summary as orientation, then inspect the actual diff and source files before making further edits.",
4788
+ verification: "Generated from git diff and refreshed by kage pr summarize or kage propose --from-diff.",
4789
+ risk_if_forgotten: "Future agents may repeat orientation work, miss branch-specific assumptions, or ignore files touched by this change.",
4790
+ stale_when: "The branch diff changes substantially, the branch is merged, or a newer change-memory packet supersedes it.",
4791
+ },
4146
4792
  freshness: {
4147
4793
  last_verified_at: now,
4148
4794
  ttl_days: 180,
@@ -4435,9 +5081,10 @@ function loadOrgInboxPackets(projectDir, org) {
4435
5081
  }
4436
5082
  function recallFromPackets(query, packets, limit, label) {
4437
5083
  const terms = tokenize(query);
5084
+ const lexicalScores = scorePacketsBm25(terms, packets);
4438
5085
  const scored = packets
4439
5086
  .map((packet) => {
4440
- const { score, why } = scorePacket(terms, packet);
5087
+ const { score, why } = lexicalScores.get(packet.id) ?? { score: 0, why: [] };
4441
5088
  return { packet, score, why_matched: why };
4442
5089
  })
4443
5090
  .filter((result) => result.score > 0)