stack-mode について
依存関係のあるプルリクエストスタックを処理するコマンド向けの共通スキル契約です。スタックを特定し、ベースから先端の順に各 PR の既存ワークフローを実行して、ルート PR に集約結果を投稿します。
主な機能
- 013つの方法でスタックを特定
- 02ベースから先端の順に PR を配置
- 03各 PR のワークフローを実行
- 04PR の失敗時に処理を停止
- 05ルート PR にサマリーを投稿
活用シーン
- 01スタック内の各 PR をレビュー
- 02依存する PR に修正ワークフローを適用
- 03スタック全体の処理結果を集約
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ athola/ claude-night-market --skill stack-modeインストール後、Claude Code で/stack-modeを入力して読み込まれたか確認します。
詳細
翻訳後に原文が更新されています。最新の内容は原文をご確認ください。
スタックモード
1 回の実行で、依存関係のある PR のスタック全体を対象に処理する必要があるコマンド向けの共通仕様です。
/pr-review --stack と /fix-pr --stack で使用されます。
目的はシンプルです。共通のベースブランチを対象とするスタックの一部である PR がある場合、スタックのルートに統合サマリーを 1 つ出力し、ベースから先端の順にスタック内のすべての PR に対してコマンドの通常のワークフローを繰り返します。
使用する場合
コマンドが --stack フラグを受け付ける(またはスタックへの所属を自動検出する)場合に、このスキルを読み込みます。複数の PR に対して通常のワークフローを繰り返す必要があるときに使用します。
単一 PR のワークフローでは、このスキルを読み込まないでください。PR ごとのワークフローは変更せず、スタックモードでそれをラップします。
使用しない場合
- スタックを作成するとき(
sanctum:stack-createを使用) - スタック内でリベースをカスケードするとき(
sanctum:stack-rebaseを使用)
仕様
呼び出し元のコマンドは、必ず次の処理を行う必要があります。
--stackのブール値フラグと、--base <branch>による上書きを受け付けます(デフォルト:master)。- メインのワークフローを実行する前に、以下のステップ 1 を呼び出してスタックへの所属を解決します。
- 解決した順序で、PR ごとにメインのワークフローを実行します。
- PR ごとの出力(スレッドへの返信、イシューへのリンクなど)は変更せずに出力します。
- ステップ 4 の形式を使って、ルート PR にスタックレベルのサマリーコメントを 1 つ出力します。
- PR ごとのゲートルールを遵守します(すべての PR で、それぞれスレッドの解決とイシューの追跡が必要です)。
呼び出し元のコマンドは、次の処理を行ってもかまいません。
- スタックへの所属を自動検出し、
--stackを明示的に要求する代わりにユーザーに確認します。 - PR ごとの失敗によって後続の PR のコンテキストが古くなる場合、反復処理を途中で終了します。
必須の進捗追跡
呼び出し元は、TodoWrite の項目を作成します。
stack-mode:membership-resolvedstack-mode:iteration-completestack-mode:root-summary-posted
ステップ 1:スタックへの所属を解決する(membership-resolved)
開始 PR 番号 $PR_NUM と省略可能なベース $BASE(デフォルト:master)を指定し、3 つの方法を順番に試します。スタックのサイズが 2 以上となる方法が見つかった時点で終了します。
方法 A:ブランチ名の規則(最も低コスト)
START_HEAD=$(gh pr view "$PR_NUM" \
--json headRefName -q .headRefName)
# stack/<feature-name>/<slice-name>
if [[ "$START_HEAD" =~ ^stack/([^/]+)/.+$ ]]; then
FEATURE="${BASH_REMATCH[1]}"
PREFIX="stack/${FEATURE}/"
# All local branches with the same prefix
STACK_BRANCHES=$(git branch --list "${PREFIX}*" \
| sed 's/^[* ]*//' | sort)
# Map branches -> PR numbers (skip branches with no PR)
STACK_PRS=()
for branch in $STACK_BRANCHES; do
pr=$(gh pr list --head "$branch" \
--json number --jq '.[0].number')
[ -n "$pr" ] && STACK_PRS+=("$pr")
done
echo "strategy=A prs=${STACK_PRS[*]}"
fi
方法 B:スタックサマリーコメント(最も信頼性が高い)
stack-push はルート PR に、列が # | Branch | PR の ## Stack Markdown テーブルを投稿します。これを解析します。
# Find the root PR of the stack containing $PR_NUM
# The current PR may be the root, or may link to it via
# a "Part of stack `stack/<feature>`" phrase in its body
BODY=$(gh pr view "$PR_NUM" --json body -q .body)
# Look for a "## Stack" table in THIS PR's comments
TABLE=$(gh pr view "$PR_NUM" --json comments \
--jq '.comments[] | select(.body | contains("## Stack"))
| .body' | head -1)
# If not on this PR, look for the root PR reference
if [ -z "$TABLE" ]; then
ROOT_REF=$(echo "$BODY" \
| grep -oE 'stack/[^ `]+' | head -1)
# ... resolve ROOT_REF -> root PR number and re-fetch
fi
# Parse the table: extract all `#<num>` from the PR column
STACK_PRS=($(echo "$TABLE" \
| grep -oE '#[0-9]+' | tr -d '#' | sort -un))
echo "strategy=B prs=${STACK_PRS[*]}"
方法 C:ベースチェーンをたどる(フォールバック)
$PR_NUM から両方向にたどります。
- 上方向:
baseRefNameが$BASEと等しくなるまで、繰り返しbaseRefNameを解決します。訪問した各 head がスタックメンバーです。 - 下方向:
baseRefNameが訪問済みのいずれかの head と等しい、オープンな PR を探します。再帰的に処理します。
# Ascend from $PR_NUM to the root
CUR=$PR_NUM
CHAIN=($CUR)
while true; do
BASE_REF=$(gh pr view "$CUR" \
--json baseRefName -q .baseRefName)
[ "$BASE_REF" = "$BASE" ] && break
PARENT=$(gh pr list --head "$BASE_REF" \
--state open --json number --jq '.[0].number')
[ -z "$PARENT" ] && break
CHAIN=("$PARENT" "${CHAIN[@]}")
CUR=$PARENT
done
# Descend from the root collecting children
ROOT=${CHAIN[0]}
FRONTIER=("$ROOT")
while [ ${#FRONTIER[@]} -gt 0 ]; do
PARENT=${FRONTIER[0]}
FRONTIER=("${FRONTIER[@]:1}")
PARENT_HEAD=$(gh pr view "$PARENT" \
--json headRefName -q .headRefName)
CHILDREN=($(gh pr list --base "$PARENT_HEAD" \
--state open --json number --jq '.[].number'))
for c in "${CHILDREN[@]}"; do
CHAIN+=("$c")
FRONTIER+=("$c")
done
done
STACK_PRS=("${CHAIN[@]}")
echo "strategy=C prs=${STACK_PRS[*]}"
順序
方法にかかわらず、最終リストをベースから先端の順に並べます(ルート PR が最初、リーフ PR が最後)。ルートは、baseRefName が $BASE と等しい PR です。
ROOT_PR=""
for p in "${STACK_PRS[@]}"; do
base=$(gh pr view "$p" --json baseRefName -q .baseRefName)
if [ "$base" = "$BASE" ]; then
ROOT_PR=$p
break
fi
done
スタックのサイズが 1 の場合、コマンドは単一 PR モードにフォールバックし、警告 stack-mode: only one PR found; --stack has no effect, proceeding as single-PR を出力する必要があります。
GitLab / Bitbucket
上記の gh 呼び出しに相当する glab / Bitbucket の方法については、Skill(leyline:git-platform) を参照してください。3 つの方法はそのまま置き換えられます。GitLab の MR 依存関係はベースチェーンの走査に相当し、glab mr view --output json の形式は gh pr view と一致します。
ステップ 2:ユーザーに確認する(明示的な指定がない場合)(membership-resolved)
呼び出し元が --stack を渡していないものの、ステップ 1 でサイズ 2 以上のスタックが解決された場合、スタックを表示して次のように尋ねます。
Detected stack of N PRs rooted at #<root>:
1. #<root> <title>
2. #<p2> <title>
...
Run the workflow on the full stack? [y/N]
デフォルトは「N」(単一 PR モード)です。--stack が明示的に指定されている場合、この確認は省略します。
ステップ 3:反復処理する(iteration-complete)
ベースから先端の順に、PR ごとに呼び出し元のメインワークフローを 1 回実行します。
FAILED_PRS=()
for p in "${STACK_PRS[@]}"; do
echo "=== stack-mode: PR #$p ($(($idx+1))/${#STACK_PRS[@]}) ==="
# Caller hook: run the single-PR workflow on $p
# This includes Gate 1 (thread resolution) and
# Gate 2 (issue tracking) per the caller's contract.
if ! run_single_pr_workflow "$p"; then
FAILED_PRS+=("$p")
# Stop iteration: downstream PRs may now be stale
break
fi
done
いずれかの PR で失敗した場合、コマンドは停止し、stack-mode: halted at #<pr>; downstream PRs untouched を報告する必要があります。後続の PR はコンテキストが変わっている可能性があるため、意図的に処理しません。
ステップ 4:ルートにスタックサマリーを投稿する(root-summary-posted)
反復処理が正常に完了したら、統合サマリーコメントを 1 つルート PR に投稿します。このサマリーは PR ごとのサマリーを置き換えるものではなく、それらを相互にリンクします。
## Stack <Command Name> Summary
**Stack**: N PRs rooted at #<root>, base `<base-branch>`
**Command**: <command> (e.g., /fix-pr, /pr-review)
**Run**: <timestamp>
| # | PR | Branch | Status | Per-PR Summary |
|---|----|--------|--------|----------------|
| 1 | #<root> | `stack/<feat>/<root-slice>` | <done/skipped/failed> | [link to comment] |
| 2 | #<p2> | `stack/<feat>/<slice-2>` | <done/skipped/failed> | [link to comment] |
| ... | ... | ... | ... | ... |
### Cross-PR Notes
- <Any observations that spanned multiple PRs>
- <Issues created that reference multiple stack PRs>
- <Base-PR-merged cascade reminders, if any>
### Next Steps
- If root PR merges, run `Skill(sanctum:stack-rebase)` to
cascade the base update through descendants.
- Reviewers: rebase-triggered stale-review flags on
descendants are expected after any force-push.
gh pr comment "$ROOT_PR" --body-file <file> を使用して投稿します。
失敗モードと復旧
| 失敗 | 検出方法 | 復旧方法 |
|---|---|---|
| スタックのサイズが 1 | ステップ 1 が 1 つの PR を返す | 警告を出し、単一 PR モードにフォールバックする |
| ベースチェーンがループする | 上方向へのたどりで訪問済みセットを使用 | 停止し、stack-mode: cycle detected を報告する |
| スタック途中の PR がゲートに失敗した | 呼び出し元がエラーを発生させる | 反復を停止し、後続の PR には手を加えない |
| Strategy B のテーブルの形式が不正 | 解析に失敗 | Strategy C に進む |
| 認証スコープが不十分 | gh が 403 を返す | 明確なエラーを表示して停止し、呼び出し元がユーザーに伝える |
備考
- スタックモードは追加機能です。単一 PR の動作に変更はありません。 新しく追加されるのは反復処理とルートの概要のみです。
- PR ごとの Gate 1(スレッドの解決)と Gate 2(課題の追跡)は変更されません。各 PR は引き続き、それぞれ独立して各ゲートを通過します。
- Gate 1 をスタック全体で共有してはいけません。ある PR で解決済みのスレッドが、別の PR でも解決済みとは限りません。
- ルートの概要は冪等です。スタックモードを再実行した場合、概要コメントは重複させずに更新します。
キーとして
## Stack <Command Name> Summaryを照合します。 stack-modeは読み取りとオーケストレーションを行うスキルであり、ブランチをプッシュ、リベース、編集することはありません。これらはstack-create、stack-push、stack-rebaseが扱います。
完了条件
- 3 つの戦略のうち少なくとも 1 つ(A:ブランチ名、B:スタックコメント、C:ベースチェーンをたどる)を使用してスタックのメンバーシップを特定し、反復処理の開始前に PR をベースから先端の順に並べている
- 必須の TodoWrite 項目 3 つ(
stack-mode:membership-resolved、stack-mode:iteration-complete、stack-mode:root-summary-posted)を作成し、順番に完了としてマークしている - すべての PR の処理後、ルート PR に統合された
## Stack <Command> Summaryコメントがちょうど 1 つ投稿されている - スタックのサイズが 1 の場合、警告
stack-mode: only one PR found; --stack has no effectを出し、単一 PR モードを使用している - PR がゲートに失敗した場合、反復処理を停止し、後続の PR には手を加えず、
stack-mode: halted at #<pr>を報告している