Six Green Branches Is Not Six Merged Changes

Read25 min · Scope → Isolate → Supervise → Land · Updated 2026-09-13

You end this lesson with: fleet/merge-policy.md — the reconciliation order and conflict budget for parallel agent branches.

Chapters

  1. What is actually going on
    01
  2. The rule and its condition
    02
  3. Worked case
    03
  4. Faded case
    04
  5. Open case
    05
  6. Where this breaks
    06
view

Lesson 13. Six Green Branches Is Not Six Merged Changes

Six agent branches finish over the course of a Tuesday afternoon. Every one is green: its own tests pass, its own diff got a fast review, its own gate ran clean. A teammate merges them in the order they happened to land, because that order is right there in the pull-request list and nothing about any single branch looks risky. The third merge changes a shared helper's return contract in a way the second merge's code had silently depended on, without touching a line the second merge had written. No merge conflict ever appears; git has nothing to complain about, because the two branches never touched the same file. The build goes red on main an hour later, on a change nobody just made, and the on-call engineer spends the rest of the afternoon bisecting five merges to find a bug that was never in any single one of them. Decide now: what check, run at what moment, would have caught this before it reached main?

This lesson's deliverable is fleet/merge-policy.md — the reconciliation order and conflict budget for parallel agent branches. It serves the Land stage of the Scope → Isolate → Supervise → Land loop: the branches already passed a gate (Land's earlier piece), and this is what turns N of them into one trustworthy main.

What is actually going on

A conflict has two very different shapes, and only one of them looks like a conflict. The first shape is textual: two branches edit overlapping lines of the same file, and git refuses to auto-merge. This is the easy case, and "easy" is not faint praise, because tooling built for exactly this problem has existed for decades and catches it before anyone's time gets wasted. The second shape is semantic: two branches touch different files, each change is correct when reviewed on its own, and the two are wrong together. Nothing about the merge itself signals this. The files don't overlap, so there's no conflict marker to see; the tests that ran against each branch alone still pass, because neither branch's own tests exercised the interaction. The only way to catch a semantic conflict is to test the combination, not the parts, and to do it every time the combination changes, not once at the end of the week.

That second fact, testing the combination every time rather than once at the end, is what a merge queue actually buys you. A queue takes branches one at a time, merges each onto the current tip of main by rebase rather than by merge commit, and re-runs the full gate against that merged result before it advances. A rebase, not a merge commit, matters here for a specific reason: a merge commit lets the branch carry a CI result that was computed against a main from an hour ago, and by the time it lands, main has moved without anyone re-testing against where it moved to. A rebase forces the branch's diff to be re-evaluated against the main it is actually about to become part of, and the gate that runs afterward is testing that exact, current combination.

diagram
drawing
A merge queue serialises every branch through one gate that re-tests the branch rebased onto the current main, not the branch in isolation.

Notice what the diagram does not show: a human manually merging the conflicting hunks. When the gate bounces a branch, the failure goes back to the agent that wrote it, packaged as a new, narrowly scoped task, with the failing evidence attached. That agent already holds the context a human reviewer would have to reconstruct from scratch: what the branch was trying to do, which parts of it are load-bearing, and which parts were incidental. A person who steps in to hand-merge instead is guessing at intent from a diff, and tends to resolve toward whichever branch they currently understand better, which is not the same thing as resolving toward whichever branch is actually correct.

The rule and its condition

Reconcile in dependency order with a conflict budget, and re-verify after each merge, not once at the end. The condition that triggers this rule is simple to check: it applies the moment more than one agent branch targets the same repository at the same time, regardless of whether their files overlap. A conflict budget caps how many branches may be open against the same file or module simultaneously, so that a third agent is never dispatched to edit a file two other agents are already mid-edit on; the dispatcher checks the budget the same way it checks the task-sizing rule from earlier in this module. Dependency order comes first within that budget: a branch that reads or calls what an earlier branch changed merges after it, rebased onto the change it depends on, so its own gate run tests against the real, current behaviour rather than a stale assumption about it.

