skip to content
Replays

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.

Gantry milestones

2 milestones 8 sprints 18 agents

203k peak context

145k median execute

A process-grounded lock makes one engine own a run, and a live transport lets other front-ends attach to watch and steer it.

2h 8m total 11m 34s per sprint

13m 22s plan 1h 20m execute 13m 11s review

75 edits 318 commands

claude harness

40.5M tokens in 421.8k tokens out

$49.73 nominal cost

This job changed Gantry's run model from "a worktree exists" to "a live process owns this run." It gave the owner a pid-bearing lock, made later invocations route through that ownership decision, and then put a live event and command transport on top of it.

The decomposition is visible in the tree: ownership became a small lock, enforcement, and registry layer before the attach work started; the transport then moved from protocol to publisher, client, lifecycle, and MCP reuse. The only recorded stress was in the publisher sprint, where the owner-side serving machinery had to be attempted again before it passed.

How this walkthrough is structured

Feature

What did the run build and ship?

Current Gantry still has the run lock, live-owner routing, attach protocol, publisher, client, lifecycle tests, and MCP bridge that this job introduced.

Build

How did Gantry structure the work?

The run was cut into ownership first and attachment second, with each milestone proving its primitive before callers and consumers depended on it.

Milestone 1

Run Ownership and Locking

3 sprints 0 agents 39m 55s wall time 136k peak context $16.38 nominal cost

Run ownership is still present as a dedicated subsystem. A lock file under the run's resolved orchestration home records the owning pid, start and resume pass through the owner-or-attach decision, and registry output can distinguish a run with a live owner from an abandoned active worktree. The old standalone test file from this milestone is gone, but the core modules and command labels remain in current source.

This milestone carried the foundation for everything that follows: Gantry needed to know whether a run was owned by a live engine before any second process could decide what to do. The work was split so the lock existed alone first, then the engine-starting path used it, then read-only listing surfaced it. That kept each agent's scope bounded: primitive semantics, caller behavior, and presentation were separate boundaries. The milestone stayed green through its sprint gates and review.

Run Lock Primitive

0 agents 10m 50s wall time 131k peak context 43.1k tokens out $3.86 nominal cost

The primitive stands in src/engine/run_lock.rs. The lock is the authoritative run.pid record, combines advisory flock ownership with a pid liveness query, resolves to the same runtime home from the main checkout or a worktree, and drops through an RAII guard. Its module comments still document the pid-reuse and clean-release tradeoffs, so the current implementation preserves the design rather than just the API surface.

This sprint was asked to carry the cross-process primitive by itself. That was the right size for a fresh agent because it did not also have to change start, resume, or list behavior; it only had to define where the lock lives, how pid liveness is read, and how stale ownership is reclaimed. The brief explicitly tied it to the existing gate lock shape, giving the worker a proven pattern for flock use and subprocess tests. Its boundary held without a red gate.

Single Owner Enforcement

0 agents 15m 15s wall time 136k peak context 53.2k tokens out $5.76 nominal cost

The centralized ownership branch still lives in main.rs. A process takes the run lock before creating a build engine, keeps the guard for the engine session, and treats a held live lock as an attach outcome rather than another owned engine. detach.rs also recognizes run.pid as the lock-backed owner record, so detached children and normal starts use the same ownership model.

This piece took the proven primitive and wired it into the one engine-starting path. The brief kept the decision narrow: a free or stale run becomes owned, while a live owner is handled at one named branch that could later change from refusal to attach. That made release behavior and detach reconciliation the sprint's hard parts without asking the worker to build the future transport. The gate stayed green, so the ownership boundary did not have to move.

Distinguish Live Runs

0 agents 6m 59s wall time 93k peak context 19.1k tokens out $3.31 nominal cost

The registry and command rendering still expose the live-owned versus orphaned distinction. Active runs with no live owner render as orphaned, while other run states keep their existing labels, and the MCP read surface reuses the same label function. That makes the ownership fact visible without turning read-only management commands into participants in locking.

This sprint was intentionally smaller and presentation-facing. It depended on the read-only ownership query but did not need to touch the acquisition path, so a memoryless agent could focus on registry state and command output. The brief's constraint was non-disturbance: list and status had to inspect the lock without taking it or mutating it. The run facts show this remained a clean boundary.

Milestone 2

Gantry Remote Attach

5 sprints 0 agents 1h 15m wall time 203k peak context $32.57 nominal cost

