Parallel work — scripts/wt
scripts/wt creates a git worktree per issue at ../<repo>-wt/<branch-leaf>/, with
heavy dev-dep directories symlinked back to the main checkout so installs are not
duplicated. This is the default workflow whenever more than one issue — or more than one
agent session — is in flight, because it prevents the branch-flip problem where one
session’s git checkout swaps the working tree out from under another.
scripts/wt new 1234 # worktree + branch off origin/<default>scripts/wt list # branch, age, pushed-state, pathscripts/wt remove 1234 # refuses if there is uncommitted workscripts/wt prune # remove merged-and-deleted worktrees — run after every mergescripts/wt stash # worktree-PRIVATE stash (see guard 4 below)scripts/wt doctor # verify symlinks and shared stack reuseIt warns at 8 active worktrees and refuses at 10 — a WIP guard, not a resource limit.
Worktrees are cheap here (dependencies symlinked, dev stack shared); what the cap
protects is your ability to finish things. Raise it per-invocation for a deliberate burst
(WT_CAP=16 scripts/wt new 1234) — see
Working the harness — a day in the life.
Concurrent wt use is safe by construction, not by coordination. Three structural
guards make it impossible for one session’s prune to reap another’s worktree:
--no-trackon branch creation. Without it a fresh branch inheritsorigin/<default>as its upstream, and all three ofprune’s conditions then line up for a never-pushed worktree — an upstream exists,origin/<branch>does not, and the branch is trivially an ancestor of the default. The worktree gets reaped while someone is working in it.--no-trackleaves no upstream, so the never-pushed check correctly skips it.- A
.wt-ownermarker and a freshness grace window.prunenever removes a worktree younger thanWT_GRACE_MIN(default 30 minutes) unless--forceis passed, protecting one that another session created moments ago and has not pushed yet. - A per-worktree
WT_TEST_DB. Each generated.envrcexports a unique test database name, so parallel test runs in sibling worktrees get isolated databases and no external lock file is needed. Have the project’s test settings read it, andsource .envrcbefore running the suite. scripts/wt stashinstead ofgit stash.refs/stashlives in the git common dir, so it is not worktree-scoped: every worktree of a repo shares one stack. Worktree A pushes, B pushes, A pops — and A gets B’s work. Because a clean apply also drops the entry, B’s uncommitted work is now gone from the stash and mixed into an unrelated tree, with nothing in either session’s output naming it. (A conflicting apply keeps the entry — that is the lucky branch of the same bug.) There is nopre-stashhook, so nothing can catch it where it happens; it has to be replaced.wt stash [push|pop|apply|list|show|drop]keeps entries underrefs/wt-stash/<worktree>and never reads or writesrefs/stash.wt doctorwarns whenrefs/stashis non-empty while worktrees are active. Untracked files are not captured (git stash createcannot) and the command says so rather than letting you assume otherwise — for a one-file detour,cpaside and back is still simplest.- Per-worktree E2E ports. Each
.envrcalso exportsWT_E2E_PORTandWT_E2E_DEV_PORT, derived from the worktree’s slug and probed forward past any port a sibling worktree has claimed or a live process holds. Playwright’sreuseExistingServeridentifies a running server by port alone, so with one hardcoded port every worktree shares one preview server and a local run can pass against another branch’s bundle. Read the variables in your E2E config (seetests/CLAUDE.md.example); unset — the main checkout and CI — your defaults apply.
prune recognizes every way a branch lands. A branch counts as merged when its tip
is an ancestor of the default branch (a merge commit), when git cherry finds every one
of its patches already there (a tip amended or rebased after its last push), or when the
forge has a merged MR/PR whose head is exactly the local tip (a squash merge). If none
can be shown — including when the forge cannot be reached — the worktree is kept. A
tracked .envrc or .wt-owner does not count as local work. Nothing reaps a worktree
after a forge merge, so run scripts/wt prune from the main checkout after merging;
/mr reminds you, and /mass-merge removes each worktree as its MR lands.
wt list shows age and pushed-state per worktree so it is obvious at a glance whose
worktree is whose.