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:
Kjell Tore Guttormsen 2026-06-20 07:27:44 +02:00
commit a5cfc331fd
3 changed files with 19 additions and 16 deletions

View file

@ -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;
}

View file

@ -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).

View file

@ -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('; ')}`);
});
});