Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/claude-ops/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
29 changes: 29 additions & 0 deletions plugins/claude-ops/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Comment thread
kyle-sexton marked this conversation as resolved.
False by its own new evidence: `sync`'s Step 5 issues `enable <id> -s <that scope>`, 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 `<checkout>/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
Expand Down
8 changes: 5 additions & 3 deletions plugins/claude-ops/skills/plugins/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down
21 changes: 12 additions & 9 deletions plugins/claude-ops/skills/plugins/context/converge.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions plugins/claude-ops/skills/plugins/context/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
`<checkout>/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
Expand Down
65 changes: 50 additions & 15 deletions plugins/claude-ops/skills/plugins/context/scope-semantics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id> -s project` adds the id to `enabledPlugins` (value `true`) in the
committed file.
- `claude plugin uninstall <id> -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 <id> -s project` | yes | adds the id to `enabledPlugins`, value `true` |
| `uninstall <id> -s project` | yes | removes the entry, leaves `"enabledPlugins": {}` |
| `enable <id> -s project` | yes | adds the id, value `true` |
| `disable <id> -s project` | yes | adds the id, value `false` |
| `update <id> -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 "<id>" 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 <id> -s <that scope>`, 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 `<checkout>/nested/subdir` recorded
`projectPath: <checkout>\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

Expand Down
9 changes: 9 additions & 0 deletions plugins/claude-ops/skills/plugins/context/sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id> -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
Comment thread
kyle-sexton marked this conversation as resolved.
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 `<old> → <new>` from
Expand Down