As built · gates passing · 2026-07-07

Drover: The Architecture

A guided walk through the system as it exists — following one worker run from spawn to sealed trace, with the real transcripts at each proven stop.

Everything marked with a transcript ran today against live agents. The tour order is the lifecycle order: it is also, not coincidentally, the order the data flows. Companion pages: the plan as approved (what we committed to) and the rolling status page.

Stop 0 of 7 · the whole machine in one figure — then we walk it

Overview: three actors, one trace

Three things exist at runtime. The worker is an unmodified harness (Claude Code today; Codex and Gemini on the observe-only lane) running a task inside a disposable container. The watcher is a path watch process injected into that same container, deriving the worker's session into Toolpath steps as they stabilize and streaming every emission out over the exec pipe. The orchestrator is drover on the host: it consumes the stream, checkpoints the sandbox on every step, and holds the fork, revert, resume, and select verbs. The trace is the contract between all three — the same document format whether it is growing live, being forked, or being read back next month.

Figure A · As-built topology — solid is running code, dashed is the phase-3 control lane
Host — drover (Python) ~/code/empathic/drover · arena as path dep trace.LiveTrace ✓ — stop 2 consumes the event stream; keeps steps/head/provisional; mirrors a host-side .path.jsonl; seals on agent completion scopes.Scope ✓ — stops 1, 3, 4, 6 start/attach · checkpoint(step) · revert(step) · fork(step) → sibling Scope · diff/select/discard over held changes control/ + meta/ — phases 3–4 inject via stream-json stdin · hook gate · 5s tick loop sidecar: snapshots.json ✓ step_id → {backend, ref}; merged into the sealed doc Sandbox — one per worker Docker, disposable, arena image + auth staging worker: harness CLI claude -p … · unmodified, unaware of drover watcher: path watch ✓ (injected binary) derives session → steps; append-only .path.jsonl $HOME session store + /work repo the worker's whole world, all inside the snapshot domain /drover-snap ✓ — shadow work.git + session tars invisible to the worker's own git, by construction event stream ✓ docker exec stdout checkpoint / restore ✓ inject · intent gate phase 3 Format layer (Rust, toolpath workspace, branch live/trace-lane) — the contract all three actors share toolpath-live ✓ path watch ✓ path fork ✓ projectors (resume, cross-harness) queries: dead ends, ancestors → Pathbase zero schema changes were needed: the JSONL append semantics, the fork-shaped base URI, and the projectors were already in the format
Stop 1 of 7 · a worker's world is created — and made snapshot-ready before the agent wakes

Spawn

