Serializing Concurrent Gates Behind a Per-Repo Lock
One gate at a time per repository, so overlapping runs stop failing each other
0 milestones 3 sprints
51m 15s total 10m 30s per sprint
14.1M tokens in 137.9k tokens out
$16.55 nominal cost
Gantry's gate is the run's proof step, but concurrent runs of the same repository used one shared checkout underneath their separate worktrees. When their gates overlapped, one run could observe another run's in-progress tree and fail for contention rather than for its own code.
This job made gate execution take a per-repository advisory lock, reported the wait as a real run state, and fixed the related merge-resolve log path so a red resolve gate leaves diagnosable output behind.
How this walkthrough is structured
Feature
What did the run build and ship?
Gate execution now passes through a locked repository-scoped chokepoint, surfaces contention to users, and archives merge-resolve gate output when that path fails.
Build
How did Gantry structure the work?
The work was cut at the interface where it mattered: prove a standalone lock first, re-plan the integration contract after review, then handle merge-resolve log capture as a separate defect.
Crash-Safe Lock Primitive
0 agents 10m 30s wall time 42.4k tokens out $3.65 nominal cost
The primitive still stands in `src/engine/gate_lock.rs`. It derives identity from the shared git common directory, stores the lock file in that stable repository location, and holds an operating-system advisory lock through a guard. Its blocking acquire polls through the try path so an abort predicate can stop a wait, and its wait hook fires only when contention is real. The module's tests still document the cross-process and crash-release guarantees that made the later wiring safe to depend on.
This sprint carried the synchronization problem without any gate call sites attached to it. Its brief asked for repository-key derivation, a process-level advisory file lock, a try-acquire path, an interruptible blocking acquire, and a guard whose lifetime defines release. That is a holdable cut for a fresh agent because the proof surface was the primitive itself: same repository contention, unrelated repository independence, process death, guard drop, and abortable waiting. The run stayed green, but review still changed the work that followed by forcing the integration sprint to bind to the primitive's actual hook shape rather than to an assumed acquire callback.
Gate Lock Integration
0 agents 18m 43s wall time 64.4k tokens out $8.27 nominal cost
Gate runs now acquire the per-repository lock inside the gate runner, and the build driver routes baseline, sprint, and merge-resolve gates through that path. `RunEvent::GateLockWaiting` remains in the domain model as a transient waiting signal, with current emissions in the build gate helpers and render paths in the TUI and headless frontend. The original integration touched a now-removed engine build file, but the behavior survived the later module split into the current gate and build submodules. A contended run is shown as waiting and clears that state when the guard is acquired.
This was the boundary sprint: it took the primitive from an isolated module and made every real gate use it. The brief narrowed the risky part to one chokepoint, with the lock held only around the gate subprocess and never across an agent stage. It also had to translate the primitive's generic waiting hook into Gantry's event stream, terminal view, headless output, and abort latch. Because the prior review had already corrected the interface contract, this sprint did not need a repair pass or retry.
Merge Resolution Logging
0 agents 10m 6s wall time 19.8k tokens out $2.97 nominal cost
The merge-resolve archive path now includes `merge-resolve-gate.log` with the resolver's own artifacts. In the current tree, `src/engine/build/finish.rs` copies the gate log from the worktree logs into the main repository's run logs and commits only those archive paths. The tests around that code exercise a failing resolve gate and assert that the archived log is present, non-empty, and carries the gate output. The original file this sprint edited was later reorganized, but the capture rule is still explicit in the build finish code.
This sprint was kept out of the locking contract even though it came from the same incident. Its brief asked only to make merge-resolve gate output survive worktree teardown on both pass and fail, especially when a red resolve gate ends the run. That made the work small enough to verify by following the resolver archive path instead of reopening the synchronization design. It landed green with no recorded recovery.