Making Sprint Counts Comparable Across Runs
Splitting a run's completion state from the sprint size a person actually scans for.
Gantry's `gantry list` and `gantry status` commands tell you how big each build is — how many sprints, the small chunks of work a run is broken into, it holds. But runs are not all laid out the same way: some are a flat list of sprints, some are grouped into milestones, and some kinds of run carry no sprint files at all. The size these commands printed was read off the very same value that tracked whether a run was finished, so the sizes did not line up across layouts — a milestone-grouped run reported a figure you could not compare against a flat one.
This job pulled the two questions apart. Whether a run is finished, partial, or still going keeps coming from the authoritative record Gantry already trusts for that. A new, display-only count answers the narrower thing a person scanning the list actually wants: how many sprints does this run stand for? It adds up the sprint progress across a milestone-grouped run, falls back to the top-level record for runs with no sprint files, and the list column that shows it is labeled SPRINTS.
Build
The cut put introducing the new count on one side and routing every command to it on the other; the foundation's review forced a re-plan, and the routing sprint is where the gate repeatedly went red.
The division was between minting the count and spending it. The first sprint added a display-only sprint count to every discovered run record while leaving the existing state-authoritative count exactly as it behaved. The briefs let that piece stand or fall on its own: its acceptance criteria are entirely about the record carrying both numbers with correct values across flat, milestone-grouped, and map/design layouts, checked by tests before any command reads it. Only once that count existed and was proven did the second sprint route `gantry list`, `gantry status`, the TUI run-picker, and the MCP responses through it. Ordering it this way meant the data model could be grounded against tests with no consumer leaning on it yet.
Both boundaries came under load. The review of the foundation sprint forced a re-plan of the work that remained, so the routing was redrawn before the second sprint opened. And the routing sprint is where the difficulty actually lived: its gate came back red more than once, drawing a fix and an investigation before the sprint was re-executed and passed. That is the boundary you would expect to bear weight — the split is only correct if every consumer picks the right side of it, the authoritative count for completion and the display count for size, and the tests that hold those two apart are what kept sending the sprint back.
Feature
Each discovered run now carries two counts — authoritative completion units and a display-only sprint size — and the list, the status output, the TUI picker, and the MCP responses each read the right one; the split still holds at HEAD.
Run size and completion used to be read off one and the same number. For a flat list of sprints that caused no trouble, but a milestone-grouped run, or a map/design run with no sprint files, printed a size that would not stand next to a flat run's, because the count driving state was never meant to answer the size question in the first place.
The design answers that with two named counts on each discovered run record. One holds authoritative completion, computed from the top-level record as before. The other, `display_sprints`, holds user-facing size, added up across built milestone directories and falling back to the top-level record for runs with no sprint files. Every place that shows run size was pointed at the display count — the list column, renamed from `UNITS` to `SPRINTS`; the "sprints done" line in status; the TUI registry run-picker — while every place that decides whether a run is empty, partial, or finished keeps reading the completion count. The MCP run JSON exposes both under `state_units` and `display_sprints`, keeping the older `sprints` key only as a backward-compatible alias rather than the contract new callers should reach for.
At today's HEAD the separation is still in place. The registry record carries `sprint_total` and `sprint_done` beside the completion counts; the MCP response emits `state_units` and `display_sprints` with the note steering new clients to them; the TUI still paints the `SPRINTS` header. Most of the lines the job introduced survive, spread across the registry, the command surface, the MCP layer, and their tests.