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