@kage-core/kage-graph-mcp 1.1.36 → 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
@@ -45,8 +45,11 @@ exports.codeGraphDir = codeGraphDir;
45
45
  exports.structuralIndexDir = structuralIndexDir;
46
46
  exports.branchesDir = branchesDir;
47
47
  exports.reviewDir = reviewDir;
48
+ exports.auditDir = auditDir;
49
+ exports.reportsDir = reportsDir;
48
50
  exports.publicBundleDir = publicBundleDir;
49
51
  exports.observationsDir = observationsDir;
52
+ exports.slotsDir = slotsDir;
50
53
  exports.daemonDir = daemonDir;
51
54
  exports.orgRootDir = orgRootDir;
52
55
  exports.orgInboxDir = orgInboxDir;
@@ -57,6 +60,9 @@ exports.marketplaceDir = marketplaceDir;
57
60
  exports.slugify = slugify;
58
61
  exports.makePacketId = makePacketId;
59
62
  exports.parseFrontmatter = parseFrontmatter;
63
+ exports.kageMemoryAccess = kageMemoryAccess;
64
+ exports.kageMemoryLifecycle = kageMemoryLifecycle;
65
+ exports.kageMemoryReconciliation = kageMemoryReconciliation;
60
66
  exports.evaluateMemoryAdmission = evaluateMemoryAdmission;
61
67
  exports.validatePacket = validatePacket;
62
68
  exports.scanSensitiveText = scanSensitiveText;
@@ -64,6 +70,9 @@ exports.catalogDomainNodeCount = catalogDomainNodeCount;
64
70
  exports.ensureMemoryDirs = ensureMemoryDirs;
65
71
  exports.loadApprovedPackets = loadApprovedPackets;
66
72
  exports.loadPendingPackets = loadPendingPackets;
73
+ exports.recordMemoryAudit = recordMemoryAudit;
74
+ exports.kageMemoryAudit = kageMemoryAudit;
75
+ exports.kageMemoryHandoff = kageMemoryHandoff;
67
76
  exports.buildStructuralFileForWorker = buildStructuralFileForWorker;
68
77
  exports.buildStructuralIndex = buildStructuralIndex;
69
78
  exports.writeLspSymbolIndex = writeLspSymbolIndex;
@@ -75,16 +84,26 @@ exports.indexProject = indexProject;
75
84
  exports.refreshProject = refreshProject;
76
85
  exports.gcProject = gcProject;
77
86
  exports.installAgentPolicy = installAgentPolicy;
87
+ exports.createDenseEmbeddingProvider = createDenseEmbeddingProvider;
88
+ exports.buildEmbeddingIndex = buildEmbeddingIndex;
78
89
  exports.recall = recall;
90
+ exports.recallWithEmbeddings = recallWithEmbeddings;
79
91
  exports.queryCodeGraph = queryCodeGraph;
92
+ exports.kageTeammateBrief = kageTeammateBrief;
80
93
  exports.kageRisk = kageRisk;
81
94
  exports.kageDependencyPath = kageDependencyPath;
82
95
  exports.kageCleanupCandidates = kageCleanupCandidates;
83
96
  exports.kageReviewerSuggestions = kageReviewerSuggestions;
84
97
  exports.kageContributors = kageContributors;
98
+ exports.kageContextSlots = kageContextSlots;
99
+ exports.setContextSlot = setContextSlot;
100
+ exports.deleteContextSlot = deleteContextSlot;
101
+ exports.kageProjectProfile = kageProjectProfile;
102
+ exports.kageCapabilityAudit = kageCapabilityAudit;
85
103
  exports.kageDecisionIntelligence = kageDecisionIntelligence;
86
104
  exports.kageModuleHealth = kageModuleHealth;
87
105
  exports.kageGraphInsights = kageGraphInsights;
106
+ exports.kageRepoXray = kageRepoXray;
88
107
  exports.kageWorkspace = kageWorkspace;
89
108
  exports.kageWorkspaceRecall = kageWorkspaceRecall;
90
109
  exports.queryGraph = queryGraph;
@@ -94,6 +113,8 @@ exports.auditProject = auditProject;
94
113
  exports.memoryInbox = memoryInbox;
95
114
  exports.qualityReport = qualityReport;
96
115
  exports.benchmarkProject = benchmarkProject;
116
+ exports.benchmarkCodingMemoryQuality = benchmarkCodingMemoryQuality;
117
+ exports.benchmarkMemoryScale = benchmarkMemoryScale;
97
118
  exports.benchmarkTaskComparison = benchmarkTaskComparison;
98
119
  exports.learn = learn;
99
120
  exports.capture = capture;
@@ -103,6 +124,9 @@ exports.setupAgent = setupAgent;
103
124
  exports.setupDoctor = setupDoctor;
104
125
  exports.verifyAgentActivation = verifyAgentActivation;
105
126
  exports.observe = observe;
127
+ exports.kageSessionCaptureReport = kageSessionCaptureReport;
128
+ exports.kageSessionReplay = kageSessionReplay;
129
+ exports.kageSessionLearningLedger = kageSessionLearningLedger;
106
130
  exports.distillSession = distillSession;
107
131
  exports.proposeFromDiff = proposeFromDiff;
108
132
  exports.buildBranchOverlay = buildBranchOverlay;
@@ -128,6 +152,9 @@ exports.doctorProject = doctorProject;
128
152
  exports.approvePending = approvePending;
129
153
  exports.rejectPending = rejectPending;
130
154
  exports.changelog = changelog;
155
+ exports.supersedeMemory = supersedeMemory;
156
+ exports.kageMemoryLineage = kageMemoryLineage;
157
+ exports.kageMemoryTimeline = kageMemoryTimeline;
131
158
  const node_crypto_1 = require("node:crypto");
132
159
  const node_child_process_1 = require("node:child_process");
133
160
  const node_fs_1 = require("node:fs");
