feat(claude-ops): lanes skill — scripted background loop-lane launcher (#508)#616
Conversation
…ncher (#508) Add a `lanes` skill that starts, restarts, stops, and reports loop lanes as named background Claude Code sessions seeded from canonical prompt files, replacing the manual morning refresh (cancel loop, clear, re-paste the canonical prompt) across N lanes. start (default) / restart first `git pull --ff-only` and `claude plugin marketplace update`, then launch each configured lane with `claude --bg -n <lane>` mirroring the lane's model/effort; status prints a per-lane running/stopped table with the live sessionId; stop ends a lane via `claude stop <sessionId>` (resolved from `claude agents --json` — there is no `claude agents stop` verb). Acts on a session only when its name is a configured lane, so a hand-started session is never touched. Lanes come from a JSON config (--config, else $CLAUDE_OPS_LANES_CONFIG, else <repo>/.work/lanes.json). Prompt files are read from a session-local `.work` dir today via a single prompt_dir/resolve_prompt_dir seam that composes with issue #480 (loop-prompt authoring skill) when durable prompt storage lands. Bumps claude-ops 0.14.0 -> 0.15.0 (minor: new feature). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011V31qpAHP3jfs76B9d5Rfo
Add a PATH-stubbed (claude + git) non-dry-run `start` case asserting the launcher actually shells out: git pull, marketplace update, and `claude --bg` seeded with the prompt-file body as a single trailing argument — closing the gap left by the dry-run command-construction tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011V31qpAHP3jfs76B9d5Rfo
|
Claude finished @kyle-sexton's task in 3m 7s —— View job Code Review
Overall this is a well-constructed skill. The CLI surface was verified empirically, the argument sanitisation is solid (array-based Medium — Flag squashing in option parsingFile: --config)
CONFIG="${2:-}"
shift
;;
--config)
[[ -n "${2:-}" && "${2}" != -* ]] || { err "--config requires a non-option argument"; exit 3; }
CONFIG="$2"; shift
;;Medium —
|
…I green) The skill-quality CI lane runs on a runner with no `claude` installed. The test suite's non-dry, non-stubbed status/stop cases tripped the launcher's `require_claude` gate (exit 4), failing the gate. - Test: resolve `claude`/`git` to the logging stubs for the whole suite so it never depends on an ambient `claude` (verified passing with `claude` absent from PATH). - Script: `status --agents-json` reads a fixture and makes no `claude` call, so it no longer requires the CLI — honoring the flag's documented offline use. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011V31qpAHP3jfs76B9d5Rfo
|
Claude finished @kyle-sexton's task in 2m 59s —— View job Code Review
What the new commits fixed
Both changes are correct and the reasoning in the commit message is accurate. Still open from the previous reviewMedium — Flag squashing in option parsing--config)
CONFIG="${2:-}"
shift
;;
Medium —
|
Two CI hygiene/plugin-gate failures on the lanes skill: - Root README.md mirrors each plugin.json description verbatim (enforced by scripts/validate-plugins.sh); update the claude-ops entry to Seven skills including lanes. - The lanes scripts carry a shebang but were committed 100644; set the git index mode to 100755 (executable), matching every sibling skill script. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011V31qpAHP3jfs76B9d5Rfo
|
Claude finished @kyle-sexton's task in 2m 2s —— View job Code Review
What
|
The comment-hygiene gate forbids tracker (issue-number) references in code comments (tracker-ref:issue-reference). Reword the prompt-storage seam comments to describe the forthcoming loop-prompt authoring skill without the issue number; the tracked #480 dependency stays cited in the skill's SKILL.md and context/config.md (markdown is not scanned by that gate). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011V31qpAHP3jfs76B9d5Rfo
|
Claude finished @kyle-sexton's task in 1m 40s —— View job Code Review
What
|
…ailure propagation
Resolve three MEDIUM review findings plus the low-priority carry-forwards:
- M1: option handlers (--config/--repo/--agents-json) now reject consuming a
token that looks like another flag (check_optarg), so `--config --dry-run` no
longer silently swallows --dry-run as the config path.
- M2: verified `startedAt` is genuinely present in real `claude agents --json`
(all sessions) and documented it in the verified-surface comment; the
most-recent-wins sort_by is correct, not a silent no-op.
- M3: per-lane callback failures now accumulate in for_each_lane and propagate
through action_start/action_restart to a non-zero script exit, so a lane that
fails to (re)launch is visible in $? — the sweep still completes all lanes.
Low-priority (all fixed): stop_lane_if_running propagates `claude stop`'s exit
code (a failed stop refuses to relaunch, avoiding a duplicate-named session);
replaced the `${TARGET_LANES:+…}` element-0 idiom with a `${#TARGET_LANES[@]}`
length check (lane_scope); validated the --agents-json fixture once in main —
its old lazy guard sat on the left of a pipe (subshell) and could never exit
the script; added tests for flag-squash rejection, partial-failure exit codes,
stop-failure paths, missing --agents-json file, and `--` end-of-options.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011V31qpAHP3jfs76B9d5Rfo
|
Claude finished @kyle-sexton's task in 2m 26s —— View job Code Review
Medium 1 — Flag squashing ✅ Fixed
A Three new test cases at L208–217 verify each option rejects a following flag with exit 3. Medium 2 —
|
Review findings — classification & resolutionAll three MEDIUM findings and every low-priority carry-forward are fixed in the latest commit (
Verification: |
|
Claude finished @kyle-sexton's task in 2m 42s —— View job Code Review
Four prior review rounds identified and addressed every medium and low-priority finding. This pass audits the final state of HEAD ( CLAUDE.md Design Rules
Correctness — Final StateAll prior-round fixes verified:
Test Coverage (55 cases)The suite is hermetic:
Minor Documentation Gap (non-blocking)
What's Solid
AssessmentAll medium and low-priority findings from four prior review rounds are resolved. The one item above is a prose-only doc gap in SKILL.md and does not block merge. This PR is ready. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f889c94d22
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
`claude agents --json` failures were coerced to `[]` via `|| echo '[]'`, so every configured lane looked stopped: a real `start` would relaunch still-live lanes (duplicate sessions) and `status` would report false "stopped" state. Load the session list once in the main shell (not lazily inside a `$(...)`/pipe subshell, where an `exit` cannot set the script's status) and abort with exit 4 on a genuine live-list failure; `--dry-run` still tolerates it (mutates nothing) to preserve offline-dry-run behaviour. Also documents the load-bearing invariant that `claude agents --json` (no `--all`) already lists active sessions only, so the name-match lookup never sees terminal sessions — do not add `--all` without filtering them. Addresses the Codex P1 "Propagate failures when listing background sessions". The companion P1 "Exclude terminal sessions" is a non-issue on the target CLI (2.1.215): default `--json` excludes completed/terminal background sessions (they require `--all` and carry `state`, not `status`). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 52s —— View job Code Review
What
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3240071f75
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The M2 fix added `startedAt` to the script header's field list but not to SKILL.md's Verified CLI Surface, which still omitted it. Align the doc and note that `claude agents --json` lists active sessions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 29s —— View job Code Review
What
|
| Finding | Status |
|---|---|
M1 — Flag squashing (check_optarg) |
✅ Fixed (f889c94) |
M2 — startedAt field verification |
✅ Fixed + doc gap closed (6449e32) |
| M3 — Partial launch exits 0 | ✅ Fixed (f889c94) |
L1 — claude stop exit code discarded |
✅ Fixed (f889c94) |
L2 — Array length idiom (lane_scope) |
✅ Fixed (f889c94) |
L3 — --agents-json not-found gap |
✅ Fixed (f889c94) |
L3 — -- passthrough gap |
✅ Fixed (f889c94) |
| Codex P1.1 — Live session list failure propagated | ✅ Fixed (3240071) |
| Codex P1.2 — Terminal sessions in lookup | ✅ Documented as non-issue (CLI guarantee) |
SKILL.md startedAt prose gap |
✅ Fixed (6449e32) |
Codex P1 (new) — claude agents --json cwd |
⬜ Non-issue for --json scripting mode; doc note optional |
| Codex P2 (new) — Duplicate lane names | ⬜ Low — documented contract, not enforced |
Assessment
6449e32 is a correct, narrowly targeted documentation fix that resolves the last open item from prior review rounds.
The two new Codex findings on the current HEAD: the cwd concern (P1) does not apply to the --json scripting path — no code change required. The duplicate-name concern (P2) is real but low-priority given the documented schema contract and the operator audience.
This PR is ready to merge. The two optional items above (cwd doc note, duplicate-name validation) are improvements worth considering but not blockers.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6449e32506
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Lane names are the safety key — stop/restart resolve a sessionId by name against a single per-action snapshot, so a duplicated name would launch two same-named background sessions while stop reaches only the most-recent one, contradicting the documented distinct-names requirement. Validate uniqueness in resolve_config and fail closed (exit 3) instead of acting on an ambiguous lane set. Addresses the Codex P2 "Reject duplicate lane names during config validation". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 3s —— View job Code Review
What
|
| Finding | Status |
|---|---|
M1 — Flag squashing (check_optarg) |
✅ Fixed (f889c94) |
M2 — startedAt field verification + SKILL.md doc |
✅ Fixed (f889c94 + 6449e32) |
| M3 — Partial launch exits 0 | ✅ Fixed (f889c94) |
L1 — claude stop exit code discarded |
✅ Fixed (f889c94) |
L2 — Array length idiom (lane_scope) |
✅ Fixed (f889c94) |
L3 — --agents-json not-found + -- passthrough gaps |
✅ Fixed (f889c94) |
| Codex P1.1 — Live session list failure propagated | ✅ Fixed (3240071) |
Codex P1.2 — Terminal sessions (cwd / --json) |
✅ Non-issue for --json scripting mode; documented |
| Codex P2 — Duplicate lane names | ✅ Fixed (fd62819) |
| Codex P1 (latest) — interactive-session kind filter | ⬜ Open — see above |
What's solid
The fd62819 commit is correct and narrowly targeted. The jq uniqueness expression is right, the comment explains the invariant clearly, and the test exercises the rejection path with an assertable error message.
The one remaining item is the kind filter. Given that the SKILL.md Purpose section makes the "never stops a hand-started interactive session" guarantee explicitly and it's testable with the field already in the verified surface, this is worth closing before merge.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fd628190aa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
running_session_id matched by name alone, so an interactive window sharing a lane name (e.g. a hand-started `work`) was treated as the lane: `stop` would hand that interactive session to `claude stop`, `start` would skip launching the real lane, and `status` would report it running. That contradicts the skill's stated guarantee that an interactive `work` window is never stopped (and the two SKILL.md notes disagreed with each other). Restrict the lookup to `kind == "background"`. Lanes are always launched with `--bg`, so a real lane is by construction a background session — this can only exclude non-lane sessions, never a live lane, so it cannot cause a duplicate launch. Reconcile the SKILL.md "Name is the identity" note accordingly. Addresses the Codex P1 "Restrict lane matches to background sessions". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 29s —— View job Code Review
What
|
| Case | What it verifies |
|---|---|
| "interactive same-name is not a running lane" | status reports stopped for the lane |
| "interactive same-name sessionId not shown" | The interactive sid-int-work never appears in status output |
| "start launches lane despite interactive namesake" | start still emits claude --bg -n work |
| "start does not skip work for interactive namesake" | start does not print skip work |
| "stop treats interactive namesake as not running" | stop prints work — not running |
| "stop never targets the interactive namesake" | claude log contains no sid-int-work |
All 6 test the correct behavior end-to-end.
SKILL.md — The "Name is the identity" Gotcha at L93–96 is updated to reflect the kind filter: "Lanes are matched by session name and kind: background". The note about hand-started interactive windows and background sessions with conflicting names is now accurate. ✅
Test count advances from 62 → 68. All cases pass hermetically via the PATH-stubbed claude/git.
Remaining open Codex findings from the prior round
These two findings landed on commit fd628190 and are not addressed in 68d53b3.
Low — Refresh failure does not abort lane launch
action_start() {
local rc=0
info "== lanes: start =="
refresh_repo_and_plugins || rc=1 # sets rc=1, but does NOT abort
info "lanes:"
for_each_lane _start_one || rc=1 # runs regardless
return "$rc"
}When git pull --ff-only or claude plugin marketplace update fails, rc is set to 1 but the sweep continues. Lanes are launched from stale state; the script exits 1 at the end. For a tool whose documented contract is "refresh then launch," silently proceeding on a dirty or diverged repo may be surprising — particularly if the user runs restart expecting a clean re-seed. The --no-pull/--no-update escape hatches exist for intentional bypass. The fix is a guard: refresh_repo_and_plugins || return 1 (or exit 4). Low severity given the operator audience and the fact that the failure is at least surfaced via exit code.
Low — Target validation runs after refresh mutations
action_restart() {
local rc=0
info "== lanes: restart$(lane_scope) =="
refresh_repo_and_plugins || rc=1 # git pull + marketplace update run FIRST
info "lanes:"
for_each_lane _restart_one || rc=1 # unknown-target check is here (L393-401)
return "$rc"
}restart does-not-exist runs git pull --ff-only and claude plugin marketplace update before for_each_lane discovers the target is unknown and exits 3. The refresh steps are idempotent and harmless, but the DX is inconsistent — stop does-not-exist also validates late, after nothing has happened. A pre-validation pass on TARGET_LANES before refresh_repo_and_plugins would make the error immediate and avoid mutations for a clearly invalid invocation.
Status of all findings
| Finding | Status |
|---|---|
M1 — Flag squashing (check_optarg) |
✅ Fixed (f889c94) |
M2 — startedAt field + SKILL.md doc |
✅ Fixed (f889c94 + 6449e32) |
| M3 — Partial launch exits 0 | ✅ Fixed (f889c94) |
L1 — claude stop exit code discarded |
✅ Fixed (f889c94) |
L2 — Array length idiom (lane_scope) |
✅ Fixed (f889c94) |
L3 — --agents-json not-found / -- passthrough gaps |
✅ Fixed (f889c94) |
| Codex P1.1 — Live session list failure propagated | ✅ Fixed (3240071) |
Codex P1.2 — Terminal sessions in --json |
✅ Non-issue (CLI guarantee documented) |
| Codex P2 — Duplicate lane names | ✅ Fixed (fd62819) |
| Codex P1 — Interactive-session kind filter | ✅ Fixed (68d53b3) |
| Codex P2 — Refresh failure continues to launch | ⬜ Open — low severity |
| Codex P2 — Target validation after refresh | ⬜ Open — low severity |
What's solid
The kind-filter change is the correct and necessary fix for the stated invariant ("a hand-started interactive work window is never stopped by this skill"). The implementation is tight: one added clause in the jq filter, a comment that explains both what the guard does and why it cannot create false negatives, SKILL.md Gotchas updated to match, and 6 targeted test cases. No risk of over-excluding real lanes since all real lanes are by construction kind: background.
The two remaining open items are low severity and represent Codex's final P2 suggestions. This PR is ready to merge on the current evidence. The two open items are worth a follow-up but are not blockers for an operator-only skill with a documented escape hatch (--no-pull/--no-update).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 68d53b38c1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
restart stopped a healthy running lane before launch_lane validated the prompt file and effort, so a deleted/empty prompt or invalid effort took the session down and then failed to relaunch it — leaving the lane down for a recoverable error. Split the prompt/effort checks out of launch_lane into validate_launch_inputs and call it in _restart_one before stop_lane_if_running, so a recoverable input error aborts the restart without touching the live session. launch_lane still re-validates, so the start path is unchanged. New regression test: restarting a running lane with a missing prompt exits non-zero, surfaces the error, and never issues `claude stop` against the live session. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 42s —— View job Code Review
What
|
| Finding | Status |
|---|---|
M1 — Flag squashing (check_optarg) |
✅ Fixed (f889c94) |
M2 — startedAt field + SKILL.md doc |
✅ Fixed (f889c94 + 6449e32) |
| M3 — Partial launch exits 0 | ✅ Fixed (f889c94) |
L1 — claude stop exit code discarded |
✅ Fixed (f889c94) |
L2 — Array length idiom (lane_scope) |
✅ Fixed (f889c94) |
L3 — --agents-json not-found + -- passthrough gaps |
✅ Fixed (f889c94) |
| Codex P1.1 — Live session list failure propagated | ✅ Fixed (3240071) |
Codex P1.2 — Terminal sessions in --json |
✅ Non-issue (CLI guarantee documented) |
| Codex P2 — Duplicate lane names | ✅ Fixed (fd62819) |
| Codex P1 — Interactive-session kind filter | ✅ Fixed (68d53b3) |
| Codex P1 — Preflight before stop in restart | ✅ Fixed (18c3465) |
| Codex P2-a — Refresh failure continues to launch | ⬜ Open — low severity |
| Codex P2-b — Target validation after refresh | ⬜ Open — low severity |
Carry-forward low-priority items (unchanged)
Both were assessed as non-blocking in the prior review round.
Codex P2-a — Refresh failure does not abort lane launch (lane-launcher.sh L488–495):
refresh_repo_and_plugins || rc=1 sets rc but the lane sweep always proceeds. A diverged or dirty checkout will still launch lanes from stale state. The --no-pull/--no-update escape hatches allow intentional bypass. Low severity because the failure is surfaced via exit code and the operator audience is expected to check it.
Codex P2-b — Target validation runs after refresh (lane-launcher.sh L497–504):
restart does-not-exist runs git pull --ff-only and claude plugin marketplace update before for_each_lane discovers the target is unknown and exits 3. Mutations happen before the error. Low severity — the mutations are idempotent.
Test count
68 → 71 cases (3 new assertions in the Codex P1 preflight section, verifying exit status, error message, and stop suppression).
Assessment
18c3465 is correct and well-tested. The Codex P1 preflight finding is cleanly resolved: validate_launch_inputs is extracted into a reusable function, called before stop_lane_if_running in _restart_one, and the test proves the stop is suppressed when inputs fail. No new issues are introduced.
All medium findings from the review series are resolved. The two remaining low-priority items are unchanged and non-blocking for an operator-only skill with documented escape hatches. This PR is ready to merge.
…lidate targets first (#707) ## Summary Two deferred low-severity follow-ups from PR #616's review of the `claude-ops` `lanes` skill (`lane-launcher.sh`). Both hardened the pre-launch refresh step so lanes never start from state the user did not sign off on. ## Fix **Item 1 — abort the launch on a failed refresh.** `action_start` / `action_restart` ran `refresh_repo_and_plugins || rc=1` and proceeded to `for_each_lane` regardless, so a failed `git pull --ff-only` (divergent/dirty checkout) or `claude plugin marketplace update` still seeded background lanes from stale repo/plugin state, surfacing non-zero only at the very end. The refresh is a documented `start`/`restart` prerequisite, so an unexpected failure now hard-stops the launch (`refresh_repo_and_plugins || return 1`) with an actionable message emitted from `refresh_repo_and_plugins`. `--no-pull` / `--no-update` remain the intentional-skip path: a skipped step leaves the refresh status `0`, so the abort keys on real failure only, never on an intentional bypass. **Item 2 — validate targets before refresh mutations.** `restart does-not-exist` ran the full refresh (`git pull` + marketplace update) before `for_each_lane` discovered the target was unknown and exited 3 — inconsistent with `stop does-not-exist`, which validates before anything happens. The `TARGET_LANES` existence check moved out of `for_each_lane` into a new `validate_target_lanes` function called from `main` ahead of action dispatch, so a misspelled target fails fast (exit 3) with no repo/plugin mutation. Single source of truth: `for_each_lane`'s stale "unknown target names are an error" comment was updated to point at the new owner. ## Verification `lane-launcher.test.sh` grew from 71 to 85 cases (+14) covering both fixes: the refresh-failure abort (pull failure and marketplace-update failure, for both `start` and `restart`, plus the `--no-pull`/`--no-update` bypass still launching) and the pre-refresh target rejection (no `pull`/`update` in the command log). New stub knobs `STUB_GIT_PULL_RC` and `STUB_CLAUDE_UPDATE_RC` drive the failure paths. ``` $ bash plugins/claude-ops/skills/lanes/scripts/lane-launcher.test.sh ... PASS: [85] restart unknown lane does not update the marketplace lane-launcher.test: PASS — 85 cases $ shellcheck plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh plugins/claude-ops/skills/lanes/scripts/lane-launcher.test.sh shellcheck: clean $ bash -n plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh # syntax OK $ jq -r '.version' plugins/claude-ops/.claude-plugin/plugin.json # 0.15.1 ``` Closes #639 ## Related - #639 — this issue (both items) - #616 — the lanes-skill PR whose review deferred these follow-ups - #508 — parent tracking issue --------- Co-authored-by: Claude <noreply@anthropic.com>
…uction (#864) (#877) ## What Evaluate the lanes skill's deterministic, read-only git probes against the pinned precompute convention (`plugins/playbooks/skills/skill-authoring/reference/precompute-context.md`) and either convert each to `!` dynamic-context injection or record why it fails the four conditions. ## Per-probe verdict | Git probe | Location | Verdict | |---|---|---| | `git rev-parse --show-toplevel` (repo root) | `SKILL.md` front matter | **Already converted** — `!` injection with `\|\| echo` fallback; `shell: bash` declared. | | `git rev-parse --show-toplevel` (lane-config resolution) | `SKILL.md` front matter | **Already converted** — `!` injection with `\|\| echo` fallback. | | staleness probe: `git fetch origin` + `git symbolic-ref` + `git log <lane-launch-commit>..<default> -- <plugin-path>` | `context/refresh.md` | **Recorded reason it fails all four conditions** — stays a body instruction. | The two front-matter probes were `!`-injected at the skill's creation (#616) and gained `shell: bash` in 0.17.2 — already compliant, nothing to convert. ## Why the staleness probe is not an injection candidate It fails **every** convention condition: - **Not deterministic/read-only + not cheap/bounded** — `git fetch` is a network round-trip (latency-variable, can hang) that also updates remote-tracking refs. - **Not needed up front every invocation** — consulted only when weighing a restart, not on a plain `status`/`stop`/`start`. - **Not judgment-independent** — needs a computed `<lane-launch-commit>` argument (the running lane's launch point) plus the pathspec of whichever plugin the lane runs; a single-pass injection cannot supply either. Recorded inline in `context/refresh.md` so a future reader does not re-litigate it. ## Automated gate skill-quality Check 18 (precompute opportunity) scans only `SKILL.md` fenced shell blocks and is silent when the skill already injects with `!` — lanes does — so the gate was already satisfied; this PR adds no new `!` blocks. ## Changes - `context/refresh.md` — durable "Not an `!` injection candidate" note next to the staleness probe. - `claude-ops` version `0.17.2` → `0.17.3` + matching CHANGELOG entry (changelog-parity gate). ## Verification - `check-skill.sh lanes` — PASS (0 errors, 0 warnings; markdownlint clean, 163/500 lines) - `check-changelog-parity.sh --check` and `--check-bump origin/main` — PASS - `validate-plugins.sh` + `claude plugin validate .` — PASS - markdownlint on changed files — 0 errors Closes #864 ## Related - #791 — origin of the side-finding - #616 — original `!` conversion of the front-matter probes - #860 — shell-declaration fleet sweep that added `shell: bash` Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
Adds a
lanesskill toclaude-ops: a scripted launcher that starts, restarts, stops, and reports loop lanes as named background Claude Code sessions seeded from canonical prompt files. It replaces the manual morning refresh — for each lane: cancel the loop, clear, re-paste its canonical prompt — with one command across N lanes.start(default) /restartfirstgit pull --ff-onlyandclaude plugin marketplace update, then launch each configured lane withclaude --bg -n <lane>mirroring that lane'smodel/effort.statusprints a per-lane running/stopped table with the live sessionId.stopends a lane viaclaude stop <sessionId>.PR Babysit) is never touched.Implementation
plugins/claude-ops/skills/lanes/— mirrors the sibling skill shape (SKILL.md+scripts/*.sh+scripts/*.test.sh+context/+evals/).disable-model-invocation: true(mutating, operator-initiated).scripts/lane-launcher.sh— action router (start/restart/status/stop),set -uo pipefail, jq-driven config parse,--dry-run/--no-pull/--no-update/--config/--repo/--agents-jsonflags, exit codes0/3/4.context/config.mdcarries the full contract): JSON resolved first-hit-wins--config→$CLAUDE_OPS_LANES_CONFIG→<repo>/.work/lanes.json. Each lane:name,promptpath, optionalmodel/effort. Lane names are not hardcoded — the set is config-driven, matching howmorning-brieftreats loop lanes ([a-z0-9_-]+, dynamic)..workdir (prompt_dir, default.work) — provisional. The launcher resolves the prompt dir in exactly one place (resolve_prompt_dir); a TODO documents that feat(claude-ops or session-flow): loop-prompt authoring skill — generate and maintain standing loop-lane prompts #480 (loop-prompt authoring skill) will own the durable home, at which point only that seam is repointed. This PR deliberately does not build durable storage.claude-ops0.14.0 → 0.15.0 (minor — new feature), re-read from liveorigin/main(still0.14.0at PR time). CHANGELOG top entry; plugin.json/README skill countSix → Sevenwith the new skill's line.Verification
The issue's phrasing was verified, not trusted. #508 wrote
claude --bg --name "<lane>"andclaude agentsas if final. I confirmed the real CLI surface on this machine (claude 2.1.215) by--helpand a real throwaway session:claude --help(relevant flags, quoted):--bg, --background Start the session as a background agent and return immediately (manage with 'claude agents');-n, --name <name> Set a display name for this session;--model <model>;--effort <level> Effort level ... (low, medium, high, xhigh, max).claude agents --json Print active sessions ... as a JSON array (for scripting; does not require a TTY).Real launch → name → list → stop cycle (this is what corrected the issue — there is no
claude agents stopverb; stop isclaude stop <id>):claude stop --help:Usage: claude stop <id> Stop a background session. Its conversation is kept; resume it later with 'claude attach <id>'.The launcher itself (real dry-run + live status against
claude agents --json):Gates (all green):
lane-launcher.test.sh— 40 cases PASS, including a PATH-stubbed (claude + git) non-dry-runstartcase proving the real dispatch path (git pull, marketplace update, andclaude --bgseeded with the prompt-file body as a single trailing arg).skill-qualitystatic contract gate:CHECK-SKILL lanes: PASS — 0 errors, 0 warning(s)(frontmatter, <500-line cap, markdownlint, evals present, script test).shellcheckclean;shfmtclean (2-space per repo.editorconfig);markdownlint-cli2clean;claude plugin validate plugins/claude-ops→Validation passed.origin/main: no open PR touchesplugins/claude-ops/.claude-plugin/plugin.json;origin/mainstill0.14.0→0.15.0free.Closes #508
Related
.workand leaves the singleresolve_prompt_dirseam for when feat(claude-ops or session-flow): loop-prompt authoring skill — generate and maintain standing loop-lane prompts #480 lands.morning-briefreads it, this skill starts/stops the lanes.🤖 Generated with a Claude Code implementation subagent (issue #508)