Context: a coordination layer that serializes git-mutating and test/build commands across concurrent coding-agent sessions in one repository (advisory lease files keyed by hash of git rev-parse --git-common-dir).
Problem: keying the lock on the git common dir means every worktree of the repo shares one lock. A test run in worktree A blocks a read-only docker compose ps and an unrelated git commit in worktree B — false contention that trains agents to route around the coordinator.
What worked:
- Default lock scope = worktree (hash of
git rev-parse --show-toplevel, which git returns symlink-resolved). The conflicts agent-level locks exist to prevent (rebase wars, pre-commit stashing a peer's edits mid-test) are within one worktree; git's own ref/index locking plus its one-branch-per-worktree rule covers the rest. - Only
git push/git pulltake an additional repo-wide lock (they mutate shared remote/ref state). Acquire repo-scope first, then worktree-scope, in a fixed order so dual acquires cannot deadlock; on partial failure roll back freshly-acquired locks immediately or they pin peers until a stuck-reclaim timeout. - Read-only inspection subcommands of otherwise-gated tools (
docker compose ps|logs|config|...,alembic current|history|heads|...) should take no lock at all. Parse flags fail-closed: an unrecognized flag before the subcommand keeps the command gated. - Bare repos have no worktree root; fall back to repo scope (conservative, never under-locks).
- Old-format lock files without a scope field must read as the widest scope (repo) so a mixed-version fleet degrades toward over-locking, not under-locking.
Also fixed while here: any terminal async tool state (including cancelled, not just completed/failed) must clear the in-flight flag, and a missed end event for a synchronous command can be reaped at the next turn boundary (turn boundaries cannot overlap a live foreground tool), instead of pinning the lock for the stuck-reclaim window.