@@ -221,6 +248,13 @@ git commit changed without graph inputs changing.
221
248
  Before finishing a task that changed files, call \`kage_pr_summarize\` or
222
249
  \`kage_propose_from_diff\`, then call \`kage_pr_check\`.
223
250
 
251
+ \`kage_context\`, Stop hooks, and \`kage_pr_check\` may report memory
252
+ reconciliation items when files linked to existing memory changed. Resolve these
253
+ as agent work before the final response: write updated memory with
254
+ \`kage_learn\`, supersede replaced packets with \`kage_supersede\`, or mark stale
255
+ only when the memory can no longer be trusted. Do not hand this off as a user
256
+ inbox chore.
257
+
224
258
  \`kage_pr_summarize\` writes a branch review summary and a repo-local
225
259
  change-memory packet. \`kage_pr_check\` verifies validation, graph freshness,
226
260
  stale packets, and whether repo memory changed with the branch. If the check
@@ -313,12 +347,21 @@ function branchesDir(projectDir) {
313
347
  function reviewDir(projectDir) {
314
348
  return (0, node_path_1.join)(memoryRoot(projectDir), "review");
315
349
  }
350
+ function auditDir(projectDir) {
351
+ return (0, node_path_1.join)(memoryRoot(projectDir), "audit");
352
+ }
353
+ function reportsDir(projectDir) {
354
+ return (0, node_path_1.join)(memoryRoot(projectDir), "reports");
355
+ }
316
356
  function publicBundleDir(projectDir) {
317
357
  return (0, node_path_1.join)(memoryRoot(projectDir), "public-bundle");
318
358
  }
319
359
  function observationsDir(projectDir) {
320
360
  return (0, node_path_1.join)(memoryRoot(projectDir), "observations");
321
361
  }
362
+ function slotsDir(projectDir) {
363
+ return (0, node_path_1.join)(memoryRoot(projectDir), "slots");
364
+ }
322
365
  function daemonDir(projectDir) {
323
366
  return (0, node_path_1.join)(memoryRoot(projectDir), "daemon");
324
367
  }
@@ -473,6 +516,421 @@ function estimateTokens(text) {
473
516
  function packetText(packet) {
474
517
  return `${packet.title}\n${packet.summary}\n${packet.body}\n${packet.type}\n${packet.tags.join(" ")}\n${packet.paths.join(" ")}`;
475
518
  }
519
+ const ACCESS_WINDOW_DAYS = 30;
520
+ const ACCESS_RECENT_CAP = 20;
521
+ function memoryAccessPath(projectDir) {
522
+ return (0, node_path_1.join)(reportsDir(projectDir), "memory-access.json");
523
+ }
524
+ function normalizeAccessRecent(value) {
525
+ const raw = Array.isArray(value) ? value : [];
526
+ return raw
527
+ .map((item) => {
528
+ const entry = item;
529
+ const at = typeof entry.at === "string" ? entry.at : "";
530
+ const time = Date.parse(at);
531
+ const rank = Number(entry.rank);
532
+ if (!Number.isFinite(time) || !Number.isFinite(rank))
533
+ return null;
534
+ return { at, rank: Math.max(1, Math.floor(rank)) };
535
+ })
536
+ .filter((item) => Boolean(item))
537
+ .sort((a, b) => Date.parse(a.at) - Date.parse(b.at))
538
+ .slice(-ACCESS_RECENT_CAP);
539
+ }
540
+ function accessWindowCutoff() {
541
+ return Date.now() - ACCESS_WINDOW_DAYS * 86_400_000;
542
+ }
543
+ function normalizeAccessEntry(raw, packet) {
544
+ const value = raw;
545
+ const packetId = packet?.id ?? (typeof value?.packet_id === "string" ? value.packet_id : "");
546
+ if (!packetId)
547
+ return null;
548
+ const recent = normalizeAccessRecent(value?.recent);
549
+ const cutoff = accessWindowCutoff();
550
+ const uses30d = recent.filter((item) => Date.parse(item.at) >= cutoff).length;
551
+ const lastRecent = recent.at(-1);
552
+ const totalUses = Math.max(Number(value?.total_uses ?? 0) || 0, recent.length);
553
+ const lastRank = Number(value?.last_rank);
554
+ const bestRankCandidates = [
555
+ Number(value?.best_rank),
556
+ ...recent.map((item) => item.rank),
557
+ ].filter((item) => Number.isFinite(item) && item > 0);
558
+ return {
559
+ packet_id: packetId,
560
+ title: packet?.title ?? String(value?.title ?? packetId),
561
+ type: packet?.type ?? (value?.type ?? "reference"),
562
+ paths: packet?.paths ?? (Array.isArray(value?.paths) ? value.paths.map(String) : []),
563
+ tags: packet?.tags ?? (Array.isArray(value?.tags) ? value.tags.map(String) : []),
564
+ total_uses: totalUses,
565
+ uses_30d: uses30d,
566
+ last_accessed_at: lastRecent?.at ?? (typeof value?.last_accessed_at === "string" ? value.last_accessed_at : null),
567
+ best_rank: bestRankCandidates.length ? Math.min(...bestRankCandidates) : null,
568
+ last_rank: Number.isFinite(lastRank) && lastRank > 0 ? Math.floor(lastRank) : (lastRecent?.rank ?? null),
569
+ recent,
570
+ };
571
+ }
572
+ function readMemoryAccessEntries(projectDir, packets = loadApprovedPackets(projectDir)) {
573
+ const byPacket = new Map(packets.map((packet) => [packet.id, packet]));
574
+ const entries = new Map();
575
+ const path = memoryAccessPath(projectDir);
576
+ if (!(0, node_fs_1.existsSync)(path))
577
+ return entries;
578
+ try {
579
+ const raw = readJson(path);
580
+ for (const item of raw.entries ?? []) {
581
+ const id = typeof item.packet_id === "string" ? item.packet_id : "";
582
+ const normalized = normalizeAccessEntry(item, byPacket.get(id));
583
+ if (normalized)
584
+ entries.set(normalized.packet_id, normalized);
585
+ }
586
+ }
587
+ catch {
588
+ return entries;
589
+ }
590
+ return entries;
591
+ }
592
+ function memoryAccessScore(entry) {
593
+ if (!entry || entry.uses_30d <= 0)
594
+ return 0;
595
+ const rankBoost = entry.best_rank ? Math.max(0, 1.2 - (entry.best_rank - 1) * 0.2) : 0;
596
+ const useBoost = Math.min(2.8, Math.log1p(entry.uses_30d) * 0.9);
597
+ return Number((rankBoost + useBoost).toFixed(2));
598
+ }
599
+ function buildMemoryAccessRecommendations(normalized, tracked, totals, packets) {
600
+ const recommendations = [];
601
+ const packetById = new Map(packets.map((packet) => [packet.id, packet]));
602
+ const reviewable = normalized.filter((entry) => {
603
+ const packet = packetById.get(entry.packet_id);
604
+ return !packet || !isGeneratedChangeMemory(packet);
605
+ });
606
+ if (!normalized.length) {
607
+ recommendations.push({
608
+ kind: "seed_usage",
609
+ severity: "info",
610
+ summary: "No approved memory packets exist yet.",
611
+ reason: "Kage cannot learn reuse patterns until the repo has reviewable memory packets.",
612
+ action: "Capture durable decisions, bug fixes, runbooks, or gotchas with kage learn or kage propose.",
613
+ });
614
+ return recommendations;
615
+ }
616
+ if (!tracked.length) {
617
+ recommendations.push({
618
+ kind: "seed_usage",
619
+ severity: "info",
620
+ summary: "No recall usage has been observed yet.",
621
+ reason: "Memory access telemetry is local and only grows when agents naturally recall repo knowledge.",
622
+ action: "Run normal agent tasks with Kage recall enabled, then reopen this report to see hot and cold memory.",
623
+ });
624
+ }
625
+ reviewable
626
+ .filter((entry) => entry.uses_30d >= 3)
627
+ .sort((a, b) => b.uses_30d - a.uses_30d || b.total_uses - a.total_uses || (a.best_rank ?? 99) - (b.best_rank ?? 99))
628
+ .slice(0, 3)
629
+ .forEach((entry) => {
630
+ recommendations.push({
631
+ kind: "promote_hot",
632
+ severity: "ok",
633
+ packet_id: entry.packet_id,
634
+ title: entry.title,
635
+ type: entry.type,
636
+ paths: entry.paths,
637
+ uses_30d: entry.uses_30d,
638
+ total_uses: entry.total_uses,
639
+ summary: `Keep verified: ${entry.title}`,
640
+ reason: `Agents recalled this packet ${entry.uses_30d} time${entry.uses_30d === 1 ? "" : "s"} in ${ACCESS_WINDOW_DAYS} days.`,
641
+ action: "Keep the packet evidence-backed; update it when the linked workflow or code path changes.",
642
+ });
643
+ });
644
+ reviewable
645
+ .filter((entry) => entry.uses_30d === 0 && entry.paths.length === 0)
646
+ .sort((a, b) => a.title.localeCompare(b.title))
647
+ .slice(0, 3)
648
+ .forEach((entry) => {
649
+ recommendations.push({
650
+ kind: "connect_paths",
651
+ severity: "warn",
652
+ packet_id: entry.packet_id,
653
+ title: entry.title,
654
+ type: entry.type,
655
+ paths: entry.paths,
656
+ uses_30d: entry.uses_30d,
657
+ total_uses: entry.total_uses,
658
+ summary: `Add code grounding: ${entry.title}`,
659
+ reason: "This packet has no code paths, so future agents have less evidence for when to recall it.",
660
+ action: "Add the files, symbols, routes, or tests this memory explains, or supersede it if it is too generic.",
661
+ });
662
+ });
663
+ reviewable
664
+ .filter((entry) => entry.total_uses === 0)
665
+ .sort((a, b) => a.title.localeCompare(b.title))
666
+ .slice(0, 3)
667
+ .forEach((entry) => {
668
+ recommendations.push({
669
+ kind: "review_cold",
670
+ severity: totals.tracked_packets ? "warn" : "info",
671
+ packet_id: entry.packet_id,
672
+ title: entry.title,
673
+ type: entry.type,
674
+ paths: entry.paths,
675
+ uses_30d: entry.uses_30d,
676
+ total_uses: entry.total_uses,
677
+ summary: `Review cold memory: ${entry.title}`,
678
+ reason: "This approved packet has not been recalled by recent agent tasks.",
679
+ action: "Verify it is still true, improve its title/tags/paths, or mark it stale during memory review.",
680
+ });
681
+ });
682
+ return recommendations.slice(0, 8);
683
+ }
684
+ function buildMemoryAccessReport(projectDir, entries, packets = loadApprovedPackets(projectDir)) {
685
+ const activeIds = new Set(packets.map((packet) => packet.id));
686
+ const normalized = packets.map((packet) => normalizeAccessEntry(entries.get(packet.id), packet)).filter((entry) => Boolean(entry));
687
+ const tracked = normalized.filter((entry) => entry.total_uses > 0 || entry.recent.length > 0);
688
+ const totalUses = tracked.reduce((sum, entry) => sum + entry.total_uses, 0);
689
+ const uses30d = tracked.reduce((sum, entry) => sum + entry.uses_30d, 0);
690
+ const lastAccessedAt = tracked
691
+ .map((entry) => entry.last_accessed_at)
692
+ .filter((value) => Boolean(value))
693
+ .sort()
694
+ .at(-1) ?? null;
695
+ const totals = {
696
+ tracked_packets: tracked.length,
697
+ total_uses: totalUses,
698
+ uses_30d: uses30d,
699
+ hot_packets: tracked.filter((entry) => entry.uses_30d >= 3).length,
700
+ cold_packets: packets.filter((packet) => !entries.has(packet.id) || (entries.get(packet.id)?.uses_30d ?? 0) === 0).length,
701
+ active_packets_without_access: packets.filter((packet) => activeIds.has(packet.id) && (entries.get(packet.id)?.total_uses ?? 0) === 0).length,
702
+ last_accessed_at: lastAccessedAt,
703
+ };
704
+ return {
705
+ schema_version: 1,
706
+ project_dir: projectDir,
707
+ generated_at: nowIso(),
708
+ window_days: ACCESS_WINDOW_DAYS,
709
+ totals,
710
+ entries: normalized
711
+ .sort((a, b) => b.uses_30d - a.uses_30d || b.total_uses - a.total_uses || a.title.localeCompare(b.title)),
712
+ recommendations: buildMemoryAccessRecommendations(normalized, tracked, totals, packets),
713
+ };
714
+ }
715
+ function kageMemoryAccess(projectDir) {
716
+ ensureMemoryDirs(projectDir);
717
+ const packets = loadApprovedPackets(projectDir);
718
+ return buildMemoryAccessReport(projectDir, readMemoryAccessEntries(projectDir, packets), packets);
719
+ }
720
+ function lifecycleFreshness(packet) {
721
+ const freshness = (packet.freshness ?? {});
722
+ const ttlRaw = Number(freshness.ttl_days ?? freshness.ttlDays);
723
+ const ttlDays = Number.isFinite(ttlRaw) && ttlRaw > 0 ? Math.floor(ttlRaw) : null;
724
+ const verifiedAt = typeof freshness.last_verified_at === "string"
725
+ ? freshness.last_verified_at
726
+ : (packet.updated_at || packet.created_at || null);
727
+ const verifiedTime = verifiedAt ? Date.parse(verifiedAt) : Number.NaN;
728
+ const ageDays = Number.isFinite(verifiedTime)
729
+ ? Math.max(0, Math.floor((Date.now() - verifiedTime) / 86_400_000))
730
+ : null;
731
+ return {
732
+ ttl_days: ttlDays,
733
+ last_verified_at: verifiedAt,
734
+ age_days: ageDays,
735
+ expired: ttlDays !== null && ageDays !== null && ageDays > ttlDays,
736
+ };
737
+ }
738
+ function lifecycleActionForPacket(packet, access, staleReasons) {
739
+ const quality = (packet.quality ?? {});
740
+ const reportsStale = Number(quality.reports_stale ?? 0);
741
+ const votesDown = Number(quality.votes_down ?? 0);
742
+ if (packet.status === "pending") {
743
+ return {
744
+ health: "cold",
745
+ recommended_action: "review_pending",
746
+ severity: "warn",
747
+ reason: "This packet is pending and has not crossed the repo review boundary.",
748
+ action: "Approve, reject, merge, or keep pending after checking evidence and sensitivity.",
749
+ };
750
+ }
751
+ if (isGeneratedChangeMemory(packet)) {
752
+ return {
753
+ health: "generated",
754
+ recommended_action: "archive_generated",
755
+ severity: "info",
756
+ reason: "This is generated branch/change context, useful for handoff but not durable repo lore.",
757
+ action: "Keep it as branch context while relevant; supersede it with a concise human-reviewed memory if the lesson is durable.",
758
+ };
759
+ }
760
+ if (staleReasons.length) {
761
+ return {
762
+ health: reportsStale || votesDown ? "disputed" : "stale",
763
+ recommended_action: reportsStale || votesDown ? "resolve_feedback" : "review_stale",
764
+ severity: "blocker",
765
+ reason: staleReasons[0],
766
+ action: "Verify, update, supersede, or deprecate this memory before trusting it in recall.",
767
+ };
768
+ }
769
+ if (!packet.paths.filter(meaningfulMemoryPath).length) {
770
+ return {
771
+ health: "ungrounded",
772
+ recommended_action: "add_grounding",
773
+ severity: "warn",
774
+ reason: "This approved memory has no concrete code path grounding.",
775
+ action: "Add relevant files, symbols, routes, tests, or docs so agents know when to recall it.",
776
+ };
777
+ }
778
+ if ((access?.uses_30d ?? 0) >= 3) {
779
+ return {
780
+ health: "hot",
781
+ recommended_action: "promote_hot",
782
+ severity: "ok",
783
+ reason: `Agents recalled this memory ${access?.uses_30d ?? 0} times in the last ${ACCESS_WINDOW_DAYS} days.`,
784
+ action: "Keep it verified and evidence-backed; treat it as high-value repo lore.",
785
+ };
786
+ }
787
+ if ((access?.total_uses ?? 0) === 0) {
788
+ return {
789
+ health: "cold",
790
+ recommended_action: "seed_usage",
791
+ severity: "info",
792
+ reason: "This approved memory has not been recalled by local agent tasks yet.",
793
+ action: "Keep it if it is durable, but improve title/tags/paths if future agents are not finding it.",
794
+ };
795
+ }
796
+ return {
797
+ health: "healthy",
798
+ recommended_action: "keep_verified",
799
+ severity: "ok",
800
+ reason: "This memory is approved, grounded, non-stale, and has recall history.",
801
+ action: "Keep it current when the linked code or workflow changes.",
802
+ };
803
+ }
804
+ function buildLifecycleRecommendations(items) {
805
+ const order = [
806
+ "resolve_feedback",
807
+ "review_stale",
808
+ "add_grounding",
809
+ "review_pending",
810
+ "promote_hot",
811
+ "seed_usage",
812
+ "archive_generated",
813
+ "keep_verified",
814
+ ];
815
+ const recommendations = [];
816
+ for (const action of order) {
817
+ const matches = items.filter((item) => item.recommended_action === action);
818
+ if (!matches.length)
819
+ continue;
820
+ const first = matches[0];
821
+ const count = matches.length;
822
+ const summary = count === 1
823
+ ? first.action
824
+ : `${first.action} (${count} packet${count === 1 ? "" : "s"})`;
825
+ recommendations.push({
826
+ kind: action,
827
+ severity: first.severity,
828
+ packet_id: first.packet_id,
829
+ title: first.title,
830
+ summary,
831
+ reason: first.reason,
832
+ action: first.action,
833
+ });
834
+ }
835
+ return recommendations.slice(0, 8);
836
+ }
837
+ function kageMemoryLifecycle(projectDir) {
838
+ ensureMemoryDirs(projectDir);
839
+ const packets = [...loadApprovedPackets(projectDir), ...loadPendingPackets(projectDir)]
840
+ .sort((a, b) => a.title.localeCompare(b.title));
841
+ const access = readMemoryAccessEntries(projectDir, packets.filter((packet) => packet.status === "approved"));
842
+ const items = packets.map((packet) => {
843
+ const accessEntry = access.get(packet.id);
844
+ const staleReasons = staleMemoryReasons(projectDir, packet);
845
+ const action = lifecycleActionForPacket(packet, accessEntry, staleReasons);
846
+ const quality = (packet.quality ?? {});
847
+ const item = {
848
+ packet_id: packet.id,
849
+ title: packet.title,
850
+ type: packet.type,
851
+ status: packet.status,
852
+ health: action.health,
853
+ recommended_action: action.recommended_action,
854
+ severity: action.severity,
855
+ paths: packet.paths,
856
+ tags: packet.tags,
857
+ source_refs: packet.source_refs.length,
858
+ uses_30d: accessEntry?.uses_30d ?? 0,
859
+ total_uses: accessEntry?.total_uses ?? 0,
860
+ last_accessed_at: accessEntry?.last_accessed_at ?? null,
861
+ feedback: {
862
+ votes_up: Number(quality.votes_up ?? 0),
863
+ votes_down: Number(quality.votes_down ?? 0),
864
+ reports_stale: Number(quality.reports_stale ?? 0),
865
+ score: packetFeedbackScore(packet),
866
+ },
867
+ freshness: lifecycleFreshness(packet),
868
+ stale_reasons: staleReasons,
869
+ reason: action.reason,
870
+ action: action.action,
871
+ };
872
+ return item;
873
+ });
874
+ return {
875
+ schema_version: 1,
876
+ project_dir: projectDir,
877
+ generated_at: nowIso(),
878
+ totals: {
879
+ approved: packets.filter((packet) => packet.status === "approved").length,
880
+ pending: packets.filter((packet) => packet.status === "pending").length,
881
+ deprecated: packets.filter((packet) => packet.status === "deprecated").length,
882
+ superseded: packets.filter((packet) => packet.status === "superseded").length,
883
+ healthy: items.filter((item) => item.health === "healthy").length,
884
+ hot: items.filter((item) => item.health === "hot").length,
885
+ cold: items.filter((item) => item.health === "cold").length,
886
+ stale: items.filter((item) => item.health === "stale" || item.health === "disputed").length,
887
+ disputed: items.filter((item) => item.health === "disputed").length,
888
+ ungrounded: items.filter((item) => item.health === "ungrounded").length,
889
+ generated: items.filter((item) => item.health === "generated").length,
890
+ with_evidence: packets.filter((packet) => packet.source_refs.length > 0).length,
891
+ with_paths: packets.filter((packet) => packet.paths.filter(meaningfulMemoryPath).length > 0).length,
892
+ },
893
+ items: items.sort((a, b) => {
894
+ const severityRank = { blocker: 0, warn: 1, info: 2, ok: 3 };
895
+ return severityRank[a.severity] - severityRank[b.severity]
896
+ || b.uses_30d - a.uses_30d
897
+ || a.title.localeCompare(b.title);
898
+ }),
899
+ recommendations: buildLifecycleRecommendations(items),
900
+ };
901
+ }
902
+ function recordRecallAccess(projectDir, results) {
903
+ if (!results.length)
904
+ return;
905
+ try {
906
+ ensureMemoryDirs(projectDir);
907
+ const packets = loadApprovedPackets(projectDir);
908
+ const byPacket = new Map(packets.map((packet) => [packet.id, packet]));
909
+ const entries = readMemoryAccessEntries(projectDir, packets);
910
+ const at = nowIso();
911
+ results.slice(0, 10).forEach((result, index) => {
912
+ const packet = byPacket.get(result.packet.id);
913
+ if (!packet)
914
+ return;
915
+ const current = normalizeAccessEntry(entries.get(packet.id), packet) ?? normalizeAccessEntry({ packet_id: packet.id }, packet);
916
+ if (!current)
917
+ return;
918
+ const rank = index + 1;
919
+ current.total_uses += 1;
920
+ current.last_accessed_at = at;
921
+ current.last_rank = rank;
922
+ current.best_rank = current.best_rank ? Math.min(current.best_rank, rank) : rank;
923
+ current.recent.push({ at, rank });
924
+ current.recent = normalizeAccessRecent(current.recent);
925
+ current.uses_30d = current.recent.filter((item) => Date.parse(item.at) >= accessWindowCutoff()).length;
926
+ entries.set(packet.id, current);
927
+ });
928
+ writeJson(memoryAccessPath(projectDir), buildMemoryAccessReport(projectDir, entries, packets));
929
+ }
930
+ catch {
931
+ // Recall should never fail because local access telemetry could not be updated.
932
+ }
933
+ }
476
934
  function isGeneratedChangeMemory(packet) {
477
935
  return packet.type === "workflow"
478
936
  && packet.tags.includes("change-memory")
@@ -492,11 +950,36 @@ function jaccard(a, b) {
492
950
  return intersection / (a.size + b.size - intersection);
493
951
  }
494
952
  function duplicateCandidates(projectDir, packet, threshold = 0.58) {
495
- const current = tokenSet(packetText(packet));
496
- return [...loadApprovedPackets(projectDir), ...loadPendingPackets(projectDir)]
953
+ return duplicateCandidatesWithContext(packet, memoryQualityContext(projectDir), threshold);
954
+ }
955
+ function memoryQualityContext(projectDir) {
956
+ const packets = [...loadApprovedPackets(projectDir), ...loadPendingPackets(projectDir)];
957
+ return {
958
+ packets,
959
+ tokenSets: new Map(packets.map((packet) => [packet.id, tokenSet(packetText(packet))])),
960
+ };
961
+ }
962
+ function duplicateCandidatesWithContext(packet, context, threshold = 0.58) {
963
+ const current = context.tokenSets.get(packet.id) ?? tokenSet(packetText(packet));
964
+ const packetTags = new Set(packet.tags);
965
+ const packetPaths = new Set(packet.paths);
966
+ const candidates = context.packets.length <= 100
967
+ ? context.packets
968
+ : context.packets
969
+ .map((candidate) => {
970
+ const sharedTags = candidate.tags.filter((tag) => packetTags.has(tag)).length;
971
+ const sharedPaths = candidate.paths.filter((path) => packetPaths.has(path)).length;
972
+ const typeMatch = candidate.type === packet.type ? 1 : 0;
973
+ return { candidate, preScore: sharedPaths * 3 + sharedTags * 2 + typeMatch };
974
+ })
975
+ .filter((entry) => entry.preScore > 0)
976
+ .sort((a, b) => b.preScore - a.preScore || a.candidate.title.localeCompare(b.candidate.title))
977
+ .slice(0, 250)
978
+ .map((entry) => entry.candidate);
979
+ return candidates
497
980
  .filter((candidate) => candidate.id !== packet.id)
498
981
  .filter((candidate) => !(isGeneratedChangeMemory(packet) && isGeneratedChangeMemory(candidate)))
499
- .map((candidate) => ({ packet: candidate, score: jaccard(current, tokenSet(packetText(candidate))) }))
982
+ .map((candidate) => ({ packet: candidate, score: jaccard(current, context.tokenSets.get(candidate.id) ?? tokenSet(packetText(candidate))) }))
500
983
  .filter((entry) => entry.score >= threshold)
501
984
  .sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title))
502
985
  .slice(0, 5)
@@ -508,16 +991,98 @@ function duplicateCandidates(projectDir, packet, threshold = 0.58) {
508
991
  }));
509
992
  }
510
993
  function packetFeedbackScore(packet) {
511
- const quality = packet.quality;
994
+ const quality = (packet.quality ?? {});
512
995
  return Number(quality.votes_up ?? 0) * 2 - Number(quality.votes_down ?? 0) * 3 - Number(quality.reports_stale ?? 0) * 4;
513
996
  }
997
+ function recallQualityScore(packet) {
998
+ const stored = Number((packet.quality ?? {}).score);
999
+ if (Number.isFinite(stored))
1000
+ return Math.max(0, Math.min(10, stored / 10));
1001
+ let score = 45;
1002
+ if (["runbook", "bug_fix", "decision", "rationale", "convention", "workflow", "gotcha", "policy", "issue_context", "code_explanation", "negative_result", "constraint"].includes(packet.type))
1003
+ score += 14;
1004
+ if (packet.source_refs.length)
1005
+ score += 12;
1006
+ if (packet.paths.length)
1007
+ score += 10;
1008
+ if (packet.tags.length)
1009
+ score += 5;
1010
+ const bodyTokens = tokenize(packet.body).length;
1011
+ if (bodyTokens >= 12 && bodyTokens <= 180)
1012
+ score += 10;
1013
+ if (/(verified by|evidence:|because|root cause|rationale|decision|run|command|avoid|prefer)/i.test(packet.body))
1014
+ score += 8;
1015
+ if (packet.body.length < 60)
1016
+ score -= 18;
1017
+ if (!packet.paths.length && !["repo_map", "reference", "policy"].includes(packet.type))
1018
+ score -= 10;
1019
+ if (!packet.source_refs.length)
1020
+ score -= 12;
1021
+ return Math.max(0, Math.min(10, score / 10));
1022
+ }
514
1023
  function meaningfulMemoryPath(path) {
515
1024
  return path !== "root" && path !== "." && !isNoisePath(path);
516
1025
  }
517
- function staleMemoryReasons(projectDir, packet) {
1026
+ function fingerprintableMemoryPath(path) {
1027
+ const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "");
1028
+ return meaningfulMemoryPath(normalized) && !normalized.startsWith(".agent_memory/");
1029
+ }
1030
+ function memoryPathFingerprint(projectDir, path, cache) {
1031
+ const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "");
1032
+ if (!fingerprintableMemoryPath(normalized))
1033
+ return null;
1034
+ const cacheKey = `${projectDir}\0${normalized}`;
1035
+ if (cache?.has(cacheKey))
1036
+ return cache.get(cacheKey) ?? null;
1037
+ const absolutePath = (0, node_path_1.join)(projectDir, normalized);
1038
+ try {
1039
+ const stats = (0, node_fs_1.statSync)(absolutePath);
1040
+ if (!stats.isFile()) {
1041
+ cache?.set(cacheKey, null);
1042
+ return null;
1043
+ }
1044
+ const fingerprint = {
1045
+ path: normalized,
1046
+ sha256: sha256Hex((0, node_fs_1.readFileSync)(absolutePath)),
1047
+ size: stats.size,
1048
+ };
1049
+ cache?.set(cacheKey, fingerprint);
1050
+ return fingerprint;
1051
+ }
1052
+ catch {
1053
+ cache?.set(cacheKey, null);
1054
+ return null;
1055
+ }
1056
+ }
1057
+ function memoryPathFingerprints(projectDir, paths) {
1058
+ const fingerprints = [];
1059
+ for (const path of unique(paths).filter(fingerprintableMemoryPath)) {
1060
+ const fingerprint = memoryPathFingerprint(projectDir, path);
1061
+ if (fingerprint)
1062
+ fingerprints.push(fingerprint);
1063
+ }
1064
+ return fingerprints;
1065
+ }
1066
+ function packetStoredPathFingerprints(packet) {
1067
+ const raw = (packet.freshness ?? {}).path_fingerprints;
1068
+ if (!Array.isArray(raw))
1069
+ return [];
1070
+ return raw.flatMap((item) => {
1071
+ if (!item || typeof item !== "object")
1072
+ return [];
1073
+ const record = item;
1074
+ const path = typeof record.path === "string" ? record.path : "";
1075
+ const sha256 = typeof record.sha256 === "string" ? record.sha256 : "";
1076
+ const size = Number(record.size ?? 0);
1077
+ if (!path || !sha256 || !Number.isFinite(size) || !fingerprintableMemoryPath(path))
1078
+ return [];
1079
+ return [{ path, sha256, size }];
1080
+ });
1081
+ }
1082
+ function staleMemoryReasons(projectDir, packet, fingerprintCache) {
518
1083
  const reasons = [];
519
- const quality = packet.quality;
520
- const freshness = packet.freshness;
1084
+ const quality = (packet.quality ?? {});
1085
+ const freshness = (packet.freshness ?? {});
521
1086
  if (packet.status === "deprecated" || packet.status === "superseded") {
522
1087
  reasons.push(`packet status is ${packet.status}`);
523
1088
  }
@@ -539,10 +1104,103 @@ function staleMemoryReasons(projectDir, packet) {
539
1104
  else if (missingPaths.length > 0) {
540
1105
  reasons.push(`some referenced paths are missing: ${missingPaths.slice(0, 4).join(", ")}`);
541
1106
  }
1107
+ if (freshness.path_fingerprint_policy === "source_hash_staleness") {
1108
+ const storedFingerprints = packetStoredPathFingerprints(packet);
1109
+ const changedPaths = storedFingerprints
1110
+ .filter((fingerprint) => (0, node_fs_1.existsSync)((0, node_path_1.join)(projectDir, fingerprint.path)))
1111
+ .filter((fingerprint) => {
1112
+ const current = memoryPathFingerprint(projectDir, fingerprint.path, fingerprintCache);
1113
+ return current !== null && current.sha256 !== fingerprint.sha256;
1114
+ })
1115
+ .map((fingerprint) => fingerprint.path);
1116
+ if (changedPaths.length) {
1117
+ reasons.push(`linked path changed since memory was verified: ${changedPaths.slice(0, 4).join(", ")}`);
1118
+ }
1119
+ }
542
1120
  return unique(reasons);
543
1121
  }
544
- function classifyPacket(projectDir, packet) {
545
- const quality = evaluateMemoryQuality(projectDir, packet);
1122
+ function changedPathsFromStaleReasons(reasons) {
1123
+ return unique(reasons.flatMap((reason) => {
1124
+ const match = reason.match(/^linked path changed since memory was verified: (.+)$/);
1125
+ if (!match)
1126
+ return [];
1127
+ return match[1].split(",").map((path) => path.trim()).filter(Boolean);
1128
+ }));
1129
+ }
1130
+ function observationTouchedPaths(observations) {
1131
+ return unique(observations
1132
+ .filter((event) => event.type === "file_change" && typeof event.path === "string" && event.path.trim().length > 0)
1133
+ .map((event) => event.path.replace(/\\/g, "/").replace(/^\/+/, ""))
1134
+ .filter(meaningfulMemoryPath)).sort();
1135
+ }
1136
+ function packetPathSet(packet) {
1137
+ return new Set(packet.paths.map((path) => path.replace(/\\/g, "/").replace(/^\/+/, "")).filter(meaningfulMemoryPath));
1138
+ }
1139
+ function reconciliationInstruction(items) {
1140
+ if (!items.length)
1141
+ return "No agent memory reconciliation is required.";
1142
+ const lines = items.slice(0, 5).map((item) => `- ${item.packet_id}: ${item.title} (${item.changed_paths.join(", ") || item.paths.join(", ")}) -> ${item.next_action}`);
1143
+ return [
1144
+ "Memory reconciliation required before final response.",
1145
+ "You changed code that is linked to existing repo memory. Update the memory yourself; do not push this to the user as a manual inbox chore.",
1146
+ "For each item, call kage_learn to save the new behavior and kage_supersede when replacing the old packet, or mark stale only if the memory is no longer trusted.",
1147
+ ...lines,
1148
+ ].join("\n");
1149
+ }
1150
+ function kageMemoryReconciliation(projectDir, options = {}) {
1151
+ ensureMemoryDirs(projectDir);
1152
+ const observations = loadObservations(projectDir, options.sessionId);
1153
+ const touchedPaths = observationTouchedPaths(observations);
1154
+ const sessionIdsByPath = new Map();
1155
+ for (const event of observations) {
1156
+ if (event.type !== "file_change" || !event.path)
1157
+ continue;
1158
+ const path = event.path.replace(/\\/g, "/").replace(/^\/+/, "");
1159
+ if (!meaningfulMemoryPath(path))
1160
+ continue;
1161
+ const sessions = sessionIdsByPath.get(path) ?? new Set();
1162
+ sessions.add(event.session_id);
1163
+ sessionIdsByPath.set(path, sessions);
1164
+ }
1165
+ const fingerprintCache = new Map();
1166
+ const items = loadApprovedPackets(projectDir)
1167
+ .flatMap((packet) => {
1168
+ if ((packet.freshness ?? {}).path_fingerprint_policy !== "source_hash_staleness")
1169
+ return [];
1170
+ const reasons = staleMemoryReasons(projectDir, packet, fingerprintCache);
1171
+ const changedPaths = changedPathsFromStaleReasons(reasons);
1172
+ if (!changedPaths.length)
1173
+ return [];
1174
+ const paths = packetPathSet(packet);
1175
+ const observedSessionIds = unique(changedPaths.flatMap((path) => [...(sessionIdsByPath.get(path) ?? new Set())])).sort();
1176
+ const relevantTouchedPaths = touchedPaths.filter((path) => paths.has(path));
1177
+ return [{
1178
+ packet_id: packet.id,
1179
+ title: packet.title,
1180
+ type: packet.type,
1181
+ status: packet.status,
1182
+ paths: packet.paths,
1183
+ changed_paths: unique([...changedPaths, ...relevantTouchedPaths]).sort(),
1184
+ observed_session_ids: observedSessionIds,
1185
+ stale_reasons: reasons,
1186
+ suggested_action: "agent_update_or_supersede",
1187
+ next_action: `Agent must update repo memory for ${packet.id}: call kage learn with the new verified behavior, then kage supersede --packet ${packet.id} --replacement <new-packet-id> if this packet is replaced.`,
1188
+ }];
1189
+ })
1190
+ .sort((a, b) => a.title.localeCompare(b.title))
1191
+ .slice(0, Math.max(1, options.limit ?? 25));
1192
+ return {
1193
+ ok: items.length === 0,
1194
+ project_dir: projectDir,
1195
+ generated_at: nowIso(),
1196
+ session_id: options.sessionId,
1197
+ touched_paths: touchedPaths,
1198
+ unresolved_count: items.length,
1199
+ items,
1200
+ agent_instruction: reconciliationInstruction(items),
1201
+ };
1202
+ }
1203
+ function classifyPacket(projectDir, packet, context, quality = evaluateMemoryQuality(projectDir, packet, context)) {
546
1204
  const score = Number(quality.score);
547
1205
  const duplicates = quality.duplicate_candidates;
548
1206
  if (staleMemoryReasons(projectDir, packet).length)
@@ -566,7 +1224,7 @@ function suggestedAction(classification, status) {
566
1224
  return "approve";
567
1225
  return "keep";
568
1226
  }
569
- function evaluateMemoryQuality(projectDir, packet) {
1227
+ function evaluateMemoryQuality(projectDir, packet, context) {
570
1228
  const reasons = [];
571
1229
  const risks = [];
572
1230
  let score = 45;
@@ -610,7 +1268,7 @@ function evaluateMemoryQuality(projectDir, packet) {
610
1268
  score -= 12;
611
1269
  risks.push("missing source evidence");
612
1270
  }
613
- const duplicates = duplicateCandidates(projectDir, packet);
1271
+ const duplicates = context ? duplicateCandidatesWithContext(packet, context) : duplicateCandidates(projectDir, packet);
614
1272
  if (duplicates.length) {
615
1273
  score -= 18;
616
1274
  risks.push("possible duplicate memory");
@@ -841,9 +1499,12 @@ function ensureMemoryDirs(projectDir) {
841
1499
  ensureDir(graphDir(projectDir));
842
1500
  ensureDir(codeGraphDir(projectDir));
843
1501
  ensureDir(branchesDir(projectDir));
1502
+ ensureDir(auditDir(projectDir));
844
1503
  ensureDir(reviewDir(projectDir));
1504
+ ensureDir(reportsDir(projectDir));
845
1505
  ensureDir(publicBundleDir(projectDir));
846
1506
  ensureDir(observationsDir(projectDir));
1507
+ ensureDir(slotsDir(projectDir));
847
1508
  ensureDir(daemonDir(projectDir));
848
1509
  ensureDir(globalCdnDir(projectDir));
849
1510
  ensureDir(marketplaceDir(projectDir));
@@ -896,6 +1557,299 @@ function writePacket(projectDir, packet, statusDir) {
896
1557
  writeJson(path, packet);
897
1558
  return path;
898
1559
  }
1560
+ function memoryAuditPath(projectDir) {
1561
+ return (0, node_path_1.join)(auditDir(projectDir), "events.jsonl");
1562
+ }
1563
+ function auditActor() {
1564
+ return process.env.KAGE_ACTOR || process.env.USER || process.env.LOGNAME || "repo-local-agent";
1565
+ }
1566
+ function recordMemoryAudit(projectDir, operation, packets, details = {}) {
1567
+ ensureDir(auditDir(projectDir));
1568
+ const timestamp = nowIso();
1569
+ const packetIds = unique(packets.map((packet) => packet.id).filter(Boolean));
1570
+ const entry = {
1571
+ schema_version: 1,
1572
+ id: `audit:${(0, node_crypto_1.createHash)("sha256").update(`${timestamp}:${operation}:${packetIds.join(",")}`).digest("hex").slice(0, 16)}`,
1573
+ timestamp,
1574
+ operation,
1575
+ packet_ids: packetIds,
1576
+ packet_titles: unique(packets.map((packet) => packet.title).filter(Boolean)),
1577
+ actor: auditActor(),
1578
+ branch: gitBranch(projectDir),
1579
+ head: gitHead(projectDir),
1580
+ details,
1581
+ };
1582
+ (0, node_fs_1.writeFileSync)(memoryAuditPath(projectDir), `${JSON.stringify(entry)}\n`, { encoding: "utf8", flag: "a" });
1583
+ return entry;
1584
+ }
1585
+ function loadMemoryAuditEntries(projectDir) {
1586
+ const path = memoryAuditPath(projectDir);
1587
+ if (!(0, node_fs_1.existsSync)(path))
1588
+ return [];
1589
+ return (0, node_fs_1.readFileSync)(path, "utf8")
1590
+ .split(/\r?\n/)
1591
+ .map((line) => line.trim())
1592
+ .filter(Boolean)
1593
+ .map((line) => JSON.parse(line))
1594
+ .filter((entry) => entry.schema_version === 1 && Boolean(entry.operation));
1595
+ }
1596
+ function kageMemoryAudit(projectDir, limit = 100) {
1597
+ ensureMemoryDirs(projectDir);
1598
+ const entries = loadMemoryAuditEntries(projectDir)
1599
+ .sort((a, b) => b.timestamp.localeCompare(a.timestamp) || a.id.localeCompare(b.id));
1600
+ const boundedLimit = Math.max(1, Math.min(500, Math.floor(Number(limit) || 100)));
1601
+ const totals = {
1602
+ total: entries.length,
1603
+ capture: 0,
1604
+ feedback: 0,
1605
+ approve: 0,
1606
+ reject: 0,
1607
+ supersede: 0,
1608
+ deprecate: 0,
1609
+ delete: 0,
1610
+ };
1611
+ for (const entry of entries) {
1612
+ totals[entry.operation] = Number(totals[entry.operation] || 0) + 1;
1613
+ }
1614
+ return {
1615
+ schema_version: 1,
1616
+ project_dir: projectDir,
1617
+ generated_at: nowIso(),
1618
+ path: memoryAuditPath(projectDir),
1619
+ totals,
1620
+ entries: entries.slice(0, boundedLimit),
1621
+ recommendations: unique([
1622
+ entries.length ? "Review the memory audit trail before handoff when repo knowledge changed." : "No memory audit entries yet; explicit memory mutations will be logged here.",
1623
+ ...(totals.supersede ? ["Check supersede audit entries with kage lineage so agents use current memory."] : []),
1624
+ ...(totals.reject ? ["Rejected memory is preserved in the audit trail; capture a better packet if the lesson is still useful."] : []),
1625
+ ]),
1626
+ };
1627
+ }
1628
+ function handoffLifecycleSeverity(severity) {
1629
+ if (severity === "blocker")
1630
+ return "blocker";
1631
+ if (severity === "warn")
1632
+ return "warning";
1633
+ if (severity === "info")
1634
+ return "info";
1635
+ return "ok";
1636
+ }
1637
+ function handoffTimelineSeverity(kind) {
1638
+ if (kind === "pending" || kind === "deprecated")
1639
+ return "warning";
1640
+ return "info";
1641
+ }
1642
+ function handoffAuditSeverity(operation) {
1643
+ if (operation === "reject" || operation === "deprecate" || operation === "delete")
1644
+ return "warning";
1645
+ return "info";
1646
+ }
1647
+ function handoffAuditAction(operation) {
1648
+ if (operation === "capture")
1649
+ return "Review whether this new memory is grounded, reusable, and useful for the next agent.";
1650
+ if (operation === "feedback")
1651
+ return "Check feedback before trusting or promoting this packet.";
1652
+ if (operation === "approve")
1653
+ return "Use this approved packet as shared repo memory when it matches the task.";
1654
+ if (operation === "reject")
1655
+ return "Keep the rejection as audit history; capture a better packet if the lesson is still useful.";
1656
+ if (operation === "supersede")
1657
+ return "Use the replacement packet and avoid relying on retired memory.";
1658
+ if (operation === "deprecate")
1659
+ return "Check whether a newer packet explains why this memory was retired.";
1660
+ return "Confirm deleted memory was intentionally removed and not needed for handoff.";
1661
+ }
1662
+ function memoryHandoffDedupeKey(item) {
1663
+ return [
1664
+ item.kind,
1665
+ item.severity,
1666
+ item.packet_ids.join(","),
1667
+ item.title,
1668
+ item.summary,
1669
+ ].join(":");
1670
+ }
1671
+ function handoffPrimaryAction(items, openItems) {
1672
+ const urgent = items.find((item) => item.severity === "blocker" || item.severity === "warning");
1673
+ if (urgent) {
1674
+ return {
1675
+ label: "Resolve handoff",
1676
+ summary: urgent.summary,
1677
+ action: urgent.action,
1678
+ severity: urgent.severity,
1679
+ target: urgent.kind === "lifecycle" || urgent.kind === "session" ? "memory" : "review",
1680
+ packet_ids: urgent.packet_ids,
1681
+ };
1682
+ }
1683
+ const recent = items.find((item) => item.kind === "audit" || item.kind === "timeline");
1684
+ if (recent) {
1685
+ return {
1686
+ label: "Review recent memory",
1687
+ summary: recent.summary,
1688
+ action: recent.action,
1689
+ severity: recent.severity,
1690
+ target: "review",
1691
+ packet_ids: recent.packet_ids,
1692
+ };
1693
+ }
1694
+ return {
1695
+ label: openItems ? "Resolve handoff" : "Ready for handoff",
1696
+ summary: openItems ? `${openItems} memory handoff item${openItems === 1 ? "" : "s"} need review.` : "No memory handoff blockers are open.",
1697
+ action: openItems ? "Open the review queue before another agent relies on this memory." : "Hand work to another teammate or agent with current repo memory.",
1698
+ severity: openItems ? "warning" : "ok",
1699
+ target: "review",
1700
+ packet_ids: [],
1701
+ };
1702
+ }
1703
+ function kageMemoryHandoff(projectDir) {
1704
+ ensureMemoryDirs(projectDir);
1705
+ const inbox = memoryInbox(projectDir);
1706
+ const lifecycle = kageMemoryLifecycle(projectDir);
1707
+ const audit = kageMemoryAudit(projectDir, 20);
1708
+ const timeline = kageMemoryTimeline(projectDir, 14);
1709
+ const lineage = kageMemoryLineage(projectDir);
1710
+ const sessions = kageSessionCaptureReport(projectDir);
1711
+ const items = [];
1712
+ for (const item of inbox.items.slice(0, 8)) {
1713
+ items.push({
1714
+ kind: "inbox",
1715
+ severity: item.severity,
1716
+ title: item.title || item.kind.replace(/_/g, " "),
1717
+ summary: item.summary,
1718
+ action: item.action,
1719
+ packet_ids: item.packet_id ? [item.packet_id] : [],
1720
+ paths: item.paths ?? [],
1721
+ });
1722
+ }
1723
+ for (const item of lifecycle.recommendations.slice(0, 8)) {
1724
+ const action = item.kind === "add_grounding"
1725
+ ? "Add repo paths, symbols, routes, tests, or docs this memory explains before handoff."
1726
+ : item.action;
1727
+ items.push({
1728
+ kind: "lifecycle",
1729
+ severity: handoffLifecycleSeverity(item.severity),
1730
+ title: item.title || item.kind.replace(/_/g, " "),
1731
+ summary: item.summary,
1732
+ action,
1733
+ packet_ids: item.packet_id ? [item.packet_id] : [],
1734
+ paths: [],
1735
+ });
1736
+ }
1737
+ for (const entry of audit.entries.slice(0, 8)) {
1738
+ items.push({
1739
+ kind: "audit",
1740
+ severity: handoffAuditSeverity(entry.operation),
1741
+ title: entry.packet_titles[0] || entry.operation,
1742
+ summary: `Memory mutation: ${entry.operation}`,
1743
+ action: handoffAuditAction(entry.operation),
1744
+ packet_ids: entry.packet_ids,
1745
+ paths: [],
1746
+ date: entry.timestamp,
1747
+ });
1748
+ }
1749
+ for (const entry of timeline.entries.slice(0, 8)) {
1750
+ items.push({
1751
+ kind: "timeline",
1752
+ severity: handoffTimelineSeverity(entry.kind),
1753
+ title: entry.title,
1754
+ summary: `${entry.kind}: ${entry.summary}`,
1755
+ action: entry.action,
1756
+ packet_ids: [entry.packet_id],
1757
+ paths: entry.paths,
1758
+ date: entry.date,
1759
+ });
1760
+ }
1761
+ for (const session of sessions.sessions.filter((item) => item.durable_observations > 0).slice(0, 8)) {
1762
+ items.push({
1763
+ kind: "session",
1764
+ severity: "warning",
1765
+ title: session.session_id,
1766
+ summary: `${session.durable_observations} distillable observation${session.durable_observations === 1 ? "" : "s"} from ${session.agents.join(", ") || "agent"} session.`,
1767
+ action: session.next_action,
1768
+ packet_ids: [],
1769
+ paths: session.paths,
1770
+ date: session.last_at,
1771
+ });
1772
+ }
1773
+ for (const orphan of lineage.orphans.slice(0, 8)) {
1774
+ items.push({
1775
+ kind: "lineage",
1776
+ severity: "warning",
1777
+ title: orphan.title,
1778
+ summary: orphan.reason,
1779
+ action: orphan.action,
1780
+ packet_ids: [orphan.packet_id],
1781
+ paths: [],
1782
+ date: orphan.updated_at,
1783
+ });
1784
+ }
1785
+ for (const chain of lineage.chains.slice(0, 6)) {
1786
+ items.push({
1787
+ kind: "lineage",
1788
+ severity: "info",
1789
+ title: chain.current_title,
1790
+ summary: `Current packet supersedes ${chain.superseded_packet_ids.length} retired packet${chain.superseded_packet_ids.length === 1 ? "" : "s"}.`,
1791
+ action: chain.action,
1792
+ packet_ids: [chain.current_packet_id, ...chain.superseded_packet_ids],
1793
+ paths: chain.paths,
1794
+ date: chain.updated_at,
1795
+ });
1796
+ }
1797
+ const seen = new Set();
1798
+ const severityRank = { blocker: 0, warning: 1, info: 2, ok: 3 };
1799
+ const deduped = items
1800
+ .filter((item) => {
1801
+ const key = memoryHandoffDedupeKey(item);
1802
+ if (seen.has(key))
1803
+ return false;
1804
+ seen.add(key);
1805
+ return true;
1806
+ })
1807
+ .sort((a, b) => severityRank[a.severity] - severityRank[b.severity]
1808
+ || (b.date ?? "").localeCompare(a.date ?? "")
1809
+ || a.title.localeCompare(b.title))
1810
+ .slice(0, 30);
1811
+ const blockers = deduped.filter((item) => item.severity === "blocker").length;
1812
+ const warnings = deduped.filter((item) => item.severity === "warning").length;
1813
+ const info = deduped.filter((item) => item.severity === "info").length;
1814
+ const openItems = blockers + warnings;
1815
+ const recentChanges = timeline.totals.total;
1816
+ const recentMutations = audit.entries.length;
1817
+ const distillableSessions = sessions.totals.sessions_with_candidates;
1818
+ const durableObservations = sessions.totals.durable_observations;
1819
+ const ok = openItems === 0 && inbox.ok && lineage.totals.orphans === 0 && distillableSessions === 0;
1820
+ const recommendations = unique([
1821
+ ...(openItems ? ["Resolve handoff blockers and warnings before another agent relies on this memory."] : []),
1822
+ ...(distillableSessions ? ["Distill session observations before handoff so live agent learnings become reviewable memory packets."] : []),
1823
+ ...(recentMutations ? ["Review recent memory mutations so teammates know what changed."] : []),
1824
+ ...(recentChanges ? ["Scan the recent memory timeline before switching agents or branches."] : []),
1825
+ ...(lineage.totals.orphans ? ["Resolve superseded memories without replacement links."] : []),
1826
+ ...(!deduped.length ? ["No memory handoff work loaded; capture durable decisions, bugs, runbooks, and gotchas as work happens."] : []),
1827
+ ]);
1828
+ return {
1829
+ schema_version: 1,
1830
+ project_dir: projectDir,
1831
+ generated_at: nowIso(),
1832
+ ok,
1833
+ totals: {
1834
+ total: deduped.length,
1835
+ open_items: openItems,
1836
+ blockers,
1837
+ warnings,
1838
+ info,
1839
+ recent_changes: recentChanges,
1840
+ recent_mutations: recentMutations,
1841
+ supersession_orphans: lineage.totals.orphans,
1842
+ distillable_sessions: distillableSessions,
1843
+ durable_observations: durableObservations,
1844
+ },
1845
+ summary: openItems
1846
+ ? `${openItems} memory handoff item${openItems === 1 ? "" : "s"} need review before reuse.`
1847
+ : "Memory handoff has no blocking review work.",
1848
+ primary_action: handoffPrimaryAction(deduped, openItems),
1849
+ items: deduped,
1850
+ recommendations,
1851
+ };
1852
+ }
899
1853
  function readGit(projectDir, args) {
900
1854
  try {
901
1855
  return (0, node_child_process_1.execFileSync)("git", args, {
@@ -1057,9 +2011,10 @@ function createRepoOverviewPacket(projectDir) {
1057
2011
  let title = `${repoDisplayName(projectDir)} repo overview`;
1058
2012
  const tags = ["repo", "overview"];
1059
2013
  const bodyParts = [];
1060
- const paths = ["root"];
2014
+ const paths = [];
1061
2015
  const stack = [];
1062
2016
  if ((0, node_fs_1.existsSync)(packagePath)) {
2017
+ paths.push("package.json");
1063
2018
  const pkg = readJson(packagePath);
1064
2019
  title = `${String(pkg.name ?? repoDisplayName(projectDir))} repo overview`;
1065
2020
  const scripts = pkg.scripts && typeof pkg.scripts === "object" ? Object.keys(pkg.scripts) : [];
@@ -1083,6 +2038,7 @@ function createRepoOverviewPacket(projectDir) {
1083
2038
  }
1084
2039
  const readmeText = (0, node_fs_1.existsSync)(readmePath) ? safeReadText(readmePath) : null;
1085
2040
  if (readmeText) {
2041
+ paths.push("README.md");
1086
2042
  const readme = readmeText.slice(0, 1000);
1087
2043
  bodyParts.push(`README excerpt:\n${readme}`);
1088
2044
  }
@@ -4193,6 +5149,7 @@ function buildPacketIndexes(projectDir) {
4193
5149
  writeJson(written[1], byPath);
4194
5150
  writeJson(written[2], byTag);
4195
5151
  writeJson(written[3], byType);
5152
+ written.push(writeSparseVectorIndex(projectDir, packets));
4196
5153
  return written;
4197
5154
  }
4198
5155
  function readCurrentCodeGraph(projectDir, expectedInputHash) {
@@ -4293,6 +5250,7 @@ function currentOrBuildGraphs(projectDir) {
4293
5250
  (0, node_path_1.join)(indexesDir(projectDir), "by-path.json"),
4294
5251
  (0, node_path_1.join)(indexesDir(projectDir), "by-tag.json"),
4295
5252
  (0, node_path_1.join)(indexesDir(projectDir), "by-type.json"),
5253
+ (0, node_path_1.join)(indexesDir(projectDir), "vector-local.json"),
4296
5254
  (0, node_path_1.join)(indexesDir(projectDir), "structural.json"),
4297
5255
  (0, node_path_1.join)(indexesDir(projectDir), "graph.json"),
4298
5256
  (0, node_path_1.join)(indexesDir(projectDir), "code-graph.json"),
@@ -4380,6 +5338,8 @@ function staleSuggestedAction(reasons) {
4380
5338
  return "mark_stale";
4381
5339
  if (reasons.some((reason) => reason.includes("missing")))
4382
5340
  return "update";
5341
+ if (reasons.some((reason) => reason.includes("linked path changed")))
5342
+ return "update";
4383
5343
  if (reasons.some((reason) => reason.includes("reported")))
4384
5344
  return "supersede";
4385
5345
  return "verify";
@@ -4398,10 +5358,11 @@ function staleFinding(packet, reasons) {
4398
5358
  function refreshPacketStaleness(projectDir) {
4399
5359
  const findings = [];
4400
5360
  let updated = 0;
5361
+ const fingerprintCache = new Map();
4401
5362
  for (const entry of loadPacketEntriesFromDir(packetsDir(projectDir))) {
4402
- const reasons = staleMemoryReasons(projectDir, entry.packet);
4403
- const oldQuality = entry.packet.quality;
4404
- const oldFreshness = entry.packet.freshness;
5363
+ const reasons = staleMemoryReasons(projectDir, entry.packet, fingerprintCache);
5364
+ const oldQuality = (entry.packet.quality ?? {});
5365
+ const oldFreshness = (entry.packet.freshness ?? {});
4405
5366
  let nextQuality;
4406
5367
  if (reasons.length) {
4407
5368
  const finding = staleFinding(entry.packet, reasons);
@@ -4447,6 +5408,9 @@ function refreshProject(projectDir, options = {}) {
4447
5408
  }
4448
5409
  const validation = validateProject(projectDir);
4449
5410
  const metrics = kageMetricsShallow(projectDir, { codeGraph, knowledgeGraph, validation });
5411
+ ensureDir(reportsDir(projectDir));
5412
+ writeJson((0, node_path_1.join)(reportsDir(projectDir), "context-slots.json"), kageContextSlots(projectDir));
5413
+ writeJson((0, node_path_1.join)(reportsDir(projectDir), "handoff.json"), kageMemoryHandoff(projectDir));
4450
5414
  const nextActions = [];
4451
5415
  if (stale.findings.length)
4452
5416
  nextActions.push("Update, verify, or supersede stale repo memories before relying on them.");
@@ -4498,7 +5462,7 @@ function gcProject(projectDir, options = {}) {
4498
5462
  skipped.push({ id: packet.id, title: packet.title, reason: "healthy" });
4499
5463
  continue;
4500
5464
  }
4501
- const quality = packet.quality;
5465
+ const quality = (packet.quality ?? {});
4502
5466
  const hasHelpfulVotes = Number(quality?.votes_up ?? 0) > 0;
4503
5467
  if (hasHelpfulVotes && !options.force) {
4504
5468
  skipped.push({ id: packet.id, title: packet.title, reason: `stale but has helpful votes (use --force to override)` });
@@ -4520,6 +5484,20 @@ function gcProject(projectDir, options = {}) {
4520
5484
  }
4521
5485
  }
4522
5486
  if (!options.dryRun && (deprecated.length || deleted.length)) {
5487
+ if (deprecated.length) {
5488
+ recordMemoryAudit(projectDir, "deprecate", deprecated.map((packet) => ({ id: packet.id, title: packet.title })), {
5489
+ reason: "gc",
5490
+ count: deprecated.length,
5491
+ deprecated,
5492
+ });
5493
+ }
5494
+ if (deleted.length) {
5495
+ recordMemoryAudit(projectDir, "delete", deleted.map((packet) => ({ id: packet.id, title: packet.title })), {
5496
+ reason: "gc_force",
5497
+ count: deleted.length,
5498
+ deleted,
5499
+ });
5500
+ }
4523
5501
  const rebuilt = buildGraphIndexes(projectDir);
4524
5502
  writeJson((0, node_path_1.join)(memoryRoot(projectDir), "metrics.json"), kageMetricsShallow(projectDir, rebuilt));
4525
5503
  }
@@ -4582,12 +5560,31 @@ function installAgentPolicy(projectDir) {
4582
5560
  }
4583
5561
  return { path: agentsPath, created, updated };
4584
5562
  }
4585
- function tokenize(text) {
4586
- return text
4587
- .toLowerCase()
4588
- .split(/[^a-z0-9._/-]+/)
4589
- .map((term) => term.trim())
4590
- .filter((term) => term.length > 1 && !STOPWORDS.has(term));
5563
+ const TOKEN_RE = /[\p{L}\p{N}._/-]+/gu;
5564
+ const CJK_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
5565
+ function cjkNgrams(term) {
5566
+ const chars = Array.from(term).filter((char) => CJK_RE.test(char));
5567
+ if (chars.length === 0)
5568
+ return [];
5569
+ const grams = new Set();
5570
+ if (chars.length === 1)
5571
+ grams.add(chars[0]);
5572
+ for (let i = 0; i < chars.length - 1; i++)
5573
+ grams.add(`${chars[i]}${chars[i + 1]}`);
5574
+ return [...grams];
5575
+ }
5576
+ function tokenize(text) {
5577
+ const tokens = [];
5578
+ for (const match of text.toLowerCase().matchAll(TOKEN_RE)) {
5579
+ const term = match[0]?.trim();
5580
+ if (!term || STOPWORDS.has(term))
5581
+ continue;
5582
+ if (term.length > 1)
5583
+ tokens.push(term);
5584
+ if (CJK_RE.test(term))
5585
+ tokens.push(...cjkNgrams(term));
5586
+ }
5587
+ return tokens.filter((term) => term.length > 0 && !STOPWORDS.has(term));
4591
5588
  }
4592
5589
  function unique(values) {
4593
5590
  return [...new Set(values)];
@@ -4743,6 +5740,408 @@ function scorePacketsBm25(queryTerms, packets) {
4743
5740
  }
4744
5741
  return result;
4745
5742
  }
5743
+ function scorePacketsVector(queryTerms, packets) {
5744
+ const terms = expandQueryTerms(queryTerms);
5745
+ const queryVector = termVector(terms);
5746
+ const queryNorm = vectorNorm(queryVector);
5747
+ const result = new Map();
5748
+ if (!terms.length || queryNorm <= 0 || !packets.length)
5749
+ return result;
5750
+ for (const packet of packets) {
5751
+ const documentVector = packetSparseVector(packet);
5752
+ const score = cosineScore(queryVector, queryNorm, documentVector);
5753
+ if (score <= 0)
5754
+ continue;
5755
+ const why = terms
5756
+ .filter((term) => queryVector.has(term) && documentVector.has(term))
5757
+ .slice(0, 5)
5758
+ .map((term) => `vector-local:${term}`);
5759
+ result.set(packet.id, { score: Number((score * 0.75).toFixed(2)), why });
5760
+ }
5761
+ return result;
5762
+ }
5763
+ function scorePacketsVectorFromIndex(queryTerms, index) {
5764
+ const terms = expandQueryTerms(queryTerms);
5765
+ const queryVector = termVector(terms);
5766
+ const queryNorm = vectorNorm(queryVector);
5767
+ const result = new Map();
5768
+ if (!index || !terms.length || queryNorm <= 0 || !index.documents.length)
5769
+ return result;
5770
+ for (const document of index.documents) {
5771
+ const documentVector = new Map(document.terms);
5772
+ const score = cosineScore(queryVector, queryNorm, documentVector, document.norm);
5773
+ if (score <= 0)
5774
+ continue;
5775
+ const why = terms
5776
+ .filter((term) => queryVector.has(term) && documentVector.has(term))
5777
+ .slice(0, 5)
5778
+ .map((term) => `vector-local-index:${term}`);
5779
+ result.set(document.packet_id, { score: Number((score * 0.75).toFixed(2)), why });
5780
+ }
5781
+ return result;
5782
+ }
5783
+ function packetSparseVector(packet) {
5784
+ return termVector([
5785
+ ...tokenize(packet.title).flatMap((term) => [term, term, term, lexicalStem(term)]),
5786
+ ...tokenize(packet.summary).flatMap((term) => [term, term, lexicalStem(term)]),
5787
+ ...tokenize(packet.tags.join(" ")).flatMap((term) => [term, term, lexicalStem(term)]),
5788
+ ...tokenize(packet.paths.join(" ")).flatMap((term) => [term, lexicalStem(term)]),
5789
+ ...tokenize(packet.type).flatMap((term) => [term, lexicalStem(term)]),
5790
+ ...tokenize(packet.body).flatMap((term) => [term, lexicalStem(term)]),
5791
+ ]);
5792
+ }
5793
+ function buildSparseVectorIndex(packets) {
5794
+ return {
5795
+ schema_version: 1,
5796
+ generated_from_updated_at: packets.map((packet) => packet.updated_at).sort().at(-1) ?? null,
5797
+ packet_count: packets.length,
5798
+ documents: packets.map((packet) => {
5799
+ const vector = packetSparseVector(packet);
5800
+ return {
5801
+ packet_id: packet.id,
5802
+ terms: Array.from(vector.entries()).sort(([a], [b]) => a.localeCompare(b)),
5803
+ norm: Number(vectorNorm(vector).toFixed(6)),
5804
+ };
5805
+ }),
5806
+ };
5807
+ }
5808
+ function writeSparseVectorIndex(projectDir, packets) {
5809
+ const path = (0, node_path_1.join)(indexesDir(projectDir), "vector-local.json");
5810
+ writeJson(path, buildSparseVectorIndex(packets));
5811
+ return path;
5812
+ }
5813
+ function readSparseVectorIndex(projectDir, packets) {
5814
+ const path = (0, node_path_1.join)(indexesDir(projectDir), "vector-local.json");
5815
+ if (!(0, node_fs_1.existsSync)(path))
5816
+ return null;
5817
+ try {
5818
+ const index = readJson(path);
5819
+ if (index.schema_version !== 1)
5820
+ return null;
5821
+ if (index.packet_count !== packets.length)
5822
+ return null;
5823
+ const generatedFrom = packets.map((packet) => packet.updated_at).sort().at(-1) ?? null;
5824
+ if (index.generated_from_updated_at !== generatedFrom)
5825
+ return null;
5826
+ const packetIds = new Set(packets.map((packet) => packet.id));
5827
+ if (index.documents.length !== packets.length)
5828
+ return null;
5829
+ for (const document of index.documents) {
5830
+ if (!packetIds.has(document.packet_id))
5831
+ return null;
5832
+ if (!Array.isArray(document.terms) || !Number.isFinite(document.norm))
5833
+ return null;
5834
+ }
5835
+ return index;
5836
+ }
5837
+ catch {
5838
+ return null;
5839
+ }
5840
+ }
5841
+ function denseEmbeddingIndexPath(projectDir) {
5842
+ return (0, node_path_1.join)(indexesDir(projectDir), "embeddings-local.json");
5843
+ }
5844
+ function embeddingText(packet) {
5845
+ return [
5846
+ packet.title,
5847
+ packet.summary,
5848
+ packet.type,
5849
+ packet.tags.join(" "),
5850
+ packet.paths.join(" "),
5851
+ packet.body,
5852
+ ].filter(Boolean).join("\n").slice(0, 8000);
5853
+ }
5854
+ async function createDenseEmbeddingProvider(model = "Xenova/all-MiniLM-L6-v2") {
5855
+ let extractor = null;
5856
+ return {
5857
+ name: "xenova",
5858
+ model,
5859
+ dimensions: 384,
5860
+ async embedBatch(texts) {
5861
+ if (!extractor) {
5862
+ let transformers;
5863
+ try {
5864
+ // @ts-ignore Optional peer dependency. Kage does not install this by default.
5865
+ transformers = await import("@xenova/transformers");
5866
+ }
5867
+ catch {
5868
+ throw new Error("Install @xenova/transformers to build local embeddings: npm install @xenova/transformers");
5869
+ }
5870
+ extractor = await transformers.pipeline("feature-extraction", model);
5871
+ }
5872
+ const output = await extractor(texts, { pooling: "mean", normalize: true });
5873
+ return output.tolist();
5874
+ },
5875
+ };
5876
+ }
5877
+ function denseNorm(vector) {
5878
+ return Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
5879
+ }
5880
+ function denseCosine(query, document, documentNorm) {
5881
+ if (query.length !== document.length)
5882
+ return 0;
5883
+ const queryNorm = denseNorm(query);
5884
+ const docNorm = documentNorm ?? denseNorm(document);
5885
+ if (queryNorm <= 0 || docNorm <= 0)
5886
+ return 0;
5887
+ let dot = 0;
5888
+ for (let index = 0; index < query.length; index += 1)
5889
+ dot += query[index] * document[index];
5890
+ return dot / (queryNorm * docNorm);
5891
+ }
5892
+ async function buildEmbeddingIndex(projectDir, options = {}) {
5893
+ ensureMemoryDirs(projectDir);
5894
+ const packets = loadApprovedPackets(projectDir);
5895
+ const path = denseEmbeddingIndexPath(projectDir);
5896
+ try {
5897
+ const provider = options.provider ?? await createDenseEmbeddingProvider(options.model);
5898
+ const batchSize = Math.max(1, Math.min(64, Math.floor(options.batchSize ?? 16)));
5899
+ const documents = [];
5900
+ for (let offset = 0; offset < packets.length; offset += batchSize) {
5901
+ const batch = packets.slice(offset, offset + batchSize);
5902
+ const vectors = await provider.embedBatch(batch.map(embeddingText));
5903
+ batch.forEach((packet, index) => {
5904
+ const vector = (vectors[index] ?? []).map((value) => Number(value));
5905
+ documents.push({
5906
+ packet_id: packet.id,
5907
+ vector,
5908
+ norm: Number(denseNorm(vector).toFixed(6)),
5909
+ });
5910
+ });
5911
+ }
5912
+ const artifact = {
5913
+ schema_version: 1,
5914
+ provider: provider.name,
5915
+ model: provider.model,
5916
+ dimensions: provider.dimensions,
5917
+ generated_from_updated_at: packets.map((packet) => packet.updated_at).sort().at(-1) ?? null,
5918
+ packet_count: packets.length,
5919
+ documents,
5920
+ };
5921
+ writeJson(path, artifact);
5922
+ return {
5923
+ ok: true,
5924
+ project_dir: projectDir,
5925
+ path,
5926
+ provider: artifact.provider,
5927
+ model: artifact.model,
5928
+ dimensions: artifact.dimensions,
5929
+ packet_count: artifact.packet_count,
5930
+ errors: [],
5931
+ };
5932
+ }
5933
+ catch (error) {
5934
+ return {
5935
+ ok: false,
5936
+ project_dir: projectDir,
5937
+ path,
5938
+ provider: "none",
5939
+ model: options.model ?? "Xenova/all-MiniLM-L6-v2",
5940
+ dimensions: 0,
5941
+ packet_count: packets.length,
5942
+ errors: [String(error instanceof Error ? error.message : error)],
5943
+ };
5944
+ }
5945
+ }
5946
+ function readDenseEmbeddingIndex(projectDir, packets) {
5947
+ const path = denseEmbeddingIndexPath(projectDir);
5948
+ if (!(0, node_fs_1.existsSync)(path))
5949
+ return null;
5950
+ try {
5951
+ const index = readJson(path);
5952
+ if (index.schema_version !== 1)
5953
+ return null;
5954
+ if (index.packet_count !== packets.length)
5955
+ return null;
5956
+ const generatedFrom = packets.map((packet) => packet.updated_at).sort().at(-1) ?? null;
5957
+ if (index.generated_from_updated_at !== generatedFrom)
5958
+ return null;
5959
+ const packetIds = new Set(packets.map((packet) => packet.id));
5960
+ if (index.documents.length !== packets.length)
5961
+ return null;
5962
+ for (const document of index.documents) {
5963
+ if (!packetIds.has(document.packet_id))
5964
+ return null;
5965
+ if (!Array.isArray(document.vector) || document.vector.length !== index.dimensions)
5966
+ return null;
5967
+ if (!Number.isFinite(document.norm))
5968
+ return null;
5969
+ }
5970
+ return index;
5971
+ }
5972
+ catch {
5973
+ return null;
5974
+ }
5975
+ }
5976
+ function scorePacketsDenseEmbeddings(queryVector, index) {
5977
+ const result = new Map();
5978
+ if (!index || !queryVector.length || !index.documents.length)
5979
+ return result;
5980
+ for (const document of index.documents) {
5981
+ const score = denseCosine(queryVector, document.vector, document.norm);
5982
+ if (score <= 0)
5983
+ continue;
5984
+ result.set(document.packet_id, {
5985
+ score: Number((score * 3).toFixed(2)),
5986
+ why: [`vector-external:${index.provider}:${index.model}`],
5987
+ });
5988
+ }
5989
+ return result;
5990
+ }
5991
+ function termVector(terms) {
5992
+ const vector = new Map();
5993
+ for (const term of terms) {
5994
+ if (!term)
5995
+ continue;
5996
+ vector.set(term, (vector.get(term) ?? 0) + 1);
5997
+ }
5998
+ return vector;
5999
+ }
6000
+ function vectorNorm(vector) {
6001
+ let sum = 0;
6002
+ for (const value of vector.values())
6003
+ sum += value * value;
6004
+ return Math.sqrt(sum);
6005
+ }
6006
+ function cosineScore(queryVector, queryNorm, documentVector, knownDocumentNorm) {
6007
+ const documentNorm = knownDocumentNorm ?? vectorNorm(documentVector);
6008
+ if (queryNorm <= 0 || documentNorm <= 0)
6009
+ return 0;
6010
+ let dot = 0;
6011
+ for (const [term, queryWeight] of queryVector) {
6012
+ dot += queryWeight * (documentVector.get(term) ?? 0);
6013
+ }
6014
+ return dot / (queryNorm * documentNorm);
6015
+ }
6016
+ function scoreReferenceBodyBm25(queryTerms, packets) {
6017
+ const terms = expandQueryTerms(queryTerms);
6018
+ const references = packets.filter((packet) => packet.type === "reference");
6019
+ const documents = references.map((packet) => ({ packet, terms: tokenize(packet.body), length: Math.max(1, tokenize(packet.body).length) }));
6020
+ const result = new Map();
6021
+ if (!terms.length || !documents.length)
6022
+ return result;
6023
+ const averageLength = documents.reduce((sum, document) => sum + document.length, 0) / documents.length || 1;
6024
+ const documentFrequency = new Map();
6025
+ for (const term of terms) {
6026
+ documentFrequency.set(term, documents.filter((document) => document.terms.includes(term)).length);
6027
+ }
6028
+ for (const document of documents) {
6029
+ const termFrequency = new Map();
6030
+ for (const term of document.terms)
6031
+ termFrequency.set(term, (termFrequency.get(term) ?? 0) + 1);
6032
+ let score = 0;
6033
+ for (const term of terms) {
6034
+ const frequency = termFrequency.get(term) ?? 0;
6035
+ if (frequency <= 0)
6036
+ continue;
6037
+ const df = documentFrequency.get(term) ?? 0;
6038
+ const idf = Math.log(1 + (documents.length - df + 0.5) / (df + 0.5));
6039
+ const denominator = frequency + 1.5 * (1 - 0.75 + 0.75 * (document.length / averageLength));
6040
+ score += idf * ((frequency * 2.5) / denominator);
6041
+ }
6042
+ if (score > 0)
6043
+ result.set(document.packet.id, Number(score.toFixed(2)));
6044
+ }
6045
+ return result;
6046
+ }
6047
+ function extractTemporalAnchorDate(query) {
6048
+ const labeled = query.match(/\b(?:question|current|today|query)\s+date\s*:\s*(\d{4})[/-](\d{1,2})[/-](\d{1,2})/i);
6049
+ if (!labeled)
6050
+ return null;
6051
+ const year = Number(labeled[1]);
6052
+ const month = Number(labeled[2]);
6053
+ const day = Number(labeled[3]);
6054
+ if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day))
6055
+ return null;
6056
+ const date = new Date(Date.UTC(year, month - 1, day));
6057
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day)
6058
+ return null;
6059
+ return date;
6060
+ }
6061
+ function stripTemporalMetadata(query) {
6062
+ return query
6063
+ .split(/\r?\n/)
6064
+ .filter((line) => !/^\s*(?:question|current|today|query)\s+date\s*:/i.test(line))
6065
+ .join("\n");
6066
+ }
6067
+ function shiftUtcDays(date, days) {
6068
+ const shifted = new Date(date.getTime());
6069
+ shifted.setUTCDate(shifted.getUTCDate() + days);
6070
+ return shifted;
6071
+ }
6072
+ function formatUtcDate(date) {
6073
+ const year = date.getUTCFullYear();
6074
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
6075
+ const day = String(date.getUTCDate()).padStart(2, "0");
6076
+ return `${year}/${month}/${day}`;
6077
+ }
6078
+ function monthName(date) {
6079
+ return date.toLocaleString("en-US", { month: "long", timeZone: "UTC" });
6080
+ }
6081
+ function temporalQueryTerms(query) {
6082
+ const anchor = extractTemporalAnchorDate(query);
6083
+ if (!anchor)
6084
+ return [];
6085
+ const lower = query.toLowerCase();
6086
+ const hints = [];
6087
+ const addTargetDate = (daysAgo) => {
6088
+ const target = shiftUtcDays(anchor, -daysAgo);
6089
+ hints.push(`target date ${formatUtcDate(target)} ${monthName(target)} ${target.getUTCDate()}`);
6090
+ };
6091
+ if (/\b(?:ten|10)\s+days?\s+ago\b/.test(lower))
6092
+ addTargetDate(10);
6093
+ if (/\b(?:two|2)\s+weeks?\s+ago\b/.test(lower))
6094
+ addTargetDate(14);
6095
+ if (/\b(?:three|3)\s+weeks?\s+ago\b/.test(lower))
6096
+ addTargetDate(21);
6097
+ if (/\b(?:four|4)\s+weeks?\s+ago\b/.test(lower))
6098
+ addTargetDate(28);
6099
+ if (/\b(?:past|last)\s+month\b/.test(lower)) {
6100
+ const start = shiftUtcDays(anchor, -31);
6101
+ hints.push(`target month ${monthName(start)} ${start.getUTCFullYear()} ${formatUtcDate(start)} to ${formatUtcDate(anchor)}`);
6102
+ }
6103
+ return tokenize(hints.join(" "));
6104
+ }
6105
+ function semanticConceptTerms(query) {
6106
+ const lower = query.toLowerCase();
6107
+ const hints = [];
6108
+ const labels = [];
6109
+ if (/\b(homegrown|garden|gardening|harvest|harvested|produce)\b/.test(lower)) {
6110
+ hints.push("garden gardening planted planting plants herbs vegetables tomato tomatoes harvest harvested homegrown produce");
6111
+ labels.push("garden-produce");
6112
+ }
6113
+ if (/\b(battery life|phone battery|charging|charger|power bank|powerbank)\b/.test(lower)) {
6114
+ hints.push("battery phone charging charger charged power bank powerbank portable battery-saving");
6115
+ labels.push("phone-battery");
6116
+ }
6117
+ if (/\b(sibling|siblings|brother|brothers|sister|sisters)\b/.test(lower)) {
6118
+ hints.push("sibling siblings brother brothers sister sisters family");
6119
+ labels.push("family-siblings");
6120
+ }
6121
+ if (/\b(business milestone|milestone|first client|contract)\b/.test(lower)) {
6122
+ hints.push("business milestone signed contract client customer deal");
6123
+ labels.push("business-milestone");
6124
+ }
6125
+ if (/\b(kitchen appliance|appliance|appliances)\b/.test(lower)) {
6126
+ hints.push("kitchen appliance appliances bought purchased oven stove microwave blender mixer toaster grill smoker");
6127
+ labels.push("kitchen-appliance");
6128
+ }
6129
+ return { terms: tokenize(hints.join(" ")), labels };
6130
+ }
6131
+ function recallQueryExpansion(query) {
6132
+ const stripped = stripTemporalMetadata(query);
6133
+ const semantic = semanticConceptTerms(stripped);
6134
+ const baseTerms = tokenize(stripped);
6135
+ const temporalTerms = temporalQueryTerms(query);
6136
+ const semanticTerms = semantic.terms;
6137
+ return {
6138
+ baseTerms,
6139
+ temporalTerms,
6140
+ semanticTerms,
6141
+ semanticLabels: semantic.labels,
6142
+ terms: [...baseTerms, ...temporalTerms, ...semanticTerms],
6143
+ };
6144
+ }
4746
6145
  function recallIntentBoost(queryTerms, packet) {
4747
6146
  const terms = new Set(expandQueryTerms(queryTerms));
4748
6147
  const commandIntent = ["run", "test", "tests", "build", "command", "commands"].some((term) => terms.has(term));
@@ -4789,40 +6188,131 @@ function recallGraphLookup(graph) {
4789
6188
  }
4790
6189
  return { packetEntityByPacketId, edgesByEntityId };
4791
6190
  }
4792
- function recallBreakdown(projectDir, terms, packet, textScore, graph = buildKnowledgeGraph(projectDir), lookup = recallGraphLookup(graph)) {
6191
+ function recallBreakdown(projectDir, terms, packet, textScore, temporalScore = 0, semanticScore = 0, vectorScore = 0, usageScore = 0, graph = buildKnowledgeGraph(projectDir), lookup = recallGraphLookup(graph)) {
4793
6192
  const packetEntityId = lookup.packetEntityByPacketId.get(packet.id);
4794
6193
  const rawGraphScore = packetEntityId
4795
6194
  ? (lookup.edgesByEntityId.get(packetEntityId) ?? []).reduce((sum, edge) => sum + scoreText(terms, edge.fact), 0)
4796
6195
  : 0;
4797
- const graphScore = Math.min(rawGraphScore * 0.45, textScore > 0 ? textScore * 1.5 + 12 : 8);
6196
+ const graphCap = packet.type === "reference"
6197
+ ? 0
6198
+ : (textScore > 0 ? textScore * 1.5 + 12 : 8);
6199
+ const graphWeight = packet.type === "reference" ? 0 : 0.45;
6200
+ const graphScore = Math.min(rawGraphScore * graphWeight, graphCap);
4798
6201
  const pathTypeTag = scoreText(terms, `${packet.type} ${packet.tags.join(" ")} ${packet.paths.join(" ")}`, [packet.type, ...packet.tags, ...packet.paths]);
4799
6202
  const intent = recallIntentBoost(terms, packet);
4800
6203
  const freshness = packet.status === "approved" ? 2 : packet.status === "pending" ? 0 : -5;
4801
- const quality = Number(packet.quality.score ?? evaluateMemoryQuality(projectDir, packet).score) / 10;
6204
+ const quality = recallQualityScore(packet);
4802
6205
  const feedback = packetFeedbackScore(packet);
4803
- const vector = 0;
4804
- const final = Number((textScore + graphScore + pathTypeTag * 0.8 + intent + vector + freshness + quality + feedback).toFixed(2));
4805
- return { bm25: textScore, text: textScore, graph: Number(graphScore.toFixed(2)), path_type_tag: pathTypeTag, intent, vector, freshness, quality: Number(quality.toFixed(2)), feedback, final };
6206
+ const vector = Number(vectorScore.toFixed(2));
6207
+ const usage = Number(usageScore.toFixed(2));
6208
+ const pathTypeTagWeight = packet.type === "reference" ? 0.2 : 0.8;
6209
+ const final = Number((textScore + graphScore + pathTypeTag * pathTypeTagWeight + intent + vector + usage + freshness + quality + feedback).toFixed(2));
6210
+ return {
6211
+ bm25: textScore,
6212
+ text: textScore,
6213
+ temporal: Number(temporalScore.toFixed(2)),
6214
+ semantic: Number(semanticScore.toFixed(2)),
6215
+ graph: Number(graphScore.toFixed(2)),
6216
+ path_type_tag: pathTypeTag,
6217
+ intent,
6218
+ vector,
6219
+ usage,
6220
+ freshness,
6221
+ quality: Number(quality.toFixed(2)),
6222
+ feedback,
6223
+ final,
6224
+ };
4806
6225
  }
4807
- function recall(projectDir, query, limit = 5, explain = false, inputs = {}) {
6226
+ function recallDiversitySource(packet) {
6227
+ for (const ref of packet.source_refs) {
6228
+ if (ref.kind === "observation_session" && typeof ref.session_id === "string" && ref.session_id.trim()) {
6229
+ return `session:${ref.session_id.trim()}`;
6230
+ }
6231
+ }
6232
+ return null;
6233
+ }
6234
+ function diversifyRecallEntries(entries, limit, maxPerSource = 3) {
6235
+ if (limit <= maxPerSource)
6236
+ return entries.slice(0, limit);
6237
+ const selected = [];
6238
+ const deferred = [];
6239
+ const sourceCounts = new Map();
6240
+ for (const entry of entries) {
6241
+ const source = recallDiversitySource(entry.packet);
6242
+ if (source) {
6243
+ const count = sourceCounts.get(source) ?? 0;
6244
+ if (count >= maxPerSource) {
6245
+ deferred.push(entry);
6246
+ continue;
6247
+ }
6248
+ sourceCounts.set(source, count + 1);
6249
+ }
6250
+ selected.push(entry);
6251
+ if (selected.length >= limit)
6252
+ return selected.slice(0, limit);
6253
+ }
6254
+ for (const entry of deferred) {
6255
+ if (selected.length >= limit)
6256
+ break;
6257
+ selected.push(entry);
6258
+ }
6259
+ return selected.slice(0, limit);
6260
+ }
6261
+ function recallWithVectorScores(projectDir, query, limit = 5, explain = false, inputs = {}, externalVectorScores) {
4808
6262
  const current = inputs.codeGraph && inputs.knowledgeGraph ? null : readCurrentGraphs(projectDir);
4809
6263
  const detailedIndex = inputs.codeGraph && inputs.knowledgeGraph || current ? null : indexProjectDetailed(projectDir);
4810
6264
  const codeGraph = inputs.codeGraph ?? current?.codeGraph ?? detailedIndex?.codeGraph ?? buildCodeGraph(projectDir);
4811
6265
  const knowledgeGraph = inputs.knowledgeGraph ?? current?.knowledgeGraph ?? detailedIndex?.knowledgeGraph ?? buildKnowledgeGraph(projectDir, codeGraph);
4812
- const terms = tokenize(query);
6266
+ const expansion = inputs.semanticExpansion === false
6267
+ ? (() => {
6268
+ const baseTerms = tokenize(stripTemporalMetadata(query));
6269
+ const temporalTerms = temporalQueryTerms(query);
6270
+ return {
6271
+ baseTerms,
6272
+ temporalTerms,
6273
+ semanticTerms: [],
6274
+ semanticLabels: [],
6275
+ terms: [...baseTerms, ...temporalTerms],
6276
+ };
6277
+ })()
6278
+ : recallQueryExpansion(query);
6279
+ const terms = expansion.terms;
4813
6280
  const approvedPackets = loadApprovedPackets(projectDir);
4814
- const lexicalScores = scorePacketsBm25(terms, approvedPackets);
6281
+ const baseScores = scorePacketsBm25(expansion.baseTerms, approvedPackets);
6282
+ const temporalScores = scorePacketsBm25(expansion.temporalTerms, approvedPackets);
6283
+ const semanticScores = scorePacketsBm25(expansion.semanticTerms, approvedPackets);
6284
+ const sparseVectorIndex = externalVectorScores ? null : readSparseVectorIndex(projectDir, approvedPackets);
6285
+ const vectorScores = externalVectorScores ?? (sparseVectorIndex
6286
+ ? scorePacketsVectorFromIndex(terms, sparseVectorIndex)
6287
+ : scorePacketsVector(terms, approvedPackets));
6288
+ const referenceBodyScores = scoreReferenceBodyBm25(terms, approvedPackets);
6289
+ const accessEntries = readMemoryAccessEntries(projectDir, approvedPackets);
4815
6290
  const graphLookup = recallGraphLookup(knowledgeGraph);
4816
- const scored = approvedPackets
6291
+ const rankedScored = approvedPackets
4817
6292
  .map((packet) => {
4818
- const { score, why } = lexicalScores.get(packet.id) ?? { score: 0, why: [] };
4819
- const score_breakdown = recallBreakdown(projectDir, terms, packet, score, knowledgeGraph, graphLookup);
4820
- const relevance = score + score_breakdown.graph + score_breakdown.path_type_tag + score_breakdown.intent + score_breakdown.vector;
4821
- return { packet, score: score_breakdown.final, relevance, why_matched: why, score_breakdown };
6293
+ const base = baseScores.get(packet.id) ?? { score: 0, why: [] };
6294
+ const temporal = temporalScores.get(packet.id) ?? { score: 0, why: [] };
6295
+ const semantic = semanticScores.get(packet.id) ?? { score: 0, why: [] };
6296
+ const vector = vectorScores.get(packet.id) ?? { score: 0, why: [] };
6297
+ const referenceBodyScore = referenceBodyScores.get(packet.id) ?? 0;
6298
+ const lexicalScore = base.score + temporal.score + semantic.score;
6299
+ const textScore = packet.type === "reference" ? Math.max(lexicalScore, referenceBodyScore) : lexicalScore;
6300
+ const usageScore = memoryAccessScore(accessEntries.get(packet.id));
6301
+ const score_breakdown = recallBreakdown(projectDir, terms, packet, textScore, temporal.score, semantic.score, vector.score, usageScore, knowledgeGraph, graphLookup);
6302
+ const relevance = textScore + score_breakdown.graph + score_breakdown.path_type_tag + score_breakdown.intent + score_breakdown.vector;
6303
+ const why = [
6304
+ ...base.why,
6305
+ ...temporal.why.map((item) => `temporal:${item}`),
6306
+ ...semantic.why.map((item) => `semantic:${item}`),
6307
+ ...(semantic.score > 0 ? expansion.semanticLabels.map((label) => `semantic-concept:${label}`) : []),
6308
+ ...vector.why,
6309
+ ...(usageScore > 0 ? [`usage:${accessEntries.get(packet.id)?.uses_30d ?? 0} recalls in 30d`] : []),
6310
+ ];
6311
+ return { packet, score: score_breakdown.final, relevance, why_matched: unique(why).slice(0, 12), score_breakdown };
4822
6312
  })
4823
6313
  .filter((entry) => entry.relevance > 0)
4824
- .sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title))
4825
- .slice(0, limit)
6314
+ .sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title));
6315
+ const scored = diversifyRecallEntries(rankedScored, limit)
4826
6316
  .map(({ relevance, ...entry }) => entry);
4827
6317
  const pendingSeen = new Set();
4828
6318
  const pendingPackets = recallablePendingPackets(projectDir);
@@ -4844,11 +6334,13 @@ function recall(projectDir, query, limit = 5, explain = false, inputs = {}) {
4844
6334
  .slice(0, 3);
4845
6335
  const graphContext = queryGraph(projectDir, query, 5, knowledgeGraph);
4846
6336
  const codeContext = queryCodeGraph(projectDir, query, 5, codeGraph);
6337
+ const pinnedContext = renderPinnedRepoContext(readContextSlots(projectDir));
4847
6338
  const lines = [
4848
6339
  `# Kage Context`,
