fix(plh): require CLAUDE.md commands/agents/hooks section only for shipped components

plugin-health flagged "CLAUDE.md missing <commands|agents|hooks> section" regardless of
whether the plugin actually had that component — e.g. graceful-handoff (no commands/ or
agents/ dir) got two spurious medium findings. Same over-report class as the model-field fix.

Now gated on component presence (pluginShipsComponent): a section is required only if the
plugin ships that component (commands/ or agents/ with .md, or hooks/hooks.json). Across the 5
stable plugins this drops 12 spurious findings to 3 legitimate ones (graceful-handoff hooks,
ai-psychosis commands+hooks). New fixture plugin-section-coverage proves both directions.
Found via the marketplace-wide review. Suite 950/950, self-audit A/A, scanner count 13.
tests badge 949 -> 950.

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 09:06:20 +02:00
commit 292352eff8
6 changed files with 56 additions and 1 deletions

View file

@ -0,0 +1,5 @@
{
"name": "section-coverage",
"description": "Fixture: ships one command, no agents or hooks",
"version": "1.0.0"
}

View file

@ -0,0 +1,7 @@
# Section Coverage Fixture
## Overview
This plugin ships one command and no agents or hooks. Its CLAUDE.md intentionally omits the
Commands section, so the missing-section check should fire for `commands` (a component it has)
but NOT for `agents` or `hooks` (components it does not have).

View file

@ -0,0 +1,7 @@
---
description: A demo command for the section-coverage fixture
---
# Foo
Demo command body.

View file

@ -13,6 +13,7 @@ const DUP_NAME = resolve(FIXTURES, 'duplicate-plugin-name');
const DUP_CMD = resolve(FIXTURES, 'duplicate-command-name');
const SHADOW = resolve(FIXTURES, 'plugin-shadow-folder');
const SKILLS_ARR = resolve(FIXTURES, 'plugin-skills-array');
const SECTION_COV = resolve(FIXTURES, 'plugin-section-coverage');
describe('discoverPlugins', () => {
it('discovers a single plugin when pointed at plugin dir', async () => {
@ -72,6 +73,25 @@ describe('scan on valid test-plugin', () => {
});
});
describe('CLAUDE.md section findings track present components', () => {
it('flags a missing section only for components the plugin actually ships', async () => {
resetCounter();
const result = await scan(SECTION_COV);
// section-coverage ships commands/ but no agents/ or hooks, and its CLAUDE.md omits the
// Commands section. Per the component-aware rule: flag the present component's missing
// section, never require docs for absent components.
const sectionFindings = result.findings.filter(f =>
f.scanner === 'PLH' && /missing.{0,40}section/i.test(f.title || '')
);
assert.ok(sectionFindings.some(f => /command/i.test(f.title)),
'missing Commands section must be flagged when commands/ exists');
assert.ok(!sectionFindings.some(f => /agent/i.test(f.title)),
'agents section must not be flagged (no agents/)');
assert.ok(!sectionFindings.some(f => /hook/i.test(f.title)),
'hooks section must not be flagged (no hooks)');
});
});
describe('scan on broken-plugin', () => {
it('detects missing plugin.json fields', async () => {
resetCounter();