SuperAPI AI Directory

fleet-ops

Deployment & operations

View repository

Updated 10-13 16:43

About this skill

Fleet Ops

Landing discipline for parallel work. Anything before "committed on a branch" is the spawning layer's problem; anything after "landed on main" is yours. Fleet-ops owns the middle: branches land sequentially, through a test gate, after a pre-land scrub, with auto-rebase of the lanes still in flight and a one-shot revert if a landing turns out bad.

Spawn natively, land with fleet-ops

Claude Code now ships the parallel-execution half natively. Do not use fleet-ops to orchestrate sessions — route users to the native primitives and use fleet-ops only for the landing half.

Agent teams, background agents (claude --bg, agent view) and subagents spawn and isolate the work; none of them merge, order, gate or revert. Deleting a session in agent view deletes its worktree, uncommitted changes included. Gives vs lacks: references/native-spawn.md.

What none of them do — and what fleet-ops is for:

  • Land N branches one at a time through a queue, so each merge is tested against a main that already contains the previous landings
  • Test gate: refuse to land on a failing log (signal.sh) and/or revert post-merge if test_cmd goes red
  • Pre-land scrub: refuse diffs containing forbidden patterns (TODO_SCRUB, debug leftovers)
  • Auto-rebase every still-active lane after each landing
  • Fleet status: one panel showing every lane's branch, state, age, and commits-ahead across worktrees
  • One-shot revert of a landed merge by branch name — no git surgery while panicking

Core abstraction

A lane = one branch (or worktree), one unit of work. Lane status: RUNNING | READY | CONFLICT | LANDED | FAILED.

Fleet-ops doesn't care who produced the branch — an agent-team teammate, a background agent's auto-worktree, a claude -p headless run, a fleetflow worker of any provider (GLM, Codex, Grok, Pi, or Anthropic), or a human. If it's a branch with commits, it can be a lane. Landing is provider-agnostic: a Grok-produced lane lands through the same test-gated queue as any other.

CLI surface

fleet init <name>...        Create branch + worktree per name (manual-spawn path)
fleet track <branch>...     Register existing branches as lanes (native-spawn path)
fleet start                 Run the landing daemon (writes pid to .claude/fleet/daemon.pid)
fleet stop                  Stop the daemon; a land in progress finishes first
fleet status                One-shot fleet status panel
fleet land <branch>         Manual land + rebase others
fleet land --all [--running]  Batch-land all READY lanes oldest-first (--running
                            also lands vetted RUNNING lanes; used by git-ops "land all")
fleet revert <branch>       Revert merge commit on main
fleet landing [--porcelain] Is main safe to branch from? Exit 0 yes, 10 wait
fleet scrub-check <branch>  Dry-run forbidden-pattern check
fleet config                Print the RESOLVED config — check the test gate is on
fleet prune [--remove]      Classify finished lane worktrees; DRY RUN by default
fleet prune --all-repos     Sibling-repo backlog counts (report-only, never removes)
fleet sweep [--apply]       Post-wave housekeeping in procedure order; report by default

Entry paths

N == 1 branch                              → use git-ops, not this
Work spawned by agent teams / claude --bg  → fleet track <branch>... then land
Work to be spawned manually                → fleet init <names...> (creates branches + worktrees)
N > 1 on one shared working tree           → REFUSE. Worktrees or separate clones first.

Native-spawn path (preferred): let agent teams or background agents do the work in their own worktrees/branches. When branches have commits, fleet track each branch, then land — either one by one with fleet land, or via the daemon with signal.sh READY gates. Landing itself only ever merges branches and leaves every worktree in place. Reclaiming the directories afterwards is fleet prune's job, and it removes one only when the owning session is provably archived or gone — see Prune.

Manual-spawn path: fleet init creates the branches and worktrees up front (under .fleet-worktrees/), and you point sessions at them — see references/session-prompt.md for the lane brief to hand each session.