4849
6340
  "",
4850
6341
  `Query: ${query}`,
4851
6342
  "",
6343
+ ...(pinnedContext ? [pinnedContext, ""] : []),
4852
6344
  codeContext.symbols.length || codeContext.routes.length || codeContext.tests.length || codeContext.files.length ? "## Relevant Code Graph" : "",
4853
6345
  ...codeContext.routes.slice(0, 3).map((route, index) => `${index + 1}. [route] ${route.method} ${route.path} -> ${route.file_path}:${route.line}`),
4854
6346
  ...codeContext.symbols.slice(0, 5).map((symbol, index) => `${index + 1}. [symbol] ${symbol.kind} ${symbol.name} in ${symbol.path}:${symbol.line}`),
@@ -4876,7 +6368,7 @@ function recall(projectDir, query, limit = 5, explain = false, inputs = {}) {
4876
6368
  graphContext.edges.length ? "## Related Graph Facts" : "",
4877
6369
  ...graphContext.edges.slice(0, 5).map((edge, index) => `${index + 1}. ${edge.fact} (evidence: ${edge.evidence.join(", ")})`),
4878
6370
  ];
4879
- return {
6371
+ const result = {
4880
6372
  query,
4881
6373
  context_block: lines.join("\n"),
4882
6374
  results: scored,
@@ -4890,6 +6382,25 @@ function recall(projectDir, query, limit = 5, explain = false, inputs = {}) {
4890
6382
  }))