Textual conflictSemantic conflict
What it looks likeTwo branches edit overlapping lines of the same fileTwo branches edit disjoint files or lines; each is correct alone
Who or what catches itgit's own merge machinery, before the gate even runsNothing, unless a test encodes the invariant the two changes jointly rely on
How common it isRare once agents are scoped to disjoint files by the task specThe default risk whenever two branches touch a shared invariant through different files
The fixReject the merge; hand the conflicting hunk back to its agentRe-run the full gate against the merged result; if no test encodes the invariant, the queue cannot catch it either

The conflict budget matters because the odds of a textual collision do not grow in a straight line with the number of branches open against one file, they grow with the number of pairs of branches.

Illustrative composite. Say a fleet's own history shows that two branches touching the same file have roughly a 1-in-5 chance of a textual conflict by the time either merges (a number to calibrate from your own repo, never import from this course). For k branches simultaneously open against that file, the number of pairs is k·(k−1)/2, and the chance that at least one pair collides is 1 − (1 − 0.2)^(k·(k−1)/2). At k=2 that is one pair: 1 − 0.8¹ = 20%. At k=3, three pairs: 1 − 0.8³ ≈ 49%. At k=4, six pairs: 1 − 0.8⁶ ≈ 74%. At k=5, ten pairs: 1 − 0.8¹⁰ ≈ 89%.

diagram
drawing
Modelled conflict probability climbs steeply as more branches stay open against one file; two is the last point before the curve bends sharply upward.

Age compounds the same curve.

Illustrative composite. If main moves fast enough that an open branch has roughly a 5% chance per day of drifting into a conflict with something already merged, the chance of at least one conflict by day 3 is 1 − 0.95³ ≈ 14%, and by day 7 it is 1 − 0.95⁷ ≈ 30%.

A conflict budget that caps how many branches may open against a file, with no limit on how long any one of them may sit open, still lets a branch drift into that curve on its own; the budget and a short time-to-merge target are two separate controls, not one.

Worked case

Six agent branches are open against a small order-fulfilment service at once: stock.ts tracks per-warehouse inventory, warehouse.ts decides how to satisfy an order across locations, and checkout.ts turns a reservation into an order. The branches, in the order they happen to finish review, are chore/stock-log-lines, feat/admin-audit-log, feat/discount-codes, feat/preorder-oversell, feat/warehouse-ops-summary, and feat/multi-warehouse-reserve.

BranchFiles touchedDepends on
1. feat/admin-audit-logadmin.tsnone
2. feat/preorder-oversellstock.tsnone
3. chore/stock-log-linesstock.tsbranch 2 (same file, budget slot)
4. feat/multi-warehouse-reservewarehouse.tsbranch 2 (reads its new floor logic)
5. feat/warehouse-ops-summarywarehouse.tsbranch 4 (same file, budget slot)
6. feat/discount-codescheckout.tsbranch 4 (calls its new function)

Decide now: does the finish order above decide the merge order? It does not. Branch 1 has no dependency and no file in common with anything else, so it merges first purely to shrink the queue. Branches 2 and 3 both touch stock.ts, which is the file's two-branch budget filled; 2 merges before 3 because 3's logging lines sit inside the function 2 is also changing, and 3 rebases onto 2's result rather than the other way around, since 2 is the change with real logic in it. Branches 4 and 5 both touch warehouse.ts, so the same budget applies, and 4 goes first because 5 is a read-only addition with nothing depending on it. Branch 6 waits for branch 4 specifically, not because of a file overlap, but because branch 6's code calls a function branch 4 introduces. The order is 1, 2, 3, 4, 5, 6, decided by dependency and file budget, with the accident of which pull request happened to get reviewed first playing no part in it.

