@kage-core/kage-graph-mcp 1.1.37 → 1.1.38

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
@@ -89,6 +89,7 @@ exports.buildEmbeddingIndex = buildEmbeddingIndex;
89
89
  exports.recall = recall;
90
90
  exports.recallWithEmbeddings = recallWithEmbeddings;
91
91
  exports.queryCodeGraph = queryCodeGraph;
92
+ exports.kageTeammateBrief = kageTeammateBrief;
92
93
  exports.kageRisk = kageRisk;
93
94
  exports.kageDependencyPath = kageDependencyPath;
94
95
  exports.kageCleanupCandidates = kageCleanupCandidates;
@@ -102,6 +103,7 @@ exports.kageCapabilityAudit = kageCapabilityAudit;
102
103
  exports.kageDecisionIntelligence = kageDecisionIntelligence;
103
104
  exports.kageModuleHealth = kageModuleHealth;
104
105
  exports.kageGraphInsights = kageGraphInsights;
106
+ exports.kageRepoXray = kageRepoXray;
105
107
  exports.kageWorkspace = kageWorkspace;
106
108
  exports.kageWorkspaceRecall = kageWorkspaceRecall;
107
109
  exports.queryGraph = queryGraph;
@@ -124,6 +126,7 @@ exports.verifyAgentActivation = verifyAgentActivation;
124
126
  exports.observe = observe;
125
127
  exports.kageSessionCaptureReport = kageSessionCaptureReport;
126
128
  exports.kageSessionReplay = kageSessionReplay;
129
+ exports.kageSessionLearningLedger = kageSessionLearningLedger;
127
130
  exports.distillSession = distillSession;
128
131
  exports.proposeFromDiff = proposeFromDiff;
129
132
  exports.buildBranchOverlay = buildBranchOverlay;
@@ -6555,6 +6558,112 @@ function queryCodeGraph(projectDir, query, limit = 10, graph) {
6555
6558
  structural_edges: structuralEdges,
6556
6559
  };
6557
6560
  }