4891
6383
  : undefined,
4892
6384
  };
6385
+ if (inputs.trackAccess !== false)
6386
+ recordRecallAccess(projectDir, result.results);
6387
+ return result;
6388
+ }
6389
+ function recall(projectDir, query, limit = 5, explain = false, inputs = {}) {
6390
+ return recallWithVectorScores(projectDir, query, limit, explain, inputs);
6391
+ }
6392
+ async function recallWithEmbeddings(projectDir, query, limit = 5, explain = false, options = {}) {
6393
+ const packets = loadApprovedPackets(projectDir);
6394
+ const index = readDenseEmbeddingIndex(projectDir, packets);
6395
+ if (!index) {
6396
+ const result = recall(projectDir, query, limit, explain, { trackAccess: options.trackAccess, semanticExpansion: options.semanticExpansion });
6397
+ result.context_block = `${result.context_block}\n\nEmbedding recall note: no current .agent_memory/indexes/embeddings-local.json artifact found. Run kage embeddings build --project <repo> after installing @xenova/transformers.`;
6398
+ return result;
6399
+ }
6400
+ const provider = options.provider ?? await createDenseEmbeddingProvider(options.model ?? index.model);
6401
+ const [queryVector] = await provider.embedBatch([query]);
6402
+ const vectorScores = scorePacketsDenseEmbeddings(queryVector ?? [], index);
6403
+ return recallWithVectorScores(projectDir, query, limit, explain, { trackAccess: options.trackAccess, semanticExpansion: options.semanticExpansion }, vectorScores);
4893
6404
  }
