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

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,13 +84,21 @@ 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;
80
92
  exports.kageRisk = kageRisk;
81
93
  exports.kageDependencyPath = kageDependencyPath;
82
94
  exports.kageCleanupCandidates = kageCleanupCandidates;
83
95
  exports.kageReviewerSuggestions = kageReviewerSuggestions;
84
96
  exports.kageContributors = kageContributors;
97
+ exports.kageContextSlots = kageContextSlots;
98
+ exports.setContextSlot = setContextSlot;
99
+ exports.deleteContextSlot = deleteContextSlot;
100
+ exports.kageProjectProfile = kageProjectProfile;
101
+ exports.kageCapabilityAudit = kageCapabilityAudit;
85
102
  exports.kageDecisionIntelligence = kageDecisionIntelligence;
86
103
  exports.kageModuleHealth = kageModuleHealth;
87
104
  exports.kageGraphInsights = kageGraphInsights;
@@ -94,6 +111,8 @@ exports.auditProject = auditProject;
94
111
  exports.memoryInbox = memoryInbox;
95
112
  exports.qualityReport = qualityReport;
96
113
  exports.benchmarkProject = benchmarkProject;
114
+ exports.benchmarkCodingMemoryQuality = benchmarkCodingMemoryQuality;
115
+ exports.benchmarkMemoryScale = benchmarkMemoryScale;
97
116
  exports.benchmarkTaskComparison = benchmarkTaskComparison;
98
117
  exports.learn = learn;
99
118
  exports.capture = capture;
@@ -103,6 +122,8 @@ exports.setupAgent = setupAgent;
103
122
  exports.setupDoctor = setupDoctor;
104
123
  exports.verifyAgentActivation = verifyAgentActivation;
105
124
  exports.observe = observe;
125
+ exports.kageSessionCaptureReport = kageSessionCaptureReport;
126
+ exports.kageSessionReplay = kageSessionReplay;
106
127
  exports.distillSession = distillSession;
107
128
  exports.proposeFromDiff = proposeFromDiff;
108
129
  exports.buildBranchOverlay = buildBranchOverlay;
@@ -128,6 +149,9 @@ exports.doctorProject = doctorProject;
128
149
  exports.approvePending = approvePending;
129
150
  exports.rejectPending = rejectPending;
130
151
  exports.changelog = changelog;
152
+ exports.supersedeMemory = supersedeMemory;
153
+ exports.kageMemoryLineage = kageMemoryLineage;
154
+ exports.kageMemoryTimeline = kageMemoryTimeline;
131
155
  const node_crypto_1 = require("node:crypto");
132
156
  const node_child_process_1 = require("node:child_process");
133
157
  const node_fs_1 = require("node:fs");
