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:
141
scripts/sync-fusion-skill-tools.mjs
Normal file
141
scripts/sync-fusion-skill-tools.mjs
Normal 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();
|
||||
Reference in New Issue
Block a user