@kage-core/kage-graph-mcp 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +14 -0
- package/dist/daemon.js +3 -1
- package/dist/kernel.js +139 -23
- package/package.json +1 -1
- package/viewer/console.js +638 -0
- package/viewer/index.html +284 -282
- package/viewer/app.js +0 -6782
- package/viewer/data.html +0 -296
- package/viewer/graph.html +0 -296
- package/viewer/intel.html +0 -296
- package/viewer/memory.html +0 -367
- package/viewer/owners.html +0 -296
- package/viewer/review.html +0 -307
- package/viewer/styles.css +0 -2878
package/dist/cli.js
CHANGED
|
@@ -39,6 +39,7 @@ Usage:
|
|
|
39
39
|
kage branch --project <dir> [--json]
|
|
40
40
|
kage metrics --project <dir> [--json]
|
|
41
41
|
kage memory-access --project <dir> [--json]
|
|
42
|
+
kage activity --project <dir> [--json]
|
|
42
43
|
kage memory-audit --project <dir> [--limit <n>] [--json]
|
|
43
44
|
kage slots --project <dir> [--json]
|
|
44
45
|
kage slots set --project <dir> --label <label> --content <text> [--description <text>] [--paths a,b] [--tags a,b] [--size-limit <n>] [--unpinned] [--json]
|
|
@@ -983,6 +984,19 @@ async function main() {
|
|
|
983
984
|
}
|
|
984
985
|
return;
|
|
985
986
|
}
|
|
987
|
+
if (command === "activity") {
|
|
988
|
+
const result = (0, kernel_js_1.kageActivity)(projectArg(args));
|
|
989
|
+
if (args.includes("--json")) {
|
|
990
|
+
console.log(JSON.stringify(result, null, 2));
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
console.log(`Kage activity: ${result.totals.events} events (${result.totals.recalls} recalls, ${result.totals.captures} captures); ${result.totals.recalls_7d} recalls in 7 days`);
|
|
994
|
+
console.log("\nRecent:");
|
|
995
|
+
for (const event of result.events.slice(0, 15)) {
|
|
996
|
+
console.log(`- ${event.at.slice(0, 16).replace("T", " ")} ${event.kind.padEnd(9)} ${event.title}`);
|
|
997
|
+
}
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
986
1000
|
if (command === "lifecycle" || command === "memory-lifecycle") {
|
|
987
1001
|
const result = (0, kernel_js_1.kageMemoryLifecycle)(projectArg(args));
|
|
988
1002
|
if (args.includes("--json")) {
|
package/dist/daemon.js
CHANGED
|
@@ -570,6 +570,7 @@ async function startViewer(projectDir, options = {}) {
|
|
|
570
570
|
const memoryAuditPath = (0, node_path_1.join)(reportsDir, "memory-audit.json");
|
|
571
571
|
const handoffPath = (0, node_path_1.join)(reportsDir, "handoff.json");
|
|
572
572
|
const lifecyclePath = (0, node_path_1.join)(reportsDir, "lifecycle.json");
|
|
573
|
+
const activityPath = (0, node_path_1.join)(reportsDir, "activity.json");
|
|
573
574
|
const timelinePath = (0, node_path_1.join)(reportsDir, "timeline.json");
|
|
574
575
|
const lineagePath = (0, node_path_1.join)(reportsDir, "lineage.json");
|
|
575
576
|
const setupPath = (0, node_path_1.join)(reportsDir, "setup.json");
|
|
@@ -600,6 +601,7 @@ async function startViewer(projectDir, options = {}) {
|
|
|
600
601
|
(0, node_fs_1.writeFileSync)(memoryAuditPath, JSON.stringify((0, kernel_js_1.kageMemoryAudit)(projectDir), null, 2));
|
|
601
602
|
(0, node_fs_1.writeFileSync)(handoffPath, JSON.stringify((0, kernel_js_1.kageMemoryHandoff)(projectDir), null, 2));
|
|
602
603
|
(0, node_fs_1.writeFileSync)(lifecyclePath, JSON.stringify((0, kernel_js_1.kageMemoryLifecycle)(projectDir), null, 2));
|
|
604
|
+
(0, node_fs_1.writeFileSync)(activityPath, JSON.stringify((0, kernel_js_1.kageActivity)(projectDir), null, 2));
|
|
603
605
|
(0, node_fs_1.writeFileSync)(timelinePath, JSON.stringify((0, kernel_js_1.kageMemoryTimeline)(projectDir), null, 2));
|
|
604
606
|
(0, node_fs_1.writeFileSync)(lineagePath, JSON.stringify((0, kernel_js_1.kageMemoryLineage)(projectDir), null, 2));
|
|
605
607
|
(0, node_fs_1.writeFileSync)(setupPath, JSON.stringify((0, kernel_js_1.setupDoctor)(projectDir), null, 2));
|
|
@@ -609,7 +611,7 @@ async function startViewer(projectDir, options = {}) {
|
|
|
609
611
|
catch {
|
|
610
612
|
// non-fatal: viewer will show 404 for reports if generation fails
|
|
611
613
|
}
|
|
612
|
-
const url = `http://${host}:${port}/viewer/index.html?graph=${encodeURIComponent(graphPath)}&code=${encodeURIComponent(codePath)}&metrics=${encodeURIComponent(metricsPath)}&inbox=${encodeURIComponent(inboxPath)}&review=${encodeURIComponent(reviewPath)}&pending=${encodeURIComponent(pendingDir)}&quality=${encodeURIComponent(qualityPath)}&benchmark=${encodeURIComponent(benchmarkPath)}&contributors=${encodeURIComponent(contributorsPath)}&profile=${encodeURIComponent(profilePath)}&xray=${encodeURIComponent(xrayPath)}&capabilities=${encodeURIComponent(capabilitiesPath)}&slots=${encodeURIComponent(slotsPath)}&decisions=${encodeURIComponent(decisionsPath)}&risk=${encodeURIComponent(riskPath)}&moduleHealth=${encodeURIComponent(moduleHealthPath)}&graphInsights=${encodeURIComponent(graphInsightsPath)}&workspace=${encodeURIComponent(workspacePath)}&sessions=${encodeURIComponent(sessionsPath)}&replay=${encodeURIComponent(replayPath)}&memoryAccess=${encodeURIComponent(memoryAccessPath)}&memoryAudit=${encodeURIComponent(memoryAuditPath)}&handoff=${encodeURIComponent(handoffPath)}&lifecycle=${encodeURIComponent(lifecyclePath)}&timeline=${encodeURIComponent(timelinePath)}&lineage=${encodeURIComponent(lineagePath)}&setup=${encodeURIComponent(setupPath)}&trust=${encodeURIComponent(trustPath)}&suppressed=${encodeURIComponent(suppressedPath)}&view=code`;
|
|
614
|
+
const url = `http://${host}:${port}/viewer/index.html?graph=${encodeURIComponent(graphPath)}&code=${encodeURIComponent(codePath)}&metrics=${encodeURIComponent(metricsPath)}&inbox=${encodeURIComponent(inboxPath)}&review=${encodeURIComponent(reviewPath)}&pending=${encodeURIComponent(pendingDir)}&quality=${encodeURIComponent(qualityPath)}&benchmark=${encodeURIComponent(benchmarkPath)}&contributors=${encodeURIComponent(contributorsPath)}&profile=${encodeURIComponent(profilePath)}&xray=${encodeURIComponent(xrayPath)}&capabilities=${encodeURIComponent(capabilitiesPath)}&slots=${encodeURIComponent(slotsPath)}&decisions=${encodeURIComponent(decisionsPath)}&risk=${encodeURIComponent(riskPath)}&moduleHealth=${encodeURIComponent(moduleHealthPath)}&graphInsights=${encodeURIComponent(graphInsightsPath)}&workspace=${encodeURIComponent(workspacePath)}&sessions=${encodeURIComponent(sessionsPath)}&replay=${encodeURIComponent(replayPath)}&memoryAccess=${encodeURIComponent(memoryAccessPath)}&memoryAudit=${encodeURIComponent(memoryAuditPath)}&handoff=${encodeURIComponent(handoffPath)}&lifecycle=${encodeURIComponent(lifecyclePath)}&activity=${encodeURIComponent(activityPath)}&timeline=${encodeURIComponent(timelinePath)}&lineage=${encodeURIComponent(lineagePath)}&setup=${encodeURIComponent(setupPath)}&trust=${encodeURIComponent(trustPath)}&suppressed=${encodeURIComponent(suppressedPath)}&view=code`;
|
|
613
615
|
const server = (0, node_http_1.createServer)((req, res) => {
|
|
614
616
|
const requestUrl = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
615
617
|
let filePath = null;
|
package/dist/kernel.js
CHANGED
|
@@ -62,6 +62,7 @@ exports.makePacketId = makePacketId;
|
|
|
62
62
|
exports.parseFrontmatter = parseFrontmatter;
|
|
63
63
|
exports.kageMemoryAccess = kageMemoryAccess;
|
|
64
64
|
exports.kageMemoryLifecycle = kageMemoryLifecycle;
|
|
65
|
+
exports.kageActivity = kageActivity;
|
|
65
66
|
exports.kageMemoryReconciliation = kageMemoryReconciliation;
|
|
66
67
|
exports.evaluateMemoryAdmission = evaluateMemoryAdmission;
|
|
67
68
|
exports.validatePacket = validatePacket;
|
|
@@ -873,6 +874,8 @@ function kageMemoryLifecycle(projectDir) {
|
|
|
873
874
|
const item = {
|
|
874
875
|
packet_id: packet.id,
|
|
875
876
|
title: packet.title,
|
|
877
|
+
summary: packet.summary ?? "",
|
|
878
|
+
body: packet.body ?? "",
|
|
876
879
|
type: packet.type,
|
|
877
880
|
status: packet.status,
|
|
878
881
|
health: action.health,
|
|
@@ -957,6 +960,54 @@ function recordRecallAccess(projectDir, results) {
|
|
|
957
960
|
// Recall should never fail because local access telemetry could not be updated.
|
|
958
961
|
}
|
|
959
962
|
}
|
|
963
|
+
const AUDIT_ACTIVITY_KIND = {
|
|
964
|
+
capture: "capture", approve: "capture", supersede: "supersede", deprecate: "deprecate",
|
|
965
|
+
update: "update", promote: "promote", feedback: "feedback",
|
|
966
|
+
};
|
|
967
|
+
function kageActivity(projectDir, options = {}) {
|
|
968
|
+
const limit = options.limit ?? 80;
|
|
969
|
+
const events = [];
|
|
970
|
+
let recalls = 0;
|
|
971
|
+
readMemoryAccessEntries(projectDir).forEach((entry) => {
|
|
972
|
+
(entry.recent ?? []).forEach((r) => {
|
|
973
|
+
if (!r || !r.at)
|
|
974
|
+
return;
|
|
975
|
+
recalls += 1;
|
|
976
|
+
events.push({ at: r.at, kind: "recall", title: entry.title, detail: `recalled · rank ${r.rank}` });
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
let captures = 0;
|
|
980
|
+
for (const audit of loadMemoryAuditEntries(projectDir)) {
|
|
981
|
+
const kind = AUDIT_ACTIVITY_KIND[audit.operation] ?? "other";
|
|
982
|
+
if (kind === "capture")
|
|
983
|
+
captures += 1;
|
|
984
|
+
const extra = audit.packet_titles.length > 1 ? ` (+${audit.packet_titles.length - 1} more)` : "";
|
|
985
|
+
events.push({ at: audit.timestamp, kind, title: (audit.packet_titles[0] ?? audit.operation) + extra, detail: audit.operation, actor: audit.actor });
|
|
986
|
+
}
|
|
987
|
+
events.sort((a, b) => (Date.parse(b.at) || 0) - (Date.parse(a.at) || 0));
|
|
988
|
+
const dayMap = new Map();
|
|
989
|
+
events.forEach((e) => { if (e.kind === "recall") {
|
|
990
|
+
const d = e.at.slice(0, 10);
|
|
991
|
+
dayMap.set(d, (dayMap.get(d) ?? 0) + 1);
|
|
992
|
+
} });
|
|
993
|
+
// Zero-fill the last 14 calendar days so the chart reads as a timeline, not a lone bar.
|
|
994
|
+
const daily = [];
|
|
995
|
+
for (let i = 13; i >= 0; i -= 1) {
|
|
996
|
+
const day = new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
997
|
+
daily.push({ day, recalls: dayMap.get(day) ?? 0 });
|
|
998
|
+
}
|
|
999
|
+
const cutoff7 = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
1000
|
+
const recalls7d = events.filter((e) => e.kind === "recall" && (Date.parse(e.at) || 0) >= cutoff7).length;
|
|
1001
|
+
return {
|
|
1002
|
+
schema_version: 1,
|
|
1003
|
+
project_dir: projectDir,
|
|
1004
|
+
generated_at: nowIso(),
|
|
1005
|
+
window_days: ACCESS_WINDOW_DAYS,
|
|
1006
|
+
totals: { events: events.length, recalls, captures, recalls_7d: recalls7d },
|
|
1007
|
+
daily,
|
|
1008
|
+
events: events.slice(0, limit),
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
960
1011
|
function isGeneratedChangeMemory(packet) {
|
|
961
1012
|
return packet.type === "workflow"
|
|
962
1013
|
&& packet.tags.includes("change-memory")
|
|
@@ -1122,7 +1173,7 @@ function staleMemoryReasons(projectDir, packet, fingerprintCache) {
|
|
|
1122
1173
|
if (ageDays > ttlDays)
|
|
1123
1174
|
reasons.push(`freshness ttl expired (${Math.floor(ageDays)}d old, ttl ${ttlDays}d)`);
|
|
1124
1175
|
}
|
|
1125
|
-
const paths = packet.paths.filter(meaningfulMemoryPath);
|
|
1176
|
+
const paths = packet.paths.filter((path) => meaningfulMemoryPath(path) && !isGroundingIgnored(projectDir, path));
|
|
1126
1177
|
const missingPaths = paths.filter((path) => !(0, node_fs_1.existsSync)((0, node_path_1.join)(projectDir, path)));
|
|
1127
1178
|
if (paths.length > 0 && missingPaths.length === paths.length) {
|
|
1128
1179
|
reasons.push(`all referenced paths are missing: ${missingPaths.slice(0, 4).join(", ")}`);
|
|
@@ -1133,6 +1184,7 @@ function staleMemoryReasons(projectDir, packet, fingerprintCache) {
|
|
|
1133
1184
|
if (freshness.path_fingerprint_policy === "source_hash_staleness") {
|
|
1134
1185
|
const storedFingerprints = packetStoredPathFingerprints(packet);
|
|
1135
1186
|
const changedPaths = storedFingerprints
|
|
1187
|
+
.filter((fingerprint) => !isGroundingIgnored(projectDir, fingerprint.path))
|
|
1136
1188
|
.filter((fingerprint) => (0, node_fs_1.existsSync)((0, node_path_1.join)(projectDir, fingerprint.path)))
|
|
1137
1189
|
.filter((fingerprint) => {
|
|
1138
1190
|
const current = memoryPathFingerprint(projectDir, fingerprint.path, fingerprintCache);
|
|
@@ -2341,7 +2393,7 @@ function pathExistsInRepo(projectDir, packetPath) {
|
|
|
2341
2393
|
}
|
|
2342
2394
|
function packetGroundingWarnings(projectDir, packet, source) {
|
|
2343
2395
|
const warnings = [];
|
|
2344
|
-
const meaningfulPaths = packet.paths.filter((path) => path && path !== "root" && !shouldSkipRepoMemoryPath(path));
|
|
2396
|
+
const meaningfulPaths = packet.paths.filter((path) => path && path !== "root" && !shouldSkipRepoMemoryPath(path) && !isGroundingIgnored(projectDir, path));
|
|
2345
2397
|
const missingPaths = meaningfulPaths.filter((path) => !pathExistsInRepo(projectDir, path));
|
|
2346
2398
|
if (meaningfulPaths.length && missingPaths.length === meaningfulPaths.length) {
|
|
2347
2399
|
warnings.push(`${source}: none of the referenced paths exist in this repo: ${missingPaths.join(", ")}`);
|
|
@@ -2805,6 +2857,54 @@ function readKageIgnore(projectDir) {
|
|
|
2805
2857
|
.map((line) => line.trim())
|
|
2806
2858
|
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
2807
2859
|
}
|
|
2860
|
+
// A repo can declare non-knowledge paths (e.g. a presentation/visualization layer)
|
|
2861
|
+
// in .kageignore. Those paths must not count as memory grounding: memory should never
|
|
2862
|
+
// be anchored to, or marked stale by, files the repo says are not knowledge-bearing.
|
|
2863
|
+
function normalizeRelPath(path) {
|
|
2864
|
+
return String(path).replace(/\\/g, "/").replace(/^\/+/, "");
|
|
2865
|
+
}
|
|
2866
|
+
function isGroundingIgnored(projectDir, path) {
|
|
2867
|
+
const patterns = readKageIgnore(projectDir);
|
|
2868
|
+
if (!patterns.length)
|
|
2869
|
+
return false;
|
|
2870
|
+
return isKageIgnored(normalizeRelPath(path), patterns);
|
|
2871
|
+
}
|
|
2872
|
+
// Strip .kageignore'd paths from a packet's grounding (paths, source refs, and
|
|
2873
|
+
// path fingerprints). Returns a new packet if anything changed, else null.
|
|
2874
|
+
function prunePacketGroundingPaths(packet, patterns) {
|
|
2875
|
+
if (!patterns.length)
|
|
2876
|
+
return null;
|
|
2877
|
+
const ignored = (p) => typeof p === "string" && isKageIgnored(normalizeRelPath(p), patterns);
|
|
2878
|
+
let changed = false;
|
|
2879
|
+
const paths = packet.paths.filter((p) => (ignored(p) ? ((changed = true), false) : true));
|
|
2880
|
+
const sourceRefs = packet.source_refs.map((ref) => {
|
|
2881
|
+
const next = { ...ref };
|
|
2882
|
+
if (ignored(next.path)) {
|
|
2883
|
+
delete next.path;
|
|
2884
|
+
changed = true;
|
|
2885
|
+
}
|
|
2886
|
+
if (Array.isArray(next.changed_files)) {
|
|
2887
|
+
const kept = next.changed_files.filter((f) => !ignored(f));
|
|
2888
|
+
if (kept.length !== next.changed_files.length) {
|
|
2889
|
+
next.changed_files = kept;
|
|
2890
|
+
changed = true;
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
return next;
|
|
2894
|
+
});
|
|
2895
|
+
const freshness = { ...(packet.freshness ?? {}) };
|
|
2896
|
+
if (Array.isArray(freshness.path_fingerprints)) {
|
|
2897
|
+
const fps = freshness.path_fingerprints;
|
|
2898
|
+
const kept = fps.filter((f) => !ignored(f?.path));
|
|
2899
|
+
if (kept.length !== fps.length) {
|
|
2900
|
+
freshness.path_fingerprints = kept;
|
|
2901
|
+
changed = true;
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
if (!changed)
|
|
2905
|
+
return null;
|
|
2906
|
+
return { ...packet, paths, source_refs: sourceRefs, freshness };
|
|
2907
|
+
}
|
|
2808
2908
|
function wildcardPattern(pattern) {
|
|
2809
2909
|
const escaped = pattern
|
|
2810
2910
|
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
@@ -5447,13 +5547,18 @@ function refreshPacketStaleness(projectDir) {
|
|
|
5447
5547
|
const findings = [];
|
|
5448
5548
|
let updated = 0;
|
|
5449
5549
|
const fingerprintCache = new Map();
|
|
5550
|
+
const ignorePatterns = readKageIgnore(projectDir);
|
|
5450
5551
|
for (const entry of loadPacketEntriesFromDir(packetsDir(projectDir))) {
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
const
|
|
5552
|
+
// Drop any .kageignore'd grounding (presentation layers etc.) from the stored packet
|
|
5553
|
+
// so memory is never anchored to non-knowledge files.
|
|
5554
|
+
const pruned = prunePacketGroundingPaths(entry.packet, ignorePatterns);
|
|
5555
|
+
const packet = pruned ?? entry.packet;
|
|
5556
|
+
const reasons = staleMemoryReasons(projectDir, packet, fingerprintCache);
|
|
5557
|
+
const oldQuality = (packet.quality ?? {});
|
|
5558
|
+
const oldFreshness = (packet.freshness ?? {});
|
|
5454
5559
|
let nextQuality;
|
|
5455
5560
|
if (reasons.length) {
|
|
5456
|
-
const finding = staleFinding(
|
|
5561
|
+
const finding = staleFinding(packet, reasons);
|
|
5457
5562
|
findings.push(finding);
|
|
5458
5563
|
nextQuality = {
|
|
5459
5564
|
...oldQuality,
|
|
@@ -5467,11 +5572,12 @@ function refreshPacketStaleness(projectDir) {
|
|
|
5467
5572
|
nextQuality = rest;
|
|
5468
5573
|
}
|
|
5469
5574
|
const nextFreshness = oldFreshness;
|
|
5470
|
-
const changed =
|
|
5575
|
+
const changed = pruned !== null
|
|
5576
|
+
|| JSON.stringify(oldQuality) !== JSON.stringify(nextQuality)
|
|
5471
5577
|
|| JSON.stringify(oldFreshness) !== JSON.stringify(nextFreshness);
|
|
5472
5578
|
if (changed) {
|
|
5473
5579
|
writeJson(entry.path, {
|
|
5474
|
-
...
|
|
5580
|
+
...packet,
|
|
5475
5581
|
freshness: nextFreshness,
|
|
5476
5582
|
quality: nextQuality,
|
|
5477
5583
|
updated_at: nowIso(),
|
|
@@ -10897,8 +11003,11 @@ function capture(input) {
|
|
|
10897
11003
|
};
|
|
10898
11004
|
}
|
|
10899
11005
|
const warnings = [];
|
|
10900
|
-
|
|
10901
|
-
|
|
11006
|
+
// .kageignore'd paths (e.g. a presentation/visualization layer) are not knowledge-bearing,
|
|
11007
|
+
// so they never become grounding for a packet — dropped before validation and storage.
|
|
11008
|
+
const groundedPaths = (input.paths ?? []).filter((path) => path && !isGroundingIgnored(input.projectDir, path));
|
|
11009
|
+
const meaningfulPaths = groundedPaths
|
|
11010
|
+
.filter((path) => meaningfulMemoryPath(path) && !shouldSkipRepoMemoryPath(path));
|
|
10902
11011
|
const missingPaths = meaningfulPaths.filter((path) => !pathExistsInRepo(input.projectDir, path));
|
|
10903
11012
|
// Citation validation. Strict mode (agent-facing record_memory tools / CLI) rejects a
|
|
10904
11013
|
// write whose every cited path is missing — the PRD's "reject if citations don't exist".
|
|
@@ -10937,7 +11046,7 @@ function capture(input) {
|
|
|
10937
11046
|
status: "approved",
|
|
10938
11047
|
confidence: DEFAULT_CONFIDENCE,
|
|
10939
11048
|
tags: input.tags ?? [],
|
|
10940
|
-
paths:
|
|
11049
|
+
paths: groundedPaths,
|
|
10941
11050
|
stack: input.stack ?? [],
|
|
10942
11051
|
source_refs: [
|
|
10943
11052
|
{
|
|
@@ -10949,7 +11058,7 @@ function capture(input) {
|
|
|
10949
11058
|
freshness: {
|
|
10950
11059
|
ttl_days: 365,
|
|
10951
11060
|
last_verified_at: createdAt,
|
|
10952
|
-
path_fingerprints: memoryPathFingerprints(input.projectDir,
|
|
11061
|
+
path_fingerprints: memoryPathFingerprints(input.projectDir, groundedPaths),
|
|
10953
11062
|
path_fingerprint_policy: "source_hash_staleness",
|
|
10954
11063
|
verification: "repo_local_agent_capture",
|
|
10955
11064
|
},
|
|
@@ -12509,11 +12618,14 @@ function prCheck(projectDir) {
|
|
|
12509
12618
|
const tree = gitTree(projectDir);
|
|
12510
12619
|
const codeInputHash = currentCodeGraphInputHash(projectDir);
|
|
12511
12620
|
const memoryInputHash = knowledgeGraphInputHash(projectDir, codeInputHash);
|
|
12512
|
-
const
|
|
12621
|
+
const fpCache = new Map();
|
|
12622
|
+
const staleEntries = loadPacketsFromDir(packetsDir(projectDir))
|
|
12513
12623
|
.filter((packet) => packet.status === "approved" || packet.status === "pending")
|
|
12514
|
-
.map((packet) => ({ packet, reasons: staleMemoryReasons(projectDir, packet) }))
|
|
12515
|
-
.filter((entry) => entry.reasons.length)
|
|
12516
|
-
|
|
12624
|
+
.map((packet) => ({ packet, reasons: staleMemoryReasons(projectDir, packet, fpCache), hard: recallHardStaleReason(projectDir, packet, fpCache) !== null }))
|
|
12625
|
+
.filter((entry) => entry.reasons.length);
|
|
12626
|
+
const stalePackets = staleEntries.map((entry) => staleFinding(entry.packet, entry.reasons));
|
|
12627
|
+
const hardStaleCount = staleEntries.filter((entry) => entry.hard).length;
|
|
12628
|
+
const softStaleCount = staleEntries.length - hardStaleCount;
|
|
12517
12629
|
const memoryPacketChanges = unique(rawStatus
|
|
12518
12630
|
.split(/\r?\n/)
|
|
12519
12631
|
.map(parsePorcelainPath)
|
|
@@ -12526,13 +12638,18 @@ function prCheck(projectDir) {
|
|
|
12526
12638
|
const errors = [...validation.errors];
|
|
12527
12639
|
const warnings = [...validation.warnings];
|
|
12528
12640
|
const requiredActions = [];
|
|
12529
|
-
|
|
12530
|
-
|
|
12531
|
-
|
|
12641
|
+
// Block only on hard-stale memory (cited files deleted, ttl expired, reported
|
|
12642
|
+
// stale). Soft-stale ("linked code changed since capture") is normal during
|
|
12643
|
+
// active development — surface it as a warning, don't fail the gate.
|
|
12644
|
+
if (hardStaleCount) {
|
|
12645
|
+
errors.push(`${hardStaleCount} memory packet(s) are hard-stale (deleted citations, expired ttl, or reported) and must be updated or superseded.`);
|
|
12646
|
+
requiredActions.push("Run kage compact (or kage gc), then update or supersede the affected packets.");
|
|
12647
|
+
}
|
|
12648
|
+
if (softStaleCount) {
|
|
12649
|
+
warnings.push(`${softStaleCount} memory packet(s) reference code that changed since capture — review with kage verify (not blocking).`);
|
|
12532
12650
|
}
|
|
12533
12651
|
if (reconciliation.unresolved_count > 0) {
|
|
12534
|
-
|
|
12535
|
-
requiredActions.push(...reconciliation.items.slice(0, 5).map((item) => item.next_action));
|
|
12652
|
+
warnings.push(`${reconciliation.unresolved_count} memory reconciliation item(s) may need update after recent code changes (review on handoff; not blocking).`);
|
|
12536
12653
|
}
|
|
12537
12654
|
if (!codeGraphCurrent || !memoryGraphCurrent) {
|
|
12538
12655
|
errors.push("Generated graph artifacts are missing or not current for this working tree content.");
|
|
@@ -12540,8 +12657,7 @@ function prCheck(projectDir) {
|
|
|
12540
12657
|
}
|
|
12541
12658
|
const distillableSessions = sessions.sessions.filter((session) => session.durable_observations > 0);
|
|
12542
12659
|
if (distillableSessions.length) {
|
|
12543
|
-
|
|
12544
|
-
requiredActions.push(...distillableSessions.slice(0, 5).map((session) => session.next_action));
|
|
12660
|
+
warnings.push(`${distillableSessions.length} distillable session learning${distillableSessions.length === 1 ? "" : "s"} pending review (run kage distill; not blocking).`);
|
|
12545
12661
|
}
|
|
12546
12662
|
if (!memoryPacketChanges.length && overlay.changed_files.some((path) => !path.startsWith(".agent_memory/"))) {
|
|
12547
12663
|
warnings.push("No repo memory packet changed for this branch. If durable knowledge was learned, run kage propose --from-diff or kage learn.");
|