Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

worktree pool

a pool of reusable git worktrees shared across sessions, so expensive build trees (especially WebKit's WebKitBuild/) are reused instead of rebuilt from scratch every time

each session owns a worktree through a lockfile that records its session identity

a released worktree is reset to a clean base but keeps its build output, so the next session that claims it only needs an incremental build

commands

all commands live in ~/Developer/worktrees/.pool/bin

the wt entry point dispatches to the rest, so wt claim runs wt-claim and so on

put the bin directory on your PATH for the nicest experience:

export PATH="$HOME/Developer/worktrees/.pool/bin:$PATH"
command purpose
wt claim [REF] [--repo PATH] [--base REF] [--branch REF] [--note TEXT] [--prefer=MODE] [--seed-cache[=MODE]] [--create] [--timeout=SECONDS] [--progress=SECONDS] [--notify=MODE] [--notify-command=CMD] [--refresh] claim a worktree and print its path; reusable worktrees are scored by affinity/cache/recency, dead local terminal owners are immediately reclaimable, and unavoidable creation can opt into cache seeding
wt release [--repo PATH] [--base REF] [--all] [--force] return this session's worktree to the pool, reset to a clean base but keeping the build cache (a worktree with uncommitted changes is kept unless --force); run with no arguments from inside a pooled worktree, it releases that worktree instead (like .)
wt release NAME|REF... [--force] [--delete-branch] (or --worktree NAME / --branch REF) release one or more specific pooled worktrees, each named by its worktree name/path, its checked-out branch, or . for the one you are in (auto-detected), whichever session owns it, so you can reclaim a departed session's worktree by hand; --delete-branch also drops the branch ref
wt build [NAME|REF] [--notify=MODE] [--notify-command=CMD] -- <cmd...> run a build under a machine wide lock so only one heavy build runs at a time; target another worktree by name/branch and optionally notify when the build finishes
wt run [NAME|REF] -- <cmd...> run a command without the build lock; target selection matches wt build, and no target runs in the current directory
wt exec [--repo PATH|--all] [--free] [--mine] [--dry-run] -- <cmd...> run a command sequentially across selected worktrees with aggregate status; reusable worktrees are temporarily claimed to avoid races
wt open [NAME|REF] [--command=CMD] [--dry-run] open a selected worktree with the configured editor/browser command or platform default
wt edit [NAME|REF] [--command=CMD] [--dry-run] open a selected worktree with an explicit/configured command or $EDITOR; target resolution matches wt open and wt path
wt adopt PATH [--note TEXT] [--force] move an existing linked worktree into a managed pool name while preserving its branch, changes, and ignored caches
wt status [--disk] [--mine] [--free] [--stale] [--dirty] [--sort=MODE] [--watch[=SECONDS]] [--color=MODE] [--hyperlinks=MODE] [--porcelain] [--repo PATH] show pool state using the live terminal width, with optional color and clickable worktree names; non-tty human output is unbounded and --porcelain remains plain tab-separated data
wt config [list|get|set|unset] read and update validated persistent settings stored in .pool/config; environment variables still take precedence at runtime
wt migrate [--dry-run] normalize any legacy worktree names to the current scheme (safe to run repeatedly)
wt remove [--repo PATH] [--all] [--force] [--dry-run] delete free or stale pooled worktrees to reclaim disk (a live or dirty worktree is kept unless --force)
wt remove --lru [--repo PATH|--all] [--max-free=N] [--max-disk=SIZE] [--dry-run] remove the oldest reusable worktrees only until the configured count and disk limits are met; options override WT_MAX_FREE and WT_MAX_DISK
wt remove NAME|REF... [--force] [--dry-run] (or --worktree NAME / --branch REF) delete one or more specific pooled worktrees, each named by its worktree name/path, its checked-out branch, or . (auto-detected), whichever session owns it
wt path [NAME|REF] (or --worktree NAME / --branch REF) print a pooled worktree's path with no side effects, by name/branch/., or (no argument) the worktree the current directory is in, else the one this session owns; handy as cd "$(wt path <branch>)"
wt doctor [--fix] report inconsistencies (orphaned locks, leftover mutexes, dangling worktrees); --fix cleans up orphaned locks and mutexes and prunes git worktree metadata

run wt <command> --help for the options of a command

run from inside a pooled worktree, a bare wt release or wt path (no argument) acts on that worktree, so you never have to name the one you are standing in

when a branch is checked out in worktrees for several repos, a bare NAME|REF or --branch REF resolves the ambiguity from the current directory: the worktree you are standing in, otherwise the one for the current repo

shell integration

optional niceties once bin is on your PATH:

bash

source ~/Developer/worktrees/.pool/bin/wt-completion.bash

zsh

autoload -Uz compinit && compinit
source ~/Developer/worktrees/.pool/bin/wt-completion.zsh

fish

source ~/Developer/worktrees/.pool/bin/wt-completion.fish

each integration completes subcommands, flags, worktree names, and branches, and defines wtcd; for example, wtcd pr-68663-slotted enters that worktree in the current shell

example

WT="$(wt claim --repo ~/Developer/WebKit --branch dcrousso/my-task --note 'bug 262834')"
cd "$WT"
# ... make edits now ...
wt build -- Tools/Scripts/build-webkit --release
wt build -- Tools/Scripts/run-webkit-tests --release --no-build inspector
# leave the branch for review and return the worktree:
git switch --detach
wt release --repo ~/Developer/WebKit

call wt claim again at the start of each turn during a long session

it is re-entrant, so it returns the same worktree and refreshes the heartbeat; passing --note also replaces the existing note

a bare claim never creates a new worktree; pass a branch REF (or --create for a detached worktree) when allocating another worktree is intentional

wt claim my-branch --timeout=300 waits up to five minutes for a reusable worktree, then checks out my-branch; --timeout=0 is a single reuse-only attempt

--prefer=release, debug, warm, or recent adjusts reusable-worktree scoring; auto prefers branch/base affinity, warm caches, free slots, and recency in that order

while waiting, --progress=10 reports pool state every ten seconds, --notify=bell rings on acquisition, and --notify-command=CMD runs a user-configured command with WT_WORKTREE, WT_BRANCH, and WT_WAIT_SECONDS

configuration

wt config list, get, set, and unset manage pool-local settings in .pool/config

key default environment override purpose
max-free unset WT_MAX_FREE reusable worktree limit for lru cleanup
max-disk unset WT_MAX_DISK scoped disk limit for lru cleanup
prune-on-release false WT_PRUNE_ON_RELEASE run best-effort global lru cleanup after release
claim-prefer auto WT_CLAIM_PREFER default reusable-worktree scoring mode
seed-cache none WT_SEED_CACHE new-worktree cache seeding mode (none, auto, clone, or copy)
timeout-progress 10 WT_TIMEOUT_PROGRESS seconds between timeout progress reports; 0 disables
notify none WT_NOTIFY acquisition notification mode (none or bell)
notify-command unset WT_NOTIFY_COMMAND shell command run after a timed claim succeeds
build-notify none WT_BUILD_NOTIFY build completion notification mode (none or bell)
build-notify-command unset WT_BUILD_NOTIFY_COMMAND shell command run after a build finishes
open-command unset WT_OPEN_COMMAND shell command used by wt open, with the path passed as $1
edit-command unset WT_EDIT_COMMAND shell command used by wt edit, with the path passed as $1
status-sort name WT_STATUS_SORT default status ordering (name, repo, age, or size)
color auto WT_COLOR human status color mode (auto, always, or never)
hyperlinks auto WT_HYPERLINKS OSC 8 file hyperlink mode (auto, always, or never)

the pool also reads these environment-only settings:

  • WT_POOL_ROOT sets the pool root (default ~/Developer/worktrees)
  • WT_STALE_HOURS sets the idle hours before a lock is reclaimable (default 18)
  • WT_BUILD_DIRS sets the paths that wt status treats as a warm build cache, colon separated and relative to a worktree
  • WT_SESSION_ID is set from any tool that has its own per session id
  • WT_SESSION_ENV is the ordered list of explicit variables checked before automatic agent and terminal detection (default WT_SESSION_ID COPILOT_AGENT_SESSION_ID)

session identity

a session is the first explicit WT_SESSION_ENV value, then an automatic copilot/codex/claude identity, then TERM_SESSION_ID

when none is set, the pool falls back to the controlling terminal, and then to the parent process

so it works out of the box for a human at a terminal, GitHub Copilot, Codex, and Claude

after explicit variables, the pool searches all non-empty COPILOT_*, CODEX_*, and CLAUDE_* values; session/thread/conversation/task/run IDs outrank terminal identity, while generic prefixed environments are a final sorted-hash fallback so raw values never enter lockfiles

for any other agent, point the pool at that tool's own session value once:

export WT_SESSION_ID="<your tool's stable per session id>"

or add the tool's variable to the search list:

export WT_SESSION_ENV="WT_SESSION_ID MY_AGENT_SESSION_ID TERM_SESSION_ID"

design

ownership and locking

each worktree has one lockfile at .pool/locks/<name>.lock, kept outside the worktree so it never appears in git status

a claim is atomic

the full lock body is written to a temp file and hardlinked into place with ln, which fails if the target already exists, so two sessions can never both win and a lock is never seen without its contents

reclaiming a free or stale lock is serialized by a short lived mutex, so two racing sessions can never both take over the same worktree

claim order

a claim first reuses a worktree this session already owns, then scores every free or stale worktree before trying the best candidate

branch and base matches outrank cache preferences, followed by the requested release/debug/warm policy, free-slot safety, and last-use recency

it creates a new worktree only when every reusable candidate is unavailable and creation was explicitly allowed with a branch REF, --branch REF, or --create; --timeout=SECONDS suppresses creation and retries once per second

the lock for a new index is acquired before git worktree add, so racing sessions cannot collide on the same index

--seed-cache=auto attempts only filesystem clone/reflink operations, while copy explicitly permits a full recursive copy of configured build directories from the newest warm worktree

release instead of delete

releasing resets the worktree to a clean detached base with git clean -fd

there is no -x, so ignored build output such as WebKitBuild/ stays in place

the lockfile is removed and the worktree remains for the next claimant

lru cleanup

wt remove --lru orders reusable worktrees by their last release or stale heartbeat and removes only enough oldest entries to satisfy --max-free and --max-disk

live, dirty, and current worktrees count toward disk usage but are never removed by lru cleanup

when prune-on-release is enabled, release runs the same lru policy after freeing worktrees; cleanup is best effort and never changes whether the release itself succeeded

status views

state filters are alternatives, so wt status --free --stale shows both unlocked and reclaimable worktrees

age and size sorts place the largest value first, while name and repo sorts are ascending; wt status --watch reruns the human view until interrupted

human output uses COLUMNS when explicitly set, otherwise reads the controlling terminal with stty; when no live terminal width exists, values are not truncated

color highlights free, stale, dirty, current, and session-owned worktrees; worktree names use OSC 8 file:// links when enabled

auto enables tty features only on an interactive non-dumb terminal, NO_COLOR disables non-explicit color, and --porcelain never emits color or hyperlink escapes

stale reclaim

each turn refreshes the lock heartbeat

a lock idle for longer than WT_STALE_HOURS (default 18) is treated as abandoned and may be reclaimed

free worktrees normally outrank stale ones, while explicit branch or cache preferences can select a better stale candidate

locks created by stable local terminal sessions also record host and parent PID; when that same-host process exits, the lock is reclaimable immediately, while agent or explicit session identities continue to rely only on heartbeats

global build lock

wt build serializes builds machine wide through an fcntl lock, because flock(1) is not available on macOS

concurrent WebKit builds already saturate the CPU and share Xcode's compilation cache, so running several at once only thrashes CPU and IO

serializing keeps each one fast

build completion hooks release the global build lock before ringing a bell or running build-notify-command; hooks receive WT_BUILD_EXIT, WT_BUILD_SECONDS, WT_WORKTREE, and WT_BRANCH

batch, open, and adoption

wt exec runs sequentially and continues after failures; reusable worktrees are temporarily owned for the command and returned afterward

wt open passes the selected path as $1 to a trusted configured shell command, or uses open on macOS and xdg-open elsewhere

wt edit passes the selected path as $1 to edit-command, or launches $EDITOR with the path last; both receive WT_WORKTREE and WT_BRANCH

wt adopt rejects the main worktree and dirty worktrees by default, reserves a pool lock before moving, and preserves ignored build output

default branch

when --base is omitted, the base is detected from origin/HEAD, falling back to main, then master, then the current HEAD

q/a

why not ccache?

as of Xcode 26, content addressable compilation caching is built into LLVM and enabled by WebKit (COMPILATION_CACHE_ENABLE_CACHING)

that cache lives at ~/Library/Developer/Xcode/DerivedData/CompilationCache.noindex (tens of GB) and is already shared across every worktree on the machine, which is why both incremental and fresh worktree builds get cache hits automatically

a separate ccache would be redundant, so it is intentionally omitted

notes

  • pooled worktrees are named <repo>-<N> and are keyed by the repo basename, including dot-prefixed names such as .pool-1, so a WebKit worktree is never confused with a playwright one
  • task named worktrees such as wk-262834 are never touched by the pool
  • the commands target macOS system bash (3.2) and python3, so they also run on Linux without changes
  • test/selftest.sh runs the whole suite, and each file under test/ (claim.sh, release.sh, migrate.sh, and so on) also runs on its own
  • every test builds its own isolated pool against throwaway repos and never touches the real pool

About

reusable git worktrees that keep expensive build caches warm across sessions

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages