diff --git a/scanners/plugin-health-scanner.mjs b/scanners/plugin-health-scanner.mjs index 3503cf1..8bc6d06 100644 --- a/scanners/plugin-health-scanner.mjs +++ b/scanners/plugin-health-scanner.mjs @@ -19,18 +19,19 @@ const SCANNER = 'PLH'; const REQUIRED_PLUGIN_JSON_FIELDS = ['name', 'description', 'version']; const RECOMMENDED_CLAUDE_MD_SECTIONS = ['commands', 'agents', 'hooks']; -// Keys as they appear after yaml-parser normalizeKey (hyphens → underscores) +// Keys as they appear after yaml-parser normalizeKey (hyphens → underscores). +// Field requirements are pinned to the primary docs, NOT to "every field a plugin could set": +// - Commands/skills (code.claude.com/docs slash-commands): "All fields are optional. Only +// `description` is recommended." `name` defaults to the directory name; `model` and +// `allowed-tools` are optional. So only `description` is flagged. +// - Subagents (code.claude.com/docs sub-agents): "Only `name` and `description` are required." +// `model` (defaults to `inherit`) and `tools` (inherits all) are optional. const REQUIRED_COMMAND_FRONTMATTER = [ - { key: 'name', display: 'name' }, { key: 'description', display: 'description' }, - { key: 'model', display: 'model' }, - { key: 'allowed_tools', display: 'allowed-tools' }, ]; const REQUIRED_AGENT_FRONTMATTER = [ { key: 'name', display: 'name' }, { key: 'description', display: 'description' }, - { key: 'model', display: 'model' }, - { key: 'tools', display: 'tools' }, ]; // Component-path keys that REPLACE the default folder (per code.claude.com/docs @@ -346,7 +347,7 @@ async function scanSinglePlugin(pluginDir) { title: 'Command missing frontmatter', description: `Command "${file}" in plugin "${pluginName}" has no frontmatter`, file: filePath, - recommendation: 'Add YAML frontmatter with name, description, model', + recommendation: 'Add YAML frontmatter with a description (other command fields are optional)', })); continue; } @@ -385,7 +386,7 @@ async function scanSinglePlugin(pluginDir) { title: 'Agent missing frontmatter', description: `Agent "${file}" in plugin "${pluginName}" has no frontmatter`, file: filePath, - recommendation: 'Add YAML frontmatter with name, description, model, tools', + recommendation: 'Add YAML frontmatter with name and description (model and tools are optional)', })); continue; } diff --git a/tests/fixtures/broken-plugin/agents/bad-agent.md b/tests/fixtures/broken-plugin/agents/bad-agent.md index 790b741..2db7b36 100644 --- a/tests/fixtures/broken-plugin/agents/bad-agent.md +++ b/tests/fixtures/broken-plugin/agents/bad-agent.md @@ -1,8 +1,8 @@ --- name: bad-agent -description: Missing model and tools --- # Bad Agent -No model or tools in frontmatter. +Has a `name` but no `description`. Per CC sub-agents docs, `name` and `description` are +required for subagents; `model` and `tools` are optional (inherit / all-tools by default). diff --git a/tests/scanners/plugin-health-scanner.test.mjs b/tests/scanners/plugin-health-scanner.test.mjs index 0dc0414..b509193 100644 --- a/tests/scanners/plugin-health-scanner.test.mjs +++ b/tests/scanners/plugin-health-scanner.test.mjs @@ -99,15 +99,17 @@ describe('scan on broken-plugin', () => { assert.equal(noFrontmatter.length, 1, 'Should detect command without frontmatter'); }); - it('detects agent missing required frontmatter fields', async () => { + it('flags missing required agent field (description) but not optional model/tools', async () => { resetCounter(); const result = await scan(BROKEN_PLUGIN); - // CA-PLH-005 (missing model) and CA-PLH-006 (missing tools) in broken-plugin. - const missingAgent = result.findings.filter(f => - f.scanner === 'PLH' && (f.id === 'CA-PLH-005' || f.id === 'CA-PLH-006') + // Per CC sub-agents docs: only `name` and `description` are required; `model` and `tools` + // are optional (inherit / all-tools by default). bad-agent.md has `name` only. + const agentFields = result.findings.filter(f => + f.scanner === 'PLH' && /Agent missing frontmatter field/.test(f.title || '') ); - // bad-agent.md has name+description but missing model and tools - assert.ok(missingAgent.length >= 2, `Should detect missing model and tools, got ${missingAgent.length}: ${missingAgent.map(f => f.id).join(', ')}`); + assert.ok(agentFields.some(f => /description/.test(f.title)), 'missing required `description` must be flagged'); + assert.ok(!agentFields.some(f => /\b(model|tools)\b/.test(f.title)), + `optional model/tools must not be flagged, got: ${agentFields.map(f => f.title).join('; ')}`); }); });