diagram
drawing
Solid arrows are same-file budget order; dashed arrows are a real code dependency, and both point toward the branch that must merge first.

Branch 2 changes stock.ts's reserve function so a preorder SKU may go negative by a reviewed, signed-off amount:

diff
+export const MAX_OVERSELL = 5; // ops sign-off, ticket BUG-2214

 export function reserve(sku: string, qty: number, warehouseId: string): boolean {
   const level = warehouses[warehouseId].stockLevels[sku] ?? 0;
-  if (level - qty < 0) return false;
+  // Preorder SKUs may go negative; the warehouse team guarantees
+  // same-week replenishment up to MAX_OVERSELL units per location.
+  const floor = PREORDER_SKUS.has(sku) ? -MAX_OVERSELL : 0;
+  if (level - qty < floor) return false;
   warehouses[warehouseId].stockLevels[sku] = level - qty;
   return true;
 }

Its own tests reserve against one warehouse, confirm a 5-unit oversell succeeds and a 6-unit oversell fails, and the review approves it: correct, on its own terms. Branch 4 adds a new, purely additive function to warehouse.ts that lets one order draw stock from more than one location:

ts
// warehouse.ts — new in branch 4; nothing else in the file changes.
import { reserve, MAX_OVERSELL } from "./stock";

export function reserveAcrossWarehouses(sku: string, qty: number): boolean {
  const ids = Object.keys(warehouses);
  let remaining = qty;

  // Pass 1: use real stock wherever it exists.
  for (const id of ids) {
    if (remaining <= 0) break;
    const have = Math.max(warehouses[id].stockLevels[sku] ?? 0, 0);
    const take = Math.min(remaining, have);
    if (take > 0 && reserve(sku, take, id)) remaining -= take;
  }

  // Pass 2: real stock is gone everywhere. Let a preorder SKU overdraw
  // a little from each remaining warehouse instead of maxing out one.
  for (const id of ids) {
    if (remaining <= 0) break;
    const take = Math.min(remaining, MAX_OVERSELL);
    if (reserve(sku, take, id)) remaining -= take;
  }

  return remaining <= 0;
}

Its own tests use warehouses that actually hold stock, and confirm an order too big for any single location now succeeds by drawing from several: also correct, on its own terms, and the review never exercises the preorder floor at all. Neither diff touches checkout.ts, which existed before both branches and which neither one opens:

ts
// checkout.ts — exists before either branch, and neither one opens it.
import { reserveAcrossWarehouses } from "./warehouse";

export function placeOrder(sku: string, qty: number) {
  const reserved = reserveAcrossWarehouses(sku, qty);
  if (!reserved) throw new OutOfStockError(sku);

  // Oversell is capped at MAX_OVERSELL units per SKU (ops sign-off,
  // BUG-2214), so a successful reservation never needs a manual
  // backorder review.
  return createOrder(sku, qty);
}

Three warehouses, all showing zero real stock for a preorder SKU. A customer orders 12 units. reserveAcrossWarehouses's first pass finds nothing to take. Its second pass draws 5 units from warehouse one (0 − 5 = −5, exactly at the floor), 5 more from warehouse two (same), and the remaining 2 from warehouse three (0 − 2 = −2, inside the floor). remaining reaches zero, the function returns true, and placeOrder ships all 12 units as a silent backorder, more than double the 5-unit cap the comment in both files names, with no manual review triggered anywhere. Both diffs merge cleanly, in dependency order, with the gate green after each one; no conflict marker ever appears, because branch 2 and branch 4 never touch the same file, and neither touches checkout.ts at all.

Faded case

Skip to the open case if this is familiar. A notification service has three open branches: feat/email-templates touches only templates.ts and depends on nothing; fix/dedup-window touches notify.ts, tightening the window that suppresses duplicate sends from 60 seconds to 10; and feat/daily-digest touches digest.ts, and calls a function, shouldSend(), that fix/dedup-window also changes the behaviour of.

