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
mainthat already contains the previous landings - Test gate: refuse to land on a failing log (
signal.sh) and/or revert post-merge iftest_cmdgoes 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:
- Scrub —
git diff main...branchchecked againstforbidden_pattern; hits refuse the land and mark the laneCONFLICT - Clean-base check — refuses if
mainhas uncommitted tracked changes - Merge —
--no-ffwith messagemerge: <branch>(whatfleet revertfinds later). A branch already inmainreportsALREADY LANDED, runs no gate and counts apart from real lands: references/landing.md - Test gate — runs
test_cmd; on failure, hard-resetsmainto the tip captured before the merge (neverHEAD^, which on an already-merged branch is another session's merge) and marks the laneFAILED. An unsettest_cmdrefuses the land:signal.sh's log gate alone verifies nothing. Maketest_cmdthe full suite sweep, not just the lane's files (why). Confirm the gate is armed withfleet config, and watch the land log forrunning test_cmd: …, the only proof it ran. - 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 laneCONFLICT
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 scenariosreferences/session-prompt.md— lane brief to embed inclaude --bgprompts, 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/CONFLICTscripts/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 orjq. Detail: references/session-awareness.mdscripts/sweep.sh—fleet sweep: the post-wave pass.--porcelain/--jsonrows (phase, subject, verdict, detail, action); exit 10 on findings. E.g.fleet sweep --porcelain | awk -F'\t' '$3=="ARCHIVE-REQUEST"'. Detail: references/sweep.md