@kage-core/kage-graph-mcp 1.1.38 → 1.2.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 +77 -4
- package/dist/index.js +63 -5
- package/dist/kernel.js +369 -14
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -29,6 +29,8 @@ Usage:
|
|
|
29
29
|
kage hook uninstall --project <dir> [--json]
|
|
30
30
|
kage refresh --project <dir> [--full] [--json]
|
|
31
31
|
kage gc --project <dir> [--dry-run] [--force] [--json]
|
|
32
|
+
kage compact --project <dir> [--dry-run] [--json]
|
|
33
|
+
kage verify --project <dir> [--id <packet-id>] [--json]
|
|
32
34
|
kage pr summarize --project <dir> [--json]
|
|
33
35
|
kage pr check --project <dir> [--json]
|
|
34
36
|
kage upgrade [--dry-run]
|
|
@@ -74,14 +76,14 @@ Usage:
|
|
|
74
76
|
kage graph "<query>" --project <dir> [--json]
|
|
75
77
|
kage graph-registry --project <dir> [--json]
|
|
76
78
|
kage embeddings build --project <dir> [--model Xenova/all-MiniLM-L6-v2] [--json]
|
|
77
|
-
kage recall "<query>" --project <dir> [--json] [--explain] [--embeddings]
|
|
79
|
+
kage recall "<query>" --project <dir> [--json] [--explain] [--embeddings] [--max-context-tokens <n>] [--structural-hops <n>]
|
|
78
80
|
kage observe --project <dir> --event <json>
|
|
79
81
|
kage sessions --project <dir> [--json]
|
|
80
82
|
kage replay --project <dir> [--session <id>] [--limit <n>] [--json]
|
|
81
83
|
kage distill --project <dir> --session <id>
|
|
82
|
-
kage learn --project <dir> --learning <text> [--title <title>] [--type <type>] [--evidence <text>] [--verified-by <text>] [--tags a,b] [--paths a,b]
|
|
84
|
+
kage learn --project <dir> --learning <text> [--title <title>] [--type <type>] [--evidence <text>] [--verified-by <text>] [--tags a,b] [--paths a,b] [--graph-nodes a,b] [--allow-missing-paths]
|
|
83
85
|
kage feedback --project <dir> --packet <packet-id> --kind helpful|wrong|stale
|
|
84
|
-
kage capture --project <dir> --title <title> --body <body> [--type <type>] [--summary <summary>] [--tags a,b] [--paths a,b] [--stack a,b]
|
|
86
|
+
kage capture --project <dir> --title <title> --body <body> [--type <type>] [--summary <summary>] [--tags a,b] [--paths a,b] [--stack a,b] [--graph-nodes a,b] [--allow-missing-paths]
|
|
85
87
|
kage propose --project <dir> --from-diff
|
|
86
88
|
kage review-artifact --project <dir>
|
|
87
89
|
kage promote --project <dir> --public <packet-id>
|
|
@@ -404,6 +406,65 @@ async function main() {
|
|
|
404
406
|
}
|
|
405
407
|
return;
|
|
406
408
|
}
|
|
409
|
+
if (command === "compact") {
|
|
410
|
+
const project = projectArg(args);
|
|
411
|
+
const dryRun = args.includes("--dry-run");
|
|
412
|
+
const result = (0, kernel_js_1.compactProject)(project, { dryRun });
|
|
413
|
+
if (args.includes("--json")) {
|
|
414
|
+
console.log(JSON.stringify(result, null, 2));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const label = dryRun ? " [dry-run]" : "";
|
|
418
|
+
console.log(`Kage compact${label} — scanned ${result.total_scanned} packets`);
|
|
419
|
+
if (result.pruned_citations.length) {
|
|
420
|
+
console.log(`\nPruned dead citations (${result.pruned_citations.length}):`);
|
|
421
|
+
for (const p of result.pruned_citations)
|
|
422
|
+
console.log(` ✂ ${p.title} — removed ${p.removed_paths.join(", ")}`);
|
|
423
|
+
}
|
|
424
|
+
if (result.deprecated.length) {
|
|
425
|
+
console.log(`\nDeprecated stale (${result.deprecated.length}):`);
|
|
426
|
+
for (const p of result.deprecated)
|
|
427
|
+
console.log(` ✗ ${p.title} — ${p.reason}`);
|
|
428
|
+
}
|
|
429
|
+
if (result.duplicate_clusters.length) {
|
|
430
|
+
console.log(`\nDuplicate clusters to merge (${result.duplicate_clusters.length}) — review with kage_compact / kage supersede:`);
|
|
431
|
+
for (const cluster of result.duplicate_clusters) {
|
|
432
|
+
console.log(` ~${cluster.score} similarity:`);
|
|
433
|
+
for (const member of cluster.packets)
|
|
434
|
+
console.log(` • ${member.title} (${member.id})`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (!result.pruned_citations.length && !result.deprecated.length && !result.duplicate_clusters.length) {
|
|
438
|
+
console.log("Nothing to compact — memory is clean.");
|
|
439
|
+
}
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (command === "verify") {
|
|
443
|
+
const project = projectArg(args);
|
|
444
|
+
const idFlag = args.indexOf("--id");
|
|
445
|
+
const id = idFlag >= 0 ? args[idFlag + 1] : undefined;
|
|
446
|
+
const result = (0, kernel_js_1.verifyCitations)(project, { id });
|
|
447
|
+
if (args.includes("--json")) {
|
|
448
|
+
console.log(JSON.stringify(result, null, 2));
|
|
449
|
+
if (!result.ok)
|
|
450
|
+
process.exit(2);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (!result.ok) {
|
|
454
|
+
console.log(`Verification failed:\n${result.errors.map((error) => ` - ${error}`).join("\n")}`);
|
|
455
|
+
process.exit(2);
|
|
456
|
+
}
|
|
457
|
+
console.log(`Kage verify — ${result.checked} packet(s): ${result.valid} valid, ${result.stale} stale, ${result.ungrounded} ungrounded`);
|
|
458
|
+
for (const entry of result.packets.filter((p) => p.stale || !p.grounded)) {
|
|
459
|
+
const flags = [entry.stale ? `stale:${entry.stale_severity}` : "", entry.grounded ? "" : "ungrounded"].filter(Boolean).join(", ");
|
|
460
|
+
console.log(` ⚠ ${entry.title} [${flags}]`);
|
|
461
|
+
if (entry.missing_paths.length)
|
|
462
|
+
console.log(` missing: ${entry.missing_paths.join(", ")}`);
|
|
463
|
+
for (const reason of entry.stale_reasons)
|
|
464
|
+
console.log(` - ${reason}`);
|
|
465
|
+
}
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
407
468
|
if (command === "refresh") {
|
|
408
469
|
const result = (0, kernel_js_1.refreshProject)(projectArg(args), { full: args.includes("--full") });
|
|
409
470
|
if (args.includes("--json")) {
|
|
@@ -1377,12 +1438,17 @@ async function main() {
|
|
|
1377
1438
|
tags: listArg(takeArg(args, "--tags")),
|
|
1378
1439
|
paths: listArg(takeArg(args, "--paths")),
|
|
1379
1440
|
stack: listArg(takeArg(args, "--stack")),
|
|
1441
|
+
graphNodes: listArg(takeArg(args, "--graph-nodes")),
|
|
1442
|
+
allowMissingPaths: args.includes("--allow-missing-paths"),
|
|
1443
|
+
strictCitations: true,
|
|
1380
1444
|
});
|
|
1381
1445
|
if (!result.ok) {
|
|
1382
1446
|
console.error(`Learning capture blocked:\n${result.errors.map((error) => ` - ${error}`).join("\n")}`);
|
|
1383
1447
|
process.exit(2);
|
|
1384
1448
|
}
|
|
1385
1449
|
console.log(`Captured session learning: ${result.path}`);
|
|
1450
|
+
if (result.warnings?.length)
|
|
1451
|
+
console.log(`Warnings:\n${result.warnings.map((warning) => ` - ${warning}`).join("\n")}`);
|
|
1386
1452
|
console.log("Repo-local memory is written immediately. Promotion to org/global still requires explicit review.");
|
|
1387
1453
|
return;
|
|
1388
1454
|
}
|
|
@@ -1565,9 +1631,11 @@ async function main() {
|
|
|
1565
1631
|
const query = firstPositional(args);
|
|
1566
1632
|
if (!query)
|
|
1567
1633
|
usage();
|
|
1634
|
+
const maxContextTokens = args.includes("--max-context-tokens") ? numberArg(args, "--max-context-tokens", 0) : undefined;
|
|
1635
|
+
const structuralHops = args.includes("--structural-hops") ? numberArg(args, "--structural-hops", 2) : undefined;
|
|
1568
1636
|
const result = args.includes("--embeddings")
|
|
1569
1637
|
? await (0, kernel_js_1.recallWithEmbeddings)(projectArg(args), query, 5, args.includes("--explain"))
|
|
1570
|
-
: (0, kernel_js_1.recall)(projectArg(args), query, 5, args.includes("--explain"));
|
|
1638
|
+
: (0, kernel_js_1.recall)(projectArg(args), query, 5, args.includes("--explain"), { maxContextTokens, structuralHops });
|
|
1571
1639
|
if (args.includes("--json"))
|
|
1572
1640
|
console.log(JSON.stringify(result, null, 2));
|
|
1573
1641
|
else
|
|
@@ -1711,6 +1779,9 @@ async function main() {
|
|
|
1711
1779
|
tags: listArg(takeArg(args, "--tags")),
|
|
1712
1780
|
paths: listArg(takeArg(args, "--paths")),
|
|
1713
1781
|
stack: listArg(takeArg(args, "--stack")),
|
|
1782
|
+
graphNodes: listArg(takeArg(args, "--graph-nodes")),
|
|
1783
|
+
allowMissingPaths: args.includes("--allow-missing-paths"),
|
|
1784
|
+
strictCitations: true,
|
|
1714
1785
|
};
|
|
1715
1786
|
const result = (0, kernel_js_1.capture)(input);
|
|
1716
1787
|
if (!result.ok) {
|
|
@@ -1718,6 +1789,8 @@ async function main() {
|
|
|
1718
1789
|
process.exit(2);
|
|
1719
1790
|
}
|
|
1720
1791
|
console.log(`Captured repo-local packet: ${result.path}`);
|
|
1792
|
+
if (result.warnings?.length)
|
|
1793
|
+
console.log(`Warnings:\n${result.warnings.map((warning) => ` - ${warning}`).join("\n")}`);
|
|
1721
1794
|
console.log("Repo-local memory is written immediately. Promotion to org/global still requires explicit review.");
|
|
1722
1795
|
return;
|
|
1723
1796
|
}
|
package/dist/index.js
CHANGED
|
@@ -152,6 +152,8 @@ function listTools() {
|
|
|
152
152
|
explain: { type: "boolean" },
|
|
153
153
|
embeddings: { type: "boolean" },
|
|
154
154
|
json: { type: "boolean" },
|
|
155
|
+
max_context_tokens: { type: "number" },
|
|
156
|
+
structural_hops: { type: "number", description: "If >0, append a bounded N-hop code-graph blast radius seeded from the recalled memory's files." },
|
|
155
157
|
},
|
|
156
158
|
required: ["query", "project_dir"],
|
|
157
159
|
},
|
|
@@ -668,7 +670,7 @@ function listTools() {
|
|
|
668
670
|
},
|
|
669
671
|
{
|
|
670
672
|
name: "kage_learn",
|
|
671
|
-
description: "Capture an actual reusable learning from the current session as repo-local memory. Prefer this over diff proposal when the agent knows what was learned.",
|
|
673
|
+
description: "Capture an actual reusable learning from the current session as repo-local memory. Prefer this over diff proposal when the agent knows what was learned. Capture is rejected if every referenced path is missing from the repo; set allow_missing_paths to record anyway (e.g. a file you are about to create).",
|
|
672
674
|
inputSchema: {
|
|
673
675
|
type: "object",
|
|
674
676
|
properties: {
|
|
@@ -681,13 +683,15 @@ function listTools() {
|
|
|
681
683
|
tags: { type: "array", items: { type: "string" } },
|
|
682
684
|
paths: { type: "array", items: { type: "string" } },
|
|
683
685
|
stack: { type: "array", items: { type: "string" } },
|
|
686
|
+
graph_nodes: { type: "array", items: { type: "string" } },
|
|
687
|
+
allow_missing_paths: { type: "boolean" },
|
|
684
688
|
},
|
|
685
689
|
required: ["project_dir", "learning"],
|
|
686
690
|
},
|
|
687
691
|
},
|
|
688
692
|
{
|
|
689
693
|
name: "kage_capture",
|
|
690
|
-
description: "Create a repo-local Kage memory packet immediately. Org/global promotion still requires explicit human review.",
|
|
694
|
+
description: "Create a repo-local Kage memory packet immediately. Org/global promotion still requires explicit human review. Capture is rejected if every referenced path is missing from the repo; set allow_missing_paths to record anyway.",
|
|
691
695
|
inputSchema: {
|
|
692
696
|
type: "object",
|
|
693
697
|
properties: {
|
|
@@ -699,10 +703,36 @@ function listTools() {
|
|
|
699
703
|
tags: { type: "array", items: { type: "string" } },
|
|
700
704
|
paths: { type: "array", items: { type: "string" } },
|
|
701
705
|
stack: { type: "array", items: { type: "string" } },
|
|
706
|
+
graph_nodes: { type: "array", items: { type: "string" }, description: "Code-graph node references (symbol/route/file) this memory is about." },
|
|
707
|
+
allow_missing_paths: { type: "boolean" },
|
|
702
708
|
},
|
|
703
709
|
required: ["project_dir", "title", "body"],
|
|
704
710
|
},
|
|
705
711
|
},
|
|
712
|
+
{
|
|
713
|
+
name: "kage_verify_citations",
|
|
714
|
+
description: "Verify that a memory packet's cited file paths still exist and that the memory is not stale, before trusting it. Pass an id to check one packet, or omit to audit all approved repo memory. Returns grounding and staleness for each.",
|
|
715
|
+
inputSchema: {
|
|
716
|
+
type: "object",
|
|
717
|
+
properties: {
|
|
718
|
+
project_dir: { type: "string" },
|
|
719
|
+
id: { type: "string" },
|
|
720
|
+
},
|
|
721
|
+
required: ["project_dir"],
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
name: "kage_compact",
|
|
726
|
+
description: "Consolidate repo memory: prune dead citations, deprecate hard-stale packets, and surface near-duplicate clusters to merge (via kage_supersede). Defaults to a dry run; pass dry_run=false to apply pruning/deprecation. Duplicate merging stays an agent decision — no hosted LLM is used.",
|
|
727
|
+
inputSchema: {
|
|
728
|
+
type: "object",
|
|
729
|
+
properties: {
|
|
730
|
+
project_dir: { type: "string" },
|
|
731
|
+
dry_run: { type: "boolean" },
|
|
732
|
+
},
|
|
733
|
+
required: ["project_dir"],
|
|
734
|
+
},
|
|
735
|
+
},
|
|
706
736
|
{
|
|
707
737
|
name: "kage_observe",
|
|
708
738
|
description: "Store an automatic local observation event from an agent session. Observations are privacy-scanned, deduplicated, and never published automatically.",
|
|
@@ -1092,9 +1122,11 @@ async function callTool(name, args) {
|
|
|
1092
1122
|
};
|
|
1093
1123
|
}
|
|
1094
1124
|
if (name === "kage_recall") {
|
|
1125
|
+
const maxContextTokens = typeof args?.max_context_tokens === "number" ? args.max_context_tokens : undefined;
|
|
1126
|
+
const structuralHops = typeof args?.structural_hops === "number" ? args.structural_hops : undefined;
|
|
1095
1127
|
const result = args?.embeddings
|
|
1096
1128
|
? await (0, kernel_js_1.recallWithEmbeddings)(String(args?.project_dir ?? ""), String(args?.query ?? ""), Number(args?.limit ?? 5), Boolean(args?.explain))
|
|
1097
|
-
: (0, kernel_js_1.recall)(String(args?.project_dir ?? ""), String(args?.query ?? ""), Number(args?.limit ?? 5), Boolean(args?.explain));
|
|
1129
|
+
: (0, kernel_js_1.recall)(String(args?.project_dir ?? ""), String(args?.query ?? ""), Number(args?.limit ?? 5), Boolean(args?.explain), { maxContextTokens, structuralHops });
|
|
1098
1130
|
return {
|
|
1099
1131
|
content: [{ type: "text", text: args?.json || args?.explain ? JSON.stringify(result, null, 2) : result.context_block }],
|
|
1100
1132
|
};
|
|
@@ -1403,13 +1435,17 @@ async function callTool(name, args) {
|
|
|
1403
1435
|
tags: arrayArg(args?.tags),
|
|
1404
1436
|
paths: arrayArg(args?.paths),
|
|
1405
1437
|
stack: arrayArg(args?.stack),
|
|
1438
|
+
graphNodes: arrayArg(args?.graph_nodes),
|
|
1439
|
+
allowMissingPaths: Boolean(args?.allow_missing_paths),
|
|
1440
|
+
strictCitations: true,
|
|
1406
1441
|
});
|
|
1442
|
+
const learnWarnings = result.warnings?.length ? `\nWarnings:\n${result.warnings.map((warning) => `- ${warning}`).join("\n")}` : "";
|
|
1407
1443
|
return {
|
|
1408
1444
|
content: [
|
|
1409
1445
|
{
|
|
1410
1446
|
type: "text",
|
|
1411
1447
|
text: result.ok
|
|
1412
|
-
? `Captured session learning: ${result.path}\nRepo-local memory is written immediately. Org/global promotion still requires explicit review
|
|
1448
|
+
? `Captured session learning: ${result.path}\nRepo-local memory is written immediately. Org/global promotion still requires explicit review.${learnWarnings}`
|
|
1413
1449
|
: `Learning capture blocked:\n${result.errors.map((error) => `- ${error}`).join("\n")}`,
|
|
1414
1450
|
},
|
|
1415
1451
|
],
|
|
@@ -1426,19 +1462,41 @@ async function callTool(name, args) {
|
|
|
1426
1462
|
tags: arrayArg(args?.tags),
|
|
1427
1463
|
paths: arrayArg(args?.paths),
|
|
1428
1464
|
stack: arrayArg(args?.stack),
|
|
1465
|
+
graphNodes: arrayArg(args?.graph_nodes),
|
|
1466
|
+
allowMissingPaths: Boolean(args?.allow_missing_paths),
|
|
1467
|
+
strictCitations: true,
|
|
1429
1468
|
});
|
|
1469
|
+
const captureWarnings = result.warnings?.length ? `\nWarnings:\n${result.warnings.map((warning) => `- ${warning}`).join("\n")}` : "";
|
|
1430
1470
|
return {
|
|
1431
1471
|
content: [
|
|
1432
1472
|
{
|
|
1433
1473
|
type: "text",
|
|
1434
1474
|
text: result.ok
|
|
1435
|
-
? `Captured repo-local packet: ${result.path}\nOrg/global promotion still requires explicit review
|
|
1475
|
+
? `Captured repo-local packet: ${result.path}\nOrg/global promotion still requires explicit review.${captureWarnings}`
|
|
1436
1476
|
: `Capture blocked:\n${result.errors.map((error) => `- ${error}`).join("\n")}`,
|
|
1437
1477
|
},
|
|
1438
1478
|
],
|
|
1439
1479
|
isError: !result.ok,
|
|
1440
1480
|
};
|
|
1441
1481
|
}
|
|
1482
|
+
if (name === "kage_verify_citations") {
|
|
1483
|
+
const result = (0, kernel_js_1.verifyCitations)(String(args?.project_dir ?? ""), {
|
|
1484
|
+
id: args?.id ? String(args.id) : undefined,
|
|
1485
|
+
});
|
|
1486
|
+
return {
|
|
1487
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1488
|
+
isError: !result.ok,
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
if (name === "kage_compact") {
|
|
1492
|
+
const result = (0, kernel_js_1.compactProject)(String(args?.project_dir ?? ""), {
|
|
1493
|
+
dryRun: args?.dry_run === undefined ? true : Boolean(args.dry_run),
|
|
1494
|
+
});
|
|
1495
|
+
return {
|
|
1496
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1497
|
+
isError: !result.ok,
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1442
1500
|
if (name === "kage_observe") {
|
|
1443
1501
|
const projectDir = String(args?.project_dir ?? "");
|
|
1444
1502
|
const event = { ...args };
|
package/dist/kernel.js
CHANGED
|
@@ -83,6 +83,8 @@ exports.buildIndexes = buildIndexes;
|
|
|
83
83
|
exports.indexProject = indexProject;
|
|
84
84
|
exports.refreshProject = refreshProject;
|
|
85
85
|
exports.gcProject = gcProject;
|
|
86
|
+
exports.verifyCitations = verifyCitations;
|
|
87
|
+
exports.compactProject = compactProject;
|
|
86
88
|
exports.installAgentPolicy = installAgentPolicy;
|
|
87
89
|
exports.createDenseEmbeddingProvider = createDenseEmbeddingProvider;
|
|
88
90
|
exports.buildEmbeddingIndex = buildEmbeddingIndex;
|
|
@@ -180,6 +182,27 @@ exports.MEMORY_TYPES = [
|
|
|
180
182
|
"negative_result",
|
|
181
183
|
"constraint",
|
|
182
184
|
];
|
|
185
|
+
// Bounded context assembly (PRD Feature 2: "inject only the relevant rule + structural
|
|
186
|
+
// map, dropping the rest"). Opt-in: when a token budget is set, keep the highest-priority
|
|
187
|
+
// sections (preamble + code graph + memory come first in the block) and drop trailing
|
|
188
|
+
// lower-priority sections until the estimate fits. Default (no budget) is unchanged.
|
|
189
|
+
function boundContextBlock(block, budget) {
|
|
190
|
+
if (!Number.isFinite(budget) || budget <= 0 || estimateTokens(block) <= budget)
|
|
191
|
+
return block;
|
|
192
|
+
const parts = block.split(/\n(?=## )/);
|
|
193
|
+
const kept = [parts[0]];
|
|
194
|
+
let dropped = 0;
|
|
195
|
+
for (const section of parts.slice(1)) {
|
|
196
|
+
if (estimateTokens([...kept, section].join("\n")) <= budget)
|
|
197
|
+
kept.push(section);
|
|
198
|
+
else
|
|
199
|
+
dropped += 1;
|
|
200
|
+
}
|
|
201
|
+
const result = kept.join("\n");
|
|
202
|
+
return dropped
|
|
203
|
+
? `${result}\n\n_Context trimmed to ~${budget} tokens; ${dropped} lower-priority section(s) dropped._`
|
|
204
|
+
: result;
|
|
205
|
+
}
|
|
183
206
|
const graphMemoryCache = new Map();
|
|
184
207
|
exports.SETUP_AGENTS = [
|
|
185
208
|
"codex",
|
|
@@ -1119,6 +1142,52 @@ function staleMemoryReasons(projectDir, packet, fingerprintCache) {
|
|
|
1119
1142
|
}
|
|
1120
1143
|
return unique(reasons);
|
|
1121
1144
|
}
|
|
1145
|
+
// Classifies stale reasons into severity. "hard" reasons (deprecated status, user
|
|
1146
|
+
// reported stale, ttl expired, all citations deleted) mean the memory should be
|
|
1147
|
+
// excluded from recall; "soft" reasons (some citations missing, a linked file
|
|
1148
|
+
// changed) mean keep-but-flag — the memory may just need review, not suppression.
|
|
1149
|
+
function staleSeverity(reasons) {
|
|
1150
|
+
if (!reasons.length)
|
|
1151
|
+
return "none";
|
|
1152
|
+
const hard = reasons.some((reason) => reason.startsWith("packet status is") ||
|
|
1153
|
+
reason.startsWith("user or agent reported") ||
|
|
1154
|
+
reason.startsWith("freshness ttl expired") ||
|
|
1155
|
+
reason.startsWith("all referenced paths are missing"));
|
|
1156
|
+
return hard ? "hard" : "soft";
|
|
1157
|
+
}
|
|
1158
|
+
// Decides whether a packet should be excluded from the recall payload (PRD Feature 3:
|
|
1159
|
+
// "deleted or heavily refactored since the timestamp"). Distinct from staleMemoryReasons:
|
|
1160
|
+
// a citation that NEVER existed (no stored fingerprint) is an ungrounded write — guarded
|
|
1161
|
+
// at capture time — not recall-time staleness, so it does NOT trigger exclusion here.
|
|
1162
|
+
// Returns a reason string when the memory is hard-stale, otherwise null.
|
|
1163
|
+
function recallHardStaleReason(projectDir, packet, cache) {
|
|
1164
|
+
if (packet.status === "deprecated" || packet.status === "superseded")
|
|
1165
|
+
return `packet status is ${packet.status}`;
|
|
1166
|
+
const quality = (packet.quality ?? {});
|
|
1167
|
+
if (Number(quality.reports_stale ?? 0) > 0)
|
|
1168
|
+
return "user or agent reported this memory stale";
|
|
1169
|
+
const freshness = (packet.freshness ?? {});
|
|
1170
|
+
const ttlDays = Number(freshness.ttl_days ?? freshness.ttlDays ?? 0);
|
|
1171
|
+
const verifiedAt = Date.parse(String(freshness.last_verified_at ?? packet.updated_at ?? packet.created_at));
|
|
1172
|
+
if (Number.isFinite(ttlDays) && ttlDays > 0 && Number.isFinite(verifiedAt)) {
|
|
1173
|
+
const ageDays = (Date.now() - verifiedAt) / (1000 * 60 * 60 * 24);
|
|
1174
|
+
if (ageDays > ttlDays)
|
|
1175
|
+
return `freshness ttl expired (${Math.floor(ageDays)}d old, ttl ${ttlDays}d)`;
|
|
1176
|
+
}
|
|
1177
|
+
// Only paths that existed at capture get a stored fingerprint; if every one of them is
|
|
1178
|
+
// now gone, the memory's evidence was deleted out from under it.
|
|
1179
|
+
const stored = packetStoredPathFingerprints(packet);
|
|
1180
|
+
if (stored.length) {
|
|
1181
|
+
const deleted = stored.filter((fingerprint) => {
|
|
1182
|
+
const current = memoryPathFingerprint(projectDir, fingerprint.path, cache);
|
|
1183
|
+
return current === null;
|
|
1184
|
+
});
|
|
1185
|
+
if (deleted.length === stored.length) {
|
|
1186
|
+
return `all cited files deleted since capture: ${deleted.slice(0, 4).map((fingerprint) => fingerprint.path).join(", ")}`;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return null;
|
|
1190
|
+
}
|
|
1122
1191
|
function changedPathsFromStaleReasons(reasons) {
|
|
1123
1192
|
return unique(reasons.flatMap((reason) => {
|
|
1124
1193
|
const match = reason.match(/^linked path changed since memory was verified: (.+)$/);
|
|
@@ -1523,13 +1592,28 @@ function walkFiles(root, predicate) {
|
|
|
1523
1592
|
}
|
|
1524
1593
|
return out.sort();
|
|
1525
1594
|
}
|
|
1595
|
+
// Tolerant packet read: a single corrupt or merge-conflicted packet (e.g. an
|
|
1596
|
+
// unresolved `<<<<<<<` from a teammate's git merge) must not take down all of
|
|
1597
|
+
// recall/verify/compact. Skip the bad file with a warning and keep going.
|
|
1598
|
+
function tryReadPacket(path) {
|
|
1599
|
+
try {
|
|
1600
|
+
return readJson(path);
|
|
1601
|
+
}
|
|
1602
|
+
catch (error) {
|
|
1603
|
+
process.stderr.write(`kage: skipping unreadable memory packet ${path}: ${error.message}\n`);
|
|
1604
|
+
return null;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1526
1607
|
function loadPacketsFromDir(dir) {
|
|
1527
1608
|
if (!(0, node_fs_1.existsSync)(dir))
|
|
1528
1609
|
return [];
|
|
1529
1610
|
return (0, node_fs_1.readdirSync)(dir)
|
|
1530
1611
|
.filter((name) => name.endsWith(".json"))
|
|
1531
1612
|
.sort()
|
|
1532
|
-
.
|
|
1613
|
+
.flatMap((name) => {
|
|
1614
|
+
const packet = tryReadPacket((0, node_path_1.join)(dir, name));
|
|
1615
|
+
return packet ? [packet] : [];
|
|
1616
|
+
});
|
|
1533
1617
|
}
|
|
1534
1618
|
function loadPacketEntriesFromDir(dir) {
|
|
1535
1619
|
if (!(0, node_fs_1.existsSync)(dir))
|
|
@@ -1537,9 +1621,10 @@ function loadPacketEntriesFromDir(dir) {
|
|
|
1537
1621
|
return (0, node_fs_1.readdirSync)(dir)
|
|
1538
1622
|
.filter((name) => name.endsWith(".json"))
|
|
1539
1623
|
.sort()
|
|
1540
|
-
.
|
|
1624
|
+
.flatMap((name) => {
|
|
1541
1625
|
const path = (0, node_path_1.join)(dir, name);
|
|
1542
|
-
|
|
1626
|
+
const packet = tryReadPacket(path);
|
|
1627
|
+
return packet ? [{ path, packet }] : [];
|
|
1543
1628
|
});
|
|
1544
1629
|
}
|
|
1545
1630
|
function loadApprovedPackets(projectDir) {
|
|
@@ -5510,6 +5595,115 @@ function gcProject(projectDir, options = {}) {
|
|
|
5510
5595
|
total_scanned: packetEntries.length,
|
|
5511
5596
|
};
|
|
5512
5597
|
}
|
|
5598
|
+
// On-demand citation/freshness check the agent can call before trusting a memory.
|
|
5599
|
+
// Pass an id to verify one packet, or omit to audit all approved memory.
|
|
5600
|
+
function verifyCitations(projectDir, options = {}) {
|
|
5601
|
+
ensureMemoryDirs(projectDir);
|
|
5602
|
+
const approved = loadApprovedPackets(projectDir);
|
|
5603
|
+
const targets = options.id ? approved.filter((packet) => packet.id === options.id) : approved;
|
|
5604
|
+
if (options.id && !targets.length) {
|
|
5605
|
+
return { ok: false, project_dir: projectDir, checked: 0, valid: 0, stale: 0, ungrounded: 0, packets: [], errors: [`Approved packet not found: ${options.id}`] };
|
|
5606
|
+
}
|
|
5607
|
+
const cache = new Map();
|
|
5608
|
+
const packets = targets.map((packet) => {
|
|
5609
|
+
const meaningful = packet.paths.filter((path) => meaningfulMemoryPath(path) && !shouldSkipRepoMemoryPath(path));
|
|
5610
|
+
const missing = meaningful.filter((path) => !pathExistsInRepo(projectDir, path));
|
|
5611
|
+
const reasons = staleMemoryReasons(projectDir, packet, cache);
|
|
5612
|
+
const severity = staleSeverity(reasons);
|
|
5613
|
+
const grounded = packetGroundingWarnings(projectDir, packet, "packet").length === 0;
|
|
5614
|
+
return {
|
|
5615
|
+
id: packet.id,
|
|
5616
|
+
title: packet.title,
|
|
5617
|
+
status: packet.status,
|
|
5618
|
+
paths: packet.paths,
|
|
5619
|
+
missing_paths: missing,
|
|
5620
|
+
grounded,
|
|
5621
|
+
stale: severity !== "none",
|
|
5622
|
+
stale_severity: severity,
|
|
5623
|
+
stale_reasons: reasons,
|
|
5624
|
+
};
|
|
5625
|
+
});
|
|
5626
|
+
return {
|
|
5627
|
+
ok: true,
|
|
5628
|
+
project_dir: projectDir,
|
|
5629
|
+
checked: packets.length,
|
|
5630
|
+
valid: packets.filter((entry) => !entry.stale && entry.grounded).length,
|
|
5631
|
+
stale: packets.filter((entry) => entry.stale).length,
|
|
5632
|
+
ungrounded: packets.filter((entry) => !entry.grounded).length,
|
|
5633
|
+
packets,
|
|
5634
|
+
errors: [],
|
|
5635
|
+
};
|
|
5636
|
+
}
|
|
5637
|
+
// Deterministic memory consolidation (no hosted LLM — preserves the no-API-key promise):
|
|
5638
|
+
// 1. prune dead citations from packets and refresh their path fingerprints,
|
|
5639
|
+
// 2. deprecate hard-stale packets (delegating to the same severity rules as recall/gc),
|
|
5640
|
+
// 3. surface near-duplicate clusters for an agent to merge via kage_supersede.
|
|
5641
|
+
function compactProject(projectDir, options = {}) {
|
|
5642
|
+
ensureMemoryDirs(projectDir);
|
|
5643
|
+
const dryRun = options.dryRun === true;
|
|
5644
|
+
const entries = loadPacketEntriesFromDir(packetsDir(projectDir));
|
|
5645
|
+
const prunedCitations = [];
|
|
5646
|
+
const deprecated = [];
|
|
5647
|
+
const cache = new Map();
|
|
5648
|
+
for (const { path, packet } of entries) {
|
|
5649
|
+
if (packet.status === "deprecated" || packet.status === "superseded")
|
|
5650
|
+
continue;
|
|
5651
|
+
const hardReason = recallHardStaleReason(projectDir, packet, cache);
|
|
5652
|
+
if (hardReason) {
|
|
5653
|
+
deprecated.push({ id: packet.id, title: packet.title, reason: hardReason });
|
|
5654
|
+
if (!dryRun)
|
|
5655
|
+
writeJson(path, { ...packet, status: "deprecated", updated_at: nowIso() });
|
|
5656
|
+
continue;
|
|
5657
|
+
}
|
|
5658
|
+
const meaningful = packet.paths.filter((p) => meaningfulMemoryPath(p) && !shouldSkipRepoMemoryPath(p));
|
|
5659
|
+
const missing = meaningful.filter((p) => !pathExistsInRepo(projectDir, p));
|
|
5660
|
+
if (missing.length) {
|
|
5661
|
+
const keptPaths = packet.paths.filter((p) => !missing.includes(p));
|
|
5662
|
+
prunedCitations.push({ id: packet.id, title: packet.title, removed_paths: missing });
|
|
5663
|
+
if (!dryRun) {
|
|
5664
|
+
writeJson(path, {
|
|
5665
|
+
...packet,
|
|
5666
|
+
paths: keptPaths,
|
|
5667
|
+
freshness: {
|
|
5668
|
+
...(packet.freshness ?? {}),
|
|
5669
|
+
path_fingerprints: memoryPathFingerprints(projectDir, keptPaths),
|
|
5670
|
+
last_verified_at: nowIso(),
|
|
5671
|
+
},
|
|
5672
|
+
updated_at: nowIso(),
|
|
5673
|
+
});
|
|
5674
|
+
}
|
|
5675
|
+
}
|
|
5676
|
+
}
|
|
5677
|
+
// Cluster near-duplicate approved packets (report only — merging is an agent decision).
|
|
5678
|
+
const context = memoryQualityContext(projectDir);
|
|
5679
|
+
const approved = context.packets.filter((packet) => packet.status === "approved");
|
|
5680
|
+
const seen = new Set();
|
|
5681
|
+
const clusters = [];
|
|
5682
|
+
for (const packet of approved) {
|
|
5683
|
+
if (seen.has(packet.id))
|
|
5684
|
+
continue;
|
|
5685
|
+
const dupes = duplicateCandidatesWithContext(packet, context, 0.6).filter((dupe) => dupe.status === "approved");
|
|
5686
|
+
if (!dupes.length)
|
|
5687
|
+
continue;
|
|
5688
|
+
const members = [{ id: packet.id, title: packet.title }, ...dupes.map((dupe) => ({ id: dupe.id, title: dupe.title }))];
|
|
5689
|
+
members.forEach((member) => seen.add(member.id));
|
|
5690
|
+
clusters.push({ score: Math.max(...dupes.map((dupe) => dupe.score)), packets: members });
|
|
5691
|
+
}
|
|
5692
|
+
if (!dryRun && (prunedCitations.length || deprecated.length)) {
|
|
5693
|
+
const rebuilt = buildGraphIndexes(projectDir);
|
|
5694
|
+
writeJson((0, node_path_1.join)(memoryRoot(projectDir), "metrics.json"), kageMetricsShallow(projectDir, rebuilt));
|
|
5695
|
+
}
|
|
5696
|
+
return {
|
|
5697
|
+
ok: true,
|
|
5698
|
+
project_dir: projectDir,
|
|
5699
|
+
dry_run: dryRun,
|
|
5700
|
+
pruned_citations: prunedCitations,
|
|
5701
|
+
deprecated,
|
|
5702
|
+
duplicate_clusters: clusters,
|
|
5703
|
+
total_scanned: entries.length,
|
|
5704
|
+
errors: [],
|
|
5705
|
+
};
|
|
5706
|
+
}
|
|
5513
5707
|
function installAgentPolicy(projectDir) {
|
|
5514
5708
|
const agentsPath = (0, node_path_1.join)(projectDir, "AGENTS.md");
|
|
5515
5709
|
const claudePath = (0, node_path_1.join)(projectDir, "CLAUDE.md");
|
|
@@ -6277,7 +6471,23 @@ function recallWithVectorScores(projectDir, query, limit = 5, explain = false, i
|
|
|
6277
6471
|
})()
|
|
6278
6472
|
: recallQueryExpansion(query);
|
|
6279
6473
|
const terms = expansion.terms;
|
|
6280
|
-
const
|
|
6474
|
+
const allApprovedPackets = loadApprovedPackets(projectDir);
|
|
6475
|
+
const includeStale = inputs.includeStale === true;
|
|
6476
|
+
const staleFingerprintCache = new Map();
|
|
6477
|
+
const suppressed = [];
|
|
6478
|
+
// Just-in-time staleness gate: hard-stale memory (deleted citations, expired ttl,
|
|
6479
|
+
// reported stale) is excluded from the recall payload so the agent never sees it
|
|
6480
|
+
// as valid. Suppression is recorded (not silent) so `kage verify` can explain it.
|
|
6481
|
+
const approvedPackets = includeStale
|
|
6482
|
+
? allApprovedPackets
|
|
6483
|
+
: allApprovedPackets.filter((packet) => {
|
|
6484
|
+
const reason = recallHardStaleReason(projectDir, packet, staleFingerprintCache);
|
|
6485
|
+
if (reason) {
|
|
6486
|
+
suppressed.push({ id: packet.id, title: packet.title, reason });
|
|
6487
|
+
return false;
|
|
6488
|
+
}
|
|
6489
|
+
return true;
|
|
6490
|
+
});
|
|
6281
6491
|
const baseScores = scorePacketsBm25(expansion.baseTerms, approvedPackets);
|
|
6282
6492
|
const temporalScores = scorePacketsBm25(expansion.temporalTerms, approvedPackets);
|
|
6283
6493
|
const semanticScores = scorePacketsBm25(expansion.semanticTerms, approvedPackets);
|
|
@@ -6335,6 +6545,13 @@ function recallWithVectorScores(projectDir, query, limit = 5, explain = false, i
|
|
|
6335
6545
|
const graphContext = queryGraph(projectDir, query, 5, knowledgeGraph);
|
|
6336
6546
|
const codeContext = queryCodeGraph(projectDir, query, 5, codeGraph);
|
|
6337
6547
|
const pinnedContext = renderPinnedRepoContext(readContextSlots(projectDir));
|
|
6548
|
+
// PRD Feature 2: traverse the code graph outward from the recalled memory's files
|
|
6549
|
+
// (the semantic entry point) to assemble a bounded structural blast radius. Opt-in
|
|
6550
|
+
// via inputs.structuralHops so default recall output is unchanged.
|
|
6551
|
+
const structuralHops = inputs.structuralHops ?? 0;
|
|
6552
|
+
const blastRadius = structuralHops > 0
|
|
6553
|
+
? structuralBlastRadius(codeGraph, unique(scored.flatMap((entry) => entry.packet.paths).filter((path) => meaningfulMemoryPath(path))), structuralHops)
|
|
6554
|
+
: [];
|
|
6338
6555
|
const lines = [
|
|
6339
6556
|
`# Kage Context`,
|
|
6340
6557
|
"",
|
|
@@ -6347,6 +6564,9 @@ function recallWithVectorScores(projectDir, query, limit = 5, explain = false, i
|
|
|
6347
6564
|
...codeContext.tests.slice(0, 3).map((test, index) => `${index + 1}. [test] ${test.title} in ${test.test_path}:${test.line}${test.covers_symbol ? ` covers ${test.covers_symbol}` : ""}`),
|
|
6348
6565
|
...(!codeContext.symbols.length && !codeContext.routes.length && !codeContext.tests.length ? codeContext.files.slice(0, 3).map((file, index) => `${index + 1}. [file] ${file.path} (${file.kind})`) : []),
|
|
6349
6566
|
"",
|
|
6567
|
+
...(blastRadius.length
|
|
6568
|
+
? [`## Structural Blast Radius (${structuralHops}-hop)`, ...blastRadius.map((path, index) => `${index + 1}. ${path}`), ""]
|
|
6569
|
+
: []),
|
|
6350
6570
|
scored.length ? "## Relevant Memory" : "No relevant repo memory found.",
|
|
6351
6571
|
...scored.flatMap((entry, index) => [
|
|
6352
6572
|
"",
|
|
@@ -6367,11 +6587,16 @@ function recallWithVectorScores(projectDir, query, limit = 5, explain = false, i
|
|
|
6367
6587
|
"",
|
|
6368
6588
|
graphContext.edges.length ? "## Related Graph Facts" : "",
|
|
6369
6589
|
...graphContext.edges.slice(0, 5).map((edge, index) => `${index + 1}. ${edge.fact} (evidence: ${edge.evidence.join(", ")})`),
|
|
6590
|
+
...(suppressed.length
|
|
6591
|
+
? ["", `_${suppressed.length} stale memory packet(s) excluded from recall. Run kage verify for details._`]
|
|
6592
|
+
: []),
|
|
6370
6593
|
];
|
|
6594
|
+
const assembledBlock = lines.join("\n");
|
|
6371
6595
|
const result = {
|
|
6372
6596
|
query,
|
|
6373
|
-
context_block:
|
|
6597
|
+
context_block: inputs.maxContextTokens ? boundContextBlock(assembledBlock, inputs.maxContextTokens) : assembledBlock,
|
|
6374
6598
|
results: scored,
|
|
6599
|
+
suppressed: suppressed.length ? suppressed : undefined,
|
|
6375
6600
|
explanations: explain
|
|
6376
6601
|
? scored.map((entry) => ({
|
|
6377
6602
|
packet_id: entry.packet.id,
|
|
@@ -6830,6 +7055,45 @@ function codeDependents(graph) {
|
|
|
6830
7055
|
}
|
|
6831
7056
|
return dependents;
|
|
6832
7057
|
}
|
|
7058
|
+
// Bounded N-hop structural traversal from a set of seed files (the files the recalled
|
|
7059
|
+
// memory is about) — the PRD's "structural blast radius". Walks both import directions
|
|
7060
|
+
// (who-depends-on and what-it-depends-on), excludes the seeds, and ranks by how many
|
|
7061
|
+
// files depend on each node. Pure graph traversal; no full-repo scan.
|
|
7062
|
+
function structuralBlastRadius(graph, seedPaths, hops, limit = 8) {
|
|
7063
|
+
const seeds = seedPaths.filter(Boolean);
|
|
7064
|
+
if (!seeds.length || hops <= 0)
|
|
7065
|
+
return [];
|
|
7066
|
+
const dependents = codeDependents(graph);
|
|
7067
|
+
const dependencies = new Map();
|
|
7068
|
+
for (const edge of graph.imports) {
|
|
7069
|
+
if (!edge.from_path || !edge.to_path)
|
|
7070
|
+
continue;
|
|
7071
|
+
const list = dependencies.get(edge.from_path) ?? new Set();
|
|
7072
|
+
list.add(edge.to_path);
|
|
7073
|
+
dependencies.set(edge.from_path, list);
|
|
7074
|
+
}
|
|
7075
|
+
const seedSet = new Set(seeds);
|
|
7076
|
+
const visited = new Set();
|
|
7077
|
+
let frontier = new Set(seeds);
|
|
7078
|
+
for (let depth = 0; depth < hops; depth += 1) {
|
|
7079
|
+
const next = new Set();
|
|
7080
|
+
for (const node of frontier) {
|
|
7081
|
+
for (const neighbor of [...(dependents.get(node) ?? []), ...(dependencies.get(node) ?? [])]) {
|
|
7082
|
+
if (seedSet.has(neighbor) || visited.has(neighbor))
|
|
7083
|
+
continue;
|
|
7084
|
+
visited.add(neighbor);
|
|
7085
|
+
next.add(neighbor);
|
|
7086
|
+
}
|
|
7087
|
+
}
|
|
7088
|
+
frontier = next;
|
|
7089
|
+
}
|
|
7090
|
+
const score = new Map();
|
|
7091
|
+
for (const [path, incoming] of dependents.entries())
|
|
7092
|
+
score.set(path, incoming.size);
|
|
7093
|
+
return [...visited]
|
|
7094
|
+
.sort((a, b) => (score.get(b) ?? 0) - (score.get(a) ?? 0) || a.localeCompare(b))
|
|
7095
|
+
.slice(0, limit);
|
|
7096
|
+
}
|
|
6833
7097
|
function impactSurface(target, dependents, graph) {
|
|
6834
7098
|
const visited = new Set();
|
|
6835
7099
|
let frontier = new Set([target]);
|
|
@@ -10453,6 +10717,9 @@ function learn(input) {
|
|
|
10453
10717
|
paths: input.paths,
|
|
10454
10718
|
stack: input.stack,
|
|
10455
10719
|
context: input.context,
|
|
10720
|
+
allowMissingPaths: input.allowMissingPaths,
|
|
10721
|
+
strictCitations: input.strictCitations,
|
|
10722
|
+
graphNodes: input.graphNodes,
|
|
10456
10723
|
});
|
|
10457
10724
|
}
|
|
10458
10725
|
function capture(input) {
|
|
@@ -10468,7 +10735,34 @@ function capture(input) {
|
|
|
10468
10735
|
errors: [`Sensitive content blocked: ${unique(scanFindings).join(", ")}`],
|
|
10469
10736
|
};
|
|
10470
10737
|
}
|
|
10738
|
+
const warnings = [];
|
|
10739
|
+
const meaningfulPaths = (input.paths ?? [])
|
|
10740
|
+
.filter((path) => path && meaningfulMemoryPath(path) && !shouldSkipRepoMemoryPath(path));
|
|
10741
|
+
const missingPaths = meaningfulPaths.filter((path) => !pathExistsInRepo(input.projectDir, path));
|
|
10742
|
+
// Citation validation. Strict mode (agent-facing record_memory tools / CLI) rejects a
|
|
10743
|
+
// write whose every cited path is missing — the PRD's "reject if citations don't exist".
|
|
10744
|
+
// The core library stays permissive (warn-only) for programmatic callers and migrations.
|
|
10745
|
+
if (input.strictCitations && meaningfulPaths.length && missingPaths.length === meaningfulPaths.length && !input.allowMissingPaths) {
|
|
10746
|
+
return {
|
|
10747
|
+
ok: false,
|
|
10748
|
+
errors: [
|
|
10749
|
+
`Citation validation failed: none of the referenced paths exist in this repo: ${missingPaths.join(", ")}. ` +
|
|
10750
|
+
`Fix the paths, or pass allow_missing_paths to record anyway (e.g. for a file you are about to create).`,
|
|
10751
|
+
],
|
|
10752
|
+
warnings: [],
|
|
10753
|
+
};
|
|
10754
|
+
}
|
|
10755
|
+
if (missingPaths.length) {
|
|
10756
|
+
warnings.push(`Some referenced paths do not exist in this repo: ${missingPaths.join(", ")}`);
|
|
10757
|
+
}
|
|
10471
10758
|
const createdAt = nowIso();
|
|
10759
|
+
// Agent-asserted links to code-graph nodes (PRD `graph_nodes`): the agent recording the
|
|
10760
|
+
// memory knows which symbol/route/file the rule is about, so let it declare them instead
|
|
10761
|
+
// of relying solely on background derivation.
|
|
10762
|
+
const graphEdges = unique(input.graphNodes ?? [])
|
|
10763
|
+
.map((node) => node.trim())
|
|
10764
|
+
.filter(Boolean)
|
|
10765
|
+
.map((node) => ({ relation: "references_code", to: node, evidence: "agent_capture", created_at: createdAt }));
|
|
10472
10766
|
const packet = {
|
|
10473
10767
|
schema_version: exports.PACKET_SCHEMA_VERSION,
|
|
10474
10768
|
id: makePacketId(input.projectDir, type, input.title, String(Date.now())),
|
|
@@ -10510,10 +10804,12 @@ function capture(input) {
|
|
|
10510
10804
|
},
|
|
10511
10805
|
created_at: createdAt,
|
|
10512
10806
|
updated_at: createdAt,
|
|
10807
|
+
author_branch: gitBranch(input.projectDir),
|
|
10513
10808
|
};
|
|
10809
|
+
packet.edges = graphEdges;
|
|
10514
10810
|
const validation = validatePacket(packet);
|
|
10515
10811
|
if (!validation.ok)
|
|
10516
|
-
return { ok: false, errors: validation.errors };
|
|
10812
|
+
return { ok: false, errors: validation.errors, warnings };
|
|
10517
10813
|
packet.quality = {
|
|
10518
10814
|
...packet.quality,
|
|
10519
10815
|
...evaluateMemoryQuality(input.projectDir, packet),
|
|
@@ -10525,7 +10821,7 @@ function capture(input) {
|
|
|
10525
10821
|
path: (0, node_path_1.relative)(input.projectDir, path),
|
|
10526
10822
|
source_kind: packet.source_refs[0]?.kind ?? "explicit_capture",
|
|
10527
10823
|
});
|
|
10528
|
-
return { ok: true, packet, path, errors: [] };
|
|
10824
|
+
return { ok: true, packet, path, errors: [], warnings };
|
|
10529
10825
|
}
|
|
10530
10826
|
function createPublicCandidate(projectDir, id) {
|
|
10531
10827
|
ensureMemoryDirs(projectDir);
|
|
@@ -12110,12 +12406,65 @@ function regexpEscape(value) {
|
|
|
12110
12406
|
function shellQuote(value) {
|
|
12111
12407
|
return `'${value.replace(/'/g, "'\"'\"'")}'`;
|
|
12112
12408
|
}
|
|
12113
|
-
function gitHookPath(projectDir) {
|
|
12114
|
-
const raw = readGit(projectDir, ["rev-parse", "--git-path",
|
|
12409
|
+
function gitHookPath(projectDir, hookName = "post-commit") {
|
|
12410
|
+
const raw = readGit(projectDir, ["rev-parse", "--git-path", `hooks/${hookName}`]);
|
|
12115
12411
|
if (!raw)
|
|
12116
12412
|
return null;
|
|
12117
12413
|
return (0, node_path_1.resolve)(projectDir, raw);
|
|
12118
12414
|
}
|
|
12415
|
+
// Hooks that fire after history changes underfoot (git pull / merge / checkout):
|
|
12416
|
+
// re-index repo memory so newly pulled teammate packets are immediately recallable.
|
|
12417
|
+
const KAGE_SYNC_HOOKS = ["post-merge", "post-checkout"];
|
|
12418
|
+
function kageSyncHookBlock(projectDir) {
|
|
12419
|
+
const project = shellQuote((0, node_path_1.resolve)(projectDir));
|
|
12420
|
+
return [
|
|
12421
|
+
KAGE_POST_COMMIT_HOOK_START,
|
|
12422
|
+
"# Kage sync hook: re-index repo memory after pull/merge/checkout.",
|
|
12423
|
+
"# Set KAGE_SKIP_HOOK=1 to bypass, or KAGE_BIN=/path/to/kage to override.",
|
|
12424
|
+
"if [ \"${KAGE_SKIP_HOOK:-0}\" != \"1\" ]; then",
|
|
12425
|
+
" KAGE_BIN=\"${KAGE_BIN:-kage}\"",
|
|
12426
|
+
" if command -v \"$KAGE_BIN\" >/dev/null 2>&1; then",
|
|
12427
|
+
" (",
|
|
12428
|
+
` "$KAGE_BIN" index --project ${project} --json >/dev/null 2>&1 || true`,
|
|
12429
|
+
" ) &",
|
|
12430
|
+
" fi",
|
|
12431
|
+
"fi",
|
|
12432
|
+
KAGE_POST_COMMIT_HOOK_END,
|
|
12433
|
+
].join("\n");
|
|
12434
|
+
}
|
|
12435
|
+
function installSyncHooks(projectDir) {
|
|
12436
|
+
const installed = [];
|
|
12437
|
+
for (const hookName of KAGE_SYNC_HOOKS) {
|
|
12438
|
+
const hookPath = gitHookPath(projectDir, hookName);
|
|
12439
|
+
if (!hookPath)
|
|
12440
|
+
continue;
|
|
12441
|
+
ensureDir((0, node_path_1.dirname)(hookPath));
|
|
12442
|
+
const existing = safeReadText(hookPath) ?? "";
|
|
12443
|
+
const base = stripKageHookBlock(existing);
|
|
12444
|
+
const prefix = base.trim() ? base.trimEnd() : "#!/bin/sh";
|
|
12445
|
+
const next = `${prefix}\n\n${kageSyncHookBlock(projectDir)}\n`;
|
|
12446
|
+
if (existing !== next)
|
|
12447
|
+
(0, node_fs_1.writeFileSync)(hookPath, next, "utf8");
|
|
12448
|
+
(0, node_fs_1.chmodSync)(hookPath, 0o755);
|
|
12449
|
+
installed.push(hookPath);
|
|
12450
|
+
}
|
|
12451
|
+
return installed;
|
|
12452
|
+
}
|
|
12453
|
+
function uninstallSyncHooks(projectDir) {
|
|
12454
|
+
const removed = [];
|
|
12455
|
+
for (const hookName of KAGE_SYNC_HOOKS) {
|
|
12456
|
+
const hookPath = gitHookPath(projectDir, hookName);
|
|
12457
|
+
if (!hookPath)
|
|
12458
|
+
continue;
|
|
12459
|
+
const existing = safeReadText(hookPath) ?? "";
|
|
12460
|
+
if (!hasKageHookBlock(existing))
|
|
12461
|
+
continue;
|
|
12462
|
+
(0, node_fs_1.writeFileSync)(hookPath, stripKageHookBlock(existing), "utf8");
|
|
12463
|
+
(0, node_fs_1.chmodSync)(hookPath, 0o755);
|
|
12464
|
+
removed.push(hookPath);
|
|
12465
|
+
}
|
|
12466
|
+
return removed;
|
|
12467
|
+
}
|
|
12119
12468
|
function hasKageHookBlock(content) {
|
|
12120
12469
|
return content.includes(KAGE_POST_COMMIT_HOOK_START) && content.includes(KAGE_POST_COMMIT_HOOK_END);
|
|
12121
12470
|
}
|
|
@@ -12191,20 +12540,24 @@ function kageHookInstall(projectDir) {
|
|
|
12191
12540
|
const base = stripKageHookBlock(existing);
|
|
12192
12541
|
const prefix = base.trim() ? base.trimEnd() : "#!/bin/sh";
|
|
12193
12542
|
const next = `${prefix}\n\n${kagePostCommitHookBlock(projectDir)}\n`;
|
|
12194
|
-
const
|
|
12195
|
-
if (
|
|
12543
|
+
const commitChanged = existing !== next;
|
|
12544
|
+
if (commitChanged)
|
|
12196
12545
|
(0, node_fs_1.writeFileSync)(hookPath, next, "utf8");
|
|
12197
12546
|
(0, node_fs_1.chmodSync)(hookPath, 0o755);
|
|
12547
|
+
const syncHooks = installSyncHooks(projectDir);
|
|
12198
12548
|
return {
|
|
12199
12549
|
ok: true,
|
|
12200
12550
|
action: "install",
|
|
12201
12551
|
project_dir: projectDir,
|
|
12202
12552
|
hook_path: hookPath,
|
|
12203
12553
|
installed: true,
|
|
12204
|
-
changed,
|
|
12205
|
-
message:
|
|
12554
|
+
changed: commitChanged,
|
|
12555
|
+
message: commitChanged
|
|
12556
|
+
? "Installed Kage post-commit hook and pull/merge sync hooks."
|
|
12557
|
+
: "Kage post-commit and sync hooks are already current.",
|
|
12206
12558
|
errors: [],
|
|
12207
12559
|
warnings: [],
|
|
12560
|
+
additional_hooks: syncHooks,
|
|
12208
12561
|
};
|
|
12209
12562
|
}
|
|
12210
12563
|
function kageHookUninstall(projectDir) {
|
|
@@ -12240,6 +12593,7 @@ function kageHookUninstall(projectDir) {
|
|
|
12240
12593
|
const next = stripKageHookBlock(existing);
|
|
12241
12594
|
(0, node_fs_1.writeFileSync)(hookPath, next, "utf8");
|
|
12242
12595
|
(0, node_fs_1.chmodSync)(hookPath, 0o755);
|
|
12596
|
+
const removedSyncHooks = uninstallSyncHooks(projectDir);
|
|
12243
12597
|
return {
|
|
12244
12598
|
ok: true,
|
|
12245
12599
|
action: "uninstall",
|
|
@@ -12247,9 +12601,10 @@ function kageHookUninstall(projectDir) {
|
|
|
12247
12601
|
hook_path: hookPath,
|
|
12248
12602
|
installed: false,
|
|
12249
12603
|
changed: true,
|
|
12250
|
-
message: "Removed Kage post-commit
|
|
12604
|
+
message: "Removed Kage post-commit and sync hooks.",
|
|
12251
12605
|
errors: [],
|
|
12252
12606
|
warnings: [],
|
|
12607
|
+
additional_hooks: removedSyncHooks,
|
|
12253
12608
|
};
|
|
12254
12609
|
}
|
|
12255
12610
|
function exportPublicBundle(projectDir) {
|