feat(plh): flag plugin namespace collisions (same declared name)

Two plugins that declare the same `name` in plugin.json collapse into one
component namespace (/name:command, name:skill, agent "name"). Resolution
between two installed same-name plugins is undocumented, so one plugin's
commands/skills/agents are silently shadowed and unreachable. PLH now flags
this at medium severity, keying on the declared `name` (not folder basename,
via new declaredName on scanSinglePlugin) with a COL-shaped details.namespaces
payload. Name-less plugins are excluded from the collision map.

Search-first (code.claude.com/docs/en/plugins): plugin components are
namespaced by the declared name, so a plugin component can never shadow a
user/project one — only a same-name collision loses components. This refutes
the original "plugin vs user vs project shadowing" framing in the backlog.

Adds humanizer pattern, fixture (duplicate-plugin-name: 2 colliding + 2
name-less), and 3 tests. Suite 929->932. self-audit A 97 / A 100, scanners 13.
This commit is contained in:
Kjell Tore Guttormsen 2026-06-19 15:13:19 +02:00
commit c6c5f17752
11 changed files with 182 additions and 3 deletions

View file

@ -109,7 +109,7 @@ Default: auto-detects scope from git context. Override with `/config-audit full|
node --test 'tests/**/*.test.mjs'
```
929 tests across 56 test files (17 lib + 29 scanner + 1 hook + 1 agent + 3 commands + 1 knowledge + 4 top-level). Test fixtures in `tests/fixtures/`. Top-level humanizer tests: `json-backcompat.test.mjs`, `raw-backcompat.test.mjs`, `scenario-read-test.test.mjs`, `snapshot-default-output.test.mjs`.
932 tests across 56 test files (17 lib + 29 scanner + 1 hook + 1 agent + 3 commands + 1 knowledge + 4 top-level). Test fixtures in `tests/fixtures/`. Top-level humanizer tests: `json-backcompat.test.mjs`, `raw-backcompat.test.mjs`, `scenario-read-test.test.mjs`, `snapshot-default-output.test.mjs`.
### CML scanner — context-window-scaled char budget
@ -151,6 +151,27 @@ Beyond deny/allow overlap, the DIS scanner now also flags:
These predicates live in `scanners/lib/permission-rules.mjs` (shared with the CNF
conflict-detector). Behavior verified against `code.claude.com/docs/en/permissions`.
### PLH scanner — plugin namespace collision
The standalone PLH scanner (cross-plugin checks in `scan()`) flags **plugin namespace
collisions**: two or more discovered plugins that declare the **same `name`** in
`plugin.json`. The search-first finding that shaped this check: Claude Code namespaces
every plugin component by the declared `name``/name:command`, `name:skill`, agent
`name` (verified against `code.claude.com/docs/en/plugins`, and observable in any session's
namespaced skill listing). A plugin component therefore can **never** shadow a user- or
project-level one; the only shadow that loses components is a same-`name` collision, where
the namespaces collapse into one and CC must pick a winner. Resolution between two installed
same-name plugins is **undocumented**, so the loser's commands/skills/agents go silently
unreachable — hence severity **MEDIUM** (dead config), `category: 'plugin-hygiene'`, with a
COL-shaped `details.namespaces` payload (`{ source: 'plugin:<dir>', name, path }`).
Two design notes: (1) the check keys on the declared `name` field, **not** `basename(dir)`
the folder name is irrelevant to the namespace; `scanSinglePlugin` now returns `declaredName`
for this. (2) Name-less plugins are excluded from the collision map (they are flagged by the
missing-field check and must never group on an `undefined` key). The pre-existing cross-plugin
**command**-name conflict check still uses the folder basename and over-reports given
namespacing — left as a separate backlog candidate, intentionally out of scope here.
## Gotchas
- Session directories accumulate — use `/config-audit cleanup` to manage