The PR Train and the PR Resolver

A merge train for parallel coding agents. When several sessions finish within minutes of each other, every merge makes every other branch stale. The train gathers the pull requests that are ready, checks them together in one run, and moves main once.

Coming

The train for your repositories. Today it lands Liquidsoft’s own, onMain’s and this site’s.

Pull requests force-pushed (rebased) at least once
before34%
after16%
checks.yml runs per pull request
before2.02
after1.42
Ejections per 100 pull requests assembled
before13.6
after8.9
Pull requests marked ready more than once
before18%
after9%
Median minutes from ready to landed
before26
after55

Measured on liquidsoftio/onmain, onMain’s own repository, by scripts/train_metrics.py: 50 pull requests from 25 September 2026 to 27 September 2026 before, and 76 from 27 September 2026 to 1 October 2026 after. Before and after are either side of the gather window and the conflict work that landed with it (onMain ADR-074), not hand merging against the train: merging by hand over the 3.2 days before the first train run force-pushed 34% of pull requests too. What the train adds over that is that main only moves to a commit the whole suite passed. Ejections are counted per pull request assembled into a batch, so one assembled twice counts twice; “marked ready more than once” includes the train’s own relabel after it halves a batch.

Agents made writing the change fast. Landing it became the queue.

Building onMain with three to six sessions at once, every merge made the other branches stale, and every rebase started another full check run. Over about 26 hours on 26 and 27 September 2026, with the train landing one pull request at a time, each landed plan cost about 500 billable CI minutes (onMain ADR-074).

A session marks its work ready and walks away.

  1. Hand offA session whose pull request is green labels it ready and ends. Nothing waits on the landing.
  2. GatherThe train starts when dispatched, or on its schedule, and waits until no new pull request has been marked ready for five minutes, twenty at most.
  3. AssembleIt merges each ready pull request onto main in number order. One that conflicts is ejected with its files named; the rest carry on.
  4. Verify onceThe whole suite runs one time on the assembled batch. If it fails, the batch is halved and tried again, and only a pull request that fails on its own is ejected.
  5. Landmain moves to the commit that passed, and the train comments that commit on every pull request it landed.

main only moves to a commit the whole suite passed.

The train will not move main to a commit that was not checked, and it will not overwrite main if it moved during the run.

Coming

The PR Resolver

An ejected pull request should not need a person.

When the train ejects a pull request for a conflict, the Resolver tries a plain rebase onto the new main. If that is clean, it marks the pull request ready again itself. That much runs on Liquidsoft’s own repository, and it has not yet carried a pull request through to landing.

For a real conflict, it will propose a resolution from both sides and the pull request’s own description, checked like any other commit. A person marks a proposed resolution ready.

Conflict resolution has not run in production yet. On a test corpus of 15 conflicts it resolved 14 in each of two runs, and none wrongly (onMain ADR-120, corpus 141e4321, one model, 30 September 2026).

What people ask first.

How is this different from GitHub’s merge queue?

The guarantee is the same: GitHub’s merge queue also checks each pull request together with the ones ahead of it, and merges only what passed. Two things differ. GitHub offers its merge queue on public repositories owned by an organization and on private ones under GitHub Enterprise Cloud (GitHub’s documentation, “Managing a merge queue”, read 1 October 2026); Liquidsoft’s repositories are private on GitHub’s Team plan, where the train runs as a GitHub Action. And the merge queue builds each queued pull request in its own group, while the train checks a whole batch in one run: fewer CI runs when a batch passes, and more runs, one after another, to narrow a failure down.

Does landing deploy?

Now it does: a green landing fast-forwards `main`, and the train dispatches this site’s deploy, so what landed is live without a separate hand-dispatch. A rollback stays a person’s decision, and a load-balancer change is applied by a person before the deploy that needs it.

Landing takes longer. Is that a problem?

The gather window adds minutes: the median went from 26 to 55. Nobody waits on it, because the session ended when it handed off.

What happens to a pull request that keeps conflicting?

It stays out of the batch with its conflicting files named, and the rest lands without it.