4894
6405
  function scoreText(terms, text, boosts = []) {
4895
6406
  const haystack = text.toLowerCase();
@@ -5047,6 +6558,112 @@ function queryCodeGraph(projectDir, query, limit = 10, graph) {
5047
6558
  structural_edges: structuralEdges,
5048
6559
  };
5049
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
+ }
5050
6667
  function gitLines(projectDir, args) {
5051
6668
  return (readGit(projectDir, args) ?? "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
5052
6669
  }
@@ -5154,7 +6771,7 @@ function gitFileSignal(projectDir, path, graphPaths) {
5154
6771
  }
5155
6772
  function gitChangedFiles(projectDir) {
5156
6773
  return gitLines(projectDir, ["status", "--porcelain", "-uall"])
5157
- .map((line) => line.slice(3).trim().split(" -> ").at(-1) ?? "")
6774
+ .map((line) => parsePorcelainPath(line).split(" -> ").at(-1) ?? "")
5158
6775
  .filter(Boolean)
5159
6776
  .map((path) => gitPathToProjectRelative(projectDir, path) ?? path)
5160
6777
  .filter((path) => !isNoisePath(path));
@@ -5863,6 +7480,493 @@ function kageContributors(projectDir) {
5863
7480
  : "No contributor profiles could be computed.",
5864
7481
  };
5865
7482
  }
7483
+ const DEFAULT_CONTEXT_SLOT_LIMIT = 2000;
7484
+ const MAX_CONTEXT_SLOT_LIMIT = 8000;
7485
+ const MAX_PINNED_CONTEXT_CHARS = 6000;
7486
+ function slotsPath(projectDir) {
7487
+ return (0, node_path_1.join)(slotsDir(projectDir), "slots.json");
7488
+ }
7489
+ function validSlotLabel(label) {
7490
+ return /^[a-z][a-z0-9_]{0,63}$/.test(label);
7491
+ }
7492
+ function normalizeSlot(raw, fallbackAt) {
7493
+ if (!raw || typeof raw !== "object")
7494
+ return null;
7495
+ const data = raw;
7496
+ const label = typeof data.label === "string" ? data.label.trim() : "";
7497
+ if (!validSlotLabel(label))
7498
+ return null;
7499
+ const sizeLimit = Number(data.size_limit ?? data.sizeLimit ?? DEFAULT_CONTEXT_SLOT_LIMIT);
7500
+ const normalizedLimit = Number.isFinite(sizeLimit)
7501
+ ? Math.max(1, Math.min(MAX_CONTEXT_SLOT_LIMIT, Math.floor(sizeLimit)))
7502
+ : DEFAULT_CONTEXT_SLOT_LIMIT;
7503
+ const content = typeof data.content === "string" ? data.content : "";
7504
+ return {
7505
+ label,
7506
+ content: content.slice(0, normalizedLimit),
7507
+ description: typeof data.description === "string" ? data.description.trim() : "",
7508
+ pinned: data.pinned !== false,
7509
+ size_limit: normalizedLimit,
7510
+ paths: Array.isArray(data.paths) ? data.paths.map(String).map((item) => item.trim()).filter(Boolean) : [],
7511
+ tags: Array.isArray(data.tags) ? data.tags.map(String).map((item) => item.trim()).filter(Boolean) : [],
7512
+ created_at: typeof data.created_at === "string" ? data.created_at : fallbackAt,
7513
+ updated_at: typeof data.updated_at === "string" ? data.updated_at : fallbackAt,
7514
+ };
7515
+ }
7516
+ function readContextSlots(projectDir) {
7517
+ const path = slotsPath(projectDir);
7518
+ if (!(0, node_fs_1.existsSync)(path))
7519
+ return [];
7520
+ try {
7521
+ const parsed = readJson(path);
7522
+ const rawSlots = Array.isArray(parsed) ? parsed : Array.isArray(parsed.slots) ? parsed.slots : [];
7523
+ const at = nowIso();
7524
+ const byLabel = new Map();
7525
+ for (const raw of rawSlots) {
7526
+ const slot = normalizeSlot(raw, at);
7527
+ if (slot)
7528
+ byLabel.set(slot.label, slot);
7529
+ }
7530
+ return [...byLabel.values()].sort((a, b) => a.label.localeCompare(b.label));
7531
+ }
7532
+ catch {
7533
+ return [];
7534
+ }
7535
+ }
7536
+ function writeContextSlots(projectDir, slots) {
7537
+ writeJson(slotsPath(projectDir), {
7538
+ schema_version: 1,
7539
+ updated_at: nowIso(),
7540
+ slots: [...slots].sort((a, b) => a.label.localeCompare(b.label)),
7541
+ });
7542
+ }
7543
+ function renderPinnedRepoContext(slots) {
7544
+ const pinned = slots.filter((slot) => slot.pinned && slot.content.trim());
7545
+ if (!pinned.length)
7546
+ return "";
7547
+ const lines = ["## Pinned Repo Context"];
7548
+ let used = 0;
7549
+ for (const slot of pinned) {
7550
+ const meta = [
7551
+ slot.description ? `description: ${slot.description}` : "",
7552
+ slot.paths.length ? `paths: ${slot.paths.slice(0, 6).join(", ")}` : "",
7553
+ slot.tags.length ? `tags: ${slot.tags.slice(0, 8).join(", ")}` : "",
7554
+ ].filter(Boolean);
7555
+ const block = [
7556
+ "",
7557
+ `### ${slot.label}`,
7558
+ ...meta.map((item) => `_${item}_`),
7559
+ slot.content.trim(),
7560
+ ].join("\n");
7561
+ if (used + block.length > MAX_PINNED_CONTEXT_CHARS) {
7562
+ lines.push("\n_Context slots truncated to keep recall compact._");
7563
+ break;
7564
+ }
7565
+ lines.push(block);
7566
+ used += block.length;
7567
+ }
7568
+ return lines.join("\n");
7569
+ }
7570
+ function kageContextSlots(projectDir) {
7571
+ ensureMemoryDirs(projectDir);
7572
+ const slots = readContextSlots(projectDir);
7573
+ const pinnedContext = renderPinnedRepoContext(slots);
7574
+ const pinned = slots.filter((slot) => slot.pinned && slot.content.trim());
7575
+ const warnings = [];
7576
+ for (const slot of slots) {
7577
+ if (!slot.paths.length && !slot.tags.length)
7578
+ warnings.push(`Slot ${slot.label} has no paths or tags for grounding.`);
7579
+ }
7580
+ return {
7581
+ schema_version: 1,
7582
+ project_dir: projectDir,
7583
+ generated_at: nowIso(),
7584
+ slots_path: (0, node_path_1.relative)(projectDir, slotsPath(projectDir)),
7585
+ totals: {
7586
+ slots: slots.length,
7587
+ pinned: pinned.length,
7588
+ context_chars: pinnedContext.length,
7589
+ },
7590
+ slots,
7591
+ pinned_context_block: pinnedContext,
7592
+ summary: pinned.length
7593
+ ? `${pinned.length} pinned repo context slot(s), ${slots.length} total.`
7594
+ : "No pinned repo context slots yet.",
7595
+ warnings,
7596
+ };
7597
+ }
7598
+ function setContextSlot(projectDir, input) {
7599
+ ensureMemoryDirs(projectDir);
7600
+ const label = String(input.label ?? "").trim();
7601
+ if (!validSlotLabel(label)) {
7602
+ return { ok: false, errors: ["label must start with a lowercase letter and contain only lowercase letters, numbers, and underscores"] };
7603
+ }
7604
+ const content = String(input.content ?? "").trim();
7605
+ if (!content)
7606
+ return { ok: false, errors: ["content is required"] };
7607
+ const findings = scanSensitiveText(content);
7608
+ if (findings.length)
7609
+ return { ok: false, errors: [`Refusing to save context slot with sensitive content: ${findings.join(", ")}`] };
7610
+ const requestedLimit = input.size_limit ?? DEFAULT_CONTEXT_SLOT_LIMIT;
7611
+ const sizeLimit = Number.isFinite(Number(requestedLimit))
7612
+ ? Math.max(1, Math.min(MAX_CONTEXT_SLOT_LIMIT, Math.floor(Number(requestedLimit))))
7613
+ : DEFAULT_CONTEXT_SLOT_LIMIT;
7614
+ if (content.length > sizeLimit) {
7615
+ return { ok: false, errors: [`content exceeds size limit (${content.length} > ${sizeLimit})`] };
7616
+ }
7617
+ const at = nowIso();
7618
+ const slots = readContextSlots(projectDir);
7619
+ const existing = slots.find((slot) => slot.label === label);
7620
+ const next = {
7621
+ label,
7622
+ content,
7623
+ description: input.description?.trim() ?? existing?.description ?? "",
7624
+ pinned: input.pinned ?? existing?.pinned ?? true,
7625
+ size_limit: sizeLimit,
7626
+ paths: unique((input.paths ?? existing?.paths ?? []).map((item) => item.trim()).filter(Boolean)),
7627
+ tags: unique((input.tags ?? existing?.tags ?? []).map((item) => item.trim()).filter(Boolean)),
7628
+ created_at: existing?.created_at ?? at,
7629
+ updated_at: at,
7630
+ };
7631
+ const merged = slots.filter((slot) => slot.label !== label);
7632
+ merged.push(next);
7633
+ writeContextSlots(projectDir, merged);
7634
+ return { ok: true, slot: next, report: kageContextSlots(projectDir), errors: [] };
7635
+ }
7636
+ function deleteContextSlot(projectDir, label) {
7637
+ ensureMemoryDirs(projectDir);
7638
+ const normalized = String(label ?? "").trim();
7639
+ if (!validSlotLabel(normalized))
7640
+ return { ok: false, errors: ["valid label is required"] };
7641
+ const slots = readContextSlots(projectDir);
7642
+ const deleted = slots.find((slot) => slot.label === normalized);
7643
+ if (!deleted)
7644
+ return { ok: false, errors: [`slot not found: ${normalized}`] };
7645
+ writeContextSlots(projectDir, slots.filter((slot) => slot.label !== normalized));
7646
+ return { ok: true, deleted, report: kageContextSlots(projectDir), errors: [] };
7647
+ }
7648
+ function kageProjectProfile(projectDir) {
7649
+ const graph = readCurrentCodeGraph(projectDir) ?? buildCodeGraph(projectDir);
7650
+ const structural = readCurrentStructuralIndex(projectDir);
7651
+ const approved = loadApprovedPackets(projectDir);
7652
+ const decisionPackets = approved.filter((packet) => DECISION_INTELLIGENCE_TYPES.has(packet.type));
7653
+ const graphPaths = new Set(graph.files.map((file) => file.path));
7654
+ const sourceFiles = graph.files.filter((file) => file.kind === "source");
7655
+ const testFiles = graph.files.filter((file) => file.kind === "test");
7656
+ const packetsByPath = new Map();
7657
+ for (const packet of approved) {
7658
+ for (const path of packet.paths.filter((item) => graphPaths.has(item))) {
7659
+ const list = packetsByPath.get(path) ?? [];
7660
+ list.push(packet);
7661
+ packetsByPath.set(path, list);
7662
+ }
7663
+ }
7664
+ const codeConceptCounts = new Map();
7665
+ for (const file of structural?.files ?? []) {
7666
+ for (const concept of file.concepts) {
7667
+ if (!concept || concept.length < 3)
7668
+ continue;
7669
+ codeConceptCounts.set(concept, (codeConceptCounts.get(concept) ?? 0) + 1);
7670
+ }
7671
+ }
7672
+ const memoryConceptCounts = new Map();
7673
+ for (const packet of approved) {
7674
+ for (const tag of packet.tags.filter((item) => item && !["session-learning", "external-comparison"].includes(item))) {
7675
+ memoryConceptCounts.set(tag, (memoryConceptCounts.get(tag) ?? 0) + 1);
7676
+ }
7677
+ }
7678
+ const conceptNames = new Set([...codeConceptCounts.keys(), ...memoryConceptCounts.keys()]);
7679
+ const topConcepts = [...conceptNames].map((concept) => ({
7680
+ concept,
7681
+ count: (codeConceptCounts.get(concept) ?? 0) + (memoryConceptCounts.get(concept) ?? 0),
7682
+ sources: [
7683
+ ...(codeConceptCounts.has(concept) ? ["code"] : []),
7684
+ ...(memoryConceptCounts.has(concept) ? ["memory"] : []),
7685
+ ],
7686
+ }))
7687
+ .sort((a, b) => b.count - a.count || b.sources.length - a.sources.length || a.concept.localeCompare(b.concept))
7688
+ .slice(0, 12);
7689
+ const { forward, reverse } = codeGraphAdjacency(graph);
7690
+ const rank = filePageRank(graph, forward);
7691
+ const routeCounts = countBy(graph.routes, (route) => route.file_path);
7692
+ const testCounts = countBy(graph.tests, (test) => test.test_path);
7693
+ const keyFiles = graph.files
7694
+ .map((file) => {
7695
+ const dependents = reverse.get(file.path)?.size ?? 0;
7696
+ const imports = forward.get(file.path)?.size ?? 0;
7697
+ const memoryPackets = packetsByPath.get(file.path)?.length ?? 0;
7698
+ const routes = routeCounts[file.path] ?? 0;
7699
+ const tests = testCounts[file.path] ?? 0;
7700
+ const score = Number(((rank.get(file.path) ?? 0) * 1000 +
7701
+ dependents * 8 +
7702
+ imports * 3 +
7703
+ memoryPackets * 12 +
7704
+ routes * 10 +
7705
+ tests * 4 +
7706
+ (file.kind === "source" ? 3 : 0)).toFixed(2));
7707
+ const why = [
7708
+ ...(memoryPackets ? [`${memoryPackets} linked memory packet(s)`] : []),
7709
+ ...(dependents ? [`${dependents} dependent file(s)`] : []),
7710
+ ...(routes ? [`${routes} route(s)`] : []),
7711
+ ...(tests ? [`${tests} test signal(s)`] : []),
7712
+ ...(imports ? [`${imports} outgoing import(s)`] : []),
7713
+ ];
7714
+ return { path: file.path, kind: file.kind, language: file.language, dependents, imports, memory_packets: memoryPackets, routes, tests, score, why: why.length ? why : ["structural code graph signal"] };
7715
+ })
7716
+ .sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
7717
+ .slice(0, 12);
7718
+ const highValuePackets = decisionPackets
7719
+ .map((packet) => ({ packet, score: qualityScore(packet) ?? Number(evaluateMemoryQuality(projectDir, packet).score ?? 0) }))
7720
+ .sort((a, b) => b.score - a.score || b.packet.paths.length - a.packet.paths.length || a.packet.title.localeCompare(b.packet.title))
7721
+ .slice(0, 8)
7722
+ .map(({ packet }) => ({
7723
+ packet_id: packet.id,
7724
+ title: packet.title,
7725
+ type: packet.type,
7726
+ paths: packet.paths.filter((path) => graphPaths.has(path)).slice(0, 6),
7727
+ summary: packet.summary,
7728
+ }));
7729
+ const runCommands = graph.packages
7730
+ .filter((item) => item.kind === "script")
7731
+ .map((item) => ({ name: item.name, command: item.version }))
7732
+ .sort((a, b) => a.name.localeCompare(b.name))
7733
+ .slice(0, 12);
7734
+ const coveragePercent = percent(packetsByPath.size, Math.max(1, sourceFiles.length + testFiles.length));
7735
+ const warnings = [
7736
+ ...(!structural ? ["Structural index is missing; top concepts only include memory tags. Run kage refresh."] : []),
7737
+ ...(!gitHead(projectDir) ? ["Git history is unavailable, so profile excludes ownership and churn signals."] : []),
7738
+ ];
7739
+ const nextActions = [
7740
+ ...(coveragePercent < 60 ? ["Capture or ground memory for high-signal source paths with no linked repo knowledge."] : []),
7741
+ ...(!runCommands.length ? ["Capture runbook memory for test/build/dev commands if package scripts are not available."] : []),
7742
+ ...(topConcepts.filter((item) => item.sources.length === 2).length < 3 ? ["Add tags or paths so important concepts connect both code and memory."] : []),
7743
+ ...(warnings.length ? ["Run kage refresh before using this profile for handoff."] : []),
7744
+ ];
7745
+ return {
7746
+ schema_version: 1,
7747
+ project_dir: projectDir,
7748
+ generated_at: nowIso(),
7749
+ repo_state: graph.repo_state,
7750
+ summary: `${graph.files.length} files, ${approved.length} memory packet(s), ${coveragePercent}% memory-code coverage. Top concept: ${topConcepts[0]?.concept ?? "none"}.`,
7751
+ totals: {
7752
+ files: graph.files.length,
7753
+ source_files: sourceFiles.length,
7754
+ test_files: testFiles.length,
7755
+ symbols: graph.symbols.length,
7756
+ routes: graph.routes.length,
7757
+ tests: graph.tests.length,
7758
+ approved_memory: approved.length,
7759
+ decision_memory: decisionPackets.length,
7760
+ memory_code_coverage_percent: coveragePercent,
7761
+ },
7762
+ languages: Object.entries(countBy(graph.files, (file) => file.language))
7763
+ .map(([language, files]) => ({ language, files }))
7764
+ .sort((a, b) => b.files - a.files || a.language.localeCompare(b.language)),
7765
+ top_concepts: topConcepts,
7766
+ key_files: keyFiles,
7767
+ memory_focus: {
7768
+ by_type: countBy(approved, (packet) => packet.type),
7769
+ top_tags: Object.entries(countBy(approved.flatMap((packet) => packet.tags.filter((tag) => tag !== "session-learning")), (tag) => tag))
7770
+ .map(([tag, count]) => ({ tag, count }))
7771
+ .sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag))
7772
+ .slice(0, 12),
7773
+ high_value_packets: highValuePackets,
7774
+ },
7775
+ run_commands: runCommands,
7776
+ next_actions: nextActions.length ? nextActions : ["Project profile is ready for agent handoff."],
7777
+ warnings: unique(warnings),
7778
+ };
7779
+ }
7780
+ function capabilityStatus(score) {
7781
+ if (score >= 80)
7782
+ return "ready";
7783
+ if (score >= 50)
7784
+ return "watch";
7785
+ return "gap";
7786
+ }
7787
+ function capabilityPillar(id, label, checks, evidence, gaps, actions) {
7788
+ const score = checks.length ? percent(checks.filter(Boolean).length, checks.length) : 0;
7789
+ return {
7790
+ id,
7791
+ label,
7792
+ score,
7793
+ status: capabilityStatus(score),
7794
+ evidence,
7795
+ gaps: unique(gaps),
7796
+ actions: unique(actions),
7797
+ };
7798
+ }
7799
+ function kageCapabilityAudit(projectDir) {
7800
+ ensureMemoryDirs(projectDir);
7801
+ const metrics = kageMetrics(projectDir);
7802
+ const approved = loadApprovedPackets(projectDir);
7803
+ const quality = qualityReport(projectDir);
7804
+ const slots = kageContextSlots(projectDir);
7805
+ const sessions = kageSessionCaptureReport(projectDir);
7806
+ const replay = kageSessionReplay(projectDir, { limit: 50 });
7807
+ const handoff = kageMemoryHandoff(projectDir);
7808
+ const audit = kageMemoryAudit(projectDir, 50);
7809
+ const benchmark = benchmarkProject(projectDir);
7810
+ const memoryCodeLinks = metrics.memory_graph.edges;
7811
+ const reportsPath = reportsDir(projectDir);
7812
+ const viewerAppPath = (0, node_path_1.join)(__dirname, "..", "viewer", "app.js");
7813
+ const repoRoot = (0, node_path_1.resolve)(__dirname, "..", "..");
7814
+ const longMemEvalDoc = (0, node_path_1.join)(repoRoot, "benchmarks", "LONGMEMEVAL.md");
7815
+ const scaleBench = (0, node_path_1.join)(repoRoot, "benchmarks", "scale-kage-memory.mjs");
7816
+ const codingBench = (0, node_path_1.join)(repoRoot, "benchmarks", "coding-memory-quality.mjs");
7817
+ const generatedReports = [
7818
+ "benchmark.json",
7819
+ "handoff.json",
7820
+ "lifecycle.json",
7821
+ "memory-audit.json",
7822
+ "profile.json",
7823
+ "replay.json",
7824
+ ].filter((name) => (0, node_fs_1.existsSync)((0, node_path_1.join)(reportsPath, name)));
7825
+ const checklist = [
7826
+ {
7827
+ requirement: "reviewable repo memory",
7828
+ pass: approved.length > 0,
7829
+ evidence: `${approved.length} approved packet(s) in .agent_memory/packets`,
7830
+ action: "Capture durable decisions, runbooks, bugs, and gotchas with kage learn or kage capture.",
7831
+ },
7832
+ {
7833
+ requirement: "code-linked memory",
7834
+ pass: memoryCodeLinks > 0,
7835
+ evidence: `${memoryCodeLinks} memory graph edge(s), ${metrics.code_graph.files} indexed code file(s)`,
7836
+ action: "Add paths to packets and run kage refresh so memory connects to changed code.",
7837
+ },
7838
+ {
7839
+ requirement: "pinned context",
7840
+ pass: slots.totals.pinned > 0,
7841
+ evidence: `${slots.totals.pinned} pinned slot(s)`,
7842
+ action: "Add tiny stable repo guidance with kage slots set.",
7843
+ },
7844
+ {
7845
+ requirement: "privacy-preserving session proof",
7846
+ pass: replay.totals.events > 0 || sessions.totals.sessions > 0,
7847
+ evidence: `${replay.totals.events} replay event(s), ${sessions.totals.durable_observations} durable candidate(s)`,
7848
+ action: "Enable observe hooks or call kage_observe, then distill durable candidates.",
7849
+ },
7850
+ {
7851
+ requirement: "benchmark proof",
7852
+ pass: benchmark.ok && (0, node_fs_1.existsSync)(longMemEvalDoc) && (0, node_fs_1.existsSync)(scaleBench) && (0, node_fs_1.existsSync)(codingBench),
7853
+ evidence: `local gates ${benchmark.ok ? "pass" : "fail"}; benchmark harnesses ${(0, node_fs_1.existsSync)(longMemEvalDoc) && (0, node_fs_1.existsSync)(scaleBench) && (0, node_fs_1.existsSync)(codingBench) ? "present" : "missing"}`,
7854
+ action: "Run kage benchmark --memory-quality and kage benchmark --scale before publishing performance claims.",
7855
+ },
7856
+ {
7857
+ requirement: "viewer proof surface",
7858
+ pass: (0, node_fs_1.existsSync)(viewerAppPath),
7859
+ evidence: `viewer app ${(0, node_fs_1.existsSync)(viewerAppPath) ? "present" : "missing"}; ${generatedReports.length} generated report(s) loaded`,
7860
+ action: "Run kage viewer after refresh so dashboard reports are generated for review.",
7861
+ },
7862
+ {
7863
+ requirement: "handoff governance",
7864
+ pass: handoff.totals.open_items === 0 && quality.totals.pending === 0,
7865
+ evidence: `${handoff.totals.open_items} handoff item(s), ${quality.totals.pending} pending packet(s)`,
7866
+ action: "Clear handoff blockers, pending packets, stale memory, and duplicate candidates before merge.",
7867
+ },
7868
+ {
7869
+ requirement: "local-first storage",
7870
+ pass: (0, node_fs_1.existsSync)(packetsDir(projectDir)),
7871
+ evidence: ".agent_memory stores packets, reports, indexes, observations, and slots locally",
7872
+ action: "Keep generated indexes rebuildable and review durable packets in git.",
7873
+ },
7874
+ ];
7875
+ const memoryPillar = capabilityPillar("memory", "Repo memory", [
7876
+ checklist[0].pass,
7877
+ checklist[1].pass,
7878
+ checklist[2].pass,
7879
+ metrics.harness.validation_ok,
7880
+ ], [
7881
+ { label: "Approved memory", value: approved.length, source: ".agent_memory/packets" },
7882
+ { label: "Memory-code links", value: memoryCodeLinks, source: ".agent_memory/graph" },
7883
+ { label: "Pinned slots", value: slots.totals.pinned, source: ".agent_memory/slots" },
7884
+ { label: "Validation", value: metrics.harness.validation_ok ? "clean" : "check", source: "kage refresh" },
7885
+ ], [
7886
+ ...(!checklist[0].pass ? [checklist[0].action] : []),
7887
+ ...(!checklist[1].pass ? [checklist[1].action] : []),
7888
+ ...(!checklist[2].pass ? [checklist[2].action] : []),
7889
+ ...(!metrics.harness.validation_ok ? ["Fix validation warnings before trusting repo memory."] : []),
7890
+ ], [
7891
+ "Use kage_context for task recall; use kage_learn when an agent discovers reusable repo logic.",
7892
+ ...(quality.totals.needs_review ? ["Review low-signal packets so agents do not reuse weak memory."] : []),
7893
+ ]);
7894
+ const collaborationPillar = capabilityPillar("collaboration", "Team collaboration", [
7895
+ checklist[3].pass,
7896
+ audit.totals.total > 0,
7897
+ handoff.totals.open_items === 0,
7898
+ quality.totals.pending === 0,
7899
+ ], [
7900
+ { label: "Replay events", value: replay.totals.events, source: ".agent_memory/observations" },
7901
+ { label: "Durable candidates", value: replay.totals.durable_candidates, source: "kage replay" },
7902
+ { label: "Audit mutations", value: audit.totals.total, source: ".agent_memory/audit" },
7903
+ { label: "Handoff open items", value: handoff.totals.open_items, source: ".agent_memory/reports/handoff.json" },
7904
+ ], [
7905
+ ...(!checklist[3].pass ? [checklist[3].action] : []),
7906
+ ...(audit.totals.total === 0 ? ["No memory mutation audit yet; capture or review memory during real work."] : []),
7907
+ ...(handoff.totals.open_items ? [handoff.primary_action.summary] : []),
7908
+ ], [
7909
+ replay.totals.durable_candidates ? "Distill replay candidates into reviewable packets before handoff." : "Keep observation hooks enabled so future work becomes reviewable memory.",
7910
+ ]);
7911
+ const benchmarkPillar = capabilityPillar("benchmark", "Benchmark proof", [
7912
+ benchmark.ok,
7913
+ (0, node_fs_1.existsSync)(longMemEvalDoc),
7914
+ (0, node_fs_1.existsSync)(scaleBench),
7915
+ (0, node_fs_1.existsSync)(codingBench),
7916
+ ], [
7917
+ { label: "Local gates", value: benchmark.ok ? "pass" : "fail", source: "kage benchmark --project ." },
7918
+ { label: "Overall score", value: benchmark.overall_score, source: "benchmarkProject" },
7919
+ { label: "LongMemEval harness", value: (0, node_fs_1.existsSync)(longMemEvalDoc), source: "benchmarks/LONGMEMEVAL.md" },
7920
+ { label: "Scale harness", value: (0, node_fs_1.existsSync)(scaleBench), source: "benchmarks/scale-kage-memory.mjs" },
7921
+ { label: "Coding-memory harness", value: (0, node_fs_1.existsSync)(codingBench), source: "benchmarks/coding-memory-quality.mjs" },
7922
+ ], [
7923
+ ...(!benchmark.ok ? ["Fix failing local benchmark gates before quoting Kage readiness."] : []),
7924
+ ...(!(0, node_fs_1.existsSync)(longMemEvalDoc) ? ["Add or restore LongMemEval methodology and commands."] : []),
7925
+ ...(!(0, node_fs_1.existsSync)(scaleBench) || !(0, node_fs_1.existsSync)(codingBench) ? ["Restore packaged memory quality and scale benchmarks."] : []),
7926
+ ], [
7927
+ "Use benchmark JSON and the viewer proof ledger for performance claims; do not rely on README prose alone.",
7928
+ ]);
7929
+ const viewerPillar = capabilityPillar("dashboard_viewer", "Dashboard and viewer", [
7930
+ (0, node_fs_1.existsSync)(viewerAppPath),
7931
+ generatedReports.length >= 4,
7932
+ (0, node_fs_1.existsSync)((0, node_path_1.join)(reportsPath, "replay.json")) || replay.totals.events > 0,
7933
+ (0, node_fs_1.existsSync)((0, node_path_1.join)(reportsPath, "profile.json")) || metrics.code_graph.files > 0,
7934
+ ], [
7935
+ { label: "Viewer app", value: (0, node_fs_1.existsSync)(viewerAppPath) ? "present" : "missing", source: "mcp/viewer/app.js" },
7936
+ { label: "Generated reports", value: generatedReports.length, source: ".agent_memory/reports" },
7937
+ { label: "Replay report", value: (0, node_fs_1.existsSync)((0, node_path_1.join)(reportsPath, "replay.json")), source: ".agent_memory/reports/replay.json" },
7938
+ { label: "Code files indexed", value: metrics.code_graph.files, source: ".agent_memory/code_graph" },
7939
+ ], [
7940
+ ...(!(0, node_fs_1.existsSync)(viewerAppPath) ? ["Restore the local viewer app bundle."] : []),
7941
+ ...(generatedReports.length < 4 ? ["Run kage viewer or kage refresh to materialize dashboard reports."] : []),
7942
+ ...(!(0, node_fs_1.existsSync)((0, node_path_1.join)(reportsPath, "replay.json")) && !replay.totals.events ? ["Generate replay report data with kage viewer after observation hooks have captured a session."] : []),
7943
+ ...(!(0, node_fs_1.existsSync)((0, node_path_1.join)(reportsPath, "profile.json")) && !metrics.code_graph.files ? ["Run kage refresh so profile and graph summaries are current."] : []),
7944
+ ], [
7945
+ "Open kage viewer before demos so dashboard, proof, memory, and replay surfaces load from current artifacts.",
7946
+ ]);
7947
+ const pillars = [memoryPillar, collaborationPillar, benchmarkPillar, viewerPillar];
7948
+ const overall = Math.round(pillars.reduce((sum, pillar) => sum + pillar.score, 0) / Math.max(1, pillars.length));
7949
+ const overallStatus = pillars.some((pillar) => pillar.status === "gap")
7950
+ ? "gap"
7951
+ : pillars.some((pillar) => pillar.status !== "ready")
7952
+ ? "watch"
7953
+ : capabilityStatus(overall);
7954
+ const nextActions = unique([
7955
+ ...pillars.flatMap((pillar) => pillar.gaps.slice(0, 2)),
7956
+ ...pillars.filter((pillar) => pillar.status !== "ready").map((pillar) => pillar.actions[0]).filter(Boolean),
7957
+ ]);
7958
+ return {
7959
+ schema_version: 1,
7960
+ project_dir: projectDir,
7961
+ generated_at: nowIso(),
7962
+ overall_score: overall,
7963
+ status: overallStatus,
7964
+ summary: `Kage memory system readiness is ${overall}/100 across repo memory, collaboration, benchmark proof, and viewer proof.`,
7965
+ pillars,
7966
+ checklist,
7967
+ next_actions: nextActions.length ? nextActions : ["Capability audit is ready. Keep refresh, benchmarks, and viewer reports current before publishing claims."],
7968
+ };
7969
+ }
5866
7970
  const DECISION_INTELLIGENCE_TYPES = new Set([
5867
7971
  "bug_fix",
5868
7972
  "code_explanation",
@@ -5883,7 +7987,8 @@ function decisionContextValue(packet, key) {
5883
7987
  return typeof value === "string" && value.trim() ? value.trim() : null;
5884
7988
  }
5885
7989
  function qualityScore(packet) {
5886
- const score = Number(packet.quality.score);
7990
+ const quality = packet.quality;
7991
+ const score = Number(quality?.score);
5887
7992
  return Number.isFinite(score) ? score : null;
5888
7993
  }
5889
7994
  function kageDecisionIntelligence(projectDir) {
@@ -6311,13 +8416,261 @@ function kageGraphInsights(projectDir) {
6311
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).`,
6312
8417
  };
6313
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
+ }
6314
8669
  const WORKSPACE_SKIP_DIRS = new Set([
6315
8670
  ".agent_memory",
6316
8671
  ".git",
6317
8672
  ".hg",
6318
8673
  ".next",
6319
- ".repowise",
6320
- ".repowise-workspace",
6321
8674
  "coverage",
6322
8675
  "dist",
6323
8676
  "node_modules",
@@ -6861,10 +9214,11 @@ function kageMetrics(projectDir) {
6861
9214
  const indexedSourceFiles = sourceFiles.filter((file) => file.parser !== "metadata");
6862
9215
  const coverage = indexManifest.coverage.indexable_files > 0 ? indexManifest.coverage.coverage_percent : percent(indexedSourceFiles.length, sourceFiles.length);
6863
9216
  const allPackets = [...loadPacketsFromDir(packetsDir(projectDir)), ...loadPacketsFromDir(pendingDir(projectDir))];
9217
+ const qualityContext = memoryQualityContext(projectDir);
6864
9218
  const qualityScores = allPackets
6865
- .map((packet) => Number(packet.quality.score ?? evaluateMemoryQuality(projectDir, packet).score))
9219
+ .map((packet) => Number((packet.quality ?? {}).score ?? evaluateMemoryQuality(projectDir, packet, qualityContext).score))
6866
9220
  .filter((score) => Number.isFinite(score));
6867
- const duplicatePairs = allPackets.reduce((sum, packet) => sum + duplicateCandidates(projectDir, packet).length, 0);
9221
+ const duplicatePairs = allPackets.reduce((sum, packet) => sum + duplicateCandidatesWithContext(packet, qualityContext).length, 0);
6868
9222
  const indexedSourceTokens = Math.ceil(sourceFiles.reduce((sum, file) => sum + file.size_bytes, 0) / 4);
6869
9223
  const memoryTokens = allPackets.reduce((sum, packet) => sum + estimateTokens(packetText(packet)), 0);
6870
9224
  // Estimated size of a typical recall response: structured packet summaries + code graph
@@ -6882,6 +9236,7 @@ function kageMetrics(projectDir) {
6882
9236
  validation.warnings.length * 2)));
6883
9237
  const quality = qualityReport(projectDir);
6884
9238
  const benchmark = benchmarkProject(projectDir, { codeGraph, knowledgeGraph });
9239
+ const access = kageMemoryAccess(projectDir);
6885
9240
  return {
6886
9241
  schema_version: 1,
6887
9242
  project_dir: projectDir,
@@ -6935,6 +9290,7 @@ function kageMetrics(projectDir) {
6935
9290
  estimated_recall_context_tokens: recallContextTokens,
6936
9291
  estimated_tokens_saved_per_recall: tokensSaved,
6937
9292
  },
9293
+ memory_access: access.totals,
6938
9294
  harness: {
6939
9295
  policy_installed: policyInstalled,
6940
9296
  validation_ok: validation.ok,
@@ -7165,10 +9521,11 @@ function memoryInbox(projectDir) {
7165
9521
  }
7166
9522
  function qualityReport(projectDir) {
7167
9523
  ensureMemoryDirs(projectDir);
7168
- const packets = [...loadPacketsFromDir(packetsDir(projectDir)), ...loadPacketsFromDir(pendingDir(projectDir))];
9524
+ const context = memoryQualityContext(projectDir);
9525
+ const packets = context.packets;
7169
9526
  const rows = packets.map((packet) => {
7170
- const quality = evaluateMemoryQuality(projectDir, packet);
7171
- const classification = classifyPacket(projectDir, packet);
9527
+ const quality = evaluateMemoryQuality(projectDir, packet, context);
9528
+ const classification = classifyPacket(projectDir, packet, context, quality);
7172
9529
  return {
7173
9530
  id: packet.id,
7174
9531
  title: packet.title,
@@ -7183,11 +9540,11 @@ function qualityReport(projectDir) {
7183
9540
  });
7184
9541
  const active = packets.filter((packet) => packet.status === "approved" || packet.status === "pending");
7185
9542
  const staleWrong = packets.reduce((sum, packet) => {
7186
- const q = packet.quality;
9543
+ const q = (packet.quality ?? {});
7187
9544
  return sum + Number(q.votes_down ?? 0) + Number(q.reports_stale ?? 0);
7188
9545
  }, 0);
7189
9546
  const feedbackTotal = packets.reduce((sum, packet) => {
7190
- const q = packet.quality;
9547
+ const q = (packet.quality ?? {});
7191
9548
  return sum + Number(q.votes_up ?? 0) + Number(q.votes_down ?? 0) + Number(q.reports_stale ?? 0);
7192
9549
  }, 0);
7193
9550
  const withEvidence = active.filter((packet) => packet.source_refs.length > 0).length;
@@ -7229,7 +9586,7 @@ function benchmarkProject(projectDir, inputs = {}) {
7229
9586
  { query: "what changed on this branch", expected: "branch" },
7230
9587
  { query: "what gotchas exist", expected: "gotcha" },
7231
9588
  ].map((scenario) => {
7232
- const result = recall(projectDir, scenario.query, 5, true, { codeGraph, knowledgeGraph });
9589
+ const result = recall(projectDir, scenario.query, 5, true, { codeGraph, knowledgeGraph, trackAccess: false });
7233
9590
  const text = `${result.context_block}\n${result.results.map((entry) => packetText(entry.packet)).join("\n")}`.toLowerCase();
7234
9591
  return {
7235
9592
  query: scenario.query,
@@ -7282,24 +9639,537 @@ function benchmarkProject(projectDir, inputs = {}) {
7282
9639
  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);
7283
9640
  return {
7284
9641
  schema_version: 1,
7285
- project_dir: projectDir,
9642
+ project_dir: projectDir,
9643
+ generated_at: nowIso(),
9644
+ ok: gates.filter((gate) => gate.required).every((gate) => gate.pass),
9645
+ overall_score: gateScore,
9646
+ gates,
9647
+ scenarios,
9648
+ pain_metrics: {
9649
+ setup_runbook_coverage_percent: typeCoverage.runbook ? 100 : 0,
9650
+ bug_fix_coverage_percent: typeCoverage.bug_fix ? 100 : 0,
9651
+ decision_coverage_percent: typeCoverage.decision ? 100 : 0,
9652
+ code_flow_coverage_percent: codeFlowCoverage,
9653
+ recall_hit_rate_percent: recallHitRate,
9654
+ estimated_rediscovery_avoided: scenarios.filter((scenario) => scenario.hit).length,
9655
+ estimated_tokens_saved: metrics.savings.estimated_tokens_saved_per_recall,
9656
+ time_to_first_use_seconds: metrics.harness.policy_installed ? 30 : 90,
9657
+ },
9658
+ };
9659
+ }
9660
+ function benchmarkCodingMemoryQuality(options = {}) {
9661
+ const topK = Math.max(1, Math.floor(options.topK ?? 10));
9662
+ const metricsK = unique([5, 10, 20, topK].filter((value) => Number.isFinite(value) && value > 0)).sort((a, b) => a - b);
9663
+ const packetsPerTopic = Math.max(1, Math.floor(options.packetsPerTopic ?? 5));
9664
+ const distractorsPerTopic = Math.max(0, Math.floor(options.distractorsPerTopic ?? 7));
9665
+ const runDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "kage-coding-memory-quality-"));
9666
+ const projectDir = (0, node_path_1.join)(runDir, "project");
9667
+ const startedAt = Date.now();
9668
+ const { observations, queries } = codingMemoryQualityDataset(packetsPerTopic, distractorsPerTopic);
9669
+ writeCodingMemoryQualityProject(projectDir, observations);
9670
+ const refreshStarted = Date.now();
9671
+ refreshProject(projectDir);
9672
+ const refreshMs = Date.now() - refreshStarted;
9673
+ const perQuery = queries.map((query) => {
9674
+ const recallLimit = Math.max(...metricsK);
9675
+ const started = Date.now();
9676
+ const recalled = recall(projectDir, query.query, recallLimit, true, { trackAccess: false });
9677
+ const latencyMs = Date.now() - started;
9678
+ const relevant = new Set(query.relevant_packet_ids);
9679
+ const retrieved = recalled.results.map((result, index) => ({
9680
+ rank: index + 1,
9681
+ packet_id: result.packet.id,
9682
+ title: result.packet.title,
9683
+ score: result.score,
9684
+ }));
9685
+ return {
9686
+ query: query.query,
9687
+ category: query.category,
9688
+ description: query.description,
9689
+ relevant_count: relevant.size,
9690
+ retrieved,
9691
+ latency_ms: latencyMs,
9692
+ context_tokens: estimateTokens(recalled.context_block),
9693
+ recall: Object.fromEntries(metricsK.map((k) => [`at_${k}`, roundDecimal(codingRecallAt(retrieved, relevant, k) * 100, 2)])),
9694
+ precision_at_5_percent: roundDecimal(codingPrecisionAt(retrieved, relevant, 5) * 100, 2),
9695
+ ndcg_at_10: roundDecimal(codingNdcgAt(retrieved, relevant, 10), 4),
9696
+ mrr: roundDecimal(codingMrr(retrieved, relevant), 4),
9697
+ };
9698
+ });
9699
+ const sourceDiversity = codingMemorySourceDiversityProbe((0, node_path_1.join)(runDir, "source-diversity"));
9700
+ const allMemoryTokens = estimateTokens(loadApprovedPackets(projectDir).map(packetText).join("\n\n"));
9701
+ const averageContextTokens = Math.round(averageNumber(perQuery.map((item) => item.context_tokens)));
9702
+ const recallByK = Object.fromEntries(metricsK.map((k) => [`recall_at_${k}_percent`, roundDecimal(averageNumber(perQuery.map((item) => item.recall[`at_${k}`] ?? 0)), 2)]));
9703
+ const summary = {
9704
+ benchmark: "Kage coding memory quality",
9705
+ retrieval_mode: "kage-recall-default",
9706
+ packets: observations.length,
9707
+ queries: queries.length,
9708
+ top_k: topK,
9709
+ refresh_ms: refreshMs,
9710
+ ...recallByK,
9711
+ recall_at_k_percent: Number(recallByK[`recall_at_${topK}_percent`] ?? 0),
9712
+ precision_at_5_percent: roundDecimal(averageNumber(perQuery.map((item) => item.precision_at_5_percent)), 2),
9713
+ ndcg_at_10: roundDecimal(averageNumber(perQuery.map((item) => item.ndcg_at_10)), 4),
9714
+ mrr: roundDecimal(averageNumber(perQuery.map((item) => item.mrr)), 4),
9715
+ median_latency_ms: percentileNumber(perQuery.map((item) => item.latency_ms), 0.5),
9716
+ p95_latency_ms: percentileNumber(perQuery.map((item) => item.latency_ms), 0.95),
9717
+ all_memory_tokens: allMemoryTokens,
9718
+ average_context_tokens: averageContextTokens,
9719
+ context_reduction_percent: roundDecimal(((allMemoryTokens - averageContextTokens) / Math.max(1, allMemoryTokens)) * 100, 2),
9720
+ source_diversity_pass: sourceDiversity.pass,
9721
+ source_diversity_unique_sources: sourceDiversity.unique_sources,
9722
+ source_diversity_max_results_from_one_source: sourceDiversity.max_results_from_one_source,
9723
+ };
9724
+ const report = {
9725
+ schema_version: 1,
9726
+ benchmark: "Kage coding memory quality",
9727
+ generated_at: nowIso(),
9728
+ dataset: {
9729
+ observations: observations.length,
9730
+ queries: queries.length,
9731
+ packets_per_topic: packetsPerTopic,
9732
+ distractors_per_topic: distractorsPerTopic,
9733
+ categories: countByKey(queries, (item) => item.category),
9734
+ },
9735
+ top_k: topK,
9736
+ metrics_k: metricsK,
9737
+ duration_ms: Date.now() - startedAt,
9738
+ workdir: options.keep ? projectDir : null,
9739
+ summary,
9740
+ source_diversity: sourceDiversity,
9741
+ by_category: codingQualityByCategory(perQuery, metricsK),
9742
+ per_query: perQuery,
9743
+ baselines: {
9744
+ load_all_memory: {
9745
+ context_tokens: allMemoryTokens,
9746
+ note: "Upper-bound context cost if every memory packet is loaded instead of retrieved.",
9747
+ },
9748
+ kage_recall: {
9749
+ average_context_tokens: averageContextTokens,
9750
+ context_reduction_percent: summary.context_reduction_percent,
9751
+ },
9752
+ },
9753
+ caveats: [
9754
+ "This is a reproducible synthetic coding-memory quality benchmark, not an academic benchmark.",
9755
+ "The corpus is labeled with durable repo learnings, issue causes, runbooks, and decisions across sessions.",
9756
+ "Recall@K measures whether Kage retrieves the labeled memory packets, not whether an LLM answers correctly.",
9757
+ "Use LongMemEval-S for external long-term memory retrieval; use this harness to track coding-agent memory regressions.",
9758
+ ],
9759
+ };
9760
+ if (!options.keep)
9761
+ (0, node_fs_1.rmSync)(runDir, { recursive: true, force: true });
9762
+ return report;
9763
+ }
9764
+ function codingMemorySourceDiversityProbe(projectDir) {
9765
+ const query = "checkout retry idempotency session diversity";
9766
+ const topK = 4;
9767
+ const packetDir = packetsDir(projectDir);
9768
+ (0, node_fs_1.mkdirSync)(packetDir, { recursive: true });
9769
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(projectDir, "package.json"), JSON.stringify({ name: "kage-source-diversity", scripts: { test: "vitest" } }, null, 2));
9770
+ const now = "2026-05-18T00:00:00.000Z";
9771
+ const packets = [
9772
+ ["diversity:noise:a", "A checkout retry diversity note", "noisy-session"],
9773
+ ["diversity:noise:b", "B checkout retry diversity note", "noisy-session"],
9774
+ ["diversity:noise:c", "C checkout retry diversity note", "noisy-session"],
9775
+ ["diversity:noise:d", "D checkout retry diversity note", "noisy-session"],
9776
+ ["diversity:independent:z", "Z checkout retry independent note", "independent-session"],
9777
+ ];
9778
+ for (const [id, title, sessionId] of packets) {
9779
+ const packet = {
9780
+ schema_version: exports.PACKET_SCHEMA_VERSION,
9781
+ id,
9782
+ title,
9783
+ summary: "Checkout retry idempotency session diversity memory.",
9784
+ body: "Checkout retry idempotency session diversity behavior must include independent session knowledge when one live-agent session produces many similar memories.",
9785
+ type: "bug_fix",
9786
+ scope: "repo",
9787
+ visibility: "team",
9788
+ sensitivity: "internal",
9789
+ status: "approved",
9790
+ confidence: 0.7,
9791
+ tags: ["coding-memory-quality", "source-diversity", "checkout", "retry", "idempotency"],
9792
+ paths: ["src/checkout-retry.ts"],
9793
+ stack: [],
9794
+ source_refs: [{ kind: "observation_session", session_id: sessionId, captured_at: now }],
9795
+ context: {
9796
+ fact: "Recall should include independent session knowledge when one observed session is noisy.",
9797
+ verification: "Synthetic source-diversity benchmark packet.",
9798
+ },
9799
+ freshness: { ttl_days: 365, last_verified_at: now, verification: "synthetic_source_diversity" },
9800
+ edges: [],
9801
+ quality: {
9802
+ reviewer: "benchmark-harness",
9803
+ votes_up: 0,
9804
+ votes_down: 0,
9805
+ uses_30d: 0,
9806
+ reports_stale: 0,
9807
+ review_boundary: "external_benchmark",
9808
+ promotion_requires_review: true,
9809
+ },
9810
+ created_at: now,
9811
+ updated_at: now,
9812
+ };
9813
+ writeJson((0, node_path_1.join)(packetDir, `${slugify(id)}.json`), packet);
9814
+ }
9815
+ refreshProject(projectDir);
9816
+ const recalled = recall(projectDir, query, topK, false, { trackAccess: false });
9817
+ const retrieved = recalled.results.map((result, index) => {
9818
+ const source = recallDiversitySource(result.packet) ?? "unknown";
9819
+ return {
9820
+ rank: index + 1,
9821
+ packet_id: result.packet.id,
9822
+ title: result.packet.title,
9823
+ source,
9824
+ };
9825
+ });
9826
+ const sourceCounts = countByKey(retrieved, (item) => item.source);
9827
+ const maxResultsFromOneSource = Math.max(0, ...Object.values(sourceCounts));
9828
+ const independentRank = retrieved.find((item) => item.source === "session:independent-session")?.rank ?? null;
9829
+ return {
9830
+ query,
9831
+ top_k: topK,
9832
+ max_results_from_one_source: maxResultsFromOneSource,
9833
+ unique_sources: Object.keys(sourceCounts).length,
9834
+ independent_source_rank: independentRank,
9835
+ pass: maxResultsFromOneSource <= 3 && independentRank !== null && independentRank <= topK,
9836
+ retrieved,
9837
+ };
9838
+ }
9839
+ const MEMORY_SCALE_QUERIES = [
9840
+ { query: "How did we set up OAuth providers?", topic: "oauth providers" },
9841
+ { query: "What was the N+1 query fix?", topic: "n+1 query fix" },
9842
+ { query: "PostgreSQL full-text search setup", topic: "postgres full text search" },
9843
+ { query: "bcrypt password hashing configuration", topic: "bcrypt password hashing" },
9844
+ { query: "Vitest unit testing setup", topic: "vitest unit testing" },
9845
+ { query: "webhook retry exponential backoff", topic: "webhook retry backoff" },
9846
+ { query: "ESLint flat config migration", topic: "eslint flat config" },
9847
+ { query: "Kubernetes HPA autoscaling configuration", topic: "kubernetes hpa autoscaling" },
9848
+ { query: "Prisma database seed script", topic: "prisma seed script" },
9849
+ { query: "API cursor-based pagination", topic: "cursor pagination api" },
9850
+ { query: "CSRF protection double-submit cookie", topic: "csrf double submit cookie" },
9851
+ { query: "blue-green deployment rollback", topic: "blue green rollback" },
9852
+ ];
9853
+ function benchmarkMemoryScale(options = {}) {
9854
+ const sizes = (options.sizes && options.sizes.length ? options.sizes : [240, 1000, 5000])
9855
+ .map((value) => Math.floor(Number(value)))
9856
+ .filter((value) => Number.isFinite(value) && value > 0);
9857
+ const normalizedSizes = unique(sizes.length ? sizes : [240, 1000, 5000]).sort((a, b) => a - b);
9858
+ const topK = Math.max(1, Math.floor(options.topK ?? 10));
9859
+ const runDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "kage-scale-memory-"));
9860
+ const startedAt = Date.now();
9861
+ const results = [];
9862
+ for (const size of normalizedSizes) {
9863
+ const projectDir = (0, node_path_1.join)(runDir, `size-${size}`);
9864
+ writeMemoryScaleProject(projectDir, size);
9865
+ const refreshStarted = Date.now();
9866
+ refreshProject(projectDir);
9867
+ const refreshMs = Date.now() - refreshStarted;
9868
+ const queries = MEMORY_SCALE_QUERIES.map((item) => {
9869
+ const started = Date.now();
9870
+ const recalled = recall(projectDir, item.query, topK, false, { trackAccess: false });
9871
+ const latencyMs = Date.now() - started;
9872
+ const topic = item.topic.toLowerCase();
9873
+ const hitRank = recalled.results.findIndex((result) => packetText(result.packet).toLowerCase().includes(topic));
9874
+ return {
9875
+ query: item.query,
9876
+ topic: item.topic,
9877
+ hit: hitRank >= 0,
9878
+ rank: hitRank >= 0 ? hitRank + 1 : null,
9879
+ latency_ms: latencyMs,
9880
+ context_tokens: estimateTokens(recalled.context_block),
9881
+ };
9882
+ });
9883
+ const allMemoryTokens = estimateTokens(loadApprovedPackets(projectDir).map(packetText).join("\n\n"));
9884
+ const averageContextTokens = Math.round(averageNumber(queries.map((item) => item.context_tokens)));
9885
+ results.push({
9886
+ packets: size,
9887
+ refresh_ms: refreshMs,
9888
+ recall_hit_rate_percent: roundDecimal((queries.filter((item) => item.hit).length / queries.length) * 100, 2),
9889
+ median_recall_latency_ms: percentileNumber(queries.map((item) => item.latency_ms), 0.5),
9890
+ p95_recall_latency_ms: percentileNumber(queries.map((item) => item.latency_ms), 0.95),
9891
+ all_memory_tokens: allMemoryTokens,
9892
+ average_context_tokens: averageContextTokens,
9893
+ context_reduction_percent: roundDecimal(((allMemoryTokens - averageContextTokens) / Math.max(1, allMemoryTokens)) * 100, 2),
9894
+ queries,
9895
+ });
9896
+ }
9897
+ const largest = results.at(-1);
9898
+ const report = {
9899
+ schema_version: 1,
9900
+ benchmark: "Kage synthetic memory scale",
7286
9901
  generated_at: nowIso(),
7287
- ok: gates.filter((gate) => gate.required).every((gate) => gate.pass),
7288
- overall_score: gateScore,
7289
- gates,
7290
- scenarios,
7291
- pain_metrics: {
7292
- setup_runbook_coverage_percent: typeCoverage.runbook ? 100 : 0,
7293
- bug_fix_coverage_percent: typeCoverage.bug_fix ? 100 : 0,
7294
- decision_coverage_percent: typeCoverage.decision ? 100 : 0,
7295
- code_flow_coverage_percent: codeFlowCoverage,
7296
- recall_hit_rate_percent: recallHitRate,
7297
- estimated_rediscovery_avoided: scenarios.filter((scenario) => scenario.hit).length,
7298
- estimated_tokens_saved: metrics.savings.estimated_tokens_saved_per_recall,
7299
- time_to_first_use_seconds: metrics.harness.policy_installed ? 30 : 90,
9902
+ sizes: normalizedSizes,
9903
+ top_k: topK,
9904
+ duration_ms: Date.now() - startedAt,
9905
+ workdir: options.keep ? runDir : null,
9906
+ summary: {
9907
+ benchmark: "Kage synthetic memory scale",
9908
+ largest_packets: largest?.packets ?? 0,
9909
+ largest_hit_rate_percent: largest?.recall_hit_rate_percent ?? 0,
9910
+ largest_median_recall_latency_ms: largest?.median_recall_latency_ms ?? 0,
9911
+ largest_context_reduction_percent: largest?.context_reduction_percent ?? 0,
7300
9912
  },
9913
+ results,
9914
+ caveats: [
9915
+ "This is a synthetic repo-memory scale benchmark, not an academic benchmark.",
9916
+ "Packets are generated as approved repo-local memories and indexed with Kage refresh.",
9917
+ "Recall hit rate checks whether the expected topic appears in the top-k returned packets.",
9918
+ "Context reduction compares loading all generated memory text with Kage's returned recall context.",
9919
+ ],
9920
+ };
9921
+ if (!options.keep)
9922
+ (0, node_fs_1.rmSync)(runDir, { recursive: true, force: true });
9923
+ return report;
9924
+ }
9925
+ function writeMemoryScaleProject(projectDir, count) {
9926
+ ensureDir((0, node_path_1.join)(projectDir, ".agent_memory", "packets"));
9927
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(projectDir, "package.json"), JSON.stringify({ name: "kage-scale-bench", scripts: { test: "vitest" } }), "utf8");
9928
+ const now = "2026-05-17T00:00:00.000Z";
9929
+ for (let index = 0; index < count; index += 1) {
9930
+ const queryTopic = MEMORY_SCALE_QUERIES[index % MEMORY_SCALE_QUERIES.length].topic;
9931
+ const module = `src/module-${String(index % 120).padStart(3, "0")}.ts`;
9932
+ const packet = {
9933
+ schema_version: 2,
9934
+ id: `scale:packet:${index}`,
9935
+ title: `Session ${String(index).padStart(5, "0")} memory for ${queryTopic}`,
9936
+ summary: `Reusable repo learning about ${queryTopic}.`,
9937
+ body: `During session ${index}, the agent learned this reusable repo fact about ${queryTopic}. Keep this nuance when editing ${module}. Run the relevant tests after changes. This packet intentionally represents old cross-session knowledge that should be retrieved by topic, not loaded wholesale.`,
9938
+ type: index % 5 === 0 ? "runbook" : index % 5 === 1 ? "bug_fix" : index % 5 === 2 ? "decision" : index % 5 === 3 ? "workflow" : "code_explanation",
9939
+ scope: "repo",
9940
+ visibility: "team",
9941
+ sensitivity: "internal",
9942
+ status: "approved",
9943
+ confidence: 0.7,
9944
+ tags: ["scale-benchmark", slugify(queryTopic), `session-${Math.floor(index / 8)}`],
9945
+ paths: [module],
9946
+ stack: [],
9947
+ source_refs: [{ kind: "external_benchmark", captured_at: now }],
9948
+ context: {
9949
+ fact: `Reusable repo learning about ${queryTopic}.`,
9950
+ trigger: itemScaleTrigger(queryTopic),
9951
+ action: `Recall this before editing ${module}.`,
9952
+ },
9953
+ freshness: { ttl_days: 365, last_verified_at: now, verification: "synthetic_scale_benchmark" },
9954
+ edges: [],
9955
+ quality: {
9956
+ reviewer: "benchmark-harness",
9957
+ votes_up: 0,
9958
+ votes_down: 0,
9959
+ uses_30d: 0,
9960
+ reports_stale: 0,
9961
+ review_boundary: "external_benchmark",
9962
+ promotion_requires_review: true,
9963
+ },
9964
+ created_at: now,
9965
+ updated_at: now,
9966
+ };
9967
+ writeJson((0, node_path_1.join)(projectDir, ".agent_memory", "packets", `${String(index).padStart(6, "0")}-${slugify(queryTopic)}.json`), packet);
9968
+ }
9969
+ }
9970
+ function itemScaleTrigger(topic) {
9971
+ return `Recall when asked about ${topic}.`;
9972
+ }
9973
+ function codingMemoryQualityDataset(targetCount, distractorCount) {
9974
+ const topics = [
9975
+ codingTopic("checkout-retry-split", "exact", "checkout retry logic", ["checkout", "retry", "idempotency", "session-state"], "Callback retries use idempotency keys while user checkout retries use session state. Do not merge them."),
9976
+ codingTopic("oauth-callback-url", "cross-session", "oauth callback mismatch", ["oauth", "callback", "redirect-uri", "production"], "OAuth failed because production callback URLs used http instead of https."),
9977
+ codingTopic("test-db-isolation", "runbook", "test database isolation", ["tests", "database", "transactions", "isolation"], "Integration tests isolate database state with transaction rollback and a fresh seed."),
9978
+ codingTopic("edge-runtime-db", "semantic", "edge runtime database client", ["edge-runtime", "database", "serverless", "connection-pooling"], "Edge handlers cannot use the normal pooled database client; route them through the serverless-safe client."),
9979
+ codingTopic("webhook-signature-order", "exact", "webhook signature raw body", ["webhook", "signature", "raw-body", "security"], "Webhook verification must read the raw body before JSON parsing or signatures fail."),
9980
+ codingTopic("playwright-navigation-race", "cross-session", "flaky playwright navigation", ["playwright", "flaky-test", "navigation", "ci"], "The flaky login test needed waitForURL after submit because navigation raced assertions in CI."),
9981
+ codingTopic("cache-invalidation-after-write", "semantic", "cache invalidation after mutation", ["cache", "invalidation", "mutation", "redis"], "Mutations must invalidate list and detail cache keys or stale rows leak into the UI."),
9982
+ codingTopic("prisma-migration-drift", "cross-session", "prisma migration drift", ["prisma", "migration", "drift", "production"], "Production drift came from a manual ALTER; resolve the migration before deploying."),
9983
+ codingTopic("rate-limit-shared-identity", "semantic", "rate limit identity key", ["rate-limit", "identity", "security", "redis"], "Rate limits should key by user ID when authenticated and IP only for anonymous requests."),
9984
+ codingTopic("api-pagination-cursor", "exact", "cursor pagination contract", ["pagination", "cursor", "api", "backward-compatibility"], "Pagination returns opaque cursors with hasNextPage; clients must not decode cursor internals."),
9985
+ codingTopic("upload-presigned-url", "runbook", "presigned upload flow", ["uploads", "s3", "presigned-url", "validation"], "Uploads request a short-lived presigned URL, validate MIME type, then write metadata after success."),
9986
+ codingTopic("rbac-admin-editor-viewer", "semantic", "role based access control", ["rbac", "authorization", "roles", "jwt"], "RBAC supports admin, editor, and viewer roles stored in JWT custom claims."),
9987
+ codingTopic("observability-correlation-id", "runbook", "request correlation id", ["observability", "logging", "correlation-id", "debugging"], "Every request gets a correlation ID that must flow through logs, jobs, and API errors."),
9988
+ codingTopic("background-job-idempotency", "semantic", "background job idempotency", ["jobs", "idempotency", "queue", "retry"], "Background jobs store idempotency keys so retries do not duplicate side effects."),
9989
+ codingTopic("feature-flag-rollout", "decision", "feature flag rollout", ["feature-flags", "rollout", "experiments", "kill-switch"], "Risky features ship behind flags with percentage rollout and an immediate kill switch."),
9990
+ codingTopic("monorepo-package-boundary", "decision", "monorepo package boundary", ["monorepo", "packages", "imports", "architecture"], "UI packages cannot import server-only modules; shared types live in the contracts package."),
9991
+ codingTopic("graphql-n-plus-one", "cross-session", "graphql n plus one", ["graphql", "n+1", "dataloader", "performance"], "GraphQL resolvers batch relation loading through DataLoader to avoid N+1 queries."),
9992
+ codingTopic("secret-env-validation", "runbook", "environment secret validation", ["env", "secrets", "validation", "startup"], "Startup validates required env names without logging secret values."),
9993
+ codingTopic("mobile-safe-area-layout", "semantic", "mobile safe area layout", ["mobile", "safe-area", "layout", "css"], "Mobile bottom navigation uses safe-area insets to avoid overlapping OS controls."),
9994
+ codingTopic("release-changelog-source", "decision", "release changelog source", ["release", "changelog", "git", "automation"], "Release notes are generated from merged PR labels and verified against the git diff."),
9995
+ ];
9996
+ const observations = [];
9997
+ let index = 0;
9998
+ for (const item of topics) {
9999
+ for (let variant = 0; variant < targetCount; variant += 1)
10000
+ observations.push(codingObservation(index++, item, true, variant));
10001
+ for (let variant = 0; variant < distractorCount; variant += 1) {
10002
+ const neighbor = topics[(topics.indexOf(item) + variant + 3) % topics.length];
10003
+ observations.push(codingDistractor(index++, item, neighbor, variant));
10004
+ }
10005
+ }
10006
+ const queries = topics.map((item) => ({
10007
+ query: item.query,
10008
+ category: item.category,
10009
+ description: `Retrieve durable repo memory for ${item.query}.`,
10010
+ relevant_packet_ids: observations.filter((obs) => obs.topic === item.id && obs.target).map((obs) => obs.id),
10011
+ }));
10012
+ return { observations, queries };
10013
+ }
10014
+ function codingTopic(id, category, query, concepts, lesson) {
10015
+ return { id, category, query, concepts, lesson };
10016
+ }
10017
+ function codingObservation(index, item, target, variant) {
10018
+ const file = codingFileForTopic(item.id, variant);
10019
+ return {
10020
+ id: `coding-memory:${String(index).padStart(4, "0")}:${target ? "target" : "near"}:${item.id}`,
10021
+ session_id: `session-${String(Math.floor(index / 8)).padStart(3, "0")}`,
10022
+ topic: item.id,
10023
+ target,
10024
+ category: item.category,
10025
+ title: `${titleCase(item.query)} repo memory ${variant + 1}`,
10026
+ summary: `Reusable learning about ${item.query}: ${item.lesson}`,
10027
+ body: [
10028
+ `During a real agent session, this durable repo learning was captured for ${item.query}.`,
10029
+ item.lesson,
10030
+ `Concepts: ${item.concepts.join(", ")}.`,
10031
+ `When touching ${file}, recall this before refactoring, debugging, or changing tests.`,
10032
+ `Verification path: inspect ${file} and run the focused tests for ${item.concepts[0]}.`,
10033
+ ].join(" "),
10034
+ concepts: item.concepts,
10035
+ file,
10036
+ };
10037
+ }
10038
+ function codingDistractor(index, item, neighbor, variant) {
10039
+ const file = `src/shared-${variant % 5}.ts`;
10040
+ const concepts = unique([item.concepts[0], neighbor.concepts[0], "maintenance", "repo-context"]);
10041
+ return {
10042
+ id: `coding-memory:${String(index).padStart(4, "0")}:distractor:${item.id}`,
10043
+ session_id: `session-${String(Math.floor(index / 8)).padStart(3, "0")}`,
10044
+ topic: `distractor-${item.id}-${variant}`,
10045
+ target: false,
10046
+ category: "semantic",
10047
+ title: `Shared maintenance note ${variant + 1}`,
10048
+ summary: "Nearby repo context that shares broad vocabulary but is not the labeled durable learning.",
10049
+ body: [
10050
+ "This packet is intentionally adjacent context for the coding-memory benchmark.",
10051
+ `It mentions broad areas like ${concepts.join(", ")} without carrying the specific reusable lesson.`,
10052
+ `When editing ${file}, use this as background only; it is not the target memory for a focused recall query.`,
10053
+ ].join(" "),
10054
+ concepts,
10055
+ file,
7301
10056
  };
7302
10057
  }
10058
+ function writeCodingMemoryQualityProject(projectDir, observations) {
10059
+ const packetDir = packetsDir(projectDir);
10060
+ (0, node_fs_1.mkdirSync)(packetDir, { recursive: true });
10061
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(projectDir, "src"), { recursive: true });
10062
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(projectDir, "package.json"), JSON.stringify({ name: "kage-coding-memory-quality", scripts: { test: "vitest" } }, null, 2));
10063
+ const now = "2026-05-18T00:00:00.000Z";
10064
+ for (const obs of observations) {
10065
+ const packet = {
10066
+ schema_version: exports.PACKET_SCHEMA_VERSION,
10067
+ id: obs.id,
10068
+ title: obs.title,
10069
+ summary: obs.summary,
10070
+ body: obs.body,
10071
+ type: codingTypeForCategory(obs.category),
10072
+ scope: "repo",
10073
+ visibility: "team",
10074
+ sensitivity: "internal",
10075
+ status: "approved",
10076
+ confidence: 0.7,
10077
+ tags: ["coding-memory-quality", obs.category, obs.topic, ...obs.concepts],
10078
+ paths: [obs.file],
10079
+ stack: [],
10080
+ source_refs: [{ kind: "external_benchmark", captured_at: now }],
10081
+ context: {
10082
+ fact: obs.summary,
10083
+ verification: `Synthetic labeled coding-memory benchmark packet for ${obs.topic}.`,
10084
+ },
10085
+ freshness: { ttl_days: 365, last_verified_at: now, verification: "synthetic_coding_memory_quality" },
10086
+ edges: [],
10087
+ quality: {
10088
+ reviewer: "benchmark-harness",
10089
+ votes_up: 0,
10090
+ votes_down: 0,
10091
+ uses_30d: 0,
10092
+ reports_stale: 0,
10093
+ review_boundary: "external_benchmark",
10094
+ promotion_requires_review: true,
10095
+ },
10096
+ created_at: now,
10097
+ updated_at: now,
10098
+ };
10099
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(packetDir, `${slugify(obs.id)}.json`), JSON.stringify(packet, null, 2));
10100
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(projectDir, obs.file), `export const topic = ${JSON.stringify(obs.topic)};\n`, "utf8");
10101
+ }
10102
+ }
10103
+ function codingQualityByCategory(perQuery, metricsK) {
10104
+ const groups = new Map();
10105
+ for (const item of perQuery)
10106
+ groups.set(item.category, [...(groups.get(item.category) ?? []), item]);
10107
+ return Array.from(groups.entries()).map(([category, rows]) => ({
10108
+ category,
10109
+ queries: rows.length,
10110
+ ...Object.fromEntries(metricsK.map((k) => [`recall_at_${k}_percent`, roundDecimal(averageNumber(rows.map((item) => item.recall[`at_${k}`] ?? 0)), 2)])),
10111
+ ndcg_at_10: roundDecimal(averageNumber(rows.map((item) => item.ndcg_at_10)), 4),
10112
+ mrr: roundDecimal(averageNumber(rows.map((item) => item.mrr)), 4),
10113
+ }));
10114
+ }
10115
+ function codingRecallAt(retrieved, relevant, k) {
10116
+ if (!relevant.size)
10117
+ return 0;
10118
+ return retrieved.slice(0, k).filter((item) => relevant.has(item.packet_id)).length / relevant.size;
10119
+ }
10120
+ function codingPrecisionAt(retrieved, relevant, k) {
10121
+ const rows = retrieved.slice(0, k);
10122
+ return rows.length ? rows.filter((item) => relevant.has(item.packet_id)).length / rows.length : 0;
10123
+ }
10124
+ function codingNdcgAt(retrieved, relevant, k) {
10125
+ const dcg = retrieved.slice(0, k).reduce((sum, item, index) => sum + (relevant.has(item.packet_id) ? 1 / Math.log2(index + 2) : 0), 0);
10126
+ const idealHits = Math.min(relevant.size, k);
10127
+ let ideal = 0;
10128
+ for (let index = 0; index < idealHits; index += 1)
10129
+ ideal += 1 / Math.log2(index + 2);
10130
+ return ideal ? dcg / ideal : 0;
10131
+ }
10132
+ function codingMrr(retrieved, relevant) {
10133
+ const index = retrieved.findIndex((item) => relevant.has(item.packet_id));
10134
+ return index >= 0 ? 1 / (index + 1) : 0;
10135
+ }
10136
+ function codingTypeForCategory(category) {
10137
+ if (category === "runbook")
10138
+ return "runbook";
10139
+ if (category === "decision")
10140
+ return "decision";
10141
+ if (category === "cross-session")
10142
+ return "bug_fix";
10143
+ return "code_explanation";
10144
+ }
10145
+ function codingFileForTopic(topic, variant) {
10146
+ return `src/${slugify(topic)}-${variant % 3}.ts`;
10147
+ }
10148
+ function averageNumber(values) {
10149
+ return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
10150
+ }
10151
+ function percentileNumber(values, p) {
10152
+ if (!values.length)
10153
+ return 0;
10154
+ const sorted = values.slice().sort((a, b) => a - b);
10155
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * p) - 1));
10156
+ return sorted[index];
10157
+ }
10158
+ function roundDecimal(value, digits = 2) {
10159
+ const factor = 10 ** digits;
10160
+ return Math.round(value * factor) / factor;
10161
+ }
10162
+ function countByKey(rows, fn) {
10163
+ const counts = {};
10164
+ for (const row of rows) {
10165
+ const key = fn(row);
10166
+ counts[key] = (counts[key] ?? 0) + 1;
10167
+ }
10168
+ return counts;
10169
+ }
10170
+ function titleCase(value) {
10171
+ return value.replace(/\b[a-z]/g, (match) => match.toUpperCase());
10172
+ }
7303
10173
  function baselineDiscoveryFiles(projectDir, task) {
7304
10174
  const terms = tokenize(task);
7305
10175
  const graph = buildCodeGraph(projectDir);
@@ -7624,6 +10494,8 @@ function capture(input) {
7624
10494
  freshness: {
7625
10495
  ttl_days: 365,
7626
10496
  last_verified_at: createdAt,
10497
+ path_fingerprints: memoryPathFingerprints(input.projectDir, input.paths ?? []),
10498
+ path_fingerprint_policy: "source_hash_staleness",
7627
10499
  verification: "repo_local_agent_capture",
7628
10500
  },
7629
10501
  edges: [],
@@ -7647,6 +10519,12 @@ function capture(input) {
7647
10519
  ...evaluateMemoryQuality(input.projectDir, packet),
7648
10520
  };
7649
10521
  const path = writePacket(input.projectDir, packet, "packets");
10522
+ recordMemoryAudit(input.projectDir, "capture", [packet], {
10523
+ type: packet.type,
10524
+ status: packet.status,
10525
+ path: (0, node_path_1.relative)(input.projectDir, path),
10526
+ source_kind: packet.source_refs[0]?.kind ?? "explicit_capture",
10527
+ });
7650
10528
  return { ok: true, packet, path, errors: [] };
7651
10529
  }
7652
10530
  function createPublicCandidate(projectDir, id) {
@@ -7862,7 +10740,7 @@ fi
7862
10740
  KAGE_MSG="$POLICY" python3 -c "import json,os; print(json.dumps({'systemMessage': os.environ['KAGE_MSG']}))"
7863
10741
  `;
7864
10742
  const stopHookScript = `#!/usr/bin/env bash
7865
- # Kage Stop hook — best-effort repo memory refresh before Claude Code finishes.
10743
+ # Kage Stop hook — refreshes repo memory and blocks final handoff when linked memory needs agent reconciliation.
7866
10744
  # Silent if Kage is not initialized in the current project or no git changes exist.
7867
10745
  set -euo pipefail
7868
10746
 
@@ -7875,6 +10753,145 @@ command -v kage >/dev/null 2>&1 || exit 0
7875
10753
  if git -C "$CWD" status --porcelain -uall >/dev/null 2>&1 && [[ -n "$(git -C "$CWD" status --porcelain -uall)" ]]; then
7876
10754
  kage refresh --project "$CWD" --json >/dev/null 2>&1 || true
7877
10755
  kage pr summarize --project "$CWD" --json >/dev/null 2>&1 || true
10756
+ RECONCILE_OUTPUT="$(kage reconcile --project "$CWD" --json 2>/dev/null || true)"
10757
+ RECONCILE_UNRESOLVED="$(printf "%s" "$RECONCILE_OUTPUT" | python3 -c 'import json, sys
10758
+ try:
10759
+ d = json.load(sys.stdin)
10760
+ except Exception:
10761
+ d = {}
10762
+ print(int(d.get("unresolved_count") or 0))
10763
+ ' 2>/dev/null || echo "0")"
10764
+ if [[ "$RECONCILE_UNRESOLVED" != "0" ]]; then
10765
+ printf "%s" "$RECONCILE_OUTPUT" | python3 -c 'import json, sys
10766
+ try:
10767
+ d = json.load(sys.stdin)
10768
+ except Exception:
10769
+ d = {}
10770
+ print(d.get("agent_instruction") or "Kage memory reconciliation required before final response.")
10771
+ ' >&2
10772
+ exit 2
10773
+ fi
10774
+ fi
10775
+
10776
+ exit 0
10777
+ `;
10778
+ const observeHookScript = `#!/usr/bin/env bash
10779
+ # Kage Observe hook — captures durable Claude Code session signals and recalls repo memory.
10780
+ # Silent if Kage is not initialized in the current project.
10781
+ set -euo pipefail
10782
+
10783
+ PAYLOAD="$(cat || true)"
10784
+ CWD="$(PAYLOAD="$PAYLOAD" python3 -c 'import json, os
10785
+ try:
10786
+ d = json.loads(os.environ.get("PAYLOAD") or "{}")
10787
+ except Exception:
10788
+ d = {}
10789
+ print(d.get("cwd") or os.environ.get("CLAUDE_PROJECT_DIR") or "")
10790
+ ' 2>/dev/null || echo "")"
10791
+
10792
+ [[ -d "$CWD/.agent_memory" ]] || exit 0
10793
+ command -v kage >/dev/null 2>&1 || exit 0
10794
+
10795
+ EVENT="$(PAYLOAD="$PAYLOAD" python3 -c 'import json, os
10796
+ try:
10797
+ d = json.loads(os.environ.get("PAYLOAD") or "{}")
10798
+ except Exception:
10799
+ d = {}
10800
+ print(d.get("hook_event_name") or d.get("event") or "")
10801
+ ' 2>/dev/null || echo "")"
10802
+
10803
+ SESSION="$(PAYLOAD="$PAYLOAD" python3 -c 'import json, os
10804
+ try:
10805
+ d = json.loads(os.environ.get("PAYLOAD") or "{}")
10806
+ except Exception:
10807
+ d = {}
10808
+ print(d.get("session_id") or d.get("sessionId") or "default")
10809
+ ' 2>/dev/null || echo "default")"
10810
+
10811
+ OBSERVATION="$(PAYLOAD="$PAYLOAD" python3 -c 'import json, os
10812
+ try:
10813
+ d = json.loads(os.environ.get("PAYLOAD") or "{}")
10814
+ except Exception:
10815
+ d = {}
10816
+
10817
+ def first(*values):
10818
+ for value in values:
10819
+ if isinstance(value, str) and value.strip():
10820
+ return value.strip()
10821
+ return ""
10822
+
10823
+ def compact(value, limit=1200):
10824
+ if isinstance(value, (dict, list)):
10825
+ text = json.dumps(value, sort_keys=True)
10826
+ elif value is None:
10827
+ text = ""
10828
+ else:
10829
+ text = str(value)
10830
+ text = " ".join(text.split())
10831
+ return text[:limit]
10832
+
10833
+ event_name = first(d.get("hook_event_name"), d.get("event"))
10834
+ session_id = first(d.get("session_id"), d.get("sessionId"), "default")
10835
+ agent = first(d.get("agent"), "claude-code")
10836
+ tool = first(d.get("tool_name"), d.get("toolName"))
10837
+ tool_input = d.get("tool_input") or d.get("toolInput") or {}
10838
+ tool_response = d.get("tool_response") or d.get("toolResponse") or d.get("result") or {}
10839
+ prompt = first(d.get("prompt"), d.get("user_prompt"), d.get("message"))
10840
+ path = ""
10841
+ command = ""
10842
+ if isinstance(tool_input, dict):
10843
+ path = first(tool_input.get("file_path"), tool_input.get("path"), tool_input.get("notebook_path"))
10844
+ command = first(tool_input.get("command"))
10845
+
10846
+ if event_name == "UserPromptSubmit":
10847
+ payload = {"type": "user_prompt", "text": prompt, "summary": compact(prompt, 240)}
10848
+ elif event_name == "PostToolUseFailure":
10849
+ payload = {"type": "command_result" if command else "tool_result", "tool": tool, "path": path, "command": command, "summary": "Tool failed: " + compact(tool_response or d, 320), "text": compact(tool_response or d)}
10850
+ elif event_name == "PostToolUse":
10851
+ obs_type = "file_change" if path else ("command_result" if command else "tool_use")
10852
+ payload = {"type": obs_type, "tool": tool, "path": path, "command": command, "summary": compact(tool_response or tool_input, 320), "text": compact(tool_response or tool_input)}
10853
+ elif event_name == "PreCompact":
10854
+ payload = {"type": "session_end", "summary": "Claude Code is compacting context; distill durable observations before compaction."}
10855
+ elif event_name == "SessionEnd":
10856
+ payload = {"type": "session_end", "summary": "Claude Code session ended; distill durable observations for teammate handoff."}
10857
+ else:
10858
+ payload = {"type": "tool_use", "tool": tool, "path": path, "command": command, "summary": compact(d, 320), "text": compact(d)}
10859
+
10860
+ payload.update({"session_id": session_id, "agent": agent})
10861
+ print(json.dumps(payload, separators=(",", ":")))
10862
+ ' 2>/dev/null || echo "")"
10863
+
10864
+ if [[ -n "$OBSERVATION" ]]; then
10865
+ kage observe --project "$CWD" --event "$OBSERVATION" --json >/dev/null 2>&1 || true
10866
+ fi
10867
+
10868
+ if [[ "$EVENT" == "PreCompact" || "$EVENT" == "SessionEnd" ]]; then
10869
+ kage distill --project "$CWD" --session "$SESSION" --json >/dev/null 2>&1 || true
10870
+ fi
10871
+
10872
+ if [[ "$EVENT" == "UserPromptSubmit" ]]; then
10873
+ QUERY="$(PAYLOAD="$PAYLOAD" python3 -c 'import json, os
10874
+ try:
10875
+ d = json.loads(os.environ.get("PAYLOAD") or "{}")
10876
+ except Exception:
10877
+ d = {}
10878
+ print((d.get("prompt") or d.get("user_prompt") or d.get("message") or "")[:1000])
10879
+ ' 2>/dev/null || echo "")"
10880
+ if [[ -n "$QUERY" ]]; then
10881
+ CONTEXT="$(kage recall "$QUERY" --project "$CWD" --json 2>/dev/null | python3 -c 'import json, sys
10882
+ try:
10883
+ d = json.load(sys.stdin)
10884
+ except Exception:
10885
+ d = {}
10886
+ text = d.get("context_block") or ""
10887
+ print(text[:6000] if d.get("results") else "")
10888
+ ' 2>/dev/null || true)"
10889
+ if [[ -n "$CONTEXT" ]]; then
10890
+ KAGE_CONTEXT="$CONTEXT" python3 -c 'import json, os
10891
+ print(json.dumps({"additionalContext": os.environ.get("KAGE_CONTEXT", "")}))
10892
+ '
10893
+ fi
10894
+ fi
7878
10895
  fi
7879
10896
 
7880
10897
  exit 0
@@ -7883,13 +10900,18 @@ exit 0
7883
10900
  const hookEntry = {
7884
10901
  hooks: {
7885
10902
  SessionStart: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/session-start.sh", timeout: 5 }] }],
10903
+ UserPromptSubmit: [{ hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 12 }] }],
10904
+ PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 5 }] }],
10905
+ PostToolUseFailure: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 5 }] }],
10906
+ PreCompact: [{ hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 20 }] }],
7886
10907
  Stop: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/stop.sh", timeout: 20 }] }],
10908
+ SessionEnd: [{ hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 20 }] }],
7887
10909
  },
7888
10910
  };
7889
10911
  setSnippet(path, JSON.stringify({ mcpServers: { kage: server } }, null, 2), [
7890
10912
  "Add the MCP server to ~/.claude.json, then restart Claude Code.",
7891
10913
  "alwaysLoad: true makes Kage tools immediately visible without requiring ToolSearch.",
7892
- `Also create ${hookDir}/session-start.sh and ${hookDir}/stop.sh with the hook scripts and add SessionStart/Stop hooks to ~/.claude/settings.json.`,
10914
+ `Also create ${hookDir}/session-start.sh, observe.sh, and stop.sh with the hook scripts and add SessionStart/UserPromptSubmit/PostToolUse/PostToolUseFailure/PreCompact/Stop/SessionEnd hooks to ~/.claude/settings.json.`,
7893
10915
  "Run `kage init --project <repo>` inside each repo to install the ambient memory policy.",
7894
10916
  ], true);
7895
10917
  if (options.write) {
@@ -7897,6 +10919,7 @@ exit 0
7897
10919
  // Install the ambient session-start hook
7898
10920
  (0, node_fs_1.mkdirSync)(hookDir, { recursive: true });
7899
10921
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(hookDir, "session-start.sh"), hookScript, { mode: 0o755 });
10922
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(hookDir, "observe.sh"), observeHookScript, { mode: 0o755 });
7900
10923
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(hookDir, "stop.sh"), stopHookScript, { mode: 0o755 });
7901
10924
  upsertJsonSettings(settingsPath, hookEntry);
7902
10925
  result.wrote = true;
@@ -7914,7 +10937,7 @@ exit 0
7914
10937
  if (agent === "aider") {
7915
10938
  setSnippet(null, "Kage Aider support uses daemon REST mode: start with `kage daemon start --project <repo>` and point Aider automation at http://127.0.0.1:3111.", [
7916
10939
  "Run `kage daemon start --project <repo>`.",
7917
- "Use REST endpoints `/kage/recall`, `/kage/observe`, and `/kage/distill` from Aider scripts.",
10940
+ "Use REST endpoints `/kage/context`, `/kage/profile`, `/kage/context-slots`, `/kage/recall`, `/kage/capture`, `/kage/learn`, `/kage/feedback`, `/kage/observe`, `/kage/distill`, and `/kage/setup-doctor` from Aider scripts.",
7918
10941
  ]);
7919
10942
  return result;
7920
10943
  }
@@ -7996,14 +11019,20 @@ function upsertTomlMcpBlock(text, block) {
7996
11019
  }
7997
11020
  return `${out.join("\n").trimEnd()}\n`;
7998
11021
  }
7999
- function setupDoctor(projectDir) {
11022
+ function setupDoctor(projectDir, options = {}) {
8000
11023
  return exports.SETUP_AGENTS.map((agent) => {
8001
- const setup = setupAgent(agent, projectDir);
11024
+ const setup = setupAgent(agent, projectDir, { homeDir: options.homeDir, serverPath: options.serverPath });
11025
+ const hookSummary = agent === "claude-code"
11026
+ ? claudeAmbientHookSummary(options.homeDir ?? process.env.HOME ?? "~")
11027
+ : undefined;
11028
+ const configPresent = Boolean(setup.config_path && (0, node_fs_1.existsSync)(setup.config_path));
11029
+ const configured = configPresent && (!hookSummary || hookSummary.ready);
8002
11030
  return {
8003
11031
  agent,
8004
- configured: Boolean(setup.config_path && (0, node_fs_1.existsSync)(setup.config_path)),
11032
+ configured,
8005
11033
  config_path: setup.config_path,
8006
11034
  notes: setup.instructions,
11035
+ hook_summary: hookSummary,
8007
11036
  };
8008
11037
  });
8009
11038
  }
@@ -8013,6 +11042,45 @@ function configMentionsKage(path) {
8013
11042
  const text = (0, node_fs_1.readFileSync)(path, "utf8");
8014
11043
  return /\bkage\b/.test(text) && /(mcp|mcpServers|mcp_servers)/i.test(text);
8015
11044
  }
11045
+ const CLAUDE_AMBIENT_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PostToolUse", "PostToolUseFailure", "PreCompact", "Stop", "SessionEnd"];
11046
+ function claudeHookEventConfigured(settings, event) {
11047
+ const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks)
11048
+ ? settings.hooks
11049
+ : {};
11050
+ const entry = hooks[event];
11051
+ if (!Array.isArray(entry) || !entry.length)
11052
+ return false;
11053
+ const text = JSON.stringify(entry);
11054
+ if (event === "SessionStart")
11055
+ return text.includes("session-start.sh");
11056
+ if (event === "Stop")
11057
+ return text.includes("stop.sh");
11058
+ return text.includes("observe.sh");
11059
+ }
11060
+ function claudeAmbientHookSummary(homeDir) {
11061
+ const settingsPath = (0, node_path_1.join)(homeDir, ".claude", "settings.json");
11062
+ const hookDir = (0, node_path_1.join)(homeDir, ".claude", "kage", "hooks");
11063
+ const scriptPaths = [(0, node_path_1.join)(hookDir, "session-start.sh"), (0, node_path_1.join)(hookDir, "observe.sh"), (0, node_path_1.join)(hookDir, "stop.sh")];
11064
+ let settings = {};
11065
+ if ((0, node_fs_1.existsSync)(settingsPath)) {
11066
+ const parsed = readJson(settingsPath);
11067
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
11068
+ settings = parsed;
11069
+ }
11070
+ const installed = CLAUDE_AMBIENT_HOOK_EVENTS.filter((event) => claudeHookEventConfigured(settings, event));
11071
+ const missing = CLAUDE_AMBIENT_HOOK_EVENTS.filter((event) => !installed.includes(event));
11072
+ for (const scriptPath of scriptPaths) {
11073
+ if (!(0, node_fs_1.existsSync)(scriptPath))
11074
+ missing.push((0, node_path_1.basename)(scriptPath));
11075
+ }
11076
+ return {
11077
+ required: [...CLAUDE_AMBIENT_HOOK_EVENTS],
11078
+ installed,
11079
+ missing: unique(missing),
11080
+ script_paths: scriptPaths,
11081
+ ready: missing.length === 0,
11082
+ };
11083
+ }
8016
11084
  function verifyAgentActivation(agent, projectDir, options = {}) {
8017
11085
  if (!exports.SETUP_AGENTS.includes(agent))
8018
11086
  throw new Error(`Unsupported agent: ${agent}`);
@@ -8022,7 +11090,7 @@ function verifyAgentActivation(agent, projectDir, options = {}) {
8022
11090
  const refreshed = indexProject(projectDir);
8023
11091
  const policyPath = (0, node_path_1.join)(projectDir, "AGENTS.md");
8024
11092
  const policyInstalled = (0, node_fs_1.existsSync)(policyPath) && (0, node_fs_1.readFileSync)(policyPath, "utf8").includes(AGENTS_POLICY_MARKER);
8025
- const requiredIndexes = ["catalog.json", "by-path.json", "by-tag.json", "by-type.json", "graph.json", "code-graph.json"];
11093
+ const requiredIndexes = ["catalog.json", "by-path.json", "by-tag.json", "by-type.json", "vector-local.json", "graph.json", "code-graph.json"];
8026
11094
  const indexSet = new Set(refreshed.indexes.map((path) => (0, node_path_1.basename)(path)));
8027
11095
  const indexesPresent = requiredIndexes.every((name) => indexSet.has(name));
8028
11096
  const recallResult = recall(projectDir, "kage setup repo memory code graph", 3, true);
@@ -8030,6 +11098,10 @@ function verifyAgentActivation(agent, projectDir, options = {}) {
8030
11098
  const recallWorks = recallResult.context_block.includes("Kage Context");
8031
11099
  const codeGraphWorks = codeGraph.files.length > 0;
8032
11100
  const mcpToolReachable = Boolean(options.mcpToolReachable);
11101
+ const hookSummary = agent === "claude-code"
11102
+ ? claudeAmbientHookSummary(options.homeDir ?? process.env.HOME ?? "~")
11103
+ : { required: [], installed: [], missing: [], script_paths: [], ready: true };
11104
+ const ambientHooksPresent = hookSummary.ready;
8033
11105
  const warnings = [];
8034
11106
  const nextSteps = [];
8035
11107
  if (!configPresent) {
@@ -8048,11 +11120,15 @@ function verifyAgentActivation(agent, projectDir, options = {}) {
8048
11120
  warnings.push("Generated indexes are missing or incomplete.");
8049
11121
  nextSteps.push(`Run: kage index --project ${projectDir}`);
8050
11122
  }
11123
+ if (!ambientHooksPresent && agent === "claude-code") {
11124
+ warnings.push(`Claude Code ambient memory hooks are incomplete: missing ${hookSummary.missing.join(", ")}.`);
11125
+ nextSteps.push(`Run: kage setup claude-code --project ${projectDir} --write`);
11126
+ }
8051
11127
  if (!mcpToolReachable) {
8052
11128
  warnings.push("This CLI can verify config, policy, recall, and code graph, but cannot prove the current agent session loaded the MCP server.");
8053
11129
  nextSteps.push(`Restart ${agent}, then ask it to call kage_verify_agent or list MCP tools.`);
8054
11130
  }
8055
- const status = !configPresent || !configHasKage ? "needs_setup" :
11131
+ const status = !configPresent || !configHasKage || !ambientHooksPresent ? "needs_setup" :
8056
11132
  !indexesPresent || !recallWorks || !codeGraphWorks ? "needs_index" :
8057
11133
  !mcpToolReachable ? "restart_required" :
8058
11134
  "ready";
@@ -8068,7 +11144,9 @@ function verifyAgentActivation(agent, projectDir, options = {}) {
8068
11144
  recall_works: recallWorks,
8069
11145
  code_graph_works: codeGraphWorks,
8070
11146
  mcp_tool_reachable: mcpToolReachable,
11147
+ ambient_hooks_present: ambientHooksPresent,
8071
11148
  },
11149
+ hook_summary: agent === "claude-code" ? hookSummary : undefined,
8072
11150
  config_path: setup.config_path,
8073
11151
  recall_preview: recallResult.results[0]?.packet.title ?? "No matching memory packet; recall surface is still reachable.",
8074
11152
  code_graph_summary: `${codeGraph.files.length} files, ${codeGraph.symbols.length} symbols, ${codeGraph.calls.length} calls, ${codeGraph.tests.length} tests`,
@@ -8264,6 +11342,342 @@ function reusablePromptObservation(event) {
8264
11342
  return "";
8265
11343
  return text;
8266
11344
  }
11345
+ function kageSessionCaptureReport(projectDir) {
11346
+ ensureMemoryDirs(projectDir);
11347
+ const observations = loadObservations(projectDir);
11348
+ const knownCommands = knownRepoCommands(projectDir);
11349
+ const bySession = new Map();
11350
+ for (const observation of observations) {
11351
+ const rows = bySession.get(observation.session_id) ?? [];
11352
+ rows.push(observation);
11353
+ bySession.set(observation.session_id, rows);
11354
+ }
11355
+ const sessions = Array.from(bySession.entries()).map(([sessionId, rows]) => {
11356
+ const sorted = rows.slice().sort((a, b) => a.timestamp.localeCompare(b.timestamp));
11357
+ const commandCandidates = sorted.filter((event) => event.type === "command_result" && reusableCommandObservation(event, knownCommands));
11358
+ const fileCandidates = sorted.filter((event) => event.type === "file_change" && reusableFileObservation(event));
11359
+ const promptCandidates = sorted.filter((event) => event.type === "user_prompt" && reusablePromptObservation(event));
11360
+ const candidateTypes = unique([
11361
+ ...(commandCandidates.length ? ["runbook"] : []),
11362
+ ...(fileCandidates.length ? ["workflow"] : []),
11363
+ ...(promptCandidates.length ? ["decision/context"] : []),
11364
+ ]);
11365
+ const durable = commandCandidates.length + fileCandidates.length + promptCandidates.length;
11366
+ return {
11367
+ session_id: sessionId,
11368
+ first_at: sorted[0]?.timestamp ?? "",
11369
+ last_at: sorted.at(-1)?.timestamp ?? "",
11370
+ observations: sorted.length,
11371
+ durable_observations: durable,
11372
+ agents: unique(sorted.map((event) => event.agent).filter(Boolean)),
11373
+ event_type_counts: countBy(sorted, (event) => event.type),
11374
+ commands: unique(sorted.map((event) => event.command).filter(Boolean)).slice(0, 8),
11375
+ paths: unique(sorted.map((event) => event.path).filter(Boolean)).slice(0, 12),
11376
+ candidate_types: candidateTypes,
11377
+ next_action: durable > 0
11378
+ ? `Run kage distill --project . --session ${sessionId} and review the generated packets.`
11379
+ : "No durable memory candidate yet; keep this as local telemetry only.",
11380
+ };
11381
+ }).sort((a, b) => b.last_at.localeCompare(a.last_at));
11382
+ return {
11383
+ schema_version: 1,
11384
+ project_dir: projectDir,
11385
+ generated_at: nowIso(),
11386
+ totals: {
11387
+ sessions: sessions.length,
11388
+ observations: observations.length,
11389
+ sessions_with_candidates: sessions.filter((session) => session.durable_observations > 0).length,
11390
+ durable_observations: sessions.reduce((sum, session) => sum + session.durable_observations, 0),
11391
+ },
11392
+ event_type_counts: countBy(observations, (event) => event.type),
11393
+ sessions,
11394
+ privacy_model: "Observations stay repo-local and privacy-scanned. Distillation writes reviewable memory packets only for durable learnings; raw transcript replay is not the product surface.",
11395
+ };
11396
+ }
11397
+ function observationCandidate(projectDir, event) {
11398
+ if (event.type === "command_result" && reusableCommandObservation(event, knownRepoCommands(projectDir))) {
11399
+ return { durable: true, type: "runbook" };
11400
+ }
11401
+ if (event.type === "file_change" && reusableFileObservation(event)) {
11402
+ return { durable: true, type: "workflow" };
11403
+ }
11404
+ if (event.type === "user_prompt" && reusablePromptObservation(event)) {
11405
+ return { durable: true, type: "decision/context" };
11406
+ }
11407
+ return { durable: false };
11408
+ }
11409
+ function observationLabel(event) {
11410
+ if (event.type === "command_result")
11411
+ return `Command${typeof event.exit_code === "number" ? ` exit ${event.exit_code}` : ""}`;
11412
+ if (event.type === "file_change")
11413
+ return event.path ? `File change: ${event.path}` : "File change";
11414
+ if (event.type === "tool_use")
11415
+ return event.tool ? `Tool use: ${event.tool}` : "Tool use";
11416
+ if (event.type === "tool_result")
11417
+ return event.tool ? `Tool result: ${event.tool}` : "Tool result";
11418
+ if (event.type === "test_result")
11419
+ return `Test result${typeof event.exit_code === "number" ? ` exit ${event.exit_code}` : ""}`;
11420
+ if (event.type === "user_prompt")
11421
+ return "User prompt";
11422
+ if (event.type === "session_start")
11423
+ return "Session started";
11424
+ if (event.type === "session_end")
11425
+ return "Session ended";
11426
+ return event.type;
11427
+ }
11428
+ function observationDigestSummary(event) {
11429
+ if (event.summary?.trim())
11430
+ return summarize(event.summary.trim()).slice(0, 220);
11431
+ if (event.type === "command_result" && event.command) {
11432
+ return `Command ${event.command} completed${typeof event.exit_code === "number" ? ` with exit ${event.exit_code}` : ""}.`;
11433
+ }
11434
+ if (event.type === "file_change" && event.path)
11435
+ return `Changed ${event.path}.`;
11436
+ if ((event.type === "tool_use" || event.type === "tool_result") && event.tool)
11437
+ return `${observationLabel(event)}.`;
11438
+ if (event.type === "test_result" && event.command)
11439
+ return `Test command ${event.command} completed${typeof event.exit_code === "number" ? ` with exit ${event.exit_code}` : ""}.`;
11440
+ return observationLabel(event);
11441
+ }
11442
+ function kageSessionReplay(projectDir, options = {}) {
11443
+ ensureMemoryDirs(projectDir);
11444
+ const limit = Math.max(1, Math.min(1000, Math.floor(options.limit ?? 200)));
11445
+ const observations = loadObservations(projectDir, options.sessionId);
11446
+ const bySession = new Map();
11447
+ for (const observation of observations) {
11448
+ const rows = bySession.get(observation.session_id) ?? [];
11449
+ rows.push(observation);
11450
+ bySession.set(observation.session_id, rows);
11451
+ }
11452
+ const sessions = Array.from(bySession.entries()).map(([sessionId, rows]) => {
11453
+ const sorted = rows.slice().sort((a, b) => a.timestamp.localeCompare(b.timestamp));
11454
+ const candidates = sorted.map((event) => observationCandidate(projectDir, event)).filter((candidate) => candidate.durable);
11455
+ return {
11456
+ session_id: sessionId,
11457
+ first_at: sorted[0]?.timestamp ?? "",
11458
+ last_at: sorted.at(-1)?.timestamp ?? "",
11459
+ events: sorted.length,
11460
+ durable_candidates: candidates.length,
11461
+ agents: unique(sorted.map((event) => event.agent).filter(Boolean)),
11462
+ event_type_counts: countBy(sorted, (event) => event.type),
11463
+ commands: unique(sorted.map((event) => event.command).filter(Boolean)).slice(0, 8),
11464
+ paths: unique(sorted.map((event) => event.path).filter(Boolean)).slice(0, 12),
11465
+ tools: unique(sorted.map((event) => event.tool).filter(Boolean)).slice(0, 8),
11466
+ distill_command: `kage distill --project . --session ${sessionId}`,
11467
+ };
11468
+ }).sort((a, b) => b.last_at.localeCompare(a.last_at));
11469
+ const firstTimestampBySession = new Map();
11470
+ for (const [sessionId, rows] of bySession.entries()) {
11471
+ const first = rows.slice().sort((a, b) => a.timestamp.localeCompare(b.timestamp))[0]?.timestamp;
11472
+ firstTimestampBySession.set(sessionId, first ? Date.parse(first) : 0);
11473
+ }
11474
+ const events = observations.slice(0, limit).map((event, index) => {
11475
+ const candidate = observationCandidate(projectDir, event);
11476
+ const first = firstTimestampBySession.get(event.session_id) ?? Date.parse(event.timestamp);
11477
+ const current = Date.parse(event.timestamp);
11478
+ return {
11479
+ index,
11480
+ timestamp: event.timestamp,
11481
+ offset_ms: Number.isFinite(current - first) ? Math.max(0, current - first) : 0,
11482
+ session_id: event.session_id,
11483
+ type: event.type,
11484
+ ...(event.agent ? { agent: event.agent } : {}),
11485
+ label: observationLabel(event),
11486
+ summary: observationDigestSummary(event),
11487
+ ...(event.tool ? { tool: event.tool } : {}),
11488
+ ...(event.path ? { path: event.path } : {}),
11489
+ ...(event.command ? { command: event.command } : {}),
11490
+ ...(typeof event.exit_code === "number" ? { exit_code: event.exit_code } : {}),
11491
+ durable_candidate: candidate.durable,
11492
+ ...(candidate.type ? { candidate_type: candidate.type } : {}),
11493
+ raw_text_included: false,
11494
+ sensitive_redacted: false,
11495
+ };
11496
+ });
11497
+ const durableCandidates = events.filter((event) => event.durable_candidate).length;
11498
+ return {
11499
+ schema_version: 1,
11500
+ project_dir: projectDir,
11501
+ generated_at: nowIso(),
11502
+ ...(options.sessionId ? { selected_session_id: options.sessionId } : {}),
11503
+ totals: {
11504
+ sessions: sessions.length,
11505
+ events: observations.length,
11506
+ durable_candidates: durableCandidates,
11507
+ },
11508
+ sessions,
11509
+ events,
11510
+ privacy_model: "Session replay is a privacy-preserving digest: raw transcript text is not included, observations are sensitive-scanned before storage, and durable learnings must be distilled into reviewable memory packets.",
11511
+ next_action: durableCandidates > 0
11512
+ ? "Run the listed distill command for sessions with durable candidates, then review the generated memory packets before sharing."
11513
+ : "No durable candidates in this digest yet; keep observing or capture reusable learnings with kage learn.",
11514
+ };
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
+ }
8267
11681
  function distillSession(projectDir, sessionId) {
8268
11682
  const observations = loadObservations(projectDir, sessionId);
8269
11683
  const candidates = [];
@@ -8431,6 +11845,8 @@ function createDiffChangeMemory(projectDir, summary) {
8431
11845
  freshness: {
8432
11846
  last_verified_at: now,
8433
11847
  ttl_days: 180,
11848
+ path_fingerprints: memoryPathFingerprints(projectDir, summary.changed_files.slice(0, 40)),
11849
+ path_fingerprint_policy: "source_hash_staleness",
8434
11850
  verification: "git_diff",
8435
11851
  },
8436
11852
  edges: summary.changed_files.slice(0, 20).map((file) => ({
@@ -8631,6 +12047,7 @@ function prCheck(projectDir) {
8631
12047
  const codeInputHash = currentCodeGraphInputHash(projectDir);
8632
12048
  const memoryInputHash = knowledgeGraphInputHash(projectDir, codeInputHash);
8633
12049
  const stalePackets = loadPacketsFromDir(packetsDir(projectDir))
12050
+ .filter((packet) => packet.status === "approved" || packet.status === "pending")
8634
12051
  .map((packet) => ({ packet, reasons: staleMemoryReasons(projectDir, packet) }))
8635
12052
  .filter((entry) => entry.reasons.length)
8636
12053
  .map((entry) => staleFinding(entry.packet, entry.reasons));
@@ -8641,6 +12058,8 @@ function prCheck(projectDir) {
8641
12058
  .filter((path) => path.startsWith(".agent_memory/packets/") && path.endsWith(".json"))).sort();
8642
12059
  const codeGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/code_graph/graph.json", { head: overlay.head, tree, inputHash: codeInputHash });
8643
12060
  const memoryGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/graph/graph.json", { head: overlay.head, tree, inputHash: memoryInputHash });
12061
+ const sessions = kageSessionCaptureReport(projectDir);
12062
+ const reconciliation = kageMemoryReconciliation(projectDir);
8644
12063
  const errors = [...validation.errors];
8645
12064
  const warnings = [...validation.warnings];
8646
12065
  const requiredActions = [];
@@ -8648,10 +12067,19 @@ function prCheck(projectDir) {
8648
12067
  errors.push(`${stalePackets.length} stale memory packet(s) require update, verification, or supersession.`);
8649
12068
  requiredActions.push("Run kage refresh, then update or supersede stale packets.");
8650
12069
  }
12070
+ if (reconciliation.unresolved_count > 0) {
12071
+ errors.push(`${reconciliation.unresolved_count} memory reconciliation item(s) require agent update or supersession.`);
12072
+ requiredActions.push(...reconciliation.items.slice(0, 5).map((item) => item.next_action));
12073
+ }
8651
12074
  if (!codeGraphCurrent || !memoryGraphCurrent) {
8652
12075
  errors.push("Generated graph artifacts are missing or not current for this working tree content.");
8653
12076
  requiredActions.push("Run kage refresh --project <dir> before merge.");
8654
12077
  }
12078
+ const distillableSessions = sessions.sessions.filter((session) => session.durable_observations > 0);
12079
+ if (distillableSessions.length) {
12080
+ errors.push(`${distillableSessions.length} distillable session learning${distillableSessions.length === 1 ? "" : "s"} require review before merge.`);
12081
+ requiredActions.push(...distillableSessions.slice(0, 5).map((session) => session.next_action));
12082
+ }
8655
12083
  if (!memoryPacketChanges.length && overlay.changed_files.some((path) => !path.startsWith(".agent_memory/"))) {
8656
12084
  warnings.push("No repo memory packet changed for this branch. If durable knowledge was learned, run kage propose --from-diff or kage learn.");
8657
12085
  }
@@ -8668,6 +12096,7 @@ function prCheck(projectDir) {
8668
12096
  memory_packet_changes: memoryPacketChanges,
8669
12097
  code_graph_current: codeGraphCurrent,
8670
12098
  memory_graph_current: memoryGraphCurrent,
12099
+ memory_reconciliation: reconciliation,
8671
12100
  errors,
8672
12101
  warnings,
8673
12102
  required_actions: requiredActions,
@@ -9152,7 +12581,7 @@ function recordFeedback(projectDir, id, feedback) {
9152
12581
  const packet = readJson(path);
9153
12582
  if (packet.id !== id)
9154
12583
  continue;
9155
- const quality = packet.quality;
12584
+ const quality = (packet.quality ?? {});
9156
12585
  const increment = (key) => {
9157
12586
  quality[key] = Number(quality[key] ?? 0) + 1;
9158
12587
  };
@@ -9171,6 +12600,10 @@ function recordFeedback(projectDir, id, feedback) {
9171
12600
  };
9172
12601
  }
9173
12602
  writeJson(path, packet);
12603
+ recordMemoryAudit(projectDir, "feedback", [packet], {
12604
+ feedback,
12605
+ path: (0, node_path_1.relative)(projectDir, path),
12606
+ });
9174
12607
  buildIndexes(projectDir);
9175
12608
  return { ok: true, packet, path, errors: [] };
9176
12609
  }
@@ -9180,6 +12613,7 @@ function validateProject(projectDir) {
9180
12613
  ensureMemoryDirs(projectDir);
9181
12614
  const errors = [];
9182
12615
  const warnings = [];
12616
+ const qualityContext = memoryQualityContext(projectDir);
9183
12617
  for (const [dir, label] of [
9184
12618
  [packetsDir(projectDir), "packet"],
9185
12619
  [pendingDir(projectDir), "pending"],
@@ -9194,7 +12628,7 @@ function validateProject(projectDir) {
9194
12628
  const activeMemory = packet.status === "approved" || packet.status === "pending";
9195
12629
  if (activeMemory) {
9196
12630
  warnings.push(...packetGroundingWarnings(projectDir, packet, (0, node_path_1.relative)(projectDir, packetPath)));
9197
- const quality = evaluateMemoryQuality(projectDir, packet);
12631
+ const quality = evaluateMemoryQuality(projectDir, packet, qualityContext);
9198
12632
  if (Number(quality.score) < 55)
9199
12633
  warnings.push(`${(0, node_path_1.relative)(projectDir, packetPath)}: low memory quality score ${quality.score}`);
9200
12634
  const duplicates = quality.duplicate_candidates;
@@ -9298,7 +12732,7 @@ function initProject(projectDir) {
9298
12732
  }
9299
12733
  function doctorProject(projectDir) {
9300
12734
  ensureMemoryDirs(projectDir);
9301
- const expectedIndexes = ["catalog.json", "by-path.json", "by-tag.json", "by-type.json", "graph.json", "code-graph.json"];
12735
+ const expectedIndexes = ["catalog.json", "by-path.json", "by-tag.json", "by-type.json", "vector-local.json", "graph.json", "code-graph.json"];
9302
12736
  const present = expectedIndexes.filter((name) => (0, node_fs_1.existsSync)((0, node_path_1.join)(indexesDir(projectDir), name)));
9303
12737
  const missing = expectedIndexes.filter((name) => !present.includes(name));
9304
12738
  const validation = validateProject(projectDir);
@@ -9331,6 +12765,10 @@ function approvePending(projectDir, id) {
9331
12765
  const target = (0, node_path_1.join)(packetsDir(projectDir), packetFileName(packet));
9332
12766
  writeJson(target, packet);
9333
12767
  (0, node_fs_1.renameSync)(path, `${path}.approved`);
12768
+ recordMemoryAudit(projectDir, "approve", [packet], {
12769
+ from: (0, node_path_1.relative)(projectDir, path),
12770
+ to: (0, node_path_1.relative)(projectDir, target),
12771
+ });
9334
12772
  buildIndexes(projectDir);
9335
12773
  return target;
9336
12774
  }
@@ -9344,6 +12782,10 @@ function rejectPending(projectDir, id) {
9344
12782
  if (packet.id === id) {
9345
12783
  const target = `${path}.rejected`;
9346
12784
  (0, node_fs_1.renameSync)(path, target);
12785
+ recordMemoryAudit(projectDir, "reject", [packet], {
12786
+ from: (0, node_path_1.relative)(projectDir, path),
12787
+ to: (0, node_path_1.relative)(projectDir, target),
12788
+ });
9347
12789
  return target;
9348
12790
  }
9349
12791
  }
@@ -9390,3 +12832,275 @@ function changelog(projectDir, days = 7) {
9390
12832
  total: added.length + updated.length + deprecated.length,
9391
12833
  };
9392
12834
  }
12835
+ function timelineSourceKind(packet) {
12836
+ const first = packet.source_refs[0];
12837
+ const kind = first && typeof first.kind === "string" ? first.kind : "";
12838
+ if (kind)
12839
+ return kind;
12840
+ if (isGeneratedChangeMemory(packet))
12841
+ return "git_diff";
12842
+ return "memory_packet";
12843
+ }
12844
+ function timelineAction(kind, packet) {
12845
+ if (kind === "pending")
12846
+ return "Review this pending packet before it becomes shared repo memory.";
12847
+ if (kind === "deprecated")
12848
+ return "Check whether a newer packet supersedes this memory before relying on it.";
12849
+ if (kind === "updated")
12850
+ return "Review the latest rationale, paths, and evidence before future agents reuse it.";
12851
+ if (isGeneratedChangeMemory(packet))
12852
+ return "Use as branch handoff context; turn durable lessons into focused memory packets.";
12853
+ return "Review recent memory changes so teammates understand what agents just learned.";
12854
+ }
12855
+ function timelineEntry(kind, packet, date) {
12856
+ return {
12857
+ kind,
12858
+ packet_id: packet.id,
12859
+ title: packet.title,
12860
+ type: packet.type,
12861
+ status: packet.status,
12862
+ date,
12863
+ summary: packet.summary,
12864
+ paths: packet.paths,
12865
+ tags: packet.tags,
12866
+ source_kind: timelineSourceKind(packet),
12867
+ action: timelineAction(kind, packet),
12868
+ };
12869
+ }
12870
+ function packetEdgeValue(edge, key) {
12871
+ const value = edge[key];
12872
+ return typeof value === "string" ? value : "";
12873
+ }
12874
+ function upsertPacketEdge(packet, relation, to, evidence, at) {
12875
+ const exists = packet.edges.some((edge) => packetEdgeValue(edge, "relation") === relation && packetEdgeValue(edge, "to") === to);
12876
+ if (exists)
12877
+ return;
12878
+ packet.edges.push({
12879
+ relation,
12880
+ to,
12881
+ evidence,
12882
+ created_at: at,
12883
+ });
12884
+ }
12885
+ function packetSupersededBy(packet) {
12886
+ const qualityReplacement = packet.quality?.superseded_by;
12887
+ if (typeof qualityReplacement === "string" && qualityReplacement.trim())
12888
+ return qualityReplacement.trim();
12889
+ const freshnessReplacement = packet.freshness?.superseded_by;
12890
+ if (typeof freshnessReplacement === "string" && freshnessReplacement.trim())
12891
+ return freshnessReplacement.trim();
12892
+ const edge = packet.edges.find((item) => packetEdgeValue(item, "relation") === "superseded_by" && packetEdgeValue(item, "to"));
12893
+ return edge ? packetEdgeValue(edge, "to") : "";
12894
+ }
12895
+ function packetSupersessionReason(packet) {
12896
+ const qualityReason = packet.quality?.superseded_reason;
12897
+ if (typeof qualityReason === "string" && qualityReason.trim())
12898
+ return qualityReason.trim();
12899
+ const freshnessReason = packet.freshness?.superseded_reason;
12900
+ if (typeof freshnessReason === "string" && freshnessReason.trim())
12901
+ return freshnessReason.trim();
12902
+ const edge = packet.edges.find((item) => packetEdgeValue(item, "relation") === "superseded_by");
12903
+ const evidence = edge ? packetEdgeValue(edge, "evidence") : "";
12904
+ return evidence || "This memory was superseded by newer repo knowledge.";
12905
+ }
12906
+ function supersedeMemory(projectDir, oldPacketId, replacementPacketId, reason = "") {
12907
+ ensureMemoryDirs(projectDir);
12908
+ const trimmedReason = reason.trim() || "Newer repo memory supersedes this packet.";
12909
+ const warnings = [];
12910
+ if (oldPacketId === replacementPacketId) {
12911
+ return {
12912
+ ok: false,
12913
+ project_dir: projectDir,
12914
+ old_packet_id: oldPacketId,
12915
+ replacement_packet_id: replacementPacketId,
12916
+ reason: trimmedReason,
12917
+ errors: ["A memory packet cannot supersede itself."],
12918
+ warnings,
12919
+ };
12920
+ }
12921
+ const entries = loadPacketEntriesFromDir(packetsDir(projectDir));
12922
+ const oldEntry = entries.find((entry) => entry.packet.id === oldPacketId);
12923
+ const replacementEntry = entries.find((entry) => entry.packet.id === replacementPacketId);
12924
+ const errors = [];
12925
+ if (!oldEntry)
12926
+ errors.push(`Packet not found: ${oldPacketId}`);
12927
+ if (!replacementEntry)
12928
+ errors.push(`Replacement packet not found: ${replacementPacketId}`);
12929
+ if (errors.length) {
12930
+ return {
12931
+ ok: false,
12932
+ project_dir: projectDir,
12933
+ old_packet_id: oldPacketId,
12934
+ replacement_packet_id: replacementPacketId,
12935
+ reason: trimmedReason,
12936
+ errors,
12937
+ warnings,
12938
+ };
12939
+ }
12940
+ const oldPacket = oldEntry.packet;
12941
+ const replacementPacket = replacementEntry.packet;
12942
+ if (replacementPacket.status !== "approved") {
12943
+ warnings.push(`Replacement packet status is ${replacementPacket.status}; approved replacements are safest for recall.`);
12944
+ }
12945
+ const at = nowIso();
12946
+ oldPacket.status = "superseded";
12947
+ oldPacket.updated_at = at;
12948
+ oldPacket.quality = {
12949
+ ...oldPacket.quality,
12950
+ superseded_by: replacementPacket.id,
12951
+ superseded_reason: trimmedReason,
12952
+ };
12953
+ oldPacket.freshness = {
12954
+ ...oldPacket.freshness,
12955
+ superseded_at: at,
12956
+ superseded_by: replacementPacket.id,
12957
+ superseded_reason: trimmedReason,
12958
+ };
12959
+ upsertPacketEdge(oldPacket, "superseded_by", replacementPacket.id, trimmedReason, at);
12960
+ replacementPacket.updated_at = at;
12961
+ upsertPacketEdge(replacementPacket, "supersedes", oldPacket.id, trimmedReason, at);
12962
+ writeJson(oldEntry.path, oldPacket);
12963
+ writeJson(replacementEntry.path, replacementPacket);
12964
+ recordMemoryAudit(projectDir, "supersede", [oldPacket, replacementPacket], {
12965
+ old_packet_id: oldPacket.id,
12966
+ replacement_packet_id: replacementPacket.id,
12967
+ reason: trimmedReason,
12968
+ old_path: (0, node_path_1.relative)(projectDir, oldEntry.path),
12969
+ replacement_path: (0, node_path_1.relative)(projectDir, replacementEntry.path),
12970
+ });
12971
+ buildIndexes(projectDir);
12972
+ return {
12973
+ ok: true,
12974
+ project_dir: projectDir,
12975
+ old_packet_id: oldPacket.id,
12976
+ replacement_packet_id: replacementPacket.id,
12977
+ reason: trimmedReason,
12978
+ old_packet: oldPacket,
12979
+ replacement_packet: replacementPacket,
12980
+ old_path: oldEntry.path,
12981
+ replacement_path: replacementEntry.path,
12982
+ errors: [],
12983
+ warnings,
12984
+ };
12985
+ }
12986
+ function kageMemoryLineage(projectDir) {
12987
+ ensureMemoryDirs(projectDir);
12988
+ const packets = loadPacketsFromDir(packetsDir(projectDir));
12989
+ const byId = new Map(packets.map((packet) => [packet.id, packet]));
12990
+ const supersededPackets = packets.filter((packet) => packet.status === "superseded" || packetSupersededBy(packet));
12991
+ const grouped = new Map();
12992
+ const orphans = [];
12993
+ for (const packet of supersededPackets) {
12994
+ const replacementId = packetSupersededBy(packet);
12995
+ if (!replacementId || !byId.has(replacementId)) {
12996
+ orphans.push({
12997
+ packet_id: packet.id,
12998
+ title: packet.title,
12999
+ status: packet.status,
13000
+ updated_at: packet.updated_at,
13001
+ reason: replacementId ? `Replacement packet is missing: ${replacementId}` : packetSupersessionReason(packet),
13002
+ action: "Add a replacement link or restore this packet only if the old memory is still correct.",
13003
+ });
13004
+ continue;
13005
+ }
13006
+ const list = grouped.get(replacementId) ?? [];
13007
+ list.push(packet);
13008
+ grouped.set(replacementId, list);
13009
+ }
13010
+ const chains = [];
13011
+ for (const [replacementId, oldPackets] of grouped) {
13012
+ const replacement = byId.get(replacementId);
13013
+ if (!replacement)
13014
+ continue;
13015
+ oldPackets.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || a.title.localeCompare(b.title));
13016
+ const paths = unique([...replacement.paths, ...oldPackets.flatMap((packet) => packet.paths)]).slice(0, 12);
13017
+ chains.push({
13018
+ current_packet_id: replacement.id,
13019
+ current_title: replacement.title,
13020
+ current_status: replacement.status,
13021
+ superseded_packet_ids: oldPackets.map((packet) => packet.id),
13022
+ superseded_titles: oldPackets.map((packet) => packet.title),
13023
+ reason: packetSupersessionReason(oldPackets[0]),
13024
+ paths,
13025
+ updated_at: [replacement.updated_at, ...oldPackets.map((packet) => packet.updated_at)].sort().at(-1) ?? replacement.updated_at,
13026
+ action: "Use the current replacement packet in recall; keep superseded packets only as audit history.",
13027
+ });
13028
+ }
13029
+ chains.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || a.current_title.localeCompare(b.current_title));
13030
+ orphans.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || a.title.localeCompare(b.title));
13031
+ const recommendations = unique([
13032
+ ...(chains.length ? ["Use current replacement packets during handoff so agents do not rely on retired memory."] : []),
13033
+ ...(orphans.length ? ["Resolve superseded memories without a replacement link before trusting old context."] : []),
13034
+ ...(!chains.length && !orphans.length ? ["No superseded memory chains yet; use kage supersede when a better packet replaces old repo knowledge."] : []),
13035
+ ]);
13036
+ return {
13037
+ schema_version: 1,
13038
+ project_dir: projectDir,
13039
+ generated_at: nowIso(),
13040
+ totals: {
13041
+ superseded: supersededPackets.length,
13042
+ chains: chains.length,
13043
+ orphans: orphans.length,
13044
+ replacements_missing: orphans.filter((item) => item.reason.startsWith("Replacement packet is missing:")).length,
13045
+ },
13046
+ chains,
13047
+ orphans,
13048
+ recommendations,
13049
+ };
13050
+ }
13051
+ function kageMemoryTimeline(projectDir, days = 14) {
13052
+ ensureMemoryDirs(projectDir);
13053
+ const boundedDays = Math.max(1, Math.min(365, Math.floor(Number(days) || 14)));
13054
+ const since = new Date(Date.now() - boundedDays * 24 * 60 * 60 * 1000);
13055
+ const sinceIso = since.toISOString();
13056
+ const packets = loadPacketsFromDir(packetsDir(projectDir));
13057
+ const pending = loadPendingPackets(projectDir);
13058
+ const entries = [];
13059
+ for (const packet of packets) {
13060
+ const createdAt = packet.created_at ?? "";
13061
+ const updatedAt = packet.updated_at ?? "";
13062
+ const isRecentlyCreated = createdAt >= sinceIso;
13063
+ const isRecentlyUpdated = updatedAt >= sinceIso && updatedAt !== createdAt;
13064
+ if (packet.status === "deprecated" || packet.status === "superseded") {
13065
+ if (isRecentlyCreated || isRecentlyUpdated)
13066
+ entries.push(timelineEntry("deprecated", packet, updatedAt || createdAt));
13067
+ }
13068
+ else if (packet.status === "approved") {
13069
+ if (isRecentlyCreated)
13070
+ entries.push(timelineEntry("added", packet, createdAt));
13071
+ else if (isRecentlyUpdated)
13072
+ entries.push(timelineEntry("updated", packet, updatedAt));
13073
+ }
13074
+ }
13075
+ for (const packet of pending) {
13076
+ const createdAt = packet.created_at ?? packet.updated_at ?? "";
13077
+ const updatedAt = packet.updated_at ?? createdAt;
13078
+ const date = updatedAt >= createdAt ? updatedAt : createdAt;
13079
+ if (date >= sinceIso)
13080
+ entries.push(timelineEntry("pending", packet, date));
13081
+ }
13082
+ entries.sort((a, b) => b.date.localeCompare(a.date) || a.title.localeCompare(b.title));
13083
+ const totals = {
13084
+ added: entries.filter((entry) => entry.kind === "added").length,
13085
+ updated: entries.filter((entry) => entry.kind === "updated").length,
13086
+ deprecated: entries.filter((entry) => entry.kind === "deprecated").length,
13087
+ pending: entries.filter((entry) => entry.kind === "pending").length,
13088
+ total: entries.length,
13089
+ };
13090
+ const recommendations = unique([
13091
+ ...(entries.length ? ["Review recent memory changes during handoff so teammates know what agents learned."] : ["No recent memory changes; capture durable decisions, bugs, runbooks, or gotchas as work happens."]),
13092
+ ...(totals.pending ? ["Approve, reject, merge, or keep pending memory before relying on it across teammates."] : []),
13093
+ ...(totals.deprecated ? ["Check deprecated or superseded memories for replacement packets before recall."] : []),
13094
+ ...(totals.updated ? ["Inspect updated memories for changed rationale, evidence, or affected paths."] : []),
13095
+ ]);
13096
+ return {
13097
+ schema_version: 1,
13098
+ project_dir: projectDir,
13099
+ generated_at: nowIso(),
13100
+ days: boundedDays,
13101
+ since: sinceIso,
13102
+ totals,
13103
+ entries,
13104
+ recommendations,
13105
+ };
13106
+ }