The attach transport is still a set of separate modules rather than a feature buried inside the engine. The protocol, publisher, client, lifecycle tests, command routing, and MCP bridge are present, and the documentation now treats the same subscribe-to-events and send-commands contract as groundwork for another front-end. No web UI was built by this job, matching the milestone scope.

This milestone built on the first milestone's live-owner branch instead of reopening ownership. Its sprints form a transport stack: protocol and endpoint, owner-side serving, remote client routing, lifecycle rules, then a second consumer through MCP. The only recorded stress landed where the owner had to serve the live stream and merge subscriber commands; that sprint had to be attempted again before continuing. After that, the remaining attach work stayed within the planned boundaries.

Define Protocol and Endpoint

0 agents 7m 58s wall time 122k peak context 34.6k tokens out $3.57 nominal cost

src/engine/attach.rs still defines the wire protocol and endpoint resolver. It uses a separate newline-delimited JSON message format for complete live events and commands, defines subscribe, welcome, event, and history-complete messages, and derives the attach socket path as a sibling of the run lock. The module comments still explain why the history journal is not the wire codec.

This sprint carried the transport contract before any socket server or client existed. The brief made that isolation important because the journal format was lossy while a live attacher needed the full event stream and command set. A fresh agent could concentrate on codec completeness, timestamp preservation, handshake messages, and a canonical endpoint resolver. Nothing else in run behavior changed at this boundary.

Serve Live Events

0 agents 20m 18s wall time 175k peak context 83.9k tokens out $7.53 nominal cost

src/engine/attach_serve.rs still contains the owner-side decorator. It wraps an engine with a hub, publishes the endpoint for the life of the owned run, mirrors events to the local front-end and subscribers, merges commands into the engine channel, and reclaims the socket when the run winds down. Its current comments describe the replay log and the join race that the sprint was asked to solve.

This was the sprint where the cut came under stress. The worker had to insert a publisher around a running engine, preserve the owner's local stream, fan out events, merge subscriber commands, and handle late joiners without gaps or duplicates. The first execute attempt failed, then the retry passed through the gates and review. That locates the transport's hard boundary on owner-side serving, not on the protocol definition.

Attach Remote Clients

0 agents 11m 16s wall time 174k peak context 43.1k tokens out $5.44 nominal cost

src/engine/attach_client.rs still implements the subscriber side as an Engine, and main.rs still routes a live-owned run into that client instead of starting another BuildEngine. The client consumes protocol markers internally, forwards replayed and live events through the normal front-end channel, and sends front-end commands back over the socket. Dead or idle paths remain owner paths rather than attach paths.

This sprint could lean on both earlier transport pieces. Its job was to make a remote run look like the same engine interface the TUI and headless modes already drive, then flip the existing live-owner branch from refusal to attach. That was a contained change because the previous milestone had already centralized the owner decision. The run facts show no gate or review stress after that routing was added.

Manage Client Lifecycle

0 agents 11m 52s wall time 203k peak context 41.5k tokens out $6.46 nominal cost

src/engine/attach_lifecycle.rs remains as the lifecycle proof. It wires the real run lock, served engine, and attach client together to show an attached front-end can disconnect while the owner keeps running, and that owner termination releases the lock and endpoint. The README prose introduced here has since been rewritten, but the lifecycle model is documented in the agent execution guide and reflected in TUI owner-versus-attacher controls.

Once front-ends could attach, this sprint separated leaving a view from stopping a run. The brief made lifecycle the whole unit: attached abort and quit should close only the client, while owner termination should still wind down the engine and release ownership. That scope was test-heavy but conceptually narrow, because the data path already existed. It passed without forcing a re-plan.

Integrate MCP Protocol

0 agents 12m 9s wall time 145k peak context 44.2k tokens out $5.87 nominal cost

The MCP bridge still reaches live runs over the attach transport. src/mcp/attach.rs wraps the attach client into an AttachedEngine, commands.rs attaches standalone gantry mcp when the registry reports a live owner, and non-live runs still fail run-control actions cleanly. The current docs describe the endpoint, history-then-live delivery, command channel, and owner-versus-attacher lifecycle as the contract another front-end would reuse.

The last sprint used the completed transport as a proof of generality. Instead of changing the engine, it had to make a separate gantry mcp process attach through the same client shape and document the protocol for a future front-end. That made the unit a consumer integration and documentation pass, with the hard transport semantics already settled. Its gate and review stayed green.