Skip to content

Risk-routed merge resolution: let git handle the boring conflicts

The merge conflict people picture is a human squinting at <<<<<<< markers and deciding which side meant the right thing. That conflict exists. It is just not the common case worth optimizing first.

In an AI build loop, most merge-time collisions are less dramatic. Two agents append independent records to the same registry. Two engine tickets regenerate the same client fixture tree from different source changes. Two docs tickets touch an append-mostly glossary. If every one of those conflicts goes straight to a model or a human, the system teaches the expensive resolver to do boring work.

The better shape is risk routing: send each conflict to the lowest-risk resolver that can safely handle its class.

In this repo the ladder is:

  1. deterministic .gitattributes merge drivers;
  2. default-on, preflight-gated AI conflict resolution;
  3. a human backstop when the gates refuse to bless the result.

That is not a "no LLMs touch merges" story. The current workflow explicitly uses an AI conflict resolver by default after deterministic resolution is exhausted. The point is narrower and more useful: make the deterministic tier large enough that the model sees only the residual conflicts that actually need judgment, then make the model's output pass the same review preflight as any other AI-authored code.

The underused lever: per-path merge drivers

Most developers know .gitattributes for line endings. Fewer use it as a per-path conflict router.

Git lets a repository say: when this path conflicts, use this merge driver. That can be a built-in driver like union, or a named custom driver registered in the local clone before a rebase. The important move is that the decision is path-specific. You do not declare "all conflicts are safe." You declare "this class of file has a mechanical resolution rule, and only this class gets it."

This repo's .gitattributes currently has three interesting merge-resolution classes:

client/src/lib/live/__fixtures__/** merge=bgui-regen-fixtures
workflow/test/fixtures/merge-deterministic-driver/union/** merge=union

docs/client/CLIENT_GLOSSARY.md merge=union
docs/overview/GLOSSARY.md merge=union
workflow/docs/GLOSSARY.md merge=union

The merge=union cases are for append-mostly files whose independent entries can be combined safely. The explicit pattern matters. Union is not a good default for source code, prose paragraphs, or anything where order and local context carry meaning. It is useful for attributed append-only records where the duplicate-entry and vocabulary lints remain responsible for catching bad content after the merge.

The bgui-regen-fixtures case is different. The generated fixture file itself is not trusted. The driver leaves ordinary conflict markers in place, and the merge stage resolves them only by rerunning tests/fixtures/generate_server_payloads.py after proving that the generator inputs are not conflicted. In other words, the generated artifact is resolved as a pure function of unconflicted source inputs.

That is the pattern worth stealing. Do not ask a model to reconcile generated output when the generator can derive it again. Do not ask a human to paste both sides of an append log when git can union the lines and the existing lint can reject duplicates. Put the boring class behind a deterministic driver.

Why the deterministic tier is first

Parallel ticket batches create a specific kind of merge pressure. The scheduler uses ## Touches to avoid launching two agents that clearly edit the same surface, and it serializes cross-cut hubs such as docs navigation, workflow docs, generated fixtures, and lockfiles. But launch-time serialization cannot predict every merge-time collision. A ticket can be source-disjoint and still regenerate the same fixture index. Two independent glossary additions can touch the same append-mostly file. A path declaration can be narrower than the files a tool rewrites.

For those classes, deterministic merge drivers are the cheapest safe resolver. They have three properties the higher tiers do not:

  • No model call means no prompt interpretation and no invented reconciliation.
  • The policy is visible in versioned repo text: one path pattern, one driver.
  • The resolved tree still goes through review preflight before it can merge.

That last point keeps the deterministic tier honest. The union driver is allowed only because the file class is append-mostly and downstream lint still checks the shape. The fixture regeneration path is allowed only because the generator inputs must be marker-free before the generated tree is rewritten. A deterministic resolver is not trusted because it is deterministic; it is trusted because its preconditions are narrow and its output is gated.

A real append conflict on glossary-style docs is the kind of incident this absorbs. The entries were independent, the file was append-mostly, and the expensive part was not deciding between meanings. The expensive part was that the system had not yet told git the file belonged to the union class. Adding the explicit .gitattributes patterns moved that whole class down the ladder.

The AI tier is default-on, not forbidden

After the deterministic drivers and regeneration path are exhausted, the merge stage invokes the configured ai_conflict_resolver by default. That is the current policy in workflow/docs/MERGE_SERIALIZATION.md, and it is deliberate.

The resolver runs inside the ticket clone against the remaining conflicted files. It is not limited to a whitelist of file classes, and it does not become safe because the model said the resolution was safe. Its compact JSON response keeps stage logs from capturing file contents, tokens, keys, or private game values, but the real safety boundary is later:

marker-free tree -> review preflight -> force-with-lease -> one merge retry

If the resolver leaves markers, errors, fails preflight, loses the force-with-lease race, or the single retry still cannot merge, the workflow restores the original branch head and routes to human abandonment.

There is an emergency kill switch:

BGUI_NO_AI_CONFLICT_RESOLVE=1

That skips only the AI rung and preserves the older human path. It is not the normal mode. The normal mode is AI-first, gated by the same preflight suite that guards ordinary AI-authored work.

The honest limitation is that merge-stage AI resolution happens after review, so it does not re-run the full /dev-review surface/security review. It runs bash workflow/scripts/dev.sh review-preflight. If that ever proves too weak, the next capability should be stronger merge-time verification, not pretending the model was never in the path.

The human tier is a stop signal, not a retry loop

The final rung is abandon-human. It is reached when the mechanical and AI tiers cannot produce a marker-free tree that passes the gate, or when the conflict is a genuine semantic judgment call.

Examples:

  • remaining conflict markers after deterministic and AI attempts;
  • fixture generator inputs are themselves conflicted;
  • review preflight rejects the deterministic or AI-resolved tree;
  • force-with-lease fails because the branch changed under the resolver;
  • the one retry still cannot merge;
  • two tickets edit competing CRITICAL agent rules and a person must decide which policy is true.

When that happens, the PR is left open and the branch is not deleted. The orchestrator records a merge-conflict cause, emits the abandon-human diagnosis without spending another model call, and stops. That matters. A rejected merge is not a prompt-engineering opportunity. It is evidence that the lower-risk resolvers did not earn the right to finish.

The reusable design

The reusable idea is not the exact driver names. It is the ladder.

Start by classifying conflict surfaces by the cheapest resolver that can be made safe:

Conflict class Resolver Safety condition
attributed append-only records merge=union duplicate/shape lint still gates the result
generated artifacts regenerate from source generator inputs must be unconflicted
ordinary residual conflicts AI resolver marker-free tree must pass preflight
semantic policy conflict human automation stops with the PR open

Then encode the cheap classes where git can see them: .gitattributes, named drivers, explicit path patterns, and narrow preconditions. Let the scheduler use those classes as merge-recoverable overlaps only when the path class really is recoverable. Keep the higher tiers small.

That is what risk routing buys. The system does not need to pretend all merge conflicts are equal. It can let git handle the boring conflicts, let the model try the residual conflicts behind a gate, and spend the human only where judgment still changes the outcome.


Or: subscribe for more posts on building autonomous AI development infrastructure.