@kage-core/kage-graph-mcp 1.1.3 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/cli.js +41 -0
- package/dist/index.js +18 -0
- package/dist/kernel.js +166 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,7 @@ kage recall "how do I run tests" --project /path/to/repo
|
|
|
38
38
|
kage recall "how do I run tests" --project /path/to/repo --explain --json
|
|
39
39
|
kage quality --project /path/to/repo
|
|
40
40
|
kage benchmark --project /path/to/repo
|
|
41
|
+
kage benchmark --project /path/to/repo --compare --task "how do I run tests"
|
|
41
42
|
kage viewer --project /path/to/repo
|
|
42
43
|
kage daemon start --project /path/to/repo
|
|
43
44
|
kage observe --project /path/to/repo --event '{"type":"command_result","session_id":"s1","command":"npm test","exit_code":0}'
|
|
@@ -125,6 +126,12 @@ and parser coverage, code graph counts, evidence coverage, approved vs pending
|
|
|
125
126
|
memory, validation status, estimated tokens saved per recall, duplicate
|
|
126
127
|
candidates, average memory quality, and a readiness score.
|
|
127
128
|
|
|
129
|
+
Use `kage benchmark --compare --task "<task>" --project <repo>` or
|
|
130
|
+
`kage_benchmark_compare` to compare the same task on the same repo with and
|
|
131
|
+
without Kage. It estimates manual full-file rediscovery tokens/steps, compares
|
|
132
|
+
them to compact Kage recall plus code graph context, and returns evidence plus
|
|
133
|
+
caveats for honest marketing proof.
|
|
134
|
+
|
|
128
135
|
Use `kage refresh --project <repo>` or the `kage_refresh` MCP tool after
|
|
129
136
|
meaningful file changes. Refresh rebuilds indexes, code graph, memory graph,
|
|
130
137
|
metrics, and stale-memory metadata. Memory is marked stale when status or
|
|
@@ -204,6 +211,7 @@ Local repo tools:
|
|
|
204
211
|
- `kage_pr_check`
|
|
205
212
|
- `kage_quality`
|
|
206
213
|
- `kage_benchmark`
|
|
214
|
+
- `kage_benchmark_compare`
|
|
207
215
|
- `kage_setup_agent`
|
|
208
216
|
- `kage_graph`
|
|
209
217
|
- `kage_graph_visual`
|
package/dist/cli.js
CHANGED
|
@@ -31,6 +31,7 @@ Usage:
|
|
|
31
31
|
kage metrics --project <dir> [--json]
|
|
32
32
|
kage quality --project <dir> [--json]
|
|
33
33
|
kage benchmark --project <dir> [--json]
|
|
34
|
+
kage benchmark --project <dir> --compare --task <task> [--json]
|
|
34
35
|
kage code-graph --project <dir> [--json]
|
|
35
36
|
kage code-graph "<query>" --project <dir> [--json]
|
|
36
37
|
kage graph --project <dir> [--json]
|
|
@@ -533,6 +534,46 @@ async function main() {
|
|
|
533
534
|
return;
|
|
534
535
|
}
|
|
535
536
|
if (command === "benchmark") {
|
|
537
|
+
if (args.includes("--compare")) {
|
|
538
|
+
const result = (0, kernel_js_1.benchmarkTaskComparison)(projectArg(args), takeArg(args, "--task") ?? firstPositional(args) ?? "how do I run tests");
|
|
539
|
+
if (args.includes("--json")) {
|
|
540
|
+
console.log(JSON.stringify(result, null, 2));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
console.log(`Kage A/B Benchmark: ${result.project_dir}`);
|
|
544
|
+
console.log(`Task: ${result.task}`);
|
|
545
|
+
console.log("");
|
|
546
|
+
console.log("Without Kage:");
|
|
547
|
+
console.log(` Files examined: ${result.baseline_without_kage.files_examined}`);
|
|
548
|
+
console.log(` Full-file tokens: ${result.baseline_without_kage.full_file_tokens}`);
|
|
549
|
+
console.log(` Steps: ${result.baseline_without_kage.steps}`);
|
|
550
|
+
console.log(` Estimated time: ${result.baseline_without_kage.estimated_time_seconds}s`);
|
|
551
|
+
console.log("");
|
|
552
|
+
console.log("With Kage:");
|
|
553
|
+
console.log(` Memory packets: ${result.with_kage.memory_packets_used}`);
|
|
554
|
+
console.log(` Code facts: ${result.with_kage.code_files_returned + result.with_kage.code_symbols_returned + result.with_kage.code_routes_returned + result.with_kage.code_tests_returned}`);
|
|
555
|
+
console.log(` Context tokens: ${result.with_kage.context_tokens}`);
|
|
556
|
+
console.log(` Steps: ${result.with_kage.steps}`);
|
|
557
|
+
console.log(` Estimated time: ${result.with_kage.estimated_time_seconds}s`);
|
|
558
|
+
console.log("");
|
|
559
|
+
console.log("Delta:");
|
|
560
|
+
console.log(` Estimated tokens saved: ${result.delta.estimated_tokens_saved}`);
|
|
561
|
+
console.log(` Context reduction: ${result.delta.context_reduction_percent}%`);
|
|
562
|
+
console.log(` Rediscovery steps saved: ${result.delta.rediscovery_steps_saved}`);
|
|
563
|
+
console.log(` Estimated time saved: ${result.delta.estimated_time_saved_seconds}s`);
|
|
564
|
+
console.log(` Full-file reads avoided: ${result.delta.full_file_reads_avoided}`);
|
|
565
|
+
console.log(` Recall hit: ${result.delta.recall_hit ? "yes" : "no"}`);
|
|
566
|
+
console.log(` Code graph hit: ${result.delta.code_graph_hit ? "yes" : "no"}`);
|
|
567
|
+
console.log("");
|
|
568
|
+
console.log("Baseline files:");
|
|
569
|
+
for (const file of result.evidence.baseline_files.slice(0, 8))
|
|
570
|
+
console.log(` - ${file.path} (${file.tokens} tokens): ${file.why}`);
|
|
571
|
+
console.log("");
|
|
572
|
+
console.log("Kage memory:");
|
|
573
|
+
for (const packet of result.evidence.kage_memory.slice(0, 5))
|
|
574
|
+
console.log(` - ${packet.title} (${packet.type}, score ${packet.score})`);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
536
577
|
const result = (0, kernel_js_1.benchmarkProject)(projectArg(args));
|
|
537
578
|
if (args.includes("--json")) {
|
|
538
579
|
console.log(JSON.stringify(result, null, 2));
|
package/dist/index.js
CHANGED
|
@@ -209,6 +209,18 @@ function listTools() {
|
|
|
209
209
|
required: ["project_dir"],
|
|
210
210
|
},
|
|
211
211
|
},
|
|
212
|
+
{
|
|
213
|
+
name: "kage_benchmark_compare",
|
|
214
|
+
description: "Compare the same task on the same repo with and without Kage. Reports estimated baseline discovery tokens/steps versus Kage recall/code-graph context, with evidence and caveats.",
|
|
215
|
+
inputSchema: {
|
|
216
|
+
type: "object",
|
|
217
|
+
properties: {
|
|
218
|
+
project_dir: { type: "string" },
|
|
219
|
+
task: { type: "string" },
|
|
220
|
+
},
|
|
221
|
+
required: ["project_dir", "task"],
|
|
222
|
+
},
|
|
223
|
+
},
|
|
212
224
|
{
|
|
213
225
|
name: "kage_setup_agent",
|
|
214
226
|
description: "Generate MCP/setup instructions for Codex, Claude Code, Cursor, Windsurf, Gemini CLI, OpenCode, Cline, Goose, Roo Code, Kilo Code, Claude Desktop, Aider, or generic MCP.",
|
|
@@ -661,6 +673,12 @@ async function callTool(name, args) {
|
|
|
661
673
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
662
674
|
};
|
|
663
675
|
}
|
|
676
|
+
if (name === "kage_benchmark_compare") {
|
|
677
|
+
const result = (0, kernel_js_1.benchmarkTaskComparison)(String(args?.project_dir ?? ""), String(args?.task ?? ""));
|
|
678
|
+
return {
|
|
679
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
680
|
+
};
|
|
681
|
+
}
|
|
664
682
|
if (name === "kage_setup_agent") {
|
|
665
683
|
const result = (0, kernel_js_1.setupAgent)(String(args?.agent ?? ""), String(args?.project_dir ?? ""), { write: Boolean(args?.write) });
|
|
666
684
|
return {
|
package/dist/kernel.js
CHANGED
|
@@ -75,6 +75,7 @@ exports.graphMermaid = graphMermaid;
|
|
|
75
75
|
exports.kageMetrics = kageMetrics;
|
|
76
76
|
exports.qualityReport = qualityReport;
|
|
77
77
|
exports.benchmarkProject = benchmarkProject;
|
|
78
|
+
exports.benchmarkTaskComparison = benchmarkTaskComparison;
|
|
78
79
|
exports.learn = learn;
|
|
79
80
|
exports.capture = capture;
|
|
80
81
|
exports.createPublicCandidate = createPublicCandidate;
|
|
@@ -180,11 +181,23 @@ Keep captures concise and future-facing. Do not store raw transcripts.
|
|
|
180
181
|
|
|
181
182
|
## End-Of-Task Proposal
|
|
182
183
|
|
|
183
|
-
|
|
184
|
+
After meaningful file changes, call \`kage_refresh\` so indexes, code graph,
|
|
185
|
+
memory graph, metrics, and stale-memory checks are current.
|
|
184
186
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
187
|
+
Before finishing a task that changed files, call \`kage_pr_summarize\` or
|
|
188
|
+
\`kage_propose_from_diff\`, then call \`kage_pr_check\`.
|
|
189
|
+
|
|
190
|
+
\`kage_pr_summarize\` writes a branch review summary and a repo-local
|
|
191
|
+
change-memory packet. \`kage_pr_check\` verifies validation, graph freshness,
|
|
192
|
+
stale packets, and whether repo memory changed with the branch. If the check
|
|
193
|
+
fails, explain the required actions instead of hiding the failure. Git or PR
|
|
194
|
+
review is the repo-level review boundary.
|
|
195
|
+
|
|
196
|
+
## Package Updates
|
|
197
|
+
|
|
198
|
+
If the user asks to update Kage, run \`kage upgrade\`, then verify setup with
|
|
199
|
+
\`kage setup verify-agent --agent <agent> --project <repo>\`. Tell the user to
|
|
200
|
+
restart the agent when MCP tools need to reload.
|
|
188
201
|
|
|
189
202
|
## Feedback
|
|
190
203
|
|
|
@@ -210,7 +223,9 @@ For normal coding tasks:
|
|
|
210
223
|
4. \`kage_graph\` for remembered decisions, bugs, workflows, and conventions
|
|
211
224
|
5. Work on the task
|
|
212
225
|
6. \`kage_learn\` for concrete learnings
|
|
213
|
-
7. \`
|
|
226
|
+
7. \`kage_refresh\` after meaningful file changes
|
|
227
|
+
8. \`kage_pr_summarize\` or \`kage_propose_from_diff\` before the final response to create repo-local change memory
|
|
228
|
+
9. \`kage_pr_check\` before final handoff or merge readiness claims
|
|
214
229
|
|
|
215
230
|
For quick factual questions, \`kage_recall\` alone is enough. For status or demo requests, call \`kage_metrics\`.
|
|
216
231
|
${AGENTS_POLICY_END}
|
|
@@ -3050,6 +3065,110 @@ function benchmarkProject(projectDir) {
|
|
|
3050
3065
|
},
|
|
3051
3066
|
};
|
|
3052
3067
|
}
|
|
3068
|
+
function baselineDiscoveryFiles(projectDir, task) {
|
|
3069
|
+
const terms = tokenize(task);
|
|
3070
|
+
const graph = buildCodeGraph(projectDir);
|
|
3071
|
+
const candidatePaths = unique([
|
|
3072
|
+
"README.md",
|
|
3073
|
+
"AGENTS.md",
|
|
3074
|
+
"CLAUDE.md",
|
|
3075
|
+
"package.json",
|
|
3076
|
+
...graph.files.map((file) => file.path),
|
|
3077
|
+
]).filter((path) => path && !shouldSkipRepoMemoryPath(path));
|
|
3078
|
+
return candidatePaths
|
|
3079
|
+
.map((path) => {
|
|
3080
|
+
const absolute = (0, node_path_1.join)(projectDir, path);
|
|
3081
|
+
if (!(0, node_fs_1.existsSync)(absolute))
|
|
3082
|
+
return null;
|
|
3083
|
+
const stats = (0, node_fs_1.statSync)(absolute);
|
|
3084
|
+
if (!stats.isFile() || stats.size > 240_000)
|
|
3085
|
+
return null;
|
|
3086
|
+
const text = (0, node_fs_1.readFileSync)(absolute, "utf8");
|
|
3087
|
+
const score = scoreText(terms, `${path}\n${text.slice(0, 8000)}`, [path]);
|
|
3088
|
+
const alwaysUseful = ["README.md", "AGENTS.md", "CLAUDE.md", "package.json"].includes(path);
|
|
3089
|
+
if (score <= 0 && !alwaysUseful)
|
|
3090
|
+
return null;
|
|
3091
|
+
return {
|
|
3092
|
+
path,
|
|
3093
|
+
tokens: Math.max(1, Math.ceil(stats.size / 4)),
|
|
3094
|
+
why: score > 0 ? "task terms matched path or file content" : "standard repo orientation file",
|
|
3095
|
+
score: score + (alwaysUseful ? 1 : 0),
|
|
3096
|
+
};
|
|
3097
|
+
})
|
|
3098
|
+
.filter((entry) => Boolean(entry))
|
|
3099
|
+
.sort((a, b) => b.score - a.score || b.tokens - a.tokens || a.path.localeCompare(b.path))
|
|
3100
|
+
.slice(0, 10);
|
|
3101
|
+
}
|
|
3102
|
+
function benchmarkTaskComparison(projectDir, task) {
|
|
3103
|
+
ensureMemoryDirs(projectDir);
|
|
3104
|
+
const query = task.trim() || "how do I run tests";
|
|
3105
|
+
const baselineFiles = baselineDiscoveryFiles(projectDir, query);
|
|
3106
|
+
const baselineTokens = baselineFiles.reduce((sum, file) => sum + file.tokens, 0);
|
|
3107
|
+
const recallResult = recall(projectDir, query, 5, true);
|
|
3108
|
+
const codeResult = queryCodeGraph(projectDir, query, 10);
|
|
3109
|
+
const kageContext = `${recallResult.context_block}\n\n${codeResult.context_block}`;
|
|
3110
|
+
const kageTokens = estimateTokens(kageContext);
|
|
3111
|
+
const codeFactLines = [
|
|
3112
|
+
...codeResult.routes.map((route) => `[route] ${route.method} ${route.path} in ${route.file_path}:${route.line}`),
|
|
3113
|
+
...codeResult.symbols.map((symbol) => `[symbol] ${symbol.kind} ${symbol.name} in ${symbol.path}:${symbol.line}`),
|
|
3114
|
+
...codeResult.tests.map((test) => `[test] ${test.title} in ${test.test_path}:${test.line}`),
|
|
3115
|
+
...codeResult.files.slice(0, 5).map((file) => `[file] ${file.path} (${file.kind}, ${file.language}, ${file.parser})`),
|
|
3116
|
+
];
|
|
3117
|
+
const baselineSteps = Math.max(3, baselineFiles.length + 2);
|
|
3118
|
+
const kageSteps = 3;
|
|
3119
|
+
const tokensSaved = Math.max(0, baselineTokens - kageTokens);
|
|
3120
|
+
const contextReduction = baselineTokens > 0 ? percent(tokensSaved, baselineTokens) : 0;
|
|
3121
|
+
const timeSaved = Math.max(0, baselineSteps * 45 - kageSteps * 12);
|
|
3122
|
+
return {
|
|
3123
|
+
schema_version: 1,
|
|
3124
|
+
project_dir: projectDir,
|
|
3125
|
+
task: query,
|
|
3126
|
+
generated_at: nowIso(),
|
|
3127
|
+
baseline_without_kage: {
|
|
3128
|
+
strategy: "manual_repo_discovery_estimate",
|
|
3129
|
+
files_examined: baselineFiles.length,
|
|
3130
|
+
full_file_tokens: baselineTokens,
|
|
3131
|
+
steps: baselineSteps,
|
|
3132
|
+
estimated_time_seconds: baselineSteps * 45,
|
|
3133
|
+
},
|
|
3134
|
+
with_kage: {
|
|
3135
|
+
strategy: "recall_plus_code_graph",
|
|
3136
|
+
recall_results: recallResult.results.length,
|
|
3137
|
+
memory_packets_used: recallResult.results.length,
|
|
3138
|
+
code_files_returned: codeResult.files.length,
|
|
3139
|
+
code_symbols_returned: codeResult.symbols.length,
|
|
3140
|
+
code_routes_returned: codeResult.routes.length,
|
|
3141
|
+
code_tests_returned: codeResult.tests.length,
|
|
3142
|
+
context_tokens: kageTokens,
|
|
3143
|
+
steps: kageSteps,
|
|
3144
|
+
estimated_time_seconds: kageSteps * 12,
|
|
3145
|
+
},
|
|
3146
|
+
delta: {
|
|
3147
|
+
estimated_tokens_saved: tokensSaved,
|
|
3148
|
+
context_reduction_percent: contextReduction,
|
|
3149
|
+
rediscovery_steps_saved: Math.max(0, baselineSteps - kageSteps),
|
|
3150
|
+
estimated_time_saved_seconds: timeSaved,
|
|
3151
|
+
full_file_reads_avoided: Math.max(0, baselineFiles.length - codeResult.files.length),
|
|
3152
|
+
recall_hit: recallResult.results.length > 0,
|
|
3153
|
+
code_graph_hit: codeFactLines.length > 0,
|
|
3154
|
+
},
|
|
3155
|
+
evidence: {
|
|
3156
|
+
baseline_files: baselineFiles.map(({ path, tokens, why }) => ({ path, tokens, why })),
|
|
3157
|
+
kage_memory: recallResult.results.map((entry) => ({
|
|
3158
|
+
id: entry.packet.id,
|
|
3159
|
+
title: entry.packet.title,
|
|
3160
|
+
type: entry.packet.type,
|
|
3161
|
+
score: entry.score,
|
|
3162
|
+
})),
|
|
3163
|
+
kage_code_facts: codeFactLines.slice(0, 12),
|
|
3164
|
+
},
|
|
3165
|
+
caveats: [
|
|
3166
|
+
"Baseline is a deterministic manual-discovery estimate, not a live human or agent timing trace.",
|
|
3167
|
+
"Token savings estimate full-file reads avoided versus compact Kage recall/code-graph context.",
|
|
3168
|
+
"Use this for relative proof on the same repo/task, not cross-repo absolute claims.",
|
|
3169
|
+
],
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3053
3172
|
function kageMetricsShallow(projectDir) {
|
|
3054
3173
|
const codeGraph = buildCodeGraph(projectDir);
|
|
3055
3174
|
const knowledgeGraph = buildKnowledgeGraph(projectDir);
|
|
@@ -3414,22 +3533,42 @@ Before making code changes or answering implementation questions:
|
|
|
3414
3533
|
3. Call kage_code_graph for file, symbol, route, test, or dependency questions.
|
|
3415
3534
|
4. Call kage_graph for decisions, bugs, workflows, and conventions.
|
|
3416
3535
|
When you learn something reusable: kage_learn.
|
|
3417
|
-
|
|
3536
|
+
After meaningful file changes: kage_refresh.
|
|
3537
|
+
Before finishing a task that changed files: kage_pr_summarize or kage_propose_from_diff, then kage_pr_check.
|
|
3418
3538
|
If recalled memory helped: kage_feedback helpful. If wrong or stale: kage_feedback wrong or stale."
|
|
3419
3539
|
fi
|
|
3420
3540
|
|
|
3421
3541
|
KAGE_MSG="$POLICY" python3 -c "import json,os; print(json.dumps({'systemMessage': os.environ['KAGE_MSG']}))"
|
|
3542
|
+
`;
|
|
3543
|
+
const stopHookScript = `#!/usr/bin/env bash
|
|
3544
|
+
# Kage Stop hook — best-effort repo memory refresh before Claude Code finishes.
|
|
3545
|
+
# Silent if Kage is not initialized in the current project or no git changes exist.
|
|
3546
|
+
set -euo pipefail
|
|
3547
|
+
|
|
3548
|
+
PAYLOAD="$(cat || true)"
|
|
3549
|
+
CWD="$(printf "%s" "$PAYLOAD" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('cwd',''))" 2>/dev/null || echo "")"
|
|
3550
|
+
|
|
3551
|
+
[[ -d "$CWD/.agent_memory" ]] || exit 0
|
|
3552
|
+
command -v kage >/dev/null 2>&1 || exit 0
|
|
3553
|
+
|
|
3554
|
+
if git -C "$CWD" status --porcelain -uall >/dev/null 2>&1 && [[ -n "$(git -C "$CWD" status --porcelain -uall)" ]]; then
|
|
3555
|
+
kage refresh --project "$CWD" --json >/dev/null 2>&1 || true
|
|
3556
|
+
kage pr summarize --project "$CWD" --json >/dev/null 2>&1 || true
|
|
3557
|
+
fi
|
|
3558
|
+
|
|
3559
|
+
exit 0
|
|
3422
3560
|
`;
|
|
3423
3561
|
const settingsPath = (0, node_path_1.join)(home, ".claude", "settings.json");
|
|
3424
3562
|
const hookEntry = {
|
|
3425
3563
|
hooks: {
|
|
3426
3564
|
SessionStart: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/session-start.sh", timeout: 5 }] }],
|
|
3565
|
+
Stop: [{ matcher: "", hooks: [{ type: "command", command: "bash ~/.claude/kage/hooks/stop.sh", timeout: 20 }] }],
|
|
3427
3566
|
},
|
|
3428
3567
|
};
|
|
3429
3568
|
setSnippet(path, JSON.stringify({ mcpServers: { kage: server } }, null, 2), [
|
|
3430
3569
|
"Add the MCP server to ~/.claude.json, then restart Claude Code.",
|
|
3431
3570
|
"alwaysLoad: true makes Kage tools immediately visible without requiring ToolSearch.",
|
|
3432
|
-
`Also create ${hookDir}/session-start.sh with the hook
|
|
3571
|
+
`Also create ${hookDir}/session-start.sh and ${hookDir}/stop.sh with the hook scripts and add SessionStart/Stop hooks to ~/.claude/settings.json.`,
|
|
3433
3572
|
"Run `kage init --project <repo>` inside each repo to install the ambient memory policy.",
|
|
3434
3573
|
], true);
|
|
3435
3574
|
if (options.write) {
|
|
@@ -3437,6 +3576,7 @@ KAGE_MSG="$POLICY" python3 -c "import json,os; print(json.dumps({'systemMessage'
|
|
|
3437
3576
|
// Install the ambient session-start hook
|
|
3438
3577
|
(0, node_fs_1.mkdirSync)(hookDir, { recursive: true });
|
|
3439
3578
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(hookDir, "session-start.sh"), hookScript, { mode: 0o755 });
|
|
3579
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(hookDir, "stop.sh"), stopHookScript, { mode: 0o755 });
|
|
3440
3580
|
upsertJsonSettings(settingsPath, hookEntry);
|
|
3441
3581
|
result.wrote = true;
|
|
3442
3582
|
}
|
|
@@ -3862,11 +4002,25 @@ function distillSession(projectDir, sessionId) {
|
|
|
3862
4002
|
function createDiffChangeMemory(projectDir, summary) {
|
|
3863
4003
|
const branch = summary.branch ?? "detached";
|
|
3864
4004
|
const head = summary.head ?? "unknown";
|
|
3865
|
-
const fingerprint = (0, node_crypto_1.createHash)("sha256")
|
|
3866
|
-
.update(`${branch}\n${head}\n${summary.changed_files.join("\n")}\n${summary.diff_stat}`)
|
|
3867
|
-
.digest("hex")
|
|
3868
|
-
.slice(0, 10);
|
|
3869
4005
|
const title = `Change memory: ${branch}`;
|
|
4006
|
+
// Remove any stale change-memory packets for this branch so propose_from_diff
|
|
4007
|
+
// replaces rather than accumulates. The stable ID (branch-only, no fingerprint)
|
|
4008
|
+
// makes writePacket idempotent going forward; this sweep handles packets that
|
|
4009
|
+
// were written with the old fingerprint-based ID.
|
|
4010
|
+
const stalePrefix = `workflow-${slugify(title)}-`;
|
|
4011
|
+
const stableId = makePacketId(projectDir, "workflow", title);
|
|
4012
|
+
const stableFileName = `${stalePrefix}${(0, node_crypto_1.createHash)("sha256").update(stableId).digest("hex").slice(0, 8)}.json`;
|
|
4013
|
+
try {
|
|
4014
|
+
const existing = (0, node_fs_1.readdirSync)(packetsDir(projectDir)).filter((name) => name.startsWith(stalePrefix) && name !== stableFileName);
|
|
4015
|
+
for (const name of existing) {
|
|
4016
|
+
const stale = (0, node_path_1.join)(packetsDir(projectDir), name);
|
|
4017
|
+
const stalePacket = readJson(stale);
|
|
4018
|
+
if (stalePacket?.type === "workflow" && stalePacket?.title === title) {
|
|
4019
|
+
(0, node_fs_1.unlinkSync)(stale);
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
catch { /* non-fatal */ }
|
|
3870
4024
|
const verifyCommands = npmScriptCommands(projectDir)
|
|
3871
4025
|
.filter((command) => /(test|check|lint|build|type|verify)/i.test(command))
|
|
3872
4026
|
.slice(0, 8);
|
|
@@ -3900,7 +4054,7 @@ function createDiffChangeMemory(projectDir, summary) {
|
|
|
3900
4054
|
const now = nowIso();
|
|
3901
4055
|
const packet = {
|
|
3902
4056
|
schema_version: exports.PACKET_SCHEMA_VERSION,
|
|
3903
|
-
id:
|
|
4057
|
+
id: stableId,
|
|
3904
4058
|
title,
|
|
3905
4059
|
summary: `Repo-local context for ${summary.changed_files.length} changed repo path${summary.changed_files.length === 1 ? "" : "s"} on ${branch}.`,
|
|
3906
4060
|
body,
|