From 0d8cec45a8a22f35b486f7c9fd2df9994a966de1 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:34:42 -0400 Subject: [PATCH] fix(claude-ops): correct converge's settings-write claim and scope-keying gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit converge.md Step 5 said `uninstall -s project` "can remove an enabledPlugins entry" from committed settings, which reads as "a clean tree means nothing was written" — so a run that predicted no diff had grounds to skip the check. It always writes. Verified on Claude Code 2.1.228 with single calls against a clean tracked .claude/settings.json: it empties the map to `"enabledPlugins": {}` rather than deleting the key, writes the key even into a file that never had one, and rewrites the file in Claude Code's key order so unrelated sibling keys move. Step 5 now checks every touched project unconditionally and classifies the diff as inert or substantive, because the two warrant opposite advice. scope-semantics.md gains that as a section beside the update exemption, which was re-verified on the same version under the hardest available conditions and still holds. Also recorded: project scope keys on the working directory, not the repository — two checkouts of one repo pin independently, so converge must keep them as separate rows with separate cd targets. And one observation on installed_plugins.json write timing, logged as a data point that does not retire sync.md's fallback. Co-authored-by: Claude Opus 5 (1M context) --- plugins/claude-ops/.claude-plugin/plugin.json | 2 +- plugins/claude-ops/CHANGELOG.md | 28 +++++++++++++++++++ .../skills/plugins/context/converge.md | 28 +++++++++++++++---- .../skills/plugins/context/scope-semantics.md | 26 +++++++++++++++-- .../claude-ops/skills/plugins/context/sync.md | 6 ++++ 5 files changed, 81 insertions(+), 9 deletions(-) diff --git a/plugins/claude-ops/.claude-plugin/plugin.json b/plugins/claude-ops/.claude-plugin/plugin.json index e48002bbdb..f899cc1432 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.6", + "version": "0.31.7", "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 256ee087a6..651ec4bc1e 100644 --- a/plugins/claude-ops/CHANGELOG.md +++ b/plugins/claude-ops/CHANGELOG.md @@ -3,6 +3,34 @@ 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.7] + +### Fixed + +- **`skills/plugins` predicted the wrong settings-write behavior for `converge`.** `converge.md` + Step 5 said `uninstall -s project` "can remove an `enabledPlugins` entry" from committed settings, + which reads as "a clean tree means nothing was written". It always writes: verified on Claude Code + 2.1.228 with single calls against a clean tracked `.claude/settings.json`, it empties the map to + `"enabledPlugins": {}` rather than deleting the key, writes the key even into a file that never had + one, and rewrites the file in Claude Code's key order so unrelated sibling keys move. Step 5 now + checks every touched project unconditionally and classifies the diff as inert (empty map plus + reorder — recommend discarding, so a team-shared file carries no churn) or substantive (an entry + actually removed — the user decides). `scope-semantics.md` records install's and uninstall's + behavior as a section beside the update exemption, which was re-verified on the same version and + still holds. + +### Added + +- **`skills/plugins` records that project scope keys on the working directory, not the repository.** + Verified by uninstalling one id in a repo's main checkout and watching its `git worktree`'s record + for the same id survive. Two checkouts of one repo share a `.git` and a tracked + `.claude/settings.json` yet pin independently, so `converge` must keep them as separate rows with + separate `cd` targets — converging one never clears the other. +- **`sync.md` records one observation on `installed_plugins.json` write timing.** A 63-plugin + user-scope sweep on 2.1.228 had all 21 CLI-reported updates already visible to a post-sweep + re-read. Logged as a single data point that does not retire the `` fallback, since it shows + only that the write landed before the re-read on that run. + ## [0.31.6] ### Changed diff --git a/plugins/claude-ops/skills/plugins/context/converge.md b/plugins/claude-ops/skills/plugins/context/converge.md index c5f624a768..4146c3fb0f 100644 --- a/plugins/claude-ops/skills/plugins/context/converge.md +++ b/plugins/claude-ops/skills/plugins/context/converge.md @@ -60,6 +60,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`. + 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 share the same strategy — do not infer consent from one confirm to the next. @@ -79,11 +86,22 @@ Step 1 if meaningful time has passed or another mutation already landed. ## Step 5 — Surface the resulting diff After all confirmed mutations run, `git diff` (or the equivalent status check) any project's -committed `.claude/settings.json` that `-s project` mutations may have touched. Per -[scope-semantics.md](scope-semantics.md), a plain `claude plugin update -s project` does **not** -write committed settings — but `claude plugin uninstall -s project` (this action's actual mechanism) -can remove an `enabledPlugins` entry from it. Show the diff; never commit it. The user reviews and -commits (or discards) it through their own normal git workflow. +committed `.claude/settings.json` that `-s project` mutations touched — every one of them, not only +the ones expected to change. Per [scope-semantics.md](scope-semantics.md), `claude plugin uninstall +-s project` (this action's actual mechanism) **always** writes that file: it removes the id's +`enabledPlugins` entry, leaves `"enabledPlugins": {}` when that empties the map, writes the key even +into a file that never had one, and rewrites the whole file in Claude Code's key order. A clean tree +after an uninstall is the surprising outcome, not a dirty one — never predict "no diff" from the +absence of an `enabledPlugins` key and skip the check on that basis. + +Classify each diff before showing it, because the two cases warrant opposite advice: + +- **Inert** — only an empty `"enabledPlugins": {}` added and/or sibling keys reordered. No behavior + changes. Say so and recommend discarding it, so a tracked, team-shared file does not carry churn. +- **Substantive** — an actual `enabledPlugins` entry removed. That is a real change to what the + project enables for everyone who checks it out. Show it and leave the decision to the user. + +Never commit either. The user reviews and commits (or discards) through their own git workflow. ## Non-interactive execution diff --git a/plugins/claude-ops/skills/plugins/context/scope-semantics.md b/plugins/claude-ops/skills/plugins/context/scope-semantics.md index 488f3cb344..a1a1067762 100644 --- a/plugins/claude-ops/skills/plugins/context/scope-semantics.md +++ b/plugins/claude-ops/skills/plugins/context/scope-semantics.md @@ -50,9 +50,29 @@ probe — never from `list`/`details` text. **Empirically verified** (hash-compared a real repo's `.claude/settings.json` and `.claude/settings.local.json` before and after): `claude plugin update -s project` updates only the machine-local `installed_plugins.json` record. `enabledPlugins` carries no version — the -committed settings files are untouched by an update. `sync`'s in-repo update step is safe to run -without a settings-diff review; `converge`'s scope-*consolidation* is the one action that can add or -remove an `enabledPlugins` entry, and only that action surfaces a settings diff. +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. + +## `plugin install|uninstall -s project` DO write 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. ## `/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 aaf9166f0c..ce1867bf85 100644 --- a/plugins/claude-ops/skills/plugins/context/sync.md +++ b/plugins/claude-ops/skills/plugins/context/sync.md @@ -34,6 +34,12 @@ an id and the post-sweep version is unchanged, report the CLI's reported value, named none — never report ``, and never count that id as not-updated. A report line that says nothing changed for a plugin that did change is worse than one that admits it cannot tell. +One data point, not a licence to drop the fallback: on Claude Code 2.1.228 a 63-plugin user-scope +sweep had all 21 CLI-reported updates already reflected in a post-sweep `fleet-state.sh` re-read, so +source 3 agreed with source 2 on every id. That establishes the write landed before the re-read on +that run — not that it is synchronous per call, and not that it holds on another version. Keep +source 2 primary and keep the divergence handling above. + ## Step 1 — Marketplace refresh For each target marketplace (the resolved default, the named one, or every marketplace when the