skip to content
Jobs

Merge Finalization That Recovers Itself

One finish path, a truthful success check, and resume that converges without hand cleanup.

Gantry milestones
One finish path, a truthful success check, and resume that converges without hand cleanup.

A Gantry run ends by merging its work back into the shared main branch. The old finish decided whether that merge had succeeded by asking whether the whole main checkout was clean — and that question could lie in both directions. An unrelated file left sitting in the checkout, nothing to do with the merge, could make a merge that had actually landed look like a failure, stranding the run in a state where the work was on main but the run thought it had not finished. And a fast pre-check that guessed a merge would apply cleanly could steer past the step that reconciles genuine conflicts, so a real clash slipped through.

This job rebuilt the finish so it judges the merge by what actually happened to it: the run's branch is contained in main and no real half-applied merge leftovers remain, with unrelated files ignored. Every finish — the live one that a running job takes, and the offline `gantry merge` a person runs by hand — goes through one path, and a run interrupted partway through finishing can be resumed and complete on its own rather than needing someone to clean up by hand.

Build

The build put one definition of a landed merge first and routed every finish path through it; the two gates that went red were the paths that had to compose that definition rather than state it.

The foundation piece defined the success condition and nothing else: a merge counts as landed when the branch is contained in main and no genuine half-applied merge leftovers remain, and unrelated untracked files in the shared checkout are explicitly not evidence of failure. Landing that as its own sprint is what let everything after it be written against a single shared meaning of "the merge succeeded" instead of each path re-deriving one — and it let that correctness property be tested against the offline partial-merge path before any of the path-unification work existed.

The split that made the rest agent-sized was defining-the-condition versus applying-it. Collapsing the live finish onto one integrate-then-reconcile-then-land path — where the pre-check becomes a hint that can never suppress the conflict resolver once real conflict markers appear — is behaviour that only means anything once the postcondition exists to accept or reject its result. Sequenced that way, each piece stood on a proven one below it.

The stress landed on exactly the two sprints that had to compose the earlier pieces rather than introduce new semantics. Resume self-healing — the highest-effort piece, which must clear stale in-worktree merge leftovers, retry a fresh integration, and recognise an already-landed branch as finished instead of attempting an empty re-merge — failed its gate once and was repaired before landing. The offline-merge audit, whose entire job is to keep the shared helpers from drifting back toward whole-checkout cleanliness while keeping the offline path report-and-stop with no resolver, came back around and its gate went red before it landed too. The difficulty lived where behaviour had to reuse the shared definition without letting it drift, not where the definition was written. No boundary forced a re-plan, and the final cleanup-and-docs pass stayed inside the original cut.

Feature

Before this job, Gantry's finish trusted git's opinion of a clean checkout; now one path proves the merge landed and left no real residue, and a resumed run heals itself instead of stranding work on main.

Before this job the finish could not be trusted at its own edges. A whole-checkout cleanliness check meant an unrelated untracked file could reclassify a landed merge as failed and leave a split-brain state, and a probe that predicted a clean merge could bypass the resolver when a real conflict actually appeared. The repair replaced the success check with a merge-scoped one, unified the two live paths into a single integrate-then-resolve-then-land sequence, taught resume to converge on its own, and held the offline `gantry merge` path to report-and-stop with no resolver.

Standing in the tree today, the design holds. The success check lives in `main_has_no_merge_residue` in `worktree.rs`, which looks only for an in-progress `MERGE_HEAD` and unmerged index entries and carries a comment stating it is intentionally not a whole-checkout check — unrelated edits are not evidence the merge failed. The finish path in `finish.rs` treats the probe as a hint only and routes real conflict markers to the resolver, with the branch-contained-and-no-residue predicate as its terminal condition. The superseded resolver-only plan is gone from the active plan set, and the earlier false-failure handover now sits under `docs/archive` as a design record, exactly the disposition the plan asked for.

Most of the lines this job introduced are still present at `HEAD`, and where the finish code has moved on it has grown rather than been replaced: a later residue adjudicator now sits on top of the same resolver path and reuses this job's postcondition and single-path guard rather than standing up a second implementation. The plan document that drove the work has since been tidied away, which is housekeeping, not a reversal — the invariant it asked for is what the code now describes.

14 sessions
peak 157,765 · median execute 100,530 · heaviest 157,765 (03) context
unavailable tokens
unavailable cost
0 x 5 milestones x sprints
45 edits
355 commands
1h 7m duration
6 execute · 6 review · 2 plan · 2 fix · 1 gate-build roles
2 x 0 fixes x replans
codex harness