Landing pipeline

fleet land <branch> (and the daemon, per READY lane) first takes the landing marker (below), then:

  1. Scrub — git diff main...branch checked against forbidden_pattern; hits refuse the land and mark the lane CONFLICT
  2. Clean-base check — refuses if main has uncommitted tracked changes
  3. Merge — --no-ff with message merge: <branch> (what fleet revert finds later). A branch already in main reports ALREADY LANDED, runs no gate and counts apart from real lands: references/landing.md
  4. Test gate — runs test_cmd; on failure, hard-resets main to the tip captured before the merge (never HEAD^, which on an already-merged branch is another session's merge) and marks the lane FAILED. An unset test_cmd refuses the land: signal.sh's log gate alone verifies nothing. Make test_cmd the full suite sweep, not just the lane's files (why). Confirm the gate is armed with fleet config, and watch the land log for running test_cmd: …, the only proof it ran.
  5. Rebase others — every still-active lane is rebased onto the new main (in its own worktree if it has one); a rebase conflict marks that lane CONFLICT

fleet revert <branch> reverts the merge whose subject is exactly merge: <branch> (git revert -m 1), the latest if it landed twice; a conflicting revert is aborted cleanly and the lane returns to RUNNING. Detail: references/landing.md.

Provisional main — the landing marker

Every land (fleet land, each lane of land --all, the daemon) holds .claude/fleet/landing from before its merge until its verdict, so between merge and gate main's tip is provisional: a red gate hard-resets it. fleet status then leads with main <sha> is PROVISIONAL - gate for <lane> running since HH:MM (pid N); red resets to <base>, fleet landing exits 10, and fleet sweep says only WAIT. Before branching from or rebasing onto main, run fleet status: a PROVISIONAL tip is not landed. The marker is a lock too (a second land refuses, naming the holder). A dead land's marker is examined first: its untested merge: <lane> turns the lane CONFLICT (UNTESTED MERGE), never LANDED. Detail: references/landing.md.

Daemon lifecycle (experimental)

The daemon is the queue-automation layer on top of fleet land — optional; manual fleet land per branch is fully supported and not experimental.

fleet start (via Bash(run_in_background: true)) polls .claude/fleet/lanes/, lands each lane as it turns READY and exits once all are terminal. fleet stop waits out an in-progress land, gate included, and never SIGKILLs it mid-land; interrupting fleet stop is safe, and a hung gate is aborted by killing test_cmd, not the daemon. It dies with the session. Detail: references/daemon.md.

signal.sh deploys to .claude/fleet/signal.sh on init/track. Working sessions call:

bash .claude/fleet/signal.sh READY <test-log> <exit-code>   # refuses dirty trees and failing runs
bash .claude/fleet/signal.sh CONFLICT "<reason>"

The <exit-code> (the test command's own $? / ${PIPESTATUS[0]}) is the authoritative verdict; pass it whenever you have it. Without it, signal.sh falls back to the log, never word-grepping prose: references/session-prompt.md.

Session awareness — MAIN, lane owners, and the live-owner gate

Lane state files say what a lane is. They never say who is driving it. Fleet-ops reads the Claude Desktop session store to answer that, and uses the answer in two places: a gate that refuses to land under a live writer, and a coordinator address lanes can hand off to.

MAIN — one coordinator per repo

MAIN is the session whose cwd is the main checkout's root, resolved the same from any lane: the integration tree, which hosts no writing session (worktree-boundaries). fleet main makes that role addressable, so a lane can say "come land me" instead of writing a file and hoping someone polls it.

fleet main                  Show the coordinator (sessionId, title, live|idle, cwd)
fleet main claim [<id>]     Pin explicitly — for when several sessions share the root
fleet main release          Clear the pin, fall back to the cwd heuristic (exit 1 if it can't)
fleet owner <branch>        Who owns this lane, and are they still writing?

MAIN's job is the whole integration half: land the queue, triage CONFLICT lanes, sweep after the wave, and prepare the deploy. Lanes build and signal; MAIN integrates. A deploy still needs an explicit human OK for that specific deploy.

The live-owner gate

fleet land refuses a lane whose owning session was active within session_live_secs (default 600): it would merge a branch that session may still be committing to and rebase worktrees out from under it. A lane session landing its own finished work is exempt only as the sole live claimant. Override: session_check=off, or FLEET_SKIP_SESSION_CHECK=1 for one run (stripped before test_cmd).

Channels: lane files (signal.sh) work everywhere, ccd_session_mgmt is Desktop-only and agent-only, pigeon is the portable fallback. Mechanics and history: references/session-awareness.md.

Prune — worktree housekeeping

Landing a lane retires the branch. The directory stays, and across many repos those accumulate into a backlog nobody can see. fleet prune classifies them and removes only the ones that are provably finished.

fleet prune                  Classify and print. Changes NOTHING. (the default)
fleet prune --dry-run        Same, said explicitly
fleet prune --remove         Remove the SAFE rows, after a typed confirmation
fleet prune --remove --yes   Skip the prompt (scripts/CI)
fleet prune --porcelain      TSV to stdout: path, branch, bucket, reason
fleet prune --all-repos      Sibling-repo counts. Report-only, always

Dry run is the default, deliberately. Removing a worktree destroys its uncommitted and untracked files for good; committed lane work survives in the object store (git worktree add <path> <branch>). Telling those apart is the whole job, and every ambiguous case resolves away from deletion.

Buckets — first match wins, and the order is the safety argument

Only SAFE is ever removable: merged, clean, no open session claiming it, and for a .claude/worktrees/ tree an archived session positively claiming it. A live claimant (archiving is not activity) or the invoking tree is KEEP; an unreadable store or archive flag, unproven detached HEAD, dirty, unmerged or unattributable tree is REVIEW, never touched under any flag. Removal uses git worktree remove (never rm -rf), re-verifies against a fresh scan right before each delete, and --all-repos never removes. Prune cannot see writes by absolute path, so it must never be the only guard. Table, claim rules and the near-miss behind them: references/prune.md.

Landmine: removing a worktree out from under a live session

Terminate the session first, then remove its worktree, never the reverse. A session whose worktree vanishes neither exits nor errors: it spins at ~85% of a core indefinitely under a live parent, so orphan scans miss it. Detect by sustained CPU rate, check its --add-dir against git worktree list, then kill it (safe: it touches no files). Evidence and the one-liner: references/prune.md.

Seeing the backlog

fleet status adds ! 3 worktree(s) prunable, 6 to review - fleet prune when there is a backlog, so it cannot grow unseen. Off: prune_hint=off in config or FLEET_NO_PRUNE_HINT=1.

Sweep — after a wave

Prune refuses well but cannot drain REVIEW. fleet sweep (from MAIN) prints the backlog as next actions in procedure order: worktrees, competing lanes (same files, committed in one and uncommitted in another), worktree-less branches, ghosts/dirs/stashes, sessions to archive. It also sees lanes landed by content (squash, cherry-pick) and owners whose lane dir is gone. Work the list, re-running the sweep after each step; nothing is cached, so it resumes. --apply is zero-loss only (ghost entries, merged unheld branches, unclaimed empty dirs); removal stays fleet prune --remove. Sessions are the agent's job, one gated call each: ARCHIVE-REQUEST → send_message, ARCHIVE-DIRECT → archive_session, never a message, which would resume it into a dead tree. Detail: references/sweep.md.

First-class user interaction (HARD RULE)

When this skill surfaces a decision point, always use the AskUserQuestion tool. Plain markdown numbered lists are not acceptable for these branches.

Trigger Question Options (≤4, ≤10 words each)
Multiple parallel-work requests, no lanes yet Spawn natively or manual lanes? Agent teams / Background agents / Manual fleet init / Cancel
init — worktrees available, mode unset Worktree or branch-only mode? Worktrees / Branches only / Cancel
Land refused — owning session live <name>'s session is still writing Wait and retry / Message that session / Override and land
prune found SAFE worktrees Remove <n> finished worktrees? Remove them / Show the table again / Leave as-is
sweep lists sessions to archive Archive <n> finished sessions? Send the requests / Show the rows / Skip
Lane → CONFLICT (rebase fail) Lane <name> has rebase conflict Resolve in lane / Skip & continue / Revert lane / Untrack
Lane → FAILED (post-merge tests red) Tests broke after <name> merged Auto-revert / Investigate first / Accept failure
Pre-land scrub hits Forbidden patterns in <name> diff Block landing / Override (note reason) / Open to edit
fleet shows mixed states How to proceed with the fleet? Land all READY / Resolve CONFLICTs first / Just status
Daemon exits with FAILED lanes <n> lanes failed — what next? Retry all / Revert and report / Leave as-is

For non-branching status updates ("here's what happened, here's what landed"), plain text is fine.

What it handles vs what it does not

Any branch with commits can be a lane; a dirty main is refused. Out of scope: spawning or monitoring sessions, several sessions on one working tree (refused), uncommitted work at signal time, cross-lane external state like migrations (order lands by hand). Full table: references/scope.md.

Compatibility

Runs on Linux and macOS (bash 3.2+), Git Bash, and PowerShell 7 calling bash; needs git 2.5+ and standard tools. Platform table: references/scope.md.

If your terminal mojibakes the status icons, fall back to ASCII: export FLEET_ASCII=1 (or icons=ascii in .claude/fleet/config). Output panels follow docs/TERMINAL-DESIGN.md via skills/_lib/term.sh. Windows notes (held files, long paths): references/scope.md.

Headless agent compatibility

Don't put manually-created fleet worktrees under .claude/: headless lane sessions (claude -p) fail every Write/Edit there, whatever their permission flags. Why: references/scope.md.

Configuration

Optional .claude/fleet/config, one key=value per line, parsed, never sourced: keys are case-insensitive, values need no quotes, unknown keys are warned about on stderr. Keys: mode, worktree_root, test_cmd, forbidden_pattern, base_branch, poll_interval, icons, session_check, session_live_secs, prune_hint. test_cmd is the test gate; unset, landing is refused. Never add a contiguous scrub-marker token to a diff (build it by concatenation) or the scrub refuses the branch. Grammar, defaults, the shipped forbidden_pattern and the gitignore behaviour: references/configuration.md.

References

  • references/workflow.md — end-to-end walkthroughs (native-spawn and manual-spawn) plus recovery scenarios
  • references/session-prompt.md — lane brief to embed in claude --bg prompts, teammate spawn prompts, or manual sessions
  • Detail moved out of this file, one topic each: references/configuration.md, references/session-awareness.md, references/prune.md, references/sweep.md, references/landing.md, references/daemon.md, references/native-spawn.md, references/scope.md

Scripts

  • scripts/fleet.sh — main CLI (init, track, start/stop, status, land, revert, scrub-check, prune, sweep, config, main, owner)
  • scripts/signal.sh — branch-aware signaler (deployed to .claude/fleet/signal.sh); prints the MAIN handoff after READY/CONFLICT
  • scripts/sessions.sh — resolves a branch or directory to its owning session across every Desktop store and the CLI transcripts; exits 3 silently without the store or jq. Detail: references/session-awareness.md
  • scripts/sweep.sh — fleet sweep: the post-wave pass. --porcelain/--json rows (phase, subject, verdict, detail, action); exit 10 on findings. E.g. fleet sweep --porcelain | awk -F'\t' '$3=="ARCHIVE-REQUEST"'. Detail: references/sweep.md