Usage Sidecars Written Beside Every Stage
Capture per-turn context usage as each stage finishes, so a run carries its own numbers
Gantry reports how much context each agent stage burned — the peak token count a stage reached, per harness, across a run. That number used to be reconstructed after the fact: the analyzer went looking for an external store the harness happened to keep, correlated it back to the stage by hand, and fell back to an estimate when it found nothing. That works only as long as the outside store still exists and can be matched, which is fragile for anything but the harness whose usage already sits in Gantry's own logs.
This job made a run carry its own usage numbers. As each counted stage finishes, Gantry now writes a small file beside that stage's transcript — a self-describing header plus, when the harness exposes it, one record per turn. The format is the same shape whatever agent produced the work, so a Codex stage, an Opencode stage, a Gemini stage and a Claude stage all leave the same kind of artifact. When a harness only exposes a running total and no per-turn detail, the file says so plainly instead of inventing precision. Analysis then reads that file first and reaches for the old external-store correlation only for runs recorded before this existed.
Build
One shared usage format was pinned before any harness-specific extractor was written, so four extractors could be built as independent leaf work — and the contract sprint itself was the piece a review sent back for re-planning.
The ordering that made this agent-sized puts the shared data model first and everything that depends on it after. The opening sprint defines the sidecar format alone — header fields, the per-turn record, the JSONL type discriminator, what a degraded capture must still carry — with no extractor in existence to consume it. Only then do four sprints each teach one harness to fill that format: the Claude inline path as the reference definition of context size, the Codex rollout extractor, the Opencode session extractor, and the Gemini path that falls back to an aggregate-only file when no per-turn signal exists. Freezing the format before the extractors meant a wrong field cost one rewrite of the contract, not four reworks of extractors already built against it — and each extractor could be proven against synthetic data on its own.
That is also where the difficulty concentrated, and the run shows it at the boundary you would expect. The contract sprint is the one whose review forced a re-plan of the remaining work — the shared shape that all four extractors lean on was the piece redrawn under load, which is the honest place for a re-plan to land in a contract-first cut. It was also sent back once by the gate before it settled.
The extractors that fought back were the ones capturing a store Gantry had no example of. The Opencode sprint — the one brief that had to validate real session-id behaviour against a throwaway run because the fleet held no Opencode data to test against — went red at the gate and was fixed. The sprint that wires sidecars into the analyzer as its first-choice source failed its post-review gate, when the merge re-ran the suite, and was repaired. The Claude reference, the Codex extractor, the Gemini degraded path and the closing documentation pass all landed without a red gate. The cut held: the four extractors never had to reconcile with each other, only with the format the first sprint had fixed.
Feature
A run's context-usage numbers used to be reconstructed from an external store after the fact; now each stage writes its own normalized usage file at finalization, and analysis prefers it — a design that still stands at HEAD.
Before this job, the peak-context number for a stage came from whatever the harness left lying around outside Gantry, matched back to the stage by correlation, or an estimate when nothing matched. Only the harness whose per-turn usage already lived in Gantry's logs was self-contained; the rest depended on an external store still being there and still being matchable when analysis ran.
What the job left is one module defining the normalized sidecar — a header line carrying harness, stage, peak context tokens, source tag, correlating ids and a degradation note, followed by per-turn records — and a finalization hook that writes it beside the transcript only for the stages the analysis counts. The hook treats capture as telemetry that can never fail an otherwise-good stage: when an extractor returns nothing, it writes a header-only file with a note rather than erroring. Each of the four harnesses supplies its own series through that one hook. The analyzer reads the sidecar's header peak first, accepts only the landed header/turn type discriminator, and drops to the prior per-harness correlation only when the sidecar is absent or unusable, so historical runs analyze as they did before.
Standing in the tree at today's HEAD, the design is intact: the sidecar module, the finalization hook in the harness layer, all four harness extractors, and the sidecar-first branch of the context scanner are present and still carry most of the lines the job introduced. What is gone is housekeeping — the two plan documents the run worked from were tidied away after it finished, and the harness reference and Opencode docs it updated remain. The scanner still names the header/turn contract as its primary source and keeps the external-rollout path only as the historical fallback the closing sprint documented.