diff --git a/plugins/claude-ops/.claude-plugin/plugin.json b/plugins/claude-ops/.claude-plugin/plugin.json index f899cc1432..0a794e0935 100644 --- a/plugins/claude-ops/.claude-plugin/plugin.json +++ b/plugins/claude-ops/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "claude-ops", - "version": "0.31.7", + "version": "0.31.8", "description": "Claude Code operations toolkit. Ten skills: inventory (read-only enumeration of the complete invocable surface \u2014 every built-in CLI command with aliases and hidden/gated status, every bundled skill, and every component of every installed plugin across all marketplaces; reads the shipped binary because upstream publishes no built-in command list, and carries an integrity verdict so a drifted build reports counts as floors rather than silently short totals), audit-install-state (read-only audit of the machine-scope ~/.claude installation directory and ~/.claude.json \u2014 full inventory split into an authored surface and rolled-up bulk trees, product-managed retention vs genuinely unmanaged state, filename-scheme resolution before any process-liveness check, and deliberate/mid-experiment detection; reports, never deletes), audit-performance (read-only slowness-diagnostic capture run at the moment the machine or a session feels slow \u2014 CLI version, retention-sweep health including the silent unparsable-settings pause, a timed census walk of the install tree as a sweep-cost proxy, active-session and plugin-fleet counts, a process census, and a bundled known-performance-issues reference; separates the three documented suspects \u2014 accumulated state, version regression, component bloat \u2014 and routes remediation out; reports, never mutates), observability (read locally captured telemetry \u2014 OTEL store, collector, hook-event JSONL, ccusage \u2014 with trend reports and store pruning), known-issues (search known Claude product GitHub bugs, check service health, maintain a persistent tracked-issue registry), changelog (ingest Claude Code changelog entries and integrate them into the current repo), plugins (bring a machine's plugin fleet current on demand \u2014 marketplace refresh, effective-scope updates including in-repo project/local installs, new-plugin install per policy, scope-divergence detection and explicit convergence), morning-brief (read-only gh-based operator morning view \u2014 queue-label counts, merge-ready PRs, parked decisions with their RECOMMENDED lines, and loop-lane telemetry freshness), lanes (start/restart/stop/status loop lanes as named background Claude Code sessions seeded from canonical prompt files, with per-lane model/effort, a repo-pull + marketplace-refresh launch step, and a consume-restarts action \u2014 an OS-schedulable reader that relaunches stopped lanes whose telemetry carries a restart_request), and a re-runnable setup action that settles where the known-issues registry lives. Plus a family of seven advisory *-audit telemetry-emitter hooks (API errors, config changes, instruction loads, permission denials, pre-compaction, skill usage, tool failures) that emit the shared hook-telemetry envelope, and a reference sink that maps envelopes into the hook-events.jsonl the observability skill reads.", "author": { "name": "Melodic Software", diff --git a/plugins/claude-ops/CHANGELOG.md b/plugins/claude-ops/CHANGELOG.md index 651ec4bc1e..5aa4f617c9 100644 --- a/plugins/claude-ops/CHANGELOG.md +++ b/plugins/claude-ops/CHANGELOG.md @@ -3,6 +3,35 @@ All notable changes to the `claude-ops` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.31.8] + +### Fixed + +- **`scope-semantics.md` claimed `converge` was the only action that surfaces a settings diff.** + False by its own new evidence: `sync`'s Step 5 issues `enable -s `, and + `enable -s project` writes the committed file the same way `install` does. The update exemption is + re-verified and still holds, but it is now documented as the exception rather than the rule. + +### Added + +- **`scope-semantics.md` gains a verified write-behavior table for project scope.** `install`, + `uninstall`, `enable`, and `disable` at `-s project` all write the committed + `.claude/settings.json`; `update` does not; `-s local` writes the gitignored + `.claude/settings.local.json` instead. Also recorded: `enable -s project` gates on the *merged + effective* value, so enabling an id that is `true` only at user scope fails rather than writing a + project entry. +- **`sync.md` Step 5 names its own exposure.** A `-s project` enable can leave a team-shared tracked + file modified with no diff surfaced — the failure class `converge` Step 5 prevents, in the default + action. Flagged with instructions to name it in the report; the diff-surfacing remediation is + tracked separately. +- **`gotchas.md` records that a subdirectory install is invisible to the skill.** The CLI keys + `projectPath` on the literal cwd — installing from `/nested/subdir` recorded that + subdirectory and created its own `.claude/settings.json` — while `fleet-state.sh` resolves the + checkout root. A plugin installed below the checkout root therefore never matches + `currentProject`, never updates, and never appears in a divergence row, while still loading in + that subtree. The same mechanism is why two `git worktree` checkouts of one repo pin + independently, which `converge` Step 2 now states directly. + ## [0.31.7] ### Fixed diff --git a/plugins/claude-ops/skills/plugins/SKILL.md b/plugins/claude-ops/skills/plugins/SKILL.md index 511836877d..5f73a2812d 100644 --- a/plugins/claude-ops/skills/plugins/SKILL.md +++ b/plugins/claude-ops/skills/plugins/SKILL.md @@ -33,8 +33,10 @@ per-scope `enabledPlugins`) against the *local* marketplace catalog — a differ completeness, not settings-vs-upstream drift). **Never silently fixes drift it finds.** `sync` mutates only via the documented CLI actions below; -`converge` is the one action that can touch a committed `.claude/settings.json`, and only after an -explicit per-plugin confirm. +`sync` can also write a committed `.claude/settings.json` when Step 5 issues `enable -s project` +(see [context/scope-semantics.md](context/scope-semantics.md)); `converge` is the action that +consolidates cross-scope divergence and can touch committed settings after an explicit per-plugin +confirm. ## Action Router @@ -49,7 +51,7 @@ ordering, and their failure handling live only in the linked file. |---|---|---|---| | `sync` (default) | Yes — CLI only | Marketplace, install, and enable-state maintenance for the effective fleet | [context/sync.md](context/sync.md) | | `audit` | No | Same algorithm as `sync`, every mutating step replaced with a prediction; issues zero mutating CLI calls | "Action: audit" below | -| `converge` | Yes — the one action that can touch committed settings | Cross-scope divergence reconciliation, preview- and confirm-gated | [context/converge.md](context/converge.md) | +| `converge` | Yes — can rewrite committed settings after confirm | Cross-scope divergence reconciliation, preview- and confirm-gated | [context/converge.md](context/converge.md) | Bare invocation (no arguments) → `sync` against the default marketplace. `help` or an unrecognized action → show this table. diff --git a/plugins/claude-ops/skills/plugins/context/converge.md b/plugins/claude-ops/skills/plugins/context/converge.md index 4146c3fb0f..db21f393f2 100644 --- a/plugins/claude-ops/skills/plugins/context/converge.md +++ b/plugins/claude-ops/skills/plugins/context/converge.md @@ -1,8 +1,10 @@ # Converge — explicit scope consolidation -`converge` is the **only** action that can touch a committed `.claude/settings.json`. It never runs -implicitly from `sync` — `sync`'s report only names the `converge` command; the user runs it -explicitly. +`converge` is the action that consolidates cross-scope divergence and can rewrite a committed +`.claude/settings.json` after explicit per-plugin confirm. It never runs implicitly from `sync` — +`sync` can also write committed settings when Step 5 issues `enable -s project` (see +[scope-semantics.md](scope-semantics.md)), but `sync`'s report only names the `converge` command for +divergence; the user runs `converge` explicitly. ## Autonomous-session abort (run this check FIRST, before any preview work) @@ -60,12 +62,13 @@ rows a bulk report collapses) — never construct the proposed command as a bare running the bare form for a row whose `projectPath` isn't the current directory would silently mutate — or fail against — the wrong repo's settings. -Project scope keys on the **working directory**, not the repository — verified on Claude Code -2.1.228 by uninstalling one id in a repo's main checkout and observing its git worktree's record for -the same id survive untouched. Two checkouts of one repo (a `git worktree`, sharing one `.git` and -one tracked `.claude/settings.json`) therefore hold independent `projectPath` records and pin -independently. Never collapse them into one row, and never assume converging one clears the other: -each needs its own `cd`. +Two `git worktree` checkouts of one repository pin independently — verified on Claude Code 2.1.228 +by uninstalling one id in a repo's main checkout and observing the worktree's record for the same id +survive untouched. They share one `.git` and one tracked `.claude/settings.json` yet hold separate +`projectPath` records, so never collapse them into one row and never assume converging one clears +the other: each needs its own `cd`. Per [scope-semantics.md](scope-semantics.md), the CLI keys +`projectPath` on the literal cwd while `fleet-state.sh` matches on the checkout root — that gap is a +blind spot in its own right, recorded in [gotchas.md](gotchas.md). Present every plugin's proposed strategy and exact CLI command(s) before running anything — do not batch-apply. Per Brief Decision 6 (V1): confirm **every** pin individually, even when many plugins diff --git a/plugins/claude-ops/skills/plugins/context/gotchas.md b/plugins/claude-ops/skills/plugins/context/gotchas.md index b8316bc00b..2f528a0f16 100644 --- a/plugins/claude-ops/skills/plugins/context/gotchas.md +++ b/plugins/claude-ops/skills/plugins/context/gotchas.md @@ -39,6 +39,29 @@ copy) before comparing — empirically verified to fold both representations to canonical string. Never hand-roll a separate path comparison anywhere else in this skill; always go through the `currentProject` field `fleet-state.sh` already computed. +## A subdirectory install is invisible to this skill — `currentProject` cannot see it + +Distinct from the spelling mismatch above: here both sides are spelled correctly and still never +match, because they name *different directories*. Per +[scope-semantics.md](scope-semantics.md), `claude plugin install -s project` records `projectPath` as +the **literal cwd** — verified on Claude Code 2.1.228, where installing from +`/nested/subdir` recorded that subdirectory and created its own +`nested/subdir/.claude/settings.json`. `fleet-state.sh` resolves the project root to the **checkout +root** instead. + +So a plugin installed at project scope from anywhere below the checkout root gets a `projectPath` +that `fleet-state.sh` will never match for `currentProject`: `currentProject` stays `false`, so +`sync`'s Step 2 never updates it — while `converge` can still target a divergence row for the same +id when another scope record exists, because `fleet-state.sh` groups every installed record by id +without filtering on `currentProject`. The subtree install still loads for anyone working there. The +failure is silent for sync/update in the same way the spelling mismatch is, and the same report +still gets produced. + +This is a genuine gap, not a parser bug to fix by widening the comparison: matching a record against +every ancestor of the checkout root would claim project state the skill has not established is +project state. Treat an unexplained "installed but never converges" report as a candidate for this, +and confirm by reading the record's `projectPath` directly. + ## Concurrency / TOCTOU `fleet-state.sh`'s output is a snapshot. A background `autoUpdate` sweep (random delay up to ten diff --git a/plugins/claude-ops/skills/plugins/context/scope-semantics.md b/plugins/claude-ops/skills/plugins/context/scope-semantics.md index a1a1067762..6ae0f67665 100644 --- a/plugins/claude-ops/skills/plugins/context/scope-semantics.md +++ b/plugins/claude-ops/skills/plugins/context/scope-semantics.md @@ -53,26 +53,61 @@ the machine-local `installed_plugins.json` record. `enabledPlugins` carries no v committed settings files are untouched by an update. Re-verified on Claude Code 2.1.228 under the hardest available conditions: a tracked `.claude/settings.json` that a sibling `install -s project` had just rewritten, reverted to clean, then updated — the update left it clean. `sync`'s in-repo -update step is therefore safe to run without a settings-diff review, and `converge` is the only -action this skill runs that surfaces a settings diff. +update step is therefore safe to run without a settings-diff review. It is the exception, not the +rule: the next section lists the calls that do write. -## `plugin install|uninstall -s project` DO write committed settings +## Every call that touches `enabledPlugins` at project scope writes committed settings **Empirically verified on Claude Code 2.1.228** — one call each, against a clean tracked `.claude/settings.json`, git-diffed after every step: -- `claude plugin install -s project` adds the id to `enabledPlugins` (value `true`) in the - committed file. -- `claude plugin uninstall -s project` removes that entry but leaves `"enabledPlugins": {}` - behind — it empties the map rather than deleting the key, and it writes the key even into a - committed file that never had one. -- Both rewrite the whole file in Claude Code's own key order, so sibling keys unrelated to plugins - can move. The reorder is a serialization artifact of the write, not a semantic change. - -Consequence for `converge`: an `uninstall -s project` against a project whose committed settings -carry no `enabledPlugins` entry still dirties the tracked file, with a diff that changes no -behavior — an empty map plus a key reorder. Expect that diff; it is not evidence an entry was -removed. [converge.md](converge.md) Step 5 classifies it. +| Call | Writes `.claude/settings.json`? | Effect | +|---|---|---| +| `install -s project` | yes | adds the id to `enabledPlugins`, value `true` | +| `uninstall -s project` | yes | removes the entry, leaves `"enabledPlugins": {}` | +| `enable -s project` | yes | adds the id, value `true` | +| `disable -s project` | yes | adds the id, value `false` | +| `update -s project` | no | the exemption above | + +Three properties hold across every writing call: + +- **The key is created when absent.** `uninstall` writes `enabledPlugins` even into a committed file + that never had it, emptying the map to `{}` rather than deleting the key. +- **The whole file is re-serialized in Claude Code's key order,** so sibling keys unrelated to + plugins can move. The reorder is a serialization artifact, not a semantic change. +- **`-s local` writes `.claude/settings.local.json` instead** — verified for `enable`/`disable`, + which created that file and left the tracked `.claude/settings.json` clean. That file is + gitignored, so local scope never dirties team-shared state. + +`enable -s project` gates on the **merged effective** value, not that scope's raw map: enabling an id +that is `true` only at user scope fails with `Plugin "" is already enabled at project scope` +rather than writing a project-scope entry. + +Two consequences: + +- **`converge`** — an `uninstall -s project` against a project whose committed settings carry no + `enabledPlugins` entry still dirties the tracked file, with a diff that changes no behavior: an + empty map plus a key reorder. Expect it; it is not evidence an entry was removed. + [converge.md](converge.md) Step 5 classifies it. +- **`sync`** — Step 5 issues `enable -s `, so a `project`-scope completeness gap + makes the default, non-destructive action write a team-shared tracked file. `sync` surfaces no + settings diff today; [sync.md](sync.md) Step 5 flags the exposure. + +## Project scope: the CLI keys on the cwd, `fleet-state.sh` matches on the checkout root + +**Empirically verified on Claude Code 2.1.228.** `-s project` has no path flag — it acts on the +current directory, and it means that literally. Installing from `/nested/subdir` recorded +`projectPath: \nested\subdir` and created a fresh `nested/subdir/.claude/settings.json`, +rather than resolving up to the checkout root. + +`fleet-state.sh` resolves its project root differently: `CLAUDE_PROJECT_DIR`, else +`git rev-parse --show-toplevel`, else a `.claude`-corroborated cwd (`fleet-state.sh:211-221`), and +`fleet-state.test.sh` pins that a session invoked from a nested subdirectory still matches the +checkout-root record. + +The two layers therefore disagree, which is a real blind spot — see +[gotchas.md](gotchas.md). It also means two `git worktree` checkouts of one repo, sharing one `.git` +and one tracked `.claude/settings.json`, hold independent records and pin independently. ## `/reload-plugins` — bare by default, `--force` for the MCP-cache-invalidation case diff --git a/plugins/claude-ops/skills/plugins/context/sync.md b/plugins/claude-ops/skills/plugins/context/sync.md index ce1867bf85..2dba760539 100644 --- a/plugins/claude-ops/skills/plugins/context/sync.md +++ b/plugins/claude-ops/skills/plugins/context/sync.md @@ -179,6 +179,15 @@ Never touches an id that has an explicit entry anywhere (true — already enable false — deliberate opt-out, never flipped). This step only fills a genuine gap: installed but never recorded either way. +**Known exposure — `-s project` here dirties a tracked file, and `sync` surfaces no diff.** +`enable -s project` writes the project's committed `.claude/settings.json` (verified on Claude +Code 2.1.228; see [scope-semantics.md](scope-semantics.md)), so this step can leave a team-shared +tracked file modified without the report ever mentioning it — the failure class +[converge.md](converge.md) Step 5 exists to prevent, in the default action. `-s user` and `-s local` +are unaffected: local scope writes the gitignored `.claude/settings.local.json`. Until this step +gains converge's diff-surfacing, name any `-s project` enable in the report so the user knows to +check `git status`. + ## Step 6 — Report Emit the report per SKILL.md's "Report" section, filling each updated plugin's ` → ` from