| name | pr-queue |
| description | This skill should be used when the user asks to '複数のPRを順番に処理して', 'PRキューを回して', 'PRをまとめてmergeまで面倒見て', 'PRをスタックして', 'process these PRs in order', 'orchestrate PR merges', 'PRパイプライン', 'pr-stack', or wants multiple pull requests analyzed for dependencies, chained via base branches, and processed sequentially by Opus sub-agents. |
| tools | ["Bash(git *)","Bash(gh pr *)","Bash(gh run *)","Agent","SendMessage","Read","Write","Edit","TodoWrite","AskUserQuestion","ScheduleWakeup"] |
PR Queue
複数の PR を自動分析して依存関係を検出し、base ブランチをチェーンして逐次処理するオーケストレーションスキル。
pr-watch-auto との差分:
pr-watch-auto は単一 PR のライフサイクル(base 追従 → conflict 解消 → CI 監視 → failure 修正 → merge 完了)を foreground で完結させる
pr-queue は複数 PR の依存関係を自動検出し、base ブランチをチェーンし、処理順序を制御するオーケストレーター。個々の PR 処理は Opus サブエージェント経由で pr-watch-auto に委譲する
MUST: このスキルを実行するセッションはオーケストレーション(状態管理・依存解決・サブエージェント起動・ユーザー対話)のみを行う。conflict 解消、CI 修正、push などの PR 実処理は一切行わず、すべて Opus サブエージェントに委譲する。
データモデル
PREntry:
id: string // PR 番号("#123")またはブランチ名
number: int // gh pr number
url: string // PR URL
branch: string // head branch name
base: string // current base branch (auto-managed)
original_base: string // base branch at queue registration time
depends_on: string[] // ids of PRs that must merge before this one
chain_position: int // position in the processing chain (1-indexed)
status: "pending" | "analyzing" | "ready" | "dispatched" | "merged" | "failed" | "blocked"
analysis: PRAnalysis // diff analysis result from sub-agent
agent_id: string // sub-agent id (set when dispatched)
failure_reason: string // set when failed or blocked
merged_at: string // timestamp when merged
PRAnalysis:
changed_files: string[] // list of changed file paths
summary: string // one-line summary of what this PR does
exports_added: string[] // new exports (functions, types, components)
imports_from_other_prs: string[] // imports that reference files changed in other PRs
dependency_reasons: DependencyReason[] // why this PR depends on others
DependencyReason:
depends_on_pr: string // id of the PR this depends on
reason: string // human-readable explanation
QueueState:
entries: PREntry[]
dependency_chain: string[] // ordered list of dependent PR ids (base chained)
independent_prs: string[] // list of independent PR ids (base = main, processed after chain)
processing_order: string[] // full order: dependency_chain + independent_prs
created_at: string
state_file: string // path to .local-agents/pr-queue/<ts>.md
全体フロー
function pr_queue_orchestrate():
// Phase 1: 入力パースと PR 情報取得
queue = parse_input(user_input)
// Phase 2: サブエージェントで並列分析 → 依存関係自動検出
analyze_all_prs(queue)
// Phase 3: 依存チェーンと独立 PR を分類し、処理順序を決定
build_processing_order(queue)
// Phase 4: ユーザーに依存関係と処理順序を確認
confirm_chain_with_user(queue)
// Phase 5: 依存チェーンの base ブランチをチェーン設定(独立 PR は base 変更なし)
apply_base_chain(queue)
// Phase 6: 処理順に1つずつ Opus サブエージェントで処理
process_sequentially(queue)
Phase 1: 入力パース
PR 番号、URL、ブランチ名のリストを受け取る。依存関係はユーザーに聞かず、Phase 2 で自動検出する。
function parse_input(user_input):
prs = extract_pr_references(user_input) // #123, URL, branch name
for each pr in prs:
entry = PREntry()
entry.id = pr.ref
info = run("gh pr view " + pr.ref + " --json number,url,headRefName,baseRefName,state")
entry.number = info.number
entry.url = info.url
entry.branch = info.headRefName
entry.base = info.baseRefName
entry.original_base = info.baseRefName
entry.status = "pending"
entry.depends_on = []
queue.entries.append(entry)
return queue
Phase 2: PR 分析(サブエージェント並列起動)
各 PR の diff を sonnet サブエージェントで並列分析し、依存関係を自動検出する。
MUST: 分析サブエージェントは全 PR 分を並列で起動する(最大5並列)。
function analyze_all_prs(queue):
all_pr_branches = [e.branch for e in queue.entries]
all_pr_ids = [e.id for e in queue.entries]
// 全 PR の分析サブエージェントを並列起動
agents = []
for entry in queue.entries:
entry.status = "analyzing"
agent = Agent(
prompt: build_analysis_prompt(entry, all_pr_ids, all_pr_branches),
description: "analyze " + entry.id,
model: "sonnet",
run_in_background: true
)
agents.append((agent.id, entry))
// 全分析の完了を待機
for agent_id, entry in agents:
result = wait_for_agent(agent_id)
entry.analysis = parse_analysis_result(result)
entry.status = "pending"
// 分析結果から依存関係グラフを構築
resolve_dependencies(queue)
function build_analysis_prompt(entry, all_pr_ids, all_pr_branches):
return """
以下の PR の diff を分析し、依存関係情報を抽出してください。
対象 PR: {entry.url} (#{entry.number})
ブランチ: {entry.branch}
同時に処理される他の PR:
{for id, branch in zip(all_pr_ids, all_pr_branches) if id != entry.id:
"- " + id + " (branch: " + branch + ")"}
以下のコマンドで diff を取得して分析してください:
gh pr diff {entry.number}
分析観点:
- 変更ファイル一覧
- この PR が何をしているか(1行要約)
- 新規に export している関数・型・コンポーネント
- 他の PR が変更しているファイルへの import/参照があるか
- 他の PR で追加される export を使用しているか
- 同じファイルを変更している他の PR はあるか
以下の形式で報告:
CHANGED_FILES: [comma-separated file paths]
SUMMARY: [one-line summary]
EXPORTS_ADDED: [comma-separated export names, or "none"]
DEPENDS_ON: [list of PR ids this PR depends on, with reasons]
- {pr_id}: {reason}
DEPENDS_ON_NONE: (if no dependencies detected)
"""
function resolve_dependencies(queue):
for entry in queue.entries:
if entry.analysis and entry.analysis.dependency_reasons:
for reason in entry.analysis.dependency_reasons:
if reason.depends_on_pr in [e.id for e in queue.entries]:
entry.depends_on.append(reason.depends_on_pr)
// 相互依存の検出: 同じファイルを変更している PR 同士
for i, a in enumerate(queue.entries):
for j, b in enumerate(queue.entries):
if i >= j:
continue
overlap = set(a.analysis.changed_files) & set(b.analysis.changed_files)
if overlap and a.id not in b.depends_on and b.id not in a.depends_on:
// 相互に同じファイルを触っている場合、PR 番号が小さい方を先にする
if a.number < b.number:
b.depends_on.append(a.id)
else:
a.depends_on.append(b.id)
validate_no_circular_dependencies(queue)
Phase 3: 依存チェーンと独立 PR の分類
依存関係グラフから「依存チェーン」(依存を持つ or 依存される PR)と「独立 PR」(依存なし&被依存なし)を分離し、処理順序を決定する。
独立 PR を依存チェーンに混ぜると、base が無関係な PR のブランチに設定され diff が汚染される。独立 PR は base = main を維持し、依存チェーンの処理完了後に逐次処理する。
ダイヤモンド依存(A→B, A→C, B+C→D)は線形化される。同じ依存レベルの PR 同士(B と C)はチェーン上の前後関係で base が連鎖するため、PR ページの diff 表示に直前 PR の変更が含まれるが、merge 順序は依存関係を守るため機能的に正しい。Phase 4 でダイヤモンド構造をユーザーに明示すること。
function build_processing_order(queue):
// 依存関係に参加している PR を特定
involved_in_deps = set()
for entry in queue.entries:
if entry.depends_on:
involved_in_deps.add(entry.id)
for dep_id in entry.depends_on:
involved_in_deps.add(dep_id)
dependent_entries = [e for e in queue.entries if e.id in involved_in_deps]
independent_entries = [e for e in queue.entries if e.id not in involved_in_deps]
// 依存チェーンをトポロジカルソート(同順位は PR 番号昇順で tiebreak)
dependency_chain = topological_sort(dependent_entries, tiebreak=by_pr_number_asc)
// 独立 PR は PR 番号昇順で末尾に配置
independent_prs = sorted([e.id for e in independent_entries], key=lambda id: find_entry(id).number)
queue.dependency_chain = dependency_chain
queue.independent_prs = independent_prs
queue.processing_order = dependency_chain + independent_prs
for i, entry_id in enumerate(queue.processing_order):
entry = find_entry(entry_id)
entry.chain_position = i + 1
function validate_no_circular_dependencies(queue):
visited = {}
for entry in queue.entries:
if detect_cycle(entry, visited, set()):
report_circular_dependency(entry)
ask_user_to_resolve()
Phase 4: ユーザー確認
検出した依存関係と処理順序をユーザーに提示し、承認を得る。依存チェーンと独立 PR を区別して表示する。
function confirm_chain_with_user(queue):
summary = "## 依存チェーン(base をチェーン設定)\n\n"
for entry_id in queue.dependency_chain:
entry = find_entry(entry_id)
deps = ", ".join(entry.depends_on) if entry.depends_on else "なし"
summary += "- " + str(entry.chain_position) + ". " + entry.id
summary += " — " + entry.analysis.summary + "\n"
summary += " deps: [" + deps + "]\n"
if entry.analysis.dependency_reasons:
for reason in entry.analysis.dependency_reasons:
summary += " - " + reason.depends_on_pr + ": " + reason.reason + "\n"
if queue.independent_prs:
summary += "\n## 独立 PR(base = main のまま、チェーン後に処理)\n\n"
for entry_id in queue.independent_prs:
entry = find_entry(entry_id)
summary += "- " + str(entry.chain_position) + ". " + entry.id
summary += " — " + entry.analysis.summary + "\n"
response = AskUserQuestion(
summary + "\nこの順序で処理しますか?",
options: [
"この順序で処理する",
"順序を変更する",
"中断する"
]
)
if response == "順序を変更する":
new_order = get_user_order_modification()
queue.processing_order = new_order
recalculate_chain_positions(queue)
if response == "中断する":
exit()
Phase 5: base ブランチチェーン設定
MUST: 依存チェーン内の PR のみ、処理順に前の PR のブランチを base に設定する。チェーンの先頭 PR の base はそのまま(通常 main)。独立 PR は base を変更しない(main を維持)。
gh pr edit --base はオーケストレーターが直接実行する(状態管理の一環であり、PR 実処理ではないため)。
function apply_base_chain(queue):
// 依存チェーンの PR のみ base をチェーン
for i, entry_id in enumerate(queue.dependency_chain):
entry = find_entry(entry_id)
if i == 0:
log("PR " + entry.id + " (チェーン先頭): base = " + entry.base + " (変更なし)")
continue
prev_entry_id = queue.dependency_chain[i - 1]
prev_entry = find_entry(prev_entry_id)
new_base = prev_entry.branch
run("gh pr edit " + entry.number + " --base " + new_base)
entry.base = new_base
log("PR " + entry.id + " の base を " + new_base + " に変更(← PR " + prev_entry.id + ")")
// 独立 PR は base 変更なし
for entry_id in queue.independent_prs:
entry = find_entry(entry_id)
log("PR " + entry.id + " (独立): base = " + entry.base + " (変更なし)")
state_file = create_state_file(queue)
queue.state_file = state_file
Phase 6: 逐次処理
MUST: 処理順(依存チェーン → 独立 PR)に 1つずつ Opus サブエージェントに処理を委譲する。前の PR が merge されてから次の PR を dispatch する。
function process_sequentially(queue):
for i, entry_id in enumerate(queue.processing_order):
entry = find_entry(entry_id)
if entry.status in ["merged", "failed", "blocked"]:
continue
// 依存先がすべて merged であることを確認
for dep_id in entry.depends_on:
dep = find_entry(dep_id)
if dep.status == "failed":
entry.status = "blocked"
entry.failure_reason = "dependency " + dep_id + " failed"
propagate_failure_to_dependents(entry, queue)
break
if dep.status != "merged":
entry.status = "blocked"
entry.failure_reason = "dependency " + dep_id + " not yet merged"
break
if entry.status == "blocked":
update_state_file(queue.state_file, queue)
report_blocked_and_ask_user(entry, queue)
continue
// Opus サブエージェントに処理を委譲
agent_id = dispatch_pr_agent(entry)
entry.status = "dispatched"
entry.agent_id = agent_id
// サブエージェントの完了を待機
result = wait_for_agent(agent_id)
if result.merged:
entry.status = "merged"
entry.merged_at = now()
update_base_after_merge(entry, queue, i)
else:
entry.status = "failed"
entry.failure_reason = result.reason
propagate_failure_to_dependents(entry, queue)
update_state_file(queue.state_file, queue)
report_failure_and_ask_user(entry, queue)
update_state_file(queue.state_file, queue)
report_current_status(queue)
// 完了レポート
update_state_file(queue.state_file, queue)
report_final_summary(queue)
base ブランチ管理(merge 後)
PR が merge されたら、次の PR の base を更新する。squash merge により前の PR のブランチが消えるため、base を main(デフォルトブランチ)に付け替える。
オーケストレーターが直接 gh pr edit --base を実行する(状態管理の一環)。
function update_base_after_merge(merged_entry, queue, processing_index):
// 依存チェーン内の merge の場合: チェーン内の次の PR の base を更新
if merged_entry.id in queue.dependency_chain:
chain_idx = queue.dependency_chain.index(merged_entry.id)
if chain_idx + 1 < len(queue.dependency_chain):
next_entry_id = queue.dependency_chain[chain_idx + 1]
next_entry = find_entry(next_entry_id)
// squash merge 後、前のブランチは削除される → base を main に付け替え
default_branch = merged_entry.original_base // 通常 "main"
run("gh pr edit " + next_entry.number + " --base " + default_branch)
next_entry.base = default_branch
log("PR " + next_entry.id + " の base を " + default_branch + " に更新(PR " + merged_entry.id + " merge 完了のため)")
// 独立 PR は base = main のままなので更新不要
サブエージェント dispatch
MUST: 個々の PR 処理は Opus サブエージェントに委譲する。オーケストレーターセッションでは git 操作・ファイル編集・CI 修正を一切行わない。
function dispatch_pr_agent(entry):
prompt = build_pr_agent_prompt(entry)
agent = Agent(
prompt: prompt,
description: "PR " + entry.id + " watch",
model: "opus",
run_in_background: true
)
return agent.id
function build_pr_agent_prompt(entry):
return """
以下の PR を merge 完了まで処理してください。
PR: {entry.url} (#{entry.number})
ブランチ: {entry.branch}
base: {entry.base}
処理内容(pr-watch-auto 相当):
1. base ブランチ({entry.base})の最新を merge
2. conflict があれば解消し、/verify-merge で検証
3. push して CI を監視
4. CI failure があれば調査・修正して再 push
5. merge 可能になったら merge(squash)
6. merge 完了を確認
完了したら以下の形式で報告:
- status: merged / failed / blocked
- merge_commit: (hash if merged)
- actions_taken: (実行した操作の要約)
- failure_reason: (failed/blocked の場合)
PROHIBIT: ScheduleWakeup の使用。merge まで foreground で処理を継続すること。
"""
状態ファイル
.local-agents/pr-queue/<timestamp>-<rand>.md に状態を記録する。
function create_state_file(queue):
path = ".local-agents/pr-queue/" + timestamp() + "-" + rand(4) + ".md"
write_state(path, queue)
return path
function write_state(path, queue):
content = "# PR Queue State\n\n"
content += "## 依存チェーン(base チェーン設定済み)\n\n"
for entry_id in queue.dependency_chain:
entry = find_entry(entry_id)
icon = status_icon(entry.status)
deps = ", ".join(entry.depends_on) if entry.depends_on else "なし"
content += "- " + icon + " " + str(entry.chain_position) + ". "
content += entry.id + " (" + entry.status + ")"
content += " — base: " + entry.base + ", deps: [" + deps + "]\n"
if entry.analysis:
content += " " + entry.analysis.summary + "\n"
if queue.independent_prs:
content += "\n## 独立 PR(base = main)\n\n"
for entry_id in queue.independent_prs:
entry = find_entry(entry_id)
icon = status_icon(entry.status)
content += "- " + icon + " " + str(entry.chain_position) + ". "
content += entry.id + " (" + entry.status + ")"
content += " — base: " + entry.base + "\n"
if entry.analysis:
content += " " + entry.analysis.summary + "\n"
content += "\n## ログ\n\n"
write(path, content)
完了レポート
function report_final_summary(queue):
merged = [e for e in queue.entries if e.status == "merged"]
failed = [e for e in queue.entries if e.status == "failed"]
blocked = [e for e in queue.entries if e.status == "blocked"]
report:
- 処理完了: {len(merged)} PR merged
- 失敗: {len(failed)} PR (各 failure_reason)
- ブロック: {len(blocked)} PR (各 failure_reason)
- 状態ファイル: {state_file path}
Bundled Resources
(No bundled resources found)