Git worktrees 無法消除編碼智能體的任務衝突,只會把問題延至合併階段
Worktrees do not remove the collision. They move it to merge time.
Git worktrees 為各編碼智能體提供獨立工作目錄,避免互相覆寫檔案,但不會檢查任務計劃是否相容,因此矛盾可能延至合併時才被發現。文中的 PaymentService 案例顯示,Stripe 與 PayPal 分支即使乾淨合併,兩條呼叫路徑也都能執行,PayPal 功能仍可能依賴標記為待刪除的類別。
Git worktrees give each coding agent its own checkout, so the agents stop overwriting each other's files. They do not remove the collision between what the agents are trying to do. That collision is still there, and with worktrees you find it later, when you merge the branches.
Sometimes Git stops you at that point. Sometimes it merges cleanly and the damage is only visible if you go looking. Below is a small repository you can build in two minutes that shows both outcomes, then what you can check with Git alone and what you need to move earlier.
A merge-time collision is a conflict between two agents' plans that nothing detects until their branches are combined. The plans were incompatible from the moment both were made. Worktrees keep the files apart while the work happens, so the incompatibility sits unseen until integration.
What do worktrees isolate, and what do they leave alone?
A worktree is a second checkout of the same repository, on its own branch, in its own directory. Two agents in two worktrees cannot overwrite each other's half-written files, break each other's builds with a stray install, or read stale code because the other agent changed it underneath.
That is the whole of it, and it is worth having. But look at what is not on the list:
- What each agent has decided to build. That lives in a prompt, not in a file.
- What each agent assumes will still exist. Agent B adds a method to a class because the class is there. It has no way to know Agent A is retiring it.
- The order in which the work should land. Worktrees do not know that one task has to be merged before the other is started.
Isolation helps while the agents work. It decides nothing about whether their work belongs in the same codebase.
What does Git report when you merge?
Git merges by comparing changed lines against a common ancestor. I ran the same two-agent task through a throwaway repository three different ways, with Git 2.51.0, and Git gave three different answers.
| What Agent A did | What Agent B did | What Git said at merge |
|---|---|---|
Deleted payments.py after moving callers off it |
Added a method to payments.py
|
CONFLICT (modify/delete). Stopped. |
Rewrote the call in checkout.py
|
Added a new function next to that call in checkout.py
|
CONFLICT (content). Stopped. |
Moved callers to a new class and left payments.py in place |
Added a method to payments.py and a new file that uses it |
Clean merge. No output. |
The first two are the cases people mean when they say worktrees "work": the plans touch the same file, Git notices, and a person decides. The third is the one that matters here.
So Git's answer depends on how the agents happened to arrange their edits, not on whether the plans were compatible. The same disagreement is loud in one run and silent in the next.
What does the clean merge look like?
Here is the third case from the table, end to end. The task: Agent A is asked to replace PaymentService with StripePaymentService. Agent B is asked to add PayPal support to PaymentService. Each gets its own worktree and branch.
git worktree add ../agent-a -b agent/stripe
git worktree add ../agent-b -b agent/paypal
Agent A creates shop/stripe_payments.py, moves every caller in shop/checkout.py to the new class, and leaves a note at the top of shop/payments.py: # Deprecated: callers moved to StripePaymentService. Delete next release. It commits.
Agent B adds a charge_paypal method to the end of shop/payments.py and a new file, shop/paypal_checkout.py, that calls it. It commits.
Then the integration, in the main checkout:
$ git merge agent/stripe
shop/checkout.py | 4 ++--
shop/stripe_payments.py | 3 +++
2 files changed, 5 insertions(+), 2 deletions(-)
create mode 100644 shop/stripe_payments.py
$ git merge agent/paypal
shop/payments.py | 3 +++
shop/paypal_checkout.py | 4 ++++
2 files changed, 7 insertions(+)
create mode 100644 shop/paypal_checkout.py
$ git status --short
(clean)
Now look at what the merged repository says:
$ head -2 shop/payments.py
# Deprecated: callers moved to StripePaymentService. Delete next release.
class PaymentService:
$ grep -rn "PaymentService" shop/ | grep -v Stripe
shop/payments.py:2:class PaymentService:
shop/paypal_checkout.py:1:from shop.payments import PaymentService
shop/paypal_checkout.py:4: return PaymentService().charge_paypal(cents)
$ python3 -c "from shop.checkout import checkout; from shop.paypal_checkout import checkout_paypal; print(checkout(500)); print(checkout_paypal(500))"
charged 500 via stripe
charged 500 via paypal
Both paths run. Nothing fails. And the codebase now has one class marked for deletion, with PayPal, a new feature, built on top of it. The next person to follow the deprecation note deletes the class and takes PayPal with it. Or they leave it, and the repository permanently has two payment classes, one per gateway.
No test fails, because each branch's tests passed on its own and the merged tests exercise two working paths. Each agent also reported success, and it was right. The only thing wrong is a contradiction between two plans, and no tool in the standard pipeline reads plans.
Why is merge time the expensive moment?
The incompatibility existed the second the second agent started. What changes with time is the cost of resolving it. This is reasoning, not a measurement: I have no timing data to give you.
- At planning time, you change a sentence in a prompt. Nothing has been written.
- While the agents work, you stop one and redirect it. You lose part of a session.
- At merge time, both agents have finished and both branches are green. Now you are choosing between two completed pieces of work, or rewriting one to sit on the other. If the conflict is the loud kind, you also have to understand both diffs to resolve it.
- After merge, the conflict is in the codebase, and the only evidence of it is a deprecation note and a class that should not have grown.
Worktrees move the discovery from the first row of that list to the third or fourth. They make the work itself smoother and the surprise later. That trade is usually fine with two agents and a small task. It stops being fine as the number of agents grows, because the number of pairs of plans grows faster than the number of agents.
What can you do with Git alone?
Quite a lot, and you should do all of it before you add a tool.
- Decide the order of destructive work. If a task replaces, removes, renames or migrates something, nobody else should be editing that thing. Run the destructive task first, merge it, then start the others from the merged commit.
-
Define the boundary before you split. If two tasks meet at an interface, make one small change that defines it, merge it, and start both agents from that commit. In the example, that is a
PaymentProviderinterface first, then Stripe and PayPal behind it. - Assign by what a task changes, not by its name. "Modernize payments" and "add PayPal" sound like two features. They change one class.
- Test the merged result, not each branch. Merge into a throwaway integration branch and run your real checks there. In the example this would have passed, so it catches the loud failures and not the quiet one, but it catches a lot.
-
Ask Git whether a merge is clean before you do it.
git merge-tree --write-tree agent/stripe agent/paypalperforms the merge in memory and reports conflicts without touching your working tree. It is quick, and it gives you the same answer the merge would, which in the third case is "clean". Treat that answer as a statement about text only. - Write the plans down where every agent reads them. A shared file that lists each task, what it changes and what operation it performs catches duplicate work. It catches replace-versus-extend only if someone reads two entries side by side and notices.
All of these are the same move: get the plans into a form you can compare before the code exists. Past two or three agents, the weak point is that a person has to do the comparing.
What changes if the plans are compared before the code?
That is the step we built Foremerge for. It is an open-source coordination protocol that sits above Git. Each agent declares what it is about to change, and what it will do to it, before it edits anything.
foremerge intent publish \
--agent "$AGENT" \
--task "modernize-payments" \
--summary "Replace PaymentService with StripePaymentService" \
--scope symbol:PaymentService=replace
When another agent declares symbol:PaymentService=extend, the comparison raises a destructive_vs_additive finding at HIGH severity, while both worktrees are still clean:
{
"kind": "destructive_vs_additive",
"severity": "HIGH",
"scope": { "kind": "symbol", "key": "PaymentService" },
"explanation": "One intent will replace `PaymentService` while the other will extend it; both declare the same semantic scope.",
"suggestion": "Coordinate on a stable `PaymentProvider` contract first, then implement StripePaymentProvider and PayPalPaymentProvider behind it and migrate callers deliberately. This is a heuristic suggestion, not an automatic design decision."
}
The finding does not block anyone. Claims are advisory, so two agents can still work the same area when that is fine. It is information for the agent or the person, delivered at the cheapest point on the list above. The store is a SQLite file in the repository's Git directory, which every linked worktree shares, so the agents keep isolated files and see the same declarations.
The limits are real. It trusts the declaration: an agent that says extend and then rewrites the class is not caught, though the declaration and the diff are both on record. A scope on a whole class does not match a scope on one of its methods in this release. Two names for the same concept can slip past it. It does not replace review, CI or someone owning the architecture, and a missing finding never shows that two changes are compatible. There are no published results on time saved.
Do you need any of this?
If you run one agent at a time, no. If your agents work on clearly separate parts of the code and you merge in sequence, worktrees and the list above are enough. The collision gets expensive when several agents run at once on one codebase, when they come from different tools, or when you cannot read all their plans before they finish.
Frequently asked questions
Do git worktrees prevent merge conflicts between AI agents?
No. Worktrees stop agents from overwriting each other's files while they work. Conflicts still appear when the branches are merged, and plans that contradict each other without touching the same lines merge cleanly and are not reported at all.
Why did Git merge two agents' branches with no conflict and the code still not make sense?
Git compares changed lines against a common ancestor, not intentions. If one agent retires a class and another builds on it in a different file, no lines overlap. The merge is clean and the codebase now contradicts itself.
Does git merge-tree tell me whether two agent branches are compatible?
It tells you whether Git can combine them without a textual conflict, without changing your working tree. That is useful and fast. A clean answer says nothing about whether the two plans are compatible, and in the example above it is clean.
Should one agent finish before the next starts?
When a task replaces, removes or renames something other work depends on, yes: run it first, merge it, and start the others from the result. Tasks that only add or extend different things can run in parallel.
Should I merge each agent's branch separately or together?
Merge into a throwaway integration branch and run your real checks on the combined result before anything reaches your main branch. Each branch passing its own tests tells you about that branch only.
Does Foremerge replace worktrees?
No. Worktrees isolate files and Foremerge shares plans, so you use both. Foremerge reads the same store from every worktree and from Claude Code, Codex and Cursor, and its claims never lock anything.
If you only ever run one agent, skip this
If you run two or more agents on one repository, worktrees handle the files and a comparison of declared plans handles the part Git cannot see. Foremerge is one Rust binary, local-first and Apache-2.0. Install it with curl -fsSL https://foremerge.com/install.sh | sh, then run foremerge init. One limit to start with: it can only compare what agents declare.
來源:DEV Community · dev.to