fix(plh): align required-frontmatter with CC docs (drop optional model/tools/name)
plugin-health-scanner required `model`+`allowed-tools` on commands and `model`+`tools` on agents, plus `name` on commands. Per primary docs these are OPTIONAL: - Commands/skills (code.claude.com/docs slash-commands): "All fields are optional. Only `description` is recommended." `name` defaults to the directory name. - Subagents (code.claude.com/docs sub-agents): "Only `name` and `description` are required"; `model` defaults to `inherit`, `tools` inherits all. REQUIRED_COMMAND_FRONTMATTER -> [description]; REQUIRED_AGENT_FRONTMATTER -> [name, description]. This was over-reporting: every command without an explicit `model` got a spurious medium finding (10 on the okr plugin alone). Found via the okr pilot review. Suite 949/949, self-audit A/A, scanner count 13 (no new scanner). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
This commit is contained in:
parent
47efc5979e
commit
a5cfc331fd
3 changed files with 19 additions and 16 deletions
|
|
@ -19,18 +19,19 @@ const SCANNER = 'PLH';
|
||||||
|
|
||||||
const REQUIRED_PLUGIN_JSON_FIELDS = ['name', 'description', 'version'];
|
const REQUIRED_PLUGIN_JSON_FIELDS = ['name', 'description', 'version'];
|
||||||
const RECOMMENDED_CLAUDE_MD_SECTIONS = ['commands', 'agents', 'hooks'];
|
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 = [
|
const REQUIRED_COMMAND_FRONTMATTER = [
|
||||||
{ key: 'name', display: 'name' },
|
|
||||||
{ key: 'description', display: 'description' },
|
{ key: 'description', display: 'description' },
|
||||||
{ key: 'model', display: 'model' },
|
|
||||||
{ key: 'allowed_tools', display: 'allowed-tools' },
|
|
||||||
];
|
];
|
||||||
const REQUIRED_AGENT_FRONTMATTER = [
|
const REQUIRED_AGENT_FRONTMATTER = [
|
||||||
{ key: 'name', display: 'name' },
|
{ key: 'name', display: 'name' },
|
||||||
{ key: 'description', display: 'description' },
|
{ 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
|
// 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',
|
title: 'Command missing frontmatter',
|
||||||
description: `Command "${file}" in plugin "${pluginName}" has no frontmatter`,
|
description: `Command "${file}" in plugin "${pluginName}" has no frontmatter`,
|
||||||
file: filePath,
|
file: filePath,
|
||||||
recommendation: 'Add YAML frontmatter with name, description, model',
|
recommendation: 'Add YAML frontmatter with a description (other command fields are optional)',
|
||||||
}));
|
}));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -385,7 +386,7 @@ async function scanSinglePlugin(pluginDir) {
|
||||||
title: 'Agent missing frontmatter',
|
title: 'Agent missing frontmatter',
|
||||||
description: `Agent "${file}" in plugin "${pluginName}" has no frontmatter`,
|
description: `Agent "${file}" in plugin "${pluginName}" has no frontmatter`,
|
||||||
file: filePath,
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
name: bad-agent
|
name: bad-agent
|
||||||
description: Missing model and tools
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Bad Agent
|
# 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).
|
||||||
|
|
|
||||||
|
|
@ -99,15 +99,17 @@ describe('scan on broken-plugin', () => {
|
||||||
assert.equal(noFrontmatter.length, 1, 'Should detect command without frontmatter');
|
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();
|
resetCounter();
|
||||||
const result = await scan(BROKEN_PLUGIN);
|
const result = await scan(BROKEN_PLUGIN);
|
||||||
// CA-PLH-005 (missing model) and CA-PLH-006 (missing tools) in broken-plugin.
|
// Per CC sub-agents docs: only `name` and `description` are required; `model` and `tools`
|
||||||
const missingAgent = result.findings.filter(f =>
|
// are optional (inherit / all-tools by default). bad-agent.md has `name` only.
|
||||||
f.scanner === 'PLH' && (f.id === 'CA-PLH-005' || f.id === 'CA-PLH-006')
|
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(agentFields.some(f => /description/.test(f.title)), 'missing required `description` must be flagged');
|
||||||
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 => /\b(model|tools)\b/.test(f.title)),
|
||||||
|
`optional model/tools must not be flagged, got: ${agentFields.map(f => f.title).join('; ')}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue