Running several coding agents feels fast until the first three “finished” branches all target the same release. Each agent may have a green test run in its own worktree, yet the combined change can break an import, weaken a test, or quietly reverse another branch’s assumption. Git can catch overlapping lines. It cannot tell you that one agent renamed a function while another added a caller for the old name.
The answer is not to give every agent a bigger prompt or to let them merge whenever CI is green. Put one small, explicit AI coding agent merge queue between parallel work and the shared branch. It accepts a proposed change, rechecks it against the current base, detects meaningful collisions, and records why the change landed or stopped.
This guide is tool-neutral. It works whether agents run from a terminal, an IDE, or an internal orchestrator. The goal is simple: parallelize implementation, then serialize integration with evidence.
Why worktrees solve only the first problem
A separate Git worktree is a strong default. It prevents one running agent from overwriting another agent’s files and makes each task easy to inspect or discard. But a worktree isolates a file tree, not intent.
Imagine two agents start from the same commit:
| Agent | Task | Local result |
|---|---|---|
| A | Replace getUser(id) with getAccount(id) | Tests pass in its branch |
| B | Add audit logging after getUser(id) | Tests pass in its branch |
If A lands first, B may fail to rebase. Worse, B might merge cleanly after a broad compatibility wrapper is added, but log the wrong identifier. That is a semantic collision: the code has no conflict markers, yet the combined behavior is wrong.
Recent developer discussions describe exactly this pressure: worktrees make concurrent sessions possible, but teams still need a single landing lane, file ownership, and an integration check. A recent merge-conflict dataset also found textual conflicts common in agent-created pull requests. Treat those signals as a reason to design the integration boundary—not as a reason to trust a percentage from one study for every repository.
Model the queue as a state machine
Avoid a queue that is merely a list of branches. Each candidate needs a durable record so retries do not merge stale work or lose a human decision.
type QueueState = | "proposed" | "claimed" | "rebasing" | "validating" | "needs_review" | "merged" | "rejected"; type MergeCandidate = { id: string; branch: string; baseSha: string; // the commit the agent started from headSha: string; // immutable proposed commit task: string; claims: string[]; // intended paths or components checks: Record<string, "pending" | "passed" | "failed">; state: QueueState; attempt: number; }; The important fields are baseSha, headSha, and attempt. Never accept “latest branch” as evidence. The queue should know exactly which commit the agent produced and which base it was evaluated against.
A minimal transition rule is:
proposed → claimed → rebasing → validating → merged ↘ needs_review → rejected | proposed Only the queue advances a candidate to merged. The implementation agent can create a proposal, attach evidence, and respond to feedback, but it does not write directly to the protected branch.
Ask agents to claim intent before editing
File locks are useful, but paths alone are too blunt. Two safe changes can touch one shared configuration file; two dangerous changes can touch different files while changing the same API contract. Use two lightweight claims:
- Path claim: likely files or directories.
- Behavior claim: interfaces, routes, migrations, feature flags, or invariants the task expects to change.
For example:
task: add-invoice-export paths: - apps/api/src/invoices/** - apps/web/src/invoices/** behavior: provides: - GET /v1/invoices/:id/export depends_on: - Invoice.status invariants: - exports never include payment tokens - unavailable invoices return 404 Claims are planning data, not a security boundary. An agent can discover a necessary additional file. When that happens, require it to update the claim and re-run collision detection. This is much safer than silently expanding scope after the work has begun.
Detect collisions before Git does
Run a cheap collision check as soon as a candidate enters the queue. It should compare its claims with every waiting and recently landed candidate.
function collisionRisk(a: MergeCandidate, b: MergeCandidate) { const sharedPath = a.claims.some((p) => b.claims.includes(p)); const sameContract = a.claims.some((c) => b.claims.includes(c)); return sharedPath || sameContract; } Real implementations should use structured fields rather than one mixed string array. Start simple, then add high-value signals:
- changed exported symbols or API schema names;
- database migration and generated-client changes;
- package-lock, dependency, or build-config edits;
- edits to existing tests, test runners, or coverage rules;
- feature-flag names and environment variables;
- files that an earlier candidate changed after this candidate’s
baseSha.
Mark a risky pair as needs_review or enforce ordering. Do not automatically reject it. A queue is a coordination tool, not a punishment system.
Rebase in a disposable integration worktree
Never rebase or merge a candidate in the agent’s working directory. Create a fresh integration worktree from the protected branch. That makes every queue attempt reproducible and keeps a failed conflict resolution away from the original proposal.
git fetch origin main git worktree add --detach ../integrate-task-184 origin/main cd ../integrate-task-184 git cherry-pick "$CANDIDATE_SHA" pnpm lint pnpm test -- --runInBand pnpm build If the cherry-pick conflicts, preserve the worktree and record the conflicting paths, current base SHA, and conflict output. Let the original agent receive that bounded repair task—or route it to a human. Do not have a separate “merge agent” make broad product decisions with no access to the original task contract.
On a clean attempt, commit the exact integrated tree or fast-forward the protected branch through your normal pull-request process. Record the new base SHA. The next candidate must validate against that new state.
Use layered checks instead of one expensive CI gate
Re-running the full suite for every tiny change is often too slow. Skipping verification is worse. Use layers, and make the layers explicit in the candidate receipt.
| Layer | Example | When to require it |
|---|---|---|
| Diff policy | No secret files, no disabled tests, no changed CI command | Every candidate |
| Focused tests | Package or route tests | Every code change |
| Contract checks | API schema, migration, public types | Contract claim or dependency edit |
| Integration smoke | Build plus key user path | Before landing user-facing changes |
| Full suite | Cross-service tests, security scans | Risky queue entries or scheduled batch |
The diff policy deserves special attention with coding agents. A passing test is weak evidence if the same candidate deleted an assertion, added skip, altered the test command, or changed a fixture that hides the failure. Flag those edits for review; do not assume bad intent.
policy: block_if_changed: - .github/workflows/** - package.json - '**/*.snap' block_if_added_pattern: - 'describe\.skip' - 'it\.only' - '@ts-nocheck' require_human_review_if: - 'test/**' - 'db/migrations/**' Tune this to the repository. A blanket ban on test changes makes agents unable to add coverage. The useful distinction is between adding a new test and modifying an existing assertion or the command that proves correctness.
Make semantic checks concrete
Semantic collision detection can sound like a call for a magic AI reviewer. Start with facts that existing tools can produce:
- Compare generated OpenAPI, GraphQL, protobuf, or TypeScript declaration diffs.
- Fail if a migration changes a table also claimed by a waiting migration.
- Run a small contract test from a different package than the changed code.
- Run search-based checks for renamed public symbols.
- Ask a reviewer—human or agent—to answer a fixed question: “Which existing consumer would now behave differently?”
Here is a compact receipt that lets a reviewer see the evidence without rereading a long agent transcript:
{ "candidate": "task-184", "head": "a13e9d2", "validatedAgainst": "f4b8c10", "attempt": 2, "claims": ["GET /v1/invoices/:id/export", "Invoice.status"], "checks": [ { "name": "api-contract", "status": "passed", "artifact": "openapi.diff" }, { "name": "invoice-tests", "status": "passed", "artifact": "test.log" }, { "name": "diff-policy", "status": "review", "reason": "existing test edited" } ], "decision": "needs_review" } This is where a merge queue differs from an auto-merge switch. It retains uncertainty instead of converting it into a green badge.
Decide what happens when the base moves
The base will move often with parallel agents. Define this policy before the queue is busy:
- If only docs changed after validation, reuse focused test evidence when your risk policy permits it.
- If claimed paths or contracts changed, re-run the candidate from a clean integration worktree.
- If a candidate has failed rebase twice, stop automatic retrying and ask for a smaller task or an explicit conflict-resolution decision.
- If a migration, auth, billing, deployment, or security file changes, require human approval regardless of test status.
This prevents a common failure mode: a queue that repeatedly rebases an old branch until it appears clean, with no one noticing that the original task assumptions have expired.
Measure the queue without rewarding risky speed
Track a few operational measures per repository:
- queue wait time and integration time;
- rebase-failure rate and semantic-collision rate;
- post-merge revert or reopen rate;
- percentage of candidates requiring human review;
- validation cost by check layer.
Do not use “automerge percentage” as the north-star metric. A healthy queue sometimes stops work. The better outcome is a shorter path from a well-scoped proposal to a trustworthy shared branch.
A practical rollout plan
Start with one repository and two concurrent agents. First add dedicated worktrees, immutable commit IDs, and a single merge operator. Next record path and behavior claims, then add fresh-base focused tests. Only after those steps are stable should you automate low-risk merges.
Keep the manual escape hatch. A developer should be able to pause the queue, inspect the disposable integration tree, and make a clear decision. Automation is valuable when it makes that decision easier to audit, not when it makes it disappear.
FAQ
Do I need a merge queue for one coding agent?
Usually not. One agent and one human can use normal pull-request checks. Add a queue when independent tasks overlap in time, a branch is frequently stale before review, or agents can propose changes faster than the team can safely integrate them.
Are Git worktrees enough for parallel coding agents?
No. Worktrees prevent simultaneous file writes, but they do not reserve intent, resolve future merge conflicts, or validate a branch against changes that landed after the agent started. Pair them with a fresh integration attempt.
Should an agent resolve its own merge conflict?
It can resolve a narrow, well-explained conflict if it receives the original task contract and the current diff. Escalate when resolution changes a public contract, migration, permission rule, or product behavior. A clean merge is not proof that intent was preserved.
How do I catch semantic merge conflicts?
Compare contracts and generated artifacts, run tests from consuming packages, inspect changed exports and routes, and require a fixed reviewer question about affected consumers. Combine these targeted checks with Git’s textual conflict detection.
Can I auto-merge agent changes after checks pass?
Yes, for explicitly low-risk paths with a clean fresh-base validation and no collision signals. Keep human review for sensitive files and uncertain changes. Auto-merge should be a policy decision backed by evidence, not the default outcome of a passing command.
What should happen to a failed integration worktree?
Keep it long enough to preserve conflict output and validation artifacts, then clean it up using a recorded state such as merged, rejected, or abandoned. Do not delete solely by age; an agent or reviewer may still need the evidence.
