Skip to content

cdfork Guide

cdfork — multi-agent worktree orchestration

Section titled “cdfork — multi-agent worktree orchestration”

cdfork is BoB’s fan-out tool. It spawns N parallel /warp-drive sessions in isolated git worktrees, each in its own tmux window, so you can run independent work clusters in parallel without stepping on yourself.

Tmux + git are the state. There is no daemon, no state file, no database. If the orchestrator dies, your worktrees and commits are still intact and you can resume by attaching the tmux session.

Every cdfork command below works from both worlds — pick whichever you’re in:

  • Terminalcdfork <sub> (the shell alias that resolves to bin/cdfork).
  • Slash command/cdfork <sub> inside a Claude Code session; the /cdfork command is a thin wrapper that forwards every argument to the same bin/cdfork, so the flags are identical.

So cdfork fork branch-a branch-b and /cdfork fork branch-a branch-b do the same thing. The tables and examples below write the Terminal form; prefix any of them with / to get the slash form. (cdfork-pair is the one exception — see that section.)

The Command column shows the Terminal form (cdfork …); each has the matching Slash command /cdfork … (e.g. /cdfork status).

Command (Terminal — prefix / for the slash form) Purpose
cdfork fork <branch>... Spawn one warp-drive session per branch
cdfork fork --from-issues [N] Auto-discover N approved requirement issues, derive branches, fan out
cdfork fork --from-flightplan <N> Fan out flight-plan #N’s runnable wave — independent items in parallel, dependents held (#1613)
cdfork status [--json] List active worktrees + their tmux + warp-drive state
cdfork drop <branch> Safely remove a worktree + its tmux window
cdfork drop --all Drop every cdfork worktree for the current repo
cdfork pair --contract --backend --frontend --branch Cross-repo contract-driven parallel mode
cdfork-pair ... Standalone shape of cdfork pair (Terminal only — no /cdfork-pair; use /cdfork pair)
cdfork help Show command summary

cdfork is part of BoB. Once you’ve run bob-install.sh (which adds the BoB shell-wrapper block to your .bashrc / .zshrc), cdfork is available as a shell alias that delegates to ${BOB_SOURCE:-$HOME/.claude}/bin/cdfork. Run source ~/.zshrc (or open a new shell) to activate.

The standalone cdfork-pair script lives at bin/cdfork-pair and is invoked the same way once BoB is on PATH.

Tool Why Install
git Worktrees macOS: brew install git. Linux: distro package.
tmux Window management macOS: brew install tmux (not installed by default!). Linux: apt install tmux / dnf install tmux.
claude Spawned in each window The Claude Code CLI. Available globally once Claude Code is installed. The BoB claude() shell wrapper handles the -a1/-a2/-a3 automation flags.
gh --from-issues brew install gh / distro package.
jq Status JSON, from-issues/from-flightplan filter brew install jq / distro package.
node --from-flightplan (runs flightplan.js remaining) Already required by BoB’s scripts.

cdfork’s preflight check runs before any worktree is touched and fails fast if any of these are missing.

For a repo at /path/to/<repo>/, cdfork puts worktrees at:

/path/to/<repo>.worktrees/<branch>/

Sibling directory, not nested inside the repo. This keeps Drupal/Acquia-strict roots clean and avoids confusing composer (which traverses the dir tree looking for vendor/).

For branches with slashes (feat/foo), git creates the intermediate dir naturally — you’ll see <repo>.worktrees/feat/foo/.

Mode Session Window
cdfork fork cdfork-<repo> one per branch, named <branch>
cdfork pair cdfork-pair-<branch> (slashes → _) <branch>-be and <branch>-fe

To attach: tmux attach -t cdfork-<repo> (or the pair session name printed in the summary).

Why tmux send-keys and not tmux new-window <cmd>

Section titled “Why tmux send-keys and not tmux new-window <cmd>”

cdfork uses tmux send-keys to type the launch command into each window after the shell has loaded its rc file. This is critical because the BoB claude() shell wrapper (which intercepts -a1/-a2/-a3 and routes them to switch-level.sh) is defined in your shell init, not on PATH. If we used tmux new-window <cmd>, the wrapper wouldn’t exist when <cmd> ran, and the automation level wouldn’t be set.

The send-keys approach also lets users see the literal command land on screen, so it’s obvious what’s about to run.

Mode 1 — Laptop, interactive. cdfork fork branch-a branch-b, attach the tmux session, watch the agents work, intervene when they hit a decision. Intended use: 2–3 sessions in parallel. Keeps a human in the loop on each.

Mode 2 — farm-01, unattended. Same command, headless. Each warp-drive session runs at automation Level 3, only escalating to GitHub Issues for genuine project decisions. SSH in occasionally to check cdfork status or attach the tmux session to spot-check progress. Intended use: 4+ sessions overnight or during the day on independent module clusters.

Pinch-point rules — the serial-only label

Section titled “Pinch-point rules — the serial-only label”

Parallelism works for module code, frontend code, tests, and cleanup sweeps.

Parallelism breaks for single-source-of-truth changes:

  • Drupal config syncconfig/sync/ is one tree. Two parallel branches exporting config will conflict.
  • Composer / pnpm lockfiles — one lockfile, deterministic resolution.
  • Database migrations / update.php — schema must apply in order.
  • Compiled theme assets — committed build output (e.g. nanaclaro, nanawall22) belongs to one builder.
  • Acquia deploy-gated work — single deploy pipeline.

Issues with these characteristics should carry the serial-only label. cdfork fork --from-issues excludes them from auto-discovery by default. Pass --include-serial only for the rare case where you’ve manually verified the work doesn’t actually touch a pinch point.

