implement-issue-tree

implement-issue-tree

親イシュー配下のサブイシュー(孫含む)を依存順を保ちつつ worktree で並列に自動実装・push 前 review・PR 作成・CI 監視・マージ可能状態化まで一括自動化。 「イシューツリーを並列実装」「配下のサブイシューをまとめて実装」「ツリー全体を並列で実装して」「イシュー階層を自動開発」で使用。 per-issue 計画立案(Plan: セッション継承モデル)→実装(Implement: sonnet)の分業。push 前 review(Review 通過後にのみ push・PR 作成して CI を 1 回だけ起動)。 外部チェック構成は args の externalChecks で明示({"app", "context"} の組で宣言。[] で「なし」を確定して不要待機なし・未指定なら自動マージ停止・slug のみの旧形式は自動マージ fail-closed)。 自動 squash merge は autoMerge: true + externalChecks 明示(全 App の信頼済み context 宣言込み)の opt-in で実行(merge-exec の自己取得再検証 + サーバー側 branch protection 実測が前提。既定 false はマージ可能状態で停止し人間がマージ。サーバー側 workflow サンプル(upstream の docs/implement-issue-tree/auto-merge-sample.yml 参照)+ branch protection への委譲も可)。並列度(parallel)と依存(dependsOn)で実行順を制御。 Phase 単位で厳密に直列化したい場合は phaseGate: true(opt-in。ルート直下の子〔Phase 親〕を sub-issues リスト順に直列化し、前 Phase の全子孫が merged/closed になるまで次 Phase 配下に着手しない。既定 false は現行の post-order 優先度のみ)。 単一イシューの実装は implement-issue、PR レビューは implement-review-pr を参照。

2Star
0Fork
更新于 2026/9/25
请求的译文尚未完成,当前显示原始混合语言内容。
SKILL.md
只读
名称
implement-issue-tree
描述

親イシュー配下のサブイシュー(孫含む)を依存順を保ちつつ worktree で並列に自動実装・push 前 review・PR 作成・CI 監視・マージ可能状態化まで一括自動化。 「イシューツリーを並列実装」「配下のサブイシューをまとめて実装」「ツリー全体を並列で実装して」「イシュー階層を自動開発」で使用。 per-issue 計画立案(Plan: セッション継承モデル)→実装(Implement: sonnet)の分業。push 前 review(Review 通過後にのみ push・PR 作成して CI を 1 回だけ起動)。 外部チェック構成は args の externalChecks で明示({"app", "context"} の組で宣言。[] で「なし」を確定して不要待機なし・未指定なら自動マージ停止・slug のみの旧形式は自動マージ fail-closed)。 自動 squash merge は autoMerge: true + externalChecks 明示(全 App の信頼済み context 宣言込み)の opt-in で実行(merge-exec の自己取得再検証 + サーバー側 branch protection 実測が前提。既定 false はマージ可能状態で停止し人間がマージ。サーバー側 workflow サンプル(upstream の docs/implement-issue-tree/auto-merge-sample.yml 参照)+ branch protection への委譲も可)。並列度(parallel)と依存(dependsOn)で実行順を制御。 Phase 単位で厳密に直列化したい場合は phaseGate: true(opt-in。ルート直下の子〔Phase 親〕を sub-issues リスト順に直列化し、前 Phase の全子孫が merged/closed になるまで次 Phase 配下に着手しない。既定 false は現行の post-order 優先度のみ)。 単一イシューの実装は implement-issue、PR レビューは implement-review-pr を参照。

implement-issue-tree

親イシュー番号を指定し、配下のサブイシュー(孫含む)を依存順を保ちつつ worktree で並列に自動実装・ローカル diff レビュー・push + PR 作成・CI 監視・マージ可能状態化まで自動化する Workflow を起動する。squash merge は autoMerge: true + externalChecks 明示(全 App の信頼済み check context 宣言込み)の opt-in ランでのみクライアント側で実行する(references/automerge-design.md の「クライアント側自動マージの設計」節参照)。既定(autoMerge 未指定 / false)ではマージせず停止し、マージは GitHub 上で人間が行う。

CI リソース節約のため「push 前 review」設計を採用している。Implement フェーズではローカルブランチにコミットのみ積み、Review が全通過した後にはじめて push・PR 作成を行う。Review が収束失敗した場合は push も PR も作らないため、CI が一切起動しない。push(PR 作成時・Merge ループの fix 後の再 push)の直前には必ず base ブランチを取り込む(git fetch → git merge)。並列ラン(parallel >= 2)で兄弟イシューの PR が先にマージされていると、作成時点の base が既に古くコンフリクトしている場合があり、その状態のまま push すると GitHub は test merge commit を作れず pull_request トリガーの CI check-run が 1 件も発行されないため、push 前ゲートで解消を試みてから push する(解消不能なら push 自体を止める)。さらに push 直後(PR 作成時・fix の再 push 時)には、push した head にチェックが発行されたか(check-run の total_count + combined status の statuses 件数の合計。gh pr checks / merge-exec と同じ集計定義。commit status のみを発行する CI で 0 件と誤判定しないため)と mergeable を有界(30 秒間隔・最大 5 分)で確認し、checksStarted / mergeableAfterPush として返す(push 前ゲート通過後に兄弟 PR がマージされて base が動いたケースを、監視ラウンドを消費する前に捕捉する)。mergeableAfterPush: CONFLICTING を受けたホストは monitor を 1 ラウンドも起動せずに base 取り込み(baseMergeCount で有界)へ直行する。両値はエージェントの自己申告でありマージ判定には使わない(分岐ヒントと状態ファイルへの記録専用。_/issue-trees/<n>.json の pushChecksStarted / pushMergeable)。

末端の実装イシューは post-order DFS の順序を優先度として空きスロットへ貪欲投入し、最大 parallel(既定 3)件まで並列実行する。各 implement / fix は独立した git worktree で隔離実行されるため、並列でもブランチ・working copy が衝突しない。機能的依存(dependsOn)と親子関係(親は全子の完了を待つ verify-close)だけが待機条件となる。

前提条件

  • gh CLI がインストールされ、認証済みであること(gh auth status で確認)
  • jq CLI がインストールされていること(command -v jq で確認)。「全チェックが pass に見えるのにマージが進まない場合(cancel された run の残存 check)」節の人間の診断専用コマンド (B) は gh api --paginate --slurp の生 JSON を外部の jq へパイプして平坦化・集約するため、gh --jq だけでは代替できない。未導入の場合はそのコマンドを実行せず(rerun もせず)blocked として扱う
  • awk CLI がインストールされていること(command -v awk で確認)。同節のエージェント実行可能コマンド (A) は --jq がページ単位にしか適用できないため、ページ跨ぎの重複を集約する際にシェル側 awk へ依存する。未導入の場合はそのコマンドを実行せず UNDETERMINED(判定不能)として扱う
  • git working tree が clean であること(git status で確認)
  • マージ先ブランチが CI green の状態であること(autoMerge 運用ではランの完了後にも確認する。後述の strict = false 前提により、古い base に対して成功したチェックのままマージされ得るため)。この確認はマージ先ブランチへの push で CI が起動することに依存する。push トリガの workflow が無い、または paths フィルタで該当 head では起動しないリポジトリでは前提確認・完了後確認のいずれも検証不能であり、autoMerge: true は非推奨とする。成立可否の確認手順(対象(マージ先)ブランチを検査するプローブ。branch 未指定時のみ既定ブランチへフォールバック)と不成立時の扱いは references/automerge-design.md の「補償策の成立確認(base CI プローブ)」節を参照
  • (autoMerge: true で使う場合)ベースブランチの ruleset で required status checks の strict(マージ前の base 最新化必須 = strict_required_status_checks_policy)を false にしていること。true だと 1 件マージするたびに他の open PR の base が陳腐化し、並列ラン(parallel >= 2)が収束しない。G0 は strict を要件にしないため false でも自動マージは成立する(references/automerge-design.md の「strict を G0 の要件にしない理由」節)
  • 対象リポジトリへの書き込み権限があること
  • 親イシューと子イシューが GitHub の sub-issues API で紐付いていること(紐付けは create-issue / create-issue-tree を参照)

使い方

Workflow ツールで scriptPath にこのスキルディレクトリ内の scripts/implement-issue-tree.js を指定して起動する。パスは導入形態で異なり、後述の merge-guard hook のパスと同じ導入形態なら同じルート配下にある(js と hook は必ず同一のスキルディレクトリに同居する)。3 レイアウト:

  • upstream skills/ レイアウト(本リポジトリ Fandhe-AI/agent-cli-skills のソース): skills/implement-issue-tree/scripts/implement-issue-tree.js
  • .agents/skills/ に vendored(npx skills add Fandhe-AI/agent-cli-skills で導入した downstream リポジトリ): .agents/skills/implement-issue-tree/scripts/implement-issue-tree.js
  • .claude/skills/ symlink 経由(本リポジトリが内部参照に使うレイアウト。実体は skills/ を指す symlink): .claude/skills/implement-issue-tree/scripts/implement-issue-tree.js
{
  "scriptPath": "<このスキルディレクトリ>/scripts/implement-issue-tree.js",
  "args": {
    "parent": "<親イシュー番号>",
    "branch": "<マージ先ブランチ(省略時 main)>",
    "parallel": "<並列度 1〜8(省略時 3)>",
    "phaseGate": "<boolean。true でルート直下の子(Phase 親 or leaf)を sub-issues リスト順(siblingIndex)に直列化し、前 Phase の全子孫が merged/closed になるまで次 Phase 配下のノードに着手しない(opt-in。Issue #494)。既定 false は現行動作(post-order 優先度のみ・追い越しあり)を完全に維持する。boolean 以外はエラーで停止>",
    "externalChecks": "<外部チェック App と信頼済み required check context の組の配列(例: [{\"app\": \"cursor\", \"context\": \"Cursor Bugbot\"}]。使用しない場合は []。slug 文字列のみの旧形式も受理するが、context 未宣言のためクライアント側自動マージは fail-closed で停止する)>",
    "optinTestCommands": "<人間がラン起動時に明示する、opt-in テストとして実行してよいコマンドの承認一覧(文字列配列・最大 20 件。省略時 []。イシュー本文の <!-- optin-tests: ... --> 宣言はこの一覧と正規化後の文字列完全一致でのみ採用される。1 件でも許可形式外なら起動時エラーで停止。PR #503 codex P0: 本文だけでは新しいコマンドを持ち込めない設計。次節「opt-in テストの宣言」参照)>",
    "autoMerge": "<boolean。true + externalChecks 明示(確定。全 App の信頼済み context 宣言込み)でクライアント側 squash merge を実行する(opt-in。references/automerge-design.md の「クライアント側自動マージの設計」節参照)。既定 false / externalChecks 未確定時はマージ可能状態で停止し、マージは GitHub 上で人間が行うか、サーバー側 auto-merge workflow(upstream の docs/implement-issue-tree/auto-merge-sample.yml)+ branch protection に委ねる>",
    "maxResidualWorktrees": "<残置 worktree 総数の上限(0 以上の整数。省略時 100、0 でこの軸のみ上限なし)>",
    "maxResidualWorktreeBytes": "<残置 worktree ディスク使用量の上限(バイト。0 以上の整数。省略時 53687091200 = 50 GiB、0 でこの軸のみ上限なし。件数軸とは独立、Issue #348)>",
    "maxBaseMerges": "<PR が base とコンフリクト(mergeable: CONFLICTING)した際の自動 base 取り込みの回数上限(0〜10 の整数。省略時 3、0 で自動 base 取り込みを無効化しコンフリクトを即 blocked にする)。fixCount とは独立の予算軸(Issue #441)>",
    "repo": "<対象リポジトリの owner/repo(例: \"Fandhe-AI/agent-cli-skills\")。base 取り込み(maxBaseMerges > 0)の worktree routing ガードが期待する owner/repo として使うホスト側明示宣言。未指定時は base 取り込みの自動起動が無効化され、コンフリクトは blocked+quality で終端する(PR #443 codex P0: エージェント自己申告値は信頼境界に使わない)>"
  }
}

例: 親イシュー #42 の配下を main へ、並列度 3・Cursor Bugbot 導入済みで実行する場合(マージ可能状態まで自動で進み、マージは GitHub 上で人間が行う):

{
  "scriptPath": ".claude/skills/implement-issue-tree/scripts/implement-issue-tree.js",
  "args": { "parent": 42, "branch": "main", "parallel": 3, "externalChecks": [{"app": "cursor", "context": "Cursor Bugbot"}] }
}

引数