6561
+ function fileHintsFromText(text) {
6562
+ const matches = text.match(/[A-Za-z0-9_./@-]+\.(?:ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|kts|rb|php|cs|c|h|cc|cpp|hpp|swift|json|md)\b/g) ?? [];
6563
+ return [...new Set(matches.map((match) => match.replace(/^\.\//, "")).filter((match) => !/^https?:\/\//.test(match)))];
6564
+ }
6565
+ function dedupeStrings(values) {
6566
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
6567
+ }
6568
+ function teammateBriefLines(brief) {
6569
+ const verification = brief.verification_contract;
6570
+ const lines = [
6571
+ "\n## Teammate Brief",
6572
+ "Purpose: reduce verification debt and context loss for this task.",
6573
+ "",
6574
+ "### Verification Contract",
6575
+ ];
6576
+ if (verification.focus_files.length) {
6577
+ lines.push(`Focus files: ${verification.focus_files.join(", ")}`);
6578
+ }
6579
+ if (verification.related_tests.length) {
6580
+ lines.push("Related tests:");
6581
+ for (const test of verification.related_tests.slice(0, 5)) {
6582
+ lines.push(`- ${test.test_path}${test.title ? ` - ${test.title}` : ""}${test.covers ? ` (covers ${test.covers})` : ""}`);
6583
+ }
6584
+ }
6585
+ else if (verification.focus_files.length) {
6586
+ lines.push("Related tests: none found in the current code graph.");
6587
+ }
6588
+ if (verification.test_gap_files.length) {
6589
+ lines.push(`Test gaps: ${verification.test_gap_files.join(", ")}`);
6590
+ }
6591
+ if (brief.memory_warnings.length) {
6592
+ lines.push("", "### Memory Warnings", ...brief.memory_warnings.slice(0, 5).map((warning) => `- ${warning}`));
6593
+ }
6594
+ lines.push("", "### Next Actions");
6595
+ for (const action of brief.next_actions.slice(0, 6)) {
6596
+ lines.push(`- ${action}`);
6597
+ }
6598
+ return lines;
6599
+ }
6600
+ function kageTeammateBrief(projectDir, options) {
6601
+ const query = options.query;
6602
+ const focusFiles = dedupeStrings([
6603
+ ...(options.targets ?? []),
6604
+ ...(options.changedFiles ?? []),
6605
+ ...fileHintsFromText(query),
6606
+ ]);
6607
+ const codeQuery = dedupeStrings([query, ...focusFiles]).join(" ");
6608
+ const code = queryCodeGraph(projectDir, codeQuery || query, 12);
6609
+ const relatedTests = code.tests
6610
+ .map((test) => ({
6611
+ test_path: test.test_path,
6612
+ title: test.title,
6613
+ covers: test.covers_path ?? test.covers_symbol ?? null,
6614
+ }))
6615
+ .filter((test, index, all) => all.findIndex((item) => item.test_path === test.test_path && item.title === test.title) === index);
6616
+ const riskTargets = options.riskResult ? Object.values(options.riskResult.targets) : [];
6617
+ const testGapFiles = dedupeStrings([
6618
+ ...riskTargets.filter((target) => target.test_gap).map((target) => target.target),
6619
+ ...(focusFiles.length && !relatedTests.length ? focusFiles : []),
6620
+ ]);
6621
+ const memoryWarnings = [
6622
+ ...((options.recallResult?.results ?? [])
6623
+ .filter((entry) => Boolean((entry.packet.quality ?? {}).stale))
6624
+ .map((entry) => `Recalled memory may be stale: ${entry.packet.title}.`)),
6625
+ ...(options.reconciliation?.unresolved_count
6626
+ ? [`${options.reconciliation.unresolved_count} linked memory item(s) need update, supersede, or stale marking before handoff.`]
6627
+ : []),
6628
+ ];
6629
+ const requiredActions = [
6630
+ ...(relatedTests.length
6631
+ ? [`Run or account for related test coverage: ${relatedTests.slice(0, 3).map((test) => test.test_path).join(", ")}.`]
6632
+ : focusFiles.length
6633
+ ? ["No related tests were found; identify the correct verification before claiming completion."]
6634
+ : ["Identify task-specific verification before claiming completion."]),
6635
+ ...testGapFiles.map((file) => `Resolve test-gap risk for ${file} or explain why existing verification is sufficient.`),
6636
+ ...(memoryWarnings.length ? ["Resolve memory warnings before final handoff."] : []),
6637
+ ];
6638
+ const nextActions = dedupeStrings([
6639
+ ...requiredActions,
6640
+ ...(riskTargets.length
6641
+ ? riskTargets
6642
+ .filter((target) => target.co_change_warnings.length)
6643
+ .slice(0, 2)
6644
+ .map((target) => `Review co-change partners for ${target.target}: ${target.co_change_warnings.slice(0, 3).map((item) => item.file_path).join(", ")}.`)
6645
+ : []),
6646
+ "Keep any durable lesson evidence-backed; future agents should inherit only verified repo knowledge.",
6647
+ ]);
6648
+ const briefWithoutBlock = {
6649
+ schema_version: 1,
6650
+ project_dir: projectDir,
6651
+ generated_at: nowIso(),
6652
+ query,
6653
+ verification_contract: {
6654
+ focus_files: focusFiles,
6655
+ related_tests: relatedTests,
6656
+ test_gap_files: testGapFiles,
6657
+ required_actions: requiredActions,
6658
+ },
6659
+ memory_warnings: memoryWarnings,
6660
+ next_actions: nextActions,
6661
+ };
6662
+ return {
6663
+ ...briefWithoutBlock,
6664
+ context_block: teammateBriefLines(briefWithoutBlock).join("\n"),
6665
+ };
6666
+ }
6558
6667
  function gitLines(projectDir, args) {
6559
6668
  return (readGit(projectDir, args) ?? "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
6560
6669
  }
@@ -6662,7 +6771,7 @@ function gitFileSignal(projectDir, path, graphPaths) {
6662
6771
  }
6663
6772
  function gitChangedFiles(projectDir) {
6664
6773
  return gitLines(projectDir, ["status", "--porcelain", "-uall"])
6665
- .map((line) => line.slice(3).trim().split(" -> ").at(-1) ?? "")
6774
+ .map((line) => parsePorcelainPath(line).split(" -> ").at(-1) ?? "")
6666
6775
  .filter(Boolean)
6667
6776
  .map((path) => gitPathToProjectRelative(projectDir, path) ?? path)
6668
6777
  .filter((path) => !isNoisePath(path));
@@ -7562,7 +7671,7 @@ function kageProjectProfile(projectDir) {
7562
7671
  }
7563
7672
  const memoryConceptCounts = new Map();
7564
7673
  for (const packet of approved) {
7565
- for (const tag of packet.tags.filter((item) => item && !["session-learning", "agentmemory-comparison"].includes(item))) {
7674
+ for (const tag of packet.tags.filter((item) => item && !["session-learning", "external-comparison"].includes(item))) {
7566
7675
  memoryConceptCounts.set(tag, (memoryConceptCounts.get(tag) ?? 0) + 1);
7567
7676
  }
7568
7677
  }
@@ -8307,13 +8416,261 @@ function kageGraphInsights(projectDir) {
8307
8416
  summary: `${centralFiles.length} central file(s), ${cycles.length} dependency cycle(s), ${communities.length} communit${communities.length === 1 ? "y" : "ies"}, ${flows.length} entry flow(s).`,
8308
8417
  };
8309
8418
  }
8419
+ function xrayItem(input) {
8420
+ return {
8421
+ ...input,
8422
+ strength: Math.max(1, Math.min(100, Math.round(input.strength ?? 50))),
8423
+ status: input.status ?? "ok",
8424
+ };
8425
+ }
8426
+ function uniqueXrayItems(items) {
8427
+ const byPath = new Map();
8428
+ for (const item of items) {
8429
+ const existing = byPath.get(item.path);
8430
+ if (!existing || item.strength > existing.strength || (item.status === "risk" && existing.status !== "risk")) {
8431
+ byPath.set(item.path, item);
8432
+ }
8433
+ }
8434
+ return [...byPath.values()].sort((a, b) => b.strength - a.strength || a.path.localeCompare(b.path));
8435
+ }
8436
+ function isXrayCodePath(path, graphPaths) {
8437
+ const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "");
8438
+ return graphPaths.has(normalized) && !normalized.startsWith(".agent_memory/") && !normalized.startsWith("agent_memory/");
8439
+ }
8440
+ function kageRepoXray(projectDir) {
8441
+ const graph = readCurrentCodeGraph(projectDir) ?? buildCodeGraph(projectDir);
8442
+ const profile = kageProjectProfile(projectDir);
8443
+ const risk = kageRisk(projectDir);
8444
+ const health = kageModuleHealth(projectDir);
8445
+ const insights = kageGraphInsights(projectDir);
8446
+ const decisions = kageDecisionIntelligence(projectDir);
8447
+ const approved = loadApprovedPackets(projectDir);
8448
+ const graphPaths = new Set(graph.files.map((file) => file.path));
8449
+ const routeCounts = countBy(graph.routes, (route) => route.file_path);
8450
+ const testsBySource = new Map();
8451
+ for (const test of graph.tests) {
8452
+ const key = test.covers_path ?? test.covers_symbol ?? "";
8453
+ if (!key)
8454
+ continue;
8455
+ const list = testsBySource.get(key) ?? [];
8456
+ list.push(test);
8457
+ testsBySource.set(key, list);
8458
+ }
8459
+ const entryItems = uniqueXrayItems([
8460
+ ...graph.routes.map((route) => xrayItem({
8461
+ label: `${route.method} ${route.path}`,
8462
+ path: route.file_path,
8463
+ kind: "route",
8464
+ strength: 90,
8465
+ status: "ok",
8466
+ evidence: [`Route handler in ${route.file_path}`, `${route.method} ${route.path}`],
8467
+ action: "Start here to understand request flow before changing runtime behavior.",
8468
+ })),
8469
+ ...insights.entry_flows.map((flow) => xrayItem({
8470
+ label: flow.entry,
8471
+ path: flow.entry,
8472
+ kind: "entry_flow",
8473
+ strength: Math.min(96, 64 + flow.path.length * 6),
8474
+ status: "ok",
8475
+ evidence: [`Entry flow: ${flow.path.slice(0, 5).join(" -> ")}`],
8476
+ action: "Trace this entry flow before editing shared dependencies.",
8477
+ })),
8478
+ ...profile.run_commands.slice(0, 4).map((command) => xrayItem({
8479
+ label: command.name,
8480
+ path: "package.json",
8481
+ kind: "script",
8482
+ strength: 58,
8483
+ status: "ok",
8484
+ evidence: [`package script: ${command.name} = ${command.command}`],
8485
+ action: "Use this command evidence when verifying changes.",
8486
+ })),
8487
+ ]).slice(0, 8);
8488
+ const centralByPath = new Map(insights.central_files.map((file) => [file.path, file]));
8489
+ const coreItems = uniqueXrayItems([
8490
+ ...profile.key_files.map((file) => {
8491
+ const central = centralByPath.get(file.path);
8492
+ return xrayItem({
8493
+ label: file.path,
8494
+ path: file.path,
8495
+ kind: file.kind,
8496
+ strength: Math.min(100, file.score + (central ? central.dependents * 6 : 0)),
8497
+ status: "ok",
8498
+ evidence: unique([
8499
+ ...file.why,
8500
+ ...(central ? [`centrality ${central.pagerank}, ${central.dependents} dependent(s)`] : []),
8501
+ ]).slice(0, 4),
8502
+ action: "Inspect this file early; Kage sees it as a central part of the repo.",
8503
+ });
8504
+ }),
8505
+ ...insights.central_files.slice(0, 8).map((file) => xrayItem({
8506
+ label: file.path,
8507
+ path: file.path,
8508
+ kind: file.kind,
8509
+ strength: Math.min(100, 45 + file.dependents * 8 + file.imports * 3),
8510
+ status: "ok",
8511
+ evidence: [`${file.dependents} dependent(s)`, `${file.imports} outgoing import(s)`],
8512
+ action: "Use this as a structural orientation point before following dependencies.",
8513
+ })),
8514
+ ]).slice(0, 10);
8515
+ const riskTargets = Object.values(risk.targets).filter((target) => isXrayCodePath(target.target, graphPaths));
8516
+ const riskHotspots = risk.global_hotspots.filter((hotspot) => isXrayCodePath(hotspot.file_path, graphPaths));
8517
+ const riskItems = uniqueXrayItems([
8518
+ ...riskTargets.map((target) => xrayItem({
8519
+ label: target.target,
8520
+ path: target.target,
8521
+ kind: target.risk_type,
8522
+ strength: Math.max(30, Math.round(target.hotspot_score * 100), target.dependents_count * 12, target.test_gap ? 70 : 0),
8523
+ status: target.test_gap || target.risk_type === "single-owner" || target.risk_type === "churn-heavy" ? "risk" : "watch",
8524
+ evidence: [
8525
+ `${target.dependents_count} direct dependent(s)`,
8526
+ `${target.git.commit_count_90d} commit(s) in 90d`,
8527
+ target.test_gap ? "test gap" : "test signal found",
8528
+ ],
8529
+ action: "Review dependents, tests, and owners before editing this path.",
8530
+ })),
8531
+ ...riskHotspots.slice(0, 8).map((hotspot) => xrayItem({
8532
+ label: hotspot.file_path,
8533
+ path: hotspot.file_path,
8534
+ kind: "hotspot",
8535
+ strength: Math.round(hotspot.hotspot_score * 100),
8536
+ status: "risk",
8537
+ evidence: [`${hotspot.commit_count_90d} commit(s) in 90d`, `primary owner ${hotspot.primary_owner ?? "unknown"}`],
8538
+ action: "Treat this as a change hotspot; ask Kage for risk before editing.",
8539
+ })),
8540
+ ...health.modules.filter((module) => module.grade === "C" || module.grade === "D").slice(0, 5).map((module) => xrayItem({
8541
+ label: module.module,
8542
+ path: module.module === "(root)" ? "." : module.module,
8543
+ kind: "module",
8544
+ strength: 100 - module.score,
8545
+ status: module.grade === "D" ? "risk" : "watch",
8546
+ evidence: module.reasons.slice(0, 3),
8547
+ action: "Use module health reasons to decide tests and review scope.",
8548
+ })),
8549
+ ]).slice(0, 10);
8550
+ const testItems = uniqueXrayItems([
8551
+ ...graph.tests.map((test) => xrayItem({
8552
+ label: test.title || test.test_path,
8553
+ path: test.test_path,
8554
+ kind: "test",
8555
+ strength: 78,
8556
+ status: "ok",
8557
+ evidence: [`covers ${test.covers_path ?? test.covers_symbol ?? "repo behavior"}`],
8558
+ action: "Run or account for this test when changing the covered code.",
8559
+ })),
8560
+ ...graph.files
8561
+ .filter((file) => file.kind === "source" && !hasTestCoverage(file.path, graph))
8562
+ .slice(0, 8)
8563
+ .map((file) => xrayItem({
8564
+ label: file.path,
8565
+ path: file.path,
8566
+ kind: "test_gap",
8567
+ strength: routeCounts[file.path] ? 82 : 58,
8568
+ status: "watch",
8569
+ evidence: routeCounts[file.path] ? [`${routeCounts[file.path]} route(s), no direct test signal`] : ["no direct test signal"],
8570
+ action: "Identify the right verification path before claiming a change here is safe.",
8571
+ })),
8572
+ ]).slice(0, 12);
8573
+ const memoryByPath = new Map();
8574
+ for (const packet of approved) {
8575
+ for (const path of packet.paths.filter((item) => graphPaths.has(item))) {
8576
+ const list = memoryByPath.get(path) ?? [];
8577
+ list.push(packet);
8578
+ memoryByPath.set(path, list);
8579
+ }
8580
+ }
8581
+ const memoryItems = uniqueXrayItems([...memoryByPath.entries()].map(([path, packets]) => xrayItem({
8582
+ label: path,
8583
+ path,
8584
+ kind: "memory_overlay",
8585
+ strength: Math.min(100, packets.length * 22 + (testsBySource.get(path)?.length ?? 0) * 8 + (routeCounts[path] ?? 0) * 8),
8586
+ status: "ok",
8587
+ evidence: packets.slice(0, 3).map((packet) => `${packet.type}: ${packet.title}`),
8588
+ action: "Read linked memory before editing; this is repo lore attached to code.",
8589
+ }))).slice(0, 10);
8590
+ const gapItems = uniqueXrayItems(decisions.coverage_gaps.slice(0, 10).map((gap) => xrayItem({
8591
+ label: gap.path,
8592
+ path: gap.path,
8593
+ kind: "knowledge_gap",
8594
+ strength: Math.min(100, gap.dependents * 16 + gap.churn_90d * 8 + 24),
8595
+ status: "watch",
8596
+ evidence: [gap.reason, `${gap.dependents} dependent(s)`, `${gap.churn_90d} commit(s) in 90d`],
8597
+ action: "Capture why-memory here when the next session learns reusable context.",
8598
+ })));
8599
+ const layers = [
8600
+ {
8601
+ id: "entry_points",
8602
+ title: "Entry Points",
8603
+ summary: entryItems.length ? "Where runtime behavior appears to start." : "No route, script, or entry-flow signals found yet.",
8604
+ items: entryItems,
8605
+ },
8606
+ {
8607
+ id: "core_modules",
8608
+ title: "Core Modules",
8609
+ summary: coreItems.length ? "Files Kage would inspect first to understand this repo." : "No central code files found yet.",
8610
+ items: coreItems,
8611
+ },
8612
+ {
8613
+ id: "change_risk",
8614
+ title: "Change Risk",
8615
+ summary: riskItems.length ? "Hotspots, low-health modules, and risky change targets." : "No local risk signals found yet.",
8616
+ items: riskItems,
8617
+ },
8618
+ {
8619
+ id: "test_map",
8620
+ title: "Test Map",
8621
+ summary: testItems.length ? "Verification paths and code with missing direct test signals." : "No tests found in the code graph.",
8622
+ items: testItems,
8623
+ },
8624
+ {
8625
+ id: "memory_overlay",
8626
+ title: "Memory Overlay",
8627
+ summary: memoryItems.length ? "Repo knowledge already attached to code." : "No code-linked memory yet.",
8628
+ items: memoryItems,
8629
+ },
8630
+ {
8631
+ id: "knowledge_gaps",
8632
+ title: "Knowledge Gaps",
8633
+ summary: gapItems.length ? "High-signal code paths that need why-memory." : "No decision-memory coverage gaps detected.",
8634
+ items: gapItems,
8635
+ },
8636
+ ];
8637
+ const script = [
8638
+ "I mapped your repo.",
8639
+ `I found ${entryItems.length} entry point(s), ${coreItems.length} core code signal(s), ${riskItems.length} risk signal(s), and ${testItems.length} verification signal(s).`,
8640
+ memoryItems.length
8641
+ ? `${memoryItems.length} code area(s) already have attached repo memory.`
8642
+ : "I do not see much code-linked repo memory yet, so I will learn carefully during the session.",
8643
+ "Click any X-Ray item to focus the graph and see the evidence.",
8644
+ ];
8645
+ const nextActions = [
8646
+ ...(entryItems.length ? [`Start orientation from ${entryItems[0].path}.`] : ["Run kage refresh so entry points can be indexed."]),
8647
+ ...(riskItems.length ? [`Review highest-risk area ${riskItems[0].path} before making edits.`] : []),
8648
+ ...(testItems.some((item) => item.kind === "test_gap") ? ["Resolve test-map gaps by identifying task-specific verification before handoff."] : []),
8649
+ ...(gapItems.length ? ["Capture why-memory for knowledge gaps when the session uncovers durable context."] : []),
8650
+ ];
8651
+ const warnings = unique([
8652
+ ...profile.warnings,
8653
+ ...risk.warnings,
8654
+ ...health.warnings,
8655
+ ...insights.warnings,
8656
+ ...decisions.warnings,
8657
+ ]);
8658
+ return {
8659
+ schema_version: 1,
8660
+ project_dir: projectDir,
8661
+ generated_at: nowIso(),
8662
+ summary: `Repo X-Ray mapped ${graph.files.length} file(s), ${graph.symbols.length} symbol(s), ${graph.routes.length} route(s), ${graph.tests.length} test signal(s), and ${approved.length} memory packet(s).`,
8663
+ first_use_script: script,
8664
+ layers,
8665
+ next_actions: unique(nextActions),
8666
+ warnings,
8667
+ };
8668
+ }
8310
8669
  const WORKSPACE_SKIP_DIRS = new Set([
8311
8670
  ".agent_memory",
8312
8671
  ".git",
8313
8672
  ".hg",
8314
8673
  ".next",
8315
- ".repowise",
8316
- ".repowise-workspace",
8317
8674
  "coverage",
8318
8675
  "dist",
8319
8676
  "node_modules",
@@ -11156,6 +11513,171 @@ function kageSessionReplay(projectDir, options = {}) {
11156
11513
  : "No durable candidates in this digest yet; keep observing or capture reusable learnings with kage learn.",
11157
11514
  };
11158
11515
  }
11516
+ function distilledObservationSessions(projectDir) {
11517
+ const ids = new Set();
11518
+ for (const packet of [...loadApprovedPackets(projectDir), ...loadPendingPackets(projectDir)]) {
11519
+ for (const ref of packet.source_refs) {
11520
+ if (ref.kind === "observation_session" && typeof ref.session_id === "string" && ref.session_id.trim()) {
11521
+ ids.add(ref.session_id.trim());
11522
+ }
11523
+ }
11524
+ }
11525
+ return ids;
11526
+ }
11527
+ function eventLearningCandidate(event, knownCommands) {
11528
+ if (event.type === "command_result") {
11529
+ if (typeof event.exit_code === "number" && event.exit_code !== 0 && !`${event.summary ?? ""}\n${event.text ?? ""}`.trim()) {
11530
+ return null;
11531
+ }
11532
+ const reusable = reusableCommandObservation(event, knownCommands);
11533
+ if (reusable)
11534
+ return { memory_type: "runbook", reason: reusable.learning };
11535
+ }
11536
+ if (event.type === "file_change") {
11537
+ const learning = reusableFileObservation(event);
11538
+ if (learning)
11539
+ return { memory_type: "workflow", reason: learning };
11540
+ }
11541
+ if (event.type === "user_prompt") {
11542
+ const learning = reusablePromptObservation(event);
11543
+ if (learning)
11544
+ return { memory_type: "decision", reason: learning };
11545
+ }
11546
+ return null;
11547
+ }
11548
+ function ignoredObservationReason(event) {
11549
+ if (event.type === "tool_use" || event.type === "tool_result")
11550
+ return "Tool telemetry helps replay the session but is not durable repo knowledge by itself.";
11551
+ if (event.type === "command_result" || event.type === "test_result")
11552
+ return "Verification evidence is useful for this session but needs a reusable cause, fix, or runbook before saving.";
11553
+ if (event.type === "file_change")
11554
+ return "The file touch is generic; save only if it explains a convention, workflow, bug, or invariant.";
11555
+ if (event.type === "user_prompt")
11556
+ return "The prompt is episodic; save only decisions, policies, gotchas, or reusable context.";
11557
+ return "Session bookkeeping is not durable repo memory.";
11558
+ }
11559
+ function learningLedgerContextBlock(report) {
11560
+ const lines = ["\n## Session Learning Ledger"];
11561
+ if (!report.sessions.length) {
11562
+ lines.push("No observed session events found.");
11563
+ return lines.join("\n");
11564
+ }
11565
+ lines.push(`Save candidates: ${report.totals.save_candidates}`);
11566
+ lines.push(`Needs evidence: ${report.totals.needs_evidence}`);
11567
+ if (report.totals.already_distilled)
11568
+ lines.push(`Already distilled: ${report.totals.already_distilled}`);
11569
+ lines.push("");
11570
+ lines.push("### Memory Decisions");
11571
+ for (const session of report.sessions.slice(0, 3)) {
11572
+ lines.push(`Session ${session.session_id}: ${session.save_candidates} save, ${session.needs_evidence} needs evidence, ${session.ignore_items} ignore.`);
11573
+ for (const decision of session.decisions.filter((item) => item.disposition !== "ignore").slice(0, 4)) {
11574
+ lines.push(`- ${decision.disposition}: ${decision.memory_type ?? decision.event_type} - ${decision.evidence}`);
11575
+ }
11576
+ }
11577
+ lines.push("", "### Next Actions");
11578
+ for (const action of unique(report.sessions.map((session) => session.next_action)).slice(0, 4)) {
11579
+ lines.push(`- ${action}`);
11580
+ }
11581
+ return lines.join("\n");
11582
+ }
11583
+ function kageSessionLearningLedger(projectDir, options = {}) {
11584
+ ensureMemoryDirs(projectDir);
11585
+ const limit = Math.max(1, Math.min(200, Math.floor(options.limit ?? 50)));
11586
+ const observations = loadObservations(projectDir, options.sessionId);
11587
+ const knownCommands = knownRepoCommands(projectDir);
11588
+ const distilledSessions = distilledObservationSessions(projectDir);
11589
+ const bySession = new Map();
11590
+ for (const observation of observations) {
11591
+ const rows = bySession.get(observation.session_id) ?? [];
11592
+ rows.push(observation);
11593
+ bySession.set(observation.session_id, rows);
11594
+ }
11595
+ const sessions = Array.from(bySession.entries()).map(([sessionId, rows]) => {
11596
+ const sorted = rows.slice().sort((a, b) => a.timestamp.localeCompare(b.timestamp));
11597
+ const alreadyDistilled = distilledSessions.has(sessionId);
11598
+ const distillCommand = `kage distill --project . --session ${sessionId}`;
11599
+ const decisions = sorted.map((event) => {
11600
+ const candidate = eventLearningCandidate(event, knownCommands);
11601
+ const failingEvidence = (event.type === "command_result" || event.type === "test_result") && typeof event.exit_code === "number" && event.exit_code !== 0;
11602
+ const evidence = summarize(observationDigestSummary(event)).slice(0, 220);
11603
+ if (candidate) {
11604
+ return {
11605
+ observation_id: event.id,
11606
+ timestamp: event.timestamp,
11607
+ session_id: event.session_id,
11608
+ event_type: event.type,
11609
+ disposition: alreadyDistilled ? "already_distilled" : "save",
11610
+ memory_type: candidate.memory_type,
11611
+ reason: alreadyDistilled ? "A memory packet already references this observed session." : candidate.reason,
11612
+ evidence,
11613
+ ...(event.path ? { path: event.path } : {}),
11614
+ ...(event.command ? { command: normalizeCommandText(event.command) } : {}),
11615
+ ...(typeof event.exit_code === "number" ? { exit_code: event.exit_code } : {}),
11616
+ distill_command: distillCommand,
11617
+ };
11618
+ }
11619
+ return {
11620
+ observation_id: event.id,
11621
+ timestamp: event.timestamp,
11622
+ session_id: event.session_id,
11623
+ event_type: event.type,
11624
+ disposition: failingEvidence ? "needs_evidence" : "ignore",
11625
+ reason: failingEvidence ? "A failure happened, but the observation does not yet explain a reusable cause, fix, workaround, or runbook." : ignoredObservationReason(event),
11626
+ evidence,
11627
+ ...(event.path ? { path: event.path } : {}),
11628
+ ...(event.command ? { command: normalizeCommandText(event.command) } : {}),
11629
+ ...(typeof event.exit_code === "number" ? { exit_code: event.exit_code } : {}),
11630
+ ...(failingEvidence ? { distill_command: distillCommand } : {}),
11631
+ };
11632
+ });
11633
+ const saveCandidates = decisions.filter((decision) => decision.disposition === "save").length;
11634
+ const needsEvidence = decisions.filter((decision) => decision.disposition === "needs_evidence").length;
11635
+ const ignoreItems = decisions.filter((decision) => decision.disposition === "ignore").length;
11636
+ const alreadyDistilledCount = decisions.filter((decision) => decision.disposition === "already_distilled").length;
11637
+ const nextAction = saveCandidates > 0
11638
+ ? `${distillCommand} and review save candidates before handoff.`
11639
+ : needsEvidence > 0
11640
+ ? "Add a concise cause/fix summary for failing observations before deciding whether to save them."
11641
+ : alreadyDistilledCount > 0
11642
+ ? "Session learning already has memory packets; update or supersede them only if the facts changed."
11643
+ : "No save-worthy session fact yet; keep observing without creating memory noise.";
11644
+ return {
11645
+ session_id: sessionId,
11646
+ first_at: sorted[0]?.timestamp ?? "",
11647
+ last_at: sorted.at(-1)?.timestamp ?? "",
11648
+ observations: sorted.length,
11649
+ save_candidates: saveCandidates,
11650
+ ignore_items: ignoreItems,
11651
+ needs_evidence: needsEvidence,
11652
+ already_distilled: alreadyDistilledCount,
11653
+ commands: unique(sorted.map((event) => event.command).filter(Boolean)).slice(0, 8),
11654
+ paths: unique(sorted.map((event) => event.path).filter(Boolean)).slice(0, 12),
11655
+ decisions: decisions.slice(0, limit),
11656
+ next_action: nextAction,
11657
+ };
11658
+ }).sort((a, b) => b.last_at.localeCompare(a.last_at));
11659
+ const totals = {
11660
+ sessions: sessions.length,
11661
+ observations: observations.length,
11662
+ save_candidates: sessions.reduce((sum, session) => sum + session.save_candidates, 0),
11663
+ ignore_items: sessions.reduce((sum, session) => sum + session.ignore_items, 0),
11664
+ needs_evidence: sessions.reduce((sum, session) => sum + session.needs_evidence, 0),
11665
+ already_distilled: sessions.reduce((sum, session) => sum + session.already_distilled, 0),
11666
+ };
11667
+ const reportWithoutBlock = {
11668
+ schema_version: 1,
11669
+ project_dir: projectDir,
11670
+ generated_at: nowIso(),
11671
+ ...(options.sessionId ? { selected_session_id: options.sessionId } : {}),
11672
+ totals,
11673
+ sessions,
11674
+ privacy_model: "The ledger classifies privacy-scanned observation metadata into save, ignore, needs-evidence, and already-distilled decisions; raw transcript text is not the product surface.",
11675
+ };
11676
+ return {
11677
+ ...reportWithoutBlock,
11678
+ context_block: learningLedgerContextBlock(reportWithoutBlock),
11679
+ };
11680
+ }
11159
11681
  function distillSession(projectDir, sessionId) {
11160
11682
  const observations = loadObservations(projectDir, sessionId);
11161
11683
  const candidates = [];
@@ -11525,6 +12047,7 @@ function prCheck(projectDir) {
11525
12047
  const codeInputHash = currentCodeGraphInputHash(projectDir);
11526
12048
  const memoryInputHash = knowledgeGraphInputHash(projectDir, codeInputHash);
11527
12049
  const stalePackets = loadPacketsFromDir(packetsDir(projectDir))
12050
+ .filter((packet) => packet.status === "approved" || packet.status === "pending")
11528
12051
  .map((packet) => ({ packet, reasons: staleMemoryReasons(projectDir, packet) }))
11529
12052
  .filter((entry) => entry.reasons.length)
11530
12053
  .map((entry) => staleFinding(entry.packet, entry.reasons));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "1.1.37",
3
+ "version": "1.1.38",
4
4
  "description": "Local-first repo memory, code graph, and recall MCP server for coding agents",
5
5
  "main": "dist/index.js",
6
6
  "files": [