Two bugs caught by the first real concurrent dogfood (15 SDK build jobs
queued at once for clawdforge):
1. Concurrent fetch race — multiple jobs from same project all called
`git fetch +refs/heads/*:refs/heads/*` simultaneously. Git refuses
to fetch into a local branch ref that another worktree has checked
out, so jobs after the first failed fast with:
fatal: refusing to fetch into branch 'refs/heads/main' checked
out at '/workspace/clawdforge/<other-job-id>'
2. Worktree-from-local-branch — `worktree add ... main` reserved the
local branch ref to the worktree, blocking subsequent fetches even
when the lock above wasn't held.
Fix:
- Per-project asyncio.Lock around the materialize() body (clone +
fetch + worktree-add). Different projects still parallelize; same
project serializes through the cache dir.
- Fetch into remote-tracking refs only:
+refs/heads/*:refs/remotes/origin/*
Local refs/heads/* are never written, so no worktree can hold them.
- worktree add uses `--detach origin/<branch>` so each worktree is at
the remote-tracking ref. Multiple detached worktrees share the same
remote ref without conflict.
The recipe itself runs outside the lock, so concurrency=4 still
parallelizes the actual build/test work — only the brief
materialize step (clone or fetch + worktree create) serializes.
Tests still 6/6 green on test_runner.py (uses _StubWorkspace, not the
real WorkspaceManager, so it's unaffected — the real exercise is the
live re-queue against fixed code).