@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/README.md +101 -2
- package/dist/cli.js +429 -3
- package/dist/daemon.js +314 -7
- package/dist/index.js +329 -3
- package/dist/kernel.js +3255 -64
- package/package.json +1 -1
- package/viewer/app.js +1250 -41
- package/viewer/data.html +2 -9
- package/viewer/graph.html +2 -9
- package/viewer/index.html +2 -9
- package/viewer/intel.html +2 -9
- package/viewer/memory.html +73 -9
- package/viewer/owners.html +2 -9
- package/viewer/review.html +13 -9
- package/viewer/styles.css +582 -103
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
|
-
|
|
496
|
-
|
|
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
|
|
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
|
|
545
|
-
|
|
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 = [
|
|
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
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
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
|
|
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 =
|
|
6201
|
+
const quality = recallQualityScore(packet);
|
|
4802
6202
|
const feedback = packetFeedbackScore(packet);
|
|
4803
|
-
const vector =
|
|
4804
|
-
const
|
|
4805
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6288
|
+
const rankedScored = approvedPackets
|
|
4817
6289
|
.map((packet) => {
|
|
4818
|
-
const
|
|
4819
|
-
const
|
|
4820
|
-
const
|
|
4821
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 +
|
|
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
|
|
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 —
|
|
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
|
|
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/
|
|
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
|
|
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
|
+
}
|