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.
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/…).
The minimum surface for routine work — start here.
| When | Command | Effect |
|---|---|---|
| Start a new task | make worktree-claim BRANCH=fix/foo |
Pick a free slot, fetch + reset to current origin/develop, create branch, refresh .venv/ only if uv.lock/pyproject.toml changed. Prints WORKTREE_PATH=…. |
| Inside the slot, ship the work | make pr-preflight && make pr-open |
Run the local lint + fast-test gate, push, open PR vs develop, enable squash auto-merge. Walk away — auto-merge lands it once CI is green. |
| After the PR merges | make worktree-release |
Detach the slot back to origin/develop, delete the local feature branch. Refuses unless the PR state is MERGED (use FORCE=1 to escape — only when intentional). |
| See pool state any time | make worktree-pool-status |
Tabular: pool, path, branch, state, claim age, venv health, disk size. Read-only; safe to run during other operations. |
| Assert pool invariants | make worktree-pool-check |
Read-only; exits non-zero if any slot is missing, has a surviving .benchbox/claim_in_progress marker, or there are extra pool-NN directories beyond POOL_SIZE. Cheap (no gh calls); use as a pre-release sanity check or periodic local cron. |
| Recover forgotten slots | make worktree-pool-sweep-stale |
Auto-release every slot whose PR is MERGED and tree is clean. Idempotent; refuses dirty/claimed-no-PR/unknown slots. Auto-runs once before worktree-claim fails. |
Pre-approved in Claude Code's user-global settings; agents can run them without prompting.
Reported by make worktree-pool-status:
| State | Meaning | Recovery |
|---|---|---|
free |
Detached HEAD, clean tree — claimable. | None needed. |
claimed |
On a feature branch, no PR yet or PR open. Active work. | Continue work or make pr-open. |
stale |
On a feature branch whose PR is MERGED. |
make worktree-pool-sweep-stale (or worktree-release from inside). |
dirty |
Uncommitted changes (excluding .benchbox/ scratch). |
Review, commit/discard manually, then release. |
aborted |
A .benchbox/claim_in_progress marker survived a crashed claim. |
make worktree-pool-reset POOL=NN after reviewing what's there. |
unknown |
gh lookup for the branch's PR state failed (auth/network/rate limit). |
Re-run when gh auth status is clean. |
missing |
Slot directory absent. | make worktree-pool-init to recreate. |
Venv health column:
| Venv | Meaning |
|---|---|
ok |
.venv/pyvenv.cfg exists and is at least as new as uv.lock + pyproject.toml. |
stale |
.venv exists but a dependency manifest is newer — next claim re-syncs. |
missing |
.venv absent — next claim recreates it. |
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-statusThen 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 promptThe 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/fooStay 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 diffDecide: commit + open a PR, or reset (with FORCE after reviewing).
Pytest, coverage, and ruff caches accumulate per slot. Strip them across all 10 slots:
make worktree-pool-disk-cleanReports per-slot freed bytes. Does not touch .venv/ — that's
intentional, it's the expensive thing the pool retains.
Once, after cloning the main repo:
make worktree-pool-initCreates 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).
If several pool slots have unpushed work:
make pr-fanoutWalks 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.
| Target | Use |
|---|---|
make worktree-pool-init |
Bootstrap missing slots; idempotent. |
make worktree-claim BRANCH=<type>/<slug> |
Claim a free slot atomically. <type> ∈ chore|fix|feat|docs. |
make worktree-release [FORCE=1] |
Release the current pool slot back to detached origin/develop. |
make worktree-pool-status |
Read-only state listing for all slots. |
make worktree-pool-check |
Read-only pool invariant assertion: exits non-zero on missing slots, surviving .benchbox/claim_in_progress markers, or extra slots beyond POOL_SIZE. No gh calls. |
make worktree-pool-sweep-stale |
Auto-release MERGED-and-clean slots. |
make worktree-pool-reset POOL=NN [FORCE=1] |
Per-slot manual escape hatch. |
make worktree-pool-disk-clean |
Strip pytest/coverage/ruff caches across all slots. |
make worktree-list |
git worktree list passthrough. |
make worktree-prune |
Legacy non-pool worktree cleanup; explicitly skips pool slots. |
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.
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.