Worktree pool — maintainer dev loop¶
BenchBox uses a retained pool of 10 git worktrees (POOL_SIZE = 10)
for the maintainer + AI-agent dev loop. New work claims a free slot,
runs there until the PR merges, then releases it back to the pool. The
slot survives — including its .venv/ — so subsequent claims skip the
expensive setup.
This page is the maintainer/agent reference for that workflow. External contributors working from a fork do not need the pool — they clone, commit on their own branch, and open a PR; see development.md for that flow.
At a glance¶
Layout (siblings of the main clone):
/Users/joe/Developer/BenchBox ← main clone, always on develop
/Users/joe/Developer/BenchBox.pool-01 ← pool slot 1
/Users/joe/Developer/BenchBox.pool-02 ← pool slot 2
...
/Users/joe/Developer/BenchBox.pool-10 ← pool slot 10
A free slot is detached at origin/develop with a clean working tree.
A claimed slot is on a feature branch (chore/…, fix/…, feat/…,
docs/…).
Common commands¶
The minimum surface for routine work — start here.
When |
Command |
Effect |
|---|---|---|
Start a new task |
|
Pick a free slot, fetch + reset to current |
Inside the slot, ship the work |
|
Run the local lint + fast-test gate, push, open PR vs |
After the PR merges |
|
Detach the slot back to |
See pool state any time |
|
Tabular: pool, path, branch, state, claim age, venv health, disk size. Read-only; safe to run during other operations. |
Assert pool invariants |
|
Read-only; exits non-zero if any slot is missing, has a surviving |
Recover forgotten slots |
|
Auto-release every slot whose PR is |
Pre-approved in Claude Code’s user-global settings; agents can run them without prompting.
Slot states¶
Reported by make worktree-pool-status:
State |
Meaning |
Recovery |
|---|---|---|
|
Detached HEAD, clean tree — claimable. |
None needed. |
|
On a feature branch, no PR yet or PR open. Active work. |
Continue work or |
|
On a feature branch whose PR is |
|
|
Uncommitted changes (excluding |
Review, commit/discard manually, then release. |
|
A |
|
|
|
Re-run when |
|
Slot directory absent. |
|
Venv health column:
Venv |
Meaning |
|---|---|
|
|
|
|
|
|
Common scenarios¶
Pool is full when I try to claim¶
worktree-claim automatically runs worktree-pool-sweep-stale once on
the first miss and retries. If still full afterwards, every slot is
either claimed (active work), dirty (uncommitted), or unknown (gh
flake). Inspect:
make worktree-pool-status
Then either finish/abandon a claimed slot, or — only after reviewing the diff — manually reset one:
make worktree-pool-reset POOL=04 # refuses if dirty
make worktree-pool-reset POOL=04 FORCE=1 # interactive RESET prompt
claim says my branch already exists¶
The atomic-claim retry path can trip if a previous claim half-succeeded (branch created, slot marked free again). Delete the stale local branch and retry:
git branch -D chore/foo
make worktree-claim BRANCH=chore/foo
Slot has uncommitted changes I don’t recognise¶
Stay calm — this is the design. The pool deliberately does not auto-reap dirty slots, because a crashed agent session might have left work that’s only on disk. Inspect from inside:
cd ../BenchBox.pool-04
git status
git diff
Decide: commit + open a PR, or reset (with FORCE after reviewing).
Disk is filling up¶
Pytest, coverage, and ruff caches accumulate per slot. Strip them across all 10 slots:
make worktree-pool-disk-clean
Reports per-slot freed bytes. Does not touch .venv/ — that’s
intentional, it’s the expensive thing the pool retains.
Bootstrap a new workstation¶
Once, after cloning the main repo:
make worktree-pool-init
Creates BenchBox.pool-01..10 as detached siblings, runs uv sync --group dev and pre-commit install in each. Idempotent — re-running
leaves existing slots alone, only fills missing ones. Override defaults
via env: POOL_SIZE=N and WORKTREE_POOL_PARENT=… (used by the test
suite for disposable pools).
Publish across many open branches¶
If several pool slots have unpushed work:
make pr-fanout
Walks every worktree (skipping the main clone) and runs make pr-open
with bounded parallelism (PR_FANOUT_JOBS ?= 4). Sequenced so the
local pre-push hook lock doesn’t bottleneck it.
Full Make target reference¶
Target |
Use |
|---|---|
|
Bootstrap missing slots; idempotent. |
|
Claim a free slot atomically. |
|
Release the current pool slot back to detached |
|
Read-only state listing for all slots. |
|
Read-only pool invariant assertion: exits non-zero on missing slots, surviving |
|
Auto-release MERGED-and-clean slots. |
|
Per-slot manual escape hatch. |
|
Strip pytest/coverage/ruff caches across all slots. |
|
|
|
Legacy non-pool worktree cleanup; explicitly skips pool slots. |
Invariants¶
These are guaranteed by the targets above; agents and humans can rely on them:
Idempotent init.
worktree-pool-initnever destroys or resets existing slots — re-running is safe.Atomic claim. Concurrent
worktree-claiminvocations always pick distinct slots (serialized via.git/pool.lock).Retained
.venv/. Slots keep their virtualenv across release/claim cycles.uv synconly re-runs at claim time when a dependency manifest is newer than the venv.No silent reaping. Dirty slots and
abortedslots are never auto-recovered — surfacing them viapool-statusis intentional, so uncommitted work is never discarded without operator review.Pool slots never check out
develop. Free state is detachedorigin/develop. The main clone owns the localdevelopbranch and Git forbids the same branch in two worktrees.worktree-pruneskips pool slots. Routine end-of-session prune is no longer needed — slots are released, not removed.
See also¶
CLAUDE.mdandAGENTS.md— session-start rules and pre-approved command lists for AI agents.release-guide.md — release-branch flow that runs alongside this dev loop.
repo-admin-settings.md — GitHub-side admin state (rulesets, required checks, incident labels) the dev loop depends on.