@fandhe-ai/implement-issue-tree
GitHub Issue を読み込み、`_/local-plans/<issue-number>-<slug>.md` に詳細計画を作成して**ユーザー承認後**にコードを実装する。実装後はセキュリティレビュー (OWASP Top 10) → テスト実行 → Conventional Commits でコミット。Issue 番号や URL を渡された実装依頼、「Issue #N を実装して」「この Issue を着手」などで使用。
| name | implement-issue-tree |
| description | > |
| model | sonnet |
| user-invocable | true |
| argument-hint | <親イシュー番号> [マージ先ブランチ(省略時 main)] [並列度(省略時 3)] |
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 件も発行されない(Issue #435)ため、push 前ゲートで解消を試みてから push する(解消不能なら push 自体を止める)。
末端の実装イシューは post-order DFS の順序を優先度として空きスロットへ貪欲投入し、最大 parallel(既定 3)件まで並列実行する。各 implement / fix は独立した git worktree で隔離実行されるため、並列でもブランチ・working copy が衝突しない。機能的依存(dependsOn)と親子関係(親は全子の完了を待つ verify-close)だけが待機条件となる。
前提条件
ghCLI がインストールされ、認証済みであること(gh auth statusで確認)jqCLI がインストールされていること(command -v jqで確認)。「全チェックが pass に見えるのにマージが進まない場合(cancel された run の残存 check)」節の人間の診断専用コマンド (B) はgh api --paginate --slurpの生 JSON を外部のjqへパイプして平坦化・集約するため、gh --jqだけでは代替できない。未導入の場合はそのコマンドを実行せず(rerun もせず)blockedとして扱うawkCLI がインストールされていること(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)>",
"externalChecks": "<外部チェック App と信頼済み required check context の組の配列(例: [{\"app\": \"cursor\", \"context\": \"Cursor Bugbot\"}]。使用しない場合は []。slug 文字列のみの旧形式も受理するが、context 未宣言のためクライアント側自動マージは fail-closed で停止する)>",
"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 を指定すると実質的に直列実行になる |
externalChecks |
任意 | 未指定 | GitHub Actions 以外の外部チェック宣言の配列(最大 10 件)。要素は {"app": "<slug>", "context": "<required check context>"} の組で宣言する(slug は英小文字・数字・ハイフン。複数 context は contexts 配列。slug 文字列のみの旧形式も受理するが context 未宣言としてクライアント側自動マージは 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 も数える。使用中かどうかはディスク消費を変えないため。PR #185 codex P1 第 5 ラウンド)がこの値を超過(>)していたら、ディスク枯渇を防ぐため新規イシューの着手を停止する(fail-closed。既に走行中のイシュー・monitoring の継続は停止しない)。dispatch ループは新規着手の直前に毎回「開始時観測 + 本ラン積み増し(ephemeralWorktrees.length。implement / review / pr-create / fix-routing-error の新規作成台帳)」を再評価し、本ランの積み増しで上限を超えた時点でも以降の新規着手を停止する(PR #185 codex P1。バイト軸にも同種の途中経過再評価があるが、算出方法が異なるため後述)。さらに並列投入済みでまだ記録に到達していないタスク分を見込み、新規着手 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 で対象外。PR #185 Bugbot Medium と同じ線引き)であり、開始前に同じ projected 判定を適用して超過が見込まれる場合は当該イシューの再開をこの周回に限り defer する(恒久停止はしない。次周回・次回実行で予約解放後に再評価。pet-hub PR #1062 codex-review P1 対応。修正前は monitoring 再開自身の開始を無条件で許可しており、monitoring 項目を順次再開し続けると上限を無視して残置数を際限なく増やせた)。予約起因の超過見込みは今周回の投入見送り(defer)に留め、予約が解放されれば再開する。実測超過は従来どおり恒久停止する(PR #185 codex P1 第 2 ラウンド。ただしこの恒久停止=newStartSuppressed は monitoring 再開の開始自体は妨げない設計を維持しており、上記の monitoring 再開専用 defer とは独立したゲート)。ラン開始時の横断スキャン自体が失敗した場合も、いずれかの軸が有効(`maxResidualWorktrees > 0 |
maxResidualWorktreeBytes |
任意 | 53687091200(50 GiB) |
残置 worktree ディスク使用量の上限(DoS 防止ゲートのバイト軸。バイト単位。件数軸 maxResidualWorktrees と独立に検証・無効化でき、判定は OR)。ラン開始時のみの観測ではない(旧記載の訂正。codex-review 指摘・PR #390 第 2 ラウンド: 実装は当初からラン中の再評価を持っていたが本節がそれを反映していなかった)。ラン開始時に、残置パス一覧全件へ du -sk を実行して KiB を単純合計する観測に加え、共有 .git object store を除いたメイン worktree の working tree 相当サイズ(measureMainWorktreeContentBytes)を新規 1 worktree あたりの安全側予約 perWorktreeByteReserve として確定する(PR #390 codex-review P1・Cursor Bugbot High: 素の du 値は object store 全量を含み過大予約になるため除外する)。件数軸の「本ラン積み増し再評価」「予約計上」と同じ形で、perWorktreeByteReserve × (台帳件数 − 直近基準確定時点の台帳件数) の projection をラン中の新規着手・monitoring 再開の両方の直前に毎回再評価し、超過見込みで新規着手を止める(projectResidualBytes。基準確定時点までの積み増しは実測基準値に既に含まれているため、そこを差し引かないと二重計上になる。K8Dc 対応・PR #390)。さらに perWorktreeByteReserve は開始時に確定する下限 floor 値であり、ビルド成果物等で 1 worktree が floor を超えて成長した場合 projection だけでは過小評価し得るため、使い捨て worktree 台帳が BYTE_REMEASURE_LEDGER_INTERVAL(3 件)積み増されるごとに残置パス一覧+台帳パスの合計を実際に du し直し(remeasureResidualBytesIfDue)、実測が上限を超えていれば独立に新規着手を止める。加えて、新規着手(implement)の直前には台帳増分ゲートを介さず必ず実測し直す(remeasureResidualBytesNow。台帳が 3 件増えない間も実行中 worktree はビルド成果物等で成長し得るため、台帳増分だけを契機にすると容量超過後の着手を止められない fail-open が残る — PR #390 codex-review P1 第 4 ラウンド。測定コストは「同一 dispatch 周回内は 1 回」の間引きで有界化する)。この実測し直しは以後の projection の基準(residualBytesAtStart・台帳オフセット byteBaselineLedgerCount)を同一代入で更新する(実測結果を上限超過の即時判定にのみ使って破棄すると、以後の判定が古い基準のまま容量超過の新規着手を許す fail-open になる。K8Dc 対応)。合計(直近の実測基準・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 照合による帰属特定ができず、フォールバックは測定専用(削除経路へは流さない)で物理一覧を丸ごと差し替える。Issue #404)。ラン開始時観測が失敗したまま(residualBytesObserved === false)の場合は、monitoring 再開の projected 判定(前述の item.kind === 'implement' 限定の projection)自体も件数軸と同じ fail-closed 方針で defer し、観測が回復するまでそのランの monitoring 再開全体を待機させる(観測不能のまま fix-routing-error worktree の新規作成を許すと容量を確認できないまま超過し得るため)。0 は「このバイト軸のみ上限なし(チェック無効)」の明示オプトアウト(件数軸の fail-closed には影響しない。件数軸の既定値フォールバックについては maxResidualWorktrees 行を参照)。負値・非整数はエラーで停止。既定 50 GiB は Issue #348 の検討案 B(実バイト数上限)を採用した後、Issue #467 でビルド成果物の大きいリポジトリでの誤停止(vector-db #629 実測)を踏まえ 2 GiB から引き上げたもので、配布先リポジトリのファイル量に依存しない絶対閾値として件数軸既定値 100 の妥当性を補強する。残置サイズの合計上限とは独立に、実ディスクの空き容量そのものも新規着手・monitoring 再開の直前に毎回検証し直す(measureFreeDiskKib / remeasureFreeDiskNow。Issue #467 P0 codex-review 対応・PR #468 で P0/High の再指摘に追加対応: 残置合計サイズが上限(既定 50 GiB)未満でも、実ディスクの空き容量自体はそれよりずっと小さいことがあり得るため〔例: 残置 8 GiB・実空き 4 GiB の導入先では、残置サイズだけを見るゲートは 50 GiB に達するまで新規着手を止めない〕、df -Pk でメイン worktree が属するファイルシステムの実空き容量を測定する。ラン開始時 1 回だけの測定では以後の消費を反映できないため(codex-review P0)、バイト軸の remeasureResidualBytesNow と同じ「新規着手・monitoring 再開の直前に必ず実測し直す(間引きは同一 dispatch 周回内 1 回)」設計を踏襲する。判定に使う必要バイト数は単一 worktree 分の予約とだけ比較しない(codex-review P0: 実行中タスクの未消費予約・投入済み候補自身の予約を合算していないと過小評価になる)。projectFreeDiskReserveBytes が「実行中タスクの残余予約(reservedUnits。件数軸・バイト軸 (b) と同じ「最大増分 − 記録済み数」の計算をそのまま再利用)+着手候補自身の最大増分(extraReserveUnits)」の合計に 1 worktree あたりの生の容量見積り(rawPerWorktreeByteReserve) を掛けて必要バイト数を算出し、実測空き容量がこれを下回れば抑止する。バイト軸の perWorktreeByteReserve(clampPerWorktreeByteReserve で容量上限に対する予算配分としてクランプ済みの値)はここでは使わない — クランプ後の値は実際の 1 worktree サイズより小さくなり得るため、実ディスクの物理的な枯渇判定に使うと危険側を見逃す(Cursor Bugbot High 指摘)。件数軸・バイト軸と同じ OR 評価=いずれか一方でも危険側なら抑止する。測定不能時も 0 で補わず、また古い実測値をそのまま流用せず観測失敗として新規着手を停止する(fail-closed)。大容量環境や parallel を大きく設定する環境では、実行中タスクの投入済み予約(reservedUnits)の合算により必要バイト数が大きくなる(例: raw ~8 GiB × 予約合計 9 件 = 72 GiB)。ラン開始時(reservedUnits: 0)の停止は着手候補自身の予約のみで必要量が決まり、args.parallel を下げても maxResidualWorktreeBytes を変更してもこの必要量は減らない(maxResidualWorktreeBytes は残置サイズ合計の上限であり本ゲートとは独立)ため、実ディスクの空き容量を確保する(メイン worktree の gitignored なビルド成果物・依存関係の削除を含む)ことでのみ解消できる(codex-review 指摘・Issue #467)。ラン中(reservedUnits > 0)の停止は投入済み予約の合算が要因になり得るため、空き容量確保に加え args.parallel を下げることも有効) |
maxBaseMerges |
任意 | 3 |
args.repo が未指定のランでは値によらず base 取り込みは起動しない(args.repo の形式不正は起動時にエラーで停止しラン自体が始まらない。repo 行参照)。PR が base とコンフリクト(mergeable: CONFLICTING。monitor の conflicting 経路、または merge-exec の not-mergeable 写像のいずれか)した際に、fix 予算(fixCount)を消費せず自動起動する base 取り込み専用エージェント(baseMergePrompt)の回数上限(0〜10 の整数。Issue #441)。monitorsLeft と独立の第 2 の停止性ガードとして機能する。上限到達時は blocked / blockedReason: "quality" で終端する(halt 非カウント。#141 の needs-fix と異なり、上限到達後も human が PR ブランチへ直接 base を取り込んで push すればコンフリクトが解消し、次回 monitor は conflicting を返さずこの分岐自体を再度通らないため unrecoverable は使わない。blocked 終端後の再実行は monitoring として直接再開する — Recover / PR Create フェーズは通らない。baseMergeCount は上限到達値のまま引き継がれるため、上限到達後の解消には人間が PR ブランチへ base を直接取り込んで push する必要がある)。0 は自動 base 取り込みを無効化し、コンフリクト検出時点で即 blocked にする明示オプトアウト(この場合も blockedReason: "quality"。Issue #441 codex-review P1・PR #443)。負値・非整数・小数はエラーで停止(マージゲート入力と同じ厳格さ) |
repo |
任意(maxBaseMerges > 0 で base 取り込みを使うなら実質必須) |
未指定("") |
対象リポジトリの owner/repo(例: "Fandhe-AI/agent-cli-skills")。baseMergePrompt の worktree routing ガードが期待する owner/repo(expectedRepo)として使う、人間が明示するホスト側入力。エージェントの自己申告値(外部チェック観測エージェント等)は一切使わない(PR #443 codex P0: 自己申告値を信頼境界に使うと誤配置 worktree からの別リポジトリへの push を防げない)。未指定は expectedRepo を空文字のまま確定し、base 取り込み(baseMergePrompt の起動)を行わず conflicting は即 blocked / blockedReason: "quality" で終端する(fail-closed。実装・push 前 Review・PR 作成・CI 監視は値によらず自動で進む)。形式不正(isValidRepoSlug の owner/repo 形式を通らない値)は起動時にエラーで停止(マージゲート入力と同じ厳格さ。誤記を黙って未指定へ読み替えるとガードが静かに弱まるため) |
externalChecks の 4 状態(Issue #147 → 下流 sync PR codex P0 で context 束縛へ拡張):
| 指定 | 意味 | マージ挙動 |
|---|---|---|
| 未指定 | 外部チェック構成が未確定 | 観測結果にかかわらず自動マージを停止し blocked で終端する(実装・PR 作成・CI までは進む) |
[] |
「外部チェックを使用しない」と人間が確定 | 外部レビュー待機をスキップして CI green と未解決スレッドなしのみで判定する |
[{"app": "cursor", "context": "Cursor Bugbot"}] 等 |
指定 App + 信頼済み required check context を正とする(観測結果より優先) | 指定した全 App について HEAD sha に対する起動を検証する。cursor は「レビューが 1 件以上到着し、かつ CHANGES_REQUESTED が 0 件であること」(Issue #146。個別指摘はレビュースレッドとして残るため「未解決スレッド 0 件」ゲートが内容非依存に遮断する。監視側の内容評価は修正ループ用 advisory でありマージ可否の入力ではない)、それ以外の App は check-run が 1 件以上ならその全件が許容 conclusion であること、check-run が 0 件のときに限りフォールバックとして「APPROVED レビューが 1 件以上かつ否定的レビュー 0 件」であることをマージ条件とする(Issue #155)。opt-in マージでは G0 が宣言 context + App ID の組で required 化を照合する |
["cursor"] 等(slug のみの旧形式) |
App は確定するが信頼済み context が未宣言 | 監視・外部レビュー待機は上と同じ。ただしクライアント側自動マージは fail-closed で停止する(autoMerge: true でもマージせず blocked 終端。App ID だけの照合では同一 App の無関係な context の required 化でも G0 を通過してしまうため — 下流 sync PR codex P0 変種 1) |
観測ベースの検出は直近 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 はマージゲートの入力であり、誤記を黙って「未指定」や「なし確定」に読み替えるとゲートが静かに弱まるため)。
自動マージのサーバー側委譲と 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 ランのクライアント側マージは、PR #182 / PR #222 codex P0(未信頼のレビュー本文を読む 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)を抽出する。
ツリー取得に続いて、直前 3 件の merged PR の check-runs から GitHub Actions 以外の外部チェック App(例: Cursor Bugbot)を観測する。観測結果は参考値であり構成の確定情報ではない。構成の確定は args.externalChecks の明示入力で行い、明示がない限り「確定不能」として後続の Merge ステップで自動マージを停止する(Issue #147)。
# 親イシューのサブイシューを取得(--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には「機能的に先行完了が必須」のイシュー番号のみを入れる(本文の明示的な依存記述・前提実装に限る。単なる関連やコンフリクトの可能性だけなら含めない)- 祖先イシューへの
dependsOnは無視する(親は子の完了を待つ側のため) - 依存グラフに循環がある場合は DFS で検出し、循環を構成する非ツリー辺(
dependsOn)を除去してデッドロックを防ぐ - 依存ブロックは各周回で再判定する: 前提イシューの失敗・ブロックで下流が着手不能でも即座に確定せず保留し、halt(3 イシュー連続で完了できなかった場合の新規着手停止。Step 8 参照)発生前に限り、前提がラン中に外部完了(Issue CLOSED / PR MERGED)した場合は同一ラン内で下流を再判定して着手する。halt 後はプローブと状態記録(
prereqTransitions・state 永続化)のみ継続し、新規着手は再開しない(halt はユーザー判断を待つ防御であり自動解除しない)。halt 後に記録された外部完了は次回ランの再実行で下流着手に反映される(Issue #442)
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 条件を両方満たす場合にのみ実行する。
- 申告ゲート: Recover エージェントが
wipCommitted: trueを返している(退避した場合、および退避すべき未 commit 変更が最初から無かった場合に true。フック失敗等で退避できなかった場合は false) - 事実ゲート: ホストが起動する読み取り専用の安全確認エージェントが、対象 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 で新規ブランチを作成する
隔離 worktree で
git statusが clean か確認し、差分があれば作業せず失敗を返す(0b-a で既存 open PR のブランチを取得した場合・0b-b でリモートブランチを再利用した場合はスキップ)指定ブランチ(デフォルト:
main)から作業ブランチを作成する(並列時のブランチ名衝突を防ぐためブランチ名にイシュー番号を含める)渡された計画に従って実装する(計画立案は Plan フェーズで完了済み)。実装は対象リポジトリの delegation ルール・専門サブエージェントがあればそれに従い役割単位で委譲する
コメント方針(実装時):
- コードコメントは「何をするか」より「なぜ存在するか/パッケージ・サービスから見た対象の役割」を書く
- 後続の読み手(Claude を含む)は渡された情報からしか判断できないため、他ファイル・他サービス・呼び出し元/呼び出し先からの観点を明示する(このシンボルがどこから呼ばれ、どの境界を担うか)
- 詳細は対象リポジトリの
.claude/rules/code-comment-style.md(init-claudeが配備)に従う
対象リポジトリの CLAUDE.md・rules・テスト実行規約に従いビルド・lint・テストを通す。テストが失敗した場合は根本原因を調査してから修正する(
.claude/rules/debugging.mdの4フェーズを順に踏む。同一箇所で3回失敗したらアーキテクチャ問題と判断し、該当イシューをblockedとして記録してユーザーに状況を報告する)実装後に OWASP Top 10 観点でセキュリティチェックを実施する(API キーのハードコード・インジェクション等)。問題が見つかった場合は修正してから次へ進む
実装が完了したら
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)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で落とす(Issue #361) 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・最終指摘をレポートへ集約し(Issue #442。references/report-format.md 参照)、再実行時は Recover(継続/破棄)→ 通常 Implement 経路から再着手する(pr: 0 のため monitoring 再開ではない。詳細は references/recovery.md)。
依存ブロックの再判定について: 下流イシューが前提イシューの blocked(本節の Review 非収束等)で連鎖ブロックされても、スケジューラは各周回で保留状態を維持し、halt 発生前に限り、前提がラン中に外部完了(人手マージ・クローズ)した場合は同一ラン内で下流を再判定する。halt 後の外部完了検知はプローブ・状態記録のみで新規着手には反映されず、次回ランで反映される(Issue #442。詳細は Step 8 参照)。
Review / Merge の fix は fixCount(上限 6)を共有する。base とのコンフリクト(mergeable: CONFLICTING)解消は fixCount を消費せず、独立予算の baseMergeCount(上限 maxBaseMerges。既定 3)で管理される(Issue #441)。
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
)"
既存 open PR の再利用(Issue #135): 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 ランでのみ実行する(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 は起動タイミングのみ。PR #222 codex P0 対応)。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 を含まない回復専用経路に固定される(既存 Issue #168 機構。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 偽造 P0)。hook 専用の秘密注入経路がなく、hook が検証でき subagent が読めない鍵を持てないため署名 / MAC も実装不能で、偽造不能なマージ認可を hook で実装することは原理的に不可能。その後 PR #206 のクライアント側 arm(agent precheck + hook carve-out)も、carve-out が認可と結び付かず任意 subagent に arm を開放し(codex P0)、precheck が agent 自己申告で捏造可能(codex P0)、--auto の即時マージで「予約のみ」前提が虚偽(Bugbot High)と確認され撤回した。この時点では Codex 元指摘(rust-ai-library PR #441)の「境界を実装できるまで自動マージ無効化」に従い、grant / canary / branch-protection ランタイムゲートと precheck / arm / carve-out をすべて撤去し、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 / 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)へ委譲する。
監視とマージ実行の分離(Issue #145)・merged 自己申告の独立確認(Issue #160): このステップは監視・マージ実行・独立確認の 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 から受け取らない。PR #222 codex P0 対応)、マージはgh pr merge <N> --squash --delete-branch --match-head-commit <自己取得 sha>で実行して、照合とマージの間に push される競合(TOCTOU)を GitHub 側の条件評価で塞ぐ。- チェック名を除外するのは、名称が PR 側の workflow / job / matrix 定義から生成される外部由来テキストであり、マージ権限を持つ実行主体のコンテキストへ命令文を持ち込む経路になるため(PR #150 codex-review P0 対応)。外部チェックの件数(非負整数)と
.conclusion/.statusの状態 enum は任意テキストを注入できる媒体ではないため、この理由づけの対象外として--jq正規化つきの取得のみを許可する(Issue #146 / #155)。App の絞り込みは、args入力時に slug 形式(英小文字・数字・ハイフン、39 文字以内)へ検証済みの値とのapp.slug一致で行い、App 名・description・output はコンテキストへ入れない。 - 外部チェックの再検証を監視エージェント側だけに置かないのは、未信頼テキストを読む監視の判定は「マージを試みてよい」という起動条件にすぎず、ゲートの証拠にできないため。
- 監視の
headShaはマージ経路の入力に使われない(診断用の記録のみ。PR #222 codex P0 対応。マージ判定用の HEAD sha は merge-exec が自己取得・固定する)。opt-out・外部チェック構成が未確定のラン(externalChecks未指定)・信頼済み context 未宣言のラン(slug のみの旧形式)では merge-exec は回復専用経路(allowMerge=false)で起動し、新規マージは行わず「PR が既に MERGED ならイシュークローズ確認のみ」に限定される(前回ランでマージ済み・状態記録に失敗した PR の回復パスを保ちつつ fail-closed を維持する)。この限定はプロンプト解釈に任せず、マージ実行手順の文面もホスト側で分岐し、回復専用経路のプロンプトにはgh pr merge/--match-head-commitを一切含めずイシュークローズ確認のみを出力する(Issue #161 / #168)。 - マージ成功でもイシューのクローズを確認できない場合(
issueClosed: false)はmergedとして終端せず再監視でクローズを再試行し、監視回数を使い切った場合は「PR はマージ済みだがクローズ未確認」としてblockedで終端する(次回実行の monitoring 再開で回復する)。
- チェック名を除外するのは、名称が PR 側の workflow / job / matrix 定義から生成される外部由来テキストであり、マージ権限を持つ実行主体のコンテキストへ命令文を持ち込む経路になるため(PR #150 codex-review P0 対応)。外部チェックの件数(非負整数)と
- 独立確認エージェント(merge-verify、Issue #160): 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 辞退を含む。Issue #159) /merge-failed→ 再監視、unresolved-threads→ fix ループ(ただし手元にスレッド内容の構造化一覧がない場合は fix を起動せず再監視し、監視エージェントに内容を収集させる)、not-mergeable(コンフリクト等) →conflicting(base 取り込み専用エージェントへ回す。fixCountを消費しない独立予算baseMergeCount。Issue #441)、wrong-target(base 不一致・draft。fix では解消しないため fix 予算を消費しない) → blocked、pr-closed→ blocked、external-review-missing→ blocked(Issue #146 / #155。同一ラン内で再監視しても到着を保証できないため 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、へマッピングされる。
- 辞退理由(
マージ実行条件:
- CI 全 green: 全チェックが success / neutral / skipped で完了し、failure / cancelled / timed_out が 0 件かつ pending / queued / in_progress が 0 件であること。pending が残るなら監視を継続する。かつチェック総数が 1 件以上存在すること。0 件は green とみなさず、監視側は最大 10 分の再確認後に
blocked(quality)で停止する(Issue #159。workflow のon:条件・パスフィルタによる全 job スキップや required workflow 未配置で CI が一度も起動していない PR を自動マージしない fail-closed。merge-exec 側もチェック総数 0 件・gh pr checksの非ゼロ終了をchecks-not-greenとして辞退する)。例外(Issue #435 / #441): state がOPENかつmergeable: CONFLICTINGの PR はチェック総数 0 件であってもblockedへは進まず、そのままconflicting(base 取り込み専用エージェントへ回す。fixCountを消費しない独立予算baseMergeCount)へ回す — コンフリクト PR は test merge commit が作られないためpull_requestトリガーの CI check-run が構造的に起動せず、待っても収束しないため。mergeable: UNKNOWNは算出待ちとして扱い、CONFLICTINGとは扱わない(誤検出による base 取り込み予算の空費を防ぐ)。 - 外部チェック指摘なし(または「外部チェックなし」が
args.externalChecks: []で確定していること):args.externalChecksと Step 1 の観測結果に基づき後述の待機手順を実施する。構成が確定できない場合・確定済みの外部チェック App について HEAD sha に対する合格の根拠(許容 conclusion の check-run、または APPROVED レビュー)を確認できない場合はマージしない(Issue #155。「指定した App のチェックが緑」ではなく「指定した App のチェックが存在しかつ緑」を条件とする)。 - 未解決レビューコメントなし: GraphQL API で全スレッドが resolved 済みであること。resolve を実行してよいのは Merge ループの fix エージェント(push する版)のみ: 自分の修正がリモート head に反映済みであることを前提に、(a) 当該ラウンドの push 成功直後は自分が修正対応したスレッド(monitor の構造化出力由来・host 検証済みの threadId に限る。fix が自分でスレッド一覧を再取得して対象を広げることは禁止)を、(b) push なしラウンド(過去ラウンドで修正・push 済み)はホストが決定的に算出した許可リストのみを対象に、GraphQL
resolveReviewThreadmutation で resolve し、required_review_thread_resolutionゲートを人手なしで解消する(Issue #119 の「全経路 resolve 禁止」をオーナー判断で転換)。(b) の許可リストはホスト側の決定的照合のみで算出する(Issue #430): 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・#430 codex-review P0 再指摘・PR #433):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で停止する(Issue #147)。ホスト側にも同じゲートがあり、監視エージェントがreadyを返しても新規マージは行わずblockedで終端する(プロンプト + ホストの二重検証)。停止理由には観測結果(参考値)と再実行用のargs例が記録され、blocked+prは次回ランの monitoring 再開対象となる。ただし PR が既にMERGEDの場合(前回ランでマージ済み・状態記録に失敗した PR)のクローズ・状態記録の回復は、回復専用 merge-exec(allowMerge=false。プロンプトにgh pr mergeを含まない)+ merge-verify のstate=MERGED独立確認を経てmerged終端できる(Issue #168。新規マージ経路は開かず、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で停止する(Issue #146。App の障害・遅延・起動失敗時にレビューゲートを迂回させないための fail-closed。レビュー到着後に再実行すれば monitoring 再開で継続する)。 - cursor 以外の外部チェック(例: sonarcloud):
gh pr checks --watch(CI 監視)は「存在するチェックが緑になったか」しか保証せず、App がそもそも起動していなければ何も監視しないまま全 green と判定される。そのため App ごとに HEAD sha に対する check-run の起動そのものを確認する(Issue #155。従来はこの確認がなく、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 を除外して再実行する。
- cursor と他 App を併記した構成(例:
# 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):
Loading...
Select a file to preview
Analyzing security...
Checking scan reports and verification data.
Bill of Materials
Everything this skill can do — files, network, commands, and more.