cdfork fork --from-issues runs the warp-drive preflight gate once, against the parent checkout, before any worktree or window exists — bob doctor (is this machine’s BOB_HOME converged with source?) plus bob ready --dry-run (is this project warp-drive-ready?). N sessions would otherwise each hit the same gate and each file the same todo. A failed gate ends the fan-out with nothing spawned and the todo naming each fix; --dry-run only reports the verdict (it files nothing and still prints the plan). CDFORK_PREFLIGHT=0 skips the gate — for hermetic tests, never as a way past a failing host.

This rule is nanawall-specific in principle. Other projects can reuse the label if they have similar single-source-of-truth concerns (seebod migrations, bodmail pnpm workspaces, etc.) — the label is just a flag, the concept of what’s serial is project-specific.

cdfork pair — cross-repo contract-driven

Section titled “cdfork pair — cross-repo contract-driven”

Anchor use case: the nanawall headless migration (nanawalld8nanawall-web). Whenever a feature spans a backend repo and a frontend repo, both sides can move in parallel as long as they agree on a contract — request shape, response shape, error semantics. cdfork pair codifies that.

Terminal (either the cdfork pair subcommand or the standalone cdfork-pair bin):

Terminal window
cdfork pair \
--contract docs/contracts/CAP-04.md \
--backend ~/Sites/nanawall/nanawalld8 \
--frontend ~/Sites/nanawall/nanawall-web \
--branch feat/cap-04-product-detail

Slash command (/cdfork pair — same flags; there is no /cdfork-pair):

/cdfork pair --contract docs/contracts/CAP-04.md --backend ~/Sites/nanawall/nanawalld8 --frontend ~/Sites/nanawall/nanawall-web --branch feat/cap-04-product-detail

Each side gets a worktree on the same branch name, plus a tmux window in a shared session, plus a Claude session launched with --append-system-prompt carrying the absolute path to the contract — so warp-drive starts with the contract already in its system prompt and reads it before any work.

The contract document is the integration point. If both agents implement to the same contract, integration at the end is just running the cross-repo test suite.

Terminal (prefix / for the /cdfork fork --from-issues … slash form):

Terminal window
cdfork fork --from-issues 4 # default 4
cdfork fork --from-issues 6 # widen the fan-out
cdfork fork --from-issues 4 --dry-run # show plan, don't spawn
cdfork fork --from-issues 4 --include-serial # include serial-only issues

Picks up to N approved requirement issues (--label req --label approved --state open) and derives branch names: <type>/<number>-<slug>.

Type heuristic on the title:

Title prefix Type
Fix* fix/
Add* / Implement* / Build* / Introduce* / Create* feat/
Refactor* refactor/
Doc* / Docs* docs/
anything else chore/

The leading kind-word is stripped from the slug to avoid fix/301-fix-....

Fanning out a fresh instruction, not pre-existing issues? The swarm decompose engine splits one free-text instruction into N independent subtasks and emits the same <branch>\t<id>\t<title> list cdfork consumes — with overlap and pinch-point analysis up front.

Flight-plan fan-out (--from-flightplan, #1613)

Section titled “Flight-plan fan-out (--from-flightplan, #1613)”

When a flight plan declares depends_on edges, its runnable set at any moment is the independent wave — items whose prerequisites are all complete. --from-flightplan fans out exactly that wave:

Terminal window
cdfork fork --from-flightplan 1615 # spawn the current runnable wave
cdfork fork --from-flightplan 1615 --dry-run # preview it
  • The wave comes from flightplan.js remaining <N> --json — the same single gate-resolution path warp-drive consumes, so an item waiting on an unmet depends_on, gated by a pending todo, or withheld behind a barrier is never spawned.
  • Each worktree session is scoped to its own item (/warp-drive --issue <NN>), so no session widens beyond its assignment. A closed issue counts as a complete prerequisite, so dependents release as the parallel sessions land their work.
  • Dependents are picked up by re-running the same command once their prerequisites are checked or closed — wave-by-wave consumption that finishes in ~critical-path time (flightplan.js status <N> reports the critical path; the plan’s per-item check-off timestamps are the receipts).
  • serial-only items are excluded exactly as with --from-issues (--include-serial overrides) — pinch points stay out of parallel worktrees.
  • “No runnable items right now” (exit 1) means the current wave is either done or blocked; inspect with flightplan.js remaining <N> --all --json.

“missing required commands” on cdfork fork — Run the install commands under Prerequisites. The check runs before any side effect.

Worktree already existscdfork drop <branch> (or cdfork drop --all) to remove. If commits weren’t pushed, drop will warn but proceed.

Worktree refuses to drop (“uncommitted changes”) — that’s the guardrail. Either commit/stash the changes inside the worktree, or pass --force if you’re sure you want to throw them away.

tmux session got killed accidentally — your worktrees and commits are intact. Just cdfork status to inspect, then either re-attach with tmux new-session -d -s cdfork-<repo> and re-launch the agents manually, or cdfork drop --all and start fresh.

--from-issues finds nothing — check gh issue list --label req --label approved --state open. The query has to return at least one match. If issues are missing the approved label, that’s the gate.

--from-issues is in the wrong repo — gh queries the current repo. cd to the project first.

If something goes wrong mid-spawn (network glitch, disk full, half a session came up), the recovery path is:

  1. cdfork status to see what got created.
  2. cdfork drop <branch> for any half-spawned worktrees.
  3. Re-run the original cdfork fork ... command. It’s safe to re-run — existing worktrees are skipped with a warning.