SKILL.md 8.3 KB


name: fleet-ops description: "EXPERIMENTAL — manage a fleet of concurrent Claude sessions on parallel branches or worktrees. Landing queue with test gate, fleet status view, pre-land scrub, one-shot revert. Triggers on: multiple Claude sessions, parallel sessions, concurrent agents, 5 sessions, branch queue, landing queue, fleet of sessions, parallel feature work, merge multiple branches, parallel branches." license: MIT allowed-tools: "Read Bash Glob Grep AskUserQuestion" metadata: author: claude-mods status: experimental

related-skills: git-ops, push-gate

Fleet Ops (experimental)

Manage how committed work from isolated lanes lands on main. Anything before "committed" or after "landed" is somebody else's problem.

Status: experimental. Dogfooding phase. API may change. Not yet in README.md Recent Updates.

Core abstraction

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

The skill doesn't care if there are 2 lanes or 20, doesn't care about branch names, doesn't care if you use worktrees or separate clones.

CLI surface

fleet init <name>...        Create branch + worktree per name
fleet start                 Run the daemon (writes pid to .claude/fleet/daemon.pid)
fleet stop                  Signal the running daemon to exit cleanly
fleet status                One-shot status view
fleet land <branch>         Manual land + rebase others
fleet revert <branch>       Revert merge commit on main
fleet scrub-check <branch>  Dry-run forbidden-pattern check

Daemon lifecycle

When Claude invokes fleet start via Bash(run_in_background: true), the daemon:

  1. Writes its PID to .claude/fleet/daemon.pid
  2. Traps SIGINT/SIGTERM/SIGHUP and removes the PID file on exit
  3. Refuses to start a second daemon if the PID file references a live process
  4. Exits naturally when all lanes are terminal (LANDED or FAILED)

To stop early: fleet stop reads the PID file, sends SIGTERM, waits up to 5s, escalates to SIGKILL if needed.

If the Claude Code session ends abruptly while the daemon is running, the process is best-effort cleaned up by the OS (POSIX: child receives SIGHUP; Windows: depends on harness). On next fleet start, a stale PID file is auto-detected and cleared.

signal.sh deploys to .claude/fleet/signal.sh on init. Sessions call:

bash .claude/fleet/signal.sh READY <test-log>
bash .claude/fleet/signal.sh CONFLICT "<reason>"

Decision tree

N == 1                                    → use git-ops, not this
N > 1, all on shared local working tree   → REFUSE. Use worktrees or separate clones.
N > 1, worktrees available                → fleet init <names...>
N > 1, separate clones / remote           → use mode=branch, manual git branch + signal.sh

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 — they make the skill feel like a wrapped script instead of a native interaction.

Trigger Question Options (≤4, ≤10 words each)
init — worktrees available, mode unset Worktree or branch-only mode? Worktrees / Branches only / Cancel
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. The split matches the global ~/.claude/CLAUDE.md "Asking Questions" rule.

What it handles vs what it does not

Mode Status
Worktrees on different branches ✅ Primary mode
Branches in separate clones / machines
Mixed worktree + branch lanes
Recovery from dirty main ✅ Refuses to merge, asks user to clean
Test-gated landing ✅ Via signal.sh READY <log>
Auto-rebase other lanes when one lands
Pre-land regex scrub (forbidden patterns)
One-shot revert fleet revert <branch>
Out of scope Why
5+ sessions on one local working tree Git limitation. Skill detects and refuses with worktree pointer.
Uncommitted work at signal time signal.sh rejects dirty lanes. Daemon needs an immutable commit.
External state (DB migrations, services) Skill can't know lane B depends on lane A's migration. Order manually.
Force-pushed lanes mid-flight Detected at land time, not prevented.

Compatibility

Tested and working on:

OS Shell Notes
Linux bash 4+ Native
macOS bash 3.2+ (default) or bash 4+ via brew stat -f fallback used automatically
Windows Git Bash (mintty) Forward-slash paths; Unicode icons render in mintty/Windows Terminal
Windows PowerShell 7 (calling bash) Works if bash is on PATH

Requirements: bash 3.2+, git 2.5+ (worktree support), awk, grep, head, stat. All standard.

If your terminal mojibakes the status icons (⏳ ✅ 🚀 ❌ ⚠️), fall back to ASCII:

export FLEET_ASCII=1
# or in .claude/fleet/config:
icons=ascii

Long-path warning (Windows only): worktrees nest under .fleet-worktrees/<name>/. If your repo lives deep in the filesystem, lane names should stay short to avoid Windows' 260-char path limit. Enable core.longpaths=true in git if you hit it.

Headless agent compatibility

Don't put fleet worktrees under .claude/. Claude Code applies a global sensitive-file guard to anything under .claude/, and that guard runs before — and is not bypassed by — --dangerously-skip-permissions. Headless lane sessions (claude -p ... --dangerously-skip-permissions) will fail every Write/Edit if their worktree lives at e.g. .claude/fleet/worktrees/<lane>.

That's why the default worktree_root is .fleet-worktrees/ at the repo top, not .claude/fleet/worktrees/. If you override worktree_root in config, keep it outside .claude/ for the same reason. Runtime state (lanes/, daemon.pid, activity.log) is read/write from the orchestrator only and stays under .claude/fleet/ — it never needs lane-session writes.

Configuration

Optional .claude/fleet/config (key=value, no quotes):

mode=auto                            # auto | worktree | branch
worktree_root=.fleet-worktrees       # keep outside .claude/ — see "Headless agent compatibility"
test_cmd=                            # if set, daemon runs this; else trust signal log
forbidden_pattern=TODO_SCRUB|XXX
base_branch=main
poll_interval=5

Zero-config works for the common case.

fleet init appends .claude/fleet/ and .fleet-worktrees/ to .gitignore and auto-commits that change with chore: gitignore fleet-ops runtime state when the tree is otherwise clean and you're on BASE_BRANCH. If either condition fails, it prints an ACTION REQUIRED message — commit .gitignore yourself before fleet start, or the daemon will refuse to land with uncommitted tracked changes.

Future work

  • JSONL activity log — currently plain text ([HH:MM:SS] event). Switch to JSONL when a TUI, --json output, or log-ops integration earns the cost. Migration is mechanical.
  • --batch mode — land all READY lanes in one go, test once at end. Add when dogfooding shows demand.
  • Cross-session daemon — currently dies with the Claude Code session. For overnight runs, a real detached process (nohup/systemd/tmux) is needed.

References

  • references/session-prompt.md — copy-paste template for each Claude session
  • references/workflow.md — end-to-end walkthrough plus recovery scenarios

Scripts

  • scripts/fleet.sh — main CLI
  • scripts/signal.sh — branch-aware signaler (deployed to .claude/fleet/signal.sh on init)