0.3.1
Gates and Checkpoints
Follow-up release to Words and Endings, rounding off some related work. It reworks how test runners are set up, how merges are sequenced and how stopped runs are resumed.
103 commits by Gantry runs
80% of the release
25 commits by non-Gantry sessions
20% of the release
Gantry 0.3.1 covers the three days from the 0.3.0 release to the 2026-08-23 cut: 128 commits in 14 units of work — 4 gantry runs contributing 103 commits, and 10 hand or interactive-agent sessions and standalone commits contributing the other 25.
Read this before upgrading. Every pattern file with a gate step needs an edit, and every run on disk from 0.3.0 or earlier needs a repair before it can be resumed. pattern_language_compatibility moves from "2" to "3": a gate step's command key is now required, and a pattern that omits it is refused at load. Resume was rebuilt on a durable interpreter position, so a run book written by 0.3.0 has no state/position-checkpoint.json and gantry resume refuses it until gantry repair <name> reconstructs one from the run's own records. A run still in flight is the harder case: it froze its own copy of the pattern closure under .gantry/<plan>/patterns/ when it started, so it must be finished on the binary that started it or have that copy edited by hand.
Gates are declared, not guessed. The engine no longer supplies a gate command. The path search for bin/gate, the sweep that ran every bin/gate under the run directory, and the hardcoded resume-admission path are all gone; a gate step names the command it runs, and an agent step can name the gate that will judge its work with judging_gate, which the loader checks against the gate step that follows. The bundled build pattern writes a run-scoped project gate from the new write-project-gate prompt and runs it on the clean baseline, after each milestone, at the end of the build and at the merge boundaries; milestone keeps its own scoped gate. Each gate run's test coverage is parsed from its output and reported per boundary in gantry status and about.md.
Resume is checkpoint restore. Every book commit writes state/position-checkpoint.json — the stack of open call and for invocations with each frame's pattern, step index, bound parameters, current task and output bindings, plus the step, session and loop counters. A resume resets the worktree to the run branch head, restores mutable orchestration state, discards uncommitted work after the last boundary, and executes the next step live. No recorded step outcome decides anything, so a run that stopped at a stop re-executes that step rather than stopping again on its own record; inside a for, tasks whose latest ledger row is DONE are still omitted, but the resumed invocation's other steps run again. Four further defects that wedged resumes were fixed by hand after the run: a for body's kind = "path" parameters bound file contents instead of paths, a second resume overwrote the first's gate.log, a resumed run reissued identities its journal had already recorded, and an on_plan_change handler left the frame naming the wrong pattern in every checkpoint taken after it.
A stopped merge no longer loses the home branch. A run that stopped with an integrate merge unresolved and then resumed could reset that merge into a commit whose tree equalled the run branch parent — deleting and reverting home-branch files committed while the run was out, with a green gate and a commit message describing conflicts the commit did not contain. A first headless resolver failure with MERGE_HEAD present is now resolved mechanically in the home branch's favour and the run continues, and the merge boundary commit and the final merge back refuse a result that drops a home-branch path or reproduces that tree shape.
Stops carry a machine-readable identity. StopReason gained stop_identity — a typed route, class, operator kind and merge sub-kind projected from the identity journal — so headless JSON and the MCP run status carry it, and both front ends decide PAUSED versus STOP from it rather than from stop prose. It arrived with a simulation framework in src/engine/simulation.rs that starts a real engine against real git with a mock agent and drives about 30 whole-run scenarios, each descending from a recorded incident.
Every gate is declared
A gate step names its own command, and the engine supplies none.
-
19 commits,
c50175decf8b6282c0runevery-gate-is-declaredreplayEvery gate step names its command, and build gates the project
A gantry run deleted the engine's gate-resolution fallbacks and made every gate explicit. A `gate` step's `command` key is now required and is the only way a gate script is chosen: the path search that guessed at `bin/gate` locations, the filesystem sweep that ran every `bin/gate` found under the run directory, and the hardcoded resume-admission path are gone, and a `gate` step without `command` is rejected at load. `agent` steps gain an optional `judging_gate` key naming the gate that will judge the step's work, and the loader rejects a pattern whose `agent` step and the `gate` step following it name different commands. The bundled `build` pattern now authors a run-scoped project gate from the new `write-project-gate` prompt — which instructs the agent to find the project's existing full check, such as `bin/test`, rather than compose a subset — and runs it on the clean baseline, after each milestone, at the end of the build, and at the merge boundaries, while `milestone` keeps its own scoped gate from `write-milestone-gate`; `map` declares no run-scoped gate and now resumes instead of stopping. Each gate run's test coverage is parsed best-effort from its output and reported per boundary in `gantry status` and `about.md`. `pattern_language_compatibility` moves from `"2"` to `"3"`, so a pattern authored against the old header must bump it and give every `gate` step a `command`.
Resume and checkpoints
Resume rebuilt on a durable interpreter position, and the defects that wedged one.
-
2 commits,
4e423650d67bfdf13fsessionA resume shows the cards the run had, not a narration of it
An interactive agent session removed the `Replayed <step>` cards a resumed run printed in the TUI, and the matching `↻ replayed <step>` lines in the headless log — one per step the interpreter skipped. A resume already rebuilds its feed by replaying `history.jsonl`, so every stage the prior sessions ran comes back as the card it was; the `RunEvent::PatternJournal` handlers were appending a second, differently worded card on top of that reconstruction, including for replayed steps that never had a card. The interpreter still journals the record to `pattern-events.jsonl`, which is the identity bookkeeping a resume reads.
-
7 commits,
b88e4a95be52470120sessionThree resume changes landed and were reverted the same day
Hand work landed three changes on the resume path — an `output` declaring whether a slot reads it or names it, a task the ledger does not record as done executing rather than replaying, and only a leaf task body answering for its own replay — and reverted all three within the day. The problem was taken up properly by the run that rebuilt resume on the position checkpoint, and the released binary carries that work, not this. The reverted `output` key is the one to know about: a pattern authored against it does not load.
-
30 commits,
20352dc9b91153315brunresume-is-checkpoint-restorereplayResume restores a saved position instead of replaying steps
A gantry run replaced resume's step replay with a durable interpreter position. Every book commit now writes `state/position-checkpoint.json`, holding the stack of open `call` and `for` invocations with each frame's pattern, step index, bound parameters, current task and output bindings, plus the step, session and loop counters. `gantry resume` resets the run worktree to the run branch head, restores mutable orchestration state from the book head, discards uncommitted work after the last boundary, reads the checkpoint and executes the next step live; no recorded step outcome decides anything, so a run that stopped at a `stop` re-executes that step instead of stopping again on its own record. Inside a `for`, tasks whose latest ledger row is `DONE` are still omitted, but the other steps of the resumed invocation run again, including a `command` that already ran. A run started before this has no checkpoint and refuses to resume until `gantry repair <name>` reconstructs one from its records, nested invocation stacks and re-resolved parameter bindings included; the same run also reverted the output-path inference that made `sprint`'s `attempt_ceiling` retry read a filename instead of a number.
-
5 commits,
3374c3676a6c74339fsessionFour defects that stopped or wedged a resume
An interactive agent session fixed four defects found by driving a multi-level pattern. `resolve_parameter_bindings` now honours a `for` body parameter's `kind = "path"`, so `milestone.toml` passing `{{gate_script}}` into its task loop gives `sprint.toml`'s `build_gate` the script's path rather than the script's own text — which `sh -c` had been running, so the same gate graded green one level up and red one level down. Each execution of a gate step now probes for the first free `gate.log`, `gate.2.log`, … under its step id, so a second resume no longer rewrites a committed record and fails its own book commit. `new_from_checkpoint` no longer reissues an identity the resumed run's journal has already recorded, which had two records overwriting each other's session or step log. And `run_nested_pattern_in_frame` now restores `frame.active_pattern` after an `on_plan_change` handler or a non-tail `then` target, so a checkpoint taken afterwards records the frame under the right pattern; checkpoints already written wrong are repaired on restore by `repair_restored_frame_patterns`.
Merges
The integrate merge keeps the home branch, and the drop checks stop walking the tree.
-
19 commits,
d9e3f8a30b6053434frunintegrate-never-drops-the-home-brareplayThe home branch wins the integrate merge, and is never dropped
A gantry run fixed the integrate merge that folds the home branch tip into the run branch. A run that stopped with that merge unresolved and then resumed could reset a conflicted merge into a commit whose tree equalled the run branch parent — deleting and reverting home-branch files committed while the run was out, with a green gate and a commit message describing conflicts the commit did not contain. A first resolver failure with `MERGE_HEAD` present is now resolved mechanically in the home branch's favour and the run continues; giving up requires a second failure or an absent `MERGE_HEAD`. The merge boundary commit and the final merge back refuse a result that drops a home-branch path the run branch did not remove, or that reproduces that tree shape; `reset_to` refuses to run while `MERGE_HEAD` is present, and a resume aborts an in-progress merge before touching the tree. The two resolver prompts moved into `resolve-merge.md` and `adjudicate-merge.md`, overridable per project and globally.
-
8f864a3bdcommitMerge drop checks read git's diff instead of walking the repository
An interactive agent session rewrote the three merge drop checks. They had enumerated every path in a parent tree and spent a `git rev-parse` per path — 33,201 paths in Gantry's own repository at roughly 2.1 ms each, about 70 seconds per validation and twice per finish, once of them in the shared main checkout. They now take their candidates from one `git diff` and keep their per-candidate blob checks unchanged, so the verdict is identical. Rename detection is off, because a renamed-away path would otherwise leave the deleted set the checks exist to catch.
Stops and simulation
A typed stop identity, and about 30 whole-run scenarios driven against a mock agent.
-
35 commits,
2a9b694331d5e8083brunrun-simulation-frameworkreplayMachine-readable stop identity, and 30 whole-run simulations
A gantry run built `src/engine/simulation.rs`, a data-driven framework that starts a real `RunEngine` against real git with a marker-keyed mock agent, and a corpus of about 30 scenarios covering green-path builds, the merge family, and the recorded resume and stop incidents. It also changed shipped behaviour: `StopReason` gained `stop_identity`, a typed `route` (`pattern`, `operator`, `engine`), `class` (`step`, `absence`, `plan`, `gate`, `record`, `merge`), `operator_kind` (`pause`, `abort`) and merge sub-kind, projected from the identity journal, so headless JSON and the MCP run status now carry `stop_identity` and both front ends decide PAUSED versus STOP from it instead of from stop prose. The four new vocabularies are described and exported through the reference dataset. `sprint.toml`'s `stop_handover` note now names `state/handover.md`, and a stop-evidence regression found during the run was fixed so a stop without its own facts still gets declared-output and summary evidence derived.
Cards and bundled patterns
What a card is named after, what a stop note says, and what the bundled patterns' comments explain.
-
2 commits,
29c6631afdd3833514sessionAn execute card is named by its task, not by the step running it
An interactive agent session changed what run cards are named after. `Stage::Execute` now takes the task's own title ahead of the step's authored title, because a pattern names a step once for every task that passes through it and that name cannot be as informative as the task at hand; execute is the only stage whose card is the task itself, so it is the only one reordered. The `write-milestones` and `write-sprints` prompts now require a planner's brief to open with a title line, so a task list no longer falls back to whatever section heading the brief happened to begin with. Gate steps in the bundled patterns declare the title `Testing` rather than a different question on each of ten identical gate cards.
-
92c2593f5commitA sprint retries when the recovery rung leaves no verdict
An interactive agent session changed `sprint.toml`. `troubleshoot`'s repaired-and-verified end state tells the rung to run the gate itself, see it exit 0 and write nothing else, so a rung reached `route_rung_verdict` with no `rung_verdict` exactly when its own gate run had disagreed with the pipeline's re-gate — a suite whose wall-clock varies across the same tree was enough — and that arm stopped the run. It now routes to `check_attempt_ceiling`, the same retry the rung would have asked for, with the attempt ceiling still bounding the recursion. `stop_rung_verdict_absent` had no other caller and is gone.
-
3 commits,
448cb255f4ab329fa8sessionThe bundled patterns' comments describe the step above them
An interactive agent session rewrote every comment block in the five bundled patterns: no history of a retired format, no inventory of what was dropped, no rationale defending a decision, and no key or claim that is not true of the file. `map.toml` and `until.toml` had carried four header lines each and no step commentary; they now carry one line per step like the other three. The `recovery_rung` position parameter is described without a ladder that does not exist in the engine. The shipped pattern snapshots and the reference dataset were regenerated against the rewritten files.
-
1e5314307commitA ceiling stop card names the value it rejected
An interactive agent session made `stop_invalid_attempt_ceiling` and `until`'s `stop_invalid_cycle_ceiling` interpolate the rejected value into their notes, so a filename where a number belongs is visible on the stop card rather than only in the state file the card told the operator to inspect. A bundled-pattern test now drives the production `World` end to end over a real directory and pins both halves of the Output Read Contract: a value-less output carried into a `call` parameter arrives as the file's text, and the same output in a gate step's typed `command` slot arrives as its path.
Maintenance
-
5a4717d6bcommitRun start reports only the defaults Gantry overwrote
An interactive agent session cut `Config::defaults_reconciliation_cards` to the one reconciliation outcome that asks something of the reader: `diverged`, where Gantry overwrote the operator's file. A refreshed pristine default, a removed withdrawn default and a kept fork are no longer announced at run start, and `DefaultsReconciliation` still records all five. `config.toml` left `MANAGED_DEFAULT_ROOTS`: it is the operator's own settings file, which `gantry config harness` writes and Gantry ships no default for, so every operator's copy was reported at the start of every run as an edited copy of a default this version no longer ships. Card prose now suppresses a path only in object position after a preposition, so a notice whose subject is a path still names it.
-
2 commits,
9199f0e7cfb472a16dsessionInteractive blocker investigations skip permission prompts
An interactive agent session made the blocker investigation launch Claude Code and OpenCode with `--dangerously-skip-permissions`. The investigation opens in the run worktree, where every stage agent has already run under that flag, so the investigator was stopping at prompts for access its predecessors had. `GANTRY_CLAUDE_ARGS` and `GANTRY_OPENCODE_ARGS` still come after the flag and can override it.