引数 必須 既定 説明
parent 必須 — 親(ルート)イシュー番号。issue でも可
branch 任意 main マージ先ブランチ。不正な文字を含む場合はエラー
parallel 任意 3 並列実行数(1〜8)。1 を指定すると実質的に直列実行になる
phaseGate 任意 false true でルート(args.parent)直下の子(Phase 親 or leaf)を sub-issues リスト順(siblingIndex。タイトルの feat(phase-N): 等は非信頼データのため解析しない)に直列化する opt-in ゲート。直列化の単位は「ルート直下の子とその子孫全部(部分木)」で、単位 Uₖ(k≥1)の部分木は、それより手前の全単位 U₀…U_{k-1} の前提(子を持つ Phase 親なら自身を除く子孫全部、子を持たない leaf なら自身)がすべて merged/closed になるまで着手しない(Phase 親自身の verify-close 完了は前提に含めない設計。「前 Phase 親の全子が merged/closed」という要件に合わせるため)。新しいスケジューラ状態は作らず、既存の依存グラフ(depsMap)へ合成辺を追加するだけなので、dispatch ループ・前提プローブ(ラン中の人手マージ検知)・前回ランで作成済みの次 Phase PR の monitoring 再開にもそのままゲートが効く(同一引数での再実行でも次 Phase の PR は前 Phase 完了までマージされない)。ゲート辺は循環除去で削除されない(本文由来の逆向き dependsOn の方が無視される)。autoMerge: false では前 Phase の PR がマージ可能状態のまま blocked で停止するため、次 Phase へ進めるには人手マージ後の前提プローブ検知、または再実行を要する。parent に Phase 親そのものを渡した場合はその直下の子が直列化される(仕様上は動くが、ルート〔トラッキング issue〕への指定を推奨)。Phase ごとに別ランを直列起動する必要はない(verify-close がルートまで伝播する)。boolean 以外はエラーで停止(誤記を黙って読み替えない)
externalChecks 任意 未指定 GitHub Actions 以外の外部チェック宣言の配列(最大 10 件)。要素は {"app": "<slug>", "context": "<required check context>"} の組で宣言する(slug は英小文字・数字・ハイフン。複数 context は contexts 配列。slug 文字列のみの旧形式も受理するが context 未宣言としてクライアント側自動マージは fail-closed で停止する)。未指定と [] は意味が異なる
optinTestCommands 任意 [] opt-in テストとして実行してよいコマンドの承認一覧(文字列配列・最大 20 件。parseOptinTestCommands)。イシュー本文の <!-- optin-tests: ... --> 宣言は、正規化(水平空白の連続を 1 個へ畳む)後にこの一覧の値と文字列完全一致したものだけが実装エージェントへ渡る(次節「opt-in テストの宣言」参照)。要素は許可形式(文字集合・パストラバーサル・//・絶対パス・許可ランナー・サブコマンド制約・mvn GAV・deno リモート指定子)を通らなければ起動時エラーで停止する(externalChecks と同じ厳格さ。誤記を黙って読み替えない)。未指定 / [] の場合は本文に宣言があっても全件 invalid(fail-closed。承認一覧が無ければイシュー本文だけでコマンドを持ち込めない)
autoMerge 任意 false true + externalChecks 明示(確定。全 App の信頼済み context 宣言込み)の opt-in ランでクライアント側 squash merge を実行する(references/automerge-design.md の「クライアント側自動マージの設計」節参照。マージは merge-exec の自己取得再検証(HEAD sha・checks・スレッド・外部チェック)+ G0(ベースブランチのサーバー側強制の実測 = required status checks の bypass 不能性(ruleset は bypass_actors 空。classic branch protection のみのリポジトリは非対応 — bypass 不能性の検証に必要な protection 読取が admin 権限を要求し write トークンで証明できないため classic-unsupported で辞退)+ strict 適用(マージ前の base 最新化必須)+ レビュースレッド解消の必須化 + 手順 3 の合格判定対象チェック context の required 化(client-only チェックの不在)+ 外部チェック App の宣言 context + App ID 組(context + integration_id)束縛の required 化 + required checks 全エントリの発行元 integration_id 束縛(同名 commit status 偽装の遮断。検証できなければ issuer-unbound)。確認できなければ server-enforcement-missing で blocked 終端)+ --match-head-commit + merge-verify の独立確認を経る。monitor の出力はマージ経路の入力に使われない)。既定 false・externalChecks 未確定時・信頼済み context 未宣言時(slug のみの旧形式)はマージせず、PR はマージ可能状態の blocked で停止する(実装・push 前 Review・PR 作成・CI 監視・fix ループは値によらず自動で進む)。opt-in を使わない場合、auto-merge はサーバー側 workflow(upstream の docs/implement-issue-tree/auto-merge-sample.yml)+ branch protection への委譲、または GitHub 上での人間マージで行う(対象ブランチに branch protection を設定することを推奨)。注意: merge-guard hook 導入リポでは subagent の gh pr merge が deny されるため opt-in マージと hook は併用できない。boolean 以外はエラーで停止(誤記を黙って読み替えない)
maxResidualWorktrees 任意 100 残置 worktree 総数の上限(DoS 防止ゲートの件数軸。バイト軸 maxResidualWorktreeBytes と独立に併用され、判定は OR=どちらか一方でも超過すれば新規着手を止める)。ラン開始時に横断スキャンで観測した worktree の物理総数(メイン worktree のみ除外。状態ファイル追跡済み=使用中の worktree も数える。使用中かどうかはディスク消費を変えないため)がこの値を超過(>)していたら、ディスク枯渇を防ぐため新規イシューの着手を停止する(fail-closed。既に走行中のイシュー・monitoring の継続は停止しない)。dispatch ループは新規着手の直前に毎回「開始時観測 + 本ラン積み増し(ephemeralWorktrees.length。implement / review / pr-create / fix-routing-error の新規作成台帳)」を再評価し、本ランの積み増しで上限を超えた時点でも以降の新規着手を停止する(バイト軸にも同種の途中経過再評価があるが、算出方法が異なるため後述)。さらに並列投入済みでまだ記録に到達していないタスク分を見込み、新規着手 1 件あたり最大 6 件(implement ×1 + review ×3 + pr-create ×1 + fix-routing-error ×1。EPHEMERAL_KIND_MAX テーブルから導出)、monitoring 再開 1 件あたり最大 1 件(fix-routing-error 分)を予約計上し、「実測 + 予約 + 着手候補分」が上限を超える投入を止める。monitoring 再開自体もこの予約込み判定の対象(ただし item.kind === 'implement' の再開に限る。verify-close ノードとして到達した再開は runVerifyClose が Merge ループへ入らず fix-routing-error を積み増さないため予約 0 で対象外)であり、開始前に同じ projected 判定を適用して超過が見込まれる場合は当該イシューの再開をこの周回に限り defer する(恒久停止はしない。次周回・次回実行で予約解放後に再評価。monitoring 再開自身の開始を無条件で許可すると、monitoring 項目を順次再開し続けるだけで上限を無視して残置数を際限なく増やせるため)。予約起因の超過見込みは今周回の投入見送り(defer)に留め、予約が解放されれば再開する。実測超過は恒久停止する(ただしこの恒久停止=newStartSuppressed は monitoring 再開の開始自体は妨げない設計を維持しており、上記の monitoring 再開専用 defer とは独立したゲート)。ラン開始時の横断スキャン自体が失敗した場合も、いずれかの軸が有効(`maxResidualWorktrees > 0
maxResidualWorktreeBytes 任意 53687091200(50 GiB) 残置 worktree ディスク使用量の上限(DoS 防止ゲートのバイト軸。バイト単位。件数軸 maxResidualWorktrees と独立に検証・無効化でき、判定は OR)。ラン開始時のみの観測ではない。ラン開始時に、残置パス一覧全件へ du -sk を実行して KiB を単純合計する観測に加え、共有 .git object store を除いたメイン worktree の working tree 相当サイズ(measureMainWorktreeContentBytes)を新規 1 worktree あたりの安全側予約 perWorktreeByteReserve として確定する(素の du 値は object store 全量を含み過大予約になるため除外する)。件数軸の「本ラン積み増し再評価」「予約計上」と同じ形で、perWorktreeByteReserve × (台帳件数 − 直近基準確定時点の台帳件数) の projection をラン中の新規着手・monitoring 再開の両方の直前に毎回再評価し、超過見込みで新規着手を止める(projectResidualBytes。基準確定時点までの積み増しは実測基準値に既に含まれているため、そこを差し引かないと二重計上になる)。さらに perWorktreeByteReserve は開始時に確定する下限 floor 値であり、ビルド成果物等で 1 worktree が floor を超えて成長した場合 projection だけでは過小評価し得るため、使い捨て worktree 台帳が BYTE_REMEASURE_LEDGER_INTERVAL(3 件)積み増されるごとに残置パス一覧+台帳パスの合計を実際に du し直し(remeasureResidualBytesIfDue)、実測が上限を超えていれば独立に新規着手を止める。加えて、新規着手(implement)の直前には台帳増分ゲートを介さず必ず実測し直す(remeasureResidualBytesNow。台帳が 3 件増えない間も実行中 worktree はビルド成果物等で成長し得るため、台帳増分だけを契機にすると容量超過後の着手を止められない fail-open が残るため。測定コストは「同一 dispatch 周回内は 1 回」の間引きで有界化する)。この実測し直しは以後の projection の基準(residualBytesAtStart・台帳オフセット byteBaselineLedgerCount)を同一代入で更新する(実測結果を上限超過の即時判定にのみ使って破棄すると、以後の判定が古い基準のまま容量超過の新規着手を許す fail-open になる)。合計(直近の実測基準・projection・実測し直しのいずれか)が上限を超過していたら新規イシューの着手を停止する(fail-closed。既に走行中のイシュー・monitoring の継続は停止しない。件数軸で既に停止済みの場合は追加の抑止はせず観測値のみログへ記録する)。測定不能(du が 1 件でも失敗・非0終了・許可文字集合外のパス混入)は 0 で補わず観測失敗として扱い、この軸が有効なら新規着手を停止する(countResidualWorktrees の「検証不可」計上と同じ fail-closed の理由。実測し直しの失敗も projection へフォールバックせず新規着手を停止する。存在しないパス〔並行 cleanup による削除〕のみ 0 として許容し、du 自体の実エラーのみ測定失敗とする)。エージェントの worktreePath 省略・空文字で台帳に未検証エントリが生じた場合は、この時点で git worktree list --porcelain の物理一覧(メイン除く全件・独立レコードカウントとの件数照合付き)へフォールバックして実測を継続し、フォールバックも失敗した場合(一覧取得不成立・件数不一致・パス検証不可)のみ fail-closed で新規着手を停止する(buildPhysicalByteMeasureTargets。review / pr-create / fix 系 worktree は隔離 worktree 内で detach するため branch 照合による帰属特定ができず、フォールバックは測定専用(削除経路へは流さない)で物理一覧を丸ごと差し替える)。ラン開始時観測が失敗したまま(residualBytesObserved === false)の場合は、monitoring 再開の projected 判定(前述の item.kind === 'implement' 限定の projection)自体も件数軸と同じ fail-closed 方針で defer し、観測が回復するまでそのランの monitoring 再開全体を待機させる(観測不能のまま fix-routing-error worktree の新規作成を許すと容量を確認できないまま超過し得るため)。0 は「このバイト軸のみ上限なし(チェック無効)」の明示オプトアウト(件数軸の fail-closed には影響しない。件数軸の既定値フォールバックについては maxResidualWorktrees 行を参照)。負値・非整数はエラーで停止。既定 50 GiB はビルド成果物の大きいリポジトリでも誤停止しない水準として選んだ値で、配布先リポジトリのファイル量に依存しない絶対閾値として件数軸既定値 100 の妥当性を補強する。残置サイズの合計上限とは独立に、実ディスクの空き容量そのものも新規着手・monitoring 再開の直前に毎回検証し直す(measureFreeDiskKib / remeasureFreeDiskNow。残置合計サイズが上限(既定 50 GiB)未満でも、実ディスクの空き容量自体はそれよりずっと小さいことがあり得るため〔例: 残置 8 GiB・実空き 4 GiB の導入先では、残置サイズだけを見るゲートは 50 GiB に達するまで新規着手を止めない〕、df -Pk でメイン worktree が属するファイルシステムの実空き容量を測定する。ラン開始時 1 回だけの測定では以後の消費を反映できないため、バイト軸の remeasureResidualBytesNow と同じ「新規着手・monitoring 再開の直前に必ず実測し直す(間引きは同一 dispatch 周回内 1 回)」設計を踏襲する。判定に使う必要バイト数は単一 worktree 分の予約とだけ比較しない(実行中タスクの未消費予約・投入済み候補自身の予約を合算していないと過小評価になる)。projectFreeDiskReserveBytes が「実行中タスクの残余予約(reservedUnits。件数軸・バイト軸 (b) と同じ「最大増分 − 記録済み数」の計算をそのまま再利用)+着手候補自身の最大増分(extraReserveUnits)」の合計に 1 worktree あたりの生の容量見積り(rawPerWorktreeByteReserve) を掛けて必要バイト数を算出し、実測空き容量がこれを下回れば抑止する。バイト軸の perWorktreeByteReserve(clampPerWorktreeByteReserve で容量上限に対する予算配分としてクランプ済みの値)はここでは使わない — クランプ後の値は実際の 1 worktree サイズより小さくなり得るため、実ディスクの物理的な枯渇判定に使うと危険側を見逃す。件数軸・バイト軸と同じ OR 評価=いずれか一方でも危険側なら抑止する。測定不能時も 0 で補わず、また古い実測値をそのまま流用せず観測失敗として新規着手を停止する(fail-closed)。大容量環境や parallel を大きく設定する環境では、実行中タスクの投入済み予約(reservedUnits)の合算により必要バイト数が大きくなる(例: raw ~8 GiB × 予約合計 9 件 = 72 GiB)。ラン開始時(reservedUnits: 0)の停止は着手候補自身の予約のみで必要量が決まり、args.parallel を下げても maxResidualWorktreeBytes を変更してもこの必要量は減らない(maxResidualWorktreeBytes は残置サイズ合計の上限であり本ゲートとは独立)ため、実ディスクの空き容量を確保する(メイン worktree の gitignored なビルド成果物・依存関係の削除を含む)ことでのみ解消できる。ラン中(reservedUnits > 0)の停止は投入済み予約の合算が要因になり得るため、空き容量確保に加え args.parallel を下げることも有効)。rawPerWorktreeByteReserve の更新(実測 du 由来の予約見積り再算出)が未解決 implement パス・implement 限定 du 失敗のいずれかで失敗しても、全件測定(du の kib)自体が成功していれば reserveStale: true を返すのみで failed: true にはしない(failed: true にすると、既に容量超過を確定済みでも verify-close を止める全 kind latch へ到達しない cap latch 省略を招くため、容量超過判定・全 kind latch は全件測定成功直後に先出しで確定する)。ただし monitoring 再開の defer 判定は別: reserveStale のまま古い予約見積りで monitoring 再開を進めると再開が作る worktree の見積りが過小評価され容量枯渇を許し得るため、monitoring 再開の defer 判定は `failed
maxBaseMerges 任意 3 args.repo が未指定のランでは値によらず base 取り込みは起動しない(args.repo の形式不正は起動時にエラーで停止しラン自体が始まらない。repo 行参照)。PR が base とコンフリクト(mergeable: CONFLICTING。monitor の conflicting 経路、または merge-exec の not-mergeable 写像のいずれか)した際に、fix 予算(fixCount)を消費せず自動起動する base 取り込み専用エージェント(baseMergePrompt)の回数上限(0〜10 の整数)。monitorsLeft と独立の第 2 の停止性ガードとして機能する。上限到達時は blocked / blockedReason: "quality" で終端する(halt 非カウント。fixCount 上限到達時の needs-fix と異なり、上限到達後も human が PR ブランチへ直接 base を取り込んで push すればコンフリクトが解消し、次回 monitor は conflicting を返さずこの分岐自体を再度通らないため unrecoverable は使わない。blocked 終端後の再実行は monitoring として直接再開する — Recover / PR Create フェーズは通らない。baseMergeCount は上限到達値のまま引き継がれるため、上限到達後の解消には人間が PR ブランチへ base を直接取り込んで push する必要がある)。0 は自動 base 取り込みを無効化し、コンフリクト検出時点で即 blocked にする明示オプトアウト(この場合も blockedReason: "quality")。負値・非整数・小数はエラーで停止(マージゲート入力と同じ厳格さ)
repo 任意(maxBaseMerges > 0 で base 取り込みを使うなら実質必須) 未指定("") 対象リポジトリの owner/repo(例: "Fandhe-AI/agent-cli-skills")。baseMergePrompt の worktree routing ガードが期待する owner/repo(expectedRepo)として使う、人間が明示するホスト側入力。エージェントの自己申告値(外部チェック観測エージェント等)は一切使わない(自己申告値を信頼境界に使うと誤配置 worktree からの別リポジトリへの push を防げない)。未指定は expectedRepo を空文字のまま確定し、base 取り込み(baseMergePrompt の起動)を行わず conflicting は即 blocked / blockedReason: "quality" で終端する(fail-closed。実装・push 前 Review・PR 作成・CI 監視は値によらず自動で進む)。形式不正(isValidRepoSlug の owner/repo 形式を通らない値)は起動時にエラーで停止(マージゲート入力と同じ厳格さ。誤記を黙って未指定へ読み替えるとガードが静かに弱まるため)

externalChecks の 4 状態:

指定 意味 マージ挙動
未指定 外部チェック構成が未確定 観測結果にかかわらず自動マージを停止し blocked で終端する(実装・PR 作成・CI までは進む)
[] 「外部チェックを使用しない」と人間が確定 外部レビュー待機をスキップして CI green と未解決スレッドなしのみで判定する
[{"app": "cursor", "context": "Cursor Bugbot"}] 等 指定 App + 信頼済み required check context を正とする(観測結果より優先) 指定した全 App について HEAD sha に対する起動を検証する。cursor は「レビューが 1 件以上到着し、かつ CHANGES_REQUESTED が 0 件であること」(個別指摘はレビュースレッドとして残るため「未解決スレッド 0 件」ゲートが内容非依存に遮断する。監視側の内容評価は修正ループ用 advisory でありマージ可否の入力ではない)、それ以外の App は check-run が 1 件以上ならその全件が許容 conclusion であること、check-run が 0 件のときに限りフォールバックとして「APPROVED レビューが 1 件以上かつ否定的レビュー 0 件」であることをマージ条件とする。opt-in マージでは G0 が宣言 context + App ID の組で required 化を照合する
["cursor"] 等(slug のみの旧形式) App は確定するが信頼済み context が未宣言 監視・外部レビュー待機は上と同じ。ただしクライアント側自動マージは fail-closed で停止する(autoMerge: true でもマージせず blocked 終端。App ID だけの照合では同一 App の無関係な context の required 化でも G0 を通過してしまうため)

観測ベースの検出は直近 3 件の merged PR しか見ないため、新規導入 App・条件付き起動 App・直近 3 件で実行されなかった App を取りこぼす。「検出なし」が不在の証明にならないのはもちろん、「検出あり」も集合としての完全性を保証しない(例: 観測で sonarcloud だけを拾い、実際には必須の cursor を取りこぼしたまま「確定済み」として cursor[bot] レビューの再検証を省いてしまう)。したがって観測結果は確定情報として扱わず、参考値としてログ・停止理由・返却値に残すだけにする。externalChecks が配列でない・slug / context の形式不正(context は 1〜255 文字で、制御文字(改行・タブ等)と前後空白のみ不可。GitHub の context には文字種契約がないため文字種は制限せず、matrix 由来の build [ubuntu] や日本語を含む context もそのまま宣言できる — シェル / jq への埋め込み安全性は単一引用符リテラル + jq --arg の値渡しで保証する)・11 件以上の場合は既定値へフォールバックせずエラーで停止する(parallel は性能ノブのため不正値を既定 3 へ落とすが、externalChecks はマージゲートの入力であり、誤記を黙って「未指定」や「なし確定」に読み替えるとゲートが静かに弱まるため)。

opt-in テストの宣言(任意・既定無効・Issue #495 / PR #503 codex P0)

#[ignore] 付きテストや make e2e-* のように既定の CI・テストでは走らないテストを受入条件に含むイシューでは、イシュー本文に HTML コメントで宣言することで、マージ前ゲートに「PR 本文へ pass の実行記録があること」を追加できる。宣言が無いイシューでは本節の分岐に一切入らない(プロンプト・判定・PR 本文のいずれも現行と一致する。既定無効)。

実行を許可するコマンドは args.optinTestCommands(人間がラン起動時に明示する承認一覧)が唯一の根拠であり、イシュー本文の宣言だけでは新しいコマンドを持ち込めない(make / npm run / yarn run / deno task 等は任意タスクのディスパッチャーであり、許可ランナー・サブコマンドの形式検証だけでは make deploy・npm run release のような任意タスク起動を閉じられないため)。イシュー本文の宣言は、正規化(先頭・末尾の空白除去、水平空白の連続を半角スペース 1 個へ畳む)後に args.optinTestCommands のいずれかの要素と文字列完全一致した場合にのみ採用される。一致しない宣言・args.optinTestCommands が未指定 / [] の状態での宣言はすべて invalid として実装起動前に blocked で停止する(fail-closed)。

宣言の書式(1 マーカー 1 コマンド、最大 10 件。承認一覧側の上限は args.optinTestCommands 最大 20 件):

<!-- optin-tests: make e2e-three-client -->
<!-- optin-tests: cargo test -- --ignored -->

上記の宣言が採用されるには、args.optinTestCommands に正規化後の値と完全一致する要素(例: ["make e2e-three-client", "cargo test -- --ignored"])が含まれている必要がある。

args.optinTestCommands の各要素は起動時に許可形式を検証する(validateOptinCommandForm。先頭トークンが許可されたテストランナー make / just / cargo / npm / pnpm / yarn / bun / go / pytest / deno / mvn / gradle / dotnet / swift / mix に限る、シェルメタ文字(; | & $ )・改行・. に隣接しない ..(親ディレクトリ参照。go test ./... のような ... を含む値は許可)・//・絶対パス引数(トークン先頭または = 直後の /。例: cargo test --target-dir /tmp/x)を含まない)。1 件でも許可形式外なら実装は一切起動せず起動時エラーで停止する(externalChecks と同じ厳格さ。誤記を黙って読み替えない)。npm / pnpm / yarn / bun / cargo / go / dotnet / swift / mix / deno / mvn / gradle は第 2 トークン(サブコマンド)も test 等に制限する。mvn は GAV 形式のゴール指定(groupId:artifactId:goal 等、: を 2 個以上含むトークン)を、deno はリモート指定子(npm: / jsr: / http: / https: で始まるトークン)を、それぞれ任意プラグイン実行・外部コード取得の迂回経路として追加で拒否する。この形式検証は承認一覧側にのみ効く(承認一覧に載った時点で許可形式である保証が確定するため、イシュー本文の宣言側は正規化 + 完全一致の照合のみを行う)。

宣言が採用されたイシューでは、Implement / 回復 Implement エージェントが各コマンドの実行と結果報告(optinTestRuns)を必須手順として行う。実行していないものを pass と報告することは禁止し、環境要因で実行できない場合は not-run と理由を返す。PR 本文には「## opt-in テスト実行記録」節(見出し 1 行と、コマンドごとの人間可読行 - opt-in テスト結果: <コマンド> => <result> + 機械可読マーカー行)が追記される。記録節はホスト検証済みの値(見出し・40 桁 sha・result・承認一覧と一致したコマンド)だけで組む固定形式の行のみから成り、not-run の理由などの補足は PR 本文へ書かず返却値・ログにのみ残す。再利用 PR・post-push fix での更新時は、行全体が記録節の見出し・人間可読行・マーカー行の形式に一致する行だけを行頭・行末アンカー付きの固定パターン(grep -vE)で全行除去してから書き直すため、見出しの重複や古い記録行は残らず、本文の説明文中に見出しや接頭辞を引用しただけの行は保持される。マーカー行は HEAD sha を先頭に束縛した書式(<!-- optin-test-record: <40 桁 sha> <pass|fail|not-run> <コマンド> -->)を使う。sha・result は固定形式(空白を含まない)でコマンド文字列(空白を含み得る)の前に置くため、grep -cxF の完全一致だけでパースが一意になる。この sha は記録を書くエージェント自身が git rev-parse HEAD で取得した、実際に push した(またはこれから push する)HEAD の値である。

マージ前(新規マージ経路のみ。recoveryOnly では適用しない)に、宣言コマンドごとに PR 本文の pass マーカー行が 1 件以上・pass 以外のマーカー行が 0 件であることを、読み取り専用の記録検証エージェントが件数のみで確認する(本文テキスト自体はコンテキスト・返却値に載せない。merge-exec と同じコンテキスト分離契約)。この件数は「現在の PR headRefOid に対するマーカー行」のみを数える(検証エージェントは gh pr view --json body,headRefOid を単一呼び出しで取得し、headRefOid が 40 桁 sha として取得できなければ全件不合格として扱う)。base 取り込み(baseMergePrompt)や別の push で HEAD が変わった後の古い記録・fix 前の記録は、sha が一致しないため grep に一切ヒットせず「存在しないもの」として扱われ、自動的に不合格になる(記録がどの HEAD に対する結果かを束縛しないと、古い pass マーカーが残っているだけでゲートを通過し得るため)。不足があればマージせず blocked(blockedReason: quality)で停止し、終端理由に不足コマンドを明記する。この PR 本文ベースの判定は、Merge ループの post-push fix が再実行した実測(optinFixState。runs と対象 HEAD の headSha を状態ファイルへ永続化し monitoring 再開時に復元する)と AND で重ねられる: fix の実測に非 pass が 1 件でも残っていれば、PR 本文が pass のままでも不合格として扱う(PR 本文更新の失敗・省略による fail-open を防ぐ多層防御。fix の headSha が現在の headRefOid と一致する場合のみ override が働き、HEAD がさらに進んだ後は無介入で PR 本文側の sha 束縛判定に委ねる — 古い実測で新しい HEAD の PR を永久に止めない)。

merge-exec 呼び出し直前に確定したこの検証済み headRefOid は、期待 HEAD sha として merge-exec へも渡す(TOCTOU 対策)。merge-exec は手順 2 で自己取得する headRefOid がこの期待値と一致することを追加で確認し、不一致なら reason: head-moved で辞退する。この期待値は --match-head-commit の入力にはならない(--match-head-commit には merge-exec の自己取得値のみを使う)うえ、一致条件を追加するだけでマージ許可を広げる入力にはならないため、「monitor 出力をマージ経路の入力に使わない」という分離原則とは矛盾しない。宣言テストが無いイシューでは expectedHeadSha は空文字のまま渡され、merge-exec のプロンプト・挙動は完全に不変。

永続化(optinFixState)は、post-push fix が push した(f.pushed === true)ラウンドごとに runs・headSha を状態ファイルへ書き込む。書込みは成否を確認し、失敗時は 1 回再試行、それでも失敗すれば blocked で fail-closed 終端する。終端経路(failMergeTerminal)の状態ファイル更新にも optinFixState を含める(含めないと、fix 実行後に別経路で終端したラウンドの実測が失われるため)。復旧手順は references/recovery.md を参照。

自動マージのサーバー側委譲と merge-guard hook(deny 専用・best-effort)

クライアント側の自動マージは autoMerge: true + externalChecks 確定(全 App の信頼済み context 宣言込み)の opt-in ランでのみ実行する(次節「クライアント側自動マージの設計」参照。opt-out 既定ではマージしない。auto-merge の予約(arm)は提供しない)。merge-guard hook は deny 専用(承認境界ではなく、迂回可能な best-effort の攻撃面削減)。

詳細: references/automerge-design.md

クライアント側自動マージの設計(重要)

opt-in ランのクライアント側マージは、未信頼のレビュー本文を読む monitor の虚偽出力による未承認マージ誘導に対して次の 3 層で対処する: monitor 出力のマージ経路からの分離・merge-exec の自己取得再検証・G0 サーバー側強制の実測(required checks の bypass 不能性に加え、レビュースレッド解消の必須化・合格判定対象チェック context の required 化(client-only チェックの不在)・外部チェック App の宣言 context + App ID 組束縛の required 化・required checks 全エントリの発行元 integration_id 束縛(同名 commit status 偽装の遮断 — issuer-unbound で辞退)まで確認し、共有 gh 認証のどのエージェントが直接マージを試みてもサーバーが同条件で拒否する構成を前提化)、の 3 層。

詳細: references/automerge-design.md

branch protection(マージ判定の本体。人間マージ・サーバー側 auto-merge の両運用で必要)

対象ベースブランチにはサーバー側 branch protection / ruleset を設定することを強く推奨する(ランタイムゲートではなく運用推奨)。compromised なローカルエージェントもサーバー側ルールは迂回できない。

詳細: references/automerge-design.md

フロー

Step 1: ツリーを取得して依存グラフ付き実行キューを構築する(Tree)

gh CLI の sub-issues API で親イシュー配下の全ツリーを再帰取得し、post-order DFS で実行キューを構築する。各 open イシューは本文を読んで機能的依存(dependsOn)を抽出する。

各 open イシューの本文からは opt-in テスト宣言マーカー(<!-- optin-tests: ... -->)も機械抽出し、ホスト側で許可形式かを再検証する(前掲「opt-in テストの宣言」節参照。宣言が無ければ後続処理は現行と変わらない)。

ツリー取得に続いて、直前 3 件の merged PR の check-runs から GitHub Actions 以外の外部チェック App(例: Cursor Bugbot)を観測する。観測結果は参考値であり構成の確定情報ではない。構成の確定は args.externalChecks の明示入力で行い、明示がない限り「確定不能」として後続の Merge ステップで自動マージを停止する。

# 親イシューのサブイシューを取得(--paginate で 100 件超も全ページ自動取得)
gh api --paginate "repos/{owner}/{repo}/issues/<parent>/sub_issues?per_page=100"

# 各 open イシューの本文を読み、機能的依存を抽出
gh issue view <N>

# 外部チェック観測(直前 3 件の merged PR の check-runs を確認。結果は参考値)
REPO=$(gh repo view --json owner,name --jq '"\(.owner.login)/\(.name)"')
# SHA は位置引数 $1、REPO は位置引数 $2、jq フィルタは位置引数 $3 で渡す
# (REPO を子シェル内で "${REPO}" と展開すると非 export の変数は sh -c に渡らず空になり、
#  gh api が必ず失敗して常に apps: [] へフォールバックする)
gh pr list --state merged --limit 3 --json headRefOid --jq '.[].headRefOid' \
  | xargs -I{} sh -c 'gh api "repos/$2/commits/$1/check-runs" --jq "$3" 2>/dev/null' \
      _ {} "$REPO" '[.check_runs[] | select(.app.slug != "github-actions") | .app.slug] | .[]' \
  | sort -u

実行キューと依存グラフの構築ルール:

  • 同一親内のサブイシューは sub_issues API 返却順(siblingIndex)で並べる
  • 子イシューがすべて完了してから親イシューを処理する(親ノードは verify-close)
  • closed 済みイシューは自動でスキップする
  • dependsOn には「機能的に先行完了が必須」のイシュー番号のみを入れる(本文の明示的な依存記述・前提実装に限る。単なる関連やコンフリクトの可能性だけなら含めない)
  • Tree エージェントの dependsOn 判断に加えて、本文の依存宣言を機械抽出して和集合を取る(plan:declared-deps-*。判断ベースの抽出は大規模ツリーで取りこぼすため、その下限として働く)。対象は依存見出し節(## 依存 / ## 依存関係 / ## 前提 / ## Depends on / ## Dependencies / ## Blocked by。見出しレベル 1〜6・末尾コロン可。## 依存クレート のような別見出しは行末アンカーで除外)の中で行頭(箇条書き記号・チェックボックスの直後)に置かれた #N の並びと、インライン記法 Depends on #N / Blocked by #N(見出し行を含む。否定語 not / no longer / never / n't の直後〔句読点なしで挟まる語は 1 語以内。not yet blocked by 等〕の宣言は除外し、無関係な否定で同じ行の依存を落とさない)。#N の並びは空白・カンマ・読点・スラッシュ・and 区切り(#1, #2, and #3 等)を受理する。節内でも行頭以外の参照(「依存なし。関連 issue #42」等)は拾わず、関連・参考・否定(関連 / 参考 / 参照 / 任意 / なし / 不要 / related / see also / optional / none / no longer / not / never / unnecessary / n't)を含む行頭項目は除外する(行単位の判定は 1 行 1 宣言の行頭項目に限り、インライン記法には適用しない)。コードフェンス内(開始時の記号と長さを保持し、同種・同長以上の終了行でのみ閉じる)・引用行・インラインコード(同じ長さのバッククォート列同士を組にして内容ごと除去。改行をまたぐコードスパンも段落単位で除去し、ブロック境界はまたがない)は除外する(機能的に先行完了が必須のものだけを依存辺にするため)。インデントされた行はコードブロックとみなさず抽出する(リスト内容インデントは行単位で判定できず、取りこぼしより有界な過剰待機を選ぶ)。gh issue view --jq が整数配列へ正規化した出力のみを扱い、本文テキストはエージェントのコンテキストへ入れない。40 件ずつのチャンクで並列実行し、ホストが依頼した番号の全件返却と、jq がイシュー番号と deps から計算した検査値 sig を返却 deps から再計算した値との一致を照合する(ホストはシェルを持たずコマンド出力を直接照合できないため、転記の誤りを検査値で検出する。欠落・契約違反〔依頼外番号・重複番号・非整数・deps 非配列・上限超過・sig 不一致〕は 1 回だけ再試行し、なお欠落すればラン開始前に停止する fail-closed。依存を取りこぼしたまま着手しないため)
  • 祖先イシューへの dependsOn は無視する(親は子の完了を待つ側のため)
  • 依存グラフに循環がある場合は DFS で検出し、循環を構成する非ツリー辺(dependsOn)を除去してデッドロックを防ぐ
  • 依存ブロックは各周回で再判定する: 前提イシューの失敗・ブロックで下流が着手不能でも即座に確定せず保留し、halt(3 イシュー連続で完了できなかった場合の新規着手停止。Step 8 参照)発生前に限り、前提がラン中に外部完了(Issue CLOSED / PR MERGED)した場合は同一ラン内で下流を再判定して着手する。halt 後はプローブと状態記録(prereqTransitions・state 永続化)のみ継続し、新規着手は再開しない(halt はユーザー判断を待つ防御であり自動解除しない)。halt 後に記録された外部完了は次回ランの再実行で下流着手に反映される
  • 既定(phaseGate 未指定 / false)では siblingIndex は post-order の優先度に過ぎない。並列ラン(parallel >= 2)では後続 Phase の leaf が前 Phase 完了前に空きスロットへ投入され得る(追い越し)。create-issue-tree / update-issue-tree が生成するルート本文の運用ルール「実行順は sub-issues リスト順が正」を厳密に保証したい場合は phaseGate: true を使う(phaseGate 引数の説明を参照)

Step 2: 中断作業の回復可否を per-issue で判断する(Recover)

各末端イシューに着手する前に、残骸 worktree / ブランチが存在するかを確認する。既存作業がなければ Recover をスキップして Plan へ進む。既存作業がある場合は Recover phase(セッション継承モデルのエージェント)が「途中作業を継続できるか」を判断し、その結果に応じて以下のどちらかへ分岐する。

  • continue(継続): 既存 branch をそのまま checkout し、回復ブリーフ(done / remaining / broken の要約)を Implement へ渡して続きから実装する。Plan はスキップされる。Recover が直接 Review へ進むことはなく、継続作業は必ず Implement → Review → Merge を経由する。旧 worktree の削除は WIP 退避の完了が検証できた場合のみ実行する(後述の削除ゲート)。検証できない場合は残骸を削除せず failed で保全する(退避されていない未コミット変更を欠いたまま継続すると不完全な実装になるため、削除だけを飛ばして継続することはしない)。加えて、旧 worktree の掃除と implementing / reviewing 遷移の完了を状態更新の戻り値で確認できなかった場合も先へ進まず failed で保全する(旧 worktree が branch を掴んだままだと新 worktree が同一 branch を checkout できず、reviewing 未永続化のまま続行すると重複実装につながるため。discard 側の掃除完了確認と対)。
  • discard(破棄): 既存 worktree と branch を削除し、通常の Plan → Implement(新規 branch)で再実行する。削除は WIP 退避の完了が検証できた場合のみ実行する(後述の削除ゲート)。検証できない場合は残骸を削除せず failed で保全し、次回ランの Recover に委ねる。加えて、worktree / branch の掃除完了を状態更新の戻り値で確認できなかった場合も Plan へ進まず failed で保全する(branch 残存下で再 Plan すると git checkout -B が WIP commit を orphan 化するため)。

Recover の判断軸は Review とは別である。Review は「実装が正しいか・マージできるか」を判定するのに対し、Recover は「この途中作業から継続するのが妥当か」を判断する。動かない・未完成でも方向が妥当なら continue(残りは Implement が完成させる)。

未 commit 変更は WIP commit として branch へ退避してから worktree を削除するため、continue / discard どちらの経路でもデータを失わない。discard の場合は WIP commit を残した状態で branch を削除するため、誤判定時に reflog から救出できる。

削除ゲート(continue / discard 共通): Recover エージェントの返す wipCommitted は自己申告値であり、誤判定・異常応答・プロンプトインジェクションで真を騙られ得る。加えて Recover は「フック失敗等で退避できなかった場合は wipCommitted: false を返して続行する」契約のため、continue も退避失敗時に返り得る。そのため continue / discard いずれの経路でも worktree の削除は次の 2 条件を両方満たす場合にのみ実行する。

  1. 申告ゲート: Recover エージェントが wipCommitted: true を返している(退避した場合、および退避すべき未 commit 変更が最初から無かった場合に true。フック失敗等で退避できなかった場合は false)
  2. 事実ゲート: ホストが起動する読み取り専用の安全確認エージェントが、対象 worktree に未 commit 変更が残っていないこと(git status --porcelain の出力が空であること)を確認できている

どちらか一方でも満たさない場合、あるいは安全確認自体が失敗した場合は worktree / branch を削除せず failed で保全する(fail-safe)。保全された残骸は次回ランの Recover が再度判断する。worktree が無い branch のみの残骸は削除対象も未 commit 変更も存在しないため、このゲートの対象外とする。

Step 3: イシューごとに実装計画を立案する(Plan)

各 末端イシューを実装する前に、セッション継承モデルのエージェントで実装計画を立案する(worktree なし・読み取りのみ)。計画は Implement エージェントへ引数で渡す(worktree 跨ぎのファイル参照を避けるため)。

Recover phase で continue 判定が出た場合は Plan をスキップし、回復ブリーフを受け取った Implement エージェントが既存 branch から直接実装を続行する。

計画には以下を含める:

  • 背景・目的(イシューが解決する課題)
  • 対象ファイル・変更箇所(パスと変更内容の概要)
  • 実装ステップ(順番に実行可能な具体的手順)
  • 検証方法(ビルド・lint・テスト・動作確認の手順)
  • OWASP Top 10 観点のセキュリティ考慮事項

計画エージェントが異常終了または計画本文が空の場合は、該当イシューを failed として記録して次へ進む。

Step 4: 末端イシューを worktree 隔離で並列実装する(Implement)

末端の実装イシューを post-order DFS 順を優先度として空きスロットへ貪欲投入し、最大 parallel(既定 3)件まで並列実行する。各 implement / fix エージェントは独立した git worktree で隔離実行されるため、並列でもブランチ・working copy が衝突しない。

ここでは push も PR 作成も行わない。CI リソース節約のため、Review 通過後にまとめて 1 回だけ push・PR 作成する設計になっている。

各イシューの処理内容(Step 3 で立案した計画に従って実装する):
0. worktree routing ガード(最初に実行): git remote get-url origin とイシュータイトル照合でカレント worktree が正しいリポ・イシューに配置されているか確認する
0b. 既存 PR・リモートブランチを確認する(中断再開・重複 PR 防止):

  • 0b-a(open PR 検索): gh pr list --state open でイシュー番号に対応する open PR が既に存在するか確認する。見つかれば新規 PR を作らずそのブランチを取得して続きから作業し、そのブランチ名を返す(PR 番号は返さない。同じブランチの open PR は後続の PR Create フェーズが再検出して再利用する)。手順 2 のブランチ作成はスキップする(origin/<base> から checkout -B し直すとその PR のコミットを失うため)
  • 0b-b(リモートブランチ再利用): open PR が見つからない場合、git ls-remote --heads origin でイシュー番号を含むリモートブランチ(命名規約: <type>/<N>-<short-name>)が残っていないか確認する。「push 成功・PR 作成失敗」で残ったブランチを検出し、git fetch origin <branch> && git checkout -B <branch> origin/<branch> で取得して push 済みコミットを保持したまま続きを実装する(origin/<base> から新規作成し直さない)。branch 名として返し、prNumber は 0 のまま(PR は後続の PR Create フェーズが作成)
  • 0b-c: open PR もリモートブランチも存在しない場合のみ手順 1・2 で新規ブランチを作成する
  1. 隔離 worktree で git status が clean か確認し、差分があれば作業せず失敗を返す

  2. (0b-a で既存 open PR のブランチを取得した場合・0b-b でリモートブランチを再利用した場合はスキップ)指定ブランチ(デフォルト: main)から作業ブランチを作成する(並列時のブランチ名衝突を防ぐためブランチ名にイシュー番号を含める)

  3. 渡された計画に従って実装する(計画立案は Plan フェーズで完了済み)。実装は対象リポジトリの delegation ルール・専門サブエージェントがあればそれに従い役割単位で委譲する

    コメント方針(実装時):

    • コードコメントは「何をするか」より「なぜ存在するか/パッケージ・サービスから見た対象の役割」を書く
    • 後続の読み手(Claude を含む)は渡された情報からしか判断できないため、他ファイル・他サービス・呼び出し元/呼び出し先からの観点を明示する(このシンボルがどこから呼ばれ、どの境界を担うか)
    • 対象リポジトリに .claude/rules/code-comment-style.md(init-claude が配備)が存在する場合はそちらの詳細規約に従う。存在しない場合は上記の要点に従う
  4. 対象リポジトリの CLAUDE.md・rules・テスト実行規約に従いビルド・lint・テストを通す。テストが失敗した場合は根本原因を調査してから修正する(対象リポジトリに .claude/rules/debugging.md が存在する場合はその4フェーズを順に踏む。存在しない場合も同じ方針〔調査→分析→仮説→修正〕を踏む。同一箇所で3回失敗したらアーキテクチャ問題と判断し、該当イシューを blocked として記録してユーザーに状況を報告する)
    4b. opt-in テスト宣言がある場合のみ: 宣言コマンドごとに実行前確認(対象リポジトリで定義されたテスト入口か)→ 単一コマンドとして実行(sh -c / eval 禁止)→ 結果(pass / fail / not-run)を報告する。実行していないものを pass と報告してはならない(前掲「opt-in テストの宣言」節参照)

  5. 実装後に OWASP Top 10 観点でセキュリティチェックを実施する(API キーのハードコード・インジェクション等)。問題が見つかった場合は修正してから次へ進む

  6. 実装が完了したら create-commit スキルに従い Conventional Commits で実装コミットを 1 つ作成する。
    コミット前に対象リポの commitlint 設定(commitlint.config.* / .commitlintrc* / package.json の
    commitlint フィールド)を読み取って type-enum / scope-enum を確認し、許可された値のみを使う。
    該当する scope が無ければ scope ごと省略する(feat: 実装内容)。scope にイシュー番号を置かない
    (scope-enum を設定したリポでは必ず落ち、Review 3 巡を消費した後の push で初めて検出される)。
    イシューとの紐付けは footer の Refs #<N> と PR 本文の Closes #<N> で行う。
    push 前 base 最新化ゲート(Step 5・Merge ループの fix)で作る base 取り込みマージコミットの subject も同じ手順で type / scope を決める(固定の chore: は type-enum / scope-enum を持つリポの commit-msg hook に拒否される。拒否されたら git merge --abort して push せず fail-closed)

  7. push・PR 作成はここでは行わない。ローカルブランチにコミットを積んだ状態で終了し、後続の Review フェーズへ渡す

# 作業ブランチ作成例(並列時の衝突回避のためイシュー番号を含める)
git fetch origin && git checkout -B feat/<N>-<short-name> origin/<base-branch>

# 実装コミット(push しない)
# scope はイシュー番号ではなく変更対象のモジュール・ディレクトリ名。
# 対象リポの commitlint の scope-enum に該当する値が無ければ scope ごと省略する。
git commit -m "$(cat <<'EOF'
feat(<module>): 実装内容

Refs #<N>
EOF
)"
# → push・PR 作成は Review 全通過後に行う

Step 5: push 前のローカル diff を独立レビューする(Review)

Implement 完了後・push 前に、worktree 隔離で独立 Review エージェントを起動してローカル diff をレビューする。push・PR 作成は行わず、ローカルコミットだけを対象にレビューする。Review エージェントは修正を行わず判定のみを担う。

CI リソース節約の目的: Review が収束失敗した場合は push も PR 作成も行わないため、CI が一切起動しない。fix のたびに push → CI 実行を繰り返すコストを削減する。

レビューは以下の2段階で実施する。

①仕様準拠レビュー(先に実施):

  • イシューの要件・受け入れ条件を充足しているか確認する
  • out-of-scope の実装が混入していないか確認する
  • Plan フェーズの計画どおりに実装されているか確認する

②コード品質レビュー(①通過後に実施):

  • 可読性・重複・設計(アーキテクチャ準拠・命名規則)を確認する
  • OWASP Top 10 セキュリティ(API キーのハードコード・インジェクション・認証認可等)を確認する

詳細は implement-review スキルを参照。

レビュー条件:

  • git checkout --detach <branch> でローカルブランチを detached HEAD として取得する(origin/<branch> は push 前のため存在しない)
  • レビュー直前に git fetch origin <base-branch>:refs/remotes/origin/<base-branch> を 必ず 1 回実行して比較基準を最新化する(ref の存在有無で分岐しない)。保存先を明示した refspec を使う — git fetch origin <base-branch> のように取得元だけを与えた形は FETCH_HEAD を更新するだけで refs/remotes/origin/<base-branch> の作成・更新を保証せず、fetch 成功後の解決に失敗して実施可能なレビューを blocked で落とす
  • git diff origin/<base-branch>...HEAD でローカル diff を確認する(origin/<base-branch>(直前に取得し直した remote-tracking ref)が比較基準。3 点ドットのため比較点は merge-base(origin/<base-branch>, HEAD) =ブランチの分岐点に固定され、以降ラン中に origin が進んでも比較点は不変。既存 ref があっても古ければ merge-base が実際の分岐点より手前に落ち、base 側の無関係なコミットが差分へ混入するため「ref があること」を新しさの根拠にしない。fetch に失敗した場合、および fetch 後も解決できない場合はレビューを実施せず state: "blocked" / highestSeverity: "none" で fail-closed 終端する。blocked は環境要因でレビュー自体が実施不能だったことを表す専用状態で、コード指摘を表す needs-fix とは呼び出し元の扱いが異なり fix エージェントを起動せず即座に終端する — needs-fix / critical は使わない。無関係なコードへの修正試行で修正予算を消費させないため)
  • Low(要改善)含む指摘が 1 件でも needs-fix。指摘なしなら ok

ok の場合は push + PR 作成(Step 4.5)を経て Merge ステップへ進む。needs-fix の場合は fix エージェントでローカルに再コミットし再レビューする(push しない)。Review は最大 3 回実施し、最終回(残り 0 回)の needs-fix では再レビューできないため fix を行わず収束失敗とする(修正後に必ず再レビューする原則を守るため。fix は実質最大 2 回)。3 回で収束しない場合はpush も PR 作成も行わず blocked として記録して次のイシューへ進む。この blocked は残置 worktree・branch・最終指摘をレポートへ集約し(references/report-format.md 参照)、再実行時は Recover(継続/破棄)→ 通常 Implement 経路から再着手する(pr: 0 のため monitoring 再開ではない。詳細は references/recovery.md)。

依存ブロックの再判定について: 下流イシューが前提イシューの blocked(本節の Review 非収束等)で連鎖ブロックされても、スケジューラは各周回で保留状態を維持し、halt 発生前に限り、前提がラン中に外部完了(人手マージ・クローズ)した場合は同一ラン内で下流を再判定する。halt 後の外部完了検知はプローブ・状態記録のみで新規着手には反映されず、次回ランで反映される(詳細は Step 8 参照)。

Review / Merge の fix は fixCount(上限 6)を共有する。base とのコンフリクト(mergeable: CONFLICTING)解消は fixCount を消費せず、独立予算の baseMergeCount(上限 maxBaseMerges。既定 3)で管理される。

Step 5.5: Review 通過後に push + PR を作成する(PR Create)

Review が全通過(ok)した後にのみ実行する。この push が CI トリガーになる(push は 1 回のみ)。

# Review 通過後にはじめて push する(CI がここで起動する)
git push origin <branch>

# PR 作成(Closes でイシューと紐付け)
gh pr create \
  --base <branch> \
  --title "feat: イシュータイトル" \
  --body "$(cat <<'EOF'
## Summary
- 実装内容の要約

Closes #<N>
EOF
)"

opt-in テスト宣言がある場合、body には Closes 行に続けて「## opt-in テスト実行記録」節(コマンドごとの機械可読マーカー行を含む。マーカーは push する HEAD sha を束縛する書式。前掲「opt-in テストの宣言」節参照)も追記する。この push 前に、base 取り込み後の現在の HEAD に対して宣言テストを再実行する(Implement 時点の結果は base 取り込みで陳腐化し得るため転記しない)。宣言が無ければ本節・再実行手順とも出力されない。

既存 open PR の再利用: push 成功後・gh pr create の前に、このブランチに対する open PR が既に存在しないかを gh pr list --state open --head <branch> --json number,baseRefName,headRefOid で必ず確認する。中断再開(PR 作成直後のクラッシュ・pr 保存済み failed からの再実行)では open PR が残っていることがあり、確認せずに gh pr create すると必ず失敗して、生きている PR が追跡されないまま残るため。

再利用の条件は 2 つあり、両方を満たす場合にのみその番号を prNumber として返す。

  • baseRefName が指定 base ブランチと一致すること(同じ head から別 base(リリースブランチ等)へ開かれた PR を再利用すると、base <branch> の契約を迂回して意図しないブランチへマージされる)
  • headRefOid が push したブランチの先端 sha と一致すること(他者・別ランの push で head が動いた PR を、検証していないコミットごとマージ対象にしない)。比較対象の sha は必ずブランチ ref(git rev-parse --verify "refs/heads/<branch>"、解決できなければ refs/remotes/origin/<branch>)から解決する。PR Create エージェントは隔離 worktree で動作し、その worktree が対象ブランチを checkout している保証がないため git rev-parse HEAD を使ってはならない

条件を満たす PR を再利用する場合は、本文に Closes #<N>(および対象外項目があれば「対象外(out-of-scope)」節)が無ければ追記する。このとき既存本文をシェルコマンド文字列・HEREDOC へ埋め込んではならない(本文は外部由来の未信頼データであり、行単独の HEREDOC 終端文字列を仕込まれると HEREDOC が早期終了して後続行が任意コマンドとして実行される)。gh pr view <N> --json body --jq .body > "$f" でファイルへ直接落とし、grep -qF で存在確認したうえで printf / 固定テンプレートの追記のみを行い、gh pr edit <N> --body-file "$f" で更新する。条件を満たさない open PR しか存在しない場合は、再利用も新規作成も行わず prNumber: 0 と理由を返して停止する(branch は保存されるため、次回実行は impl 手順 0b から回復する)。

PR 作成が失敗した場合は failed として記録し、branch を保存する。branch 保存済みの failed は次回再実行時に Recover phase を起動する(impl 手順 0b には到達しない)。continue なら回復 Implement の手順 2 が既存 branch を checkout した後に git fetch origin <branch>:refs/remotes/origin/<branch> → git merge --ff-only refs/remotes/origin/<branch> でローカルをリモート tip へ追従させ、push 済みの base 取り込みコミットを保持したまま回復する(PR Create エージェントは base 取り込みコミットを detached HEAD から push しローカル refs/heads/<branch> を更新しないため、追従なしでは次の PR 作成が remote-ahead / diverged で再失敗する)。ff 不能な真の diverged はそのまま続行し、次の PR 作成の (iv) が fail-closed で止める。

Step 6: CI / 外部チェック監視・レビューコメント解決確認・squash merge する(Merge)

gh pr checks --watch で CI を監視し、以下の全条件を満たした場合のみ squash merge する。

opt-in テスト記録ゲート: イシューで opt-in テストが宣言されている場合、新規マージ経路(recoveryOnly ではない)に限り merge-exec 呼び出しの直前で読み取り専用の記録検証エージェントを起動し、宣言コマンドごとの PR 本文 pass マーカー行の件数を、現在の headRefOid に束縛したものだけで確認する(本文は一時ファイル経由で件数のみへ正規化し、merge-exec 同様に本文テキストを実行主体のコンテキストへ入れない)。不足があれば merge-exec を起動せず blocked(blockedReason: quality)で停止する。検証済みの headRefOid は merge-exec へ期待 HEAD sha として渡し、自己取得値との一致を追加確認させる(TOCTOU 対策)。ただし PR が既に独立確認で MERGED と判定できた場合はこのエージェントを起動しない(Issue #509。既存の already-merged 回復経路へ合流する)。詳細は前掲「opt-in テストの宣言」節・references/automerge-design.md を参照。

クライアント側の自動マージは opt-in ランでのみ実行する(references/automerge-design.md の「クライアント側自動マージの設計」節参照): autoMerge: true + externalChecks 確定(全 App の信頼済み context 宣言込み)のランでは、monitor の ready 判定後に merge-exec が HEAD sha を自己取得・固定したうえで全条件(checks・未解決スレッド数・外部チェック起動・G0 = ベースブランチのサーバー側強制の実測: required checks の bypass 不能性(ruleset の bypass_actors 空。classic branch protection のみのリポジトリは非対応として classic-unsupported で辞退)・レビュースレッド解消の必須化・合格判定対象チェック context の required 化(client-only チェックの不在)・外部チェック App の宣言 context + App ID 組束縛の required 化・required checks 全エントリの発行元 integration_id 束縛(同名 commit status 偽装の遮断 — issuer-unbound で辞退))を独立再検証し、gh pr merge --squash --delete-branch --match-head-commit <自己取得 sha> で squash merge を実行、さらに merge-verify の独立確認(state=MERGED + merge-exec 申告 sha との完全一致)を通過した場合のみ merged 終端する。monitor の出力(ready / headSha)はマージ経路の入力に使われない(ready は起動タイミングのみ)。G0 を確認できないリポジトリでは server-enforcement-missing(classic branch protection のみのリポジトリは classic-unsupported)で blocked 終端する(fail-closed。ruleset ベースの branch protection を構成して再実行すれば継続する)。opt-out(既定 false)・externalChecks 未確定・信頼済み context 未宣言(slug のみの旧形式)のランでは新規マージを実行せず、PR をマージ可能状態のまま blocked(blockedReason: quality)+ pr 保持で終端する。opt-out 時は monitor が ready(虚偽含む)を返しても merge-exec は gh pr merge を含まない回復専用経路に固定される(recoveryOnly 機構。opt-in 判定はホストの決定的コード = args パースのみ。モデル出力・未信頼テキストに依存しない)。マージ済み PR のクローズ回復(already-merged 経路)は両モードで通る。この経路は「前回ランでマージ済みだが状態記録に失敗した PR」に加えて、サーバー側 auto-merge workflow(upstream の docs/implement-issue-tree/auto-merge-sample.yml)が監視中に PR をマージした場合も同様にカバーし、いずれも正常完了(merged)として終端する。blocked + pr は次回ランの monitoring 再開対象で、マージは GitHub 上で人間が行うか、サーバー側 auto-merge workflow + branch protection に委ねる(references/automerge-design.md の「自動マージのサーバー側委譲と merge-guard hook」節参照)。

クライアント側自動マージの現契約と採らない方式: host が発行する grant(正規マージコマンド全文 = expectedCommand)を merge-guard hook が完全一致照合する allow 経路では承認境界を作れない。monitor は未信頼のレビュー本文を読みつつ merge-exec と同じ Bash・gh 認証・FS を共有し、gh pr view で HEAD を取得して任意 nonce の grant を自作できる(grant 偽造)。hook 専用の秘密注入経路がなく、hook が検証でき subagent が読めない鍵を持てないため署名 / MAC も実装不能で、偽造不能なマージ認可を hook で実装することは原理的に不可能。クライアント側 arm(agent precheck + hook carve-out)も、carve-out が認可と結び付かず任意 subagent に arm を開放し、precheck が agent 自己申告で捏造可能で、--auto の即時マージで「予約のみ」前提が成立しないため採らない。hook は deny 専用(best-effort・承認境界ではない)とする。現契約(2026-08-12 の opt-in 再有効化以降): 既定(autoMerge 未指定 / false)は新規マージを行わずマージ可能状態の blocked で停止する。autoMerge: true + externalChecks 明示(全 App の信頼済み context 宣言込み)の opt-in ランに限り、monitor 出力のマージ経路からの分離・merge-exec の自己取得再検証・G0(サーバー側強制の実測。確認できなければ server-enforcement-missing で fail-closed 辞退)・--match-head-commit・merge-verify の独立確認を前提としてクライアント側 squash merge を実行する(本 Step 冒頭の opt-in 説明と references/automerge-design.md「クライアント側自動マージの設計」節参照。残存リスクとその受容記録も同節にある)。grant / canary / branch-protection ランタイムゲート・precheck / arm / hook carve-out は用意しない。正規経路の外(注入に従った monitor 自身の gh pr merge 直接実行・REST / GraphQL merge・approve・alias / extension)は merge-guard hook が best-effort で deny するが、これは迂回可能な多層防御の一層にすぎない。実際にマージを止めるのは opt-out 既定の fail-closed(host が opt-in なしに新規マージ経路を開かない)と、サーバ側 branch protection(第三者=非 author 承認必須・dismiss stale・通常/force push 禁止・required checks。references/automerge-design.md の「自動マージのサーバー側委譲と merge-guard hook」節参照)であり、opt-in ランでも G0 が同条件のサーバー側強制を実測確認できない限りマージしない。opt-in を使わない auto-merge は同節のサーバー側 workflow(upstream の docs/implement-issue-tree/auto-merge-sample.yml)へ委譲する。

監視とマージ実行の分離・merged 自己申告の独立確認: このステップは監視・マージ実行・独立確認の 3 つのエージェントに分かれる。実行基盤がエージェント単位のツール権限制御を提供しないため、これは権限の剥奪ではなくコンテキスト分離(未信頼テキストをマージ実行主体へ入れない)である。残存リスクと必要な基盤対応は「非信頼データの扱い」項目 5 を参照。

  • 監視エージェント(monitor): CI・外部チェック・レビュースレッドを確認し、state(ready / needs-fix / unresolved-comments / timeout / blocked)と headSha(40 桁)を返す助言的判定のみを行う。PR レビュー本文という未信頼データを読むため、gh pr merge / gh issue close / gh pr edit / resolve mutation の実行権限を持たない。
  • マージ実行エージェント(merge-exec): 監視が ready を返したときにホストが起動する。レビュー本文・Issue 本文・チェック名を一切読まず、リポジトリ内ファイル(CLAUDE.md・.claude/rules 等 = PR 側で変更可能な未信頼テキスト)も読まない(実装系エージェント向けの共通指示 COMMON を挿入せず、merge-verify と同一の最小指示で構成する)。PR の state / headRefOid / mergeable(enum・sha)、チェックの状態別件数(gh pr checks <N> --json state --jq '[.[].state] | group_by(.) | map({state: .[0], count: length})'。素の gh pr checks や --json name / description / link は使わない)、未解決レビュースレッドの件数のみ(GraphQL から comments を外して body を取得しない)、および args.externalChecks で確定した外部チェック App について HEAD sha に対する件数と状態 enum のみ(--jq で正規化。App 名・チェック名・body 等のテキストは取得しない)を自ら再取得して検証し、さらに G0 ゲート(ベースブランチのサーバー側強制の実測: required checks の bypass 不能性(ruleset の bypass_actors 空。classic branch protection のみのリポジトリは bypass 不能性を write トークンから証明できないため非対応 — classic-unsupported で辞退)・レビュースレッド解消の必須化・合格判定対象チェック context の required 化(HEAD sha 上の check-run / commit status のうち required に含まれないものが 0 件であることを jq の集合差の件数のみで照合。context 文字列は取得しない)・外部チェック App の宣言 context + App ID 組(context + integration_id)束縛の required 化 + required checks 全エントリの発行元 integration_id 束縛(HEAD の check-run の .app.id と一致することの件数照合。同名 commit status 偽装の遮断 — 検証できなければ issuer-unbound で辞退)。件数・真偽値のみの API 出力で確認し、確認できなければ server-enforcement-missing で辞退)を通過した場合にのみ squash merge とイシュークローズを実行する。HEAD sha は自身の gh pr view 観測から取得・固定し(monitor から受け取らない)、マージは gh pr merge <N> --squash --delete-branch --match-head-commit <自己取得 sha> で実行して、照合とマージの間に push される競合(TOCTOU)を GitHub 側の条件評価で塞ぐ。
    • チェック名を除外するのは、名称が PR 側の workflow / job / matrix 定義から生成される外部由来テキストであり、マージ権限を持つ実行主体のコンテキストへ命令文を持ち込む経路になるため。外部チェックの件数(非負整数)と .conclusion / .status の状態 enum は任意テキストを注入できる媒体ではないため、この理由づけの対象外として --jq 正規化つきの取得のみを許可する。App の絞り込みは、args 入力時に slug 形式(英小文字・数字・ハイフン、39 文字以内)へ検証済みの値との app.slug 一致で行い、App 名・description・output はコンテキストへ入れない。
    • 外部チェックの再検証を監視エージェント側だけに置かないのは、未信頼テキストを読む監視の判定は「マージを試みてよい」という起動条件にすぎず、ゲートの証拠にできないため。
    • 監視の headSha はマージ経路の入力に使われない(診断用の記録のみ。マージ判定用の HEAD sha は merge-exec が自己取得・固定する)。opt-out・外部チェック構成が未確定のラン(externalChecks 未指定)・信頼済み context 未宣言のラン(slug のみの旧形式)では merge-exec は回復専用経路(allowMerge=false)で起動し、新規マージは行わず「PR が既に MERGED ならイシュークローズ確認のみ」に限定される(前回ランでマージ済み・状態記録に失敗した PR の回復パスを保ちつつ fail-closed を維持する)。この限定はプロンプト解釈に任せず、マージ実行手順の文面もホスト側で分岐し、回復専用経路のプロンプトには gh pr merge / --match-head-commit を一切含めずイシュークローズ確認のみを出力する。
    • マージ成功でもイシューのクローズを確認できない場合(issueClosed: false)は merged として終端せず再監視でクローズを再試行し、監視回数を使い切った場合は「PR はマージ済みだがクローズ未確認」として blocked で終端する(次回実行の monitoring 再開で回復する)。
  • 独立確認エージェント(merge-verify): merge-exec が merged: true を返しても、ホストはそれを未検証のモデル出力として扱い無条件受理しない。merged: true は reason が merged / already-merged と整合する場合のみ受理候補とし(不整合・enum 外は systemic failure として failed 終端・halt カウント対象)、さらに merge-exec とは別コンテキストの読み取り専用エージェントが gh pr view <N> --json state,headRefOid,mergeCommit の取得値のみを返し、ホストが state の完全一致(MERGED)と監視時点 HEAD sha との一致(sanitizeSha 通過値。前回ランでマージ済み・headSha 未記録の already-merged 回復経路では比較対象がないため state のみ)を厳密再検証する。裏付けられない場合(state 不一致・HEAD 不一致・取得不能・無効応答)は fail-closed で blocked(blockedReason: quality)で終端し、worktree 削除・dependsOn 後続イシューの解放は行わない。blocked + pr は次回ランの monitoring 再開対象のため、実際にマージ済みなら already-merged 経路で自然回復する。返却 schema は自由文フィールドを持たず、確認エージェントはレビュー本文・Issue 本文・コメント・チェック名を一切読まない(ホストのログ・note には enum 完全一致・sanitizeSha 通過済みの検証値のみを合成する)。
    • 辞退理由(reason)はホスト側で head-moved / checks-not-green(許容外 state の存在に加え、チェック総数 0 件・gh pr checks 非ゼロ終了の fail-closed 辞退を含む) / merge-failed → 再監視、unresolved-threads → fix ループ(ただし手元にスレッド内容の構造化一覧がない場合は fix を起動せず再監視し、監視エージェントに内容を収集させる)、not-mergeable(コンフリクト等) → conflicting(base 取り込み専用エージェントへ回す。fixCount を消費しない独立予算 baseMergeCount)、wrong-target(base 不一致・draft。fix では解消しないため fix 予算を消費しない) → blocked、pr-closed → blocked、external-review-missing → blocked(同一ラン内で再監視しても到着を保証できないため fail-open せず終端し、チェック到着後の再実行で monitoring 再開により継続する。終端理由には確定済み slug 一覧と「解消しない場合は slug の誤記・当該 App 未導入を疑い、App の導入状況を確認するか当該 slug を args.externalChecks から除外する」旨を添える。合格条件の提示は App 種別で出し分ける: cursor は「HEAD sha に対する cursor[bot] レビューの到着(1 件以上)かつ CHANGES_REQUESTED 0 件(Bugbot は APPROVED を返さないため APPROVED を待たない)」、cursor 以外の slug は「check-run の合格 conclusion、check-run 0 件時のみ APPROVED レビューへフォールバック」を明記する)、enum 外 → systemic failure、へマッピングされる。

マージ実行条件:

  1. CI 全 green: 全チェックが success / neutral / skipped で完了し、failure / cancelled / timed_out が 0 件かつ pending / queued / in_progress が 0 件であること。pending が残るなら監視を継続する。かつチェック総数が 1 件以上存在すること。0 件は green とみなさず、監視側は最大 10 分の再確認後に blocked(quality)で停止する(workflow の on: 条件・パスフィルタによる全 job スキップや required workflow 未配置で CI が一度も起動していない PR を自動マージしない fail-closed。merge-exec 側もチェック総数 0 件・gh pr checks の非ゼロ終了を checks-not-green として辞退する)。例外: state が OPEN かつ mergeable: CONFLICTING の PR はチェック総数 0 件であっても blocked へは進まず、そのまま conflicting(base 取り込み専用エージェントへ回す。fixCount を消費しない独立予算 baseMergeCount)へ回す — コンフリクト PR は test merge commit が作られないため pull_request トリガーの CI check-run が構造的に起動せず、待っても収束しないため。mergeable: UNKNOWN は算出待ちとして扱い、CONFLICTING とは扱わない(誤検出による base 取り込み予算の空費を防ぐ)。 監視側の 0 件検出: monitor は gh pr checks --watch に入る前に head のチェック総数(check-run + commit status の合計)を取得し、確定値が 0 件ならただちにこの総数 0 件の判定(有界待機 + mergeable 再判定)へ直行する(どちらかの取得に失敗した場合は 0 件扱いにせず checksTotal を省略して --watch へ進む)。--watch の再実行(4 回)を使い切った場合も timeout を返さず総数確認へ進む。timeout を返してよいのは「チェックが 1 件以上存在し pending のまま監視上限に達した場合」のみで、ホストは checksTotal: 0 の timeout を受理せず conflicting へ再判定する(0 件のまま監視枠 7 ラウンド〔1 ラウンド最長およそ 40 分〕を空費して failed 終端する経路を塞ぐ)。
  2. 外部チェック指摘なし(または「外部チェックなし」が args.externalChecks: [] で確定していること): args.externalChecks と Step 1 の観測結果に基づき後述の待機手順を実施する。構成が確定できない場合・確定済みの外部チェック App について HEAD sha に対する合格の根拠(許容 conclusion の check-run、または APPROVED レビュー)を確認できない場合はマージしない(「指定した App のチェックが緑」ではなく「指定した App のチェックが存在しかつ緑」を条件とする)。
  3. 未解決レビューコメントなし: GraphQL API で全スレッドが resolved 済みであること。resolve を実行してよいのは Merge ループの fix エージェント(push する版)のみ: 自分の修正がリモート head に反映済みであることを前提に、(a) 当該ラウンドの push 成功直後は自分が修正対応したスレッド(monitor の構造化出力由来・host 検証済みの threadId に限る。fix が自分でスレッド一覧を再取得して対象を広げることは禁止)を、(b) push なしラウンド(過去ラウンドで修正・push 済み)はホストが決定的に算出した許可リストのみを対象に、GraphQL resolveReviewThread mutation で resolve し、required_review_thread_resolution ゲートを人手なしで解消する(fix 以外の全経路での resolve は禁止する。オーナー判断)。(b) の許可リストはホスト側の決定的照合のみで算出する: monitor が毎ラウンド返す compareStatus(ホストが渡した前回観測 sha から今回 headSha までの gh api compare 結果)を純粋関数 applyResolveProofObservation が観測し、ahead かつ直前ラウンドで実際に push が成功していた場合のみ resolveProof.pushHead を進めて changedFiles を累積する(behind/diverged/取得不能は force-push 等とみなし fail-closed で全体をリセット)。許可リストは computePermittedNoPushResolveIds が、その resolveProof.files(host 実測済み push の変更ファイル集合)に path(GraphQL reviewThreads の path)が含まれるスレッドのみへ絞って算出する(fix 自身の申告 sha・自前の git fetch / merge-base --is-ancestor による反映確認は一切使わない。ファイル内容の反映確認による代替経路も設けない)。resolveProof はプロセス内限定で状態ファイルへ永続化せず、resume 直後は必ず空(fail-closed)から再測定する。現状(2026-08-21): compareStatus/changedFiles は未信頼レビュー本文を読む monitor の自己申告にすぎず host が gh api compare を自ら実行して裏取りできないため、computePermittedNoPushResolveIds は proofState の内容に関わらず常に空リストを返す(fail-closed)。(b) 経路は専用の未信頼テキスト不読 proof エージェント新設まで恒久的に不成立であり、resolve が成立するのは (a) のみである。詳細な状態遷移・残存リスクは references/automerge-design.md「resolve 前提のホスト側決定的照合」を参照。monitor / merge-exec / merge-verify / Review ループ(push 前)の fix は resolve mutation を実行しない。resolve の失敗は致命的ではなく、未解決のまま残ったスレッドは次周回の monitor が unresolved として拾う。fix エージェントが検討した結果 fix 不能・現イシューのスコープ外と判断したコメントは、resolve せずその場で Issue 化もせず、対応しない理由と対応案を references/out-of-scope-support.md の「実装対象外(out-of-scope)の扱い」節の手順に従い PR 本文の「対象外(out-of-scope)」節に記録する(対象外スレッドへの自動フローの責務は記録まで)。記録されたスレッドは未解決のまま残るため、人間が resolve しない限り監視は unresolved-comments → blocked へ落ち、最終レポートでの issue 化承認・手動 resolve の判断に乗る。P0/P1 相当・セキュリティ上の指摘(脆弱性・認証認可・秘密情報露出・破壊的操作等)は「対応不要・スコープ外」の記録のみで済ませることを禁止する(修正するか、修正不能なら blocked としてユーザー判断へ委ねる。判断がつかない場合は安全側に倒し P0/P1 相当として扱う)。Issue 化の要否はユーザー承認前に確定させない(Issue 化の実行判断は同節の手順 3・4 に従い最終レポート確認時にユーザー承認のうえで実施する)。

gh pr checks --watch が終了しても「watch が終わった」だけで合格にしない。gh pr checks ${prNumber} の出力で全チェックの結論を列挙して確認する。pending が残る場合は再 watch する。failure 等があれば修正エージェント(fix)へ渡す。

外部チェック待機の 4 分岐(args.externalChecks と Step 1 の観測結果による):

  • 確定不能(externalChecks 未指定): 外部レビューを省略してよいか判断できないため、CI の結果にかかわらず state: blocked で停止する。ホスト側にも同じゲートがあり、監視エージェントが ready を返しても新規マージは行わず blocked で終端する(プロンプト + ホストの二重検証)。停止理由には観測結果(参考値)と再実行用の args 例が記録され、blocked + pr は次回ランの monitoring 再開対象となる。ただし PR が既に MERGED の場合(前回ランでマージ済み・状態記録に失敗した PR)のクローズ・状態記録の回復は、回復専用 merge-exec(allowMerge=false。プロンプトに gh pr merge を含まない)+ merge-verify の state=MERGED 独立確認を経て merged 終端できる(新規マージ経路は開かず、PR がマージ済みでなければ未確定理由の blocked で終端する)。
  • 外部チェックなし確定(externalChecks: []): 外部レビュー待機はスキップする。CI 全 green と未解決スレッドなしのみで判定する。
  • cursor(Cursor Bugbot): cursor[bot] によるレビュー待機フローを実行する。HEAD sha に対するレビューが不在なら @cursor review を 1 回だけ催促する(再投稿はしない)。Bugbot は自動実行では指摘 0 件のときレビューを投稿せず check-run のみを completed にするため、レビュー不在を「指摘なし」と解釈してはならない(この場合に催促しないと指摘なしの PR が恒久的に blocked になる)。明示依頼なら指摘 0 件でも「新規指摘なし」のレビューが投稿される。check-run は催促してよいタイミングの判定(queued / in_progress なら待つ)と失敗検出(許容外 conclusion なら needs-fix)にのみ使い、合格 conclusion を「指摘なし」の根拠にはしない(指摘ありでも success / neutral の双方が観測される)。HEAD sha に対する cursor[bot] レビューの到着を最大 10 分待ち、到着すれば指摘解決を待ってからマージする。到着しない場合は「レビューなし」とみなさず state: blocked で停止する(App の障害・遅延・起動失敗時にレビューゲートを迂回させないための fail-closed。レビュー到着後に再実行すれば monitoring 再開で継続する)。
  • cursor 以外の外部チェック(例: sonarcloud): gh pr checks --watch(CI 監視)は「存在するチェックが緑になったか」しか保証せず、App がそもそも起動していなければ何も監視しないまま全 green と判定される。そのため App ごとに HEAD sha に対する check-run の起動そのものを確認する(この確認がないと、externalChecks で sonarcloud を明示しても SonarCloud が未起動のままマージできる fail-open になる)。0 件なら最大 10 分待って再確認し、それでも 0 件なら state: blocked で停止する。check-run を作らずレビューのみ投稿する App のために <slug>[bot] レビューの HEAD sha 一致もフォールバックとして確認する(レビューは state まで検証する。合格にできるのは「APPROVED が 1 件以上、かつ CHANGES_REQUESTED / COMMENTED / PENDING が 0 件」の場合のみで、否定的レビューが APPROVED と併存する場合も不合格とする。merge-exec はレビュー本文を読まず内容を評価できないため、評価できないものは fail-closed で不合格とする。DISMISSED は GitHub 上で無効化済みのため判定に含めない)。
    • cursor と他 App を併記した構成(例: [{"app": "cursor", "context": "Cursor Bugbot"}, {"app": "sonarcloud", "context": "SonarCloud Code Analysis"}])では、cursor のレビュー到着確認に加えて他 App の起動確認も併せて実施する。
    • blocked が再実行でも解消しない場合は slug の誤記、または当該 App が対象リポジトリで動作していない可能性がある。App の導入状況を確認するか、args.externalChecks から当該 slug を除外して再実行する。
# HEAD sha を取得(push のたびに取り直す)
HEAD_SHA=$(gh pr view <pr-number> --json headRefOid -q .headRefOid)

# CI 監視
gh pr checks <pr-number> --watch --interval 60

# watch 完了後、全チェックの結論を列挙して確認する
# failure / cancelled / timed_out が 0 件、pending / queued / in_progress が 0 件であること
gh pr checks <pr-number>

# Bugbot(cursor[bot])レビューが HEAD sha に対して到着しているか確認する(cursor 確定時のみ)
# commit_id が HEAD_SHA と一致するレビューを探す(30 件超のレビューを取りこぼさないよう --paginate 必須)
gh api --paginate "repos/{owner}/{repo}/pulls/<pr-number>/reviews" \
  --jq "[.[] | select(.user.login == \"cursor[bot]\" and .commit_id == \"${HEAD_SHA}\")] | length"
# → 合計が 0 の場合は最大 10 分待つ(HEAD push から 1 分以上経過後に @cursor review を 1 回だけ催促可)
# → 待機上限を超えても到着しない場合は blocked で停止する(「レビューなし」として先へ進まない)

# cursor 以外の外部チェック App(例: sonarcloud)が HEAD sha に対して起動しているかを確認する
# commits/<sha>/check-runs は sha でスコープ済みのため jq 側で sha 比較は不要
gh api --paginate "repos/{owner}/{repo}/commits/${HEAD_SHA}/check-runs" \
  --jq '[.check_runs[] | select(.app.slug == "sonarcloud") | (.conclusion // .status)] | group_by(.) | map({v: .[0], count: length})'
# → 出力は状態 enum ごとの件数のみ(チェック名・description は取得しない)
# → 全ページの count 合計が 0 なら未起動。最大 10 分待って再確認し、なお 0 なら blocked で停止する
# → 0 件のときは <slug>[bot] レビューをフォールバックとして確認する(state 別件数のみ取得する)
gh api --paginate "repos/{owner}/{repo}/pulls/<pr-number>/reviews" \
  --jq "[.[] | select(.user.login == \"sonarcloud[bot]\" and .commit_id == \"${HEAD_SHA}\") | .state] | group_by(.) | map({v: .[0], count: length})"
# → 合格にできるのは APPROVED が 1 件以上かつ CHANGES_REQUESTED / COMMENTED / PENDING が
#   0 件の場合のみ。否定的レビューが APPROVED と併存する場合も不合格(fail-closed)

# マージ実行エージェント側の再検証も本文を読まず「件数・状態 enum」のみへ正規化して取得する
# (確定済み App ごとに実行する。合格の根拠が 1 件もなければ external-review-missing でマージしない)
gh api --paginate "repos/{owner}/{repo}/pulls/<pr-number>/reviews" \
  --jq '[.[] | select(.user.login == "cursor[bot]" and .commit_id == "<検証した HEAD sha>")] | length'
gh api --paginate "repos/{owner}/{repo}/commits/<検証した HEAD sha>/check-runs" \
  --jq '[.check_runs[] | select(.app.slug == "sonarcloud") | (.conclusion // .status)] | group_by(.) | map({v: .[0], count: length})'
# → --jq はページごとに適用されるため出力はページ数ぶんになる。全ページを合計して判定する

# レビュースレッドの解決確認(GraphQL)— 100 件超はページネーションで全件取得する
# after: $cursor を使い pageInfo.hasNextPage が false になるまでループする
gh api graphql -f query='
  query($owner: String!, $name: String!, $number: Int!, $cursor: String) {
    repository(owner: $owner, name: $name) {
      pullRequest(number: $number) {
        reviewThreads(first: 100, after: $cursor) {
          nodes { isResolved comments(last: 1) { nodes { body author { login } } } }
          pageInfo { hasNextPage endCursor }
        }
      }
    }
  }' -F owner="{owner}" -F name="{repo}" -F number=<pr-number> -F cursor=""

# CI 全 green・外部チェック指摘なし・未解決レビューコメントなしの場合のみ squash merge
# (実行するのは監視エージェントではなくマージ実行エージェント。上記条件を自ら再取得して検証したうえで実行する)
gh pr merge <pr-number> --squash --delete-branch --match-head-commit <検証した HEAD sha>

全チェックが pass に見えるのにマージが進まない場合(cancel された run の残存 check):

  • フローから観測できる症状(自動検知は存在しないため、実装されていない挙動は書かない): gh pr checks は全 pass、mergeable は MERGEABLE、コンフリクトも未解決スレッドもないのに、merge-exec が進まず再監視が繰り返され監視予算だけが減る。この状態では mergeStateStatus が BLOCKED のまま動かない。mergeStateStatus は現在どのエージェントも取得していないため、運用者が gh pr view <N> --json mergeStateStatus で確認する。
  • 原因: 同一 head sha に対し concurrency で複数 run が走ると、cancel された run が失敗結論の check run を残す。gh pr checks は同名 check の最新のみを表示するため全 pass に見えるが、branch protection の required check 判定は残存 check を拾って BLOCKED になる。同一の変更を多数のリポジトリへ同時投入する運用(後述「一斉同期・大量 PR 投入時の運用ガード」)では concurrency 競合の発生頻度が上がるため、この状態に遭遇しやすい。
  • 検知コマンド(エージェントが実行してよいものと、人間の診断専用を明確に分ける):
# (A) エージェントが実行してよい形。取得成否を先に確定してから「件数」のみを返す。
# gh api は HTTP エラーの JSON 本文も stdout へ出す仕様のため、パイプ直結だと認証失敗・404・
# レート制限の出力が uniq -d にヒットせず「重複なし(0)」に化ける
# (対象リポジトリに .claude/rules/ruleset-policy.md があれば手順 B と同じ罠として記載されている)。
# そのため (1) 取得を独立させて終了コードを見る (2) 出力の空判定を行う (3) 集計は shell 側で
# 行う、の 3 段に分ける(--jq はページごとに適用されるため group_by をページ単位で行うと
# ページ跨ぎの重複を見逃す。名前+結論の一覧をシェル側 awk で全ページ分集約する)
if ! command -v awk >/dev/null; then
  # awk 前提条件が未導入。集計不能なため判定不能として扱う(fetch 自体を実行しない。
  # (B) の command -v jq ゲートと同じく前提確認を fetch より先に行う — レート制限下で
  # 無駄な gh api 呼び出しを発生させないため)
  echo "UNDETERMINED"
else
# `rows=$(gh api ...)` を独立した単純コマンドのまま実行すると、呼び出し元 shell で
# `set -e`(errexit)が有効な場合に gh api の非ゼロ終了(認証失敗・404・レート制限等)で
# shell がここで即終了し、次行の status=$? および UNDETERMINED 分岐へ到達できない
# (if/then/else の条件式に置かれたコマンドは errexit の対象外という shell の仕様を利用し、
# 代入自体を条件式へ移すことで errexit 下でも必ず失敗分岐を実行できる形にする)。
if rows=$(gh api --paginate "repos/{owner}/{repo}/commits/${HEAD_SHA}/check-runs?per_page=100" \
  --jq '.check_runs[] | [.name, (.conclusion // "pending")] | @tsv' 2>/dev/null); then
  status=0
else
  status=$?
fi
if [ "${status}" -ne 0 ] || [ -z "${rows}" ]; then
  # 取得失敗、または check-run が 1 件も返らない。この節は「全チェックが pass に見える」状態
  # でのみ参照するため、0 件は前提と矛盾する = 取得できていない可能性が高く、判定不能として扱う
  echo "UNDETERMINED"
else
  printf '%s\n' "${rows}" | awk -F'\t' '
    { n[$1]++
      if ($2 == "success" || $2 == "neutral" || $2 == "skipped") { }
      else if ($2 == "pending") pend[$1] = 1
      else bad[$1] = 1 }
    END { d = 0; b = 0; p = 0
          for (k in n) if (n[k] >= 2) { d++; if (k in bad) b++; if (k in pend) p++ }
          printf "dup=%d bad=%d pend=%d\n", d, b, p }'
fi
fi
# → 出力は次の 2 形のみ(チェック名・エラー本文は出力に現れない):
#    `UNDETERMINED`               … 判定不能。「重複なし」ではない
#    `dup=<D> bad=<B> pend=<P>`   … 取得成功。D = 重複した check 名の数、
#                                     B = そのうち結論が `success` / `neutral` / `skipped`
#                                     (いずれも required status checks 上は合格・非ブロック扱い)
#                                     でも `pending`(未完了)でもないものを含む数(cancelled /
#                                     failure / timed_out / action_required / startup_failure /
#                                     stale 等、`success`・`neutral`・`skipped` を正常扱いする
#                                     以外は全て bad へ倒す fail-closed 分類)、P = そのうち
#                                     結論が `pending`(未完了。実際の conclusion が null で
#                                     in-progress/queued 中)を含む数
# → 読み方: **取得に成功したうえで** D が 0 なら重複なし。D >= 1 でも B = 0 かつ P = 0 の
#    場合のみ「重複はすべて正常な再実行(success/neutral/skipped 同士)」と読める。B・P は排他ではなく、
#    同じ重複名の中に bad な結論と pending な結論が両方含まれる場合は B・P 双方が 1 になる。
#    上記 2 形(正規表現 `^UNDETERMINED$` / `^dup=[0-9]+ bad=[0-9]+ pend=[0-9]+$`)以外の
#    出力も判定不能として扱う
# (B) 人間の診断専用。重複の内訳をチェック名つきで確認する
# チェック名は PR 側の workflow / job 定義から生成される未信頼テキストのため、
# monitor / merge-exec のコンテキストでは実行しない(権限境界の維持)
# 前提: 外部の `jq` CLI が必要(`gh --jq` はページ単位適用のためここでは使えない)。
# 事前に `command -v jq` で存在確認し、なければ実行せず `blocked` とする
command -v jq >/dev/null || { echo "jq が見つからないため診断を中断し blocked とする" >&2; exit 1; }
# `--paginate` の `--jq` は取得したページごとに個別適用されるため、group_by をそのまま
# 使うとページ単位の集計になり、同名 check-run が別ページに 1 件ずつ分かれて存在する場合に
# 検知できない(各ページ内では件数 1 の group にしかならず、実際は重複していても取りこぼす)。
# `--slurp` を付けると全ページを外側の配列として受け取れるが、gh api は `--slurp` と `--jq` の
# 併用を拒否する(`the --slurp option is not supported with --jq or --template`)ため、
# `--jq` は使わず `--slurp` の生 JSON 出力を外部の `jq` へパイプし、平坦化してから group_by する
# ことでページ跨ぎでも全件を一度に集約してから判定する。
# まず check 名だけで group_by し、件数 2 以上の group(=実際に重複している名前)に絞り込んでから、
# その中で conclusion 別の内訳を出す(name=conclusion のペアで group_by すると、同名 2 件が
# cancelled/success のように conclusion 違いで割れて各 group が件数 1 になり、重複そのものを
# 取りこぼすため、必ず name のみで group_by する)
gh api --paginate --slurp "repos/OWNER/REPO/commits/<sha>/check-runs?per_page=100" \
  | jq -r '[.[] | .check_runs[] | {name, conclusion}]
        | group_by(.name)
        | map(select(length >= 2))
        | map("\(.[0].name): " + ([.[] | .conclusion] | sort | group_by(.) | map("\(.[0]) x\(length)") | join(", ")))
        | join("\n")'
  • 対処(前提を先に実測してからコマンドを実行する。判断・実行の主体はラン運用者/ホスト側であり、monitor / merge-exec エージェントではない。(B) は人間の診断専用のため、このフロー全体がエージェント自律では完結しない):
    • 前提 0(判定不能の扱い): (A) が UNDETERMINED を返した、または上記 2 形以外を返した場合は判定不能。rerun せず blocked(quality)として最終レポートへ回す。判定不能を「重複なし」と読んで CI 由来を除外してはならない(認証失効・レート制限・sha 誤りが典型原因。人間が原因を確認する場合は stderr を捨てずに同じ gh api を再実行する)。
    • 前提 1(重複と結論の実測): (A) が dup=<D> bad=<B> pend=<P> を返し、D・B・P を実測する。
      • D >= 1 かつ P >= 1 の場合: 重複の中に pending(未完了)の check-run が残っている。この pending 自体が mergeStateStatus=BLOCKED の直接原因になり得るため、「重複はすべて正常な再実行」と断定して原因調査を別方向へ進めてはならない。rerun せず、pending の完了を待って再監視する(判断・実行の主体はラン運用者/ホスト側。原因不明のまま前提 2 の rerun フローへ進めない)。
      • D >= 1 かつ P = 0 かつ B >= 1 の場合のみ、前提 2(rerun 対象の一意化)へ進む。
      • D >= 1 かつ B = 0 かつ P = 0 の場合、重複はすべて正常な再実行(success/neutral/skipped 同士)由来であり「cancel された run の残存 check」ではない。rerun せず、BLOCKED の別原因(required check の context 名不一致・未解決レビュースレッド・ruleset 構成など。対象リポジトリに .claude/rules/ruleset-policy.md があればその 3 軸スイープ〔strict / bypass_actors / integration_id 残存〕を使う)へ調査を移す。
    • 前提 2(rerun 対象の一意化): (B) で重複している check 名を確認し、その名前を発行した cancelled run を job 一覧から特定する(下記コマンド)。
      • cancelled run が複数見つかり一意に絞り込めない場合: rerun せず blocked(quality)として最終レポートへ回す(誤った run を rerun すると無関係な job まで再実行し、原因不明のまま状態を変える)。
      • cancelled run が 0 件の場合: rerun 対象が存在しない。B >= 1 の残存は cancel ではなく failure / timed_out / action_required / startup_failure / stale 等の非 cancel 由来である。この残存も cancel 残存と同じ masking を受ける点に注意する — gh pr checks は同名 check の最新結論のみを表示するため(前掲「原因」節参照)、より新しい success / neutral / skipped の陰に隠れた古い failure / timed_out 等は gh pr checks の出力に現れず、通常の可視 CI 失敗としては検知できない。監視フローの needs-fix 経路(gh pr checks ベースの CI 失敗検知)に任せると見逃されるため、rerun はせず blocked(quality)として最終レポートへ回す。原因調査が必要な場合は (A)/(B) の生の check-runs 出力(gh pr checks ではなく)を根拠に、当該 check-run を発行した run をラン運用者が個別に特定・対処する。cancel 起因と決めつけて gh run rerun しない。
    • 上記を満たさないまま rerun しない(rerun は CI を再起動するため、「Review 通過後に CI を 1 回だけ起動する」設計に反する)。
# cancelled な run を head sha で列挙する(conclusion=cancelled のみに絞る)
gh api --paginate "repos/OWNER/REPO/actions/runs?head_sha=<sha>&per_page=100" \
  --jq '.workflow_runs[] | select(.conclusion == "cancelled") | .id'

# 各 cancelled run が発行した job 名を確認し、(B) で特定した重複 check 名と突き合わせる
# (check-run の名前は `<job名>` または `<job名> / <ステップ>` 形式で job に対応するため、
# jobs API の name で同定できる。複数 run の job 名が同じ重複 check 名にヒットする場合は一意化不可)
gh api --paginate "repos/OWNER/REPO/actions/runs/<cancelled-run-id>/jobs?per_page=100" \
  --jq '.jobs[].name'

# 一意に対応付けられた run に限り、全 job を回して残存 check を上書きする(--failed は付けない)
gh run rerun <run-id> -R OWNER/REPO
  • 完了ゲート整合: rerun したこと自体は green の証拠にならない。再実行後に改めて全チェックの結論を列挙し、failure / cancelled / timed_out が 0 件・pending / queued / in_progress が 0 件・チェック総数 1 件以上を確認してから合格と判断する(対象リポジトリに .claude/rules/verification.md があればその 5 段階ゲートに従う)。解消しない場合は推測で進めず blocked として最終レポートへ回す。

CI 失敗・外部チェック指摘・未解決レビュースレッドがある場合は、修正エージェント(fix)が detached HEAD で対象ブランチを取得して指摘を反映し再 push する。fix は修正作業(コミット)より前に base を必ず取り込む(git fetch → git merge)ため、base が動いていても次ラウンドの monitor へ影響しない。修正エージェントも worktree 隔離で動作するため、他の並列イシューのブランチに干渉しない。

base 取り込み専用ループ: 作成時点から base とコンフリクトしていた PR(monitor が mergeable: CONFLICTING で検出。上記マージ実行条件 1. の例外。merge-exec の not-mergeable 写像も同じ経路)は fix ループへは回さず、独立の base 取り込み専用エージェント(baseMergePrompt)を起動する。品質問題ではないため fixCount を一切消費せず、独立予算 baseMergeCount(上限 args.maxBaseMerges。既定 3)で有界化する。base 取り込みエージェントはレビュー指摘の修正・スレッド resolve・PR 本文編集を一切行わず、base の取り込み・コミット・push のみを担う。コンフリクトは種別を問わず解消しない(fail-closed)。通常ファイルの内容コンフリクトは編集する権限を持たず(build/lint/test で妥当性確認できないため)、submodule(gitlink)ポインタのコンフリクトも双方がポインタを変更した状態であり base 側の機械的採用は PR 側の gitlink 更新を黙って破棄するため解消しない。いずれも解消を試みず commitFailed: true で停止する。内容コンフリクトの検出時はあわせて conflict: true を返し、ホストは failed 終端せず同一周回で fix 経路(fixPrompt の push 前 base 最新化ゲート。build/lint/test の検証を正当に実行でき fixCount〔上限 6〕で有界)へ委譲してコンフリクト解消・検証・push を行わせる(コミット未作成のため baseMergeCount は消費しない。conflict はエージェント自己申告だが、誤申告の最悪ケースは検証付き・有界の fix 経路へ余分に回るだけで安全側)。base 取り込みマージコミットの subject は host のリテラル固定値 chore: base ブランチの変更を取り込む を使い、push 権限を持つ base 取り込みエージェントは commitlint 設定・コミット履歴を含むリポジトリ内ファイルを一切読まない(subject 算出を読み取り専用エージェントへ分離する方式は、読み取り専用がプロンプト指示のみでランタイム強制されないため採らず、未信頼読取そのものを自動 base 取り込み経路から排除する)。この固定 subject が commit-msg hook(type-enum / scope-enum を持つ commitlint 等)に拒否された場合(分岐 (c))は commitFailed: true に加えて hookRejected: true を返し、ホストは conflict と同様に failed 終端せず fix 経路へ委譲する(fix の push 前 base 最新化ゲート = runVerification=true が commitlint 設定を正当に読める権限で subject を決定し、マージコミットを作成・検証・push する)。conflict / hookRejected を伴わない commitFailed(branch / base fetch 失敗・checkout 失敗・subject 未確定等)は自動回復不能クラスとして failed 終端する。push 後は gh pr view で mergeable を再取得し、UNKNOWN なら再試行したうえでログ専用の mergeableAfter として記録し(マージ判定・分岐には使わない。実際の判定は次ラウンドの monitor がサーバー側の実値を再観測して行う)、あわせて push 後の headRefOid に対する check-run の起動有無(checksStarted。完了は待たない)もログ専用で記録する。base 取り込み上限到達時の分類: baseMergeCount >= args.maxBaseMerges に達すると blocked / blockedReason: "quality" で終端する(halt 非カウント。human が PR ブランチへ直接 base を取り込んで push すれば解消し得るため — fixCount 上限到達時の needs-fix〔fixCount が尽きても自動でしか状態が変わらない〕とは異なる。unrecoverable〔status: "failed"〕は使わず blocked とし、args-example.json と共通の「コンフリクトは blocked 終端」という公開契約に一致させる)。再実行時は monitoring 再開(isActiveMonitoring。blocked + pr 保持)により同じ PR を直接再監視し、Recover / Implement / PR Create の各フェーズは通らない。baseMergeCount は状態ファイルから到達値のまま引き継がれる(monitoring 再開時は保存値を maxBaseMerges へ clamp するのみでリセットしない)ため、この分岐へ再入しても base 取り込みループは自動では再実行されない。解消できるのは human が PR ブランチへ直接 base を取り込んで push した場合のみで、その後は次回 monitor が mergeable: CONFLICTING を検出しなくなり本分岐自体を通らなくなる。fix エージェントは修正がリモート head に反映済みであることを前提に、(a) push 成功直後は自分が修正対応したスレッド(monitor 由来・host 検証済み threadId に限る)、(b) push なしラウンドはホストが決定的に算出した許可リストのみを resolveReviewThread mutation で resolve し、resolve に成功した threadId を resolvedThreadIds で報告する(resolve 失敗は致命的ではなく、未解決のまま残ったスレッドは次周回の monitor が unresolved として拾う)。fix 対象外と判断したコメントは resolve せず、references/out-of-scope-support.md の「実装対象外(out-of-scope)の扱い」節の手順に従い PR 本文へ記録する(対象外スレッドへの自動フローの責務は記録まで。人間が GitHub 上で resolve しない限り未解決のまま残り、blocked → 最終レポートで issue 化承認・手動 resolve を判断する)。監視(monitor)は通常予算として最大 7 回まで実行する(後述の「強制スレッド再走査の救済ラウンド」で 1 回だけ延長されるため、実行全体の絶対上限は初期予算 + 1 の 8 回。詳細は同節を参照)。push 直後の CI 起動確認が mergeableAfterPush: CONFLICTING を報告したラウンドは monitor エージェントを起動しないため監視枠を消費しない(このヒントが立つ回数は PR 作成 1 回 + fix 回数〔上限 6〕に有界であり、監視回数が無限に延びることはない)。push も新規スレッド resolve も無いラウンドが 2 回連続したイシューは blocked として記録する(push なしでも monitor 報告済み・未計上の threadId への resolve を報告したラウンドは進捗ありとして連続カウントをリセットし再監視へ継続する。resolve の実効性は次周回 monitor がサーバー実値で確認する)。監視エージェントが blocked を返す場合は blockedReason(quality / unrecoverable)の付与を必須とし、ホスト側でも enum を二重検証する。省略・enum 外は unrecoverable として扱う(fail-safe)。quality(再監視・再実行で解消し得る)のみ状態ファイルへ blocked で終端して次回ランの monitoring 再開対象とし、unrecoverable(PR の未マージクローズ等)は failed で終端して再開対象から外す。修正(fix)の上限は Review と共有(上限 6)。詳細は Review ステップ参照。

修正上限(6 回)到達時の分類: 上限到達で blocked へ落ちる際は、上限に達した時点で観測していた状態で再開可否を分類する。unresolved-comments(未解決スレッドが実在する)は人間の resolve で解消し得るため quality(blocked 終端・monitoring 再開対象)、needs-fix(CI 失敗等)は修正予算が尽きているため unrecoverable(failed 終端・再開対象外)とする。後者を再開可能にすると、fixCount が上限のまま復元されたランが「即 blocked」を毎回繰り返し、blocked は halt の連続カウントに乗らないため停止防御も働かない。base 取り込み上限(baseMergeCount >= args.maxBaseMerges)はこの unrecoverable 側の判断とは分岐する: needs-fix は自動修正の予算切れであり自動化の外側では状態が変わらないが、base コンフリクトは human が PR ブランチへ直接 base を取り込んで push すれば解消し得る。解消されれば次回 monitor はもう conflicting を返さず上限判定の分岐自体を通らないため、「即 blocked を毎回繰り返す」懸念が同じ形では成立しない。したがって base 取り込み上限到達は quality(blocked 終端・monitoring 再開対象)に分類する。

StructuredOutput 未返却時の fail-safe 分類: Merge ループ突入時点で対象イシューの PR は必ず作成済みのため(impl.prNumber は runImplement が PR 作成成功後にのみ Merge ループを呼ぶ)、監視・マージ実行・マージ検証・base 取り込み・修正のいずれかのエージェント呼び出しが StructuredOutput を返さず終了した場合(agent() の例外・null 返却いずれも同じ経路へ合流させる)、systemic failure として failed(Recover → 通常 Implement 経路。再 PR 作成を含む)へ倒すと既存 PR に対して重複 PR を作りうる。そのため既存の enum 外応答(モデルが何かを返した上での不整合。厳格な failed を維持)とは区別し、blocked(次回実行で monitoring 再開)に分類する。対象は「PR が既に存在する Merge ループ内」に限り、Plan / Implement / Review / Recover / PR Create(pr: 0)の失敗分類には一切触れない(halt 防御は弱めない。blocked はこのランの中で自動リトライを一切行わない)。ルートノード(verify-close)は pr / worktree の概念を持たないため、同じ理由で blocked(halt 非カウント)に分類し、次回実行時に verify-close を素のまま再実行する。詳細は references/recovery.md の「StructuredOutput 未返却時の fail-safe」節を参照。

強制スレッド再走査の救済ラウンド: merge-exec が unresolved-threads(未解決スレッドの「件数」だけを検出)を返し、かつスレッド内容の一覧が手元にない場合、ホストは fix を起動せず forceThreadRescan を立てて次ラウンドの monitor に手順 5 の強制再走査を指示する。このとき監視予算がすでに尽きていると救済ラウンドが一度も走らないため、実行全体で 1 回だけ監視枠を延長する(2 回目以降は延長せず残り予算で終端する。merge-exec が空一覧を返し続けても監視回数は初期予算 + 1 で有界)。

延長した救済ラウンドは残り予算ゼロで走るため、その回の結果がそのまま終端になる。救済ラウンドの終端分類は、ラウンド末尾での即時判定ではなく監視ループ退出後の単一地点で 1 回だけ評価する(ready / needs-fix 等の有意な結果が得られた場合は通常の分岐処理へ進む)。判定は timeout の出所で分岐する: 監視エージェント自身が観測に失敗して返した timeout(=同一救済ラウンドで merge-exec の一過性 reason 写像が発生していない)のみを、**終端 status blocked(halt 非カウント・次回ランの monitoring 再開対象)**に分類する。救済は「未解決スレッドの内容を取り直すための追加試行」であり、観測に失敗しても「未解決スレッドが残っている」という元の品質ブロックの事実は変わらないため。lastState は timeout のまま残す(実際に観測できなかったことは終端理由の記録として正しい)。一方、同一救済ラウンドの merge-exec 由来の timeout 写像(head-moved / checks-not-green / merge-failed)は品質ブロックへ分類せず、既定の failed(halt カウント対象)へ進む。救済ラウンドの再走査自体は成立している以上、未解決スレッドが残っているとは断定できず、実体はマージ操作そのものの失敗であるため。出所を問わず一律 blocked に分類すると、恒常的な merge 失敗(特に merge-failed)が halt 連続カウントに一切算入されず、同じ救済経路へ再入し続けて halt 防御を迂回できてしまう。詳細な判定表・設計根拠は references/automerge-design.md の「救済ラウンドの終端分類」節を参照。

対象外コメントの省略件数: outOfScopeLog は本体 20 件 + 省略マーカー行((他 N 件省略))1 件の最大 21 件で永続化する。マーカーは配列全体で 1 行だけを使い、後続の fix ラウンド・中断再開を跨いで N を累積更新する。あわせて、対象外と申告済みの threadId 集合を outOfScopeSeen として状態ファイルへ保存し、再開時に復元する(省略されて outOfScopeLog に本文が残らなかった threadId を失うと、再開後の同一スレッド再申告が省略件数へ重複加算されるため)。

Step 7: 親イシューを検証してクローズする

子を持つノード(親イシュー)は、配下のすべての子イシューが完了した時点で以下を確認してクローズする。

# 1. 全子イシューが closed か確認(--paginate で 100 件超も全ページ自動取得)
gh api --paginate "repos/{owner}/{repo}/issues/<parent>/sub_issues?per_page=100" --jq '.[].state'

# 2. 受入基準・チェックリストを読む
gh issue view <parent-number>

# 3. 受入基準を満たしていればクローズ
gh issue close <parent-number> --comment "配下のサブイシューがすべて実装・マージ完了。受入基準を確認してクローズ。"

open のサブイシューが残っている場合、または受入基準が未達の場合はクローズせず failed として記録する。親ノードは全子イシューが完了するまで投入されないため、子の並列実行完了後に検証される。

Step 8: 最終レポートを生成する

全イシューの処理結果をまとめてレポートを出力する。1 イシューの失敗では即停止せず次へ進むが、**3 イシュー連続で完了できなかった場合は新規着手を停止(halt)**し、ユーザーの判断を待つ。halt 後に着手しなかったイシューは not-started として記録される。out-of-scope 項目は各 PR 本文の「対象外(out-of-scope)」節(実装・セルフレビュー由来、および Merge フェーズの未解決レビューコメント由来の記録を含む)に記録されているため、レポート確認時にそれらを参照して Issue 化判断(承認後に references/out-of-scope-support.md「実装対象外(out-of-scope)の扱い」手順 3・4 を実行)を行う。あわせて、blocked / fix 対象外の未解決コメント(Merge ループの fixCount 上限到達・blocked 到達で自力解決できなかったレビュースレッド)は done 各エントリの unresolvedComments(構造化未解決コメント一覧)/ outOfScope(fix エージェントが対象外と判断したコメントのログ)フィールドに集約されるため、レポート生成時にそれらを本節へ一覧化する。

前提イシューがラン中に外部完了(人手マージ・クローズ)した場合、halt 発生前に限り下流の依存ブロック項目は同一ラン内で再判定され着手される。halt(3 イシュー連続で完了できなかった場合の新規着手停止)後もプローブと状態記録(prereqTransitions への記録・state ファイルへの永続化)は継続するが、新規着手ゲート自体は再開しない(halt は新規イシュー投入を止めるユーザー判断待ちの防御であり、外部完了検知を理由に自動解除しない)。halt 後に検知・記録された外部完了は、次回ランの再実行時に下流着手へ反映される。この再判定件数はレポートの返却値 prereqTransitions に記録される。

レポート出力テンプレート(処理結果サマリー・完了イシュー・失敗/未着手イシュー・対象外/未解決コメントの各節)と返却値フィールドの説明は以下を参照。

返却値 mainWorktreeUntracked にメイン worktree(リポジトリルート)の未追跡ファイル検査結果が入る。ラン開始時・終了時の観測差分から本ラン中に新規出現したファイルを検出し、削除はせず警告のみ行う(並行ランや人間の作業も拾い得るため帰属は推定)。observed: false は検査自体が不成立だったことを示し、その場合は git status で手動確認する。1 件以上検出した場合はレポートにも記載し、ユーザーへ手動確認・削除を促す。

詳細: references/report-format.md

検証

各実装エージェントはテストコマンドを新規実行し、出力全体と終了コードを確認してから完了を宣言する(対象リポジトリに .claude/rules/verification.md が存在する場合はそちらの5段階ゲートに従う)。「〜のはず」「たぶん通る」等の推測語での完了主張は禁止。テスト出力・終了コードを証拠として引用してから完了を宣言する。

最終レポートの「完了イシュー」に全対象イシューが列挙され、「停止イシュー」が空であることを確認する。scripts/implement-issue-tree.js を変更した場合の非信頼データ境界・残置 worktree 上限ゲート・merge-guard hook・メイン worktree への一時ファイル残置防止の適用確認手順(grep コマンド・期待結果)は以下を参照。

スクリプトの編集は開発ファイルに対して行う: scripts/implement-issue-tree.js は Workflow へ渡す実行ファイル(生成物)であり、直接編集しない。編集は scripts/implement-issue-tree.src.js(コメント込みの開発ファイル)に対して行い、node skills/implement-issue-tree/scripts/build-workflow.mjs でコメント除去済みの実行ファイルを再生成する(行番号は両者で一致する)。同期漏れ・直接編集は CI の tests/build-workflow.test.mjs(鮮度ゲート)が検出する。

詳細: references/verification.md

よくある失敗

問題 回避策
テスト失敗の原因を調査せず当て推量で修正を繰り返す 対象リポジトリに .claude/rules/debugging.md があればその4フェーズ、無ければ同じ方針(調査→分析→仮説→修正)を踏む。3回失敗したら blocked にしてユーザーへ報告
gh pr checks --watch 終了だけで CI 合格と判断する watch 後に gh pr checks <pr-number> で全チェックの結論を列挙して確認する
仕様準拠を確認せずにコード品質レビューへ移行する Step 5 のレビューは①仕様準拠→②コード品質の順に実施する
Review 前に push・PR 作成を行う push・PR 作成は Review 全通過後の Step 5.5 で行う。Review 失敗時に CI を起動させないための設計
Review fix で push してしまう Review ループの fix はローカルコミットのみ。push は Step 5.5 のみで行う
状態ファイルが壊れたまま再実行して重複 PR を作成する パースエラー時は即停止。cat _/issue-trees/<N>.json で確認してから再実行する
中断後に手動で worktree を削除してから再実行する 再実行時に Recover phase が自動処理するため手動削除は不要。手動削除してしまうと Recover が残骸なしと判定し、中断前の作業を引き継がずに Plan から新規実行する
fix 以外のエージェントがレビュースレッドを resolve する / fix が対象外スレッドまで resolve する resolve mutation を実行してよいのは Merge ループの fix(push する版)だけで、対象はリモート head に反映済みの修正((a) push 成功直後は自分が修正対応したスレッド、(b) push なしラウンドはホストが決定的に算出した許可リストのみ。fix 自身の申告 sha・自前の反映確認は不使用。(b) は現在恒久的に空リスト = 不成立)に限る。monitor / merge-exec / merge-verify / Review ループの fix は実行しない。対象外(out-of-scope)スレッドは resolve せず記録までで停止し、人間が resolve しない限り blocked → 最終レポートへ
実装コミットの scope にイシュー番号を置く(例: feat の scope に 42 を入れる) scope-enum を持つリポでは commitlint が必ず落ちる。Review 3 巡を消費した後の push で初めて検出され、--no-verify は禁止のため回避もできない。scope はモジュール・ディレクトリ名にするか省略し、イシューの紐付けは Refs #<N> / Closes #<N> で行う
P0/P1 相当・セキュリティ指摘を対象外扱いにする fix エージェントは単独で対象外と判定して記録のみで済ませてはならない。修正するか、ユーザーまたは指摘者の承認を得るまで blocked として扱う(安全側ガード)
全チェックが pass に見えるので CI 起因を除外し、PR の差分を疑って調査を続ける 同名 check-run の重複件数を実測する(Step 6 の該当分岐)。cancel された run の残存 check が BLOCKED の原因になり得る
(A) の出力を検証せず 0 を「重複なし」と読む 取得失敗・空出力・形式不一致は UNDETERMINED。CI 由来を除外せず blocked(quality)に倒す
opt-in テスト記録不足で blocked になったまま再実行を繰り返す optinFixState の非 pass・unbound 由来の不合格(latch)は設計上意図した fail-closed であり(ホストは fix の自己申告テスト実行を直接観測できないため自動解除しない)、自動では解除しない。解消は (a) 宣言テストが実際に pass する新しいコミットを push する(既存経路。新 HEAD の pass 記録へ自動的に置き換わる)、または (b) 人間が内容を確認して GitHub 上で手動マージする、のいずれか。(b) は手動マージ後に同じ args で再実行すれば、次回実行の独立確認により opt-in ゲートを介さずイシュークローズ確認まで自動的に完了する(Issue #509)。状態ファイルの optinFixState を削除・改変して迂回する手順は用意していない。PR 本文のみが原因(latch ではない通常の記録不足)の場合は references/recovery.md の手順(手元で現在の HEAD に対して実行 → PR 本文の該当マーカー行を現在の HEAD sha・pass で更新 → 同じ args で再実行)に従う
重複の bad を cancelled / failure / timed_out のみに限定し、pending・action_required・startup_failure・stale を「正常な重複」に含める success・neutral・skipped(required status checks 上は合格・非ブロック扱い)以外は正常扱いしない。pending(未完了)は別枠の pend で検知し、それ自体が BLOCKED の原因になり得るため rerun 対象探索へ進まず待機する
neutral・skipped を bad(通常の CI 失敗)として rerun 対象探索へ進める neutral・skipped は GitHub の required status checks 判定で合格扱いになる conclusion であり fail-closed 対象ではない。success・neutral・skipped の重複は正常な再実行として扱い、BLOCKED の別原因を疑う
差分と無関係なテスト失敗を確認せず flaky と決めつけて rerun する main での同ジョブ green と差分スコープの 2 点を実測してから rerun する(下記「一斉同期・大量 PR 投入時の運用ガード」参照)

一斉同期・大量 PR 投入時の運用ガード

同一の変更を多数のリポジトリへ同時に投入する運用(skill 同期など)では、差分内容と無関係な CI 由来の失敗が出る。以下は誤診しやすい 3 類型と切り分け手順。

事象 症状 対処
並列負荷による OOM リンク中に ld terminated with signal 9 [Killed] runner のメモリ逼迫が原因のため、混雑中に rerun しても同じ結果になる。キューが空くまで待つ
新規 CI 導入リポのラベル不足 CI を新規導入したリポでは dependencies / automated ラベルが無く gh pr create --label が exit 1 になり、PR がそもそも作られない 投入前にラベルを作成する
flaky の誤判定 差分と無関係なテストが落ちる 「main で同じジョブが green」「差分が当該テストに影響しえない(変更パスを実測)」の 2 点を確認してから rerun する。前者は gh run list --branch main を単独では使わない(別 workflow の成功や skip されたジョブが紛れ込み、肝心の failing job が実は main でも失敗・未実行のまま green と誤認し得る)。失敗した PR 上の workflow ファイル名と job 名を特定したうえで、gh run list --branch main --workflow <workflow-file> --json databaseId,conclusion --limit 1 --jq '.[0].databaseId' で直近 run の ID を取得し、`gh api repos/OWNER/REPO/actions/runs/<run-id>/jobs --jq '.jobs[]

推奨投入単位: 1 バッチあたり 5 リポジトリ以下に分割し、前バッチの run がすべて完了(gh run list で in_progress / queued が 0)してから次バッチを投入する(バッチ数は ceil(対象リポジトリ数 / 5) で導出する)。これは「注意事項」にある parallel の並列度指針(並列度を上げるほど CI キューが逼迫する)のリポジトリ横断版にあたる。

一斉投入時に BLOCKED が出た場合は Step 6 の「全チェックが pass に見えるのにマージが進まない場合」の分岐を参照する。

モデル / effort 割り当て

エージェント model effort 根拠
plan:issue-tree(Tree 取得・依存抽出) sonnet medium 本文読解・依存判断
plan:declared-deps-*(本文の依存宣言の機械抽出) haiku low 定型コマンド出力の転記(判断なし)
detect:external-checks(外部チェック判定) haiku low 定型コマンド集計
state:load / state:update / state:cleanup / state:init-all / state:high-water haiku(未返却時 sonnet へ 1 回フォールバック) low jq の機械処理。StructuredOutput 未返却(例外・null・schema 不適合)が続く場合のみ同一プロンプトで sonnet へ 1 回フォールバックする(詳細は references/recovery.md)
nonce:seed(境界トークン用 seed 生成) haiku low /dev/urandom 読み出しのみ(driver に乱数源が無いため。下記「非信頼データの扱い」2 を参照)
recover:#N(中断作業の継続可否判断) (指定なし=セッション継承) medium 計画判断相当(Plan と同じ軸で判断)
plan:#N(per-issue 計画立案) (指定なし=セッション継承) high 最も複雑な計画立案
impl:#N(実装) sonnet medium 計画に沿った実装(コスト最適化)
review:#N(独立 Review) sonnet medium 品質・セキュリティ判定
fix:#N(修正) sonnet medium 実装系・コスト最適化
merge:#N(CI/レビュー監視・マージ) sonnet medium CI/レビュー判定・マージ可否ゲート
close:#N(受入基準確認・クローズ) sonnet medium 受入基準確認・クローズ

中断・失敗からの再開

実行中の状態は _/issue-trees/<親イシュー番号>.json に自動保存される。セッションが中断・強制終了した場合でも、同じ args で再実行するだけで再開できる。状態ファイルの status 遷移表・worktree の自動削除・実装エージェントによる既存 PR / リモートブランチの再利用手順は以下を参照。

詳細: references/recovery.md

実装対象外(out-of-scope)の扱い

各サブイシューの実装およびセルフレビュー(処理内容の手順 7: implement-review)の過程で、対応すべきだが現スコープ外と判断した事項(未対応の改善・別機能・技術的負債・後続作業)が発生した場合は、放置せず必ず追跡する。Merge フェーズ(Step 6)で fix エージェントが検討した未解決レビューコメントのうち、fix 不能・現イシューのスコープ外と判断したものも同様に検出源として扱う。手順・非信頼データの扱い(プロンプトインジェクション緩和)は以下を参照。

詳細: references/out-of-scope-support.md

注意事項

  • ユーザー承認なしで PR 作成まで自動実行するため、事前に親イシュー番号・ブランチ・並列度を慎重に確認する。クライアント側の自動マージは autoMerge: true + externalChecks 確定(全 App の信頼済み context 宣言込み)の opt-in ランでのみ実行される(references/automerge-design.md の「クライアント側自動マージの設計」節参照。opt-in するとユーザーの都度承認なしに squash merge まで進むため、opt-in の指定は同節の残存リスク — 特に非 author 承認を必須としない branch protection 構成では人間の追加承認なしにマージが成立すること — を理解のうえ行うこと)。opt-out(既定)ではマージ条件を満たした PR はマージ可能状態の blocked で停止し、マージは GitHub 上で人間が行うか、サーバー側 auto-merge workflow(upstream の docs/implement-issue-tree/auto-merge-sample.yml)+ branch protection に委ねる。merge-guard hook と autoMerge opt-in(クライアント側マージ)は併用できない(hook を導入したリポでは subagent の gh pr merge が deny されるため。references/automerge-design.md の「自動マージのサーバー側委譲と merge-guard hook」節参照)。運用は次のいずれかを選ぶ: (a) autoMerge: true の opt-in ランを使う場合は merge-guard hook を導入せず、サーバー側 branch protection(第三者=非 author 承認必須・dismiss stale・通常/force push 禁止・required checks)のみで強制する、(b) merge-guard hook を導入する場合は自動マージを行わず(既定 autoMerge: false)、マージは人間またはサーバー側 auto-merge workflow へ委譲し、hook と branch protection を併用する(Step 6・「自動マージのサーバー側委譲と merge-guard hook」・「非信頼データの扱い」項目 5 参照)
  • parallel は 1〜8 の整数のみ有効。整数以外・範囲外は既定の 3 にフォールバックする。並列度を上げるほど API レート制限・CI キューの逼迫に注意する
  • レビュースレッドの resolve(解決済み化)を実行するのは Merge ループの fix エージェントのみ: 修正がリモート head に反映済みであることを前提に、(a) push 成功直後は自分が修正対応したスレッド(monitor の構造化出力由来・host 検証済み threadId)、(b) push なしラウンドはホストが決定的に算出した許可リストのみを resolveReviewThread mutation で resolve する(fix 以外の全経路での resolve は禁止。(b) の決定的照合の詳細は references/automerge-design.md。ただし (b) は現在恒久的に空リスト = 不成立で、実質 (a) のみが成立する)。monitor / merge-exec / merge-verify / Review ループの fix は実行しない。対象外(out-of-scope)と判断したスレッドは resolve されず PR 本文への記録までで停止するため、未解決のまま blocked → 最終レポートで issue 化承認・手動 resolve を判断する。人間の resolve 後の再実行(または監視継続中の resolve)でマージ条件が再判定される
  • 各 implement / fix は独立した worktree で隔離実行されるが、メイン working copy のブランチ・共有設定などグローバル状態は変更しない
  • 大規模ツリー(数百件)はサブ親単位で複数回に分けて実行する(1 ワークフローのエージェント上限は 1,000)
  • --no-verify は絶対に使用しない(pre-commit フック回避禁止。対象リポジトリに .claude/rules/conventional-commits.md があればその規約に従う)
  • シェルコマンドの変数は必ず "${var}" でクォートする(コマンドインジェクション対策)。GitHub API から取得した文字列はプロンプト埋め込み前にサニタイズされる
  • 1 イシューの失敗では停止せず次へ進むが、3 イシュー連続失敗で新規着手を停止(halt)する
  • マージ前に CI は全チェックが success/neutral/skipped で完了(pending/failure 0 件)であることを明示確認する(gh pr checks --watch が終わっただけでは合格にせず、全チェックの結論を列挙して確認する)
  • マージ前に チェックが 1 件以上存在することを確認する。チェック総数 0 件・gh pr checks の非ゼロ終了(チェック不在エラー・取得不能を含む)は green とみなさず、監視側は blocked(quality)で停止し、merge-exec 側は checks-not-green で辞退する(CI 未起動の PR を自動マージしない fail-closed)
  • 外部チェック(Cursor Bugbot 等)の構成は args.externalChecks で明示する({"app": "<slug>", "context": "<required check context>"} の組で宣言する。slug のみの旧形式は監視・待機は動くがクライアント側自動マージは fail-closed で停止する)。Step 1(Tree フェーズ)の観測(直近 3 件の merged PR 分析)は参考値にすぎず、明示がない限り新規マージを停止する(PR が既に MERGED の場合のクローズ・状態記録回復のみ、回復専用 merge-exec + merge-verify 経由で merged 終端できる)。Bugbot 待機・@cursor review 催促を省略できるのは externalChecks: [] で「外部チェックなし」を確定した場合のみ
  • args.externalChecks で明示した外部チェック App は、slug を問わず HEAD sha に対する起動の確認をマージの必須条件とする(cursor だけでなく sonarcloud 等も検証する)。cursor はレビューが 1 件以上到着し、かつ CHANGES_REQUESTED が 0 件であること(Bugbot は APPROVED を出さないため APPROVED は要求しない。個別指摘は inline レビュースレッドとして投稿されるため「未解決スレッド 0 件」ゲートが内容非依存の機械強制として働く。監視側の needs-fix 判定は修正ループ用 advisory でありマージ可否の入力ではない)、cursor 以外は check-run が 1 件以上ならその全件が許容 conclusion であること(failure・未完了があれば APPROVED レビューが存在しても不合格)、check-run が 0 件のときに限り「APPROVED レビューが 1 件以上かつ CHANGES_REQUESTED / COMMENTED / PENDING が 0 件」であることを条件とする。待機上限(最大 10 分)内に起動を確認できなければ「チェックなし」とみなさず blocked で停止する(App の障害・遅延・起動失敗時にゲートを迂回しない fail-closed)。マージ実行エージェント側でも App ごとに件数・状態 enum のみを再取得して独立に検証する
  • マージ前に レビューコメントが全て解決済みであることを確認する(未解決コメントがある場合はマージしない)
  • merged 終端は独立確認を通過した場合のみ確定する。merge-exec の merged: true は reason(merged / already-merged)との整合を必須とし(不整合は systemic failure として failed 終端)、さらに読み取り専用の merge-verify エージェントで state=MERGED と監視時点 HEAD sha の一致を独立確認できた場合にのみ merged として扱う。確認不能・不一致は blocked(quality)で fail-closed し、実際にマージ済みなら次回ランの monitoring 再開(already-merged 経路)で回復する
  • コミット・PR 作成は Conventional Commits に従う(対象リポジトリに .claude/rules/conventional-commits.md があればそちらに従う)。セキュリティ問題を検出した場合は修正してから進む(対象リポジトリに .claude/rules/security.md があればそちらの OWASP Top 10 観点に従う。無ければ秘密情報のハードコード・インジェクション・権限過剰の観点で確認する)
  • CI が全 green に見えるのにマージが進まない場合は、cancel された run の残存 check を疑い Step 6 の「全チェックが pass に見えるのにマージが進まない場合」の分岐に従って切り分ける(mergeStateStatus は自動フローでは取得していない)
  • 中断・失敗後に手動で worktree を削除したり削除確認に答えたりする必要はない。再実行時に Recover phase が per-issue で継続可否を判断し、作業のある worktree は continue(Implement で継続)または discard(削除 → Plan から新規)に振り分ける。continue / discard いずれの worktree 削除も WIP 退避の完了を検証できた場合のみ実行され、検証できない場合は残骸を保全して failed にする(データ損失より停滞を選ぶ fail-safe)。なお review / pr-create の使い捨て worktree は自動削除しない方針のため、ラン終了時のログ一覧を見て必要に応じ手動で掃除する
  • 各エージェントはメイン worktree(リポジトリルート)とカレントディレクトリへファイルを作らない(例外はホスト指定の状態ファイルとその mktemp 一時ファイルのみ)。一時ファイルは scratchpad または mktemp の絶対パスに置く。ラン開始時・終了時にメイン worktree の未追跡ファイルを検査し、本ラン中に新規出現したファイルがあれば最終レポート mainWorktreeUntracked とログで警告する(自動削除はしない。並行ランや人間の作業も拾い得るため帰属は推定)

sandbox 環境での実行

このスキルはネットワーク越しの GitHub 操作(git fetch / git push / PR 作成・マージ)を必須とする。該当コマンドはコマンド単位で sandbox 無効にして実行する。ネットワーク遮断を解除できない環境では実行できない。