feat(doctor): report skill plugins the pipelines name but nothing provides
The ship-feature pipelines have always depended on the `superpowers`
plugin — skills/ship-feature/SKILL.md calls its skills by name, and the
node detect rules match `Skill` events named `brainstorming`,
`writing-plans`, `test-driven-development` — but no README, INSTALL,
manifest or SKILL.md said so, and /ccam-doctor did not check. Install the
`ccam` plugin without it and `/ccam:ship-feature-lane` runs partway, then
fails at the first skill call with no warning beforehand.
/ccam-doctor now derives the requirement from the loaded pipeline
templates rather than a hardcoded list: every `/plugin:skill` a template
names, as its own `skill` or as one node's `command`, has to come from an
installed and enabled plugin. Editing a template cannot leave the check
behind, and a custom template in DASHBOARD_PIPELINES_DIR naming some
other plugin is covered for free.
Three deliberate calls:
- WARN, never FAIL. The dashboard, hooks, CLI and MCP all work without
it; failing a doctor run over it would cry wolf.
- A plugin present but explicitly disabled counts as missing — Claude
Code will not resolve its skills either way.
- The skills sitting in ~/.claude/skills/ count as satisfied. That is a
legitimate hand-copied setup, and calling it broken is how a warning
row teaches its reader to ignore it.
Docs state the prerequisite where someone installing would look: README
and INSTALL requirements, plus docs/PLUGINS.md for how the row decides.
The install line is `superpowers@claude-plugins-official`, verified
present in that marketplace's manifest.
Verified: npm run test:server (1379, +6 covering all three calls above
plus the partially-satisfied case), npm run test:client (459), prettier
and check-headers clean. `node scripts/plugin-doctor.js` here reports
`OK Skill plugins superpowers available`; the WARN branches were checked
against a temp CLAUDE_HOME.
This commit is contained in:
@@ -4,8 +4,10 @@
|
||||
* @description Reports the health of a plugin-installed CCAM: Node version,
|
||||
* bootstrap state, runtime dependencies, server liveness, duplicate hook
|
||||
* entries (the one failure that silently doubles every token and cost figure),
|
||||
* the `ccam` CLI launcher and its PATH, and whether the committed MCP build
|
||||
* still matches `mcp/src`. Read-only — it diagnoses, it never repairs.
|
||||
* the `ccam` CLI launcher and its PATH, whether the committed MCP build still
|
||||
* matches `mcp/src`, and whether the slash commands the pipeline templates name
|
||||
* as node drivers are actually resolvable (the `superpowers` plugin, today).
|
||||
* Read-only — it diagnoses, it never repairs.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -81,6 +83,7 @@ function diagnose() {
|
||||
);
|
||||
|
||||
add(...cliStatus());
|
||||
add(...skillPluginStatus());
|
||||
|
||||
const mcp = mcpBuildStatus(PLUGIN_ROOT);
|
||||
add(
|
||||
@@ -99,6 +102,84 @@ function diagnose() {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slash commands the loaded pipeline templates name — as the pipeline's own
|
||||
* driver (`skill`) or as one node's driver (`command`) — grouped by the plugin
|
||||
* that has to provide them. Derived from the templates rather than hardcoded so
|
||||
* a custom template dropped in `DASHBOARD_PIPELINES_DIR` is checked too, and so
|
||||
* editing a built-in one cannot leave this check behind.
|
||||
*
|
||||
* `ccam` itself is dropped: it is the plugin running the diagnosis.
|
||||
*
|
||||
* @returns {Map<string, Set<string>>} plugin name → skill names it must ship.
|
||||
*/
|
||||
function pipelineSkillRequirements() {
|
||||
const requirements = new Map();
|
||||
let pipelines = [];
|
||||
try {
|
||||
pipelines = require("../server/lib/pipelines").listPipelines();
|
||||
} catch {
|
||||
return requirements; // templates unreadable — a separate failure, not this row's
|
||||
}
|
||||
for (const pipeline of pipelines) {
|
||||
for (const cmd of [pipeline.skill, ...pipeline.nodes.map((n) => n.command)]) {
|
||||
const match = /^\/([A-Za-z0-9_-]+):([A-Za-z0-9_-]+)/.exec(cmd || "");
|
||||
if (!match) continue; // a shell command, or a bare /command with no plugin
|
||||
const [, plugin, skill] = match;
|
||||
if (plugin === "ccam") continue;
|
||||
if (!requirements.has(plugin)) requirements.set(plugin, new Set());
|
||||
requirements.get(plugin).add(skill);
|
||||
}
|
||||
}
|
||||
return requirements;
|
||||
}
|
||||
|
||||
/**
|
||||
* WARN (never FAIL) when a plugin the pipelines depend on is absent: the
|
||||
* dashboard, hooks, CLI and MCP all work without it — only the pipeline skills
|
||||
* that drive it stop partway, so this must not fail a doctor run.
|
||||
*
|
||||
* A plugin listed but explicitly disabled counts as missing (Claude Code will
|
||||
* not resolve its skills), and a plugin whose skills are all present as plain
|
||||
* user skills counts as satisfied — copying them into `~/.claude/skills/`
|
||||
* instead of installing the plugin is a legitimate setup, and reporting it as
|
||||
* missing would train the reader to ignore this row.
|
||||
*/
|
||||
function skillPluginStatus() {
|
||||
const requirements = pipelineSkillRequirements();
|
||||
if (!requirements.size) return ["ok", "Skill plugins", "no pipeline names an external plugin"];
|
||||
|
||||
let plugins = [];
|
||||
let userSkills = new Set();
|
||||
try {
|
||||
const discovery = require("../server/lib/cc-discovery");
|
||||
plugins = discovery.readPlugins().plugins;
|
||||
userSkills = new Set(discovery.readSkills({ scope: "user" }).map((s) => s.name));
|
||||
} catch {
|
||||
return ["warn", "Skill plugins", "could not read the installed-plugin manifest"];
|
||||
}
|
||||
|
||||
const missing = [];
|
||||
for (const [name, skills] of requirements) {
|
||||
const installed = plugins.find(
|
||||
(p) => p.name === name && p.installPathExists && p.enabled !== false
|
||||
);
|
||||
if (installed) continue;
|
||||
if ([...skills].every((s) => userSkills.has(s))) continue;
|
||||
const absent = [...skills].filter((s) => !userSkills.has(s)).sort();
|
||||
missing.push(`${name} (needed for ${absent.join(", ")})`);
|
||||
}
|
||||
|
||||
return missing.length
|
||||
? [
|
||||
"warn",
|
||||
"Skill plugins",
|
||||
`${missing.join("; ")} — the pipelines name these commands; install with ` +
|
||||
`claude plugin install ${missing.map((m) => m.split(" ")[0]).join(" ")}`,
|
||||
]
|
||||
: ["ok", "Skill plugins", `${[...requirements.keys()].sort().join(", ")} available`];
|
||||
}
|
||||
|
||||
function countLegacyHookEntries() {
|
||||
try {
|
||||
const settings = JSON.parse(fs.readFileSync(getSettingsPath(), "utf8"));
|
||||
|
||||
Reference in New Issue
Block a user