melodic-software

claude-code-plugins

"Start, restart, stop, and check loop lanes as named background Claude Code sessions seeded from canonical prompt files — the scripted replacement for the manual morning refresh (cancel loop, clear, re-paste the canonical prompt) across N lanes on a machine. `start`/`restart` first pull the repo and refresh the plugin marketplace, then launch each configured lane with its per-lane model/effort. `consume-restarts` reads each configured lane's telemetry `restart_request` and relaunches the stopped lanes that asked — the scheduled headless reader (#1653). Use when: 'launch my lanes', 'restart the loop lanes', 'start the work lanes', 'morning lane refresh', 'stop a lane', 'which lanes are running', 'lane status', 'consume restart requests', 'lane restart consumer', 'relaunch the lanes that asked'. Mutating and operator-initiated; never touches a session whose name is not a configured lane."

melodic-software 12 Updated 4w ago

Resources

3
GitHub

Install

npx skillscat add melodic-software/claude-code-plugins/plugins-claude-ops-skills-lanes

Install via the SkillsCat registry.

SKILL.md

Pre-computed context

claude CLI: !claude --version 2>/dev/null || echo "MISSING (required)"
jq: !command -v jq >/dev/null 2>&1 && echo "present" || echo "MISSING (required)"
Repo root: !git rev-parse --show-toplevel 2>/dev/null || echo "unknown (pass --repo)"
Lane config: !bash "${CLAUDE_PLUGIN_ROOT}/skills/lanes/scripts/probe-lane-config.sh" 2>/dev/null || echo "unknown"

Variables

Arguments: $ARGUMENTS

Purpose

Running N loop lanes on a machine means a daily ritual: for each lane, cancel its
loop, clear, and re-paste its canonical prompt. This skill collapses that to one
command. start/restart pull the repo and refresh the plugin marketplace once,
then launch each configured lane as a named background session seeded from the
lane's canonical prompt file, mirroring that lane's model/effort onto the launch.
status/stop read and manage those sessions through the CLI's own
background-session surface.

Owns only its own lanes. stop/restart act on a session only when its
name is a lane in the resolved config — a hand-started session (e.g. an interactive
work window, or an unrelated PR Babysit) is never stopped by this skill.

A relaunch is the only context reset a loop lane gets

/loop re-invokes its prompt in the same session (it self-paces via
ScheduleWakeup), so a lane's context carries forward across every cycle, subagent
return, and operator turn — the loop never starts a fresh one. A running loop
cannot reset its own context on demand: a built-in like /clear issued from a
loop re-run reaches the model as plain text, not an executed command, so the model
cannot /clear itself. Any "restart the loop when context passes ~N%" discipline is
therefore an admonition with no in-session enforcement — the lane has no reliable way
to measure its own context usage and no way to act on the threshold if it could.

The context change that does happen automatically in-session is Claude Code's
auto-compaction: when the conversation nears the model's input limit, older
history is summarized in place to free space. That is not a reset — the session
continues on a lossy summary of what came before, not a fresh context — so a lane that
runs long enough will lose earlier context to compaction well before any operator
relaunch, not keep every turn until then.

restart (stop the session, relaunch from the canonical prompt) is the only
fresh-session reset: the relaunched lane starts a fresh session seeded from the
prompt file, carrying none of the prior conversation. That reset is operator- or
launcher-initiated
, not automatic — there is no per-cycle or threshold trigger that
runs restart for you today. Auto-compaction can still fire between restarts, but it
only summarizes; it does not clear the accumulated, increasingly degraded context.
Until an automatic relaunch trigger exists, treat the periodic restart (e.g. the
morning refresh) as the mechanism that keeps long-running lanes off a stale,
repeatedly-compacted context, and do not let a lane prompt assume each /loop cycle
begins with fresh context.

Run it

If the first token of $ARGUMENTS is consume-restarts, skip to
Consume restart-requests instead — it runs a
different script. Otherwise:

bash "${CLAUDE_PLUGIN_ROOT}/skills/lanes/scripts/lane-launcher.sh" --data-dir "${CLAUDE_PLUGIN_DATA}" $ARGUMENTS

Print the script's output verbatim — it is the deliverable. Preview any mutating
run first with --dry-run (prints the exact claude/git commands, seeds
nothing, kills nothing).

