feat(claude-ops): audit the installed fleet, resolved per plugin not per scope (0.34.0) - #3059
Conversation
…per scope (0.34.0) `audit-skill-visibility` could only measure a plugins DIRECTORY, defaulting to `./plugins` relative to the working directory. That answers "what is in this checkout", not "what is installed" — the question the skill exists for. `--installed` reads `~/.claude/plugins/installed_plugins.json` instead. Both answers are now available and they legitimately differ: measured here the repo holds 221 skills and the installed fleet 216, because three plugins present in the checkout were never installed. The manifest lists one entry per install SCOPE, not per plugin. Here 67 plugins carried 134 entries — a `project` and a `user` install of the same marketplace, bound to the same projectPath. The fleet is the denominator the listing budget is measured against, so counting entries would have roughly doubled the reported overflow and fabricated the headline number. Resolution keys by plugin identity, and the report prints both counts so the collapse is auditable rather than trusted. Where the scopes disagree the run does not pick and pretend. Which scope loads is undocumented and was not verifiable here: the CLI ships as a compiled executable with no readable logic, and this container offers one topology to observe. The skew is not cosmetic — 7 plugins shipped different skill sets between scopes and 19 skills different description text — so such plugins are read from the newest install, flagged `ambiguous`, and listed under a new Fleet resolution section with both candidates named. That is the same withhold-rather-than-guess floor the cold usage verdicts already hold. One case resolves `certain`: a marketplace whose source is a local `directory` loads from that checkout rather than either cached installPath, verified by a skill executing out of the marketplace directory. `collect_fleet` now delegates to a new per-plugin `collect_fleet_at`, because installs are scattered across versioned cache paths and there is no single parent directory to walk. Verified locally: 76 unit tests (71 + 5 for the resolver), the wrapper's fresh-install fixture assertion, ruff check/format, check-skill --require-evals (PASS, 0 errors 0 warnings, description 1524/1536), changelog parity, and validate-plugins. Both documented invocations were executed, and the ambiguous branch was exercised against a synthetic two-scope manifest since the live install resolves entirely through the directory-source branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D7jUPqbSEeZeEwF1wkt9WT
|
Claude finished @kyle-sexton's task in 1m 30s —— View job Security Review
No security vulnerabilities found. This PR adds read-only tooling that resolves Reviewed specifically for the usual risk classes in a change like this:
One informational (non-blocking) note: Non-security observations (out of scope for this review, not blocking):
|
|
Last security-reviewed head: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 54ecad3649
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Claude finished @kyle-sexton's task in 3m 51s —— View job Claude finished reviewing this PR
SummaryReviewed the The design holds up under inspection:
Two findings posted inline, both maintainability/coverage rather than live bugs:
Not independently verified (author-claimed only, per lane rules): the |
|
Claude has reviewed this PR 3 times. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
… newest version Addresses all five review findings on #3059. Three came from Codex and two from the code-review lane; every one was real, and the first overturns a claim this PR was built on. 1. Scope precedence IS documented, and I said it was not (Codex P1). `skills/plugins/context/scope-semantics.md` -- in this same plugin -- states `local > project > user` and warns by name against the heuristic I shipped: "the record that actually loads is the one at the highest-precedence scope present, never simply 'the newest version installed.'" I searched the compiled CLI and never grepped the plugin I was editing. The `lastUpdated` sort is gone; resolution now follows the documented rule, and the whole `ambiguous`/`scope_conflicts` framing is replaced by `superseded`, which reports the outranked records instead of hedging. 2. Another project's installs were counted (Codex P1). `project` and `local` records load only in the `projectPath` they name. Records owned elsewhere are now excluded and reported under `not_applicable` -- counting them inflated the denominator with a fleet the model can never see. Current project comes from CLAUDE_PROJECT_DIR, falling back to cwd, matching fleet-state.sh. 3. Directory-source roots were hardcoded (Codex P2). `plugins/<name>` is the common layout, not a rule; the catalog may declare `.` or any other path. The root now comes from the marketplace catalog's declared `source`. Assuming the layout would have silently dropped such a plugin's skills. Related: for a directory-source marketplace the checkout is what loads, so neither cached version is running. Reporting "loads project X, superseding user Y" there named two versions that are both beside the point; those now report `marketplace-directory` and emit no superseded pair. 4. Dead fields removed (review lane). `install_scope`/`install_confidence` were stamped onto every denominator entry and read by nothing. 5. The integration path had no test (review lane). `collect_installed` -- the function `--installed` actually wires to -- is now covered by three unit tests (catalog-declared path, unreadable config dir, another project's install excluded end to end) plus a shell contract check asserting a two-scope manifest collapses to one plugin. Verified: 84 unit tests (was 76), the shell suite's fresh-install and new --installed contract checks, ruff check/format, shellcheck, check-skill --require-evals (PASS 0 errors 0 warnings; SKILL.md trimmed to 200 lines to clear the soft target it had crossed), changelog parity, validate-plugins. Live: --installed resolves 134 records to 67 plugins / 216 skills; --plugins-root still reports 221 with no fleet block, unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D7jUPqbSEeZeEwF1wkt9WT
|
Claude finished @kyle-sexton's task in 1m 14s —— View job Security Review
Reviewed the delta since the last security review ( No security vulnerabilities found. What changed:
No GitHub Actions workflow files were touched in this commit. |
|
Claude finished @kyle-sexton's task in 5m 27s —— View job Code review complete
SummaryThis is the code-review lane (correctness/maintainability; security is a separate lane). Fix commit
The scope-collapse math, the One new finding, posted inline:
Not independently verified (author-claimed only): the |
…d contract The code-review lane caught the one surface the precedence rewrite missed. `SKILL.md` and the CHANGELOG were updated in 0b87680; this docstring was not, and it still described the contract that commit replaced -- multi-scope installs "reported `ambiguous`", a directory-source case "marked `certain`". Neither field exists in the return value any more. For a pure, unit-tested function the docstring is where a caller learns the contract, so a stale one is worse than none: it would have a reader expect multi-scope installs to be withheld and flagged, when they are resolved by `local > project > user` and counted. Now states what the function does: applicability first (project/local records load only in their own projectPath), then precedence, with `superseded` and `not_applicable` as the two report lists and the directory-source checkout as the exception to where code is read from. No behavior change. Verified: 84 unit tests, both shell contract checks, ruff check/format, check-skill --require-evals PASS 0/0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D7jUPqbSEeZeEwF1wkt9WT
|
Claude finished @kyle-sexton's task in 38s —— View job Security Review
Skip gate: stopping early. The only change since the last successful security review ( No security vulnerabilities found. This matches the conclusion of the prior two security-review passes on this PR ( |
|
Claude finished @kyle-sexton's task in 1m 16s —— View job Code review complete
SummaryThis lane already reviewed I checked the new docstring text against the actual return value (
Diff is 21 insertions / 9 deletions, confined to the docstring — no behavior change, consistent with the commit message's own claim, and nothing outside that docstring was touched in this commit. No new findings. All prior code-review-lane findings on this PR (dead Not independently verified (author-claimed only): the "84 unit tests / ruff / check-skill --require-evals PASS" verification claims in the commit message. This lane's |
No linked issue
Summary
audit-skill-visibilitycould only measure a plugins directory, defaulting to./pluginsrelative to the working directory. That answers "what is in this checkout", not "what is installed" — the question the skill exists for. #3044 documented that limitation honestly; this removes it.--installedreads~/.claude/plugins/installed_plugins.jsoninstead. Both answers are now available and they legitimately differ: measured here the repo holds 221 skills and the installed fleet 216, because three plugins present in the checkout (ai-slop,context-budget,improvement— 2+1+2 skills) were never installed. Neither number is wrong; they answer different questions.Fix
The manifest lists one entry per install SCOPE, not per plugin. Here 67 plugins carried 134 entries. Since the fleet is the denominator the listing budget is measured against, counting entries would have roughly doubled the reported overflow and fabricated the headline number.
resolve_installed()is pure (manifest + marketplace registry in, resolution out; no filesystem), keys by plugin identity, and reports both counts so the 134→67 collapse is auditable rather than trusted.Multi-scope installs resolve by the documented precedence
local > project > user— the record that loads is the highest-precedence applicable one, never the newest version installed. Superseded records are listed under a new Fleet resolution section so a pin being outranked stays visible. Getting this wrong is not cosmetic: 7 plugins here ship different skill sets between scopes and 19 skills differentdescriptiontext.Records that cannot load here are excluded and reported.
projectandlocalinstalls load only in theprojectPaththey name; another project's records would inflate the fleet with skills the model can never see. Current project comes fromCLAUDE_PROJECT_DIR, falling back to cwd — matchingfleet-state.sh.A directory-source marketplace loads its checkout, not either cached
installPath— verified by a skill executing out of the marketplace directory. The plugin root for those comes from the catalog's declaredsource, becauseplugins/<name>is the common layout but not a rule. Since neither cached version is running there, those reportmarketplace-directoryand emit no superseded pair rather than naming two versions that are both beside the point.Supporting change:
collect_fleetdelegates to a new per-plugincollect_fleet_at, because installs are scattered across versioned cache paths with no single parent to walk.Corrections during review
Five review findings, all real. Two are corrections to claims I made, and I'd rather they sit in the description than only in the thread:
1. I said scope precedence was "undocumented and not verifiable here." It is documented — in the plugin I was editing.
plugins/claude-ops/skills/plugins/context/scope-semantics.mdstateslocal > project > userand warns against my exact heuristic by name: "the record that actually loads is the one at the highest-precedence scope present, never simply 'the newest version installed.'" I searched the compiled CLI, found only string constants, and concluded the answer was unknowable — without grepping the repo I was working in. My first revision shipped alastUpdatedsort, which is precisely the failure mode that document exists to prevent. Caught by Codex (P1); fixed in0b87680a, pinned bytest_scope_precedence_beats_the_newest_version(an older project pin must beat a newer user install).2. In #3044 I deferred this work claiming "no verifiable single root to default to." Also a wrong premise —
installed_plugins.jsonis authoritative and always was. I was looking for a single root directory rather than a manifest.Also fixed in
0b87680a:projectPathfiltering (Codex P1), catalog-declared source paths instead of a hardcoded layout (Codex P2), two dead fields that nothing downstream read (review lane), and the missing test forcollect_installed— the function--installedactually wires to (review lane).Verification
All run locally on
0b87680a:test_audit_skill_visibility.pyaudit_skill_visibility.test.sh--installedcollapsed 2 records → 1 plugincheck-skill.sh --require-evalsruff check/ruff format --checkshellcheckcheck-changelog-parity.sh --check-bumpscripts/validate-plugins.shLive, both modes executed rather than asserted:
Branches the live install cannot reach are covered by tests instead: precedence (
local > project > user), another project's install excluded end to end, catalog-declared.source path, and an unreadable config directory.Related
Refs #3044 — documented the cwd-relative limitation this removes, and whose "Deliberately not done" section this closes out. Refs #3035 — shipped the skill.