fix: regenerate pi-claude-cli MCP config per session and auto-sync skill tools

The MCP config was generated lazily once and locked, so engine session-scoped
tools (fn_review_spec, fn_review_step) never reached the Claude CLI subprocess
and triage/executor sessions failed with "unknown tool" errors. Now the config
is hashed per call and rewritten when the tool set changes.

Also adds scripts/sync-fusion-skill-tools.mjs to regenerate the SKILL.md
tool-categories block from extension.ts at build time, with a --check mode
wired into skill-sync tests so drift fails CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-25 14:10:56 -07:00
parent c597dd4055
commit e8aab69c67
8 changed files with 231 additions and 26 deletions

View File

@@ -0,0 +1,141 @@
#!/usr/bin/env node
/**
* Regenerates the `tool-categories` block in the Fusion skill SKILL.md from
* the canonical list of `fn_*` tools registered in `packages/cli/src/extension.ts`.
*
* Source of truth: extension.ts (each tool is registered with `name: "fn_..."`).
* Output: the `<!-- BEGIN: tool-categories ... -->` / `<!-- END: tool-categories -->`
* block inside `packages/cli/skill/fusion/SKILL.md`.
*
* The `.claude/skills/fusion` path is a symlink to `packages/cli/skill/fusion`,
* so it picks up the change automatically.
*
* Usage:
* node scripts/sync-fusion-skill-tools.mjs # rewrite SKILL.md in place
* node scripts/sync-fusion-skill-tools.mjs --check # exit 1 if drift detected
*
* Wired into `packages/cli` build via the `prebuild` script so a fresh build
* always emits an up-to-date skill, and CI catches drift via `--check`.
*/
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, "..");
const extensionPath = resolve(repoRoot, "packages/cli/src/extension.ts");
const skillPath = resolve(repoRoot, "packages/cli/skill/fusion/SKILL.md");
const BEGIN_MARKER =
"<!-- BEGIN: tool-categories (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->";
const END_MARKER = "<!-- END: tool-categories -->";
/**
* Categorize a tool name into one of the published skill categories.
* The order of categories here defines the order of bullets in SKILL.md.
*/
function categorize(name) {
if (name.includes("github")) return "GitHub";
if (name.startsWith("fn_task_")) return "Task";
if (
name.startsWith("fn_mission_") ||
name.startsWith("fn_milestone_") ||
name.startsWith("fn_slice_") ||
name.startsWith("fn_feature_")
) {
return "Mission";
}
if (name.startsWith("fn_agent_")) return "Agent";
if (name.startsWith("fn_skills_")) return "Skills";
return "Other";
}
const CATEGORY_ORDER = ["Task", "GitHub", "Mission", "Agent", "Skills", "Other"];
const CATEGORY_LABELS = {
Task: "Task tools",
GitHub: "GitHub tools",
Mission: "Mission tools",
Agent: "Agent tools",
Skills: "Skills tools",
Other: "Other tools",
};
function extractToolNames(source) {
const names = [];
for (const match of source.matchAll(/name:\s*"(fn_[a-z_]+)"/g)) {
names.push(match[1]);
}
if (names.length === 0) {
throw new Error(
`No fn_* tool registrations found in ${extensionPath} — refusing to wipe the skill.`,
);
}
return names;
}
function buildBlock(toolNames) {
const grouped = new Map(CATEGORY_ORDER.map((c) => [c, []]));
for (const name of toolNames) {
grouped.get(categorize(name)).push(name);
}
const lines = [BEGIN_MARKER];
for (const category of CATEGORY_ORDER) {
const tools = grouped.get(category);
if (tools.length === 0) continue;
const formatted = tools.map((t) => `\`${t}\``).join(", ");
lines.push(`- **${CATEGORY_LABELS[category]}** — ${formatted}`);
}
lines.push(END_MARKER);
return lines.join("\n");
}
function replaceBlock(skillContent, newBlock) {
const beginIdx = skillContent.indexOf(BEGIN_MARKER);
const endIdx = skillContent.indexOf(END_MARKER);
if (beginIdx === -1 || endIdx === -1 || endIdx < beginIdx) {
throw new Error(
`Could not find BEGIN/END tool-categories markers in ${skillPath}. ` +
`Add them around the tool-categories bullet list.`,
);
}
const endLineEnd = endIdx + END_MARKER.length;
return (
skillContent.slice(0, beginIdx) + newBlock + skillContent.slice(endLineEnd)
);
}
function main() {
const checkOnly = process.argv.includes("--check");
const source = readFileSync(extensionPath, "utf-8");
const skillContent = readFileSync(skillPath, "utf-8");
const toolNames = extractToolNames(source);
const newBlock = buildBlock(toolNames);
const updated = replaceBlock(skillContent, newBlock);
if (updated === skillContent) {
if (!checkOnly) {
console.log(
`[sync-fusion-skill-tools] SKILL.md tool-categories block is already up to date (${toolNames.length} tools).`,
);
}
return;
}
if (checkOnly) {
console.error(
`[sync-fusion-skill-tools] SKILL.md tool-categories block is out of date.\n` +
`Run \`node scripts/sync-fusion-skill-tools.mjs\` to regenerate it.`,
);
process.exit(1);
}
writeFileSync(skillPath, updated);
console.log(
`[sync-fusion-skill-tools] Rewrote tool-categories block in ${skillPath} (${toolNames.length} tools).`,
);
}
main();