One Owner Per Run, Attach From Anywhere
A process-grounded lock makes one engine own a run, and a live transport lets other front-ends attach to watch and steer it.
A Gantry run is one engine driving one worktree, and until this job nothing stopped a second `gantry` command from starting a rival engine on the same run. Gantry judged a run "active" by whether its worktree still existed on disk — it could not tell a run that was genuinely building from one whose engine had crashed and left the directory behind. And because only the launching process ever held the engine's event stream, nothing else could look in: a supervising session or a separate tool had no way to watch a run in progress or answer a prompt it was waiting on.
This job gave a run a real owner and a way in. A process now takes a process-grounded lock before it starts an engine, so a run has exactly one owner; a second command against a live run is cleanly turned away instead of colliding, and `gantry list` shows which running-looking runs are truly live and which are abandoned. On top of that ownership it built a transport: the owner publishes its full event stream over a per-run socket, and other front-ends attach to receive the run's accumulated history and then its live activity, sending commands back to the same engine.
Build
The cut put a process-grounded ownership lock first and the live transport second, and inside each half proved the hardest primitive — the lock, then the wire codec — in isolation before any caller leaned on it.
The plan split along ownership and the transport that rides on it, and within each half the order was chosen so the riskiest piece could be proven with nothing depending on it yet. The run lock landed as a standalone module with no caller wired in, tested across real separate operating-system processes the way the existing per-repository gate lock was — the brief's stated reason being that getting its semantics and cross-process tests right there is what lets enforcement and the `gantry list` distinction become small localized edits rather than rewrites. The wire codec came first in the transport milestone for the same reason: it is the hardest thing to get right and the riskiest to discover late, so it shipped as a complete, round-trippable encoding of every event and command variant before any server or client existed, leaving the later sprints to handle connection plumbing rather than what goes over the wire.
The decision that mattered most spans the two milestones. When ownership enforcement was built, a second command on a live run was refused — but the refuse-or-attach choice was deliberately written as a single named branch whose only answer, for then, was refusal, so the transport milestone could replace refusal with attach at that one point without restructuring the surrounding code. That is exactly what the attach client sprint did: the same branch, a different answer.
No gate went red across the whole run and no review forced a re-plan. The single interruption was environmental: the sprint that makes the owner serve the transport had one execute attempt stop on a session limit, recorded as such in the ledger, and it passed clean on the retry — not a boundary redrawn under load. The up-front cost of proving the lock and the codec alone is the likeliest reason none of the boundaries ever had to move.
Feature
A run ownership lock and a full attach transport still stand at HEAD as dedicated modules, with the subscribe-and-send contract documented as the groundwork a future web view would reuse without an engine change.
Before this job Gantry had no run-level lock at all. Ownership was inferred from a worktree existing, the pid that `--detach` wrote was never read back, and two engines could drive one worktree with no coordination; a run whose engine had died looked identical to one still building. The engine also spoke to exactly one in-process front-end, so a separate process such as the standalone MCP server could offer a run-control action like answering a prompt but could only fail cleanly — there was no live engine for another process to reach.
What stands now is a liveness-aware lock keyed to each run's `.gantry/<plan>/` directory in the main checkout, resolved by the same git rule whether the caller sits inside the worktree or outside it, so the owner and any read-only observer agree on where it lives. Ownership is enforced on the one engine-starting path and surfaced by `gantry list` as live-owned versus orphaned. Above it sits the transport: a full-fidelity wire codec distinct from the lossy journal, a per-run Unix socket the owner serves, a fan-out hub that mirrors the engine's single stream to every consumer while merging their commands back, an attach client that implements the same engine interface the front-ends already drive, and the lifecycle rule that only the owner can stop a run while attachers freely come and go.
At today's HEAD the lock and each attach module are still present and carry most of the lines the job introduced. The standalone MCP server reaches a live engine over this same client, which is the shipped proof that a new out-of-process consumer needs no engine change; the contract — subscribe to events and send commands, the per-run endpoint and how it is located, history-then-live delivery, and the owner-versus-attach lifecycle — is documented as the seam a future web front-end would attach through. One of the job's test files is no longer present and the README paragraphs it added have since been rewritten, but the modules and their documented contract remain.