From c8ec46dca91b7473c14a8acdaff107908d20df4b Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:22:43 -0400 Subject: [PATCH 1/4] fix(claude-ops): series-agnostic changelog grep, raw-md fetch channel, ultracode lane effort The changelog skill's status action pinned the CC 2.1 series in its git-log greps and fetched the 511KB HTML page rather than the deterministic raw .md channel. The lanes launcher rejected `ultracode`, which `--effort` accepts, so an ultracode lane was silently unlaunchable. Observability's cost section now carries the list-rate caveat the costs page states for subscription users. Doc-alignment rows 62, 77, 86. UNVERIFIED: the producer and its verifier were both killed by a usage limit before the verification gate ran. Needs a fresh-context verifier pass before merge. Co-authored-by: Claude Fable 5 --- plugins/claude-ops/.claude-plugin/plugin.json | 2 +- plugins/claude-ops/CHANGELOG.md | 28 +++++++++++++++++++ plugins/claude-ops/skills/changelog/SKILL.md | 4 +-- .../skills/changelog/context/read-actions.md | 6 ++-- .../claude-ops/skills/lanes/context/config.md | 2 +- .../skills/lanes/scripts/lane-launcher.sh | 4 +-- .../lanes/scripts/lane-launcher.test.sh | 5 +++- .../observability/context/output-format.md | 6 ++++ 8 files changed, 47 insertions(+), 10 deletions(-) diff --git a/plugins/claude-ops/.claude-plugin/plugin.json b/plugins/claude-ops/.claude-plugin/plugin.json index 4e63631b8..0c3ce0610 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.27.0", + "version": "0.27.1", "description": "Claude Code operations toolkit. Seven skills: observability (read locally captured telemetry — OTEL store, collector, hook-event JSONL, ccusage — 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 — 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 — 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 — 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 a75ff0085..b87bc0372 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.27.1] + +### Changed + +- **`observability` marks the Token / cost dollar column as a list-rate estimate.** ccusage prices + tokens at public per-token list rates while subscription usage is plan-priced, so the USD figures + are an estimate, not a bill; [costs](https://code.claude.com/docs/en/costs.md) (verified + 2026-08-04) documents the same list-rate caveat for Claude Code's own locally computed figures. + +### Fixed + +- **`changelog` `status` no longer goes silent on the first release outside the CC 2.1 series.** + The applied-versions scan grepped git log with patterns pinned to `v2\.1\.`, so a 2.2.x/3.x + release would return nothing without erroring. The scan now matches any + `CC v..` (`-E --grep="CC v[0-9]+\.[0-9]+\.[0-9]+"`), semantics otherwise + unchanged; a skill-wide sweep confirmed no other file carries the series pin — remaining + `v2.1.x` literals are illustrative examples. +- **`changelog` fetch steps target the raw-markdown channel (`docs/en/changelog.md`), not the + rendered HTML page.** The rendered page is ~2.7MB of HTML and WebFetch silently truncates it, + losing deep versions; the ~511KB `.md` sibling serves the same content byte-stable (sizes + measured 2026-08-04). + Every fetch-source reference in the skill now points at the `.md` URL. +- **`lanes` no longer skips a lane whose effort is `ultracode`.** The launcher validated + `lanes[].effort` against `low|medium|high|xhigh|max`, so `ultracode` — a documented + `claude --effort` value since CC 2.1.203 (verified 2026-08-04 against the CLI reference) — made + the lane silently unlaunchable. The valid set now includes it; the config docs note that an older + CLI rejects the value and starts the session at default effort. + ## [0.27.0] ### Added diff --git a/plugins/claude-ops/skills/changelog/SKILL.md b/plugins/claude-ops/skills/changelog/SKILL.md index 7c15b3f21..3b9e36499 100644 --- a/plugins/claude-ops/skills/changelog/SKILL.md +++ b/plugins/claude-ops/skills/changelog/SKILL.md @@ -28,7 +28,7 @@ Distinct from: Three ways to provide changelog content (priority order): 1. **User pastes text** — skill parses inline changelog from conversation context -2. **Specific version** — `/changelog apply v2.1.152` fetches that version from `code.claude.com/docs/en/changelog` +2. **Specific version** — `/changelog apply v2.1.152` fetches that version from `code.claude.com/docs/en/changelog.md` 3. **Auto-detect latest** — `/changelog apply` (no version) automatically fetches changelog, identifies latest version, and proceeds ## Version awareness @@ -165,7 +165,7 @@ If user approves: The three read-only actions stop short of any edit — **full steps in [context/read-actions.md](context/read-actions.md)**: -- **`fetch`** — WebFetch + display a version (or latest, or a `v.X..v.Y` range) of `code.claude.com/docs/en/changelog`. No edits +- **`fetch`** — WebFetch + display a version (or latest, or a `v.X..v.Y` range) of `code.claude.com/docs/en/changelog.md` (raw markdown — the rendered HTML page truncates deep versions). No edits - **`diff`** — dry run of `apply`: Phase 0 (ingest) + Phase 1 (explore) + Phase 2 (research), stops before interview. Emits the triage table only. Answers "is this release worth an `apply`?" - **`status`** — applied versions (`git log --grep`), open routine-pipeline issues (`gh issue list`), current `claude --version`, and the gap if installed > last-applied diff --git a/plugins/claude-ops/skills/changelog/context/read-actions.md b/plugins/claude-ops/skills/changelog/context/read-actions.md index 74e6cbe2b..889572b5d 100644 --- a/plugins/claude-ops/skills/changelog/context/read-actions.md +++ b/plugins/claude-ops/skills/changelog/context/read-actions.md @@ -8,7 +8,7 @@ Read-only. Fetch and display changelog content. **With version arg** (`/changelog fetch v2.1.152`): -1. WebFetch `https://code.claude.com/docs/en/changelog` +1. WebFetch `https://code.claude.com/docs/en/changelog.md` 2. Extract section matching version 3. Display formatted @@ -34,10 +34,10 @@ Useful for: "should I bother running apply for this release?" Show changelog integration status: -1. **Applied versions** — scan git log for commits mentioning "CC v2.1." or "Claude Code v2.1." or "changelog": +1. **Applied versions** — scan git log for commits mentioning "CC v" or "Claude Code v" or "changelog": ```bash - git log --oneline --all --grep="CC v2\.1\." --grep="Claude Code v2\.1\." -20 + git log --oneline --all -E --grep="CC v[0-9]+\.[0-9]+\.[0-9]+" --grep="Claude Code v[0-9]+\.[0-9]+\.[0-9]+" -20 ``` 2. **Open issues** — if the consumer repo files CC-release tracking issues, count the pending diff --git a/plugins/claude-ops/skills/lanes/context/config.md b/plugins/claude-ops/skills/lanes/context/config.md index 0d2e950db..43b626171 100644 --- a/plugins/claude-ops/skills/lanes/context/config.md +++ b/plugins/claude-ops/skills/lanes/context/config.md @@ -35,7 +35,7 @@ A missing config exits `4`; malformed JSON or a config with no lanes exits `3`. | `lanes[].name` | yes | The lane's session name — the `--name` value the launcher gives the background session, and the key `status`/`stop` match on. Keep distinct from ad-hoc session names. | | `lanes[].prompt` | yes | Path to the lane's canonical prompt file. Relative → resolved against `prompt_dir`; absolute → used as-is. The file's full contents seed the session (positional prompt). A missing or empty file skips that lane with an error. | | `lanes[].model` | no | Passed as `claude --model`. An alias (`opus`, `sonnet`, `fable`) or a full model id. Omit to inherit the machine default. | -| `lanes[].effort` | no | Passed as `claude --effort`. One of `low`, `medium`, `high`, `xhigh`, `max` (validated; a bad value skips the lane). Omit to inherit the default. | +| `lanes[].effort` | no | Passed as `claude --effort`. One of `low`, `medium`, `high`, `xhigh`, `max`, `ultracode` (validated; a bad value skips the lane). `ultracode` needs CC ≥ 2.1.203 — an older CLI rejects the value and starts the session at default effort. Omit to inherit the default. | | `lanes[].settings` | no | A JSON **object** passed inline as `claude --settings` — a session-only override that never persists. The motivating use is opting a lane into the `autonomy` plugin's lane-stop gate via a `pluginConfigs` override (example above; the plugin id is marketplace-qualified, `@`, for however the plugin was installed). A non-object value skips the lane with an error. A gate request (`lane_stop_gate_enabled: true` under an `autonomy` key) additionally triggers launch-time ARMING (#1784): the launcher runs autonomy's `hooks/lane-stop-gate-arm.sh` and injects a random `lane_stop_gate_arm_id` into the launched settings — the trusted per-session channel the gate actually honors (it ignores the bare env mirror a repo `env` block could forge). A gate-requesting lane that cannot be armed (autonomy missing/pre-0.12.0, arming error, managed-settings veto) is skipped with an error rather than launched silently ungated. | Lane names are free-form (`work`, `work-2`, `babysit`, `decide`, …); nothing is diff --git a/plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh b/plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh index 703c36a11..2c49de875 100755 --- a/plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh +++ b/plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh @@ -90,7 +90,7 @@ # prompt required; path to the canonical prompt file (absolute, or # relative to prompt_dir). # model optional; passed as --model. -# effort optional; passed as --effort (low|medium|high|xhigh|max). +# effort optional; passed as --effort (low|medium|high|xhigh|max|ultracode). # settings optional; a JSON OBJECT passed inline as --settings for that # session only (e.g. a pluginConfigs override opting the lane into # the autonomy plugin's lane-stop gate). Non-object values are @@ -119,7 +119,7 @@ set -uo pipefail -VALID_EFFORTS="low medium high xhigh max" +VALID_EFFORTS="low medium high xhigh max ultracode" ACTION="start" CONFIG="" diff --git a/plugins/claude-ops/skills/lanes/scripts/lane-launcher.test.sh b/plugins/claude-ops/skills/lanes/scripts/lane-launcher.test.sh index f9ccf95d7..f9194d8b5 100755 --- a/plugins/claude-ops/skills/lanes/scripts/lane-launcher.test.sh +++ b/plugins/claude-ops/skills/lanes/scripts/lane-launcher.test.sh @@ -371,7 +371,8 @@ cat >"$TMP/badprompt.json" <<'JSON' "lanes": [ { "name": "gone", "prompt": "missing.md" }, { "name": "blank", "prompt": "blank.md" }, - { "name": "baddy", "prompt": "work.md", "effort": "turbo" } + { "name": "baddy", "prompt": "work.md", "effort": "turbo" }, + { "name": "ultra", "prompt": "work.md", "effort": "ultracode" } ] } JSON : >"$REPO/.work/blank.md" @@ -380,6 +381,8 @@ assert_contains "missing prompt file skipped" "$out" "prompt file not found" assert_contains "empty prompt file skipped" "$out" "prompt file is empty" assert_contains "invalid effort skipped" "$out" "invalid effort 'turbo'" assert_not_contains "no launch for bad lanes" "$out" "claude --bg -n baddy" +assert_contains "ultracode effort accepted" "$out" "claude --bg -n ultra" +assert_contains "ultracode passed through as --effort" "$out" "--effort ultracode" # ============================================================================ # Medium 1 — an option must not swallow the next flag as its value diff --git a/plugins/claude-ops/skills/observability/context/output-format.md b/plugins/claude-ops/skills/observability/context/output-format.md index d6f0ca473..105fda87e 100644 --- a/plugins/claude-ops/skills/observability/context/output-format.md +++ b/plugins/claude-ops/skills/observability/context/output-format.md @@ -35,6 +35,9 @@ Repo: · Branch: 5-hour blocks (current + prior 3): +Cost USD is a list-rate estimate, not a bill: ccusage prices tokens at public per-token list +rates, while subscription usage is plan-priced. + ## Cache health (last ) | Model | Cache read | Cache creation | Read : create | @@ -115,6 +118,9 @@ Top recurring (same `:` ≥ 3×): publishes no threshold, so any HIGH/MEDIUM cutoff would be invented here rather than sourced. It is also the one section sourced from the OTEL store rather than ccusage, which is why it sits apart from Token / cost instead of adding columns to it +- **The Token / cost caveat line is fixed copy** — Claude Code documents the same list-rate + limitation for its own locally computed dollar figures + (, verified 2026-08-04) ## Severity coloring (terminal) From 4d7b16373c5a34fabb29fed2529fa134815d2879 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:56:47 -0400 Subject: [PATCH 2/4] fix(claude-ops): version-gate the ultracode effort instead of asserting old-CLI behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding `ultracode` to the static effort allowlist let it pass the launch-input preflight on any CLI. That preflight exists precisely so `restart` can reject a bad lane BEFORE stopping it; `ultracode` is the one effort upstream version-gates, so a machine below the floor would have had its healthy lane stopped and then failed to relaunch. The preflight now checks the installed `claude --version` against the documented v2.1.203 floor and skips the lane below it. Version comparison reuses the awk component-wise form context-guard already uses — `sort -V` is GNU-only and this repo's portability lane rejects it. Also drops the claim that an older CLI "rejects the value and starts the session at default effort". The CLI reference documents the version floor but not what an older binary does with the value, so the launcher refuses rather than guessing. lane-launcher.test.sh: 177 cases pass, including the new below-floor skip, at-floor launch, and the restart case proving a refused lane stays up. Co-authored-by: Claude Opus 5 --- plugins/claude-ops/CHANGELOG.md | 8 +++- .../claude-ops/skills/lanes/context/config.md | 2 +- .../skills/lanes/scripts/lane-launcher.sh | 44 +++++++++++++++++++ .../lanes/scripts/lane-launcher.test.sh | 27 ++++++++++++ 4 files changed, 78 insertions(+), 3 deletions(-) diff --git a/plugins/claude-ops/CHANGELOG.md b/plugins/claude-ops/CHANGELOG.md index b87bc0372..f3d7affb5 100644 --- a/plugins/claude-ops/CHANGELOG.md +++ b/plugins/claude-ops/CHANGELOG.md @@ -28,8 +28,12 @@ All notable changes to the `claude-ops` plugin are documented here. Format follo - **`lanes` no longer skips a lane whose effort is `ultracode`.** The launcher validated `lanes[].effort` against `low|medium|high|xhigh|max`, so `ultracode` — a documented `claude --effort` value since CC 2.1.203 (verified 2026-08-04 against the CLI reference) — made - the lane silently unlaunchable. The valid set now includes it; the config docs note that an older - CLI rejects the value and starts the session at default effort. + the lane silently unlaunchable. The valid set now includes it, gated on the installed + `claude --version` meeting that floor: `ultracode` is the one effort upstream version-gates, and + the CLI reference does not document what an older binary does with the value, so the launcher + refuses the lane instead of guessing. The check runs in the shared launch-input preflight, which + `restart` already performs BEFORE stopping — so a lane the gate refuses keeps running rather than + being taken down and left down. ## [0.27.0] diff --git a/plugins/claude-ops/skills/lanes/context/config.md b/plugins/claude-ops/skills/lanes/context/config.md index 43b626171..ea506e1a6 100644 --- a/plugins/claude-ops/skills/lanes/context/config.md +++ b/plugins/claude-ops/skills/lanes/context/config.md @@ -35,7 +35,7 @@ A missing config exits `4`; malformed JSON or a config with no lanes exits `3`. | `lanes[].name` | yes | The lane's session name — the `--name` value the launcher gives the background session, and the key `status`/`stop` match on. Keep distinct from ad-hoc session names. | | `lanes[].prompt` | yes | Path to the lane's canonical prompt file. Relative → resolved against `prompt_dir`; absolute → used as-is. The file's full contents seed the session (positional prompt). A missing or empty file skips that lane with an error. | | `lanes[].model` | no | Passed as `claude --model`. An alias (`opus`, `sonnet`, `fable`) or a full model id. Omit to inherit the machine default. | -| `lanes[].effort` | no | Passed as `claude --effort`. One of `low`, `medium`, `high`, `xhigh`, `max`, `ultracode` (validated; a bad value skips the lane). `ultracode` needs CC ≥ 2.1.203 — an older CLI rejects the value and starts the session at default effort. Omit to inherit the default. | +| `lanes[].effort` | no | Passed as `claude --effort`. One of `low`, `medium`, `high`, `xhigh`, `max`, `ultracode` (validated; a bad value skips the lane). `ultracode` [requires Claude Code v2.1.203 or later](https://code.claude.com/docs/en/cli-reference#cli-flags); the launcher checks the installed `claude --version` and skips the lane below that floor rather than launching a session whose effort the CLI may not honor. `restart` makes that check before stopping, so a refused lane keeps running. Omit to inherit the default. | | `lanes[].settings` | no | A JSON **object** passed inline as `claude --settings` — a session-only override that never persists. The motivating use is opting a lane into the `autonomy` plugin's lane-stop gate via a `pluginConfigs` override (example above; the plugin id is marketplace-qualified, `@`, for however the plugin was installed). A non-object value skips the lane with an error. A gate request (`lane_stop_gate_enabled: true` under an `autonomy` key) additionally triggers launch-time ARMING (#1784): the launcher runs autonomy's `hooks/lane-stop-gate-arm.sh` and injects a random `lane_stop_gate_arm_id` into the launched settings — the trusted per-session channel the gate actually honors (it ignores the bare env mirror a repo `env` block could forge). A gate-requesting lane that cannot be armed (autonomy missing/pre-0.12.0, arming error, managed-settings veto) is skipped with an error rather than launched silently ungated. | Lane names are free-form (`work`, `work-2`, `babysit`, `decide`, …); nothing is diff --git a/plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh b/plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh index 2c49de875..3c1bafd8f 100755 --- a/plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh +++ b/plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh @@ -91,6 +91,8 @@ # relative to prompt_dir). # model optional; passed as --model. # effort optional; passed as --effort (low|medium|high|xhigh|max|ultracode). +# ultracode additionally requires the installed CLI to meet +# ULTRACODE_MIN_VERSION; a lane below it is skipped, not launched. # settings optional; a JSON OBJECT passed inline as --settings for that # session only (e.g. a pluginConfigs override opting the lane into # the autonomy plugin's lane-stop gate). Non-object values are @@ -121,6 +123,40 @@ set -uo pipefail VALID_EFFORTS="low medium high xhigh max ultracode" +# `ultracode` is the one effort upstream gates on a CLI version, so it is the one +# the static allowlist cannot settle on its own. Upstream documents the floor but +# not what an older CLI does with the value, so the launcher refuses rather than +# guessing — restart preflights this BEFORE stopping, keeping a healthy lane up. +ULTRACODE_MIN_VERSION="2.1.203" + +# Compared component by component in awk — deliberately NOT `sort -V`, which is a +# GNU-only construct this repo's portability lane rejects. Missing components +# compare as 0, so "2.2" reads as 2.2.0. +version_at_least() { # + [[ "$1" =~ ^[0-9]+(\.[0-9]+)*$ ]] || return 1 + awk -F. -v a="$1" -v b="$2" 'BEGIN { + na = split(a, x, "."); nb = split(b, y, ".") + n = (na > nb ? na : nb) + for (i = 1; i <= n; i++) { + av = (i <= na ? x[i] + 0 : 0); bv = (i <= nb ? y[i] + 0 : 0) + if (av > bv) exit 0 + if (av < bv) exit 1 + } + exit 0 + }' 2>/dev/null +} + +# Memoized: every lane in a run shares one `claude --version` probe. +CLI_VERSION_CACHE="" +cli_version() { + if [[ -z "$CLI_VERSION_CACHE" ]]; then + CLI_VERSION_CACHE="$(claude --version 2>/dev/null | tr -d '\r' | + grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" + [[ -z "$CLI_VERSION_CACHE" ]] && CLI_VERSION_CACHE="unknown" + fi + printf '%s' "$CLI_VERSION_CACHE" +} + ACTION="start" CONFIG="" REPO="" @@ -649,6 +685,14 @@ validate_launch_inputs() { err "lane '$name': invalid effort '$effort' (want: $VALID_EFFORTS) — skipped" return 1 fi + if [[ "$effort" == "ultracode" ]]; then + local v + v="$(cli_version)" + if ! version_at_least "$v" "$ULTRACODE_MIN_VERSION"; then + err "lane '$name': effort 'ultracode' needs Claude Code >= $ULTRACODE_MIN_VERSION (installed: $v) — skipped" + return 1 + fi + fi # `settings` must be a JSON object — the launcher passes it verbatim to # `claude --settings`, and a string/array/scalar would make the whole session # fail to launch with an opaque CLI error instead of a per-lane skip here. diff --git a/plugins/claude-ops/skills/lanes/scripts/lane-launcher.test.sh b/plugins/claude-ops/skills/lanes/scripts/lane-launcher.test.sh index f9194d8b5..79ed09bff 100755 --- a/plugins/claude-ops/skills/lanes/scripts/lane-launcher.test.sh +++ b/plugins/claude-ops/skills/lanes/scripts/lane-launcher.test.sh @@ -102,6 +102,11 @@ if [[ "\$1" == "stop" ]]; then exit "\${STUB_CLAUDE_STOP_RC:-0}"; fi # A test can force a failed \`claude plugin marketplace update\` via # STUB_CLAUDE_UPDATE_RC to exercise the refresh-failure abort path. if [[ "\$1" == "plugin" ]]; then exit "\${STUB_CLAUDE_UPDATE_RC:-0}"; fi +# STUB_CLAUDE_VERSION drives the ultracode version gate; the default clears it. +if [[ "\$1" == "--version" ]]; then + printf '%s (Claude Code)\n' "\${STUB_CLAUDE_VERSION:-2.1.220}" + exit 0 +fi STUB REAL_GIT="$(command -v git)" cat >"$STUB_BIN/git" <"$TMP/ultra.json" <<'JSON' +{ "prompt_dir": ".work", + "lanes": [ { "name": "work", "prompt": "work.md", "effort": "ultracode" } ] } +JSON +out="$(STUB_CLAUDE_VERSION=2.1.202 run_launcher start --repo "$REPO" --config "$TMP/ultra.json" --agents-json "$AGENTS_EMPTY" --dry-run 2>&1)" +assert_contains "ultracode below floor skipped" "$out" "needs Claude Code >= 2.1.203" +assert_contains "skip message reports installed version" "$out" "(installed: 2.1.202)" +assert_not_contains "no launch below the floor" "$out" "claude --bg -n work" + +out="$(STUB_CLAUDE_VERSION=2.1.203 run_launcher start --repo "$REPO" --config "$TMP/ultra.json" --agents-json "$AGENTS_EMPTY" --dry-run 2>&1)" +assert_contains "ultracode at the floor launches" "$out" "claude --bg -n work" + +# The running lane must survive a restart the version gate refuses. +: >"$CLAUDE_LOG" +out="$(STUB_CLAUDE_VERSION=2.1.202 run_launcher restart --repo "$REPO" --config "$TMP/ultra.json" --agents-json "$AGENTS_RUNNING" --dry-run 2>&1)" +assert_contains "restart refused below the floor" "$out" "needs Claude Code >= 2.1.203" +assert_not_contains "healthy lane not stopped by a refused restart" "$(cat "$CLAUDE_LOG")" "stop" + # ============================================================================ # Medium 1 — an option must not swallow the next flag as its value # ============================================================================ From 97bd84dc175f8eac3acfd827e62952fb450181c3 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:05:42 -0400 Subject: [PATCH 3/4] fix(claude-ops): arm the dry-run and effort test gates and finish the truncation-claim corrections Both changelog channels truncate identically under WebFetch, so the raw-markdown switch is now described as a size and cleanliness win rather than a depth one, and the read paths warn never to call a version absent on a truncated fetch. The ultracode refusal is re-grounded on model-config, which documents that a pre-2.1.203 binary prints `Unknown --effort value ultracode` and starts at the default effort, replacing the claim that upstream left the behavior undocumented. The two gates that could not fail are now armed: the restart gate drops --dry-run so the stop actually reaches the stub, and the effort gate asserts the real launch line instead of an info echo printed before validation. cli_version is genuinely memoized via the cache global and honors require_claude's dry-run exemption. Co-authored-by: Claude Opus 5 --- plugins/claude-ops/CHANGELOG.md | 29 ++++++++----- plugins/claude-ops/skills/changelog/SKILL.md | 2 +- .../skills/changelog/context/read-actions.md | 6 +++ .../claude-ops/skills/lanes/context/config.md | 2 +- .../skills/lanes/scripts/lane-launcher.sh | 41 ++++++++++++------- .../lanes/scripts/lane-launcher.test.sh | 39 ++++++++++++++++-- 6 files changed, 87 insertions(+), 32 deletions(-) diff --git a/plugins/claude-ops/CHANGELOG.md b/plugins/claude-ops/CHANGELOG.md index f3d7affb5..f8aba69a2 100644 --- a/plugins/claude-ops/CHANGELOG.md +++ b/plugins/claude-ops/CHANGELOG.md @@ -21,19 +21,26 @@ All notable changes to the `claude-ops` plugin are documented here. Format follo unchanged; a skill-wide sweep confirmed no other file carries the series pin — remaining `v2.1.x` literals are illustrative examples. - **`changelog` fetch steps target the raw-markdown channel (`docs/en/changelog.md`), not the - rendered HTML page.** The rendered page is ~2.7MB of HTML and WebFetch silently truncates it, - losing deep versions; the ~511KB `.md` sibling serves the same content byte-stable (sizes - measured 2026-08-04). - Every fetch-source reference in the skill now points at the `.md` URL. + rendered HTML page.** The `.md` sibling is the smaller, chrome-free channel — 514,578 B against + the rendered page's 2,696,671 B (~5x), measured 2026-08-04 — and both carry the same 355 + releases. It buys no extra version depth: WebFetch truncates **both** channels identically, to + the same 32 most-recent versions with a `[Content truncated due to length...]` marker, because + its budget applies after HTML-to-markdown conversion. Reaching a deep version needs a + range-scoped fetch or a direct `curl`, on either channel. Every fetch-source reference in the + skill now points at the `.md` URL. - **`lanes` no longer skips a lane whose effort is `ultracode`.** The launcher validated `lanes[].effort` against `low|medium|high|xhigh|max`, so `ultracode` — a documented - `claude --effort` value since CC 2.1.203 (verified 2026-08-04 against the CLI reference) — made - the lane silently unlaunchable. The valid set now includes it, gated on the installed - `claude --version` meeting that floor: `ultracode` is the one effort upstream version-gates, and - the CLI reference does not document what an older binary does with the value, so the launcher - refuses the lane instead of guessing. The check runs in the shared launch-input preflight, which - `restart` already performs BEFORE stopping — so a lane the gate refuses keeps running rather than - being taken down and left down. + `claude --effort` value since CC 2.1.203 (verified 2026-08-04 against + [model-config](https://code.claude.com/docs/en/model-config#adjust-effort-level)) — made the + lane silently unlaunchable. The valid set now includes it, gated on the installed + `claude --version` meeting that floor: below it the CLI rejects the value outright (`Unknown + --effort value 'ultracode'`) and starts the session at the default effort, so the launcher skips + the lane rather than launching it at an unintended effort. That check runs in the shared + launch-input preflight, which `restart` already performs BEFORE stopping — so a lane the gate + refuses keeps running rather than being taken down and left down. The whole run shares one + `claude --version` probe, and `--dry-run` keeps working with no CLI installed (the exemption + `require_claude` documents): with no binary to probe, the preview reports the gate unevaluated + instead of refusing a lane a real run may well launch. ## [0.27.0] diff --git a/plugins/claude-ops/skills/changelog/SKILL.md b/plugins/claude-ops/skills/changelog/SKILL.md index 3b9e36499..89d2f1ea8 100644 --- a/plugins/claude-ops/skills/changelog/SKILL.md +++ b/plugins/claude-ops/skills/changelog/SKILL.md @@ -165,7 +165,7 @@ If user approves: The three read-only actions stop short of any edit — **full steps in [context/read-actions.md](context/read-actions.md)**: -- **`fetch`** — WebFetch + display a version (or latest, or a `v.X..v.Y` range) of `code.claude.com/docs/en/changelog.md` (raw markdown — the rendered HTML page truncates deep versions). No edits +- **`fetch`** — WebFetch + display a version (or latest, or a `v.X..v.Y` range) of `code.claude.com/docs/en/changelog.md` (raw markdown — the smaller, chrome-free channel; WebFetch truncates it and the rendered page alike, so a deep version needs a range-scoped fetch or `curl`). No edits - **`diff`** — dry run of `apply`: Phase 0 (ingest) + Phase 1 (explore) + Phase 2 (research), stops before interview. Emits the triage table only. Answers "is this release worth an `apply`?" - **`status`** — applied versions (`git log --grep`), open routine-pipeline issues (`gh issue list`), current `claude --version`, and the gap if installed > last-applied diff --git a/plugins/claude-ops/skills/changelog/context/read-actions.md b/plugins/claude-ops/skills/changelog/context/read-actions.md index 889572b5d..a9c8ca70c 100644 --- a/plugins/claude-ops/skills/changelog/context/read-actions.md +++ b/plugins/claude-ops/skills/changelog/context/read-actions.md @@ -6,6 +6,12 @@ The three read-only actions (`fetch`, `diff`, `status`). SKILL.md keeps the acti Read-only. Fetch and display changelog content. +WebFetch truncates this document — the raw-markdown and rendered-HTML channels alike — to roughly +the 32 most recent releases, emitting a `[Content truncated due to length...]` marker. A version +older than that window will not be in the response no matter which channel is used, so fetch it +with a range- or anchor-scoped request, or `curl` the `.md` and slice locally. Never report a +version "absent from the changelog" on a truncated fetch. + **With version arg** (`/changelog fetch v2.1.152`): 1. WebFetch `https://code.claude.com/docs/en/changelog.md` diff --git a/plugins/claude-ops/skills/lanes/context/config.md b/plugins/claude-ops/skills/lanes/context/config.md index ea506e1a6..71bb1a83d 100644 --- a/plugins/claude-ops/skills/lanes/context/config.md +++ b/plugins/claude-ops/skills/lanes/context/config.md @@ -35,7 +35,7 @@ A missing config exits `4`; malformed JSON or a config with no lanes exits `3`. | `lanes[].name` | yes | The lane's session name — the `--name` value the launcher gives the background session, and the key `status`/`stop` match on. Keep distinct from ad-hoc session names. | | `lanes[].prompt` | yes | Path to the lane's canonical prompt file. Relative → resolved against `prompt_dir`; absolute → used as-is. The file's full contents seed the session (positional prompt). A missing or empty file skips that lane with an error. | | `lanes[].model` | no | Passed as `claude --model`. An alias (`opus`, `sonnet`, `fable`) or a full model id. Omit to inherit the machine default. | -| `lanes[].effort` | no | Passed as `claude --effort`. One of `low`, `medium`, `high`, `xhigh`, `max`, `ultracode` (validated; a bad value skips the lane). `ultracode` [requires Claude Code v2.1.203 or later](https://code.claude.com/docs/en/cli-reference#cli-flags); the launcher checks the installed `claude --version` and skips the lane below that floor rather than launching a session whose effort the CLI may not honor. `restart` makes that check before stopping, so a refused lane keeps running. Omit to inherit the default. | +| `lanes[].effort` | no | Passed as `claude --effort`. One of `low`, `medium`, `high`, `xhigh`, `max`, `ultracode` (validated; a bad value skips the lane). `ultracode` [requires Claude Code v2.1.203 or later](https://code.claude.com/docs/en/model-config#adjust-effort-level); below that floor the CLI rejects the value outright (`Unknown --effort value 'ultracode'`) and starts the session at the default effort, so the launcher checks the installed `claude --version` and skips the lane rather than launching it at an unintended effort. `restart` makes that check before stopping, so a refused lane keeps running. Omit to inherit the default. | | `lanes[].settings` | no | A JSON **object** passed inline as `claude --settings` — a session-only override that never persists. The motivating use is opting a lane into the `autonomy` plugin's lane-stop gate via a `pluginConfigs` override (example above; the plugin id is marketplace-qualified, `@`, for however the plugin was installed). A non-object value skips the lane with an error. A gate request (`lane_stop_gate_enabled: true` under an `autonomy` key) additionally triggers launch-time ARMING (#1784): the launcher runs autonomy's `hooks/lane-stop-gate-arm.sh` and injects a random `lane_stop_gate_arm_id` into the launched settings — the trusted per-session channel the gate actually honors (it ignores the bare env mirror a repo `env` block could forge). A gate-requesting lane that cannot be armed (autonomy missing/pre-0.12.0, arming error, managed-settings veto) is skipped with an error rather than launched silently ungated. | Lane names are free-form (`work`, `work-2`, `babysit`, `decide`, …); nothing is diff --git a/plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh b/plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh index 3c1bafd8f..825a8dd97 100755 --- a/plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh +++ b/plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh @@ -124,9 +124,11 @@ set -uo pipefail VALID_EFFORTS="low medium high xhigh max ultracode" # `ultracode` is the one effort upstream gates on a CLI version, so it is the one -# the static allowlist cannot settle on its own. Upstream documents the floor but -# not what an older CLI does with the value, so the launcher refuses rather than -# guessing — restart preflights this BEFORE stopping, keeping a healthy lane up. +# the static allowlist cannot settle on its own. Below the floor the CLI rejects the +# value outright (`Unknown --effort value 'ultracode'`) and starts the session at the +# default effort, so the launcher skips the lane rather than launching it at an +# unintended effort — restart preflights this BEFORE stopping, keeping a healthy lane +# up. https://code.claude.com/docs/en/model-config#adjust-effort-level ULTRACODE_MIN_VERSION="2.1.203" # Compared component by component in awk — deliberately NOT `sort -V`, which is a @@ -146,15 +148,16 @@ version_at_least() { # }' 2>/dev/null } -# Memoized: every lane in a run shares one `claude --version` probe. +# Memoized: every lane in a run shares one `claude --version` probe. Callers read +# CLI_VERSION_CACHE directly — a `$(cli_version)` substitution would populate the +# cache inside a subshell and throw it away, re-probing once per lane. CLI_VERSION_CACHE="" cli_version() { - if [[ -z "$CLI_VERSION_CACHE" ]]; then - CLI_VERSION_CACHE="$(claude --version 2>/dev/null | tr -d '\r' | - grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" - [[ -z "$CLI_VERSION_CACHE" ]] && CLI_VERSION_CACHE="unknown" - fi - printf '%s' "$CLI_VERSION_CACHE" + [[ -n "$CLI_VERSION_CACHE" ]] && return 0 + CLI_VERSION_CACHE="$(claude --version 2>/dev/null | tr -d '\r' | + grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" + [[ -n "$CLI_VERSION_CACHE" ]] || CLI_VERSION_CACHE="unknown" + return 0 } ACTION="start" @@ -686,11 +689,19 @@ validate_launch_inputs() { return 1 fi if [[ "$effort" == "ultracode" ]]; then - local v - v="$(cli_version)" - if ! version_at_least "$v" "$ULTRACODE_MIN_VERSION"; then - err "lane '$name': effort 'ultracode' needs Claude Code >= $ULTRACODE_MIN_VERSION (installed: $v) — skipped" - return 1 + # The same exemption require_claude documents: a dry run must preview with no + # CLI installed. With no binary to probe the gate cannot be evaluated, so the + # preview says so rather than refusing a lane a real run may well launch. Keyed + # on the binary's absence, not on an "unknown" version — a CLI whose --version + # is unparseable is installed, and stays refused. + if ((DRY_RUN)) && ! command -v claude >/dev/null 2>&1; then + info " lane '$name': effort 'ultracode' version gate not evaluated (no claude CLI to probe)" + else + cli_version + if ! version_at_least "$CLI_VERSION_CACHE" "$ULTRACODE_MIN_VERSION"; then + err "lane '$name': effort 'ultracode' needs Claude Code >= $ULTRACODE_MIN_VERSION (installed: $CLI_VERSION_CACHE) — skipped" + return 1 + fi fi fi # `settings` must be a JSON object — the launcher passes it verbatim to diff --git a/plugins/claude-ops/skills/lanes/scripts/lane-launcher.test.sh b/plugins/claude-ops/skills/lanes/scripts/lane-launcher.test.sh index 79ed09bff..16398b55c 100755 --- a/plugins/claude-ops/skills/lanes/scripts/lane-launcher.test.sh +++ b/plugins/claude-ops/skills/lanes/scripts/lane-launcher.test.sh @@ -387,7 +387,7 @@ assert_contains "empty prompt file skipped" "$out" "prompt file is empty" assert_contains "invalid effort skipped" "$out" "invalid effort 'turbo'" assert_not_contains "no launch for bad lanes" "$out" "claude --bg -n baddy" assert_contains "ultracode effort accepted" "$out" "claude --bg -n ultra" -assert_contains "ultracode passed through as --effort" "$out" "--effort ultracode" +assert_contains "ultracode passed through as --effort" "$out" "claude --bg -n ultra --effort ultracode" # ============================================================================ # ultracode version gate — below the floor the lane is skipped, and a restart @@ -405,11 +405,42 @@ assert_not_contains "no launch below the floor" "$out" "claude --bg -n work" out="$(STUB_CLAUDE_VERSION=2.1.203 run_launcher start --repo "$REPO" --config "$TMP/ultra.json" --agents-json "$AGENTS_EMPTY" --dry-run 2>&1)" assert_contains "ultracode at the floor launches" "$out" "claude --bg -n work" -# The running lane must survive a restart the version gate refuses. +# The running lane must survive a restart the version gate refuses. NOT --dry-run: +# `run` short-circuits under it, so the stop could never reach the stub and the +# assertion below would hold no matter when the launcher stopped the lane. : >"$CLAUDE_LOG" -out="$(STUB_CLAUDE_VERSION=2.1.202 run_launcher restart --repo "$REPO" --config "$TMP/ultra.json" --agents-json "$AGENTS_RUNNING" --dry-run 2>&1)" +out="$(STUB_CLAUDE_VERSION=2.1.202 run_launcher restart --repo "$REPO" --config "$TMP/ultra.json" --agents-json "$AGENTS_RUNNING" 2>&1)" assert_contains "restart refused below the floor" "$out" "needs Claude Code >= 2.1.203" -assert_not_contains "healthy lane not stopped by a refused restart" "$(cat "$CLAUDE_LOG")" "stop" +assert_not_contains "healthy lane not stopped by a refused restart" "$(cat "$CLAUDE_LOG")" "stop sid-work-1" + +# Every lane in a run shares ONE `claude --version` probe. Callers read the cache +# global directly; a `$(cli_version)` substitution would fill it in a subshell and +# re-probe once per lane, so this counts the probes rather than trusting the shape. +cat >"$TMP/ultra3.json" <<'JSON' +{ "prompt_dir": ".work", + "lanes": [ { "name": "u1", "prompt": "work.md", "effort": "ultracode" }, + { "name": "u2", "prompt": "work.md", "effort": "ultracode" }, + { "name": "u3", "prompt": "work.md", "effort": "ultracode" } ] } +JSON +: >"$CLAUDE_LOG" +out="$(run_launcher start --repo "$REPO" --config "$TMP/ultra3.json" --agents-json "$AGENTS_EMPTY" --dry-run 2>&1)" +assert_contains "every ultracode lane launches" "$out" "claude --bg -n u3 --effort ultracode" +assert_eq "version probe memoized across lanes" 1 "$(grep -c -- '--version' "$CLAUDE_LOG")" + +# A dry run must preview with no `claude` installed — the exemption require_claude +# documents. The ultracode gate has no binary to probe there, so it reports the gate +# unevaluated and still previews the lane instead of refusing it. +NOCLAUDE_BIN="$TMP/bin-noclaude" +mkdir -p "$NOCLAUDE_BIN" +cp "$STUB_BIN/git" "$NOCLAUDE_BIN/git" +NOCLAUDE_PATH="$NOCLAUDE_BIN:$(dirname "$(command -v jq)"):/usr/bin:/bin" +assert_eq "no-claude PATH really carries no claude" "" "$(PATH="$NOCLAUDE_PATH" bash -c 'command -v claude' || true)" +out="$( + export PATH="$NOCLAUDE_PATH" + run_launcher start --repo "$REPO" --config "$TMP/ultra.json" --agents-json "$AGENTS_EMPTY" --dry-run 2>&1 +)" +assert_contains "no-CLI dry run reports the gate unevaluated" "$out" "version gate not evaluated" +assert_contains "no-CLI dry run still previews the lane" "$out" "claude --bg -n work --effort ultracode" # ============================================================================ # Medium 1 — an option must not swallow the next flag as its value From 22e7a001e92c2ef5291bbb37a3c76d69dec856bb Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:27:33 -0400 Subject: [PATCH 4/4] fix(claude-ops): spell unparsable the way the typos gate expects Co-authored-by: Claude Opus 5 --- plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh b/plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh index 825a8dd97..184555ec6 100755 --- a/plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh +++ b/plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh @@ -693,7 +693,7 @@ validate_launch_inputs() { # CLI installed. With no binary to probe the gate cannot be evaluated, so the # preview says so rather than refusing a lane a real run may well launch. Keyed # on the binary's absence, not on an "unknown" version — a CLI whose --version - # is unparseable is installed, and stays refused. + # is unparsable is installed, and stays refused. if ((DRY_RUN)) && ! command -v claude >/dev/null 2>&1; then info " lane '$name': effort 'ultracode' version gate not evaluated (no claude CLI to probe)" else