stacked-pr
依存関係のある複数の PR を同期する。道具の選択はホストで決まる。GitHub なら gh-stack を使い、それ以外でだけ手動カスケードを行う。
このスキルの役割は、gh-stack が扱わない領域を埋めることにある: preflight、壊れたときの復旧、rebase 結果の検証、CI の上流優先修正。gh-stack skill は vendored(skills-lock.json 管理)なので、そこに知見を書き足さずここに置く。
When This Skill Applies
- 依存関係のある複数の PR を扱うとき全般
- 親ブランチが更新され、子 PR が古くなったとき
- スタックの途中の PR がマージされ、残りを re-target・rebase する必要があるとき
gh stack rebase/gh stack syncが conflict や中断で止まったとき(復旧はここが担当する)
Routing: GitHub なら必ず gh-stack
rebase を始める前にホストを判定する。推測しない。
git remote get-url origin
github.com(GitHub Enterprise 含む)→ Route A: gh-stack。手動カスケードに逃げない- それ以外(GitLab、Gerrit、素の git remote 等)→ Route B: 手動カスケード
Route A の中で git rebase --onto を手で叩くのは Repair のときだけで、それは Route B への切り替えではない。復旧したら gh-stack の管理下へ戻す。
Route A: gh-stack
コマンドの仕様・フラグ・exit code の意味は gh-stack skill が持つ。ここにはその前後にやることだけを書く。
A-1. Preflight
rebase・push の前に毎回確認する。ここを飛ばすと A-3 の復旧作業になる。
gh stack view --json # exit 0 でスタックとして認識されているか
git worktree list # スタックのブランチが他の worktree に取られていないか
git config rerere.enabled # true でないと conflict 解決が使い回されない
git config remote.pushDefault # remote が複数あるとき必要
判定:
- worktree に取られているブランチがある: gh stack はそのブランチを checkout できず、rebase が途中で止まる。さらに
gh stack rebase --abortもalready used by worktreeで復旧しきれない。先に worktree を畳むか、そのブランチだけ当該 worktree 内で手動 rebase する - exit 2 (not in a stack):
gh stack checkout <stack-number|pr-number>で取り込むか、gh stack init --base <trunk> <branch>...で作る - exit 6 (disambiguation required): 共有されていないブランチへ
gh stack checkout <branch>してから再実行する - exit 9 (stacked PRs unavailable): repo で stacked PR が有効化されていない。ユーザーに有効化を促し、それまでの暫定として Route B を使う
gh stack view --json の head が実際のブランチ tip と一致しているかも見る。ズレていたら記録が古い(A-3 の症状 1)。
gh stack view --json | jq -r '.branches[] | select(.head) | "\(.name) \(.head[0:10])"'
git rev-parse --short <branch>
A-2. 通常運用
gh stack view --json # 現状把握 (isMerged / needsRebase / pr.state)
gh stack rebase # fetch + trunk 追従 + カスケード rebase(push はしない)
- push は別ステップにする。
gh stack pushは全ブランチへの force-with-lease push であり履歴書き換えにあたるので、実行前にユーザーの承認を取る gh stack syncは fetch → rebase → push → PR 状態同期を一括で行う。承認済みで一気に流したいときだけ使う- マージ済みブランチのローカル掃除は
gh stack sync --prune。非対話環境では--pruneを明示しないと実行されない - 下段の PR がマージされたら、その時点で sync を通す。放置して trunk が進むほど A-3 の症状 1 を踏みやすくなる
rebase が成功したら A-4 を実行してから push する。
A-3. Repair playbook
症状 1: 身に覚えのないファイルで conflict する / replay 対象が多すぎる
gh stack が記録している base が古く、既に trunk に入っているコミットまで replay しようとしている。下段 PR が squash merge されたあとに trunk が進むと起きる。
検知(rebase が止まった状態で実行する):
git rev-list --count origin/<trunk>..<branch> # 本来 replay すべき自分のコミット数
wc -l < .git/rebase-merge/git-rebase-todo # 実際に replay しようとしている数
この 2 つが大きく食い違い、conflict しているファイルがブランチの関心と無関係なら確定。
対処:
gh stack rebase --abort- 手動
--ontoカスケードで下から積み直す - A-4 で検証する
gh stack pushを通して gh stack の記録(head / base)を実際の tip に合わせるgh stack view --jsonのheadが実 tip と一致し、needsRebaseが全 false になったことを確認する。まだズレるならgh stack unstack --local→gh stack init --base <trunk> <下から順のブランチ>で作り直す(--localなので GitHub 側のスタックは保持される)
症状 2: --abort が worktree で失敗する
⚠ Rebase aborted but some branches could not be fully restored:
checkout <branch>: ... is already used by worktree at '...'
復旧が中途半端に終わっている。git worktree list で場所を特定し、そのディレクトリの中で git status / git rebase --abort を実行して個別に戻す。以降は Preflight で worktree を潰してから gh stack を動かす。
症状 3: 並行編集で下流が古くなる
ユーザーや別セッションがスタックのブランチへ commit / push していることがある。カスケードの直前に必ず tip を取り直す。
for b in <下から順のブランチ>; do printf '%s %s %s\n' "$b" "$(git rev-parse --short "$b")" "$(git rev-parse --short "origin/$b" 2>/dev/null)"; done
上流の tip が想定と違ったら、その上流を起点に下流を積み直す。作業中に増えたコミットは捨てずに取り込む。
手動 --onto カスケード
下から順に、1 ブランチずつ実行する。gh stack の記録ではなく実際の SHAを使うので、記録が壊れていても影響を受けない。
# 実行前に必ず: replay されるのが自分のコミットだけか確認する
git log --oneline <古い親の tip>..<ブランチ>
git rebase --onto <新しい親の tip> <古い親の tip> <ブランチ>
- 古い親の tip は rebase 前の親ブランチの tip。
gh stack view --jsonのbase、git reflog show <親>、親が squash merge 済みならgh pr view <親PR番号> --json headRefOid --jq .headRefOidから取る - 確認コマンドの出力が想定より多いなら、指定した「古い親の tip」が実際の分岐点ではない。スタックの途中に rebase コピー(同じ subject の別 SHA)が挟まっていることがあるので、
git log --oneline origin/<trunk>..<ブランチ>と突き合わせて分岐点を取り直す - 他の worktree に取られているブランチは、その worktree の中で実行する
- conflict したら
resolve-merge-conflictskill を呼ぶ
A-4. Verify
push の前に、rebase が中身を変えていないことを確認する。
# 1. 各レイヤの diff が rebase 前後で同一か(patch-id が一致するか)
git diff <旧親> <旧tip> | git patch-id --stable
git diff <新親> <新tip> | git patch-id --stable
# 2. スタックが直列につながっているか
git merge-base --is-ancestor origin/<trunk> <bottom>
git merge-base --is-ancestor <親> <子> # 全ペアで
# 3. push 後: local と remote が一致しているか
git fetch origin --quiet
git rev-parse <branch> origin/<branch>
patch-id が食い違ったレイヤは、conflict 解決で内容が変わったか、replay 範囲を間違えている。push する前に原因を突き止める。
Route B: 手動カスケード(非 GitHub)
GitHub 以外のホストでは gh stack が使えないので、依存関係を自分で組み立てて順に rebase する。
B-1. スタックの構築
現在のブランチと、その下流を洗い出す。read-only な照会は 1 メッセージ内で同時に発行してよい。
git branch --show-current
git fetch origin <branch1> <branch2> ... # 1 回にまとめる
git merge-base --is-ancestor $(git rev-parse HEAD) <oid> # 下流判定
各ブランチの親は PR の base branch。親が現在のブランチのものから順にトポロジカルに並べる。同じ深さで互いに依存しないブランチは独立した兄弟で、順序はどちらでもよい。
B-2. 計画の提示
ブランチ・PR・rebase 先の一覧を示し、AskUserQuestion で承認を取ってから実行する。
B-3. カスケード
すべてのブランチが 1 つの working tree を共有するので、厳密に逐次で処理する。並行させると index と HEAD が壊れる。
各ブランチについて 手動 --onto カスケードと同じ手順を適用し、成功したら git push --force-with-lease origin HEAD してから次へ進む。push 後は adjust-pr-base をインラインで実行する(gh コマンド数回で終わるので切り出さない)。
親が削除・squash merge されている場合は、rebase の前に adjust-pr-base を実行する。PR の base が有効なブランチを指していないと CI が走らない。
CI 監視はカスケードの途中では行わない。全ブランチを完走させてから CI でまとめて扱う。
CI (upstream first)
gh-stack は CI を扱わない。どちらの Route でも、カスケード完走後にここを実行する。
watch-ci skill を上流から下流の順に 1 ブランチずつ呼び出す。各 PR の CI は自分の親との差分を検証するので、下流の変更が上流の CI に影響することはない。上流が通れば以後は安定する。逆に上流を直せば下流は必ず古くなるため、上流を確定させないまま下流を直すと手戻りになる。
先に全体の状態だけ見たいときは、read-only な gh pr checks を全 PR へ同時発行してよい。修正を伴う watch-ci の呼び出しは 1 ブランチずつ行う。
上流修正後の再カスケード
watch-ci が CI 失敗を直して新しいコミットを push すると、その下流が古くなる。
- そのブランチより下だけを積み直す(Route A なら
gh stack rebase --upstack、Route B なら手動で 1 つずつ) - push してから、その下流の
watch-ciを改めて呼ぶ
停止条件:
- 全ブランチ CI pass → Report へ
watch-ciが「PR の変更と無関係な失敗(flaky、インフラ)」と報告 → 記録して次のブランチへ- 同一ブランチの再カスケードが 2 回を超えた → 停止して状況を報告する
Report
## Stack Sync Report
Route: gh-stack (github.com)
Trunk: main
| Branch | PR | Rebase | Verify | Push | CI |
|--------|-----|--------|--------|------|-----|
| feat/auth-ui | #42 | ✅ clean | ✅ patch-id 一致 | ✅ | ✅ pass |
| feat/auth-api | #44 | ⚠️ conflict 2件解決 | ✅ patch-id 一致 | ✅ | ❌ lint (修正済み) |
Repair:
- gh stack の記録 base が古く 22 commits を replay しようとしたため abort し、--onto で積み直した
- gh stack push で metadata を再同期(needsRebase 全 false を確認)
報告に必ず含めるもの: 使った Route、復旧作業をしたならその内容と原因、検証の結果、未解決のまま残したもの。
Edge Cases
ブランチに open PR がない
スキップして警告する。ancestry には乗っているが PR がない(ローカル専用、PR 削除済み)。
conflict を自動解決できない
そのブランチでカスケードを止める。どのブランチのどのファイルか、どこまで rebase 済みかを報告し、手動解決後に再開を申し出る。gh stack の途中なら gh stack rebase --abort で全ブランチを戻せる(worktree の制約は 症状 2 を参照)。
スタックの途中がマージされた
Route A では gh stack rebase / sync がマージ済みブランチを skip して自動で処理する。PR の base は GitHub 側が自動 retarget するので adjust-pr-base は多くの場合 no-op になる。Route B では adjust-pr-base を rebase の前に実行する。
Boundaries
- このスキルは依存関係のある PR の同期・メンテナンスを扱う。新規スタックの作成や大きな PR の分割は扱わない。
- 大きな PR/ブランチを stacked PR に分割するには
reorganize-diffskill を使う - 大きな機能開発の stacked PR 計画を立てるには
plan-stacked-prskill を使う - これらが作ったスタックの継続的メンテナンス(カスケード rebase、CI 監視・修正、同期)は本スキルが担う
- 大きな PR/ブランチを stacked PR に分割するには
- gh stack のコマンド仕様・非対話フラグ・exit code は
gh-stackskill が持つ。ここで重複して定義しない。gh-stackは vendored なので編集もしない - conflict 解決は
resolve-merge-conflict、PR base の修正はadjust-pr-base、CI はwatch-ciとfix-github-actions-ciに委譲する - カスケード中に新しいブランチ追加を頼まれたら、カスケードを完走させてから別途対応する