Merge conflicts
How the factory resolves a pull request's merge conflicts on its own, in up to five checked attempts, and how a person resolves them in the browser with the conflict editor.
There are two ways to resolve a merge conflict: let the belai:conflict-resolver agent do it, or do it yourself in the conflict editor.
The conflict card
A pull request with conflicts gets a card labelled merge-conflict, assigned to belai:conflict-resolver. One card is open per pull request at a time. The card is filed:
- by the
conflict-fixdefault workflow, on a push to the default branch and when a pull request is opened, synchronized or reopened - by a person, with Resolve conflicts on Checkpoints or on a Pix Sandbox’s Pull requests tab
A worker on any machine with the repository can claim it.
How the agent resolves it
Belai runs the loop itself, so its promises hold whatever the model does. For each attempt, up to 5:
- Fetch the pull request’s head and base. A fork’s head is fetched as
refs/pull/N/head. - Make a fresh worktree at the head commit, on the card’s own branch.
- On the first attempt, run the build and linters on the head first. A build that already fails there, and lint findings already there, are not the merge’s fault.
- Merge the base in, with conflict markers in diff3 style. Recorded resolutions on the machine are never reused.
- Plan a strategy for each hunk: keep both sides, the current side (the pull request’s), the incoming side (the base’s), or a mix. Each attempt is told what earlier attempts chose and how they failed, and may not repeat one.
- Let the model resolve the hunks in one turn. It may not commit, merge, rebase, reset, stash, switch branches or publish.
- Check the result, in this order, stopping at the first failure:
| Check | Fails when |
|---|---|
| Markers | A conflict marker is left |
| Duplicate imports | A file imports something twice that neither side did |
| Build | A detected build check (go build, cargo build, dotnet build, Gradle, Maven, tsc, an npm build script) exits non-zero |
| Lint | A linter reports more findings than on the head |
| Tests | A detected test suite fails |
- On a pass, commit the merge and push it. On a failure, record the attempt and start the next one from a clean worktree.
A check that a permission rule stops is not a failed attempt. The card is blocked naming the check, because only a person can change the rule. A check whose tool is not installed is skipped and named in the comment as not run.
Pushing
A passing resolution is pushed to the pull request’s head branch as a fast-forward. It never force-pushes. For a pull request from a fork, Belai does not push to the fork: it pushes the attempt’s branch to origin (belai/K-xxxxxx/aN) and says so in a comment on the pull request. A card filed for a fork carries checkpoint:fork_code, so no host runs the fork’s code until a person clears it on Checkpoints.
When nothing passes
After 5 failed attempts the card gets the note conflicts unresolved after 5 attempts, moves to blocked and is labelled checkpoint:conflicts-unresolved, so no worker takes it until a person decides. Sending it back to the backlog starts a new round.
The ledger
Each attempt adds one note to the card, written by Belai from identifiers only, never model text:
conflict attempt 2/5 3fa1c09e: api/handler.go=both, go.mod=incoming -> build failed (build-go)
The card drawer and the Checkpoints inspector show these notes as a table: “Attempt 3 of 5”, “Resolved on attempt 2”.
The conflict editor
The conflict editor (/resolve/factory-conflicts?repo=owner/name&pr=N) resolves conflicts in your browser, the way GitHub’s own resolver does, and commits the merge as you, signed with your key.
- The server merges the base into the pull request (diff3). Changes that do not overlap are already in. Each overlapping block shows between
<<<<<<<(labelled with the head branch),=======and>>>>>>>(labelled with the base branch). - For each block, accept the current change (the pull request’s), the incoming change (the base’s), or both, or edit the file.
- Mark as resolved once no marker is left in the file.
- When every file is resolved, Commit merge. The dialog shows who you will commit as and how it will be signed. See Commit signing.
Lockfiles are never offered for editing. The base’s copy goes into the commit, and nothing regenerates it, so regenerate it locally or send the pull request to Pix. A file the editor cannot merge as text, a fork, a pull request too large, or one with no merge base is named, with the local git commands to use instead.
If the pull request’s head or base moved while you worked, nothing is written (head_moved, base_moved). Reloading keeps the files whose conflict did not change.
API
| Method | Path |
|---|---|
| POST | /v1/factory/checkpoints/conflicts with {repo, number}: file the card, or return the open one |
| GET | /v1/pulls/{owner}/{name}/{number}/conflicts: the merged files for the editor |
| POST | /v1/pulls/{owner}/{name}/{number}/resolve: commit the resolution as you |