Phased PR Pattern
How to ship initiatives too big for one PR without ending up with a long-lived branch hell.
Phased PR Pattern
How to ship initiatives too big for one PR without ending up with a long-lived branch hell.
TL;DR (human)
Split big initiatives into phases. Each phase is independently shippable: passes gates, is reviewable end-to-end, can ship without later phases. Merge each phase before opening the next. Fork the next phase from fresh main, not from the previous phase's branch.
For agents
Why phase
A mega-PR fails three ways:
- Unreviewable. Past ~500 LOC of meaningful change, reviewers either skim or punt.
- Long-lived conflicts. Every day the branch is open, main moves and conflicts compound.
- All-or-nothing. If phase 3 of the plan turns out to be wrong, you cannot land phases 1 and 2 cleanly.
Phasing solves all three at once.
Splitting strategy
Three good axes for splitting:
- By layer. Phase 1: schemas + types. Phase 2: stores. Phase 3: handlers. Phase 4: UI. Each phase compiles on its own with stub adapters at the next layer.
- By surface. One package per phase. Cross-cutting refactor → one PR per affected package.
- By risk. Phase 1: low-risk foundation. Phase N: the controversial change.
Pick the axis that minimises cross-phase coupling. If phases need each other to compile, you split wrong.
Per-phase rules
Each phase:
- Is one PR.
- Has its own PR-intent manifest (sub-unit references the same parent issue with
· phase Nsuffix). - Passes gates independently. The repo is shippable after each merge.
- Forks from current main at PR-open time, not from the previous phase's branch.
- Lands behind a feature flag if the partial state is not yet user-facing.
Tracker issue
The parent issue lists all phases with status:
- [x] Phase 1 — schemas + types — PR #1234
- [x] Phase 2 — stores — PR #1245
- [ ] Phase 3 — handlers
- [ ] Phase 4 — UI
- [ ] Phase 5 — feature-flag flipUpdate the parent issue at the start and end of each phase. This is the canonical "where are we" doc.
Merge cadence
Default: merge each phase with gh pr merge --merge --admin (or your equivalent) after gates pass on the rebased branch. Then delete the branch. Then fork the next phase off fresh main.
Why --admin: phased work often has tight dependencies between phases; you do not want the next phase blocked on a slow reviewer pinging an approval into a now-stale branch. Admin merge is appropriate when gates are green — never as an override of failing gates.
Why fresh main each phase: the previous phase's branch carries baggage (its commits, its conflict-resolution state). Forking from main resets the slate, avoiding cumulative drift.
Feature flags
If a partial-state phase is observable to users, gate the new behavior behind a flag, default off. Phases 1–N add the behavior; the final phase flips the default. Two benefits:
- Each phase is shippable to production without exposing half-built UX.
- The flag flip is itself a trivial PR, reversible if something goes wrong.
Cross-phase dependencies
Sometimes phase N+1 needs a change to a contract introduced in phase N. Handle by:
- Land phase N. The new contract is available.
- Open phase N+1 against current main (which now has the new contract).
Never open phase N+1 while phase N is still in review. The merge order matters.
When phasing goes wrong
- Phases are too small. A 30-line PR per phase + 20 phases = review overhead dominates the work. → Merge adjacent phases when they share a reviewer.
- Phases are too big. Each phase is 1500 LOC. → Re-split. The axis was wrong.
- Phase N+1 cannot ship without phase N+2. → Phases are coupled; you did not split well. Re-plan.
- Branch sits open for a week between phases. → Cadence too slow; conflicts compound. Aim for one phase per session.
Long-lived parent branches
Some teams use a long-lived epic/\<name\> branch with phase PRs merging into it, then a final mega-merge to main. Do not do this. Reasons:
- The mega-merge is unreviewable again.
- The epic branch conflicts with main as main moves.
- You lose the gate-per-phase discipline.
Phases merge directly to main. Each phase carries its own gate signal.
Common failure modes
- Phase 2 opened while phase 1 still under review. Conflict storm when phase 1 lands. → Strict serial: merge, then open next.
- No feature flag on a half-built UI surface. Users see broken state in prod. → Flag mid-build; flip in the final phase.
- Parent issue not updated. Reviewers can't tell which phase is current. → Update parent issue at every phase start + end.
- Phases not independently testable. Phase 1 has no behavior without phase 3; can't write a meaningful test. → Phase contract is "the system still compiles + existing tests still pass". Adding tests for the new behavior happens in the phase that adds the behavior.
See also
universal.md— Rule 8.../ai-collaboration/universal.md— Rule 5 (one sub-unit per session).merge-rules-pattern.md— for the rebase-each-phase step.