@kage-core/kage-graph-mcp 1.2.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 +74 -0
- package/dist/daemon.js +7 -1
- package/dist/index.js +22 -9
- package/dist/kernel.js +309 -26
- package/package.json +1 -1
- package/viewer/console.js +638 -0
- package/viewer/index.html +284 -280
- package/viewer/app.js +0 -6693
- 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 -2781
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;
|
|
@@ -83,6 +84,7 @@ exports.buildIndexes = buildIndexes;
|
|
|
83
84
|
exports.indexProject = indexProject;
|
|
84
85
|
exports.refreshProject = refreshProject;
|
|
85
86
|
exports.gcProject = gcProject;
|
|
87
|
+
exports.kageSuppressedMemory = kageSuppressedMemory;
|
|
86
88
|
exports.verifyCitations = verifyCitations;
|
|
87
89
|
exports.compactProject = compactProject;
|
|
88
90
|
exports.installAgentPolicy = installAgentPolicy;
|
|
@@ -114,6 +116,8 @@ exports.kageMetrics = kageMetrics;
|
|
|
114
116
|
exports.auditProject = auditProject;
|
|
115
117
|
exports.memoryInbox = memoryInbox;
|
|
116
118
|
exports.qualityReport = qualityReport;
|
|
119
|
+
exports.benchmarkTrust = benchmarkTrust;
|
|
120
|
+
exports.runDemo = runDemo;
|
|
117
121
|
exports.benchmarkProject = benchmarkProject;
|
|
118
122
|
exports.benchmarkCodingMemoryQuality = benchmarkCodingMemoryQuality;
|
|
119
123
|
exports.benchmarkMemoryScale = benchmarkMemoryScale;
|
|
@@ -870,6 +874,8 @@ function kageMemoryLifecycle(projectDir) {
|
|
|
870
874
|
const item = {
|
|
871
875
|
packet_id: packet.id,
|
|
872
876
|
title: packet.title,
|
|
877
|
+
summary: packet.summary ?? "",
|
|
878
|
+
body: packet.body ?? "",
|
|
873
879
|
type: packet.type,
|
|
874
880
|
status: packet.status,
|
|
875
881
|
health: action.health,
|
|
@@ -954,6 +960,54 @@ function recordRecallAccess(projectDir, results) {
|
|
|
954
960
|
// Recall should never fail because local access telemetry could not be updated.
|
|
955
961
|
}
|
|
956
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
|
+
}
|
|
957
1011
|
function isGeneratedChangeMemory(packet) {
|
|
958
1012
|
return packet.type === "workflow"
|
|
959
1013
|
&& packet.tags.includes("change-memory")
|
|
@@ -1119,7 +1173,7 @@ function staleMemoryReasons(projectDir, packet, fingerprintCache) {
|
|
|
1119
1173
|
if (ageDays > ttlDays)
|
|
1120
1174
|
reasons.push(`freshness ttl expired (${Math.floor(ageDays)}d old, ttl ${ttlDays}d)`);
|
|
1121
1175
|
}
|
|
1122
|
-
const paths = packet.paths.filter(meaningfulMemoryPath);
|
|
1176
|
+
const paths = packet.paths.filter((path) => meaningfulMemoryPath(path) && !isGroundingIgnored(projectDir, path));
|
|
1123
1177
|
const missingPaths = paths.filter((path) => !(0, node_fs_1.existsSync)((0, node_path_1.join)(projectDir, path)));
|
|
1124
1178
|
if (paths.length > 0 && missingPaths.length === paths.length) {
|
|
1125
1179
|
reasons.push(`all referenced paths are missing: ${missingPaths.slice(0, 4).join(", ")}`);
|
|
@@ -1130,6 +1184,7 @@ function staleMemoryReasons(projectDir, packet, fingerprintCache) {
|
|
|
1130
1184
|
if (freshness.path_fingerprint_policy === "source_hash_staleness") {
|
|
1131
1185
|
const storedFingerprints = packetStoredPathFingerprints(packet);
|
|
1132
1186
|
const changedPaths = storedFingerprints
|
|
1187
|
+
.filter((fingerprint) => !isGroundingIgnored(projectDir, fingerprint.path))
|
|
1133
1188
|
.filter((fingerprint) => (0, node_fs_1.existsSync)((0, node_path_1.join)(projectDir, fingerprint.path)))
|
|
1134
1189
|
.filter((fingerprint) => {
|
|
1135
1190
|
const current = memoryPathFingerprint(projectDir, fingerprint.path, fingerprintCache);
|
|
@@ -2338,7 +2393,7 @@ function pathExistsInRepo(projectDir, packetPath) {
|
|
|
2338
2393
|
}
|
|
2339
2394
|
function packetGroundingWarnings(projectDir, packet, source) {
|
|
2340
2395
|
const warnings = [];
|
|
2341
|
-
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));
|
|
2342
2397
|
const missingPaths = meaningfulPaths.filter((path) => !pathExistsInRepo(projectDir, path));
|
|
2343
2398
|
if (meaningfulPaths.length && missingPaths.length === meaningfulPaths.length) {
|
|
2344
2399
|
warnings.push(`${source}: none of the referenced paths exist in this repo: ${missingPaths.join(", ")}`);
|
|
@@ -2802,6 +2857,54 @@ function readKageIgnore(projectDir) {
|
|
|
2802
2857
|
.map((line) => line.trim())
|
|
2803
2858
|
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
2804
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
|
+
}
|
|
2805
2908
|
function wildcardPattern(pattern) {
|
|
2806
2909
|
const escaped = pattern
|
|
2807
2910
|
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
@@ -5444,13 +5547,18 @@ function refreshPacketStaleness(projectDir) {
|
|
|
5444
5547
|
const findings = [];
|
|
5445
5548
|
let updated = 0;
|
|
5446
5549
|
const fingerprintCache = new Map();
|
|
5550
|
+
const ignorePatterns = readKageIgnore(projectDir);
|
|
5447
5551
|
for (const entry of loadPacketEntriesFromDir(packetsDir(projectDir))) {
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
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 ?? {});
|
|
5451
5559
|
let nextQuality;
|
|
5452
5560
|
if (reasons.length) {
|
|
5453
|
-
const finding = staleFinding(
|
|
5561
|
+
const finding = staleFinding(packet, reasons);
|
|
5454
5562
|
findings.push(finding);
|
|
5455
5563
|
nextQuality = {
|
|
5456
5564
|
...oldQuality,
|
|
@@ -5464,11 +5572,12 @@ function refreshPacketStaleness(projectDir) {
|
|
|
5464
5572
|
nextQuality = rest;
|
|
5465
5573
|
}
|
|
5466
5574
|
const nextFreshness = oldFreshness;
|
|
5467
|
-
const changed =
|
|
5575
|
+
const changed = pruned !== null
|
|
5576
|
+
|| JSON.stringify(oldQuality) !== JSON.stringify(nextQuality)
|
|
5468
5577
|
|| JSON.stringify(oldFreshness) !== JSON.stringify(nextFreshness);
|
|
5469
5578
|
if (changed) {
|
|
5470
5579
|
writeJson(entry.path, {
|
|
5471
|
-
...
|
|
5580
|
+
...packet,
|
|
5472
5581
|
freshness: nextFreshness,
|
|
5473
5582
|
quality: nextQuality,
|
|
5474
5583
|
updated_at: nowIso(),
|
|
@@ -5595,8 +5704,20 @@ function gcProject(projectDir, options = {}) {
|
|
|
5595
5704
|
total_scanned: packetEntries.length,
|
|
5596
5705
|
};
|
|
5597
5706
|
}
|
|
5598
|
-
//
|
|
5599
|
-
//
|
|
5707
|
+
// The memory recall is actively WITHHOLDING from agents right now (hard-stale:
|
|
5708
|
+
// cited files deleted, ttl expired, or reported stale). This is the human-facing
|
|
5709
|
+
// counterpart to the silent recall-time exclusion — surfaced, never hidden.
|
|
5710
|
+
function kageSuppressedMemory(projectDir) {
|
|
5711
|
+
ensureMemoryDirs(projectDir);
|
|
5712
|
+
const cache = new Map();
|
|
5713
|
+
const items = loadApprovedPackets(projectDir)
|
|
5714
|
+
.map((packet) => {
|
|
5715
|
+
const reason = recallHardStaleReason(projectDir, packet, cache);
|
|
5716
|
+
return reason ? { id: packet.id, title: packet.title, type: packet.type, reason, paths: packet.paths } : null;
|
|
5717
|
+
})
|
|
5718
|
+
.filter((entry) => entry !== null);
|
|
5719
|
+
return { schema_version: 1, generated_at: nowIso(), count: items.length, items };
|
|
5720
|
+
}
|
|
5600
5721
|
function verifyCitations(projectDir, options = {}) {
|
|
5601
5722
|
ensureMemoryDirs(projectDir);
|
|
5602
5723
|
const approved = loadApprovedPackets(projectDir);
|
|
@@ -9838,6 +9959,152 @@ function qualityReport(projectDir) {
|
|
|
9838
9959
|
packets: rows,
|
|
9839
9960
|
};
|
|
9840
9961
|
}
|
|
9962
|
+
// The Trust Benchmark measures what retrieval benchmarks cannot: whether the memory
|
|
9963
|
+
// system can be TRUSTED — does it refuse to store hallucinated citations, does it
|
|
9964
|
+
// withhold memory whose evidence was deleted, and is live repo memory actually grounded.
|
|
9965
|
+
// Controlled gates run in an isolated sandbox; the grounding gate runs on the real repo.
|
|
9966
|
+
function benchmarkTrust(projectDir) {
|
|
9967
|
+
const runDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "kage-trust-"));
|
|
9968
|
+
const sandbox = (0, node_path_1.join)(runDir, "project");
|
|
9969
|
+
try {
|
|
9970
|
+
ensureMemoryDirs(sandbox);
|
|
9971
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.join)(sandbox, "src"), { recursive: true });
|
|
9972
|
+
// Gate 1 — Hallucinated-citation rejection: a strict capture whose every cited path
|
|
9973
|
+
// is missing must be rejected. (No competitor validates citations at write time.)
|
|
9974
|
+
const hallucinationAttempts = 8;
|
|
9975
|
+
let rejected = 0;
|
|
9976
|
+
for (let i = 0; i < hallucinationAttempts; i += 1) {
|
|
9977
|
+
const result = capture({
|
|
9978
|
+
projectDir: sandbox,
|
|
9979
|
+
title: `Hallucinated rule ${i}`,
|
|
9980
|
+
body: `Use the helper in src/ghost-${i}.ts for retry handling.`,
|
|
9981
|
+
type: "decision",
|
|
9982
|
+
paths: [`src/ghost-${i}.ts`],
|
|
9983
|
+
strictCitations: true,
|
|
9984
|
+
});
|
|
9985
|
+
if (!result.ok)
|
|
9986
|
+
rejected += 1;
|
|
9987
|
+
}
|
|
9988
|
+
// Gate 2 — Stale-memory exclusion: memory grounded in real files at capture time
|
|
9989
|
+
// must be withheld from recall once those files are deleted (the "deleted since
|
|
9990
|
+
// capture" signal). We only count memories that were recallable BEFORE deletion.
|
|
9991
|
+
const staleAttempts = 8;
|
|
9992
|
+
const recallableBefore = [];
|
|
9993
|
+
for (let i = 0; i < staleAttempts; i += 1) {
|
|
9994
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(sandbox, "src", `widget-${i}.ts`), `export const widget${i} = ${i};\n`, "utf8");
|
|
9995
|
+
capture({
|
|
9996
|
+
projectDir: sandbox,
|
|
9997
|
+
title: `Widget ${i} retry invariant`,
|
|
9998
|
+
body: `Widget ${i} retries use idempotency token zeta${i} in src/widget-${i}.ts to avoid duplicate charges.`,
|
|
9999
|
+
type: "decision",
|
|
10000
|
+
paths: [`src/widget-${i}.ts`],
|
|
10001
|
+
});
|
|
10002
|
+
}
|
|
10003
|
+
for (let i = 0; i < staleAttempts; i += 1) {
|
|
10004
|
+
const before = recall(sandbox, `widget ${i} retry idempotency token zeta${i}`, 5, false, { trackAccess: false });
|
|
10005
|
+
recallableBefore[i] = before.results.some((entry) => entry.packet.title === `Widget ${i} retry invariant`);
|
|
10006
|
+
}
|
|
10007
|
+
for (let i = 0; i < staleAttempts; i += 1) {
|
|
10008
|
+
(0, node_fs_1.rmSync)((0, node_path_1.join)(sandbox, "src", `widget-${i}.ts`), { force: true });
|
|
10009
|
+
}
|
|
10010
|
+
let recallableCount = 0;
|
|
10011
|
+
let excludedAfter = 0;
|
|
10012
|
+
for (let i = 0; i < staleAttempts; i += 1) {
|
|
10013
|
+
if (!recallableBefore[i])
|
|
10014
|
+
continue;
|
|
10015
|
+
recallableCount += 1;
|
|
10016
|
+
const after = recall(sandbox, `widget ${i} retry idempotency token zeta${i}`, 5, false, { trackAccess: false });
|
|
10017
|
+
const surfaced = after.results.some((entry) => entry.packet.title === `Widget ${i} retry invariant`);
|
|
10018
|
+
if (!surfaced)
|
|
10019
|
+
excludedAfter += 1;
|
|
10020
|
+
}
|
|
10021
|
+
// Gate 3 — Live grounding: how much of the real repo's approved memory is grounded
|
|
10022
|
+
// (cited files exist) and not stale.
|
|
10023
|
+
const verify = verifyCitations(projectDir);
|
|
10024
|
+
const liveChecked = verify.checked;
|
|
10025
|
+
const grounded = verify.packets.filter((entry) => entry.grounded && !entry.stale).length;
|
|
10026
|
+
const hallucinationRate = percent(rejected, hallucinationAttempts);
|
|
10027
|
+
const staleRate = percent(excludedAfter, recallableCount || staleAttempts);
|
|
10028
|
+
const liveGroundingRate = liveChecked > 0 ? percent(grounded, liveChecked) : 100;
|
|
10029
|
+
const wrongAdvicePrevented = percent(rejected + excludedAfter, hallucinationAttempts + (recallableCount || staleAttempts));
|
|
10030
|
+
const gates = [
|
|
10031
|
+
{ name: "hallucinated_citation_rejection", target: 100, actual: hallucinationRate, unit: "percent", pass: hallucinationRate >= 100 },
|
|
10032
|
+
{ name: "stale_memory_exclusion", target: 100, actual: staleRate, unit: "percent", pass: staleRate >= 100 },
|
|
10033
|
+
{ name: "live_grounding_rate", target: 80, actual: liveGroundingRate, unit: "percent", pass: liveGroundingRate >= 80 },
|
|
10034
|
+
];
|
|
10035
|
+
const trustScore = Math.round((hallucinationRate + staleRate + liveGroundingRate) / 3);
|
|
10036
|
+
return {
|
|
10037
|
+
schema_version: 1,
|
|
10038
|
+
generated_at: nowIso(),
|
|
10039
|
+
ok: gates.every((gate) => gate.pass),
|
|
10040
|
+
trust_score: trustScore,
|
|
10041
|
+
gates,
|
|
10042
|
+
metrics: {
|
|
10043
|
+
hallucinated_citation_rejection_rate: hallucinationRate,
|
|
10044
|
+
stale_memory_exclusion_rate: staleRate,
|
|
10045
|
+
live_grounding_rate: liveGroundingRate,
|
|
10046
|
+
wrong_advice_prevented_rate: wrongAdvicePrevented,
|
|
10047
|
+
},
|
|
10048
|
+
detail: {
|
|
10049
|
+
hallucination: { attempted: hallucinationAttempts, rejected },
|
|
10050
|
+
staleness: { recallable_before: recallableCount, excluded_after: excludedAfter },
|
|
10051
|
+
live_memory: { checked: liveChecked, grounded, stale: verify.stale },
|
|
10052
|
+
},
|
|
10053
|
+
};
|
|
10054
|
+
}
|
|
10055
|
+
finally {
|
|
10056
|
+
(0, node_fs_1.rmSync)(runDir, { recursive: true, force: true });
|
|
10057
|
+
}
|
|
10058
|
+
}
|
|
10059
|
+
// `kage demo`: a self-contained 60-second proof of the trust wedge. Seeds a tiny
|
|
10060
|
+
// repo with grounded memory, then shows Kage (1) reject a hallucinated citation,
|
|
10061
|
+
// (2) withhold a memory whose cited file was deleted, and (3) recall only grounded
|
|
10062
|
+
// memory — the three things that make agent memory trustworthy.
|
|
10063
|
+
function runDemo(demoDir) {
|
|
10064
|
+
(0, node_fs_1.rmSync)(demoDir, { recursive: true, force: true });
|
|
10065
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.join)(demoDir, "src"), { recursive: true });
|
|
10066
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(demoDir, "src", "auth.ts"), "export function validateToken() { return true; }\n", "utf8");
|
|
10067
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(demoDir, "src", "payments.ts"), "export function charge() { return 'ok'; }\n", "utf8");
|
|
10068
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(demoDir, "src", "legacy-retry.ts"), "export function retry() { return 1; }\n", "utf8");
|
|
10069
|
+
ensureMemoryDirs(demoDir);
|
|
10070
|
+
const captured = [];
|
|
10071
|
+
for (const m of [
|
|
10072
|
+
{ title: "Auth uses jose, not jsonwebtoken", body: "Validate tokens with jose in src/auth.ts; jsonwebtoken was removed.", paths: ["src/auth.ts"] },
|
|
10073
|
+
{ title: "Payments must be idempotent", body: "charge() in src/payments.ts must be idempotent to avoid double charges.", paths: ["src/payments.ts"] },
|
|
10074
|
+
{ title: "Legacy retry helper is the fallback", body: "Old retry logic lives in src/legacy-retry.ts and is used as a fallback.", paths: ["src/legacy-retry.ts"] },
|
|
10075
|
+
]) {
|
|
10076
|
+
const r = capture({ projectDir: demoDir, title: m.title, body: m.body, type: "decision", paths: m.paths });
|
|
10077
|
+
if (r.ok && r.packet)
|
|
10078
|
+
captured.push(r.packet.title);
|
|
10079
|
+
}
|
|
10080
|
+
// (2) delete a cited file → that memory becomes stale and is withheld from recall.
|
|
10081
|
+
(0, node_fs_1.unlinkSync)((0, node_path_1.join)(demoDir, "src", "legacy-retry.ts"));
|
|
10082
|
+
// (1) a hallucinated citation is rejected at write time.
|
|
10083
|
+
const hallucinated = capture({
|
|
10084
|
+
projectDir: demoDir,
|
|
10085
|
+
title: "Use the helper in src/ghost.ts",
|
|
10086
|
+
body: "Retry handling lives in src/ghost.ts.",
|
|
10087
|
+
type: "decision",
|
|
10088
|
+
paths: ["src/ghost.ts"],
|
|
10089
|
+
strictCitations: true,
|
|
10090
|
+
});
|
|
10091
|
+
const rejected = hallucinated.ok ? null : { title: "Use the helper in src/ghost.ts", error: hallucinated.errors[0] ?? "rejected" };
|
|
10092
|
+
// (3) recall surfaces grounded memory; the stale one is withheld.
|
|
10093
|
+
const recall = recallWithVectorScores(demoDir, "auth token payments retry idempotency", 5, false, { trackAccess: false });
|
|
10094
|
+
const recalled = recall.results.map((entry) => entry.packet.title);
|
|
10095
|
+
const withheld = kageSuppressedMemory(demoDir).items.map((item) => ({ title: item.title, reason: item.reason }));
|
|
10096
|
+
const trust = benchmarkTrust(demoDir);
|
|
10097
|
+
return {
|
|
10098
|
+
ok: true,
|
|
10099
|
+
project_dir: demoDir,
|
|
10100
|
+
captured,
|
|
10101
|
+
rejected_hallucination: rejected,
|
|
10102
|
+
recalled,
|
|
10103
|
+
withheld,
|
|
10104
|
+
trust_score: trust.trust_score,
|
|
10105
|
+
viewer_command: `kage viewer --project ${demoDir}`,
|
|
10106
|
+
};
|
|
10107
|
+
}
|
|
9841
10108
|
function benchmarkProject(projectDir, inputs = {}) {
|
|
9842
10109
|
ensureMemoryDirs(projectDir);
|
|
9843
10110
|
const built = inputs.codeGraph && inputs.knowledgeGraph ? null : currentOrBuildGraphs(projectDir);
|
|
@@ -10736,8 +11003,11 @@ function capture(input) {
|
|
|
10736
11003
|
};
|
|
10737
11004
|
}
|
|
10738
11005
|
const warnings = [];
|
|
10739
|
-
|
|
10740
|
-
|
|
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));
|
|
10741
11011
|
const missingPaths = meaningfulPaths.filter((path) => !pathExistsInRepo(input.projectDir, path));
|
|
10742
11012
|
// Citation validation. Strict mode (agent-facing record_memory tools / CLI) rejects a
|
|
10743
11013
|
// write whose every cited path is missing — the PRD's "reject if citations don't exist".
|
|
@@ -10776,7 +11046,7 @@ function capture(input) {
|
|
|
10776
11046
|
status: "approved",
|
|
10777
11047
|
confidence: DEFAULT_CONFIDENCE,
|
|
10778
11048
|
tags: input.tags ?? [],
|
|
10779
|
-
paths:
|
|
11049
|
+
paths: groundedPaths,
|
|
10780
11050
|
stack: input.stack ?? [],
|
|
10781
11051
|
source_refs: [
|
|
10782
11052
|
{
|
|
@@ -10788,7 +11058,7 @@ function capture(input) {
|
|
|
10788
11058
|
freshness: {
|
|
10789
11059
|
ttl_days: 365,
|
|
10790
11060
|
last_verified_at: createdAt,
|
|
10791
|
-
path_fingerprints: memoryPathFingerprints(input.projectDir,
|
|
11061
|
+
path_fingerprints: memoryPathFingerprints(input.projectDir, groundedPaths),
|
|
10792
11062
|
path_fingerprint_policy: "source_hash_staleness",
|
|
10793
11063
|
verification: "repo_local_agent_capture",
|
|
10794
11064
|
},
|
|
@@ -11150,6 +11420,10 @@ elif event_name == "PreCompact":
|
|
|
11150
11420
|
payload = {"type": "session_end", "summary": "Claude Code is compacting context; distill durable observations before compaction."}
|
|
11151
11421
|
elif event_name == "SessionEnd":
|
|
11152
11422
|
payload = {"type": "session_end", "summary": "Claude Code session ended; distill durable observations for teammate handoff."}
|
|
11423
|
+
elif event_name == "SubagentStop":
|
|
11424
|
+
payload = {"type": "session_end", "summary": "Subagent finished; distill durable observations from the subagent run."}
|
|
11425
|
+
elif event_name == "PreToolUse":
|
|
11426
|
+
payload = {"type": "tool_use", "tool": tool, "path": path, "command": command, "summary": "About to run: " + compact(command or tool or d, 200)}
|
|
11153
11427
|
else:
|
|
11154
11428
|
payload = {"type": "tool_use", "tool": tool, "path": path, "command": command, "summary": compact(d, 320), "text": compact(d)}
|
|
11155
11429
|
|
|
@@ -11161,7 +11435,7 @@ if [[ -n "$OBSERVATION" ]]; then
|
|
|
11161
11435
|
kage observe --project "$CWD" --event "$OBSERVATION" --json >/dev/null 2>&1 || true
|
|
11162
11436
|
fi
|
|
11163
11437
|
|
|
11164
|
-
if [[ "$EVENT" == "PreCompact" || "$EVENT" == "SessionEnd" ]]; then
|
|
11438
|
+
if [[ "$EVENT" == "PreCompact" || "$EVENT" == "SessionEnd" || "$EVENT" == "SubagentStop" ]]; then
|
|
11165
11439
|
kage distill --project "$CWD" --session "$SESSION" --json >/dev/null 2>&1 || true
|
|
11166
11440
|
fi
|
|
11167
11441
|
|
|
@@ -11197,11 +11471,13 @@ exit 0
|
|
|
11197
11471
|
hooks: {
|
|
11198
11472
|
SessionStart: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/session-start.sh", timeout: 5 }] }],
|
|
11199
11473
|
UserPromptSubmit: [{ hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 12 }] }],
|
|
11474
|
+
PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 5 }] }],
|
|
11200
11475
|
PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 5 }] }],
|
|
11201
11476
|
PostToolUseFailure: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 5 }] }],
|
|
11202
11477
|
PreCompact: [{ hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 20 }] }],
|
|
11203
11478
|
Stop: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/stop.sh", timeout: 20 }] }],
|
|
11204
11479
|
SessionEnd: [{ hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 20 }] }],
|
|
11480
|
+
SubagentStop: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/observe.sh", timeout: 20 }] }],
|
|
11205
11481
|
},
|
|
11206
11482
|
};
|
|
11207
11483
|
setSnippet(path, JSON.stringify({ mcpServers: { kage: server } }, null, 2), [
|
|
@@ -12342,11 +12618,14 @@ function prCheck(projectDir) {
|
|
|
12342
12618
|
const tree = gitTree(projectDir);
|
|
12343
12619
|
const codeInputHash = currentCodeGraphInputHash(projectDir);
|
|
12344
12620
|
const memoryInputHash = knowledgeGraphInputHash(projectDir, codeInputHash);
|
|
12345
|
-
const
|
|
12621
|
+
const fpCache = new Map();
|
|
12622
|
+
const staleEntries = loadPacketsFromDir(packetsDir(projectDir))
|
|
12346
12623
|
.filter((packet) => packet.status === "approved" || packet.status === "pending")
|
|
12347
|
-
.map((packet) => ({ packet, reasons: staleMemoryReasons(projectDir, packet) }))
|
|
12348
|
-
.filter((entry) => entry.reasons.length)
|
|
12349
|
-
|
|
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;
|
|
12350
12629
|
const memoryPacketChanges = unique(rawStatus
|
|
12351
12630
|
.split(/\r?\n/)
|
|
12352
12631
|
.map(parsePorcelainPath)
|
|
@@ -12359,13 +12638,18 @@ function prCheck(projectDir) {
|
|
|
12359
12638
|
const errors = [...validation.errors];
|
|
12360
12639
|
const warnings = [...validation.warnings];
|
|
12361
12640
|
const requiredActions = [];
|
|
12362
|
-
|
|
12363
|
-
|
|
12364
|
-
|
|
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).`);
|
|
12365
12650
|
}
|
|
12366
12651
|
if (reconciliation.unresolved_count > 0) {
|
|
12367
|
-
|
|
12368
|
-
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).`);
|
|
12369
12653
|
}
|
|
12370
12654
|
if (!codeGraphCurrent || !memoryGraphCurrent) {
|
|
12371
12655
|
errors.push("Generated graph artifacts are missing or not current for this working tree content.");
|
|
@@ -12373,8 +12657,7 @@ function prCheck(projectDir) {
|
|
|
12373
12657
|
}
|
|
12374
12658
|
const distillableSessions = sessions.sessions.filter((session) => session.durable_observations > 0);
|
|
12375
12659
|
if (distillableSessions.length) {
|
|
12376
|
-
|
|
12377
|
-
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).`);
|
|
12378
12661
|
}
|
|
12379
12662
|
if (!memoryPacketChanges.length && overlay.changed_files.some((path) => !path.startsWith(".agent_memory/"))) {
|
|
12380
12663
|
warnings.push("No repo memory packet changed for this branch. If durable knowledge was learned, run kage propose --from-diff or kage learn.");
|