skip to content
Jobs

Teaching the Merge Resolver to Prove It Dropped Nothing

The finish step now checks a resolution actually carried main's changes, and repairs the ones it didn't.

Gantry milestones
The finish step now checks a resolution actually carried main's changes, and repairs the ones it didn't.

Every Gantry run ends by folding the latest main branch back into the work it just built. When the two lines of change touch the same file, something has to resolve the conflict — and the lazy resolution is to take one side and discard the other. If it discards main's side, a real change that landed while the run was busy vanishes, and the merge still looks clean, so nothing catches it. That had already happened once: a layout change on main was silently dropped by a conflict resolution and had to be reinstated by hand.

This job closed that hole. After a resolution commits, Gantry now computes exactly what main changed while the run drifted and checks, against the actual resolved files, whether each of those changes survived. A change that is present is dismissed. A change that is missing from a region the run never touched is put back automatically — it can only have been dropped carelessly. A change that is missing from a region the run also rewrote is genuinely ambiguous, so a fresh agent is asked to reconcile the two intentions. A person is involved only in the one case that was already unresolvable. Alongside the check, every merge commit and a new event now list which of main's commits the merge folded in, so a dropped change is visible in the history rather than invisible.

Build

The self-heal was cut into a standalone audit event, a set of pure git primitives, the classifier and its automatic repair, and last the agent that handles the ambiguous residue — each tier resting on tested ground below it, and the whole run stayed green.

The milestone drew its first line between making the merge *legible* and making it *correct*. The audit concern — listing main's folded commits in the commit body and emitting a reconciliation event — is useful on every integration and needs no classifier to exist, so it was built first and shaped to hold the per-class counts a later tier would fill in. That let the event's shape and its plumbing through both front-ends be settled and tested while it still reported zeros, so the classifier sprint only had to supply numbers rather than thread a new event through every place that renders one.

The correctness half was then split by what kind of thing each piece is. The git primitives — diff main's range hunk by hunk, ask whether the run's own delta overlapped a hunk, ask whether a hunk's lines survived in the resolved files, re-apply a single hunk as a clean patch — are pure functions over constructed git fixtures, so they were delivered and unit-tested with no orchestration calling them yet. Only then did the classifier sprint wire them into the finish step as decision logic, and only then did the final sprint add the one agent stage, for the class that cannot be judged mechanically. That ordering meant the hardest piece — an agent reasoning about two histories that both rewrote the same region — sat on top of two tiers that were already proven, and it reused the existing resolve stage rather than inventing a new one.

The run never went red: no gate sent a sprint back, nothing was retried, no review forced a re-plan. The evidence that the cut fit is in what each sprint could be tested against alone — the primitives against fixtures with no classifier, the class-(b) auto-repair against the exact shape of the original dropped-layout regression, the preserved halt against a tree repair could not make green — so the difficulty was decomposed into pieces a fresh agent could prove one at a time rather than concentrated where it could stall.

Feature

Gantry's finish step gained a ground-truth check on merges: it classifies what main changed against the resolved files, deterministically restores unambiguous drops, hands the ambiguous ones to a repair agent, and adds no new reason to stop for a human.

Before this, the end of a run trusted the resolver's output as-is. A resolution that quietly dropped main's side produced a tree that compiled, passed, and merged — the regression only surfaced later when someone noticed a change had gone missing. There was no step that asked whether the merged result actually contained what main had contributed.

Now there is. The finish step computes main's delta over the range it advanced during the run and walks it hunk by hunk against the resolved files. Present hunks are the fast common case and cost nothing. A hunk absent from a region the run never rewrote is re-applied as a clean patch with no agent; if the patch will not apply, it is demoted to the ambiguous class rather than escalated. A hunk absent from a region the run also rewrote is handed to a clean-context adjudicator-repairer, told exactly what main changed there and that the tree contains none of it, and asked to re-apply main's intent or confirm the rewrite legitimately superseded it. Every repair feeds back into the same existing gate, and the operator is reached only on the one irreconcilable case that already stopped the run before — the human-halt surface did not grow.

Standing in the tree today, the design is intact. The reconciliation event and its per-class counts are in the domain types; the classifier, its deterministic repair, and the residue handoff live in the finish step; the four git primitives are methods on the worktree, and the adjudicator reuses the resolve stage exactly as the plan proposed. The tests around all of it have been reworked heavily since — much of the original test scaffolding has been rewritten or replaced as the suite evolved — but the capability itself remains, with the bulk of what the job introduced still present.

unavailable sessions
unavailable context
70,618 in / 308,160 out tokens
$45.18 cost
0 x 4 milestones x sprints
unavailable edits
unavailable commands
1h 30m duration
unavailable roles
0 x 0 fixes x replans
- harness