Illustrative composite. The service, its file names and its three branches are composite; the shape, an unrelated addition plus a behaviour change plus a caller of that behaviour, is the recurring pattern.

Decide now: which of the three has no dependency and can merge whenever it is ready, and which one must wait for the other, and why does "must wait" mean more than "touches the same directory"?

Open case

A reporting module has four branches open at once: feat/csv-export (touches reports.ts only), feat/scheduled-reports (touches scheduler.ts, and calls a function feat/csv-export adds to reports.ts), chore/reports-cleanup (touches reports.ts, removing a dead code path), and feat/report-access-log (touches audit.ts only, unrelated to the other three). Work out the merge order and the conflict-budget slot each branch occupies. The quiz's first item grades this.

Where this breaks

Reconciliation discipline narrows the window for a semantic conflict; it does not close it. The merge queue in this lesson's worked case did exactly what it was supposed to do, rebase-then-re-gate, in dependency order, and the bug still reached main, because the gate itself had no test for the invariant the two branches jointly broke. Order and re-verification only catch what the gate is capable of catching; when an invariant lives in a comment instead of a test, as MAX_OVERSELL did in both stock.ts and checkout.ts, the queue re-runs the same blind spot after every merge, not just once. The fix for that specific gap is not a merge-policy change, it is turning the comment into a property test the gate runs, which is the earlier gate lesson's territory, not this one's.

A conflict budget set too low for the fleet's actual size starves throughput: if only two branches may ever be open against a hot, frequently-touched file, and five agents each need to touch it this week, three of them sit idle waiting for a slot rather than working. The budget is a real trade-off against the review capacity from the previous lesson, not a free safety margin, and setting it requires knowing both numbers.

Finally, a stacked branch, one deliberately built on top of another rather than on main, absorbs a real dependency cleanly, the way branch 6 in the worked case depended on branch 4's new function. It cascades badly when the base branch itself gets bounced by the gate: every branch stacked on top now needs its own rebase once the base is fixed, and a long stack turns one bounce into several. Reserve stacking for dependencies that are genuine and shallow; a deep stack is a sign the underlying tasks were not sized as independently as the dispatcher assumed.

Key Takeaways

  • Reconcile branches in dependency order with a conflict budget, not in the order they finish.
  • Rebase onto current main before the gate runs; a stale merge commit tests a main that no longer exists.
  • Re-run the full gate after every merge, because the danger is the semantic conflict, not the textual one.
  • Cap how many branches may open against one file at once, and calibrate the cap from that file's own history.
  • Hand a bounced branch back to its own agent as a new task; a human hand-merge guesses at intent.
  • An invariant that lives only in a comment survives reconciliation discipline unchanged; write it as a test.

What You Have Now

fleet/merge-policy.md — the reconciliation order and conflict budget for parallel agent branches. It lives in your Fleet Kit alongside fleet/gates/ from the previous lesson and reads directly from fleet/review-policy.md's measured capacity: the budget you set here has to fit inside how many merges a day your reviewers and your gate can actually clear. The project that closes this module runs one real issue through this policy end to end, from task spec to a merged change with evidence at every gate.

Further Reading

Exercises

  1. List every file two or more of your own recently-open branches touched in the same week. For each file, would a conflict budget of two have changed when any of them merged?
  2. Take one pair of your own merged changes that never conflicted textually. Write one sentence naming the invariant, if any, that a third change could have broken by relying on both of them at once.
  3. Write fleet/merge-policy.md for your own repository: the dependency-order rule, the conflict budget per module (or per file, if your repo is small), and the rule for who resolves a bounced branch.

Check your understanding


Reading is free — your progress isn't being saved

Nothing you did on this page is recorded. Paid students get saved progress and verified completions, recorded quiz answers, the spaced-repetition review queue, labs, and a certificate for each course they finish.

30-day money-back guarantee. One-time payment — lifetime access and updates.