@@ -221,6 +245,13 @@ git commit changed without graph inputs changing.
221
245
  Before finishing a task that changed files, call \`kage_pr_summarize\` or
222
246
  \`kage_propose_from_diff\`, then call \`kage_pr_check\`.
223
247
 
248
+ \`kage_context\`, Stop hooks, and \`kage_pr_check\` may report memory
249
+ reconciliation items when files linked to existing memory changed. Resolve these
250
+ as agent work before the final response: write updated memory with
251
+ \`kage_learn\`, supersede replaced packets with \`kage_supersede\`, or mark stale
252
+ only when the memory can no longer be trusted. Do not hand this off as a user
253
+ inbox chore.
254
+
224
255
  \`kage_pr_summarize\` writes a branch review summary and a repo-local
225
256
  change-memory packet. \`kage_pr_check\` verifies validation, graph freshness,
226
257
  stale packets, and whether repo memory changed with the branch. If the check
@@ -313,12 +344,21 @@ function branchesDir(projectDir) {
313
344
  function reviewDir(projectDir) {
314
345
  return (0, node_path_1.join)(memoryRoot(projectDir), "review");
315
346
  }
347
+ function auditDir(projectDir) {
348
+ return (0, node_path_1.join)(memoryRoot(projectDir), "audit");
349
+ }
350
+ function reportsDir(projectDir) {
351
+ return (0, node_path_1.join)(memoryRoot(projectDir), "reports");
352
+ }
316
353
  function publicBundleDir(projectDir) {
317
354
  return (0, node_path_1.join)(memoryRoot(projectDir), "public-bundle");
318
355
  }
319
356
  function observationsDir(projectDir) {
320
357
  return (0, node_path_1.join)(memoryRoot(projectDir), "observations");
321
358
  }
359
+ function slotsDir(projectDir) {
360
+ return (0, node_path_1.join)(memoryRoot(projectDir), "slots");
361
+ }
322
362
  function daemonDir(projectDir) {
323
363
  return (0, node_path_1.join)(memoryRoot(projectDir), "daemon");
324
364
  }
@@ -473,6 +513,421 @@ function estimateTokens(text) {
473
513
  function packetText(packet) {
474
514
  return `${packet.title}\n${packet.summary}\n${packet.body}\n${packet.type}\n${packet.tags.join(" ")}\n${packet.paths.join(" ")}`;
475
515
  }
516
+ const ACCESS_WINDOW_DAYS = 30;
517
+ const ACCESS_RECENT_CAP = 20;
518
+ function memoryAccessPath(projectDir) {
519
+ return (0, node_path_1.join)(reportsDir(projectDir), "memory-access.json");
520
+ }
521
+ function normalizeAccessRecent(value) {
522
+ const raw = Array.isArray(value) ? value : [];
523
+ return raw
524
+ .map((item) => {
525
+ const entry = item;
526
+ const at = typeof entry.at === "string" ? entry.at : "";
527
+ const time = Date.parse(at);
528
+ const rank = Number(entry.rank);
529
+ if (!Number.isFinite(time) || !Number.isFinite(rank))
530
+ return null;
531
+ return { at, rank: Math.max(1, Math.floor(rank)) };
532
+ })
533
+ .filter((item) => Boolean(item))
534
+ .sort((a, b) => Date.parse(a.at) - Date.parse(b.at))
535
+ .slice(-ACCESS_RECENT_CAP);
536
+ }
537
+ function accessWindowCutoff() {
538
+ return Date.now() - ACCESS_WINDOW_DAYS * 86_400_000;
539
+ }
540
+ function normalizeAccessEntry(raw, packet) {
541
+ const value = raw;
542
+ const packetId = packet?.id ?? (typeof value?.packet_id === "string" ? value.packet_id : "");
543
+ if (!packetId)
544
+ return null;
545
+ const recent = normalizeAccessRecent(value?.recent);
546
+ const cutoff = accessWindowCutoff();
547
+ const uses30d = recent.filter((item) => Date.parse(item.at) >= cutoff).length;
548
+ const lastRecent = recent.at(-1);
549
+ const totalUses = Math.max(Number(value?.total_uses ?? 0) || 0, recent.length);
550
+ const lastRank = Number(value?.last_rank);
551
+ const bestRankCandidates = [
552
+ Number(value?.best_rank),
553
+ ...recent.map((item) => item.rank),
554
+ ].filter((item) => Number.isFinite(item) && item > 0);
555
+ return {
556
+ packet_id: packetId,
557
+ title: packet?.title ?? String(value?.title ?? packetId),
558
+ type: packet?.type ?? (value?.type ?? "reference"),
559
+ paths: packet?.paths ?? (Array.isArray(value?.paths) ? value.paths.map(String) : []),
560
+ tags: packet?.tags ?? (Array.isArray(value?.tags) ? value.tags.map(String) : []),
561
+ total_uses: totalUses,
562
+ uses_30d: uses30d,
563
+ last_accessed_at: lastRecent?.at ?? (typeof value?.last_accessed_at === "string" ? value.last_accessed_at : null),
564
+ best_rank: bestRankCandidates.length ? Math.min(...bestRankCandidates) : null,
565
+ last_rank: Number.isFinite(lastRank) && lastRank > 0 ? Math.floor(lastRank) : (lastRecent?.rank ?? null),
566
+ recent,
567
+ };
568
+ }
569
+ function readMemoryAccessEntries(projectDir, packets = loadApprovedPackets(projectDir)) {
570
+ const byPacket = new Map(packets.map((packet) => [packet.id, packet]));
571
+ const entries = new Map();
572
+ const path = memoryAccessPath(projectDir);
573
+ if (!(0, node_fs_1.existsSync)(path))
574
+ return entries;
575
+ try {
576
+ const raw = readJson(path);
577
+ for (const item of raw.entries ?? []) {
578
+ const id = typeof item.packet_id === "string" ? item.packet_id : "";
579
+ const normalized = normalizeAccessEntry(item, byPacket.get(id));
580
+ if (normalized)
581
+ entries.set(normalized.packet_id, normalized);
582
+ }
583
+ }
584
+ catch {
585
+ return entries;
586
+ }
587
+ return entries;
588
+ }
589
+ function memoryAccessScore(entry) {
590
+ if (!entry || entry.uses_30d <= 0)
591
+ return 0;
592
+ const rankBoost = entry.best_rank ? Math.max(0, 1.2 - (entry.best_rank - 1) * 0.2) : 0;
593
+ const useBoost = Math.min(2.8, Math.log1p(entry.uses_30d) * 0.9);
594
+ return Number((rankBoost + useBoost).toFixed(2));
595
+ }
596
+ function buildMemoryAccessRecommendations(normalized, tracked, totals, packets) {
597
+ const recommendations = [];
598
+ const packetById = new Map(packets.map((packet) => [packet.id, packet]));
599
+ const reviewable = normalized.filter((entry) => {
600
+ const packet = packetById.get(entry.packet_id);
601
+ return !packet || !isGeneratedChangeMemory(packet);
602
+ });
603
+ if (!normalized.length) {
604
+ recommendations.push({
605
+ kind: "seed_usage",
606
+ severity: "info",
607
+ summary: "No approved memory packets exist yet.",
608
+ reason: "Kage cannot learn reuse patterns until the repo has reviewable memory packets.",
609
+ action: "Capture durable decisions, bug fixes, runbooks, or gotchas with kage learn or kage propose.",
610
+ });
611
+ return recommendations;
612
+ }
613
+ if (!tracked.length) {
614
+ recommendations.push({
615
+ kind: "seed_usage",
616
+ severity: "info",
617
+ summary: "No recall usage has been observed yet.",
618
+ reason: "Memory access telemetry is local and only grows when agents naturally recall repo knowledge.",
619
+ action: "Run normal agent tasks with Kage recall enabled, then reopen this report to see hot and cold memory.",
620
+ });
621
+ }
622
+ reviewable
623
+ .filter((entry) => entry.uses_30d >= 3)
624
+ .sort((a, b) => b.uses_30d - a.uses_30d || b.total_uses - a.total_uses || (a.best_rank ?? 99) - (b.best_rank ?? 99))
625
+ .slice(0, 3)
626
+ .forEach((entry) => {
627
+ recommendations.push({
628
+ kind: "promote_hot",
629
+ severity: "ok",
630
+ packet_id: entry.packet_id,
631
+ title: entry.title,
632
+ type: entry.type,
633
+ paths: entry.paths,
634
+ uses_30d: entry.uses_30d,
635
+ total_uses: entry.total_uses,
636
+ summary: `Keep verified: ${entry.title}`,
637
+ reason: `Agents recalled this packet ${entry.uses_30d} time${entry.uses_30d === 1 ? "" : "s"} in ${ACCESS_WINDOW_DAYS} days.`,
638
+ action: "Keep the packet evidence-backed; update it when the linked workflow or code path changes.",
639
+ });
640
+ });
641
+ reviewable
642
+ .filter((entry) => entry.uses_30d === 0 && entry.paths.length === 0)
643
+ .sort((a, b) => a.title.localeCompare(b.title))
644
+ .slice(0, 3)
645
+ .forEach((entry) => {
646
+ recommendations.push({
647
+ kind: "connect_paths",
648
+ severity: "warn",
649
+ packet_id: entry.packet_id,
650
+ title: entry.title,
651
+ type: entry.type,
652
+ paths: entry.paths,
653
+ uses_30d: entry.uses_30d,
654
+ total_uses: entry.total_uses,
655
+ summary: `Add code grounding: ${entry.title}`,
656
+ reason: "This packet has no code paths, so future agents have less evidence for when to recall it.",
657
+ action: "Add the files, symbols, routes, or tests this memory explains, or supersede it if it is too generic.",
658
+ });
659
+ });
660
+ reviewable
661
+ .filter((entry) => entry.total_uses === 0)
662
+ .sort((a, b) => a.title.localeCompare(b.title))
663
+ .slice(0, 3)
664
+ .forEach((entry) => {
665
+ recommendations.push({
666
+ kind: "review_cold",
667
+ severity: totals.tracked_packets ? "warn" : "info",
668
+ packet_id: entry.packet_id,
669
+ title: entry.title,
670
+ type: entry.type,
671
+ paths: entry.paths,
672
+ uses_30d: entry.uses_30d,
673
+ total_uses: entry.total_uses,
674
+ summary: `Review cold memory: ${entry.title}`,
675
+ reason: "This approved packet has not been recalled by recent agent tasks.",
676
+ action: "Verify it is still true, improve its title/tags/paths, or mark it stale during memory review.",
677
+ });
678
+ });
679
+ return recommendations.slice(0, 8);
680
+ }
681
+ function buildMemoryAccessReport(projectDir, entries, packets = loadApprovedPackets(projectDir)) {
682
+ const activeIds = new Set(packets.map((packet) => packet.id));
683
+ const normalized = packets.map((packet) => normalizeAccessEntry(entries.get(packet.id), packet)).filter((entry) => Boolean(entry));
684
+ const tracked = normalized.filter((entry) => entry.total_uses > 0 || entry.recent.length > 0);
685
+ const totalUses = tracked.reduce((sum, entry) => sum + entry.total_uses, 0);
686
+ const uses30d = tracked.reduce((sum, entry) => sum + entry.uses_30d, 0);
687
+ const lastAccessedAt = tracked
688
+ .map((entry) => entry.last_accessed_at)
689
+ .filter((value) => Boolean(value))
690
+ .sort()
691
+ .at(-1) ?? null;
692
+ const totals = {
693
+ tracked_packets: tracked.length,
694
+ total_uses: totalUses,
695
+ uses_30d: uses30d,
696
+ hot_packets: tracked.filter((entry) => entry.uses_30d >= 3).length,
697
+ cold_packets: packets.filter((packet) => !entries.has(packet.id) || (entries.get(packet.id)?.uses_30d ?? 0) === 0).length,
698
+ active_packets_without_access: packets.filter((packet) => activeIds.has(packet.id) && (entries.get(packet.id)?.total_uses ?? 0) === 0).length,
699
+ last_accessed_at: lastAccessedAt,
700
+ };
701
+ return {
702
+ schema_version: 1,
703
+ project_dir: projectDir,
704
+ generated_at: nowIso(),
705
+ window_days: ACCESS_WINDOW_DAYS,
706
+ totals,
707
+ entries: normalized
708
+ .sort((a, b) => b.uses_30d - a.uses_30d || b.total_uses - a.total_uses || a.title.localeCompare(b.title)),
709
+ recommendations: buildMemoryAccessRecommendations(normalized, tracked, totals, packets),
710
+ };
711
+ }
712
+ function kageMemoryAccess(projectDir) {
713
+ ensureMemoryDirs(projectDir);
714
+ const packets = loadApprovedPackets(projectDir);
715
+ return buildMemoryAccessReport(projectDir, readMemoryAccessEntries(projectDir, packets), packets);
716
+ }
717
+ function lifecycleFreshness(packet) {
718
+ const freshness = (packet.freshness ?? {});
719
+ const ttlRaw = Number(freshness.ttl_days ?? freshness.ttlDays);
720
+ const ttlDays = Number.isFinite(ttlRaw) && ttlRaw > 0 ? Math.floor(ttlRaw) : null;
721
+ const verifiedAt = typeof freshness.last_verified_at === "string"
722
+ ? freshness.last_verified_at
723
+ : (packet.updated_at || packet.created_at || null);
724
+ const verifiedTime = verifiedAt ? Date.parse(verifiedAt) : Number.NaN;
725
+ const ageDays = Number.isFinite(verifiedTime)
726
+ ? Math.max(0, Math.floor((Date.now() - verifiedTime) / 86_400_000))
727
+ : null;
728
+ return {
729
+ ttl_days: ttlDays,
730
+ last_verified_at: verifiedAt,
731
+ age_days: ageDays,
732
+ expired: ttlDays !== null && ageDays !== null && ageDays > ttlDays,
733
+ };
734
+ }
735
+ function lifecycleActionForPacket(packet, access, staleReasons) {
736
+ const quality = (packet.quality ?? {});
737
+ const reportsStale = Number(quality.reports_stale ?? 0);
738
+ const votesDown = Number(quality.votes_down ?? 0);
739
+ if (packet.status === "pending") {
740
+ return {
741
+ health: "cold",
742
+ recommended_action: "review_pending",
743
+ severity: "warn",
744
+ reason: "This packet is pending and has not crossed the repo review boundary.",
745
+ action: "Approve, reject, merge, or keep pending after checking evidence and sensitivity.",
746
+ };
747
+ }
748
+ if (isGeneratedChangeMemory(packet)) {
749
+ return {
750
+ health: "generated",
751
+ recommended_action: "archive_generated",
752
+ severity: "info",
753
+ reason: "This is generated branch/change context, useful for handoff but not durable repo lore.",
754
+ action: "Keep it as branch context while relevant; supersede it with a concise human-reviewed memory if the lesson is durable.",
755
+ };
756
+ }
757
+ if (staleReasons.length) {
758
+ return {
759
+ health: reportsStale || votesDown ? "disputed" : "stale",
760
+ recommended_action: reportsStale || votesDown ? "resolve_feedback" : "review_stale",
761
+ severity: "blocker",
762
+ reason: staleReasons[0],
763
+ action: "Verify, update, supersede, or deprecate this memory before trusting it in recall.",
764
+ };
765
+ }
766
+ if (!packet.paths.filter(meaningfulMemoryPath).length) {
767
+ return {
768
+ health: "ungrounded",
769
+ recommended_action: "add_grounding",
770
+ severity: "warn",
771
+ reason: "This approved memory has no concrete code path grounding.",
772
+ action: "Add relevant files, symbols, routes, tests, or docs so agents know when to recall it.",
773
+ };
774
+ }
775
+ if ((access?.uses_30d ?? 0) >= 3) {
776
+ return {
777
+ health: "hot",
778
+ recommended_action: "promote_hot",
779
+ severity: "ok",
780
+ reason: `Agents recalled this memory ${access?.uses_30d ?? 0} times in the last ${ACCESS_WINDOW_DAYS} days.`,
781
+ action: "Keep it verified and evidence-backed; treat it as high-value repo lore.",
782
+ };
783
+ }
784
+ if ((access?.total_uses ?? 0) === 0) {
785
+ return {
786
+ health: "cold",
787
+ recommended_action: "seed_usage",
788
+ severity: "info",
789
+ reason: "This approved memory has not been recalled by local agent tasks yet.",
790
+ action: "Keep it if it is durable, but improve title/tags/paths if future agents are not finding it.",
791
+ };
792
+ }
793
+ return {
794
+ health: "healthy",
795
+ recommended_action: "keep_verified",
796
+ severity: "ok",
797
+ reason: "This memory is approved, grounded, non-stale, and has recall history.",
798
+ action: "Keep it current when the linked code or workflow changes.",
799
+ };
800
+ }
801
+ function buildLifecycleRecommendations(items) {
802
+ const order = [
803
+ "resolve_feedback",
804
+ "review_stale",
805
+ "add_grounding",
806
+ "review_pending",
807
+ "promote_hot",
808
+ "seed_usage",
809
+ "archive_generated",
810
+ "keep_verified",
811
+ ];
812
+ const recommendations = [];
813
+ for (const action of order) {
814
+ const matches = items.filter((item) => item.recommended_action === action);
815
+ if (!matches.length)
816
+ continue;
817
+ const first = matches[0];
818
+ const count = matches.length;
819
+ const summary = count === 1
820
+ ? first.action
821
+ : `${first.action} (${count} packet${count === 1 ? "" : "s"})`;
822
+ recommendations.push({
823
+ kind: action,
824
+ severity: first.severity,
825
+ packet_id: first.packet_id,
826
+ title: first.title,
827
+ summary,
828
+ reason: first.reason,
829
+ action: first.action,
830
+ });
831
+ }
832
+ return recommendations.slice(0, 8);
833
+ }
834
+ function kageMemoryLifecycle(projectDir) {
835
+ ensureMemoryDirs(projectDir);
836
+ const packets = [...loadApprovedPackets(projectDir), ...loadPendingPackets(projectDir)]
837
+ .sort((a, b) => a.title.localeCompare(b.title));
838
+ const access = readMemoryAccessEntries(projectDir, packets.filter((packet) => packet.status === "approved"));
839
+ const items = packets.map((packet) => {
840
+ const accessEntry = access.get(packet.id);
841
+ const staleReasons = staleMemoryReasons(projectDir, packet);
842
+ const action = lifecycleActionForPacket(packet, accessEntry, staleReasons);
843
+ const quality = (packet.quality ?? {});
844
+ const item = {
845
+ packet_id: packet.id,
846
+ title: packet.title,
847
+ type: packet.type,
848
+ status: packet.status,
849
+ health: action.health,
850
+ recommended_action: action.recommended_action,
851
+ severity: action.severity,
852
+ paths: packet.paths,
853
+ tags: packet.tags,
854
+ source_refs: packet.source_refs.length,
855
+ uses_30d: accessEntry?.uses_30d ?? 0,
856
+ total_uses: accessEntry?.total_uses ?? 0,
857
+ last_accessed_at: accessEntry?.last_accessed_at ?? null,
858
+ feedback: {
859
+ votes_up: Number(quality.votes_up ?? 0),
860
+ votes_down: Number(quality.votes_down ?? 0),
861
+ reports_stale: Number(quality.reports_stale ?? 0),
862
+ score: packetFeedbackScore(packet),
863
+ },
864
+ freshness: lifecycleFreshness(packet),
865
+ stale_reasons: staleReasons,
866
+ reason: action.reason,
867
+ action: action.action,
868
+ };
869
+ return item;
870
+ });
871
+ return {
872
+ schema_version: 1,
873
+ project_dir: projectDir,
874
+ generated_at: nowIso(),
875
+ totals: {
876
+ approved: packets.filter((packet) => packet.status === "approved").length,
877
+ pending: packets.filter((packet) => packet.status === "pending").length,
878
+ deprecated: packets.filter((packet) => packet.status === "deprecated").length,
879
+ superseded: packets.filter((packet) => packet.status === "superseded").length,
880
+ healthy: items.filter((item) => item.health === "healthy").length,
881
+ hot: items.filter((item) => item.health === "hot").length,
882
+ cold: items.filter((item) => item.health === "cold").length,
883
+ stale: items.filter((item) => item.health === "stale" || item.health === "disputed").length,
884
+ disputed: items.filter((item) => item.health === "disputed").length,
885
+ ungrounded: items.filter((item) => item.health === "ungrounded").length,
886
+ generated: items.filter((item) => item.health === "generated").length,
887
+ with_evidence: packets.filter((packet) => packet.source_refs.length > 0).length,
888
+ with_paths: packets.filter((packet) => packet.paths.filter(meaningfulMemoryPath).length > 0).length,
889
+ },
890
+ items: items.sort((a, b) => {
891
+ const severityRank = { blocker: 0, warn: 1, info: 2, ok: 3 };
892
+ return severityRank[a.severity] - severityRank[b.severity]
893
+ || b.uses_30d - a.uses_30d
894
+ || a.title.localeCompare(b.title);
895
+ }),
896
+ recommendations: buildLifecycleRecommendations(items),
897
+ };
898
+ }
899
+ function recordRecallAccess(projectDir, results) {
900
+ if (!results.length)
901
+ return;
902
+ try {
903
+ ensureMemoryDirs(projectDir);
904
+ const packets = loadApprovedPackets(projectDir);
905
+ const byPacket = new Map(packets.map((packet) => [packet.id, packet]));
906
+ const entries = readMemoryAccessEntries(projectDir, packets);
907
+ const at = nowIso();
908
+ results.slice(0, 10).forEach((result, index) => {
909
+ const packet = byPacket.get(result.packet.id);
910
+ if (!packet)
911
+ return;
912
+ const current = normalizeAccessEntry(entries.get(packet.id), packet) ?? normalizeAccessEntry({ packet_id: packet.id }, packet);
913
+ if (!current)
914
+ return;
915
+ const rank = index + 1;
916
+ current.total_uses += 1;
917
+ current.last_accessed_at = at;
918
+ current.last_rank = rank;
919
+ current.best_rank = current.best_rank ? Math.min(current.best_rank, rank) : rank;
920
+ current.recent.push({ at, rank });
921
+ current.recent = normalizeAccessRecent(current.recent);
922
+ current.uses_30d = current.recent.filter((item) => Date.parse(item.at) >= accessWindowCutoff()).length;
923
+ entries.set(packet.id, current);
924
+ });
925
+ writeJson(memoryAccessPath(projectDir), buildMemoryAccessReport(projectDir, entries, packets));
926
+ }
927
+ catch {
928
+ // Recall should never fail because local access telemetry could not be updated.
929
+ }
930
+ }
476
931
  function isGeneratedChangeMemory(packet) {
477
932
  return packet.type === "workflow"
478
933
  && packet.tags.includes("change-memory")
@@ -492,11 +947,36 @@ function jaccard(a, b) {
492
947
  return intersection / (a.size + b.size - intersection);
493
948
  }
494
949
  function duplicateCandidates(projectDir, packet, threshold = 0.58) {
495
- const current = tokenSet(packetText(packet));
496
- return [...loadApprovedPackets(projectDir), ...loadPendingPackets(projectDir)]
950
+ return duplicateCandidatesWithContext(packet, memoryQualityContext(projectDir), threshold);
951
+ }
952
+ function memoryQualityContext(projectDir) {
953
+ const packets = [...loadApprovedPackets(projectDir), ...loadPendingPackets(projectDir)];
954
+ return {
955
+ packets,
956
+ tokenSets: new Map(packets.map((packet) => [packet.id, tokenSet(packetText(packet))])),
957
+ };
958
+ }
959
+ function duplicateCandidatesWithContext(packet, context, threshold = 0.58) {
960
+ const current = context.tokenSets.get(packet.id) ?? tokenSet(packetText(packet));
961
+ const packetTags = new Set(packet.tags);
962
+ const packetPaths = new Set(packet.paths);
963
+ const candidates = context.packets.length <= 100
964
+ ? context.packets
965
+ : context.packets
966
+ .map((candidate) => {
967
+ const sharedTags = candidate.tags.filter((tag) => packetTags.has(tag)).length;
968
+ const sharedPaths = candidate.paths.filter((path) => packetPaths.has(path)).length;
969
+ const typeMatch = candidate.type === packet.type ? 1 : 0;
970
+ return { candidate, preScore: sharedPaths * 3 + sharedTags * 2 + typeMatch };
971
+ })
972
+ .filter((entry) => entry.preScore > 0)
973
+ .sort((a, b) => b.preScore - a.preScore || a.candidate.title.localeCompare(b.candidate.title))
974
+ .slice(0, 250)
975
+ .map((entry) => entry.candidate);
976
+ return candidates
497
977
  .filter((candidate) => candidate.id !== packet.id)
498
978
  .filter((candidate) => !(isGeneratedChangeMemory(packet) && isGeneratedChangeMemory(candidate)))
499
- .map((candidate) => ({ packet: candidate, score: jaccard(current, tokenSet(packetText(candidate))) }))
979
+ .map((candidate) => ({ packet: candidate, score: jaccard(current, context.tokenSets.get(candidate.id) ?? tokenSet(packetText(candidate))) }))
500
980
  .filter((entry) => entry.score >= threshold)
501
981
  .sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title))
502
982
  .slice(0, 5)
@@ -508,16 +988,98 @@ function duplicateCandidates(projectDir, packet, threshold = 0.58) {
508
988
  }));
509
989
  }
510
990
  function packetFeedbackScore(packet) {
511
- const quality = packet.quality;
991
+ const quality = (packet.quality ?? {});
512
992
  return Number(quality.votes_up ?? 0) * 2 - Number(quality.votes_down ?? 0) * 3 - Number(quality.reports_stale ?? 0) * 4;
513
993
  }
994
+ function recallQualityScore(packet) {
995
+ const stored = Number((packet.quality ?? {}).score);
996
+ if (Number.isFinite(stored))
997
+ return Math.max(0, Math.min(10, stored / 10));
998
+ let score = 45;
999
+ if (["runbook", "bug_fix", "decision", "rationale", "convention", "workflow", "gotcha", "policy", "issue_context", "code_explanation", "negative_result", "constraint"].includes(packet.type))
1000
+ score += 14;
1001
+ if (packet.source_refs.length)
1002
+ score += 12;
1003
+ if (packet.paths.length)
1004
+ score += 10;
1005
+ if (packet.tags.length)
1006
+ score += 5;
1007
+ const bodyTokens = tokenize(packet.body).length;
1008
+ if (bodyTokens >= 12 && bodyTokens <= 180)
1009
+ score += 10;
1010
+ if (/(verified by|evidence:|because|root cause|rationale|decision|run|command|avoid|prefer)/i.test(packet.body))
1011
+ score += 8;
1012
+ if (packet.body.length < 60)
1013
+ score -= 18;
1014
+ if (!packet.paths.length && !["repo_map", "reference", "policy"].includes(packet.type))
1015
+ score -= 10;
1016
+ if (!packet.source_refs.length)
1017
+ score -= 12;
1018
+ return Math.max(0, Math.min(10, score / 10));
1019
+ }
514
1020
  function meaningfulMemoryPath(path) {
515
1021
  return path !== "root" && path !== "." && !isNoisePath(path);
516
1022
  }
517
- function staleMemoryReasons(projectDir, packet) {
1023
+ function fingerprintableMemoryPath(path) {
1024
+ const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "");
1025
+ return meaningfulMemoryPath(normalized) && !normalized.startsWith(".agent_memory/");
1026
+ }
1027
+ function memoryPathFingerprint(projectDir, path, cache) {
1028
+ const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "");
1029
+ if (!fingerprintableMemoryPath(normalized))
1030
+ return null;
1031
+ const cacheKey = `${projectDir}\0${normalized}`;
1032
+ if (cache?.has(cacheKey))
1033
+ return cache.get(cacheKey) ?? null;
1034
+ const absolutePath = (0, node_path_1.join)(projectDir, normalized);
1035
+ try {
1036
+ const stats = (0, node_fs_1.statSync)(absolutePath);
1037
+ if (!stats.isFile()) {
1038
+ cache?.set(cacheKey, null);
1039
+ return null;
1040
+ }
1041
+ const fingerprint = {
1042
+ path: normalized,
1043
+ sha256: sha256Hex((0, node_fs_1.readFileSync)(absolutePath)),
1044
+ size: stats.size,
1045
+ };
1046
+ cache?.set(cacheKey, fingerprint);
1047
+ return fingerprint;
1048
+ }
1049
+ catch {
1050
+ cache?.set(cacheKey, null);
1051
+ return null;
1052
+ }
1053
+ }
1054
+ function memoryPathFingerprints(projectDir, paths) {
1055
+ const fingerprints = [];
1056
+ for (const path of unique(paths).filter(fingerprintableMemoryPath)) {
1057
+ const fingerprint = memoryPathFingerprint(projectDir, path);
1058
+ if (fingerprint)
1059
+ fingerprints.push(fingerprint);
1060
+ }
1061
+ return fingerprints;
1062
+ }
1063
+ function packetStoredPathFingerprints(packet) {
1064
+ const raw = (packet.freshness ?? {}).path_fingerprints;
1065
+ if (!Array.isArray(raw))
1066
+ return [];
1067
+ return raw.flatMap((item) => {
1068
+ if (!item || typeof item !== "object")
1069
+ return [];
1070
+ const record = item;
1071
+ const path = typeof record.path === "string" ? record.path : "";
1072
+ const sha256 = typeof record.sha256 === "string" ? record.sha256 : "";
1073
+ const size = Number(record.size ?? 0);
1074
+ if (!path || !sha256 || !Number.isFinite(size) || !fingerprintableMemoryPath(path))
1075
+ return [];
1076
+ return [{ path, sha256, size }];
1077
+ });
1078
+ }
1079
+ function staleMemoryReasons(projectDir, packet, fingerprintCache) {
518
1080
  const reasons = [];
519
- const quality = packet.quality;
520
- const freshness = packet.freshness;
1081
+ const quality = (packet.quality ?? {});
1082
+ const freshness = (packet.freshness ?? {});
521
1083
  if (packet.status === "deprecated" || packet.status === "superseded") {
522
1084
  reasons.push(`packet status is ${packet.status}`);
523
1085
  }
@@ -539,10 +1101,103 @@ function staleMemoryReasons(projectDir, packet) {
539
1101
  else if (missingPaths.length > 0) {
540
1102
  reasons.push(`some referenced paths are missing: ${missingPaths.slice(0, 4).join(", ")}`);
541
1103
  }
1104
+ if (freshness.path_fingerprint_policy === "source_hash_staleness") {
1105
+ const storedFingerprints = packetStoredPathFingerprints(packet);
1106
+ const changedPaths = storedFingerprints
1107
+ .filter((fingerprint) => (0, node_fs_1.existsSync)((0, node_path_1.join)(projectDir, fingerprint.path)))
1108
+ .filter((fingerprint) => {
1109
+ const current = memoryPathFingerprint(projectDir, fingerprint.path, fingerprintCache);
1110
+ return current !== null && current.sha256 !== fingerprint.sha256;
1111
+ })
1112
+ .map((fingerprint) => fingerprint.path);
1113
+ if (changedPaths.length) {
1114
+ reasons.push(`linked path changed since memory was verified: ${changedPaths.slice(0, 4).join(", ")}`);
1115
+ }
1116
+ }
542
1117
  return unique(reasons);
543
1118
  }
544
- function classifyPacket(projectDir, packet) {
545
- const quality = evaluateMemoryQuality(projectDir, packet);
1119
+ function changedPathsFromStaleReasons(reasons) {
1120
+ return unique(reasons.flatMap((reason) => {
1121
+ const match = reason.match(/^linked path changed since memory was verified: (.+)$/);
1122
+ if (!match)
1123
+ return [];
1124
+ return match[1].split(",").map((path) => path.trim()).filter(Boolean);
1125
+ }));
1126
+ }
1127
+ function observationTouchedPaths(observations) {
1128
+ return unique(observations
1129
+ .filter((event) => event.type === "file_change" && typeof event.path === "string" && event.path.trim().length > 0)
1130
+ .map((event) => event.path.replace(/\\/g, "/").replace(/^\/+/, ""))
1131
+ .filter(meaningfulMemoryPath)).sort();
1132
+ }
1133
+ function packetPathSet(packet) {
1134
+ return new Set(packet.paths.map((path) => path.replace(/\\/g, "/").replace(/^\/+/, "")).filter(meaningfulMemoryPath));
1135
+ }
1136
+ function reconciliationInstruction(items) {
1137
+ if (!items.length)
1138
+ return "No agent memory reconciliation is required.";
1139
+ const lines = items.slice(0, 5).map((item) => `- ${item.packet_id}: ${item.title} (${item.changed_paths.join(", ") || item.paths.join(", ")}) -> ${item.next_action}`);
1140
+ return [
1141
+ "Memory reconciliation required before final response.",
1142
+ "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.",
1143
+ "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.",
1144
+ ...lines,
1145
+ ].join("\n");
1146
+ }
1147
+ function kageMemoryReconciliation(projectDir, options = {}) {
1148
+ ensureMemoryDirs(projectDir);
1149
+ const observations = loadObservations(projectDir, options.sessionId);
1150
+ const touchedPaths = observationTouchedPaths(observations);
1151
+ const sessionIdsByPath = new Map();
1152
+ for (const event of observations) {
1153
+ if (event.type !== "file_change" || !event.path)
1154
+ continue;
1155
+ const path = event.path.replace(/\\/g, "/").replace(/^\/+/, "");
1156
+ if (!meaningfulMemoryPath(path))
1157
+ continue;
1158
+ const sessions = sessionIdsByPath.get(path) ?? new Set();
1159
+ sessions.add(event.session_id);
1160
+ sessionIdsByPath.set(path, sessions);
1161
+ }
1162
+ const fingerprintCache = new Map();
1163
+ const items = loadApprovedPackets(projectDir)
1164
+ .flatMap((packet) => {
1165
+ if ((packet.freshness ?? {}).path_fingerprint_policy !== "source_hash_staleness")
1166
+ return [];
1167
+ const reasons = staleMemoryReasons(projectDir, packet, fingerprintCache);
1168
+ const changedPaths = changedPathsFromStaleReasons(reasons);
1169
+ if (!changedPaths.length)
1170
+ return [];
1171
+ const paths = packetPathSet(packet);
1172
+ const observedSessionIds = unique(changedPaths.flatMap((path) => [...(sessionIdsByPath.get(path) ?? new Set())])).sort();
1173
+ const relevantTouchedPaths = touchedPaths.filter((path) => paths.has(path));
1174
+ return [{
1175
+ packet_id: packet.id,
1176
+ title: packet.title,
1177
+ type: packet.type,
1178
+ status: packet.status,
1179
+ paths: packet.paths,
1180
+ changed_paths: unique([...changedPaths, ...relevantTouchedPaths]).sort(),
1181
+ observed_session_ids: observedSessionIds,
1182
+ stale_reasons: reasons,
1183
+ suggested_action: "agent_update_or_supersede",
1184
+ 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.`,
1185
+ }];
1186
+ })
1187
+ .sort((a, b) => a.title.localeCompare(b.title))
1188
+ .slice(0, Math.max(1, options.limit ?? 25));
1189
+ return {
1190
+ ok: items.length === 0,
1191
+ project_dir: projectDir,
1192
+ generated_at: nowIso(),
1193
+ session_id: options.sessionId,
1194
+ touched_paths: touchedPaths,
1195
+ unresolved_count: items.length,
1196
+ items,
1197
+ agent_instruction: reconciliationInstruction(items),
1198
+ };
1199
+ }
1200
+ function classifyPacket(projectDir, packet, context, quality = evaluateMemoryQuality(projectDir, packet, context)) {
546
1201
  const score = Number(quality.score);
547
1202
  const duplicates = quality.duplicate_candidates;
548
1203
  if (staleMemoryReasons(projectDir, packet).length)
@@ -566,7 +1221,7 @@ function suggestedAction(classification, status) {
566
1221
  return "approve";
567
1222
  return "keep";
568
1223
  }
569
- function evaluateMemoryQuality(projectDir, packet) {
1224
+ function evaluateMemoryQuality(projectDir, packet, context) {
570
1225
  const reasons = [];
571
1226
  const risks = [];
572
1227
  let score = 45;
@@ -610,7 +1265,7 @@ function evaluateMemoryQuality(projectDir, packet) {
610
1265
  score -= 12;
611
1266
  risks.push("missing source evidence");
612
1267
  }
613
- const duplicates = duplicateCandidates(projectDir, packet);
1268
+ const duplicates = context ? duplicateCandidatesWithContext(packet, context) : duplicateCandidates(projectDir, packet);
614
1269
  if (duplicates.length) {
615
1270
  score -= 18;
616
1271
  risks.push("possible duplicate memory");
@@ -841,9 +1496,12 @@ function ensureMemoryDirs(projectDir) {
841
1496
  ensureDir(graphDir(projectDir));
842
1497
  ensureDir(codeGraphDir(projectDir));
843
1498
  ensureDir(branchesDir(projectDir));
1499
+ ensureDir(auditDir(projectDir));
844
1500
  ensureDir(reviewDir(projectDir));
1501
+ ensureDir(reportsDir(projectDir));
845
1502
  ensureDir(publicBundleDir(projectDir));
846
1503
  ensureDir(observationsDir(projectDir));
1504
+ ensureDir(slotsDir(projectDir));
847
1505
  ensureDir(daemonDir(projectDir));
848
1506
  ensureDir(globalCdnDir(projectDir));
849
1507
  ensureDir(marketplaceDir(projectDir));
@@ -896,6 +1554,299 @@ function writePacket(projectDir, packet, statusDir) {
896
1554
  writeJson(path, packet);
897
1555
  return path;
898
1556
  }
1557
+ function memoryAuditPath(projectDir) {
1558
+ return (0, node_path_1.join)(auditDir(projectDir), "events.jsonl");
1559
+ }
1560
+ function auditActor() {
1561
+ return process.env.KAGE_ACTOR || process.env.USER || process.env.LOGNAME || "repo-local-agent";
1562
+ }
1563
+ function recordMemoryAudit(projectDir, operation, packets, details = {}) {
1564
+ ensureDir(auditDir(projectDir));
1565
+ const timestamp = nowIso();
1566
+ const packetIds = unique(packets.map((packet) => packet.id).filter(Boolean));
1567
+ const entry = {
1568
+ schema_version: 1,
1569
+ id: `audit:${(0, node_crypto_1.createHash)("sha256").update(`${timestamp}:${operation}:${packetIds.join(",")}`).digest("hex").slice(0, 16)}`,
1570
+ timestamp,
1571
+ operation,
1572
+ packet_ids: packetIds,
1573
+ packet_titles: unique(packets.map((packet) => packet.title).filter(Boolean)),
1574
+ actor: auditActor(),
1575
+ branch: gitBranch(projectDir),
1576
+ head: gitHead(projectDir),
1577
+ details,
1578
+ };
1579
+ (0, node_fs_1.writeFileSync)(memoryAuditPath(projectDir), `${JSON.stringify(entry)}\n`, { encoding: "utf8", flag: "a" });
1580
+ return entry;
1581
+ }
1582
+ function loadMemoryAuditEntries(projectDir) {
1583
+ const path = memoryAuditPath(projectDir);
1584
+ if (!(0, node_fs_1.existsSync)(path))
1585
+ return [];
1586
+ return (0, node_fs_1.readFileSync)(path, "utf8")
1587
+ .split(/\r?\n/)
1588
+ .map((line) => line.trim())
1589
+ .filter(Boolean)
1590
+ .map((line) => JSON.parse(line))
1591
+ .filter((entry) => entry.schema_version === 1 && Boolean(entry.operation));
1592
+ }
1593
+ function kageMemoryAudit(projectDir, limit = 100) {
1594
+ ensureMemoryDirs(projectDir);
1595
+ const entries = loadMemoryAuditEntries(projectDir)
1596
+ .sort((a, b) => b.timestamp.localeCompare(a.timestamp) || a.id.localeCompare(b.id));
1597
+ const boundedLimit = Math.max(1, Math.min(500, Math.floor(Number(limit) || 100)));
1598
+ const totals = {
1599
+ total: entries.length,
1600
+ capture: 0,
1601
+ feedback: 0,
1602
+ approve: 0,
1603
+ reject: 0,
1604
+ supersede: 0,
1605
+ deprecate: 0,
1606
+ delete: 0,
1607
+ };
1608
+ for (const entry of entries) {
1609
+ totals[entry.operation] = Number(totals[entry.operation] || 0) + 1;
1610
+ }
1611
+ return {
1612
+ schema_version: 1,
1613
+ project_dir: projectDir,
1614
+ generated_at: nowIso(),
1615
+ path: memoryAuditPath(projectDir),
1616
+ totals,
1617
+ entries: entries.slice(0, boundedLimit),
1618
+ recommendations: unique([
1619
+ 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.",
1620
+ ...(totals.supersede ? ["Check supersede audit entries with kage lineage so agents use current memory."] : []),
1621
+ ...(totals.reject ? ["Rejected memory is preserved in the audit trail; capture a better packet if the lesson is still useful."] : []),
1622
+ ]),
1623
+ };
1624
+ }
1625
+ function handoffLifecycleSeverity(severity) {
1626
+ if (severity === "blocker")
1627
+ return "blocker";
1628
+ if (severity === "warn")
1629
+ return "warning";
1630
+ if (severity === "info")
1631
+ return "info";
1632
+ return "ok";
1633
+ }
1634
+ function handoffTimelineSeverity(kind) {
1635
+ if (kind === "pending" || kind === "deprecated")
1636
+ return "warning";
1637
+ return "info";
1638
+ }
1639
+ function handoffAuditSeverity(operation) {
1640
+ if (operation === "reject" || operation === "deprecate" || operation === "delete")
1641
+ return "warning";
1642
+ return "info";
1643
+ }
1644
+ function handoffAuditAction(operation) {
1645
+ if (operation === "capture")
1646
+ return "Review whether this new memory is grounded, reusable, and useful for the next agent.";
1647
+ if (operation === "feedback")
1648
+ return "Check feedback before trusting or promoting this packet.";
1649
+ if (operation === "approve")
1650
+ return "Use this approved packet as shared repo memory when it matches the task.";
1651
+ if (operation === "reject")
1652
+ return "Keep the rejection as audit history; capture a better packet if the lesson is still useful.";
1653
+ if (operation === "supersede")
1654
+ return "Use the replacement packet and avoid relying on retired memory.";
1655
+ if (operation === "deprecate")
1656
+ return "Check whether a newer packet explains why this memory was retired.";
1657
+ return "Confirm deleted memory was intentionally removed and not needed for handoff.";
1658
+ }
1659
+ function memoryHandoffDedupeKey(item) {
1660
+ return [
1661
+ item.kind,
1662
+ item.severity,
1663
+ item.packet_ids.join(","),
1664
+ item.title,
1665
+ item.summary,
1666
+ ].join(":");
1667
+ }
1668
+ function handoffPrimaryAction(items, openItems) {
1669
+ const urgent = items.find((item) => item.severity === "blocker" || item.severity === "warning");
1670
+ if (urgent) {
1671
+ return {
1672
+ label: "Resolve handoff",
1673
+ summary: urgent.summary,
1674
+ action: urgent.action,
1675
+ severity: urgent.severity,
1676
+ target: urgent.kind === "lifecycle" || urgent.kind === "session" ? "memory" : "review",
1677
+ packet_ids: urgent.packet_ids,
1678
+ };
1679
+ }
1680
+ const recent = items.find((item) => item.kind === "audit" || item.kind === "timeline");
1681
+ if (recent) {
1682
+ return {
1683
+ label: "Review recent memory",
1684
+ summary: recent.summary,
1685
+ action: recent.action,
1686
+ severity: recent.severity,
1687
+ target: "review",
1688
+ packet_ids: recent.packet_ids,
1689
+ };
1690
+ }
1691
+ return {
1692
+ label: openItems ? "Resolve handoff" : "Ready for handoff",
1693
+ summary: openItems ? `${openItems} memory handoff item${openItems === 1 ? "" : "s"} need review.` : "No memory handoff blockers are open.",
1694
+ action: openItems ? "Open the review queue before another agent relies on this memory." : "Hand work to another teammate or agent with current repo memory.",
1695
+ severity: openItems ? "warning" : "ok",
1696
+ target: "review",
1697
+ packet_ids: [],
1698
+ };
1699
+ }
1700
+ function kageMemoryHandoff(projectDir) {
1701
+ ensureMemoryDirs(projectDir);
1702
+ const inbox = memoryInbox(projectDir);
1703
+ const lifecycle = kageMemoryLifecycle(projectDir);
1704
+ const audit = kageMemoryAudit(projectDir, 20);
1705
+ const timeline = kageMemoryTimeline(projectDir, 14);
1706
+ const lineage = kageMemoryLineage(projectDir);
1707
+ const sessions = kageSessionCaptureReport(projectDir);
1708
+ const items = [];
1709
+ for (const item of inbox.items.slice(0, 8)) {
1710
+ items.push({
1711
+ kind: "inbox",
1712
+ severity: item.severity,
1713
+ title: item.title || item.kind.replace(/_/g, " "),
1714
+ summary: item.summary,
1715
+ action: item.action,
1716
+ packet_ids: item.packet_id ? [item.packet_id] : [],
1717
+ paths: item.paths ?? [],
1718
+ });
1719
+ }
1720
+ for (const item of lifecycle.recommendations.slice(0, 8)) {
1721
+ const action = item.kind === "add_grounding"
1722
+ ? "Add repo paths, symbols, routes, tests, or docs this memory explains before handoff."
1723
+ : item.action;
1724
+ items.push({
1725
+ kind: "lifecycle",
1726
+ severity: handoffLifecycleSeverity(item.severity),
1727
+ title: item.title || item.kind.replace(/_/g, " "),
1728
+ summary: item.summary,
1729
+ action,
1730
+ packet_ids: item.packet_id ? [item.packet_id] : [],
1731
+ paths: [],
1732
+ });
1733
+ }
1734
+ for (const entry of audit.entries.slice(0, 8)) {
1735
+ items.push({
1736
+ kind: "audit",
1737
+ severity: handoffAuditSeverity(entry.operation),
1738
+ title: entry.packet_titles[0] || entry.operation,
1739
+ summary: `Memory mutation: ${entry.operation}`,
1740
+ action: handoffAuditAction(entry.operation),
1741
+ packet_ids: entry.packet_ids,
1742
+ paths: [],
1743
+ date: entry.timestamp,
1744
+ });
1745
+ }
1746
+ for (const entry of timeline.entries.slice(0, 8)) {
1747
+ items.push({
1748
+ kind: "timeline",
1749
+ severity: handoffTimelineSeverity(entry.kind),
1750
+ title: entry.title,
1751
+ summary: `${entry.kind}: ${entry.summary}`,
1752
+ action: entry.action,
1753
+ packet_ids: [entry.packet_id],
1754
+ paths: entry.paths,
1755
+ date: entry.date,
1756
+ });
1757
+ }
1758
+ for (const session of sessions.sessions.filter((item) => item.durable_observations > 0).slice(0, 8)) {
1759
+ items.push({
1760
+ kind: "session",
1761
+ severity: "warning",
1762
+ title: session.session_id,
1763
+ summary: `${session.durable_observations} distillable observation${session.durable_observations === 1 ? "" : "s"} from ${session.agents.join(", ") || "agent"} session.`,
1764
+ action: session.next_action,
1765
+ packet_ids: [],
1766
+ paths: session.paths,
1767
+ date: session.last_at,
1768
+ });
1769
+ }
1770
+ for (const orphan of lineage.orphans.slice(0, 8)) {
1771
+ items.push({
1772
+ kind: "lineage",
1773
+ severity: "warning",
1774
+ title: orphan.title,
1775
+ summary: orphan.reason,
1776
+ action: orphan.action,
1777
+ packet_ids: [orphan.packet_id],
1778
+ paths: [],
1779
+ date: orphan.updated_at,
1780
+ });
1781
+ }
1782
+ for (const chain of lineage.chains.slice(0, 6)) {
1783
+ items.push({
1784
+ kind: "lineage",
1785
+ severity: "info",
1786
+ title: chain.current_title,
1787
+ summary: `Current packet supersedes ${chain.superseded_packet_ids.length} retired packet${chain.superseded_packet_ids.length === 1 ? "" : "s"}.`,
1788
+ action: chain.action,
1789
+ packet_ids: [chain.current_packet_id, ...chain.superseded_packet_ids],
1790
+ paths: chain.paths,
1791
+ date: chain.updated_at,
1792
+ });
1793
+ }
1794
+ const seen = new Set();
1795
+ const severityRank = { blocker: 0, warning: 1, info: 2, ok: 3 };
1796
+ const deduped = items
1797
+ .filter((item) => {
1798
+ const key = memoryHandoffDedupeKey(item);
1799
+ if (seen.has(key))
1800
+ return false;
1801
+ seen.add(key);
1802
+ return true;
1803
+ })
1804
+ .sort((a, b) => severityRank[a.severity] - severityRank[b.severity]
1805
+ || (b.date ?? "").localeCompare(a.date ?? "")
1806
+ || a.title.localeCompare(b.title))
1807
+ .slice(0, 30);
1808
+ const blockers = deduped.filter((item) => item.severity === "blocker").length;
1809
+ const warnings = deduped.filter((item) => item.severity === "warning").length;
1810
+ const info = deduped.filter((item) => item.severity === "info").length;
1811
+ const openItems = blockers + warnings;
1812
+ const recentChanges = timeline.totals.total;
1813
+ const recentMutations = audit.entries.length;
1814
+ const distillableSessions = sessions.totals.sessions_with_candidates;
1815
+ const durableObservations = sessions.totals.durable_observations;
1816
+ const ok = openItems === 0 && inbox.ok && lineage.totals.orphans === 0 && distillableSessions === 0;
1817
+ const recommendations = unique([
1818
+ ...(openItems ? ["Resolve handoff blockers and warnings before another agent relies on this memory."] : []),
1819
+ ...(distillableSessions ? ["Distill session observations before handoff so live agent learnings become reviewable memory packets."] : []),
1820
+ ...(recentMutations ? ["Review recent memory mutations so teammates know what changed."] : []),
1821
+ ...(recentChanges ? ["Scan the recent memory timeline before switching agents or branches."] : []),
1822
+ ...(lineage.totals.orphans ? ["Resolve superseded memories without replacement links."] : []),
1823
+ ...(!deduped.length ? ["No memory handoff work loaded; capture durable decisions, bugs, runbooks, and gotchas as work happens."] : []),
1824
+ ]);
1825
+ return {
1826
+ schema_version: 1,
1827
+ project_dir: projectDir,
1828
+ generated_at: nowIso(),
1829
+ ok,
1830
+ totals: {
1831
+ total: deduped.length,
1832
+ open_items: openItems,
1833
+ blockers,
1834
+ warnings,
1835
+ info,
1836
+ recent_changes: recentChanges,
1837
+ recent_mutations: recentMutations,
1838
+ supersession_orphans: lineage.totals.orphans,
1839
+ distillable_sessions: distillableSessions,
1840
+ durable_observations: durableObservations,
1841
+ },
1842
+ summary: openItems
1843
+ ? `${openItems} memory handoff item${openItems === 1 ? "" : "s"} need review before reuse.`
1844
+ : "Memory handoff has no blocking review work.",
1845
+ primary_action: handoffPrimaryAction(deduped, openItems),
1846
+ items: deduped,
1847
+ recommendations,
1848
+ };
1849
+ }
899
1850
  function readGit(projectDir, args) {
900
1851
  try {
901
1852
  return (0, node_child_process_1.execFileSync)("git", args, {
@@ -1057,9 +2008,10 @@ function createRepoOverviewPacket(projectDir) {
1057
2008
  let title = `${repoDisplayName(projectDir)} repo overview`;
1058
2009
  const tags = ["repo", "overview"];
1059
2010
  const bodyParts = [];
1060
- const paths = ["root"];
2011
+ const paths = [];
1061
2012
  const stack = [];
1062
2013
  if ((0, node_fs_1.existsSync)(packagePath)) {
2014
+ paths.push("package.json");
1063
2015
  const pkg = readJson(packagePath);
1064
2016
  title = `${String(pkg.name ?? repoDisplayName(projectDir))} repo overview`;
1065
2017
  const scripts = pkg.scripts && typeof pkg.scripts === "object" ? Object.keys(pkg.scripts) : [];
@@ -1083,6 +2035,7 @@ function createRepoOverviewPacket(projectDir) {
1083
2035
  }
1084
2036
  const readmeText = (0, node_fs_1.existsSync)(readmePath) ? safeReadText(readmePath) : null;
1085
2037
  if (readmeText) {
2038
+ paths.push("README.md");
1086
2039
  const readme = readmeText.slice(0, 1000);
1087
2040
  bodyParts.push(`README excerpt:\n${readme}`);
1088
2041
  }
@@ -4193,6 +5146,7 @@ function buildPacketIndexes(projectDir) {
4193
5146
  writeJson(written[1], byPath);
4194
5147
  writeJson(written[2], byTag);
4195
5148
  writeJson(written[3], byType);
5149
+ written.push(writeSparseVectorIndex(projectDir, packets));
4196
5150
  return written;
4197
5151
  }
4198
5152
  function readCurrentCodeGraph(projectDir, expectedInputHash) {
@@ -4293,6 +5247,7 @@ function currentOrBuildGraphs(projectDir) {
4293
5247
  (0, node_path_1.join)(indexesDir(projectDir), "by-path.json"),
4294
5248
  (0, node_path_1.join)(indexesDir(projectDir), "by-tag.json"),
4295
5249
  (0, node_path_1.join)(indexesDir(projectDir), "by-type.json"),
5250
+ (0, node_path_1.join)(indexesDir(projectDir), "vector-local.json"),
4296
5251
  (0, node_path_1.join)(indexesDir(projectDir), "structural.json"),
4297
5252
  (0, node_path_1.join)(indexesDir(projectDir), "graph.json"),
4298
5253
  (0, node_path_1.join)(indexesDir(projectDir), "code-graph.json"),
@@ -4380,6 +5335,8 @@ function staleSuggestedAction(reasons) {
4380
5335
  return "mark_stale";
4381
5336
  if (reasons.some((reason) => reason.includes("missing")))
4382
5337
  return "update";
5338
+ if (reasons.some((reason) => reason.includes("linked path changed")))
5339
+ return "update";
4383
5340
  if (reasons.some((reason) => reason.includes("reported")))
4384
5341
  return "supersede";
4385
5342
  return "verify";
@@ -4398,10 +5355,11 @@ function staleFinding(packet, reasons) {
4398
5355
  function refreshPacketStaleness(projectDir) {
4399
5356
  const findings = [];
4400
5357
  let updated = 0;
5358
+ const fingerprintCache = new Map();
4401
5359
  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;
5360
+ const reasons = staleMemoryReasons(projectDir, entry.packet, fingerprintCache);
5361
+ const oldQuality = (entry.packet.quality ?? {});
5362
+ const oldFreshness = (entry.packet.freshness ?? {});
4405
5363
  let nextQuality;
4406
5364
  if (reasons.length) {
4407
5365
  const finding = staleFinding(entry.packet, reasons);
@@ -4447,6 +5405,9 @@ function refreshProject(projectDir, options = {}) {
4447
5405
  }
4448
5406
  const validation = validateProject(projectDir);
4449
5407
  const metrics = kageMetricsShallow(projectDir, { codeGraph, knowledgeGraph, validation });
5408
+ ensureDir(reportsDir(projectDir));
5409
+ writeJson((0, node_path_1.join)(reportsDir(projectDir), "context-slots.json"), kageContextSlots(projectDir));
5410
+ writeJson((0, node_path_1.join)(reportsDir(projectDir), "handoff.json"), kageMemoryHandoff(projectDir));
4450
5411
  const nextActions = [];
4451
5412
  if (stale.findings.length)
4452
5413
  nextActions.push("Update, verify, or supersede stale repo memories before relying on them.");
@@ -4498,7 +5459,7 @@ function gcProject(projectDir, options = {}) {
4498
5459
  skipped.push({ id: packet.id, title: packet.title, reason: "healthy" });
4499
5460
  continue;
4500
5461
  }
4501
- const quality = packet.quality;
5462
+ const quality = (packet.quality ?? {});
4502
5463
  const hasHelpfulVotes = Number(quality?.votes_up ?? 0) > 0;
4503
5464
  if (hasHelpfulVotes && !options.force) {
4504
5465
  skipped.push({ id: packet.id, title: packet.title, reason: `stale but has helpful votes (use --force to override)` });
@@ -4520,6 +5481,20 @@ function gcProject(projectDir, options = {}) {
4520
5481
  }
4521
5482
  }
4522
5483
  if (!options.dryRun && (deprecated.length || deleted.length)) {
5484
+ if (deprecated.length) {
5485
+ recordMemoryAudit(projectDir, "deprecate", deprecated.map((packet) => ({ id: packet.id, title: packet.title })), {
5486
+ reason: "gc",
5487
+ count: deprecated.length,
5488
+ deprecated,
5489
+ });
5490
+ }
5491
+ if (deleted.length) {
5492
+ recordMemoryAudit(projectDir, "delete", deleted.map((packet) => ({ id: packet.id, title: packet.title })), {
5493
+ reason: "gc_force",
5494
+ count: deleted.length,
5495
+ deleted,
5496
+ });
5497
+ }
4523
5498
  const rebuilt = buildGraphIndexes(projectDir);
4524
5499
  writeJson((0, node_path_1.join)(memoryRoot(projectDir), "metrics.json"), kageMetricsShallow(projectDir, rebuilt));
4525
5500
  }
@@ -4582,12 +5557,31 @@ function installAgentPolicy(projectDir) {
4582
5557
  }
4583
5558
  return { path: agentsPath, created, updated };
4584
5559
  }
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));
5560
+ const TOKEN_RE = /[\p{L}\p{N}._/-]+/gu;
5561
+ const CJK_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
5562
+ function cjkNgrams(term) {
5563
+ const chars = Array.from(term).filter((char) => CJK_RE.test(char));
5564
+ if (chars.length === 0)
5565
+ return [];
5566
+ const grams = new Set();
5567
+ if (chars.length === 1)
5568
+ grams.add(chars[0]);
5569
+ for (let i = 0; i < chars.length - 1; i++)
5570
+ grams.add(`${chars[i]}${chars[i + 1]}`);
5571
+ return [...grams];
5572
+ }
5573
+ function tokenize(text) {
5574
+ const tokens = [];
5575
+ for (const match of text.toLowerCase().matchAll(TOKEN_RE)) {
5576
+ const term = match[0]?.trim();
5577
+ if (!term || STOPWORDS.has(term))
5578
+ continue;
5579
+ if (term.length > 1)
5580
+ tokens.push(term);
5581
+ if (CJK_RE.test(term))
5582
+ tokens.push(...cjkNgrams(term));
5583
+ }
5584
+ return tokens.filter((term) => term.length > 0 && !STOPWORDS.has(term));
4591
5585
  }
4592
5586
  function unique(values) {
4593
5587
  return [...new Set(values)];
@@ -4743,6 +5737,408 @@ function scorePacketsBm25(queryTerms, packets) {
4743
5737
  }
4744
5738
  return result;
4745
5739
  }
5740
+ function scorePacketsVector(queryTerms, packets) {
5741
+ const terms = expandQueryTerms(queryTerms);
5742
+ const queryVector = termVector(terms);
5743
+ const queryNorm = vectorNorm(queryVector);
5744
+ const result = new Map();
5745
+ if (!terms.length || queryNorm <= 0 || !packets.length)
5746
+ return result;
5747
+ for (const packet of packets) {
5748
+ const documentVector = packetSparseVector(packet);
5749
+ const score = cosineScore(queryVector, queryNorm, documentVector);
5750
+ if (score <= 0)
5751
+ continue;
5752
+ const why = terms
5753
+ .filter((term) => queryVector.has(term) && documentVector.has(term))
5754
+ .slice(0, 5)
5755
+ .map((term) => `vector-local:${term}`);
5756
+ result.set(packet.id, { score: Number((score * 0.75).toFixed(2)), why });
5757
+ }
5758
+ return result;
5759
+ }
5760
+ function scorePacketsVectorFromIndex(queryTerms, index) {
5761
+ const terms = expandQueryTerms(queryTerms);
5762
+ const queryVector = termVector(terms);
5763
+ const queryNorm = vectorNorm(queryVector);
5764
+ const result = new Map();
5765
+ if (!index || !terms.length || queryNorm <= 0 || !index.documents.length)
5766
+ return result;
5767
+ for (const document of index.documents) {
5768
+ const documentVector = new Map(document.terms);
5769
+ const score = cosineScore(queryVector, queryNorm, documentVector, document.norm);
5770
+ if (score <= 0)
5771
+ continue;
5772
+ const why = terms
5773
+ .filter((term) => queryVector.has(term) && documentVector.has(term))
5774
+ .slice(0, 5)
5775
+ .map((term) => `vector-local-index:${term}`);
5776
+ result.set(document.packet_id, { score: Number((score * 0.75).toFixed(2)), why });
5777
+ }
5778
+ return result;
5779
+ }
5780
+ function packetSparseVector(packet) {
5781
+ return termVector([
5782
+ ...tokenize(packet.title).flatMap((term) => [term, term, term, lexicalStem(term)]),
5783
+ ...tokenize(packet.summary).flatMap((term) => [term, term, lexicalStem(term)]),
5784
+ ...tokenize(packet.tags.join(" ")).flatMap((term) => [term, term, lexicalStem(term)]),
5785
+ ...tokenize(packet.paths.join(" ")).flatMap((term) => [term, lexicalStem(term)]),
5786
+ ...tokenize(packet.type).flatMap((term) => [term, lexicalStem(term)]),
5787
+ ...tokenize(packet.body).flatMap((term) => [term, lexicalStem(term)]),
5788
+ ]);
5789
+ }
5790
+ function buildSparseVectorIndex(packets) {
5791
+ return {
5792
+ schema_version: 1,
5793
+ generated_from_updated_at: packets.map((packet) => packet.updated_at).sort().at(-1) ?? null,
5794
+ packet_count: packets.length,
5795
+ documents: packets.map((packet) => {
5796
+ const vector = packetSparseVector(packet);
5797
+ return {
5798
+ packet_id: packet.id,
5799
+ terms: Array.from(vector.entries()).sort(([a], [b]) => a.localeCompare(b)),
5800
+ norm: Number(vectorNorm(vector).toFixed(6)),
5801
+ };
5802
+ }),
5803
+ };
5804
+ }
5805
+ function writeSparseVectorIndex(projectDir, packets) {
5806
+ const path = (0, node_path_1.join)(indexesDir(projectDir), "vector-local.json");
5807
+ writeJson(path, buildSparseVectorIndex(packets));
5808
+ return path;
5809
+ }
5810
+ function readSparseVectorIndex(projectDir, packets) {
5811
+ const path = (0, node_path_1.join)(indexesDir(projectDir), "vector-local.json");
5812
+ if (!(0, node_fs_1.existsSync)(path))
5813
+ return null;
5814
+ try {
5815
+ const index = readJson(path);
5816
+ if (index.schema_version !== 1)
5817
+ return null;
5818
+ if (index.packet_count !== packets.length)
5819
+ return null;
5820
+ const generatedFrom = packets.map((packet) => packet.updated_at).sort().at(-1) ?? null;
5821
+ if (index.generated_from_updated_at !== generatedFrom)
5822
+ return null;
5823
+ const packetIds = new Set(packets.map((packet) => packet.id));
5824
+ if (index.documents.length !== packets.length)
5825
+ return null;
5826
+ for (const document of index.documents) {
5827
+ if (!packetIds.has(document.packet_id))
5828
+ return null;
5829
+ if (!Array.isArray(document.terms) || !Number.isFinite(document.norm))
5830
+ return null;
5831
+ }
5832
+ return index;
5833
+ }
5834
+ catch {
5835
+ return null;
5836
+ }
5837
+ }
5838
+ function denseEmbeddingIndexPath(projectDir) {
5839
+ return (0, node_path_1.join)(indexesDir(projectDir), "embeddings-local.json");
5840
+ }
5841
+ function embeddingText(packet) {
5842
+ return [
5843
+ packet.title,
5844
+ packet.summary,
5845
+ packet.type,
5846
+ packet.tags.join(" "),
5847
+ packet.paths.join(" "),
5848
+ packet.body,
5849
+ ].filter(Boolean).join("\n").slice(0, 8000);
5850
+ }
5851
+ async function createDenseEmbeddingProvider(model = "Xenova/all-MiniLM-L6-v2") {
5852
+ let extractor = null;
5853
+ return {
5854
+ name: "xenova",
5855
+ model,
5856
+ dimensions: 384,
5857
+ async embedBatch(texts) {
5858
+ if (!extractor) {
5859
+ let transformers;
5860
+ try {
5861
+ // @ts-ignore Optional peer dependency. Kage does not install this by default.
5862
+ transformers = await import("@xenova/transformers");
5863
+ }
5864
+ catch {
5865
+ throw new Error("Install @xenova/transformers to build local embeddings: npm install @xenova/transformers");
5866
+ }
5867
+ extractor = await transformers.pipeline("feature-extraction", model);
5868
+ }
5869
+ const output = await extractor(texts, { pooling: "mean", normalize: true });
5870
+ return output.tolist();
5871
+ },
5872
+ };
5873
+ }
5874
+ function denseNorm(vector) {
5875
+ return Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
5876
+ }
5877
+ function denseCosine(query, document, documentNorm) {
5878
+ if (query.length !== document.length)
5879
+ return 0;
5880
+ const queryNorm = denseNorm(query);
5881
+ const docNorm = documentNorm ?? denseNorm(document);
5882
+ if (queryNorm <= 0 || docNorm <= 0)
5883
+ return 0;
5884
+ let dot = 0;
5885
+ for (let index = 0; index < query.length; index += 1)
5886
+ dot += query[index] * document[index];
5887
+ return dot / (queryNorm * docNorm);
5888
+ }
5889
+ async function buildEmbeddingIndex(projectDir, options = {}) {
5890
+ ensureMemoryDirs(projectDir);
5891
+ const packets = loadApprovedPackets(projectDir);
5892
+ const path = denseEmbeddingIndexPath(projectDir);
5893
+ try {
5894
+ const provider = options.provider ?? await createDenseEmbeddingProvider(options.model);
5895
+ const batchSize = Math.max(1, Math.min(64, Math.floor(options.batchSize ?? 16)));
5896
+ const documents = [];
5897
+ for (let offset = 0; offset < packets.length; offset += batchSize) {
5898
+ const batch = packets.slice(offset, offset + batchSize);
5899
+ const vectors = await provider.embedBatch(batch.map(embeddingText));
5900
+ batch.forEach((packet, index) => {
5901
+ const vector = (vectors[index] ?? []).map((value) => Number(value));
5902
+ documents.push({
5903
+ packet_id: packet.id,
5904
+ vector,
5905
+ norm: Number(denseNorm(vector).toFixed(6)),
5906
+ });
5907
+ });
5908
+ }
5909
+ const artifact = {
5910
+ schema_version: 1,
5911
+ provider: provider.name,
5912
+ model: provider.model,
5913
+ dimensions: provider.dimensions,
5914
+ generated_from_updated_at: packets.map((packet) => packet.updated_at).sort().at(-1) ?? null,
5915
+ packet_count: packets.length,
5916
+ documents,
5917
+ };
5918
+ writeJson(path, artifact);
5919
+ return {
5920
+ ok: true,
5921
+ project_dir: projectDir,
5922
+ path,
5923
+ provider: artifact.provider,
5924
+ model: artifact.model,
5925
+ dimensions: artifact.dimensions,
5926
+ packet_count: artifact.packet_count,
5927
+ errors: [],
5928
+ };
5929
+ }
5930
+ catch (error) {
5931
+ return {
5932
+ ok: false,
5933
+ project_dir: projectDir,
5934
+ path,
5935
+ provider: "none",
5936
+ model: options.model ?? "Xenova/all-MiniLM-L6-v2",
5937
+ dimensions: 0,
5938
+ packet_count: packets.length,
5939
+ errors: [String(error instanceof Error ? error.message : error)],
5940
+ };
5941
+ }
5942
+ }
5943
+ function readDenseEmbeddingIndex(projectDir, packets) {
5944
+ const path = denseEmbeddingIndexPath(projectDir);
5945
+ if (!(0, node_fs_1.existsSync)(path))
5946
+ return null;
5947
+ try {
5948
+ const index = readJson(path);
5949
+ if (index.schema_version !== 1)
5950
+ return null;
5951
+ if (index.packet_count !== packets.length)
5952
+ return null;
5953
+ const generatedFrom = packets.map((packet) => packet.updated_at).sort().at(-1) ?? null;
5954
+ if (index.generated_from_updated_at !== generatedFrom)
5955
+ return null;
5956
+ const packetIds = new Set(packets.map((packet) => packet.id));
5957
+ if (index.documents.length !== packets.length)
5958
+ return null;
5959
+ for (const document of index.documents) {
5960
+ if (!packetIds.has(document.packet_id))
5961
+ return null;
5962
+ if (!Array.isArray(document.vector) || document.vector.length !== index.dimensions)
5963
+ return null;
5964
+ if (!Number.isFinite(document.norm))
5965
+ return null;
5966
+ }
5967
+ return index;
5968
+ }
5969
+ catch {
5970
+ return null;
5971
+ }
5972
+ }
5973
+ function scorePacketsDenseEmbeddings(queryVector, index) {
5974
+ const result = new Map();
5975
+ if (!index || !queryVector.length || !index.documents.length)
5976
+ return result;
5977
+ for (const document of index.documents) {
5978
+ const score = denseCosine(queryVector, document.vector, document.norm);
5979
+ if (score <= 0)
5980
+ continue;
5981
+ result.set(document.packet_id, {
5982
+ score: Number((score * 3).toFixed(2)),
5983
+ why: [`vector-external:${index.provider}:${index.model}`],
5984
+ });
5985
+ }
5986
+ return result;
5987
+ }
5988
+ function termVector(terms) {
5989
+ const vector = new Map();
5990
+ for (const term of terms) {
5991
+ if (!term)
5992
+ continue;
5993
+ vector.set(term, (vector.get(term) ?? 0) + 1);
5994
+ }
5995
+ return vector;
5996
+ }
5997
+ function vectorNorm(vector) {
5998
+ let sum = 0;
5999
+ for (const value of vector.values())
6000
+ sum += value * value;
6001
+ return Math.sqrt(sum);
6002
+ }
6003
+ function cosineScore(queryVector, queryNorm, documentVector, knownDocumentNorm) {
6004
+ const documentNorm = knownDocumentNorm ?? vectorNorm(documentVector);
6005
+ if (queryNorm <= 0 || documentNorm <= 0)
6006
+ return 0;
6007
+ let dot = 0;
6008
+ for (const [term, queryWeight] of queryVector) {
6009
+ dot += queryWeight * (documentVector.get(term) ?? 0);
6010
+ }
6011
+ return dot / (queryNorm * documentNorm);
6012
+ }
6013
+ function scoreReferenceBodyBm25(queryTerms, packets) {
6014
+ const terms = expandQueryTerms(queryTerms);
6015
+ const references = packets.filter((packet) => packet.type === "reference");
6016
+ const documents = references.map((packet) => ({ packet, terms: tokenize(packet.body), length: Math.max(1, tokenize(packet.body).length) }));
6017
+ const result = new Map();
6018
+ if (!terms.length || !documents.length)
6019
+ return result;
6020
+ const averageLength = documents.reduce((sum, document) => sum + document.length, 0) / documents.length || 1;
6021
+ const documentFrequency = new Map();
6022
+ for (const term of terms) {
6023
+ documentFrequency.set(term, documents.filter((document) => document.terms.includes(term)).length);
6024
+ }
6025
+ for (const document of documents) {
6026
+ const termFrequency = new Map();
6027
+ for (const term of document.terms)
6028
+ termFrequency.set(term, (termFrequency.get(term) ?? 0) + 1);
6029
+ let score = 0;
6030
+ for (const term of terms) {
6031
+ const frequency = termFrequency.get(term) ?? 0;
6032
+ if (frequency <= 0)
6033
+ continue;
6034
+ const df = documentFrequency.get(term) ?? 0;
6035
+ const idf = Math.log(1 + (documents.length - df + 0.5) / (df + 0.5));
6036
+ const denominator = frequency + 1.5 * (1 - 0.75 + 0.75 * (document.length / averageLength));
6037
+ score += idf * ((frequency * 2.5) / denominator);
6038
+ }
6039
+ if (score > 0)
6040
+ result.set(document.packet.id, Number(score.toFixed(2)));
6041
+ }
6042
+ return result;
6043
+ }
6044
+ function extractTemporalAnchorDate(query) {
6045
+ const labeled = query.match(/\b(?:question|current|today|query)\s+date\s*:\s*(\d{4})[/-](\d{1,2})[/-](\d{1,2})/i);
6046
+ if (!labeled)
6047
+ return null;
6048
+ const year = Number(labeled[1]);
6049
+ const month = Number(labeled[2]);
6050
+ const day = Number(labeled[3]);
6051
+ if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day))
6052
+ return null;
6053
+ const date = new Date(Date.UTC(year, month - 1, day));
6054
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day)
6055
+ return null;
6056
+ return date;
6057
+ }
6058
+ function stripTemporalMetadata(query) {
6059
+ return query
6060
+ .split(/\r?\n/)
6061
+ .filter((line) => !/^\s*(?:question|current|today|query)\s+date\s*:/i.test(line))
6062
+ .join("\n");
6063
+ }
6064
+ function shiftUtcDays(date, days) {
6065
+ const shifted = new Date(date.getTime());
6066
+ shifted.setUTCDate(shifted.getUTCDate() + days);
6067
+ return shifted;
6068
+ }
6069
+ function formatUtcDate(date) {
6070
+ const year = date.getUTCFullYear();
6071
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
6072
+ const day = String(date.getUTCDate()).padStart(2, "0");
6073
+ return `${year}/${month}/${day}`;
6074
+ }
6075
+ function monthName(date) {
6076
+ return date.toLocaleString("en-US", { month: "long", timeZone: "UTC" });
6077
+ }
6078
+ function temporalQueryTerms(query) {
6079
+ const anchor = extractTemporalAnchorDate(query);
6080
+ if (!anchor)
6081
+ return [];
6082
+ const lower = query.toLowerCase();
6083
+ const hints = [];
6084
+ const addTargetDate = (daysAgo) => {
6085
+ const target = shiftUtcDays(anchor, -daysAgo);
6086
+ hints.push(`target date ${formatUtcDate(target)} ${monthName(target)} ${target.getUTCDate()}`);
6087
+ };
6088
+ if (/\b(?:ten|10)\s+days?\s+ago\b/.test(lower))
6089
+ addTargetDate(10);
6090
+ if (/\b(?:two|2)\s+weeks?\s+ago\b/.test(lower))
6091
+ addTargetDate(14);
6092
+ if (/\b(?:three|3)\s+weeks?\s+ago\b/.test(lower))
6093
+ addTargetDate(21);
6094
+ if (/\b(?:four|4)\s+weeks?\s+ago\b/.test(lower))
6095
+ addTargetDate(28);
6096
+ if (/\b(?:past|last)\s+month\b/.test(lower)) {
6097
+ const start = shiftUtcDays(anchor, -31);
6098
+ hints.push(`target month ${monthName(start)} ${start.getUTCFullYear()} ${formatUtcDate(start)} to ${formatUtcDate(anchor)}`);
6099
+ }
6100
+ return tokenize(hints.join(" "));
6101
+ }
6102
+ function semanticConceptTerms(query) {
6103
+ const lower = query.toLowerCase();
6104
+ const hints = [];
6105
+ const labels = [];
6106
+ if (/\b(homegrown|garden|gardening|harvest|harvested|produce)\b/.test(lower)) {
6107
+ hints.push("garden gardening planted planting plants herbs vegetables tomato tomatoes harvest harvested homegrown produce");
6108
+ labels.push("garden-produce");
6109
+ }
6110
+ if (/\b(battery life|phone battery|charging|charger|power bank|powerbank)\b/.test(lower)) {
6111
+ hints.push("battery phone charging charger charged power bank powerbank portable battery-saving");
6112
+ labels.push("phone-battery");
6113
+ }
6114
+ if (/\b(sibling|siblings|brother|brothers|sister|sisters)\b/.test(lower)) {
6115
+ hints.push("sibling siblings brother brothers sister sisters family");
6116
+ labels.push("family-siblings");
6117
+ }
6118
+ if (/\b(business milestone|milestone|first client|contract)\b/.test(lower)) {
6119
+ hints.push("business milestone signed contract client customer deal");
6120
+ labels.push("business-milestone");
6121
+ }
6122
+ if (/\b(kitchen appliance|appliance|appliances)\b/.test(lower)) {
6123
+ hints.push("kitchen appliance appliances bought purchased oven stove microwave blender mixer toaster grill smoker");
6124
+ labels.push("kitchen-appliance");
6125
+ }
6126
+ return { terms: tokenize(hints.join(" ")), labels };
6127
+ }
6128
+ function recallQueryExpansion(query) {
6129
+ const stripped = stripTemporalMetadata(query);
6130
+ const semantic = semanticConceptTerms(stripped);
6131
+ const baseTerms = tokenize(stripped);
6132
+ const temporalTerms = temporalQueryTerms(query);
6133
+ const semanticTerms = semantic.terms;
6134
+ return {
6135
+ baseTerms,
6136
+ temporalTerms,
6137
+ semanticTerms,
6138
+ semanticLabels: semantic.labels,
6139
+ terms: [...baseTerms, ...temporalTerms, ...semanticTerms],
6140
+ };
6141
+ }
4746
6142
  function recallIntentBoost(queryTerms, packet) {
4747
6143
  const terms = new Set(expandQueryTerms(queryTerms));
4748
6144
  const commandIntent = ["run", "test", "tests", "build", "command", "commands"].some((term) => terms.has(term));
@@ -4789,40 +6185,131 @@ function recallGraphLookup(graph) {
4789
6185
  }
4790
6186
  return { packetEntityByPacketId, edgesByEntityId };
4791
6187
  }
4792
- function recallBreakdown(projectDir, terms, packet, textScore, graph = buildKnowledgeGraph(projectDir), lookup = recallGraphLookup(graph)) {
6188
+ function recallBreakdown(projectDir, terms, packet, textScore, temporalScore = 0, semanticScore = 0, vectorScore = 0, usageScore = 0, graph = buildKnowledgeGraph(projectDir), lookup = recallGraphLookup(graph)) {
4793
6189
  const packetEntityId = lookup.packetEntityByPacketId.get(packet.id);
4794
6190
  const rawGraphScore = packetEntityId
4795
6191
  ? (lookup.edgesByEntityId.get(packetEntityId) ?? []).reduce((sum, edge) => sum + scoreText(terms, edge.fact), 0)
4796
6192
  : 0;
4797
- const graphScore = Math.min(rawGraphScore * 0.45, textScore > 0 ? textScore * 1.5 + 12 : 8);
6193
+ const graphCap = packet.type === "reference"
6194
+ ? 0
6195
+ : (textScore > 0 ? textScore * 1.5 + 12 : 8);
6196
+ const graphWeight = packet.type === "reference" ? 0 : 0.45;
6197
+ const graphScore = Math.min(rawGraphScore * graphWeight, graphCap);
4798
6198
  const pathTypeTag = scoreText(terms, `${packet.type} ${packet.tags.join(" ")} ${packet.paths.join(" ")}`, [packet.type, ...packet.tags, ...packet.paths]);
4799
6199
  const intent = recallIntentBoost(terms, packet);
4800
6200
  const freshness = packet.status === "approved" ? 2 : packet.status === "pending" ? 0 : -5;
4801
- const quality = Number(packet.quality.score ?? evaluateMemoryQuality(projectDir, packet).score) / 10;
6201
+ const quality = recallQualityScore(packet);
4802
6202
  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 };
6203
+ const vector = Number(vectorScore.toFixed(2));
6204
+ const usage = Number(usageScore.toFixed(2));
6205
+ const pathTypeTagWeight = packet.type === "reference" ? 0.2 : 0.8;
6206
+ const final = Number((textScore + graphScore + pathTypeTag * pathTypeTagWeight + intent + vector + usage + freshness + quality + feedback).toFixed(2));
6207
+ return {
6208
+ bm25: textScore,
6209
+ text: textScore,
6210
+ temporal: Number(temporalScore.toFixed(2)),
6211
+ semantic: Number(semanticScore.toFixed(2)),
6212
+ graph: Number(graphScore.toFixed(2)),
6213
+ path_type_tag: pathTypeTag,
6214
+ intent,
6215
+ vector,
6216
+ usage,
6217
+ freshness,
6218
+ quality: Number(quality.toFixed(2)),
6219
+ feedback,
6220
+ final,
6221
+ };
4806
6222
  }
4807
- function recall(projectDir, query, limit = 5, explain = false, inputs = {}) {
6223
+ function recallDiversitySource(packet) {
6224
+ for (const ref of packet.source_refs) {
6225
+ if (ref.kind === "observation_session" && typeof ref.session_id === "string" && ref.session_id.trim()) {
6226
+ return `session:${ref.session_id.trim()}`;
6227
+ }
6228
+ }
6229
+ return null;
6230
+ }
6231
+ function diversifyRecallEntries(entries, limit, maxPerSource = 3) {
6232
+ if (limit <= maxPerSource)
6233
+ return entries.slice(0, limit);
6234
+ const selected = [];
6235
+ const deferred = [];
6236
+ const sourceCounts = new Map();
6237
+ for (const entry of entries) {
6238
+ const source = recallDiversitySource(entry.packet);
6239
+ if (source) {
6240
+ const count = sourceCounts.get(source) ?? 0;
6241
+ if (count >= maxPerSource) {
6242
+ deferred.push(entry);
6243
+ continue;
6244
+ }
6245
+ sourceCounts.set(source, count + 1);
6246
+ }
6247
+ selected.push(entry);
6248
+ if (selected.length >= limit)
6249
+ return selected.slice(0, limit);
6250
+ }
6251
+ for (const entry of deferred) {
6252
+ if (selected.length >= limit)
6253
+ break;
6254
+ selected.push(entry);
6255
+ }
6256
+ return selected.slice(0, limit);
6257
+ }
6258
+ function recallWithVectorScores(projectDir, query, limit = 5, explain = false, inputs = {}, externalVectorScores) {
4808
6259
  const current = inputs.codeGraph && inputs.knowledgeGraph ? null : readCurrentGraphs(projectDir);
4809
6260
  const detailedIndex = inputs.codeGraph && inputs.knowledgeGraph || current ? null : indexProjectDetailed(projectDir);
4810
6261
  const codeGraph = inputs.codeGraph ?? current?.codeGraph ?? detailedIndex?.codeGraph ?? buildCodeGraph(projectDir);
4811
6262
  const knowledgeGraph = inputs.knowledgeGraph ?? current?.knowledgeGraph ?? detailedIndex?.knowledgeGraph ?? buildKnowledgeGraph(projectDir, codeGraph);
4812
- const terms = tokenize(query);
6263
+ const expansion = inputs.semanticExpansion === false
6264
+ ? (() => {
6265
+ const baseTerms = tokenize(stripTemporalMetadata(query));
6266
+ const temporalTerms = temporalQueryTerms(query);
6267
+ return {
6268
+ baseTerms,
6269
+ temporalTerms,
6270
+ semanticTerms: [],
6271
+ semanticLabels: [],
6272
+ terms: [...baseTerms, ...temporalTerms],
6273
+ };
6274
+ })()
6275
+ : recallQueryExpansion(query);
6276
+ const terms = expansion.terms;
4813
6277
  const approvedPackets = loadApprovedPackets(projectDir);
4814
- const lexicalScores = scorePacketsBm25(terms, approvedPackets);
6278
+ const baseScores = scorePacketsBm25(expansion.baseTerms, approvedPackets);
6279
+ const temporalScores = scorePacketsBm25(expansion.temporalTerms, approvedPackets);
6280
+ const semanticScores = scorePacketsBm25(expansion.semanticTerms, approvedPackets);
6281
+ const sparseVectorIndex = externalVectorScores ? null : readSparseVectorIndex(projectDir, approvedPackets);
6282
+ const vectorScores = externalVectorScores ?? (sparseVectorIndex
6283
+ ? scorePacketsVectorFromIndex(terms, sparseVectorIndex)
6284
+ : scorePacketsVector(terms, approvedPackets));
6285
+ const referenceBodyScores = scoreReferenceBodyBm25(terms, approvedPackets);
6286
+ const accessEntries = readMemoryAccessEntries(projectDir, approvedPackets);
4815
6287
  const graphLookup = recallGraphLookup(knowledgeGraph);
4816
- const scored = approvedPackets
6288
+ const rankedScored = approvedPackets
4817
6289
  .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 };
6290
+ const base = baseScores.get(packet.id) ?? { score: 0, why: [] };
6291
+ const temporal = temporalScores.get(packet.id) ?? { score: 0, why: [] };
6292
+ const semantic = semanticScores.get(packet.id) ?? { score: 0, why: [] };
6293
+ const vector = vectorScores.get(packet.id) ?? { score: 0, why: [] };
6294
+ const referenceBodyScore = referenceBodyScores.get(packet.id) ?? 0;
6295
+ const lexicalScore = base.score + temporal.score + semantic.score;
6296
+ const textScore = packet.type === "reference" ? Math.max(lexicalScore, referenceBodyScore) : lexicalScore;
6297
+ const usageScore = memoryAccessScore(accessEntries.get(packet.id));
6298
+ const score_breakdown = recallBreakdown(projectDir, terms, packet, textScore, temporal.score, semantic.score, vector.score, usageScore, knowledgeGraph, graphLookup);
6299
+ const relevance = textScore + score_breakdown.graph + score_breakdown.path_type_tag + score_breakdown.intent + score_breakdown.vector;
6300
+ const why = [
6301
+ ...base.why,
6302
+ ...temporal.why.map((item) => `temporal:${item}`),
6303
+ ...semantic.why.map((item) => `semantic:${item}`),
6304
+ ...(semantic.score > 0 ? expansion.semanticLabels.map((label) => `semantic-concept:${label}`) : []),
6305
+ ...vector.why,
6306
+ ...(usageScore > 0 ? [`usage:${accessEntries.get(packet.id)?.uses_30d ?? 0} recalls in 30d`] : []),
6307
+ ];
6308
+ return { packet, score: score_breakdown.final, relevance, why_matched: unique(why).slice(0, 12), score_breakdown };
4822
6309
  })
4823
6310
  .filter((entry) => entry.relevance > 0)
4824
- .sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title))
4825
- .slice(0, limit)
6311
+ .sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title));
6312
+ const scored = diversifyRecallEntries(rankedScored, limit)
4826
6313
  .map(({ relevance, ...entry }) => entry);
4827
6314
  const pendingSeen = new Set();
4828
6315
  const pendingPackets = recallablePendingPackets(projectDir);
@@ -4844,11 +6331,13 @@ function recall(projectDir, query, limit = 5, explain = false, inputs = {}) {
4844
6331
  .slice(0, 3);
4845
6332
  const graphContext = queryGraph(projectDir, query, 5, knowledgeGraph);
4846
6333
  const codeContext = queryCodeGraph(projectDir, query, 5, codeGraph);
6334
+ const pinnedContext = renderPinnedRepoContext(readContextSlots(projectDir));
4847
6335
  const lines = [
4848
6336
  `# Kage Context`,
4849
6337
  "",
4850
6338
  `Query: ${query}`,
4851
6339
  "",
6340
+ ...(pinnedContext ? [pinnedContext, ""] : []),
4852
6341
  codeContext.symbols.length || codeContext.routes.length || codeContext.tests.length || codeContext.files.length ? "## Relevant Code Graph" : "",
4853
6342
  ...codeContext.routes.slice(0, 3).map((route, index) => `${index + 1}. [route] ${route.method} ${route.path} -> ${route.file_path}:${route.line}`),
4854
6343
  ...codeContext.symbols.slice(0, 5).map((symbol, index) => `${index + 1}. [symbol] ${symbol.kind} ${symbol.name} in ${symbol.path}:${symbol.line}`),
@@ -4876,7 +6365,7 @@ function recall(projectDir, query, limit = 5, explain = false, inputs = {}) {
4876
6365
  graphContext.edges.length ? "## Related Graph Facts" : "",
4877
6366
  ...graphContext.edges.slice(0, 5).map((edge, index) => `${index + 1}. ${edge.fact} (evidence: ${edge.evidence.join(", ")})`),
4878
6367
  ];
4879
- return {
6368
+ const result = {
4880
6369
  query,
4881
6370
  context_block: lines.join("\n"),
4882
6371
  results: scored,
@@ -4890,6 +6379,25 @@ function recall(projectDir, query, limit = 5, explain = false, inputs = {}) {
4890
6379
  }))
4891
6380
  : undefined,
4892
6381
  };
6382
+ if (inputs.trackAccess !== false)
6383
+ recordRecallAccess(projectDir, result.results);
6384
+ return result;
6385
+ }
6386
+ function recall(projectDir, query, limit = 5, explain = false, inputs = {}) {
6387
+ return recallWithVectorScores(projectDir, query, limit, explain, inputs);
6388
+ }
6389
+ async function recallWithEmbeddings(projectDir, query, limit = 5, explain = false, options = {}) {
6390
+ const packets = loadApprovedPackets(projectDir);
6391
+ const index = readDenseEmbeddingIndex(projectDir, packets);
6392
+ if (!index) {
6393
+ const result = recall(projectDir, query, limit, explain, { trackAccess: options.trackAccess, semanticExpansion: options.semanticExpansion });
6394
+ 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.`;
6395
+ return result;
6396
+ }
6397
+ const provider = options.provider ?? await createDenseEmbeddingProvider(options.model ?? index.model);
6398
+ const [queryVector] = await provider.embedBatch([query]);
6399
+ const vectorScores = scorePacketsDenseEmbeddings(queryVector ?? [], index);
6400
+ return recallWithVectorScores(projectDir, query, limit, explain, { trackAccess: options.trackAccess, semanticExpansion: options.semanticExpansion }, vectorScores);
4893
6401
  }
4894
6402
  function scoreText(terms, text, boosts = []) {
4895
6403
  const haystack = text.toLowerCase();
@@ -5856,11 +7364,498 @@ function kageContributors(projectDir) {
5856
7364
  schema_version: 1,
5857
7365
  project_dir: projectDir,
5858
7366
  generated_at: nowIso(),
5859
- contributors: profiles,
5860
- warnings,
5861
- summary: profiles.length
5862
- ? `${profiles.length} contributor profile(s). Most active: ${profiles[0].contributor} with ${profiles[0].commits_90d} commit(s) in 90d.`
5863
- : "No contributor profiles could be computed.",
7367
+ contributors: profiles,
7368
+ warnings,
7369
+ summary: profiles.length
7370
+ ? `${profiles.length} contributor profile(s). Most active: ${profiles[0].contributor} with ${profiles[0].commits_90d} commit(s) in 90d.`
7371
+ : "No contributor profiles could be computed.",
7372
+ };
7373
+ }
7374
+ const DEFAULT_CONTEXT_SLOT_LIMIT = 2000;
7375
+ const MAX_CONTEXT_SLOT_LIMIT = 8000;
7376
+ const MAX_PINNED_CONTEXT_CHARS = 6000;
7377
+ function slotsPath(projectDir) {
7378
+ return (0, node_path_1.join)(slotsDir(projectDir), "slots.json");
7379
+ }
7380
+ function validSlotLabel(label) {
7381
+ return /^[a-z][a-z0-9_]{0,63}$/.test(label);
7382
+ }
7383
+ function normalizeSlot(raw, fallbackAt) {
7384
+ if (!raw || typeof raw !== "object")
7385
+ return null;
7386
+ const data = raw;
7387
+ const label = typeof data.label === "string" ? data.label.trim() : "";
7388
+ if (!validSlotLabel(label))
7389
+ return null;
7390
+ const sizeLimit = Number(data.size_limit ?? data.sizeLimit ?? DEFAULT_CONTEXT_SLOT_LIMIT);
7391
+ const normalizedLimit = Number.isFinite(sizeLimit)
7392
+ ? Math.max(1, Math.min(MAX_CONTEXT_SLOT_LIMIT, Math.floor(sizeLimit)))
7393
+ : DEFAULT_CONTEXT_SLOT_LIMIT;
7394
+ const content = typeof data.content === "string" ? data.content : "";
7395
+ return {
7396
+ label,
7397
+ content: content.slice(0, normalizedLimit),
7398
+ description: typeof data.description === "string" ? data.description.trim() : "",
7399
+ pinned: data.pinned !== false,
7400
+ size_limit: normalizedLimit,
7401
+ paths: Array.isArray(data.paths) ? data.paths.map(String).map((item) => item.trim()).filter(Boolean) : [],
7402
+ tags: Array.isArray(data.tags) ? data.tags.map(String).map((item) => item.trim()).filter(Boolean) : [],
7403
+ created_at: typeof data.created_at === "string" ? data.created_at : fallbackAt,
7404
+ updated_at: typeof data.updated_at === "string" ? data.updated_at : fallbackAt,
7405
+ };
7406
+ }
7407
+ function readContextSlots(projectDir) {
7408
+ const path = slotsPath(projectDir);
7409
+ if (!(0, node_fs_1.existsSync)(path))
7410
+ return [];
7411
+ try {
7412
+ const parsed = readJson(path);
7413
+ const rawSlots = Array.isArray(parsed) ? parsed : Array.isArray(parsed.slots) ? parsed.slots : [];
7414
+ const at = nowIso();
7415
+ const byLabel = new Map();
7416
+ for (const raw of rawSlots) {
7417
+ const slot = normalizeSlot(raw, at);
7418
+ if (slot)
7419
+ byLabel.set(slot.label, slot);
7420
+ }
7421
+ return [...byLabel.values()].sort((a, b) => a.label.localeCompare(b.label));
7422
+ }
7423
+ catch {
7424
+ return [];
7425
+ }
7426
+ }
7427
+ function writeContextSlots(projectDir, slots) {
7428
+ writeJson(slotsPath(projectDir), {
7429
+ schema_version: 1,
7430
+ updated_at: nowIso(),
7431
+ slots: [...slots].sort((a, b) => a.label.localeCompare(b.label)),
7432
+ });
7433
+ }
7434
+ function renderPinnedRepoContext(slots) {
7435
+ const pinned = slots.filter((slot) => slot.pinned && slot.content.trim());
7436
+ if (!pinned.length)
7437
+ return "";
7438
+ const lines = ["## Pinned Repo Context"];
7439
+ let used = 0;
7440
+ for (const slot of pinned) {
7441
+ const meta = [
7442
+ slot.description ? `description: ${slot.description}` : "",
7443
+ slot.paths.length ? `paths: ${slot.paths.slice(0, 6).join(", ")}` : "",
7444
+ slot.tags.length ? `tags: ${slot.tags.slice(0, 8).join(", ")}` : "",
7445
+ ].filter(Boolean);
7446
+ const block = [
7447
+ "",
7448
+ `### ${slot.label}`,
7449
+ ...meta.map((item) => `_${item}_`),
7450
+ slot.content.trim(),
7451
+ ].join("\n");
7452
+ if (used + block.length > MAX_PINNED_CONTEXT_CHARS) {
7453
+ lines.push("\n_Context slots truncated to keep recall compact._");
7454
+ break;
7455
+ }
7456
+ lines.push(block);
7457
+ used += block.length;
7458
+ }
7459
+ return lines.join("\n");
7460
+ }
7461
+ function kageContextSlots(projectDir) {
7462
+ ensureMemoryDirs(projectDir);
7463
+ const slots = readContextSlots(projectDir);
7464
+ const pinnedContext = renderPinnedRepoContext(slots);
7465
+ const pinned = slots.filter((slot) => slot.pinned && slot.content.trim());
7466
+ const warnings = [];
7467
+ for (const slot of slots) {
7468
+ if (!slot.paths.length && !slot.tags.length)
7469
+ warnings.push(`Slot ${slot.label} has no paths or tags for grounding.`);
7470
+ }
7471
+ return {
7472
+ schema_version: 1,
7473
+ project_dir: projectDir,
7474
+ generated_at: nowIso(),
7475
+ slots_path: (0, node_path_1.relative)(projectDir, slotsPath(projectDir)),
7476
+ totals: {
7477
+ slots: slots.length,
7478
+ pinned: pinned.length,
7479
+ context_chars: pinnedContext.length,
7480
+ },
7481
+ slots,
7482
+ pinned_context_block: pinnedContext,
7483
+ summary: pinned.length
7484
+ ? `${pinned.length} pinned repo context slot(s), ${slots.length} total.`
7485
+ : "No pinned repo context slots yet.",
7486
+ warnings,
7487
+ };
7488
+ }
7489
+ function setContextSlot(projectDir, input) {
7490
+ ensureMemoryDirs(projectDir);
7491
+ const label = String(input.label ?? "").trim();
7492
+ if (!validSlotLabel(label)) {
7493
+ return { ok: false, errors: ["label must start with a lowercase letter and contain only lowercase letters, numbers, and underscores"] };
7494
+ }
7495
+ const content = String(input.content ?? "").trim();
7496
+ if (!content)
7497
+ return { ok: false, errors: ["content is required"] };
7498
+ const findings = scanSensitiveText(content);
7499
+ if (findings.length)
7500
+ return { ok: false, errors: [`Refusing to save context slot with sensitive content: ${findings.join(", ")}`] };
7501
+ const requestedLimit = input.size_limit ?? DEFAULT_CONTEXT_SLOT_LIMIT;
7502
+ const sizeLimit = Number.isFinite(Number(requestedLimit))
7503
+ ? Math.max(1, Math.min(MAX_CONTEXT_SLOT_LIMIT, Math.floor(Number(requestedLimit))))
7504
+ : DEFAULT_CONTEXT_SLOT_LIMIT;
7505
+ if (content.length > sizeLimit) {
7506
+ return { ok: false, errors: [`content exceeds size limit (${content.length} > ${sizeLimit})`] };
7507
+ }
7508
+ const at = nowIso();
7509
+ const slots = readContextSlots(projectDir);
7510
+ const existing = slots.find((slot) => slot.label === label);
7511
+ const next = {
7512
+ label,
7513
+ content,
7514
+ description: input.description?.trim() ?? existing?.description ?? "",
7515
+ pinned: input.pinned ?? existing?.pinned ?? true,
7516
+ size_limit: sizeLimit,
7517
+ paths: unique((input.paths ?? existing?.paths ?? []).map((item) => item.trim()).filter(Boolean)),
7518
+ tags: unique((input.tags ?? existing?.tags ?? []).map((item) => item.trim()).filter(Boolean)),
7519
+ created_at: existing?.created_at ?? at,
7520
+ updated_at: at,
7521
+ };
7522
+ const merged = slots.filter((slot) => slot.label !== label);
7523
+ merged.push(next);
7524
+ writeContextSlots(projectDir, merged);
7525
+ return { ok: true, slot: next, report: kageContextSlots(projectDir), errors: [] };
7526
+ }
7527
+ function deleteContextSlot(projectDir, label) {
7528
+ ensureMemoryDirs(projectDir);
7529
+ const normalized = String(label ?? "").trim();
7530
+ if (!validSlotLabel(normalized))
7531
+ return { ok: false, errors: ["valid label is required"] };
7532
+ const slots = readContextSlots(projectDir);
7533
+ const deleted = slots.find((slot) => slot.label === normalized);
7534
+ if (!deleted)
7535
+ return { ok: false, errors: [`slot not found: ${normalized}`] };
7536
+ writeContextSlots(projectDir, slots.filter((slot) => slot.label !== normalized));
7537
+ return { ok: true, deleted, report: kageContextSlots(projectDir), errors: [] };
7538
+ }
7539
+ function kageProjectProfile(projectDir) {
7540
+ const graph = readCurrentCodeGraph(projectDir) ?? buildCodeGraph(projectDir);
7541
+ const structural = readCurrentStructuralIndex(projectDir);
7542
+ const approved = loadApprovedPackets(projectDir);
7543
+ const decisionPackets = approved.filter((packet) => DECISION_INTELLIGENCE_TYPES.has(packet.type));
7544
+ const graphPaths = new Set(graph.files.map((file) => file.path));
7545
+ const sourceFiles = graph.files.filter((file) => file.kind === "source");
7546
+ const testFiles = graph.files.filter((file) => file.kind === "test");
7547
+ const packetsByPath = new Map();
7548
+ for (const packet of approved) {
7549
+ for (const path of packet.paths.filter((item) => graphPaths.has(item))) {
7550
+ const list = packetsByPath.get(path) ?? [];
7551
+ list.push(packet);
7552
+ packetsByPath.set(path, list);
7553
+ }
7554
+ }
7555
+ const codeConceptCounts = new Map();
7556
+ for (const file of structural?.files ?? []) {
7557
+ for (const concept of file.concepts) {
7558
+ if (!concept || concept.length < 3)
7559
+ continue;
7560
+ codeConceptCounts.set(concept, (codeConceptCounts.get(concept) ?? 0) + 1);
7561
+ }
7562
+ }
7563
+ const memoryConceptCounts = new Map();
7564
+ for (const packet of approved) {
7565
+ for (const tag of packet.tags.filter((item) => item && !["session-learning", "agentmemory-comparison"].includes(item))) {
7566
+ memoryConceptCounts.set(tag, (memoryConceptCounts.get(tag) ?? 0) + 1);
7567
+ }
7568
+ }
7569
+ const conceptNames = new Set([...codeConceptCounts.keys(), ...memoryConceptCounts.keys()]);
7570
+ const topConcepts = [...conceptNames].map((concept) => ({
7571
+ concept,
7572
+ count: (codeConceptCounts.get(concept) ?? 0) + (memoryConceptCounts.get(concept) ?? 0),
7573
+ sources: [
7574
+ ...(codeConceptCounts.has(concept) ? ["code"] : []),
7575
+ ...(memoryConceptCounts.has(concept) ? ["memory"] : []),
7576
+ ],
7577
+ }))
7578
+ .sort((a, b) => b.count - a.count || b.sources.length - a.sources.length || a.concept.localeCompare(b.concept))
7579
+ .slice(0, 12);
7580
+ const { forward, reverse } = codeGraphAdjacency(graph);
7581
+ const rank = filePageRank(graph, forward);
7582
+ const routeCounts = countBy(graph.routes, (route) => route.file_path);
7583
+ const testCounts = countBy(graph.tests, (test) => test.test_path);
7584
+ const keyFiles = graph.files
7585
+ .map((file) => {
7586
+ const dependents = reverse.get(file.path)?.size ?? 0;
7587
+ const imports = forward.get(file.path)?.size ?? 0;
7588
+ const memoryPackets = packetsByPath.get(file.path)?.length ?? 0;
7589
+ const routes = routeCounts[file.path] ?? 0;
7590
+ const tests = testCounts[file.path] ?? 0;
7591
+ const score = Number(((rank.get(file.path) ?? 0) * 1000 +
7592
+ dependents * 8 +
7593
+ imports * 3 +
7594
+ memoryPackets * 12 +
7595
+ routes * 10 +
7596
+ tests * 4 +
7597
+ (file.kind === "source" ? 3 : 0)).toFixed(2));
7598
+ const why = [
7599
+ ...(memoryPackets ? [`${memoryPackets} linked memory packet(s)`] : []),
7600
+ ...(dependents ? [`${dependents} dependent file(s)`] : []),
7601
+ ...(routes ? [`${routes} route(s)`] : []),
7602
+ ...(tests ? [`${tests} test signal(s)`] : []),
7603
+ ...(imports ? [`${imports} outgoing import(s)`] : []),
7604
+ ];
7605
+ 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"] };
7606
+ })
7607
+ .sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
7608
+ .slice(0, 12);
7609
+ const highValuePackets = decisionPackets
7610
+ .map((packet) => ({ packet, score: qualityScore(packet) ?? Number(evaluateMemoryQuality(projectDir, packet).score ?? 0) }))
7611
+ .sort((a, b) => b.score - a.score || b.packet.paths.length - a.packet.paths.length || a.packet.title.localeCompare(b.packet.title))
7612
+ .slice(0, 8)
7613
+ .map(({ packet }) => ({
7614
+ packet_id: packet.id,
7615
+ title: packet.title,
7616
+ type: packet.type,
7617
+ paths: packet.paths.filter((path) => graphPaths.has(path)).slice(0, 6),
7618
+ summary: packet.summary,
7619
+ }));
7620
+ const runCommands = graph.packages
7621
+ .filter((item) => item.kind === "script")
7622
+ .map((item) => ({ name: item.name, command: item.version }))
7623
+ .sort((a, b) => a.name.localeCompare(b.name))
7624
+ .slice(0, 12);
7625
+ const coveragePercent = percent(packetsByPath.size, Math.max(1, sourceFiles.length + testFiles.length));
7626
+ const warnings = [
7627
+ ...(!structural ? ["Structural index is missing; top concepts only include memory tags. Run kage refresh."] : []),
7628
+ ...(!gitHead(projectDir) ? ["Git history is unavailable, so profile excludes ownership and churn signals."] : []),
7629
+ ];
7630
+ const nextActions = [
7631
+ ...(coveragePercent < 60 ? ["Capture or ground memory for high-signal source paths with no linked repo knowledge."] : []),
7632
+ ...(!runCommands.length ? ["Capture runbook memory for test/build/dev commands if package scripts are not available."] : []),
7633
+ ...(topConcepts.filter((item) => item.sources.length === 2).length < 3 ? ["Add tags or paths so important concepts connect both code and memory."] : []),
7634
+ ...(warnings.length ? ["Run kage refresh before using this profile for handoff."] : []),
7635
+ ];
7636
+ return {
7637
+ schema_version: 1,
7638
+ project_dir: projectDir,
7639
+ generated_at: nowIso(),
7640
+ repo_state: graph.repo_state,
7641
+ summary: `${graph.files.length} files, ${approved.length} memory packet(s), ${coveragePercent}% memory-code coverage. Top concept: ${topConcepts[0]?.concept ?? "none"}.`,
7642
+ totals: {
7643
+ files: graph.files.length,
7644
+ source_files: sourceFiles.length,
7645
+ test_files: testFiles.length,
7646
+ symbols: graph.symbols.length,
7647
+ routes: graph.routes.length,
7648
+ tests: graph.tests.length,
7649
+ approved_memory: approved.length,
7650
+ decision_memory: decisionPackets.length,
7651
+ memory_code_coverage_percent: coveragePercent,
7652
+ },
7653
+ languages: Object.entries(countBy(graph.files, (file) => file.language))
7654
+ .map(([language, files]) => ({ language, files }))
7655
+ .sort((a, b) => b.files - a.files || a.language.localeCompare(b.language)),
7656
+ top_concepts: topConcepts,
7657
+ key_files: keyFiles,
7658
+ memory_focus: {
7659
+ by_type: countBy(approved, (packet) => packet.type),
7660
+ top_tags: Object.entries(countBy(approved.flatMap((packet) => packet.tags.filter((tag) => tag !== "session-learning")), (tag) => tag))
7661
+ .map(([tag, count]) => ({ tag, count }))
7662
+ .sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag))
7663
+ .slice(0, 12),
7664
+ high_value_packets: highValuePackets,
7665
+ },
7666
+ run_commands: runCommands,
7667
+ next_actions: nextActions.length ? nextActions : ["Project profile is ready for agent handoff."],
7668
+ warnings: unique(warnings),
7669
+ };
7670
+ }
7671
+ function capabilityStatus(score) {
7672
+ if (score >= 80)
7673
+ return "ready";
7674
+ if (score >= 50)
7675
+ return "watch";
7676
+ return "gap";
7677
+ }
7678
+ function capabilityPillar(id, label, checks, evidence, gaps, actions) {
7679
+ const score = checks.length ? percent(checks.filter(Boolean).length, checks.length) : 0;
7680
+ return {
7681
+ id,
7682
+ label,
7683
+ score,
7684
+ status: capabilityStatus(score),
7685
+ evidence,
7686
+ gaps: unique(gaps),
7687
+ actions: unique(actions),
7688
+ };
7689
+ }
7690
+ function kageCapabilityAudit(projectDir) {
7691
+ ensureMemoryDirs(projectDir);
7692
+ const metrics = kageMetrics(projectDir);
7693
+ const approved = loadApprovedPackets(projectDir);
7694
+ const quality = qualityReport(projectDir);
7695
+ const slots = kageContextSlots(projectDir);
7696
+ const sessions = kageSessionCaptureReport(projectDir);
7697
+ const replay = kageSessionReplay(projectDir, { limit: 50 });
7698
+ const handoff = kageMemoryHandoff(projectDir);
7699
+ const audit = kageMemoryAudit(projectDir, 50);
7700
+ const benchmark = benchmarkProject(projectDir);
7701
+ const memoryCodeLinks = metrics.memory_graph.edges;
7702
+ const reportsPath = reportsDir(projectDir);
7703
+ const viewerAppPath = (0, node_path_1.join)(__dirname, "..", "viewer", "app.js");
7704
+ const repoRoot = (0, node_path_1.resolve)(__dirname, "..", "..");
7705
+ const longMemEvalDoc = (0, node_path_1.join)(repoRoot, "benchmarks", "LONGMEMEVAL.md");
7706
+ const scaleBench = (0, node_path_1.join)(repoRoot, "benchmarks", "scale-kage-memory.mjs");
7707
+ const codingBench = (0, node_path_1.join)(repoRoot, "benchmarks", "coding-memory-quality.mjs");
7708
+ const generatedReports = [
7709
+ "benchmark.json",
7710
+ "handoff.json",
7711
+ "lifecycle.json",
7712
+ "memory-audit.json",
7713
+ "profile.json",
7714
+ "replay.json",
7715
+ ].filter((name) => (0, node_fs_1.existsSync)((0, node_path_1.join)(reportsPath, name)));
7716
+ const checklist = [
7717
+ {
7718
+ requirement: "reviewable repo memory",
7719
+ pass: approved.length > 0,
7720
+ evidence: `${approved.length} approved packet(s) in .agent_memory/packets`,
7721
+ action: "Capture durable decisions, runbooks, bugs, and gotchas with kage learn or kage capture.",
7722
+ },
7723
+ {
7724
+ requirement: "code-linked memory",
7725
+ pass: memoryCodeLinks > 0,
7726
+ evidence: `${memoryCodeLinks} memory graph edge(s), ${metrics.code_graph.files} indexed code file(s)`,
7727
+ action: "Add paths to packets and run kage refresh so memory connects to changed code.",
7728
+ },
7729
+ {
7730
+ requirement: "pinned context",
7731
+ pass: slots.totals.pinned > 0,
7732
+ evidence: `${slots.totals.pinned} pinned slot(s)`,
7733
+ action: "Add tiny stable repo guidance with kage slots set.",
7734
+ },
7735
+ {
7736
+ requirement: "privacy-preserving session proof",
7737
+ pass: replay.totals.events > 0 || sessions.totals.sessions > 0,
7738
+ evidence: `${replay.totals.events} replay event(s), ${sessions.totals.durable_observations} durable candidate(s)`,
7739
+ action: "Enable observe hooks or call kage_observe, then distill durable candidates.",
7740
+ },
7741
+ {
7742
+ requirement: "benchmark proof",
7743
+ pass: benchmark.ok && (0, node_fs_1.existsSync)(longMemEvalDoc) && (0, node_fs_1.existsSync)(scaleBench) && (0, node_fs_1.existsSync)(codingBench),
7744
+ 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"}`,
7745
+ action: "Run kage benchmark --memory-quality and kage benchmark --scale before publishing performance claims.",
7746
+ },
7747
+ {
7748
+ requirement: "viewer proof surface",
7749
+ pass: (0, node_fs_1.existsSync)(viewerAppPath),
7750
+ evidence: `viewer app ${(0, node_fs_1.existsSync)(viewerAppPath) ? "present" : "missing"}; ${generatedReports.length} generated report(s) loaded`,
7751
+ action: "Run kage viewer after refresh so dashboard reports are generated for review.",
7752
+ },
7753
+ {
7754
+ requirement: "handoff governance",
7755
+ pass: handoff.totals.open_items === 0 && quality.totals.pending === 0,
7756
+ evidence: `${handoff.totals.open_items} handoff item(s), ${quality.totals.pending} pending packet(s)`,
7757
+ action: "Clear handoff blockers, pending packets, stale memory, and duplicate candidates before merge.",
7758
+ },
7759
+ {
7760
+ requirement: "local-first storage",
7761
+ pass: (0, node_fs_1.existsSync)(packetsDir(projectDir)),
7762
+ evidence: ".agent_memory stores packets, reports, indexes, observations, and slots locally",
7763
+ action: "Keep generated indexes rebuildable and review durable packets in git.",
7764
+ },
7765
+ ];
7766
+ const memoryPillar = capabilityPillar("memory", "Repo memory", [
7767
+ checklist[0].pass,
7768
+ checklist[1].pass,
7769
+ checklist[2].pass,
7770
+ metrics.harness.validation_ok,
7771
+ ], [
7772
+ { label: "Approved memory", value: approved.length, source: ".agent_memory/packets" },
7773
+ { label: "Memory-code links", value: memoryCodeLinks, source: ".agent_memory/graph" },
7774
+ { label: "Pinned slots", value: slots.totals.pinned, source: ".agent_memory/slots" },
7775
+ { label: "Validation", value: metrics.harness.validation_ok ? "clean" : "check", source: "kage refresh" },
7776
+ ], [
7777
+ ...(!checklist[0].pass ? [checklist[0].action] : []),
7778
+ ...(!checklist[1].pass ? [checklist[1].action] : []),
7779
+ ...(!checklist[2].pass ? [checklist[2].action] : []),
7780
+ ...(!metrics.harness.validation_ok ? ["Fix validation warnings before trusting repo memory."] : []),
7781
+ ], [
7782
+ "Use kage_context for task recall; use kage_learn when an agent discovers reusable repo logic.",
7783
+ ...(quality.totals.needs_review ? ["Review low-signal packets so agents do not reuse weak memory."] : []),
7784
+ ]);
7785
+ const collaborationPillar = capabilityPillar("collaboration", "Team collaboration", [
7786
+ checklist[3].pass,
7787
+ audit.totals.total > 0,
7788
+ handoff.totals.open_items === 0,
7789
+ quality.totals.pending === 0,
7790
+ ], [
7791
+ { label: "Replay events", value: replay.totals.events, source: ".agent_memory/observations" },
7792
+ { label: "Durable candidates", value: replay.totals.durable_candidates, source: "kage replay" },
7793
+ { label: "Audit mutations", value: audit.totals.total, source: ".agent_memory/audit" },
7794
+ { label: "Handoff open items", value: handoff.totals.open_items, source: ".agent_memory/reports/handoff.json" },
7795
+ ], [
7796
+ ...(!checklist[3].pass ? [checklist[3].action] : []),
7797
+ ...(audit.totals.total === 0 ? ["No memory mutation audit yet; capture or review memory during real work."] : []),
7798
+ ...(handoff.totals.open_items ? [handoff.primary_action.summary] : []),
7799
+ ], [
7800
+ replay.totals.durable_candidates ? "Distill replay candidates into reviewable packets before handoff." : "Keep observation hooks enabled so future work becomes reviewable memory.",
7801
+ ]);
7802
+ const benchmarkPillar = capabilityPillar("benchmark", "Benchmark proof", [
7803
+ benchmark.ok,
7804
+ (0, node_fs_1.existsSync)(longMemEvalDoc),
7805
+ (0, node_fs_1.existsSync)(scaleBench),
7806
+ (0, node_fs_1.existsSync)(codingBench),
7807
+ ], [
7808
+ { label: "Local gates", value: benchmark.ok ? "pass" : "fail", source: "kage benchmark --project ." },
7809
+ { label: "Overall score", value: benchmark.overall_score, source: "benchmarkProject" },
7810
+ { label: "LongMemEval harness", value: (0, node_fs_1.existsSync)(longMemEvalDoc), source: "benchmarks/LONGMEMEVAL.md" },
7811
+ { label: "Scale harness", value: (0, node_fs_1.existsSync)(scaleBench), source: "benchmarks/scale-kage-memory.mjs" },
7812
+ { label: "Coding-memory harness", value: (0, node_fs_1.existsSync)(codingBench), source: "benchmarks/coding-memory-quality.mjs" },
7813
+ ], [
7814
+ ...(!benchmark.ok ? ["Fix failing local benchmark gates before quoting Kage readiness."] : []),
7815
+ ...(!(0, node_fs_1.existsSync)(longMemEvalDoc) ? ["Add or restore LongMemEval methodology and commands."] : []),
7816
+ ...(!(0, node_fs_1.existsSync)(scaleBench) || !(0, node_fs_1.existsSync)(codingBench) ? ["Restore packaged memory quality and scale benchmarks."] : []),
7817
+ ], [
7818
+ "Use benchmark JSON and the viewer proof ledger for performance claims; do not rely on README prose alone.",
7819
+ ]);
7820
+ const viewerPillar = capabilityPillar("dashboard_viewer", "Dashboard and viewer", [
7821
+ (0, node_fs_1.existsSync)(viewerAppPath),
7822
+ generatedReports.length >= 4,
7823
+ (0, node_fs_1.existsSync)((0, node_path_1.join)(reportsPath, "replay.json")) || replay.totals.events > 0,
7824
+ (0, node_fs_1.existsSync)((0, node_path_1.join)(reportsPath, "profile.json")) || metrics.code_graph.files > 0,
7825
+ ], [
7826
+ { label: "Viewer app", value: (0, node_fs_1.existsSync)(viewerAppPath) ? "present" : "missing", source: "mcp/viewer/app.js" },
7827
+ { label: "Generated reports", value: generatedReports.length, source: ".agent_memory/reports" },
7828
+ { label: "Replay report", value: (0, node_fs_1.existsSync)((0, node_path_1.join)(reportsPath, "replay.json")), source: ".agent_memory/reports/replay.json" },
7829
+ { label: "Code files indexed", value: metrics.code_graph.files, source: ".agent_memory/code_graph" },
7830
+ ], [
7831
+ ...(!(0, node_fs_1.existsSync)(viewerAppPath) ? ["Restore the local viewer app bundle."] : []),
7832
+ ...(generatedReports.length < 4 ? ["Run kage viewer or kage refresh to materialize dashboard reports."] : []),
7833
+ ...(!(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."] : []),
7834
+ ...(!(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."] : []),
7835
+ ], [
7836
+ "Open kage viewer before demos so dashboard, proof, memory, and replay surfaces load from current artifacts.",
7837
+ ]);
7838
+ const pillars = [memoryPillar, collaborationPillar, benchmarkPillar, viewerPillar];
7839
+ const overall = Math.round(pillars.reduce((sum, pillar) => sum + pillar.score, 0) / Math.max(1, pillars.length));
7840
+ const overallStatus = pillars.some((pillar) => pillar.status === "gap")
7841
+ ? "gap"
7842
+ : pillars.some((pillar) => pillar.status !== "ready")
7843
+ ? "watch"
7844
+ : capabilityStatus(overall);
7845
+ const nextActions = unique([
7846
+ ...pillars.flatMap((pillar) => pillar.gaps.slice(0, 2)),
7847
+ ...pillars.filter((pillar) => pillar.status !== "ready").map((pillar) => pillar.actions[0]).filter(Boolean),
7848
+ ]);
7849
+ return {
7850
+ schema_version: 1,
7851
+ project_dir: projectDir,
7852
+ generated_at: nowIso(),
7853
+ overall_score: overall,
7854
+ status: overallStatus,
7855
+ summary: `Kage memory system readiness is ${overall}/100 across repo memory, collaboration, benchmark proof, and viewer proof.`,
7856
+ pillars,
7857
+ checklist,
7858
+ next_actions: nextActions.length ? nextActions : ["Capability audit is ready. Keep refresh, benchmarks, and viewer reports current before publishing claims."],
5864
7859
  };
5865
7860
  }
5866
7861
  const DECISION_INTELLIGENCE_TYPES = new Set([
@@ -5883,7 +7878,8 @@ function decisionContextValue(packet, key) {
5883
7878
  return typeof value === "string" && value.trim() ? value.trim() : null;
5884
7879
  }
5885
7880
  function qualityScore(packet) {
5886
- const score = Number(packet.quality.score);
7881
+ const quality = packet.quality;
7882
+ const score = Number(quality?.score);
5887
7883
  return Number.isFinite(score) ? score : null;
5888
7884
  }
5889
7885
  function kageDecisionIntelligence(projectDir) {
@@ -6861,10 +8857,11 @@ function kageMetrics(projectDir) {
6861
8857
  const indexedSourceFiles = sourceFiles.filter((file) => file.parser !== "metadata");
6862
8858
  const coverage = indexManifest.coverage.indexable_files > 0 ? indexManifest.coverage.coverage_percent : percent(indexedSourceFiles.length, sourceFiles.length);
6863
8859
  const allPackets = [...loadPacketsFromDir(packetsDir(projectDir)), ...loadPacketsFromDir(pendingDir(projectDir))];
8860
+ const qualityContext = memoryQualityContext(projectDir);
6864
8861
  const qualityScores = allPackets
6865
- .map((packet) => Number(packet.quality.score ?? evaluateMemoryQuality(projectDir, packet).score))
8862
+ .map((packet) => Number((packet.quality ?? {}).score ?? evaluateMemoryQuality(projectDir, packet, qualityContext).score))
6866
8863
  .filter((score) => Number.isFinite(score));
6867
- const duplicatePairs = allPackets.reduce((sum, packet) => sum + duplicateCandidates(projectDir, packet).length, 0);
8864
+ const duplicatePairs = allPackets.reduce((sum, packet) => sum + duplicateCandidatesWithContext(packet, qualityContext).length, 0);
6868
8865
  const indexedSourceTokens = Math.ceil(sourceFiles.reduce((sum, file) => sum + file.size_bytes, 0) / 4);
6869
8866
  const memoryTokens = allPackets.reduce((sum, packet) => sum + estimateTokens(packetText(packet)), 0);
6870
8867
  // Estimated size of a typical recall response: structured packet summaries + code graph
@@ -6882,6 +8879,7 @@ function kageMetrics(projectDir) {
6882
8879
  validation.warnings.length * 2)));
6883
8880
  const quality = qualityReport(projectDir);
6884
8881
  const benchmark = benchmarkProject(projectDir, { codeGraph, knowledgeGraph });
8882
+ const access = kageMemoryAccess(projectDir);
6885
8883
  return {
6886
8884
  schema_version: 1,
6887
8885
  project_dir: projectDir,
@@ -6935,6 +8933,7 @@ function kageMetrics(projectDir) {
6935
8933
  estimated_recall_context_tokens: recallContextTokens,
6936
8934
  estimated_tokens_saved_per_recall: tokensSaved,
6937
8935
  },
8936
+ memory_access: access.totals,
6938
8937
  harness: {
6939
8938
  policy_installed: policyInstalled,
6940
8939
  validation_ok: validation.ok,
@@ -7165,10 +9164,11 @@ function memoryInbox(projectDir) {
7165
9164
  }
7166
9165
  function qualityReport(projectDir) {
7167
9166
  ensureMemoryDirs(projectDir);
7168
- const packets = [...loadPacketsFromDir(packetsDir(projectDir)), ...loadPacketsFromDir(pendingDir(projectDir))];
9167
+ const context = memoryQualityContext(projectDir);
9168
+ const packets = context.packets;
7169
9169
  const rows = packets.map((packet) => {
7170
- const quality = evaluateMemoryQuality(projectDir, packet);
7171
- const classification = classifyPacket(projectDir, packet);
9170
+ const quality = evaluateMemoryQuality(projectDir, packet, context);
9171
+ const classification = classifyPacket(projectDir, packet, context, quality);
7172
9172
  return {
7173
9173
  id: packet.id,
7174
9174
  title: packet.title,
@@ -7183,11 +9183,11 @@ function qualityReport(projectDir) {
7183
9183
  });
7184
9184
  const active = packets.filter((packet) => packet.status === "approved" || packet.status === "pending");
7185
9185
  const staleWrong = packets.reduce((sum, packet) => {
7186
- const q = packet.quality;
9186
+ const q = (packet.quality ?? {});
7187
9187
  return sum + Number(q.votes_down ?? 0) + Number(q.reports_stale ?? 0);
7188
9188
  }, 0);
7189
9189
  const feedbackTotal = packets.reduce((sum, packet) => {
7190
- const q = packet.quality;
9190
+ const q = (packet.quality ?? {});
7191
9191
  return sum + Number(q.votes_up ?? 0) + Number(q.votes_down ?? 0) + Number(q.reports_stale ?? 0);
7192
9192
  }, 0);
7193
9193
  const withEvidence = active.filter((packet) => packet.source_refs.length > 0).length;
@@ -7229,7 +9229,7 @@ function benchmarkProject(projectDir, inputs = {}) {
7229
9229
  { query: "what changed on this branch", expected: "branch" },
7230
9230
  { query: "what gotchas exist", expected: "gotcha" },
7231
9231
  ].map((scenario) => {
7232
- const result = recall(projectDir, scenario.query, 5, true, { codeGraph, knowledgeGraph });
9232
+ const result = recall(projectDir, scenario.query, 5, true, { codeGraph, knowledgeGraph, trackAccess: false });
7233
9233
  const text = `${result.context_block}\n${result.results.map((entry) => packetText(entry.packet)).join("\n")}`.toLowerCase();
7234
9234
  return {
7235
9235
  query: scenario.query,
@@ -7300,6 +9300,519 @@ function benchmarkProject(projectDir, inputs = {}) {
7300
9300
  },
7301
9301
  };
7302
9302
  }
9303
+ function benchmarkCodingMemoryQuality(options = {}) {
9304
+ const topK = Math.max(1, Math.floor(options.topK ?? 10));
9305
+ const metricsK = unique([5, 10, 20, topK].filter((value) => Number.isFinite(value) && value > 0)).sort((a, b) => a - b);
9306
+ const packetsPerTopic = Math.max(1, Math.floor(options.packetsPerTopic ?? 5));
9307
+ const distractorsPerTopic = Math.max(0, Math.floor(options.distractorsPerTopic ?? 7));
9308
+ const runDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "kage-coding-memory-quality-"));
9309
+ const projectDir = (0, node_path_1.join)(runDir, "project");
9310
+ const startedAt = Date.now();
9311
+ const { observations, queries } = codingMemoryQualityDataset(packetsPerTopic, distractorsPerTopic);
9312
+ writeCodingMemoryQualityProject(projectDir, observations);
9313
+ const refreshStarted = Date.now();
9314
+ refreshProject(projectDir);
9315
+ const refreshMs = Date.now() - refreshStarted;
9316
+ const perQuery = queries.map((query) => {
9317
+ const recallLimit = Math.max(...metricsK);
9318
+ const started = Date.now();
9319
+ const recalled = recall(projectDir, query.query, recallLimit, true, { trackAccess: false });
9320
+ const latencyMs = Date.now() - started;
9321
+ const relevant = new Set(query.relevant_packet_ids);
9322
+ const retrieved = recalled.results.map((result, index) => ({
9323
+ rank: index + 1,
9324
+ packet_id: result.packet.id,
9325
+ title: result.packet.title,
9326
+ score: result.score,
9327
+ }));
9328
+ return {
9329
+ query: query.query,
9330
+ category: query.category,
9331
+ description: query.description,
9332
+ relevant_count: relevant.size,
9333
+ retrieved,
9334
+ latency_ms: latencyMs,
9335
+ context_tokens: estimateTokens(recalled.context_block),
9336
+ recall: Object.fromEntries(metricsK.map((k) => [`at_${k}`, roundDecimal(codingRecallAt(retrieved, relevant, k) * 100, 2)])),
9337
+ precision_at_5_percent: roundDecimal(codingPrecisionAt(retrieved, relevant, 5) * 100, 2),
9338
+ ndcg_at_10: roundDecimal(codingNdcgAt(retrieved, relevant, 10), 4),
9339
+ mrr: roundDecimal(codingMrr(retrieved, relevant), 4),
9340
+ };
9341
+ });
9342
+ const sourceDiversity = codingMemorySourceDiversityProbe((0, node_path_1.join)(runDir, "source-diversity"));
9343
+ const allMemoryTokens = estimateTokens(loadApprovedPackets(projectDir).map(packetText).join("\n\n"));
9344
+ const averageContextTokens = Math.round(averageNumber(perQuery.map((item) => item.context_tokens)));
9345
+ const recallByK = Object.fromEntries(metricsK.map((k) => [`recall_at_${k}_percent`, roundDecimal(averageNumber(perQuery.map((item) => item.recall[`at_${k}`] ?? 0)), 2)]));
9346
+ const summary = {
9347
+ benchmark: "Kage coding memory quality",
9348
+ retrieval_mode: "kage-recall-default",
9349
+ packets: observations.length,
9350
+ queries: queries.length,
9351
+ top_k: topK,
9352
+ refresh_ms: refreshMs,
9353
+ ...recallByK,
9354
+ recall_at_k_percent: Number(recallByK[`recall_at_${topK}_percent`] ?? 0),
9355
+ precision_at_5_percent: roundDecimal(averageNumber(perQuery.map((item) => item.precision_at_5_percent)), 2),
9356
+ ndcg_at_10: roundDecimal(averageNumber(perQuery.map((item) => item.ndcg_at_10)), 4),
9357
+ mrr: roundDecimal(averageNumber(perQuery.map((item) => item.mrr)), 4),
9358
+ median_latency_ms: percentileNumber(perQuery.map((item) => item.latency_ms), 0.5),
9359
+ p95_latency_ms: percentileNumber(perQuery.map((item) => item.latency_ms), 0.95),
9360
+ all_memory_tokens: allMemoryTokens,
9361
+ average_context_tokens: averageContextTokens,
9362
+ context_reduction_percent: roundDecimal(((allMemoryTokens - averageContextTokens) / Math.max(1, allMemoryTokens)) * 100, 2),
9363
+ source_diversity_pass: sourceDiversity.pass,
9364
+ source_diversity_unique_sources: sourceDiversity.unique_sources,
9365
+ source_diversity_max_results_from_one_source: sourceDiversity.max_results_from_one_source,
9366
+ };
9367
+ const report = {
9368
+ schema_version: 1,
9369
+ benchmark: "Kage coding memory quality",
9370
+ generated_at: nowIso(),
9371
+ dataset: {
9372
+ observations: observations.length,
9373
+ queries: queries.length,
9374
+ packets_per_topic: packetsPerTopic,
9375
+ distractors_per_topic: distractorsPerTopic,
9376
+ categories: countByKey(queries, (item) => item.category),
9377
+ },
9378
+ top_k: topK,
9379
+ metrics_k: metricsK,
9380
+ duration_ms: Date.now() - startedAt,
9381
+ workdir: options.keep ? projectDir : null,
9382
+ summary,
9383
+ source_diversity: sourceDiversity,
9384
+ by_category: codingQualityByCategory(perQuery, metricsK),
9385
+ per_query: perQuery,
9386
+ baselines: {
9387
+ load_all_memory: {
9388
+ context_tokens: allMemoryTokens,
9389
+ note: "Upper-bound context cost if every memory packet is loaded instead of retrieved.",
9390
+ },
9391
+ kage_recall: {
9392
+ average_context_tokens: averageContextTokens,
9393
+ context_reduction_percent: summary.context_reduction_percent,
9394
+ },
9395
+ },
9396
+ caveats: [
9397
+ "This is a reproducible synthetic coding-memory quality benchmark, not an academic benchmark.",
9398
+ "The corpus is labeled with durable repo learnings, issue causes, runbooks, and decisions across sessions.",
9399
+ "Recall@K measures whether Kage retrieves the labeled memory packets, not whether an LLM answers correctly.",
9400
+ "Use LongMemEval-S for external long-term memory retrieval; use this harness to track coding-agent memory regressions.",
9401
+ ],
9402
+ };
9403
+ if (!options.keep)
9404
+ (0, node_fs_1.rmSync)(runDir, { recursive: true, force: true });
9405
+ return report;
9406
+ }
9407
+ function codingMemorySourceDiversityProbe(projectDir) {
9408
+ const query = "checkout retry idempotency session diversity";
9409
+ const topK = 4;
9410
+ const packetDir = packetsDir(projectDir);
9411
+ (0, node_fs_1.mkdirSync)(packetDir, { recursive: true });
9412
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(projectDir, "package.json"), JSON.stringify({ name: "kage-source-diversity", scripts: { test: "vitest" } }, null, 2));
9413
+ const now = "2026-05-18T00:00:00.000Z";
9414
+ const packets = [
9415
+ ["diversity:noise:a", "A checkout retry diversity note", "noisy-session"],
9416
+ ["diversity:noise:b", "B checkout retry diversity note", "noisy-session"],
9417
+ ["diversity:noise:c", "C checkout retry diversity note", "noisy-session"],
9418
+ ["diversity:noise:d", "D checkout retry diversity note", "noisy-session"],
9419
+ ["diversity:independent:z", "Z checkout retry independent note", "independent-session"],
9420
+ ];
9421
+ for (const [id, title, sessionId] of packets) {
9422
+ const packet = {
9423
+ schema_version: exports.PACKET_SCHEMA_VERSION,
9424
+ id,
9425
+ title,
9426
+ summary: "Checkout retry idempotency session diversity memory.",
9427
+ body: "Checkout retry idempotency session diversity behavior must include independent session knowledge when one live-agent session produces many similar memories.",
9428
+ type: "bug_fix",
9429
+ scope: "repo",
9430
+ visibility: "team",
9431
+ sensitivity: "internal",
9432
+ status: "approved",
9433
+ confidence: 0.7,
9434
+ tags: ["coding-memory-quality", "source-diversity", "checkout", "retry", "idempotency"],
9435
+ paths: ["src/checkout-retry.ts"],
9436
+ stack: [],
9437
+ source_refs: [{ kind: "observation_session", session_id: sessionId, captured_at: now }],
9438
+ context: {
9439
+ fact: "Recall should include independent session knowledge when one observed session is noisy.",
9440
+ verification: "Synthetic source-diversity benchmark packet.",
9441
+ },
9442
+ freshness: { ttl_days: 365, last_verified_at: now, verification: "synthetic_source_diversity" },
9443
+ edges: [],
9444
+ quality: {
9445
+ reviewer: "benchmark-harness",
9446
+ votes_up: 0,
9447
+ votes_down: 0,
9448
+ uses_30d: 0,
9449
+ reports_stale: 0,
9450
+ review_boundary: "external_benchmark",
9451
+ promotion_requires_review: true,
9452
+ },
9453
+ created_at: now,
9454
+ updated_at: now,
9455
+ };
9456
+ writeJson((0, node_path_1.join)(packetDir, `${slugify(id)}.json`), packet);
9457
+ }
9458
+ refreshProject(projectDir);
9459
+ const recalled = recall(projectDir, query, topK, false, { trackAccess: false });
9460
+ const retrieved = recalled.results.map((result, index) => {
9461
+ const source = recallDiversitySource(result.packet) ?? "unknown";
9462
+ return {
9463
+ rank: index + 1,
9464
+ packet_id: result.packet.id,
9465
+ title: result.packet.title,
9466
+ source,
9467
+ };
9468
+ });
9469
+ const sourceCounts = countByKey(retrieved, (item) => item.source);
9470
+ const maxResultsFromOneSource = Math.max(0, ...Object.values(sourceCounts));
9471
+ const independentRank = retrieved.find((item) => item.source === "session:independent-session")?.rank ?? null;
9472
+ return {
9473
+ query,
9474
+ top_k: topK,
9475
+ max_results_from_one_source: maxResultsFromOneSource,
9476
+ unique_sources: Object.keys(sourceCounts).length,
9477
+ independent_source_rank: independentRank,
9478
+ pass: maxResultsFromOneSource <= 3 && independentRank !== null && independentRank <= topK,
9479
+ retrieved,
9480
+ };
9481
+ }
9482
+ const MEMORY_SCALE_QUERIES = [
9483
+ { query: "How did we set up OAuth providers?", topic: "oauth providers" },
9484
+ { query: "What was the N+1 query fix?", topic: "n+1 query fix" },
9485
+ { query: "PostgreSQL full-text search setup", topic: "postgres full text search" },
9486
+ { query: "bcrypt password hashing configuration", topic: "bcrypt password hashing" },
9487
+ { query: "Vitest unit testing setup", topic: "vitest unit testing" },
9488
+ { query: "webhook retry exponential backoff", topic: "webhook retry backoff" },
9489
+ { query: "ESLint flat config migration", topic: "eslint flat config" },
9490
+ { query: "Kubernetes HPA autoscaling configuration", topic: "kubernetes hpa autoscaling" },
9491
+ { query: "Prisma database seed script", topic: "prisma seed script" },
9492
+ { query: "API cursor-based pagination", topic: "cursor pagination api" },
9493
+ { query: "CSRF protection double-submit cookie", topic: "csrf double submit cookie" },
9494
+ { query: "blue-green deployment rollback", topic: "blue green rollback" },
9495
+ ];
9496
+ function benchmarkMemoryScale(options = {}) {
9497
+ const sizes = (options.sizes && options.sizes.length ? options.sizes : [240, 1000, 5000])
9498
+ .map((value) => Math.floor(Number(value)))
9499
+ .filter((value) => Number.isFinite(value) && value > 0);
9500
+ const normalizedSizes = unique(sizes.length ? sizes : [240, 1000, 5000]).sort((a, b) => a - b);
9501
+ const topK = Math.max(1, Math.floor(options.topK ?? 10));
9502
+ const runDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "kage-scale-memory-"));
9503
+ const startedAt = Date.now();
9504
+ const results = [];
9505
+ for (const size of normalizedSizes) {
9506
+ const projectDir = (0, node_path_1.join)(runDir, `size-${size}`);
9507
+ writeMemoryScaleProject(projectDir, size);
9508
+ const refreshStarted = Date.now();
9509
+ refreshProject(projectDir);
9510
+ const refreshMs = Date.now() - refreshStarted;
9511
+ const queries = MEMORY_SCALE_QUERIES.map((item) => {
9512
+ const started = Date.now();
9513
+ const recalled = recall(projectDir, item.query, topK, false, { trackAccess: false });
9514
+ const latencyMs = Date.now() - started;
9515
+ const topic = item.topic.toLowerCase();
9516
+ const hitRank = recalled.results.findIndex((result) => packetText(result.packet).toLowerCase().includes(topic));
9517
+ return {
9518
+ query: item.query,
9519
+ topic: item.topic,
9520
+ hit: hitRank >= 0,
9521
+ rank: hitRank >= 0 ? hitRank + 1 : null,
9522
+ latency_ms: latencyMs,
9523
+ context_tokens: estimateTokens(recalled.context_block),
9524
+ };
9525
+ });
9526
+ const allMemoryTokens = estimateTokens(loadApprovedPackets(projectDir).map(packetText).join("\n\n"));
9527
+ const averageContextTokens = Math.round(averageNumber(queries.map((item) => item.context_tokens)));
9528
+ results.push({
9529
+ packets: size,
9530
+ refresh_ms: refreshMs,
9531
+ recall_hit_rate_percent: roundDecimal((queries.filter((item) => item.hit).length / queries.length) * 100, 2),
9532
+ median_recall_latency_ms: percentileNumber(queries.map((item) => item.latency_ms), 0.5),
9533
+ p95_recall_latency_ms: percentileNumber(queries.map((item) => item.latency_ms), 0.95),
9534
+ all_memory_tokens: allMemoryTokens,
9535
+ average_context_tokens: averageContextTokens,
9536
+ context_reduction_percent: roundDecimal(((allMemoryTokens - averageContextTokens) / Math.max(1, allMemoryTokens)) * 100, 2),
9537
+ queries,
9538
+ });
9539
+ }
9540
+ const largest = results.at(-1);
9541
+ const report = {
9542
+ schema_version: 1,
9543
+ benchmark: "Kage synthetic memory scale",
9544
+ generated_at: nowIso(),
9545
+ sizes: normalizedSizes,
9546
+ top_k: topK,
9547
+ duration_ms: Date.now() - startedAt,
9548
+ workdir: options.keep ? runDir : null,
9549
+ summary: {
9550
+ benchmark: "Kage synthetic memory scale",
9551
+ largest_packets: largest?.packets ?? 0,
9552
+ largest_hit_rate_percent: largest?.recall_hit_rate_percent ?? 0,
9553
+ largest_median_recall_latency_ms: largest?.median_recall_latency_ms ?? 0,
9554
+ largest_context_reduction_percent: largest?.context_reduction_percent ?? 0,
9555
+ },
9556
+ results,
9557
+ caveats: [
9558
+ "This is a synthetic repo-memory scale benchmark, not an academic benchmark.",
9559
+ "Packets are generated as approved repo-local memories and indexed with Kage refresh.",
9560
+ "Recall hit rate checks whether the expected topic appears in the top-k returned packets.",
9561
+ "Context reduction compares loading all generated memory text with Kage's returned recall context.",
9562
+ ],
9563
+ };
9564
+ if (!options.keep)
9565
+ (0, node_fs_1.rmSync)(runDir, { recursive: true, force: true });
9566
+ return report;
9567
+ }
9568
+ function writeMemoryScaleProject(projectDir, count) {
9569
+ ensureDir((0, node_path_1.join)(projectDir, ".agent_memory", "packets"));
9570
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(projectDir, "package.json"), JSON.stringify({ name: "kage-scale-bench", scripts: { test: "vitest" } }), "utf8");
9571
+ const now = "2026-05-17T00:00:00.000Z";
9572
+ for (let index = 0; index < count; index += 1) {
9573
+ const queryTopic = MEMORY_SCALE_QUERIES[index % MEMORY_SCALE_QUERIES.length].topic;
9574
+ const module = `src/module-${String(index % 120).padStart(3, "0")}.ts`;
9575
+ const packet = {
9576
+ schema_version: 2,
9577
+ id: `scale:packet:${index}`,
9578
+ title: `Session ${String(index).padStart(5, "0")} memory for ${queryTopic}`,
9579
+ summary: `Reusable repo learning about ${queryTopic}.`,
9580
+ 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.`,
9581
+ type: index % 5 === 0 ? "runbook" : index % 5 === 1 ? "bug_fix" : index % 5 === 2 ? "decision" : index % 5 === 3 ? "workflow" : "code_explanation",
9582
+ scope: "repo",
9583
+ visibility: "team",
9584
+ sensitivity: "internal",
9585
+ status: "approved",
9586
+ confidence: 0.7,
9587
+ tags: ["scale-benchmark", slugify(queryTopic), `session-${Math.floor(index / 8)}`],
9588
+ paths: [module],
9589
+ stack: [],
9590
+ source_refs: [{ kind: "external_benchmark", captured_at: now }],
9591
+ context: {
9592
+ fact: `Reusable repo learning about ${queryTopic}.`,
9593
+ trigger: itemScaleTrigger(queryTopic),
9594
+ action: `Recall this before editing ${module}.`,
9595
+ },
9596
+ freshness: { ttl_days: 365, last_verified_at: now, verification: "synthetic_scale_benchmark" },
9597
+ edges: [],
9598
+ quality: {
9599
+ reviewer: "benchmark-harness",
9600
+ votes_up: 0,
9601
+ votes_down: 0,
9602
+ uses_30d: 0,
9603
+ reports_stale: 0,
9604
+ review_boundary: "external_benchmark",
9605
+ promotion_requires_review: true,
9606
+ },
9607
+ created_at: now,
9608
+ updated_at: now,
9609
+ };
9610
+ writeJson((0, node_path_1.join)(projectDir, ".agent_memory", "packets", `${String(index).padStart(6, "0")}-${slugify(queryTopic)}.json`), packet);
9611
+ }
9612
+ }
9613
+ function itemScaleTrigger(topic) {
9614
+ return `Recall when asked about ${topic}.`;
9615
+ }
9616
+ function codingMemoryQualityDataset(targetCount, distractorCount) {
9617
+ const topics = [
9618
+ 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."),
9619
+ 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."),
9620
+ codingTopic("test-db-isolation", "runbook", "test database isolation", ["tests", "database", "transactions", "isolation"], "Integration tests isolate database state with transaction rollback and a fresh seed."),
9621
+ 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."),
9622
+ 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."),
9623
+ 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."),
9624
+ 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."),
9625
+ 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."),
9626
+ 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."),
9627
+ 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."),
9628
+ 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."),
9629
+ 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."),
9630
+ 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."),
9631
+ codingTopic("background-job-idempotency", "semantic", "background job idempotency", ["jobs", "idempotency", "queue", "retry"], "Background jobs store idempotency keys so retries do not duplicate side effects."),
9632
+ 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."),
9633
+ 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."),
9634
+ 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."),
9635
+ codingTopic("secret-env-validation", "runbook", "environment secret validation", ["env", "secrets", "validation", "startup"], "Startup validates required env names without logging secret values."),
9636
+ 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."),
9637
+ 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."),
9638
+ ];
9639
+ const observations = [];
9640
+ let index = 0;
9641
+ for (const item of topics) {
9642
+ for (let variant = 0; variant < targetCount; variant += 1)
9643
+ observations.push(codingObservation(index++, item, true, variant));
9644
+ for (let variant = 0; variant < distractorCount; variant += 1) {
9645
+ const neighbor = topics[(topics.indexOf(item) + variant + 3) % topics.length];
9646
+ observations.push(codingDistractor(index++, item, neighbor, variant));
9647
+ }
9648
+ }
9649
+ const queries = topics.map((item) => ({
9650
+ query: item.query,
9651
+ category: item.category,
9652
+ description: `Retrieve durable repo memory for ${item.query}.`,
9653
+ relevant_packet_ids: observations.filter((obs) => obs.topic === item.id && obs.target).map((obs) => obs.id),
9654
+ }));
9655
+ return { observations, queries };
9656
+ }
9657
+ function codingTopic(id, category, query, concepts, lesson) {
9658
+ return { id, category, query, concepts, lesson };
9659
+ }
9660
+ function codingObservation(index, item, target, variant) {
9661
+ const file = codingFileForTopic(item.id, variant);
9662
+ return {
9663
+ id: `coding-memory:${String(index).padStart(4, "0")}:${target ? "target" : "near"}:${item.id}`,
9664
+ session_id: `session-${String(Math.floor(index / 8)).padStart(3, "0")}`,
9665
+ topic: item.id,
9666
+ target,
9667
+ category: item.category,
9668
+ title: `${titleCase(item.query)} repo memory ${variant + 1}`,
9669
+ summary: `Reusable learning about ${item.query}: ${item.lesson}`,
9670
+ body: [
9671
+ `During a real agent session, this durable repo learning was captured for ${item.query}.`,
9672
+ item.lesson,
9673
+ `Concepts: ${item.concepts.join(", ")}.`,
9674
+ `When touching ${file}, recall this before refactoring, debugging, or changing tests.`,
9675
+ `Verification path: inspect ${file} and run the focused tests for ${item.concepts[0]}.`,
9676
+ ].join(" "),
9677
+ concepts: item.concepts,
9678
+ file,
9679
+ };
9680
+ }
9681
+ function codingDistractor(index, item, neighbor, variant) {
9682
+ const file = `src/shared-${variant % 5}.ts`;
9683
+ const concepts = unique([item.concepts[0], neighbor.concepts[0], "maintenance", "repo-context"]);
9684
+ return {
9685
+ id: `coding-memory:${String(index).padStart(4, "0")}:distractor:${item.id}`,
9686
+ session_id: `session-${String(Math.floor(index / 8)).padStart(3, "0")}`,
9687
+ topic: `distractor-${item.id}-${variant}`,
9688
+ target: false,
9689
+ category: "semantic",
9690
+ title: `Shared maintenance note ${variant + 1}`,
9691
+ summary: "Nearby repo context that shares broad vocabulary but is not the labeled durable learning.",
9692
+ body: [
9693
+ "This packet is intentionally adjacent context for the coding-memory benchmark.",
9694
+ `It mentions broad areas like ${concepts.join(", ")} without carrying the specific reusable lesson.`,
9695
+ `When editing ${file}, use this as background only; it is not the target memory for a focused recall query.`,
9696
+ ].join(" "),
9697
+ concepts,
9698
+ file,
9699
+ };
9700
+ }
9701
+ function writeCodingMemoryQualityProject(projectDir, observations) {
9702
+ const packetDir = packetsDir(projectDir);
9703
+ (0, node_fs_1.mkdirSync)(packetDir, { recursive: true });
9704
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(projectDir, "src"), { recursive: true });
9705
+ (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));
9706
+ const now = "2026-05-18T00:00:00.000Z";
9707
+ for (const obs of observations) {
9708
+ const packet = {
9709
+ schema_version: exports.PACKET_SCHEMA_VERSION,
9710
+ id: obs.id,
9711
+ title: obs.title,
9712
+ summary: obs.summary,
9713
+ body: obs.body,
9714
+ type: codingTypeForCategory(obs.category),
9715
+ scope: "repo",
9716
+ visibility: "team",
9717
+ sensitivity: "internal",
9718
+ status: "approved",
9719
+ confidence: 0.7,
9720
+ tags: ["coding-memory-quality", obs.category, obs.topic, ...obs.concepts],
9721
+ paths: [obs.file],
9722
+ stack: [],
9723
+ source_refs: [{ kind: "external_benchmark", captured_at: now }],
9724
+ context: {
9725
+ fact: obs.summary,
9726
+ verification: `Synthetic labeled coding-memory benchmark packet for ${obs.topic}.`,
9727
+ },
9728
+ freshness: { ttl_days: 365, last_verified_at: now, verification: "synthetic_coding_memory_quality" },
9729
+ edges: [],
9730
+ quality: {
9731
+ reviewer: "benchmark-harness",
9732
+ votes_up: 0,
9733
+ votes_down: 0,
9734
+ uses_30d: 0,
9735
+ reports_stale: 0,
9736
+ review_boundary: "external_benchmark",
9737
+ promotion_requires_review: true,
9738
+ },
9739
+ created_at: now,
9740
+ updated_at: now,
9741
+ };
9742
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(packetDir, `${slugify(obs.id)}.json`), JSON.stringify(packet, null, 2));
9743
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(projectDir, obs.file), `export const topic = ${JSON.stringify(obs.topic)};\n`, "utf8");
9744
+ }
9745
+ }
9746
+ function codingQualityByCategory(perQuery, metricsK) {
9747
+ const groups = new Map();
9748
+ for (const item of perQuery)
9749
+ groups.set(item.category, [...(groups.get(item.category) ?? []), item]);
9750
+ return Array.from(groups.entries()).map(([category, rows]) => ({
9751
+ category,
9752
+ queries: rows.length,
9753
+ ...Object.fromEntries(metricsK.map((k) => [`recall_at_${k}_percent`, roundDecimal(averageNumber(rows.map((item) => item.recall[`at_${k}`] ?? 0)), 2)])),
9754
+ ndcg_at_10: roundDecimal(averageNumber(rows.map((item) => item.ndcg_at_10)), 4),
9755
+ mrr: roundDecimal(averageNumber(rows.map((item) => item.mrr)), 4),
9756
+ }));
9757
+ }
9758
+ function codingRecallAt(retrieved, relevant, k) {
9759
+ if (!relevant.size)
9760
+ return 0;
9761
+ return retrieved.slice(0, k).filter((item) => relevant.has(item.packet_id)).length / relevant.size;
9762
+ }
9763
+ function codingPrecisionAt(retrieved, relevant, k) {
9764
+ const rows = retrieved.slice(0, k);
9765
+ return rows.length ? rows.filter((item) => relevant.has(item.packet_id)).length / rows.length : 0;
9766
+ }
9767
+ function codingNdcgAt(retrieved, relevant, k) {
9768
+ const dcg = retrieved.slice(0, k).reduce((sum, item, index) => sum + (relevant.has(item.packet_id) ? 1 / Math.log2(index + 2) : 0), 0);
9769
+ const idealHits = Math.min(relevant.size, k);
9770
+ let ideal = 0;
9771
+ for (let index = 0; index < idealHits; index += 1)
9772
+ ideal += 1 / Math.log2(index + 2);
9773
+ return ideal ? dcg / ideal : 0;
9774
+ }
9775
+ function codingMrr(retrieved, relevant) {
9776
+ const index = retrieved.findIndex((item) => relevant.has(item.packet_id));
9777
+ return index >= 0 ? 1 / (index + 1) : 0;
9778
+ }
9779
+ function codingTypeForCategory(category) {
9780
+ if (category === "runbook")
9781
+ return "runbook";
9782
+ if (category === "decision")
9783
+ return "decision";
9784
+ if (category === "cross-session")
9785
+ return "bug_fix";
9786
+ return "code_explanation";
9787
+ }
9788
+ function codingFileForTopic(topic, variant) {
9789
+ return `src/${slugify(topic)}-${variant % 3}.ts`;
9790
+ }
9791
+ function averageNumber(values) {
9792
+ return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
9793
+ }
9794
+ function percentileNumber(values, p) {
9795
+ if (!values.length)
9796
+ return 0;
9797
+ const sorted = values.slice().sort((a, b) => a - b);
9798
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * p) - 1));
9799
+ return sorted[index];
9800
+ }
9801
+ function roundDecimal(value, digits = 2) {
9802
+ const factor = 10 ** digits;
9803
+ return Math.round(value * factor) / factor;
9804
+ }
9805
+ function countByKey(rows, fn) {
9806
+ const counts = {};
9807
+ for (const row of rows) {
9808
+ const key = fn(row);
9809
+ counts[key] = (counts[key] ?? 0) + 1;
9810
+ }
9811
+ return counts;
9812
+ }
9813
+ function titleCase(value) {
9814
+ return value.replace(/\b[a-z]/g, (match) => match.toUpperCase());
9815
+ }
7303
9816
  function baselineDiscoveryFiles(projectDir, task) {
7304
9817
  const terms = tokenize(task);
7305
9818
  const graph = buildCodeGraph(projectDir);
@@ -7624,6 +10137,8 @@ function capture(input) {
7624
10137
  freshness: {
7625
10138
  ttl_days: 365,
7626
10139
  last_verified_at: createdAt,
10140
+ path_fingerprints: memoryPathFingerprints(input.projectDir, input.paths ?? []),
10141
+ path_fingerprint_policy: "source_hash_staleness",
7627
10142
  verification: "repo_local_agent_capture",
7628
10143
  },
7629
10144
  edges: [],
@@ -7647,6 +10162,12 @@ function capture(input) {
7647
10162
  ...evaluateMemoryQuality(input.projectDir, packet),
7648
10163
  };
7649
10164
  const path = writePacket(input.projectDir, packet, "packets");
10165
+ recordMemoryAudit(input.projectDir, "capture", [packet], {
10166
+ type: packet.type,
10167
+ status: packet.status,
10168
+ path: (0, node_path_1.relative)(input.projectDir, path),
10169
+ source_kind: packet.source_refs[0]?.kind ?? "explicit_capture",
10170
+ });
7650
10171
  return { ok: true, packet, path, errors: [] };
7651
10172
  }
7652
10173
  function createPublicCandidate(projectDir, id) {
@@ -7862,7 +10383,7 @@ fi
7862
10383
  KAGE_MSG="$POLICY" python3 -c "import json,os; print(json.dumps({'systemMessage': os.environ['KAGE_MSG']}))"
7863
10384
  `;
7864
10385
  const stopHookScript = `#!/usr/bin/env bash
7865
- # Kage Stop hook — best-effort repo memory refresh before Claude Code finishes.
10386
+ # Kage Stop hook — refreshes repo memory and blocks final handoff when linked memory needs agent reconciliation.
7866
10387
  # Silent if Kage is not initialized in the current project or no git changes exist.
7867
10388
  set -euo pipefail
7868
10389
 
@@ -7875,6 +10396,145 @@ command -v kage >/dev/null 2>&1 || exit 0
7875
10396
  if git -C "$CWD" status --porcelain -uall >/dev/null 2>&1 && [[ -n "$(git -C "$CWD" status --porcelain -uall)" ]]; then
7876
10397
  kage refresh --project "$CWD" --json >/dev/null 2>&1 || true
7877
10398
  kage pr summarize --project "$CWD" --json >/dev/null 2>&1 || true
10399
+ RECONCILE_OUTPUT="$(kage reconcile --project "$CWD" --json 2>/dev/null || true)"
10400
+ RECONCILE_UNRESOLVED="$(printf "%s" "$RECONCILE_OUTPUT" | python3 -c 'import json, sys
10401
+ try:
10402
+ d = json.load(sys.stdin)
10403
+ except Exception:
10404
+ d = {}
10405
+ print(int(d.get("unresolved_count") or 0))
10406
+ ' 2>/dev/null || echo "0")"
10407
+ if [[ "$RECONCILE_UNRESOLVED" != "0" ]]; then
10408
+ printf "%s" "$RECONCILE_OUTPUT" | python3 -c 'import json, sys
10409
+ try:
10410
+ d = json.load(sys.stdin)
10411
+ except Exception:
10412
+ d = {}
10413
+ print(d.get("agent_instruction") or "Kage memory reconciliation required before final response.")
10414
+ ' >&2
10415
+ exit 2
10416
+ fi
10417
+ fi
10418
+
10419
+ exit 0
10420
+ `;
10421
+ const observeHookScript = `#!/usr/bin/env bash
10422
+ # Kage Observe hook — captures durable Claude Code session signals and recalls repo memory.
10423
+ # Silent if Kage is not initialized in the current project.
10424
+ set -euo pipefail
10425
+
10426
+ PAYLOAD="$(cat || true)"
10427
+ CWD="$(PAYLOAD="$PAYLOAD" python3 -c 'import json, os
10428
+ try:
10429
+ d = json.loads(os.environ.get("PAYLOAD") or "{}")
10430
+ except Exception:
10431
+ d = {}
10432
+ print(d.get("cwd") or os.environ.get("CLAUDE_PROJECT_DIR") or "")
10433
+ ' 2>/dev/null || echo "")"
10434
+
10435
+ [[ -d "$CWD/.agent_memory" ]] || exit 0
10436
+ command -v kage >/dev/null 2>&1 || exit 0
10437
+
10438
+ EVENT="$(PAYLOAD="$PAYLOAD" python3 -c 'import json, os
10439
+ try:
10440
+ d = json.loads(os.environ.get("PAYLOAD") or "{}")
10441
+ except Exception:
10442
+ d = {}
10443
+ print(d.get("hook_event_name") or d.get("event") or "")
10444
+ ' 2>/dev/null || echo "")"
10445
+
10446
+ SESSION="$(PAYLOAD="$PAYLOAD" python3 -c 'import json, os
10447
+ try:
10448
+ d = json.loads(os.environ.get("PAYLOAD") or "{}")
10449
+ except Exception:
10450
+ d = {}
10451
+ print(d.get("session_id") or d.get("sessionId") or "default")
10452
+ ' 2>/dev/null || echo "default")"
10453
+
10454
+ OBSERVATION="$(PAYLOAD="$PAYLOAD" python3 -c 'import json, os
10455
+ try:
10456
+ d = json.loads(os.environ.get("PAYLOAD") or "{}")
10457
+ except Exception:
10458
+ d = {}
10459
+
10460
+ def first(*values):
10461
+ for value in values:
10462
+ if isinstance(value, str) and value.strip():
10463
+ return value.strip()
10464
+ return ""
10465
+
10466
+ def compact(value, limit=1200):
10467
+ if isinstance(value, (dict, list)):
10468
+ text = json.dumps(value, sort_keys=True)
10469
+ elif value is None:
10470
+ text = ""
10471
+ else:
10472
+ text = str(value)
10473
+ text = " ".join(text.split())
10474
+ return text[:limit]
10475
+
10476
+ event_name = first(d.get("hook_event_name"), d.get("event"))
10477
+ session_id = first(d.get("session_id"), d.get("sessionId"), "default")
10478
+ agent = first(d.get("agent"), "claude-code")
10479
+ tool = first(d.get("tool_name"), d.get("toolName"))
10480
+ tool_input = d.get("tool_input") or d.get("toolInput") or {}
10481
+ tool_response = d.get("tool_response") or d.get("toolResponse") or d.get("result") or {}
10482
+ prompt = first(d.get("prompt"), d.get("user_prompt"), d.get("message"))
10483
+ path = ""
10484
+ command = ""
10485
+ if isinstance(tool_input, dict):
10486
+ path = first(tool_input.get("file_path"), tool_input.get("path"), tool_input.get("notebook_path"))
10487
+ command = first(tool_input.get("command"))
10488
+
10489
+ if event_name == "UserPromptSubmit":
10490
+ payload = {"type": "user_prompt", "text": prompt, "summary": compact(prompt, 240)}
10491
+ elif event_name == "PostToolUseFailure":
10492
+ 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)}
10493
+ elif event_name == "PostToolUse":
10494
+ obs_type = "file_change" if path else ("command_result" if command else "tool_use")
10495
+ 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)}
10496
+ elif event_name == "PreCompact":
10497
+ payload = {"type": "session_end", "summary": "Claude Code is compacting context; distill durable observations before compaction."}
10498
+ elif event_name == "SessionEnd":
10499
+ payload = {"type": "session_end", "summary": "Claude Code session ended; distill durable observations for teammate handoff."}
10500
+ else:
10501
+ payload = {"type": "tool_use", "tool": tool, "path": path, "command": command, "summary": compact(d, 320), "text": compact(d)}
10502
+
10503
+ payload.update({"session_id": session_id, "agent": agent})
10504
+ print(json.dumps(payload, separators=(",", ":")))
10505
+ ' 2>/dev/null || echo "")"
10506
+
10507
+ if [[ -n "$OBSERVATION" ]]; then
10508
+ kage observe --project "$CWD" --event "$OBSERVATION" --json >/dev/null 2>&1 || true
10509
+ fi
10510
+
10511
+ if [[ "$EVENT" == "PreCompact" || "$EVENT" == "SessionEnd" ]]; then
10512
+ kage distill --project "$CWD" --session "$SESSION" --json >/dev/null 2>&1 || true
10513
+ fi
10514
+
10515
+ if [[ "$EVENT" == "UserPromptSubmit" ]]; then
10516
+ QUERY="$(PAYLOAD="$PAYLOAD" python3 -c 'import json, os
10517
+ try:
10518
+ d = json.loads(os.environ.get("PAYLOAD") or "{}")
10519
+ except Exception:
10520
+ d = {}
10521
+ print((d.get("prompt") or d.get("user_prompt") or d.get("message") or "")[:1000])
10522
+ ' 2>/dev/null || echo "")"
10523
+ if [[ -n "$QUERY" ]]; then
10524
+ CONTEXT="$(kage recall "$QUERY" --project "$CWD" --json 2>/dev/null | python3 -c 'import json, sys
10525
+ try:
10526
+ d = json.load(sys.stdin)
10527
+ except Exception:
10528
+ d = {}
10529
+ text = d.get("context_block") or ""
10530
+ print(text[:6000] if d.get("results") else "")
10531
+ ' 2>/dev/null || true)"
10532
+ if [[ -n "$CONTEXT" ]]; then
10533
+ KAGE_CONTEXT="$CONTEXT" python3 -c 'import json, os
10534
+ print(json.dumps({"additionalContext": os.environ.get("KAGE_CONTEXT", "")}))
10535
+ '
10536
+ fi
10537
+ fi
7878
10538
  fi
7879
10539
 
7880
10540
  exit 0
@@ -7883,13 +10543,18 @@ exit 0
7883
10543
  const hookEntry = {
7884
10544
  hooks: {
7885
10545
  SessionStart: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/session-start.sh", timeout: 5 }] }],
10546
+ UserPromptSubmit: [{ hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 12 }] }],
10547
+ PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 5 }] }],
10548
+ PostToolUseFailure: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 5 }] }],
10549
+ PreCompact: [{ hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 20 }] }],
7886
10550
  Stop: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/stop.sh", timeout: 20 }] }],
10551
+ SessionEnd: [{ hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 20 }] }],
7887
10552
  },
7888
10553
  };
7889
10554
  setSnippet(path, JSON.stringify({ mcpServers: { kage: server } }, null, 2), [
7890
10555
  "Add the MCP server to ~/.claude.json, then restart Claude Code.",
7891
10556
  "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.`,
10557
+ `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
10558
  "Run `kage init --project <repo>` inside each repo to install the ambient memory policy.",
7894
10559
  ], true);
7895
10560
  if (options.write) {
@@ -7897,6 +10562,7 @@ exit 0
7897
10562
  // Install the ambient session-start hook
7898
10563
  (0, node_fs_1.mkdirSync)(hookDir, { recursive: true });
7899
10564
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(hookDir, "session-start.sh"), hookScript, { mode: 0o755 });
10565
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(hookDir, "observe.sh"), observeHookScript, { mode: 0o755 });
7900
10566
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(hookDir, "stop.sh"), stopHookScript, { mode: 0o755 });
7901
10567
  upsertJsonSettings(settingsPath, hookEntry);
7902
10568
  result.wrote = true;
@@ -7914,7 +10580,7 @@ exit 0
7914
10580
  if (agent === "aider") {
7915
10581
  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
10582
  "Run `kage daemon start --project <repo>`.",
7917
- "Use REST endpoints `/kage/recall`, `/kage/observe`, and `/kage/distill` from Aider scripts.",
10583
+ "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
10584
  ]);
7919
10585
  return result;
7920
10586
  }
@@ -7996,14 +10662,20 @@ function upsertTomlMcpBlock(text, block) {
7996
10662
  }
7997
10663
  return `${out.join("\n").trimEnd()}\n`;
7998
10664
  }
7999
- function setupDoctor(projectDir) {
10665
+ function setupDoctor(projectDir, options = {}) {
8000
10666
  return exports.SETUP_AGENTS.map((agent) => {
8001
- const setup = setupAgent(agent, projectDir);
10667
+ const setup = setupAgent(agent, projectDir, { homeDir: options.homeDir, serverPath: options.serverPath });
10668
+ const hookSummary = agent === "claude-code"
10669
+ ? claudeAmbientHookSummary(options.homeDir ?? process.env.HOME ?? "~")
10670
+ : undefined;
10671
+ const configPresent = Boolean(setup.config_path && (0, node_fs_1.existsSync)(setup.config_path));
10672
+ const configured = configPresent && (!hookSummary || hookSummary.ready);
8002
10673
  return {
8003
10674
  agent,
8004
- configured: Boolean(setup.config_path && (0, node_fs_1.existsSync)(setup.config_path)),
10675
+ configured,
8005
10676
  config_path: setup.config_path,
8006
10677
  notes: setup.instructions,
10678
+ hook_summary: hookSummary,
8007
10679
  };
8008
10680
  });
8009
10681
  }
@@ -8013,6 +10685,45 @@ function configMentionsKage(path) {
8013
10685
  const text = (0, node_fs_1.readFileSync)(path, "utf8");
8014
10686
  return /\bkage\b/.test(text) && /(mcp|mcpServers|mcp_servers)/i.test(text);
8015
10687
  }
10688
+ const CLAUDE_AMBIENT_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PostToolUse", "PostToolUseFailure", "PreCompact", "Stop", "SessionEnd"];
10689
+ function claudeHookEventConfigured(settings, event) {
10690
+ const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks)
10691
+ ? settings.hooks
10692
+ : {};
10693
+ const entry = hooks[event];
10694
+ if (!Array.isArray(entry) || !entry.length)
10695
+ return false;
10696
+ const text = JSON.stringify(entry);
10697
+ if (event === "SessionStart")
10698
+ return text.includes("session-start.sh");
10699
+ if (event === "Stop")
10700
+ return text.includes("stop.sh");
10701
+ return text.includes("observe.sh");
10702
+ }
10703
+ function claudeAmbientHookSummary(homeDir) {
10704
+ const settingsPath = (0, node_path_1.join)(homeDir, ".claude", "settings.json");
10705
+ const hookDir = (0, node_path_1.join)(homeDir, ".claude", "kage", "hooks");
10706
+ 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")];
10707
+ let settings = {};
10708
+ if ((0, node_fs_1.existsSync)(settingsPath)) {
10709
+ const parsed = readJson(settingsPath);
10710
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
10711
+ settings = parsed;
10712
+ }
10713
+ const installed = CLAUDE_AMBIENT_HOOK_EVENTS.filter((event) => claudeHookEventConfigured(settings, event));
10714
+ const missing = CLAUDE_AMBIENT_HOOK_EVENTS.filter((event) => !installed.includes(event));
10715
+ for (const scriptPath of scriptPaths) {
10716
+ if (!(0, node_fs_1.existsSync)(scriptPath))
10717
+ missing.push((0, node_path_1.basename)(scriptPath));
10718
+ }
10719
+ return {
10720
+ required: [...CLAUDE_AMBIENT_HOOK_EVENTS],
10721
+ installed,
10722
+ missing: unique(missing),
10723
+ script_paths: scriptPaths,
10724
+ ready: missing.length === 0,
10725
+ };
10726
+ }
8016
10727
  function verifyAgentActivation(agent, projectDir, options = {}) {
8017
10728
  if (!exports.SETUP_AGENTS.includes(agent))
8018
10729
  throw new Error(`Unsupported agent: ${agent}`);
@@ -8022,7 +10733,7 @@ function verifyAgentActivation(agent, projectDir, options = {}) {
8022
10733
  const refreshed = indexProject(projectDir);
8023
10734
  const policyPath = (0, node_path_1.join)(projectDir, "AGENTS.md");
8024
10735
  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"];
10736
+ const requiredIndexes = ["catalog.json", "by-path.json", "by-tag.json", "by-type.json", "vector-local.json", "graph.json", "code-graph.json"];
8026
10737
  const indexSet = new Set(refreshed.indexes.map((path) => (0, node_path_1.basename)(path)));
8027
10738
  const indexesPresent = requiredIndexes.every((name) => indexSet.has(name));
8028
10739
  const recallResult = recall(projectDir, "kage setup repo memory code graph", 3, true);
@@ -8030,6 +10741,10 @@ function verifyAgentActivation(agent, projectDir, options = {}) {
8030
10741
  const recallWorks = recallResult.context_block.includes("Kage Context");
8031
10742
  const codeGraphWorks = codeGraph.files.length > 0;
8032
10743
  const mcpToolReachable = Boolean(options.mcpToolReachable);
10744
+ const hookSummary = agent === "claude-code"
10745
+ ? claudeAmbientHookSummary(options.homeDir ?? process.env.HOME ?? "~")
10746
+ : { required: [], installed: [], missing: [], script_paths: [], ready: true };
10747
+ const ambientHooksPresent = hookSummary.ready;
8033
10748
  const warnings = [];
8034
10749
  const nextSteps = [];
8035
10750
  if (!configPresent) {
@@ -8048,11 +10763,15 @@ function verifyAgentActivation(agent, projectDir, options = {}) {
8048
10763
  warnings.push("Generated indexes are missing or incomplete.");
8049
10764
  nextSteps.push(`Run: kage index --project ${projectDir}`);
8050
10765
  }
10766
+ if (!ambientHooksPresent && agent === "claude-code") {
10767
+ warnings.push(`Claude Code ambient memory hooks are incomplete: missing ${hookSummary.missing.join(", ")}.`);
10768
+ nextSteps.push(`Run: kage setup claude-code --project ${projectDir} --write`);
10769
+ }
8051
10770
  if (!mcpToolReachable) {
8052
10771
  warnings.push("This CLI can verify config, policy, recall, and code graph, but cannot prove the current agent session loaded the MCP server.");
8053
10772
  nextSteps.push(`Restart ${agent}, then ask it to call kage_verify_agent or list MCP tools.`);
8054
10773
  }
8055
- const status = !configPresent || !configHasKage ? "needs_setup" :
10774
+ const status = !configPresent || !configHasKage || !ambientHooksPresent ? "needs_setup" :
8056
10775
  !indexesPresent || !recallWorks || !codeGraphWorks ? "needs_index" :
8057
10776
  !mcpToolReachable ? "restart_required" :
8058
10777
  "ready";
@@ -8068,7 +10787,9 @@ function verifyAgentActivation(agent, projectDir, options = {}) {
8068
10787
  recall_works: recallWorks,
8069
10788
  code_graph_works: codeGraphWorks,
8070
10789
  mcp_tool_reachable: mcpToolReachable,
10790
+ ambient_hooks_present: ambientHooksPresent,
8071
10791
  },
10792
+ hook_summary: agent === "claude-code" ? hookSummary : undefined,
8072
10793
  config_path: setup.config_path,
8073
10794
  recall_preview: recallResult.results[0]?.packet.title ?? "No matching memory packet; recall surface is still reachable.",
8074
10795
  code_graph_summary: `${codeGraph.files.length} files, ${codeGraph.symbols.length} symbols, ${codeGraph.calls.length} calls, ${codeGraph.tests.length} tests`,
@@ -8264,6 +10985,177 @@ function reusablePromptObservation(event) {
8264
10985
  return "";
8265
10986
  return text;
8266
10987
  }
10988
+ function kageSessionCaptureReport(projectDir) {
10989
+ ensureMemoryDirs(projectDir);
10990
+ const observations = loadObservations(projectDir);
10991
+ const knownCommands = knownRepoCommands(projectDir);
10992
+ const bySession = new Map();
10993
+ for (const observation of observations) {
10994
+ const rows = bySession.get(observation.session_id) ?? [];
10995
+ rows.push(observation);
10996
+ bySession.set(observation.session_id, rows);
10997
+ }
10998
+ const sessions = Array.from(bySession.entries()).map(([sessionId, rows]) => {
10999
+ const sorted = rows.slice().sort((a, b) => a.timestamp.localeCompare(b.timestamp));
11000
+ const commandCandidates = sorted.filter((event) => event.type === "command_result" && reusableCommandObservation(event, knownCommands));
11001
+ const fileCandidates = sorted.filter((event) => event.type === "file_change" && reusableFileObservation(event));
11002
+ const promptCandidates = sorted.filter((event) => event.type === "user_prompt" && reusablePromptObservation(event));
11003
+ const candidateTypes = unique([
11004
+ ...(commandCandidates.length ? ["runbook"] : []),
11005
+ ...(fileCandidates.length ? ["workflow"] : []),
11006
+ ...(promptCandidates.length ? ["decision/context"] : []),
11007
+ ]);
11008
+ const durable = commandCandidates.length + fileCandidates.length + promptCandidates.length;
11009
+ return {
11010
+ session_id: sessionId,
11011
+ first_at: sorted[0]?.timestamp ?? "",
11012
+ last_at: sorted.at(-1)?.timestamp ?? "",
11013
+ observations: sorted.length,
11014
+ durable_observations: durable,
11015
+ agents: unique(sorted.map((event) => event.agent).filter(Boolean)),
11016
+ event_type_counts: countBy(sorted, (event) => event.type),
11017
+ commands: unique(sorted.map((event) => event.command).filter(Boolean)).slice(0, 8),
11018
+ paths: unique(sorted.map((event) => event.path).filter(Boolean)).slice(0, 12),
11019
+ candidate_types: candidateTypes,
11020
+ next_action: durable > 0
11021
+ ? `Run kage distill --project . --session ${sessionId} and review the generated packets.`
11022
+ : "No durable memory candidate yet; keep this as local telemetry only.",
11023
+ };
11024
+ }).sort((a, b) => b.last_at.localeCompare(a.last_at));
11025
+ return {
11026
+ schema_version: 1,
11027
+ project_dir: projectDir,
11028
+ generated_at: nowIso(),
11029
+ totals: {
11030
+ sessions: sessions.length,
11031
+ observations: observations.length,
11032
+ sessions_with_candidates: sessions.filter((session) => session.durable_observations > 0).length,
11033
+ durable_observations: sessions.reduce((sum, session) => sum + session.durable_observations, 0),
11034
+ },
11035
+ event_type_counts: countBy(observations, (event) => event.type),
11036
+ sessions,
11037
+ 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.",
11038
+ };
11039
+ }
11040
+ function observationCandidate(projectDir, event) {
11041
+ if (event.type === "command_result" && reusableCommandObservation(event, knownRepoCommands(projectDir))) {
11042
+ return { durable: true, type: "runbook" };
11043
+ }
11044
+ if (event.type === "file_change" && reusableFileObservation(event)) {
11045
+ return { durable: true, type: "workflow" };
11046
+ }
11047
+ if (event.type === "user_prompt" && reusablePromptObservation(event)) {
11048
+ return { durable: true, type: "decision/context" };
11049
+ }
11050
+ return { durable: false };
11051
+ }
11052
+ function observationLabel(event) {
11053
+ if (event.type === "command_result")
11054
+ return `Command${typeof event.exit_code === "number" ? ` exit ${event.exit_code}` : ""}`;
11055
+ if (event.type === "file_change")
11056
+ return event.path ? `File change: ${event.path}` : "File change";
11057
+ if (event.type === "tool_use")
11058
+ return event.tool ? `Tool use: ${event.tool}` : "Tool use";
11059
+ if (event.type === "tool_result")
11060
+ return event.tool ? `Tool result: ${event.tool}` : "Tool result";
11061
+ if (event.type === "test_result")
11062
+ return `Test result${typeof event.exit_code === "number" ? ` exit ${event.exit_code}` : ""}`;
11063
+ if (event.type === "user_prompt")
11064
+ return "User prompt";
11065
+ if (event.type === "session_start")
11066
+ return "Session started";
11067
+ if (event.type === "session_end")
11068
+ return "Session ended";
11069
+ return event.type;
11070
+ }
11071
+ function observationDigestSummary(event) {
11072
+ if (event.summary?.trim())
11073
+ return summarize(event.summary.trim()).slice(0, 220);
11074
+ if (event.type === "command_result" && event.command) {
11075
+ return `Command ${event.command} completed${typeof event.exit_code === "number" ? ` with exit ${event.exit_code}` : ""}.`;
11076
+ }
11077
+ if (event.type === "file_change" && event.path)
11078
+ return `Changed ${event.path}.`;
11079
+ if ((event.type === "tool_use" || event.type === "tool_result") && event.tool)
11080
+ return `${observationLabel(event)}.`;
11081
+ if (event.type === "test_result" && event.command)
11082
+ return `Test command ${event.command} completed${typeof event.exit_code === "number" ? ` with exit ${event.exit_code}` : ""}.`;
11083
+ return observationLabel(event);
11084
+ }
11085
+ function kageSessionReplay(projectDir, options = {}) {
11086
+ ensureMemoryDirs(projectDir);
11087
+ const limit = Math.max(1, Math.min(1000, Math.floor(options.limit ?? 200)));
11088
+ const observations = loadObservations(projectDir, options.sessionId);
11089
+ const bySession = new Map();
11090
+ for (const observation of observations) {
11091
+ const rows = bySession.get(observation.session_id) ?? [];
11092
+ rows.push(observation);
11093
+ bySession.set(observation.session_id, rows);
11094
+ }
11095
+ const sessions = Array.from(bySession.entries()).map(([sessionId, rows]) => {
11096
+ const sorted = rows.slice().sort((a, b) => a.timestamp.localeCompare(b.timestamp));
11097
+ const candidates = sorted.map((event) => observationCandidate(projectDir, event)).filter((candidate) => candidate.durable);
11098
+ return {
11099
+ session_id: sessionId,
11100
+ first_at: sorted[0]?.timestamp ?? "",
11101
+ last_at: sorted.at(-1)?.timestamp ?? "",
11102
+ events: sorted.length,
11103
+ durable_candidates: candidates.length,
11104
+ agents: unique(sorted.map((event) => event.agent).filter(Boolean)),
11105
+ event_type_counts: countBy(sorted, (event) => event.type),
11106
+ commands: unique(sorted.map((event) => event.command).filter(Boolean)).slice(0, 8),
11107
+ paths: unique(sorted.map((event) => event.path).filter(Boolean)).slice(0, 12),
11108
+ tools: unique(sorted.map((event) => event.tool).filter(Boolean)).slice(0, 8),
11109
+ distill_command: `kage distill --project . --session ${sessionId}`,
11110
+ };
11111
+ }).sort((a, b) => b.last_at.localeCompare(a.last_at));
11112
+ const firstTimestampBySession = new Map();
11113
+ for (const [sessionId, rows] of bySession.entries()) {
11114
+ const first = rows.slice().sort((a, b) => a.timestamp.localeCompare(b.timestamp))[0]?.timestamp;
11115
+ firstTimestampBySession.set(sessionId, first ? Date.parse(first) : 0);
11116
+ }
11117
+ const events = observations.slice(0, limit).map((event, index) => {
11118
+ const candidate = observationCandidate(projectDir, event);
11119
+ const first = firstTimestampBySession.get(event.session_id) ?? Date.parse(event.timestamp);
11120
+ const current = Date.parse(event.timestamp);
11121
+ return {
11122
+ index,
11123
+ timestamp: event.timestamp,
11124
+ offset_ms: Number.isFinite(current - first) ? Math.max(0, current - first) : 0,
11125
+ session_id: event.session_id,
11126
+ type: event.type,
11127
+ ...(event.agent ? { agent: event.agent } : {}),
11128
+ label: observationLabel(event),
11129
+ summary: observationDigestSummary(event),
11130
+ ...(event.tool ? { tool: event.tool } : {}),
11131
+ ...(event.path ? { path: event.path } : {}),
11132
+ ...(event.command ? { command: event.command } : {}),
11133
+ ...(typeof event.exit_code === "number" ? { exit_code: event.exit_code } : {}),
11134
+ durable_candidate: candidate.durable,
11135
+ ...(candidate.type ? { candidate_type: candidate.type } : {}),
11136
+ raw_text_included: false,
11137
+ sensitive_redacted: false,
11138
+ };
11139
+ });
11140
+ const durableCandidates = events.filter((event) => event.durable_candidate).length;
11141
+ return {
11142
+ schema_version: 1,
11143
+ project_dir: projectDir,
11144
+ generated_at: nowIso(),
11145
+ ...(options.sessionId ? { selected_session_id: options.sessionId } : {}),
11146
+ totals: {
11147
+ sessions: sessions.length,
11148
+ events: observations.length,
11149
+ durable_candidates: durableCandidates,
11150
+ },
11151
+ sessions,
11152
+ events,
11153
+ 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.",
11154
+ next_action: durableCandidates > 0
11155
+ ? "Run the listed distill command for sessions with durable candidates, then review the generated memory packets before sharing."
11156
+ : "No durable candidates in this digest yet; keep observing or capture reusable learnings with kage learn.",
11157
+ };
11158
+ }
8267
11159
  function distillSession(projectDir, sessionId) {
8268
11160
  const observations = loadObservations(projectDir, sessionId);
8269
11161
  const candidates = [];
@@ -8431,6 +11323,8 @@ function createDiffChangeMemory(projectDir, summary) {
8431
11323
  freshness: {
8432
11324
  last_verified_at: now,
8433
11325
  ttl_days: 180,
11326
+ path_fingerprints: memoryPathFingerprints(projectDir, summary.changed_files.slice(0, 40)),
11327
+ path_fingerprint_policy: "source_hash_staleness",
8434
11328
  verification: "git_diff",
8435
11329
  },
8436
11330
  edges: summary.changed_files.slice(0, 20).map((file) => ({
@@ -8641,6 +11535,8 @@ function prCheck(projectDir) {
8641
11535
  .filter((path) => path.startsWith(".agent_memory/packets/") && path.endsWith(".json"))).sort();
8642
11536
  const codeGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/code_graph/graph.json", { head: overlay.head, tree, inputHash: codeInputHash });
8643
11537
  const memoryGraphCurrent = graphIsCurrent(projectDir, ".agent_memory/graph/graph.json", { head: overlay.head, tree, inputHash: memoryInputHash });
11538
+ const sessions = kageSessionCaptureReport(projectDir);
11539
+ const reconciliation = kageMemoryReconciliation(projectDir);
8644
11540
  const errors = [...validation.errors];
8645
11541
  const warnings = [...validation.warnings];
8646
11542
  const requiredActions = [];
@@ -8648,10 +11544,19 @@ function prCheck(projectDir) {
8648
11544
  errors.push(`${stalePackets.length} stale memory packet(s) require update, verification, or supersession.`);
8649
11545
  requiredActions.push("Run kage refresh, then update or supersede stale packets.");
8650
11546
  }
11547
+ if (reconciliation.unresolved_count > 0) {
11548
+ errors.push(`${reconciliation.unresolved_count} memory reconciliation item(s) require agent update or supersession.`);
11549
+ requiredActions.push(...reconciliation.items.slice(0, 5).map((item) => item.next_action));
11550
+ }
8651
11551
  if (!codeGraphCurrent || !memoryGraphCurrent) {
8652
11552
  errors.push("Generated graph artifacts are missing or not current for this working tree content.");
8653
11553
  requiredActions.push("Run kage refresh --project <dir> before merge.");
8654
11554
  }
11555
+ const distillableSessions = sessions.sessions.filter((session) => session.durable_observations > 0);
11556
+ if (distillableSessions.length) {
11557
+ errors.push(`${distillableSessions.length} distillable session learning${distillableSessions.length === 1 ? "" : "s"} require review before merge.`);
11558
+ requiredActions.push(...distillableSessions.slice(0, 5).map((session) => session.next_action));
11559
+ }
8655
11560
  if (!memoryPacketChanges.length && overlay.changed_files.some((path) => !path.startsWith(".agent_memory/"))) {
8656
11561
  warnings.push("No repo memory packet changed for this branch. If durable knowledge was learned, run kage propose --from-diff or kage learn.");
8657
11562
  }
@@ -8668,6 +11573,7 @@ function prCheck(projectDir) {
8668
11573
  memory_packet_changes: memoryPacketChanges,
8669
11574
  code_graph_current: codeGraphCurrent,
8670
11575
  memory_graph_current: memoryGraphCurrent,
11576
+ memory_reconciliation: reconciliation,
8671
11577
  errors,
8672
11578
  warnings,
8673
11579
  required_actions: requiredActions,
@@ -9152,7 +12058,7 @@ function recordFeedback(projectDir, id, feedback) {
9152
12058
  const packet = readJson(path);
9153
12059
  if (packet.id !== id)
9154
12060
  continue;
9155
- const quality = packet.quality;
12061
+ const quality = (packet.quality ?? {});
9156
12062
  const increment = (key) => {
9157
12063
  quality[key] = Number(quality[key] ?? 0) + 1;
9158
12064
  };
@@ -9171,6 +12077,10 @@ function recordFeedback(projectDir, id, feedback) {
9171
12077
  };
9172
12078
  }
9173
12079
  writeJson(path, packet);
12080
+ recordMemoryAudit(projectDir, "feedback", [packet], {
12081
+ feedback,
12082
+ path: (0, node_path_1.relative)(projectDir, path),
12083
+ });
9174
12084
  buildIndexes(projectDir);
9175
12085
  return { ok: true, packet, path, errors: [] };
9176
12086
  }
@@ -9180,6 +12090,7 @@ function validateProject(projectDir) {
9180
12090
  ensureMemoryDirs(projectDir);
9181
12091
  const errors = [];
9182
12092
  const warnings = [];
12093
+ const qualityContext = memoryQualityContext(projectDir);
9183
12094
  for (const [dir, label] of [
9184
12095
  [packetsDir(projectDir), "packet"],
9185
12096
  [pendingDir(projectDir), "pending"],
@@ -9194,7 +12105,7 @@ function validateProject(projectDir) {
9194
12105
  const activeMemory = packet.status === "approved" || packet.status === "pending";
9195
12106
  if (activeMemory) {
9196
12107
  warnings.push(...packetGroundingWarnings(projectDir, packet, (0, node_path_1.relative)(projectDir, packetPath)));
9197
- const quality = evaluateMemoryQuality(projectDir, packet);
12108
+ const quality = evaluateMemoryQuality(projectDir, packet, qualityContext);
9198
12109
  if (Number(quality.score) < 55)
9199
12110
  warnings.push(`${(0, node_path_1.relative)(projectDir, packetPath)}: low memory quality score ${quality.score}`);
9200
12111
  const duplicates = quality.duplicate_candidates;
@@ -9298,7 +12209,7 @@ function initProject(projectDir) {
9298
12209
  }
9299
12210
  function doctorProject(projectDir) {
9300
12211
  ensureMemoryDirs(projectDir);
9301
- const expectedIndexes = ["catalog.json", "by-path.json", "by-tag.json", "by-type.json", "graph.json", "code-graph.json"];
12212
+ const expectedIndexes = ["catalog.json", "by-path.json", "by-tag.json", "by-type.json", "vector-local.json", "graph.json", "code-graph.json"];
9302
12213
  const present = expectedIndexes.filter((name) => (0, node_fs_1.existsSync)((0, node_path_1.join)(indexesDir(projectDir), name)));
9303
12214
  const missing = expectedIndexes.filter((name) => !present.includes(name));
9304
12215
  const validation = validateProject(projectDir);
@@ -9331,6 +12242,10 @@ function approvePending(projectDir, id) {
9331
12242
  const target = (0, node_path_1.join)(packetsDir(projectDir), packetFileName(packet));
9332
12243
  writeJson(target, packet);
9333
12244
  (0, node_fs_1.renameSync)(path, `${path}.approved`);
12245
+ recordMemoryAudit(projectDir, "approve", [packet], {
12246
+ from: (0, node_path_1.relative)(projectDir, path),
12247
+ to: (0, node_path_1.relative)(projectDir, target),
12248
+ });
9334
12249
  buildIndexes(projectDir);
9335
12250
  return target;
9336
12251
  }
@@ -9344,6 +12259,10 @@ function rejectPending(projectDir, id) {
9344
12259
  if (packet.id === id) {
9345
12260
  const target = `${path}.rejected`;
9346
12261
  (0, node_fs_1.renameSync)(path, target);
12262
+ recordMemoryAudit(projectDir, "reject", [packet], {
12263
+ from: (0, node_path_1.relative)(projectDir, path),
12264
+ to: (0, node_path_1.relative)(projectDir, target),
12265
+ });
9347
12266
  return target;
9348
12267
  }
9349
12268
  }
@@ -9390,3 +12309,275 @@ function changelog(projectDir, days = 7) {
9390
12309
  total: added.length + updated.length + deprecated.length,
9391
12310
  };
9392
12311
  }
12312
+ function timelineSourceKind(packet) {
12313
+ const first = packet.source_refs[0];
12314
+ const kind = first && typeof first.kind === "string" ? first.kind : "";
12315
+ if (kind)
12316
+ return kind;
12317
+ if (isGeneratedChangeMemory(packet))
12318
+ return "git_diff";
12319
+ return "memory_packet";
12320
+ }
12321
+ function timelineAction(kind, packet) {
12322
+ if (kind === "pending")
12323
+ return "Review this pending packet before it becomes shared repo memory.";
12324
+ if (kind === "deprecated")
12325
+ return "Check whether a newer packet supersedes this memory before relying on it.";
12326
+ if (kind === "updated")
12327
+ return "Review the latest rationale, paths, and evidence before future agents reuse it.";
12328
+ if (isGeneratedChangeMemory(packet))
12329
+ return "Use as branch handoff context; turn durable lessons into focused memory packets.";
12330
+ return "Review recent memory changes so teammates understand what agents just learned.";
12331
+ }
12332
+ function timelineEntry(kind, packet, date) {
12333
+ return {
12334
+ kind,
12335
+ packet_id: packet.id,
12336
+ title: packet.title,
12337
+ type: packet.type,
12338
+ status: packet.status,
12339
+ date,
12340
+ summary: packet.summary,
12341
+ paths: packet.paths,
12342
+ tags: packet.tags,
12343
+ source_kind: timelineSourceKind(packet),
12344
+ action: timelineAction(kind, packet),
12345
+ };
12346
+ }
12347
+ function packetEdgeValue(edge, key) {
12348
+ const value = edge[key];
12349
+ return typeof value === "string" ? value : "";
12350
+ }
12351
+ function upsertPacketEdge(packet, relation, to, evidence, at) {
12352
+ const exists = packet.edges.some((edge) => packetEdgeValue(edge, "relation") === relation && packetEdgeValue(edge, "to") === to);
12353
+ if (exists)
12354
+ return;
12355
+ packet.edges.push({
12356
+ relation,
12357
+ to,
12358
+ evidence,
12359
+ created_at: at,
12360
+ });
12361
+ }
12362
+ function packetSupersededBy(packet) {
12363
+ const qualityReplacement = packet.quality?.superseded_by;
12364
+ if (typeof qualityReplacement === "string" && qualityReplacement.trim())
12365
+ return qualityReplacement.trim();
12366
+ const freshnessReplacement = packet.freshness?.superseded_by;
12367
+ if (typeof freshnessReplacement === "string" && freshnessReplacement.trim())
12368
+ return freshnessReplacement.trim();
12369
+ const edge = packet.edges.find((item) => packetEdgeValue(item, "relation") === "superseded_by" && packetEdgeValue(item, "to"));
12370
+ return edge ? packetEdgeValue(edge, "to") : "";
12371
+ }
12372
+ function packetSupersessionReason(packet) {
12373
+ const qualityReason = packet.quality?.superseded_reason;
12374
+ if (typeof qualityReason === "string" && qualityReason.trim())
12375
+ return qualityReason.trim();
12376
+ const freshnessReason = packet.freshness?.superseded_reason;
12377
+ if (typeof freshnessReason === "string" && freshnessReason.trim())
12378
+ return freshnessReason.trim();
12379
+ const edge = packet.edges.find((item) => packetEdgeValue(item, "relation") === "superseded_by");
12380
+ const evidence = edge ? packetEdgeValue(edge, "evidence") : "";
12381
+ return evidence || "This memory was superseded by newer repo knowledge.";
12382
+ }
12383
+ function supersedeMemory(projectDir, oldPacketId, replacementPacketId, reason = "") {
12384
+ ensureMemoryDirs(projectDir);
12385
+ const trimmedReason = reason.trim() || "Newer repo memory supersedes this packet.";
12386
+ const warnings = [];
12387
+ if (oldPacketId === replacementPacketId) {
12388
+ return {
12389
+ ok: false,
12390
+ project_dir: projectDir,
12391
+ old_packet_id: oldPacketId,
12392
+ replacement_packet_id: replacementPacketId,
12393
+ reason: trimmedReason,
12394
+ errors: ["A memory packet cannot supersede itself."],
12395
+ warnings,
12396
+ };
12397
+ }
12398
+ const entries = loadPacketEntriesFromDir(packetsDir(projectDir));
12399
+ const oldEntry = entries.find((entry) => entry.packet.id === oldPacketId);
12400
+ const replacementEntry = entries.find((entry) => entry.packet.id === replacementPacketId);
12401
+ const errors = [];
12402
+ if (!oldEntry)
12403
+ errors.push(`Packet not found: ${oldPacketId}`);
12404
+ if (!replacementEntry)
12405
+ errors.push(`Replacement packet not found: ${replacementPacketId}`);
12406
+ if (errors.length) {
12407
+ return {
12408
+ ok: false,
12409
+ project_dir: projectDir,
12410
+ old_packet_id: oldPacketId,
12411
+ replacement_packet_id: replacementPacketId,
12412
+ reason: trimmedReason,
12413
+ errors,
12414
+ warnings,
12415
+ };
12416
+ }
12417
+ const oldPacket = oldEntry.packet;
12418
+ const replacementPacket = replacementEntry.packet;
12419
+ if (replacementPacket.status !== "approved") {
12420
+ warnings.push(`Replacement packet status is ${replacementPacket.status}; approved replacements are safest for recall.`);
12421
+ }
12422
+ const at = nowIso();
12423
+ oldPacket.status = "superseded";
12424
+ oldPacket.updated_at = at;
12425
+ oldPacket.quality = {
12426
+ ...oldPacket.quality,
12427
+ superseded_by: replacementPacket.id,
12428
+ superseded_reason: trimmedReason,
12429
+ };
12430
+ oldPacket.freshness = {
12431
+ ...oldPacket.freshness,
12432
+ superseded_at: at,
12433
+ superseded_by: replacementPacket.id,
12434
+ superseded_reason: trimmedReason,
12435
+ };
12436
+ upsertPacketEdge(oldPacket, "superseded_by", replacementPacket.id, trimmedReason, at);
12437
+ replacementPacket.updated_at = at;
12438
+ upsertPacketEdge(replacementPacket, "supersedes", oldPacket.id, trimmedReason, at);
12439
+ writeJson(oldEntry.path, oldPacket);
12440
+ writeJson(replacementEntry.path, replacementPacket);
12441
+ recordMemoryAudit(projectDir, "supersede", [oldPacket, replacementPacket], {
12442
+ old_packet_id: oldPacket.id,
12443
+ replacement_packet_id: replacementPacket.id,
12444
+ reason: trimmedReason,
12445
+ old_path: (0, node_path_1.relative)(projectDir, oldEntry.path),
12446
+ replacement_path: (0, node_path_1.relative)(projectDir, replacementEntry.path),
12447
+ });
12448
+ buildIndexes(projectDir);
12449
+ return {
12450
+ ok: true,
12451
+ project_dir: projectDir,
12452
+ old_packet_id: oldPacket.id,
12453
+ replacement_packet_id: replacementPacket.id,
12454
+ reason: trimmedReason,
12455
+ old_packet: oldPacket,
12456
+ replacement_packet: replacementPacket,
12457
+ old_path: oldEntry.path,
12458
+ replacement_path: replacementEntry.path,
12459
+ errors: [],
12460
+ warnings,
12461
+ };
12462
+ }
12463
+ function kageMemoryLineage(projectDir) {
12464
+ ensureMemoryDirs(projectDir);
12465
+ const packets = loadPacketsFromDir(packetsDir(projectDir));
12466
+ const byId = new Map(packets.map((packet) => [packet.id, packet]));
12467
+ const supersededPackets = packets.filter((packet) => packet.status === "superseded" || packetSupersededBy(packet));
12468
+ const grouped = new Map();
12469
+ const orphans = [];
12470
+ for (const packet of supersededPackets) {
12471
+ const replacementId = packetSupersededBy(packet);
12472
+ if (!replacementId || !byId.has(replacementId)) {
12473
+ orphans.push({
12474
+ packet_id: packet.id,
12475
+ title: packet.title,
12476
+ status: packet.status,
12477
+ updated_at: packet.updated_at,
12478
+ reason: replacementId ? `Replacement packet is missing: ${replacementId}` : packetSupersessionReason(packet),
12479
+ action: "Add a replacement link or restore this packet only if the old memory is still correct.",
12480
+ });
12481
+ continue;
12482
+ }
12483
+ const list = grouped.get(replacementId) ?? [];
12484
+ list.push(packet);
12485
+ grouped.set(replacementId, list);
12486
+ }
12487
+ const chains = [];
12488
+ for (const [replacementId, oldPackets] of grouped) {
12489
+ const replacement = byId.get(replacementId);
12490
+ if (!replacement)
12491
+ continue;
12492
+ oldPackets.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || a.title.localeCompare(b.title));
12493
+ const paths = unique([...replacement.paths, ...oldPackets.flatMap((packet) => packet.paths)]).slice(0, 12);
12494
+ chains.push({
12495
+ current_packet_id: replacement.id,
12496
+ current_title: replacement.title,
12497
+ current_status: replacement.status,
12498
+ superseded_packet_ids: oldPackets.map((packet) => packet.id),
12499
+ superseded_titles: oldPackets.map((packet) => packet.title),
12500
+ reason: packetSupersessionReason(oldPackets[0]),
12501
+ paths,
12502
+ updated_at: [replacement.updated_at, ...oldPackets.map((packet) => packet.updated_at)].sort().at(-1) ?? replacement.updated_at,
12503
+ action: "Use the current replacement packet in recall; keep superseded packets only as audit history.",
12504
+ });
12505
+ }
12506
+ chains.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || a.current_title.localeCompare(b.current_title));
12507
+ orphans.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || a.title.localeCompare(b.title));
12508
+ const recommendations = unique([
12509
+ ...(chains.length ? ["Use current replacement packets during handoff so agents do not rely on retired memory."] : []),
12510
+ ...(orphans.length ? ["Resolve superseded memories without a replacement link before trusting old context."] : []),
12511
+ ...(!chains.length && !orphans.length ? ["No superseded memory chains yet; use kage supersede when a better packet replaces old repo knowledge."] : []),
12512
+ ]);
12513
+ return {
12514
+ schema_version: 1,
12515
+ project_dir: projectDir,
12516
+ generated_at: nowIso(),
12517
+ totals: {
12518
+ superseded: supersededPackets.length,
12519
+ chains: chains.length,
12520
+ orphans: orphans.length,
12521
+ replacements_missing: orphans.filter((item) => item.reason.startsWith("Replacement packet is missing:")).length,
12522
+ },
12523
+ chains,
12524
+ orphans,
12525
+ recommendations,
12526
+ };
12527
+ }
12528
+ function kageMemoryTimeline(projectDir, days = 14) {
12529
+ ensureMemoryDirs(projectDir);
12530
+ const boundedDays = Math.max(1, Math.min(365, Math.floor(Number(days) || 14)));
12531
+ const since = new Date(Date.now() - boundedDays * 24 * 60 * 60 * 1000);
12532
+ const sinceIso = since.toISOString();
12533
+ const packets = loadPacketsFromDir(packetsDir(projectDir));
12534
+ const pending = loadPendingPackets(projectDir);
12535
+ const entries = [];
12536
+ for (const packet of packets) {
12537
+ const createdAt = packet.created_at ?? "";
12538
+ const updatedAt = packet.updated_at ?? "";
12539
+ const isRecentlyCreated = createdAt >= sinceIso;
12540
+ const isRecentlyUpdated = updatedAt >= sinceIso && updatedAt !== createdAt;
12541
+ if (packet.status === "deprecated" || packet.status === "superseded") {
12542
+ if (isRecentlyCreated || isRecentlyUpdated)
12543
+ entries.push(timelineEntry("deprecated", packet, updatedAt || createdAt));
12544
+ }
12545
+ else if (packet.status === "approved") {
12546
+ if (isRecentlyCreated)
12547
+ entries.push(timelineEntry("added", packet, createdAt));
12548
+ else if (isRecentlyUpdated)
12549
+ entries.push(timelineEntry("updated", packet, updatedAt));
12550
+ }
12551
+ }
12552
+ for (const packet of pending) {
12553
+ const createdAt = packet.created_at ?? packet.updated_at ?? "";
12554
+ const updatedAt = packet.updated_at ?? createdAt;
12555
+ const date = updatedAt >= createdAt ? updatedAt : createdAt;
12556
+ if (date >= sinceIso)
12557
+ entries.push(timelineEntry("pending", packet, date));
12558
+ }
12559
+ entries.sort((a, b) => b.date.localeCompare(a.date) || a.title.localeCompare(b.title));
12560
+ const totals = {
12561
+ added: entries.filter((entry) => entry.kind === "added").length,
12562
+ updated: entries.filter((entry) => entry.kind === "updated").length,
12563
+ deprecated: entries.filter((entry) => entry.kind === "deprecated").length,
12564
+ pending: entries.filter((entry) => entry.kind === "pending").length,
12565
+ total: entries.length,
12566
+ };
12567
+ const recommendations = unique([
12568
+ ...(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."]),
12569
+ ...(totals.pending ? ["Approve, reject, merge, or keep pending memory before relying on it across teammates."] : []),
12570
+ ...(totals.deprecated ? ["Check deprecated or superseded memories for replacement packets before recall."] : []),
12571
+ ...(totals.updated ? ["Inspect updated memories for changed rationale, evidence, or affected paths."] : []),
12572
+ ]);
12573
+ return {
12574
+ schema_version: 1,
12575
+ project_dir: projectDir,
12576
+ generated_at: nowIso(),
12577
+ days: boundedDays,
12578
+ since: sinceIso,
12579
+ totals,
12580
+ entries,
12581
+ recommendations,
12582
+ };
12583
+ }