Scope.start() creates the container from arena's image, forwards harness auth the way arena's runner does (Claude's OAuth token travels as an environment variable from the host keychain, so no credential ever lands in a file drover touches), injects the Linux path binary, and initializes /drover-snap: a bare shadow git repository whose work tree is /work, plus a directory for session-store tars. Scope.attach() does the same to a container arena already started, which is how the gates wrap live arena runs.

The design rule set here governs everything downstream: the worker must not be able to tell it is being observed or snapshotted. The shadow repo has its own GIT_DIR, so the agent's own git log, index, and git status never change when a checkpoint lands — an agent that saw phantom commits appear mid-run would reasonably start debugging its own repository. The container tests verify this invariant directly.

Stop 2 of 7 · the run becomes a Toolpath document while it happens

Observe: the live trace lane

The watcher polls the session store and re-derives the full path on every poll with the same derive_path the import lane uses — live output cannot drift from post-hoc output because there is no second derivation to drift. Steps are withheld while they can still change (tool results attach to the trailing assistant turn; event steps re-parent as the session grows) and appended once stable, so the .path.jsonl is a valid document at every line boundary. Every emission — file lines verbatim, plus provisional tail steps — goes out on stdout, and drover's LiveTrace mirrors the file lines into a host-side copy.

Two contracts came out of the real runs rather than the plan. First, the transport is the exec pipe, not a shared filesystem: the watcher runs inside the sandbox, which behaves identically on remote VMs and ends every cross-VM coherence question. Second, sealing is completion-triggered: the orchestrator spawned the worker, so the orchestrator knows when it finished; inactivity is not a proxy for completion, because one long generation outlasts any reasonable idle window. The idle timeout survives only as a fallback against orphaned watchers.

Real transcript · the phase-1 gate — live observation equals the archival import
tests/manual_live_arena.py — run 20260707T152443
$ GATE_LIVE_MOUNT=1 ARENA_PATH_BIN=…/toolpath/target/debug/path \
    uv run python tests/manual_live_arena.py

[gate] session visible in container: 5add62b9-8952-4aea-a4a1-9243954c6cd4
[arena] run …7de017c2: verifier=PASS toolpath=ok steps=35 cost=$0.096
[gate] sealed: steps=35 head=3614750a-ab64-4da1-83fc-3b551bd0fa6b
OK: live-watched (in-container) == harvested import (35 steps)

The invariant this gate enforces is the foundation of everything else: a trace observed live is the same document a forensic import would produce later, byte for byte. It also holds in three cheaper forms — a Rust integration test replaying a captured fixture, a CLI smoke test, and the Python client's pytest.

Stop 3 of 7 · every step gets a restore point, keyed by the trace itself

Checkpoint

On every live Step event, the orchestrator calls scope.checkpoint(step_id): a shadow commit of /work plus a tar of the session store, recorded in a sidecar keyed by the Toolpath step id and merged into the sealed document as step.meta.extra.snapshot. The step id is the join key across the whole system — the trace names the moment, the checkpoint names the state, and fork takes both.

One rule the fork gate encodes: checkpoints taken during the seal flush are discarded. At seal, withheld steps flush at once; snapshotting them would stamp final-state filesystems with mid-run step ids — a quiet lie that would surface later as a fork restoring the wrong world. Only live-streamed steps earn checkpoints. The git-shadow backend is the portable default everywhere, including Prime Intellect (whose gVisor blocks OverlayFS); the OverlayFS backend remains ahead for full-rootfs tasks and the paper-parity latency table.

Stop 4 of 7 · the trace becomes a tree; the sandbox follows it

Fork and revert

Reverting is the small case: shadow-checkout the commit, clean what came after, untar the session store — the workdir digest afterwards equals the checkpoint's exactly, and the agent's own git never notices. Forking is the interesting case, and it happens at two layers at once. At the format layer, path fork truncates the document at the chosen step and rebases it: the new path keeps exactly the fork point's ancestry, heads at the fork step, and records lineage as base.uri = "toolpath:parent/step" — a representation the RFC had reserved before this project existed. At the sandbox layer, a sibling container is seeded from the shadow commit via git archive, and the native session JSONL is truncated after the fork step's line.

That native truncation is the same-harness fast lane: Claude Code names its JSONL entries by uuid, Toolpath preserves those uuids as step ids, so cutting the file after the fork step's uuid reconstructs the byte-exact prefix without any projection. Cross-harness forks — continue a Claude run in Codex — take the path fork + projector route instead.

Figure B · What a forked run looks like as a document
s1 s2 s3 prompt fork point checkpoint ✓ wrong turn parent head · FAIL stays in the parent doc as history s3′ s4′ child continues in its own doc child head base.uri = toolpath:parent/s2 Two documents, one lineage: the parent keeps its whole history including the failure; the child starts at the fork step and records where it came from. Both are valid, shareable, queryable paths.
Stop 5 of 7 · the economics: nothing before the fork point is paid for twice

Resume

The forked child resumes with plain claude --resume against its truncated session. Because the kept prefix is byte-identical to what the parent already sent, the provider's prompt cache recognizes it — the model's context is warm without reprocessing a single early step. This is the property the whole design funnels toward, and it is now a measured fact rather than a design hope:

Real transcript · the phase-2 gate — fork mid-run, resume with a cache hit
tests/manual_fork_resume.py — run 20260707T153743
$ uv run python tests/manual_fork_resume.py

[gate] session 255f2c2d-3e7c-4b85-a988-040c966fd77c in arena-20260707T153743-…
[gate] sealed 24 steps; 14 live checkpoints
[gate] forking at cdba5afe-1394-4366-ac28-c6c2809f0977
[gate] resume rc=0 result='Done! All tests pass. The slugify function correctly:
       - Lowercases text and strips accents using Unicode decomposition -'
[gate] cache_read_input_tokens on resumed turns: max=29921
OK: forked at cdba5afe…, resumed with cache_read=29921 tokens

A live worker, checkpointed on every streamed step, forked at a mid-run user turn, resumed in a sibling container — and 29,921 tokens of prefix served from cache. This is the tweet's step-8/step-10 rescue, executed: the child kept everything the parent had learned up to the fork and paid only for the new suffix.

Stop 6 of 7 · nothing the worker did touches anything until someone says so

Land or drop

A run's output is a proposal, not a side effect. The worker's changes exist as a diff against its baseline commit inside its own sandbox; Scope.diff() reads it, select() writes it out for application, and discard() throws the container away. Between held proposals and per-step checkpoints, the destructive verbs in this system all operate on copies — the only irreversible acts are the ones the worker performs against the outside world, which is exactly the class the phase-3 intent gate exists to catch before they fire.

Stop 7 of 7 · what this machine still grows: hands, judgment, and receipts

Ahead: the control tower

The system so far has eyes (the live lane) and a time machine (scopes). What it grows next, in order: hands — the phase-3 control lane, where mid-run injection rides the worker's stdin, a PreToolUse hook turns every tool intent into an allow/deny decision, and denials land in the trace as dead-end steps; judgment — the phase-4 meta-agent tick loop and the phase-5/6 applications (compression first: its every mechanical dependency passed today, and it produces the demo artifact — a failed run forked, hinted, and passing in fewer steps); and receipts — the phase-7 Tree-GRPO leg feeding prime-rl, and the phase-8 benchmark tables lined up against the paper's.

Component inventory — where everything lives
ComponentWhereStatusEvidence
toolpath-live (streaming derivation)toolpath · live/trace-lane · 911eaf9SHIPPED7 unit tests + fixture-replay roundtrip test
path watch / path forktoolpath · live/trace-lane · 911eaf9, a601e2cSHIPPEDCLI smoke gate; 4 fork tests incl. schema validation
arena --live (binary injection, session-dir mount)arena · live/trace-lane · f87e8b3SHIPPEDdrives both real-agent gates
drover trace.LiveTrace (local + exec transports)drover · main · 44c025bSHIPPEDpytest + real-run gate: live == import, 35/35
drover scopes.Scope (checkpoint/revert/fork/select)drover · main · c1e8563, b1d3adcSHIPPED4 container tests + fork-resume gate: cache_read 29,921
Snapshot refs merged into sealed docs; OverlayFS backenddrover / toolpathPHASE 2 TAIL
control/ (inject, hook gate, handoff, discard)droverPHASE 3gate defined: denied canary intent → dead-end step
meta/ + apps/ (supervisor, compress, CRO, Tree-GRPO)droverPHASES 4–7bars set by the paper's numbers
What the build taught us — carried forward as law
  1. Completion seals; inactivity never does.

    A long generation outlasts any idle window. The orchestrator knows when its worker exited; that is the seal signal, with idle demoted to an orphan-cleanup fallback.

  2. Observe through pipes, not shared filesystems.

    The watcher lives in the sandbox and streams out over exec stdout. Identical on laptops and remote VMs, and immune to cross-VM cache coherence.

  3. Snapshots must be invisible to the observed.

    Separate GIT_DIR, untouched agent index — verified by test, because an agent that sees phantom commits will debug its own repo.

  4. Seal-flush checkpoints are lies; discard them.

    Only live-streamed steps earn snapshots — flushed steps would pin final state under mid-run ids and forks would restore the wrong world.

  5. Equality gates pin one binary.

    Installed path 0.14.0 backfills empty preamble timestamps that repo main preserves faithfully (known upstream bug, fix in flight elsewhere); both lanes of any comparison run the same build.

Decided at approval
Standalone repo; full Tree-GRPO leg including a funded training run; replicate the paper's benchmarks.
Decided by the build
Exec-pipe transport; completion-triggered sealing; native truncation as the same-harness fork lane; git-shadow as the universal default backend.
Still open — worth your push-back
Whether OverlayFS can slide or be cut; when Codex/Gemini get real inject; denied-intents-as-dead-ends becomes format reality in phase 3 — last cheap moment to object.
The next artifact

One meta-agent call separates today's machinery from the first demo: read a failed trace, emit {fork_step, hint}, rerun through the proven fork-resume loop, and pass the verifier in fewer steps — one Toolpath graph whose dead end is the original attempt. The mock on the plan page becomes a transcript here.