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:
- deterministic
.gitattributesmerge drivers; - default-on, preflight-gated AI conflict resolution;
- 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:
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:
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
CRITICALagent 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.