--data-dir is passed explicitly, not left to the script's own
$CLAUDE_PLUGIN_DATA env-var fallback.
Per
plugins-reference,
${CLAUDE_PLUGIN_DATA} is exported as a real environment variable only to hook
processes and MCP/LSP subprocesses — for skill content it instead resolves by
inline text substitution anywhere the placeholder appears in the rendered
skill body, exactly like ${CLAUDE_PLUGIN_ROOT} above. A script this skill
shells out to via the Bash tool does not inherit CLAUDE_PLUGIN_DATA as an
env var, so leaving --data-dir off here would silently fall through to
lane-launcher.sh's own ~/.claude/plugins/data/claude-ops guess instead of
the marketplace-qualified directory Claude Code actually resolves. $ARGUMENTS
comes after --data-dir, so an explicit --data-dir the caller passes in
$ARGUMENTS still wins (last flag wins in lane-launcher.sh's parser).

Action Router

Parse $ARGUMENTS for the action (first token); remaining tokens are lane names
(targets for restart/stop; an unknown name is rejected).

Action Mutates Description
start (default) Yes Pull + marketplace update, then launch every configured lane not already running
restart [lane...] Yes Pull + marketplace update, then stop-and-relaunch each target lane (all, or named)
status No Per-lane table: model, effort, running/stopped, and the live sessionId
stop [lane...] Yes Stop each running target lane (all, or named) via claude stop <sessionId>
consume-restarts [check|run|print-schedule] run only Read each lane's telemetry restart_request; relaunch stopped lanes that asked (#1653)

Options: --config FILE, --repo DIR, --no-pull, --no-update, --dry-run,
--agents-json FILE (read the session list from a file instead of the live CLI —
offline/scripted reuse), --data-dir DIR (base dir for the per-lane
launch-commit marker; default $CLAUDE_PLUGIN_DATA). Exit codes: 0 ok · 3
bad argument/config · 4 prerequisite missing or repo/config unresolved.

Consume restart-requests (#1653)

A lane that hits its cycle budget or the /loop seven-day expiry writes a
restart_request into its telemetry state block and stops — it cannot relaunch
itself. The consumer is the reader that closes that gap, meant to run unattended
on an OS-owned schedule (its print-schedule action emits the registration
commands; registering them is an operator action). For consume-restarts, run:

bash "${CLAUDE_PLUGIN_ROOT}/skills/lanes/scripts/restart-consumer.sh" --data-dir "${CLAUDE_PLUGIN_DATA}" $ARGUMENTS

The script strips the leading consume-restarts token itself; the next token is
its sub-action — check (read-only report, the default), run (relaunch), or
print-schedule (emit the registration commands) — and later tokens its options.
Print the output verbatim. Relaunches delegate to lane-launcher.sh restart, so
each lane's prompt, model, effort, and settings come from the same config this
skill already uses. The consumer's polling tick is not lane pacing: lanes
stay self-paced via ScheduleWakeup, and a tick where no configured lane has a
non-null restart_request is a no-op that only refreshes the consumer's own
freshness telemetry. Design rationale, the relaunch predicate, operator
registration with Verify/Reversal lines, and the labeled UNVERIFIED items live
in context/restart-consumer.md — read it before
registering the schedule or changing the consumer.

Lane config

Lanes are defined in a JSON config, resolved first-hit-wins:
--config FILE$CLAUDE_OPS_LANES_CONFIG<repo>/.work/lanes.json. Each lane
carries a name, a prompt file path, and optional model/effort/settings
(a session-only claude --settings override — e.g. opting the lane into the
autonomy plugin's lane-stop gate). The full
schema, resolution rules, and the prompt-storage seam live in
context/config.md — read it before authoring a config.

Prompt storage is provisional (composes with #480). Today prompt files live in
a session-local .work dir (prompt_dir, default .work). Issue #480 (loop-prompt
authoring skill) is slated to own durable prompt storage. When it lands, repoint
prompt_dir at that home; the launcher resolves the prompt dir in exactly one place
(resolve_prompt_dir in the script), which is the single seam to update.

Mid-session staleness & restart cadence

A running lane keeps the skill versions it loaded at launch: a fix merged to a
plugin the lane runs does not reach that lane mid-session. This is not a missing
feature to build around — it is verified Claude Code behavior (a live session keeps
its launch-time plugin versions, /loop never re-reads a skill's body on later
cycles, and a loop can't self-trigger /reload-plugins). Restart is the honest
refresh mechanism — the same restart that clears context bloat (#496). Detect an
unconsumed self-fix with a read-only git probe against the repo's default branch,
then restart that lane at its next cycle boundary. The probe reads the launch
commit lane-launcher.sh records per lane at start/restart
(${CLAUDE_PLUGIN_DATA}/lanes/<repo-key>/<lane>-launch-commit, #792 — the data
directory is plugin-wide, so <repo-key>, a digest of the repo's canonical
path, keeps a conventional work lane in two different checkouts from sharing
one marker) — no manual fill-in needed.
Full reasoning, the probe, and the cadence live in
context/refresh.md — read it before answering "why is my
merged fix not live in the lane?" or setting a restart frequency.

Carry this line into that probe — it is the data_dir assignment
context/refresh.md deliberately leaves unresolved, because only skill content
(this file) substitutes the placeholder:

data_dir="${CLAUDE_PLUGIN_DATA}"

Copy it as it renders here, already substituted to an absolute path. Writing
the placeholder — or a ${CLAUDE_PLUGIN_DATA:-…} env fallback — inside
context/refresh.md would not work: that file is read raw, and per
plugins-reference
CLAUDE_PLUGIN_DATA reaches only hook and MCP/LSP subprocesses as a real
environment variable, never a script the Bash tool runs. The probe would then
read the unqualified ~/.claude/plugins/data/claude-ops guess, find no marker,
and skip the staleness check silently.

Verified CLI surface

The launcher shells out only to primitives confirmed on this machine's claude
(--help / real invocation): claude --bg -n <name> [--model M] [--effort E] [--settings JSON] "<prompt>" (launch a named background session, return
immediately; --settings accepts inline JSON and applies session-only, per the
CLI reference),
claude agents --json (list active sessions: pid, cwd, kind, startedAt,
sessionId, name, status),
claude stop <sessionId> (stop one session; conversation kept, resumable with
claude attach), and claude plugin marketplace update. There is no
claude agents stop verb — stop resolves the sessionId from agents --json and
only for a configured lane name.

Gotchas

  • No durable prompt home yet. .work is session-local; a fresh machine/session
    has no prompts until they are authored there (or prompt_dir is pointed at a
    committed dir). This is the #480 dependency, not a bug.
  • Name is the identity. Lanes are matched by session name and kind: background — every lane is launched with --bg, so an interactive window sharing
    a lane name is never matched or stopped. Two lanes must not share a name; a
    hand-started background session sharing a lane name would still be treated as
    that lane, so keep lane names distinct from ad-hoc background session names.
  • start is idempotent-ish, restart is not. start skips a lane already
    running; restart always stops-and-relaunches (discarding the running lane's
    in-flight conversation). Use start for "bring up whatever is down".
  • A missing/empty prompt file skips that lane (with an error) rather than
    launching an empty session. status flags [prompt MISSING].
  • The launch-commit marker is per-machine and best-effort. It lives under
    ${CLAUDE_PLUGIN_DATA} (a per-machine dir, not synced), so a lane restarted
    on a different machine has no marker there yet. A write failure only warns —
    it never fails an already-launched (or already-stopped-and-relaunched) lane —
    so a missing marker means "never started here via lane-launcher.sh", not
    "launcher broken". A (re)start that cannot record its commit also deletes
    any marker the previous launch left, so "missing" always beats a stale commit
    the probe would otherwise trust.
  • A lane name must be a single path component. It is the marker's filename,
    so config preflight exits 3 on a name containing / or \, or equal to .
    or .. — otherwise two distinct lanes could share one marker.

Per-cycle deterministic scripts (#538)

Two lane-cycle mechanics need no reasoning, so they are scripted here and a lane
prompt references the script instead of re-deriving the work every cycle. Both
follow lane-launcher.sh's conventions (jq CRLF wrapper, the --help header as
the full contract, explicit exit codes). The header is the source of truth for
each — the summary below is a pointer, not a copy.

  • scripts/machine-behavior.sh — emits the lane's MACHINE-BEHAVIOR block (gh
    identity, clone path, worktree inventory, installed plugin versions) as a
    verbatim-printable text block. Pure environment inspection: it emits only
    mechanically unambiguous facts and deliberately does NOT compute "deviations
    from standing rules" — that stays a model judgment made by reading these facts
    against the prose rules. Plugin versions are the INSTALLED runtime versions,
    which per context/refresh.md can lag repo HEAD mid-session
    (the honest number for a running lane). --plugin <id> (repeatable) scopes the
    block to the plugins a lane runs.

  • scripts/telemetry-upsert.sh — maintains exactly ONE marker-identified
    telemetry comment on a tracking issue, editing it in place instead of posting a
    second (the interim home of the #502 telemetry contract). Given --issue N --marker STR --body-file PATH, it writes a machine-detectable sentinel <!-- claude-ops:lane-telemetry marker=STR --> as the comment's first line, finds
    that sentinel across ALL comments (paginated — a match on any page prevents a
    duplicate) and PATCHes it; failing that (first migration off a hand-authored
    comment) it adopts the most recent comment BY THE AUTHENTICATED USER carrying the
    raw marker text; else it creates one. STR is [A-Za-z0-9:@._-]+ (so it can
    never close the HTML comment early), and one writer identity owns a given marker
    — which is what the loop-lane convention's <lane>@<instance> suffix makes true
    rather than aspirational (#1295): a marker naming only a lane type is shared by
    every concurrent instance of that lane, so they clobber one another's durable
    state. Both fallback boundaries treat @ as a marker char, so lane:x never
    adopts lane:x@laptop-a's comment, nor lane:x@a adopt lane:x@a@b's.
    Body input: prefer --body-file - (stdin) for a body generated in memory (e.g.
    piped from machine-behavior.sh); a real --body-file PATH must resolve under
    --body-dir (default $CLAUDE_PLUGIN_DATA), may not be a symlink, and is capped
    at 64 KiB — a prompt-driven script must not be coaxed into posting an arbitrary
    file (a secret, a token store) as a public comment. Before the write it rejects
    a body that begins with a literal @ or falls under a 16-byte floor, exiting
    3 having called no API at all; after the write it re-reads the comment and
    exits 6 if what landed lost the sentinel or fails those same assertions. A
    read-back that could not be performed also exits 6, reporting the cycle
    UNCONFIRMED — except a 404, which reports the comment NOT RETRIEVABLE
    (deleted, its issue deleted, or read access lost) and rules the UNCONFIRMED
    reading out (#952). The @ rule is positional, so a body whose FIRST line is a
    GitHub @mention is rejected too: lead with a telemetry key (lane:) and put
    mentions on a later line.

Never pass a body as an @path string

Applies to every telemetry or comment write a lane makes — through
telemetry-upsert.sh or through the gh api upsert a lane inlines, since an
installed plugin cannot invoke a sibling plugin's script.

Pass the body as file contents (--body-file PATH) or pipe it (--body-file -).
Never interpolate an @path string into a body value: gh issue comment --body @path and gh api -f body=@path send the literal text @path. Reading from a file
takes gh issue comment --body-file, or gh api -F/--field key=@path — per each
command's own --help (gh 2.95.0); gh api has no --body-file flag at all.

The failure is invisible from the outside (#943): the comment's timestamp still
moves, so any check keying on updatedAt reads the lane as fresh while it
carries no data. (This skill's sibling reader morning-brief is not that check —
it parses lane: and last-cycle: out of the body, so a degraded comment makes
the lane vanish from its report rather than look healthy. What a degraded body
deceives is any consumer that keys on the comment's timestamp instead of reading
its body.)

telemetry-upsert.sh refuses such a body before it writes anything and re-reads
what landed afterward. An inlined upsert now encodes three checks itself: a
pre-write gate (empty, leading @, not sentinel-prefixed, or under a 16-byte
payload floor measured below the sentinel line → skip the cycle, no API call), a
check of the write's own exit status (a failed write leaves the previous cycle's
body in place, which a read-back running regardless would accept), and a
post-write read-back of what the write stored. What an inline block does NOT
replicate: the 64 KiB cap, the body-file containment checks, retries, and this
script's distinct non-zero exit codes — an inline branch always exits 0 and
reports through stderr. It also inherits this script's own limits: a PATCH that
succeeds while storing the previous body still verifies, and the read-back proves
some well-formed telemetry is present, not this cycle's.

Cross-references

  • context/refresh.md — why a running lane can't hot-reload a
    merged fix to its own skills, the git staleness probe, and the restart cadence
    (#514).
  • /claude-ops:plugins — the authoritative, richer plugin-fleet sync (scope
    divergence, new-catalog installs). This skill's marketplace refresh is the light
    claude plugin marketplace update step of a launch, not a substitute.
  • /claude-ops:morning-brief — reads the loop-lane telemetry (per-lane
    last-cycle freshness). This skill starts/stops the lanes that emit it.
  • #480 (loop-prompt authoring skill) — forward dependency that will own durable
    prompt storage. #496 (context economy / restart discipline) — why lanes get
    restarted. #502 (telemetry) — the per-lane telemetry the running lanes feed.