Stack Maintenance
Keep stacked GitHub pull requests boring: map the real stack, act from the bottom, verify after every remote action, and rewrite branches only with a lease.
Use this skill for PR chains, dependent branches, branch retargeting, merge queues, stale parent branches, and review/merge triage. Prefer existing stack tools (gh stack, gh pr stack, jj spr) when the repository has them; fall back to plain gh and git only after confirming the tool is unavailable or insufficient.
Rules From Prior Stacked PR Incidents
- Trust fresh GitHub metadata, not memory. Fetch
baseRefName,headRefName,mergeStateStatus,mergeable,reviewDecision, checks, and auto-merge state before acting. - For evidence, compare against the PR head from GitHub (
headRefOidororigin/<headRefName>after fetch), not just a local branch; local branches can be stale. - Stacked PRs usually merge bottom-up. A child can become conflicted after its parent merges; expect to rebase or retarget it before merging.
mergeableandCLEANmean mergeable into the currentbaseRefName, not necessarily intomain. Report the base explicitly in triage.- Do not recommend
gh pr mergefor a PR whose base is another stack branch unless the intent is to merge into that parent branch; otherwise retarget or wait for the parent to land first. gh pr merge --auto --squashcan merge immediately for intermediate stack branches. Re-read the PR afterward; do not infer from command text.- Main may use a merge queue. If GitHub says the merge strategy is controlled by the queue, retry without an explicit strategy, then re-read
state,mergedAt, andmergeCommit. - Do not approve a PR before reviewing its diff and running the relevant local check. "Clean" only means mergeable, not reviewed.
- Use
--delete-branch=falsefor stack branches unless the user explicitly asks to prune. Other PRs may still depend on them. - Use
git push --force-with-lease, never plain force, after rebasing a PR branch. - Stop before rewriting someone else's branch unless ownership and permission are clear.
Map The Stack First
Start with the current checkout and remote state:
git status --short --branch
git fetch origin --prune
List open PRs with the fields that matter:
gh pr list --state open --limit 100 \
--json number,title,author,baseRefName,headRefName,isDraft,mergeable,mergeStateStatus,reviewDecision,autoMergeRequest,statusCheckRollup,url
Classify PRs into four buckets:
- Merge candidates: non-draft,
MERGEABLE,CLEAN, no failing checks, notCHANGES_REQUESTED. - Review candidates: clean but unapproved; inspect before approving.
- Stack repair: wrong base, conflicted, stale parent, or child branch blocked by a parent merge.
- Leave alone: draft, changes requested, branch owned by someone else, or unclear intent.
For a focused PR, inspect the exact relationship:
gh pr view <n> --json number,title,author,baseRefName,headRefName,headRefOid,mergeable,mergeStateStatus,reviewDecision,autoMergeRequest,statusCheckRollup,url
Merge Or Queue Safely
Merge bottom-up. After each merge, re-read the next child because the base may have changed.
For intermediate stack branches:
gh pr merge <n> --auto --squash --delete-branch=false
For a main-branch PR with merge queue behavior:
gh pr merge <n> --auto --delete-branch=false
Verify immediately:
gh pr view <n> --json number,state,mergedAt,mergedBy,mergeCommit,mergeStateStatus,autoMergeRequest,url,title
If the command reports an error that implies a race, stale base, or already-merged PR, re-read the PR before retrying. GitHub may have completed the merge while the CLI reported a confusing GraphQL error.
Review Before Approval
For a clean but unapproved PR:
gh pr diff <n> --name-only
gh pr diff <n> --patch
Run the smallest relevant check in a clean checkout or temporary worktree. Approve only after the diff and check match the intended change:
gh pr review <n> --approve --body "Reviewed diff and validated with <command>."
Then merge or queue it using the rules above.
Repair A Conflicted Child
Prefer stack tooling when available:
gh stack view
gh stack sync
gh stack submit
If using plain Git, repair in a throwaway worktree:
pr=123
base_branch=parent-branch
head_branch=child-branch
git fetch origin "$base_branch" "$head_branch"
tmp=$(mktemp -d "/tmp/pr${pr}.XXXXXX")
git worktree add -B "fix-pr${pr}" "$tmp" "origin/$head_branch"
cd "$tmp"
git rebase "origin/$base_branch"
Resolve conflicts, choosing the final intended content rather than mechanically taking either side:
git status --short
git add <file>
git rebase --continue
Run the relevant check. Push back with a lease:
git push --force-with-lease origin "fix-pr${pr}:$head_branch"
Re-read the PR. Merge only when it returns to MERGEABLE and CLEAN.
Clean temporary worktrees after use:
git worktree remove --force "$tmp"
Retarget Wrong Bases
If the commits are correct but the PR targets the wrong parent:
gh pr edit <n> --base <correct-base-branch>
gh pr view <n> --json number,baseRefName,headRefName,mergeable,mergeStateStatus,url
If retargeting introduces conflicts, repair the branch against the new base before merging.
Done Criteria
- Every acted-on PR has fresh
gh pr viewevidence after the action. - Merged PRs show
state: MERGED,mergedAt, and a merge commit. - Queued PRs show auto-merge or merge-queue state expected by the repository.
- Rewritten branches were pushed with
--force-with-lease. - Local temporary worktrees are removed.
- Final report lists merged, queued, repaired, and intentionally skipped PRs separately.