implement-issue-tree
Automatically codes all sub-issues under a parent issue in parallel, reviews them, creates pull requests, and gets them ready to merge.
Installation
Paste this into Claude Code, Cursor, or any agent that can run commands.
What this skill does
What it does:
- Takes a parent Issue with many sub-Issues underneath it
- Automatically implements all the sub-Issues in parallel using separate work areas
- Reviews each change locally before pushing to GitHub
- Creates pull requests and watches the automated tests run
- Can automatically merge pull requests when tests pass and reviews are done
- Keeps track of which Issues depend on others so they run in the right order
When to use it:
- When you have a parent Issue with many child Issues that need to be built
- When you want multiple Issues implemented at the same time without conflicts
- When you want automatic testing and merging after each piece is done
- When you want to see how many Issues can start now versus how many are blocked waiting
SKILL.mdShow the author's original SKILL.md (not in English)
---
name: implement-issue-tree
description: >
親イシュー配下のサブイシュー(孫以下の全子孫を含む)を依存順を保ちつつ 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 優先度のみ)。
着手できる件数・blocked になる件数を事前に知りたい場合は dryRun: true(何も書かずに見積りだけ返す)。
単一イシューの実装は implement-issue、PR レビューは implement-review-pr を参照。
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 ブランチを取り込む(起点の確立・fetch・merge・push・push の反映確認・PR の検出と作成は `scripts/pr-push.sh` が決定的に行い、エージェントはコンフリクトの解消・commitlint に合わせた subject の決定・PR の文面だけを担う。Issue #603 第 4 弾)。並列ラン(`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 分。`scripts/observe-pr.sh --mode push` が、push した HEAD が PR の head になるのを待ってから読む。Issue #603 第 3 弾)で確認し、`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` で確認)。**monitor の観測スクリプト `scripts/observe-pr.sh` は全モードで外部 `jq` を使う**(封筒の payload の組み立て、`gh api --paginate` のページごとの出力の合算〔`jq -s`〕、`--arg` / `--argjson` / `@uri`。jq 1.5 以上が要り、1.6〔ubuntu 22.04〕・1.7〔debian・macOS〕・1.8〔alpine〕で実走を確かめた)。未導入だとスクリプトは exit 3 で封筒を出さず、監視は取得失敗として扱う。ほかに、「全チェックが pass に見えるのにマージが進まない場合(cancel された run の残存 check)」節の人間の診断専用コマンド (B) は `gh api --paginate --slurp` の生 JSON を外部の `jq` へパイプして平坦化・集約するため、`gh --jq` だけでは代替できない。未導入の場合はそのコマンドを実行せず(rerun もせず)`blocked` として扱う
- `gh` の機能の前提(`observe-pr.sh`): `gh pr view --json state,headRefOid,mergeable,headRefName,baseRefName,closingIssuesReferences,isCrossRepository`・`gh api --paginate`・`gh api graphql`。複数ページの合算は `gh api --paginate` の出力を `jq -s` で行い、`gh api --slurp`(新しめの gh の機能)には依存しない。**必要な gh の最低版は未確認**(実走確認は gh 2.102.0 の実 API のみ。公式のリリースノートで各フィールドの導入版は確かめていない)。時間制限のために bash のジョブ制御(`set -m`)と `mktemp`・`date +%s`・`sleep` を使う(macOS の bash 3.2 と Linux で動く)。base 取り込みの `scripts/base-merge.sh` は `gh pr view --json number,headRefName --jq …`(gh 内蔵の jq。外部の `jq` は要らない)と、`git remote get-url --push --all`(git 2.7 以上)・`git merge --no-ff --no-edit -F`(**git 2.20 以上**。実測では 2.20.4 にあり 2.18.4 以前に無く、2.19 系は未確認。無い git では base 取り込みが `commitFailed`〔`failed` 終端〕になり push はされない。詳細は references/script-trust-model.md)・`git merge-base --is-ancestor`・`git ls-remote` を使う。`observe-pr.sh --mode push` は push した HEAD を `git rev-parse HEAD` で読むため、pr-create・fix のエージェントの作業ディレクトリ(push した worktree)で実行する
- `awk` CLI がインストールされていること(`command -v awk` で確認)。同節のエージェント実行可能コマンド (A) は `--jq` がページ単位にしか適用できないため、ページ跨ぎの重複を集約する際にシェル側 `awk` へ依存する。未導入の場合はそのコマンドを実行せず `UNDETERMINED`(判定不能)として扱う
- git working tree が clean であること(`git status` で確認)
- 測定スクリプト(`scripts/measure-*.sh`・`scripts/run-verified.sh`)が使う `bash`・`du`・`df`・`awk` と、`sha256sum` または `shasum`(macOS の標準は `shasum`)。macOS(BSD userland・bash 3.2)と Linux(GNU)の両方で動く書き方に限っている。どれかが無いと測定が失敗し、残置バイト軸が有効なランは新規着手を止める(fail-closed)
- マージ先ブランチが 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`
**`args.skillDir`(配布後は呼び出し側が必ず渡す):** `scriptPath` と同じスキルディレクトリ(上の 3 レイアウトのいずれかの `implement-issue-tree/`。`scripts/` の 1 つ上)を**絶対パス**で `args.skillDir` に渡す(例: `/Users/me/repo/.claude/skills/implement-issue-tree`。相対の `scriptPath` を使う場合は `$(pwd)/.claude/skills/implement-issue-tree` のように起動時のメイン作業ディレクトリで絶対化する)。Workflow スクリプト自身はファイルシステムに触れないため、決定的な手順をスキル同梱のスクリプトへ切り出した部分(Issue #603。測定 `scripts/measure-*.sh`・PR の観測と待機 `scripts/observe-pr.sh`・base 取り込み `scripts/base-merge.sh`・PR 作成と fix の push `scripts/pr-push.sh`)の実行場所をここから決める。**ラン開始時(`dryRun` を含む。`skillDir` を渡したラン)に、埋め込みの全スクリプトのハッシュを実ファイルと 1 回照合し(エージェント 1 回。`run-verified.sh --verify-only`)、版ずれ・欠落は何も着手せずに停止する**(予検査。検出できるのは版ずれと転記ミスで、悪意あるエージェントの捏造ではない。各スクリプトの起動時の照合は別に毎回働く)。**named workflow(`{name: "implement-issue-tree"}`)で起動する場合も `args.skillDir` が要る**(named workflow は `.claude/workflows/implement-issue-tree.js` を指すだけで、スクリプトの置き場所を Workflow ランタイムは教えない)。サブエージェントは必ずこのメイン作業ディレクトリ側の `scripts/run-verified.sh` を `bash` で起動し(実行ビットに依存しない)、worktree 内のコピーは使わない。起動前に期待ハッシュ(ビルダーが生成物へ埋め込む)と照合し、不一致・欠落は起動せず fail-closed になる。信頼モデルは [references/script-trust-model.md](references/script-trust-model.md)。形式は `args.repo` と同じ厳格さで検証し、不正な値(相対パス・`..`・`//`・末尾 `/`・許可外の文字・400 文字超・文字列以外)は黙って読み替えず起動時エラーで停止する。**未指定のとき:** `dryRun` でないラン(= 実エージェントを起動するラン。monitor が毎回 `observe-pr.sh` を使うため、`maxResidualWorktreeBytes: 0` でも必要)は、エージェントを 1 回も呼ぶ前に起動時エラーで停止する。省略できるのは `dryRun: true` のランだけ。
```json
{
"scriptPath": "<このスキルディレクトリ>/scripts/implement-issue-tree.js",
"args": {
"parent": "<親イシュー番号>",
"skillDir": "<このスキルディレクトリ(scripts/ の 1 つ上)の絶対パス。切り出したスクリプト(測定・PR の観測)の実行場所。dryRun: true のとき以外は必須。形式不正はエラーで停止。Issue #603>",
"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 以外はエラーで停止>",
"dryRun": "<boolean。true で状態ファイル・worktree・PR・issue へ一切書かず、着手できる件数・blocked になる件数(ツリー外の前提待ち・phaseGate・下流の内訳)・ツリー外の open な前提 issue の一覧だけを返して終了する(事前見積り。Issue #585)。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)>",
"agentCallLimit": "<新規着手・monitoring 再開を止める判断に使う、ランタイム上限の想定値(81〜1000 の整数。`securityReview` 有効時は 83〜1000。省略時 1000=ランタイムの上限。呼び出しそのものは止めない。範囲外・非整数はエラーで停止。Issue #592)>",
"repo": "<対象リポジトリの owner/repo(例: \"Fandhe-AI/agent-cli-skills\")。base 取り込み(maxBaseMerges > 0)の worktree routing ガードが期待する owner/repo として使うホスト側明示宣言。未指定時は base 取り込みの自動起動が無効化され、コンフリクトは blocked+quality で終端する(PR #443 codex P0: エージェント自己申告値は信頼境界に使わない)>",
"models": "<判断を伴う agent のモデルを段階ごとに上書きするオブジェクト(任意)。キーは plan / implement / review / securityReview / fix / prCreate / baseMerge、値は haiku / sonnet / opus / fable。未指定の段階は既定(plan はセッションのモデルを継ぐ・それ以外は sonnet)。オブジェクト以外・未知のキー・許可外の値はエラーで停止>",
"securityReview": "<true または {\"enabled\": true} で push 前 Review の各ラウンドにセキュリティ専用の Review エージェントを並列に足し、両方の結果を合わせて判定する(opt-in)。未指定・false は現行動作。それ以外の形はエラーで停止>"
}
}
```
例: 親イシュー `#42` の配下を `main` へ、並列度 3・Cursor Bugbot 導入済みで実行する場合(マージ可能状態まで自動で進み、マージは GitHub 上で人間が行う):
```json
{
"scriptPath": ".claude/skills/implement-issue-tree/scripts/implement-issue-tree.js",
"args": { "parent": 42, "skillDir": "/Users/me/repo/.claude/skills/implement-issue-tree", "branch": "main", "parallel": 3, "externalChecks": [{"app": "cursor", "context": "Cursor Bugbot"}] }
}
```
例: 品質を優先して計画を `fable`、実装と fix を `opus` に上げ、セキュリティ専用の Review を `opus` で足す場合(通常の Review・PR 作成・機械的な agent は既定のまま):
```json
{
"scriptPath": ".claude/skills/implement-issue-tree/scripts/implement-issue-tree.js",
"args": { "parent": 42, "skillDir": "/Users/me/repo/.claude/skills/implement-issue-tree", "externalChecks": [], "models": { "plan": "fable", "implement": "opus", "fix": "opus", "securityReview": "opus" }, "securityReview": true }
}
```
### 引数
| 引数 | 必須 | 既定 | 説明 |
|------|------|------|------|
| `parent` | 必須 | — | 親(ルート)イシュー番号。`issue` でも可 |
| `skillDir` | 条件付き必須 | — | **このスキルディレクトリ(`scripts/` の 1 つ上)の絶対パス**(Issue #603)。決定的な手順を切り出したスクリプト(現在は残置 worktree のバイト測定 `measure-residual-bytes.sh`・実ディスク空き容量 `measure-free-disk.sh`・PR の観測と有界の待機 `observe-pr.sh`・自動 base 取り込みと push の検証 `base-merge.sh`・pr-create と fix の base 取り込み・push・PR 作成 `pr-push.sh`・状態ファイルの書き込み `state.sh`・merge-exec のマージ直前の検証 `merge-gate.sh`・worktree の破棄と掃除 `worktree-remove.sh`・Recover の機械手順 `recover-prep.sh`)をサブエージェントが実行する場所。ラン開始時にこの場所の全スクリプトのハッシュを 1 回予検査する(不一致・欠落は何も着手せずに停止。`dryRun` で省略したときは行わない)。`scriptPath` と同じスキルディレクトリを渡す。**必須になるのは `dryRun` でないラン**(monitor が `observe-pr.sh` を使うため、`maxResidualWorktreeBytes: 0` でも必要)で、未指定なら起動時エラーで停止する(エラー文に渡す値の例を示す。エージェントを 1 回も呼ばない)。`dryRun: true` なら省略できる。形式は絶対パス(英数字・`.`・`_`・`-`・`/`・半角スペース、先頭は `/` + 英数字または `_`)のみで、相対パス・`..`・`//`・末尾 `/`・400 文字超・文字列以外は起動時エラー。存在・ハッシュの確認はスクリプト側(実行時)が行い、欠落・不一致は測定失敗として新規着手を止める(fail-closed)。配布後は既存の呼び出し手順がこの引数を渡すよう更新が必要 |
| `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 以外はエラーで停止(誤記を黙って読み替えない) |
| `dryRun` | 任意 | `false` | **`true` で事前見積りだけを行い、何も書かずに終了する**(Issue #585)。ツリー取得・依存の解決・ツリー外の前提 issue の state 取得は通常どおり行うが、状態ファイルの作成・更新(`state:init-all` 含む)、worktree 作成、Plan 以降の実装系エージェントの起動、PR・issue への書き込みを一切行わない。返却は `dryRun: true`・`startable`(着手見込みの件数 `count`・今すぐ着手できる件数 `readyNow`・先頭 5 件 `first`。着手できたノードが全て成功する前提の見込み)・`blocked`(`count` と、`outOfTree`〔ツリー外の open な前提を直接待つ〕/ `phaseGate`〔前 Phase 未完了の合成辺で止まる〕/ `downstream`〔それ以外の下流・子の保留〕/ `optinTestsInvalid`〔前提は満たすが opt-in テスト宣言が承認一覧と一致せず、通常実行の `runImplement` 冒頭で blocked になる実装ノード〕/ `savedBranchMismatch`〔同じく状態ファイルの branch が本イシューの命名でないため blocked になる実装ノード〕それぞれの `count` と先頭 5 件 `first`。分類は前記の順で最初に当てはまる 1 つで、前提が失敗済みで実行されないノードは自身の条件を見ずに依存側へ分類する。自身が blocked になるノードは着手見込みに入れず、下流へ伝播させる)・`preBlocked`(状態ファイルの照合不成立等で最初から止まる番号)・`closedCleanupPending`(通常実行で行う「closed 後始末」の対象見込み。`count`・先頭 5 件 `first`・最大エージェント呼び出し数 `maxAgentCalls`。dryRun では実行しない)・`outOfTreeDeps`(open / unknown / closed / ancestors)。halt・残置 worktree 上限・CI の結果は実行時にしか分からないため見積りに含めない。「着手できるノードが 0 件で blocked が大半」と分かったら、通常ランの前にツリー外の前提 issue の close や `phaseGate` の見直しを検討する。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 件あたり最大で `EPHEMERAL_KIND_MAX` テーブルの合計(implement ×1 + review ×3 + pr-create ×1 + fix-routing-error ×1 = 6 件が基本。`args.securityReview` を有効にしたランは security-review ×3、base 取り込みは `maxBaseMerges` 件がさらに加わる)、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 > 0`)なら残置総数を確認できないとみなして新規着手を停止する(fail-closed。両軸とも `0` 指定時のみ観測失敗でも続行。観測失敗時(`residualObserved === false`)も monitoring 再開(`item.kind === 'implement'` の再開に限る)は fail-closed で defer する — 観測できない状態での worktree 積み増しを許す fail-open を避けるため、`monitoringResumeGateDeferred` へ理由を記録してこの周回の再開を見送る(恒久停止ではない。次周回・次回実行で再評価する))。スキャン一覧が非空でも、独立取得したレコード総数との件数照合に不一致(転記の一部脱落の疑い)があれば同様に観測失敗として停止する。使い捨て worktree は削除しない設計(references/recovery.md の「worktree の自動削除」節)のため、この上限超過時は `git worktree list` で確認し不要な worktree を `git worktree remove` で**手動削除**してから再実行する。`0` は「この件数軸のみ上限なし(チェック無効)」の明示オプトアウト(バイト軸の fail-closed には影響しない)。**負値・非整数はエラーで停止**(マージゲート入力と同じ厳格さ。誤記を黙って読み替えない)。既定値は 100(1 イシュー消化あたり 4〜6 件積み増されるため、20 では 1 ラン 3 件着手で頭打ちになる。linked worktree は object store を共有し working tree 分のみディスク消費のため 100 件でも過大ではないが、根拠は 1 リポジトリのみの実測〔≈ 3.4 MB/件〕であり、配布先ごとに追跡ファイル量が異なるため、リポジトリ非依存の絶対閾値として `maxResidualWorktreeBytes` を必ず併用する)。**ただし `maxResidualWorktreeBytes: 0` でバイト軸を明示オプトアウトし、かつ本引数を未指定のままにした場合はこの補強が働かないため、既定値を安全側の `LEGACY_DEFAULT_MAX_RESIDUAL_WORKTREES`(20)へ自動的に引き下げる**(`parseMaxResidualWorktrees` の `bytesAxisDisabled` 引数。件数軸だけを緩和した既定値を、リポジトリ非依存の絶対閾値という補強なしに残さない)。利用者が本引数へ明示的に値を指定した場合はこのフォールバックの対象外(指定値をそのまま使う) |
| `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 || exceeded || reserveStale` の OR とし `reserveStale` も defer を発火させる(当該周回のみの一過性の待機で、次回の予約更新が成功すれば再評価される)。**`df` 実測時点から新規着手・monitoring 再開の判定直前までの間に並行タスクが worktree を記録した分(台帳増分)も、`projectFreeDiskReserveBytes` の未測定予約として加算する**(df の実測値と台帳増分は独立に古びるため、実測直後に確定した空き容量をそのまま使い回すと、その間の消費が予約にも実測空き容量にも反映されない) 測定の `count` が対象件数と一致しない結果(転記失敗)・agent 例外・出力欠落や必須フィールド欠落は一過性のエージェント失敗とみなし、別エージェントで合算最大 2 回再測定する(再測定も失敗した場合に限り fail-closed で新規着手を止める。`count` 一致で `err>0` の du 実失敗は再測定しない。Issue #566)。**測定そのもの(`du -sk` の全件合算・`df -Pk`)は `scripts/measure-residual-bytes.sh`・`scripts/measure-free-disk.sh` が行い**(Issue #603。`args.skillDir` が必要)、エージェントは `run-verified.sh` 経由のコマンドを 1 回実行して標準出力の 1 行 JSON(`nonce`・`payload`・`sha256`)を転記するだけにする。ホストは `nonce` と `sha256`、および測定した入力の SHA-256(`payload.inputSha256`。ホストが送ったパス列から再計算)を照合し、欠落・不正・不一致は上と同じ転記失敗として再測定する。この測定値は新規着手の抑止にだけ使い、マージ判定には使わない(references/script-trust-model.md) |
| `maxBaseMerges` | 任意 | `3` | **`args.repo` が未指定のランでは値によらず base 取り込みは起動しない**(`args.repo` の形式不正は起動時にエラーで停止しラン自体が始まらない。`repo` 行参照)。PR が base とコンフリクト(`mergeable: CONFLICTING`。monitor の `conflicting` 経路、または merge-exec の `not-mergeable` 写像のいずれか)した際に、fix 予算(`fixCount`)を消費せず自動起動する base 取り込み専用エージェント(`baseMergePrompt`。routing 照合・fetch・merge・push の検証は `scripts/base-merge.sh` が行い、エージェントは 1 コマンドを実行して結果を転記する)の回数上限(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"`)。`scripts/base-merge.sh` の worktree routing ガード(`--repo`。pr-create / fix の `pr-push.sh`(`--mode prepare` / `push` / `pr` の 3 モード)と、観測スクリプト `observe-pr.sh`(gh 呼び出しを `-R` 付きにする)にも同じ値を渡す。origin の fetch URL と push 先 URL の全行が、host github.com で owner/repo がこの値と大文字小文字を区別せず完全一致、かつ `gh pr view` の number と headRefName が一致すること)が期待する owner/repo(`expectedRepo`)として使う、人間が明示するホスト側入力。**エージェントの自己申告値(外部チェック観測エージェント等)は一切使わない**(自己申告値を信頼境界に使うと誤配置 worktree からの別リポジトリへの push を防げない)。未指定は `expectedRepo` を空文字のまま確定し、base 取り込み(`baseMergePrompt` の起動)を行わず conflicting は即 `blocked` / `blockedReason: "quality"` で終端する(fail-closed。実装・push 前 Review・PR 作成・CI 監視は値によらず自動で進む)。**形式不正(`isValidRepoSlug` の owner/repo 形式を通らない値)は起動時にエラーで停止**(マージゲート入力と同じ厳格さ。誤記を黙って未指定へ読み替えるとガードが静かに弱まるため) |
| `agentCallLimit` | 任意 | `1000` | 1 ランのエージェント呼び出し数の予算の基準値(81〜1000 の整数。`securityReview` 有効時は下限が 83 になる。Issue #592)。**新規着手・monitoring 再開を止める判断に使うランタイム上限の想定値**で、呼び出しそのものは止めない(開始処理・終了処理・実行中の issue は値を超えても続行する。実行中の issue の途中で止めると半端な状態を作るため。ハードキャップはランタイムが持つ)。ランタイムは 1 ランあたり 1,000 エージェントで打ち切り、打ち切りは `completed` として返るため、予算を持たないと PR 作成直後の未監視のままランが終わる。そこで、新規着手(および monitoring 再開)の前に「残り呼び出し数 ≥ 実行中の件数 × 30 + 候補の目安(実装 30・monitoring 再開 12・verify-close 6)+ 終了処理の予備(20 + 未完了ノード数 ÷ 100 の切り上げ。このランで最初に着手する 1 件〔まだ 1 件も着手しておらず実行中も 0〕には切り上げの項を課さない前進保証)」を確認し、満たさなければ着手せず実行中の issue の完了を優先する(実行中が減れば再び着手する)。見送ったノードは `not-started`(note に予算不足)として返る(note は保留した時点の事実を報告する。下記)。使用数が値を超えた最初の 1 回だけ警告ログを出し、返却値 `agentBudget.reached` が `true` になる。根拠と目安の内訳は references/agent-call-budget.md。**下限〔開始処理 30 + 終了処理の予備 20 + 1 issue の目安 30 + 1 = 81。`securityReview` 有効時は 1 issue の目安が 2 増えて 83〕未満・1,000 を超える値・非整数はエラーで停止**(開始処理だけで使い切る値は矛盾するため受理しない)。開始処理の 30 は固定の見積りで、依存宣言のチャンク数が多い大きなツリーでは実際の使用量が上回る。このランで 1 件も着手せず、予算だけが理由で保留したノードがあるときは、ランの終了時点で**予算枯渇による停滞**(返却値 `agentBudget.starved: true`)と判定する(着手の可否は monitoring 再開の worktree ゲートやラン中の前提解消などループ中にしか分からないため、起動時には断定しない)。保留ノードの note(状態ファイルに保存される blocked の note を含む)は、次ランの挙動を予測せず、**保留した時点の事実**だけを書く: 「予算不足で保留(保留時の使用 u/L、必要量 r。このランの着手 k 件)。保留した時点では `args.agentCallLimit` が N 以上なら予算検査を通った(次ランの必要量は、開始処理・先に着手するノード・ゲート実測で変わる)」。N = 保留時の使用量 + 保留時の必要量(ゲートが実際に使ったしきい値。上乗せしない)。N が 1,000 を超えるときは「(指定できる上限 1000 を超える)」という事実だけを添え、分割は案内しない(保留は `used + required > limit` でしか起きないので、既定の上限 1000 では N が常に 1,000 を超える)。返却値 `agentBudget.stoppedDispatchAt`(最初に保留した時点の使用数)と `held[].used`(そのノードを最後に保留と判定した時点の使用数)は別の時点の値で、後者が大きくなり得る。返却値 `agentBudget.held` は予算で保留した全ノード(他の停止理由と併存するものも含む)の観測値 `{ issue, kind, resume, used, required, chunks, needed, budgetOnly }` を issue 番号の昇順で返す(プログラムから読むなら `held`、人が読むなら note)。`agentBudget.neededLimit` は `held` のうち `budgetOnly` なものの `needed` の最小値(着手の有無に関わらず、無ければ `null`。1,000 を超え得て、そのまま `agentCallLimit` に渡せる値とは限らない)、`starved` は着手 0 件でそのようなノードがあるとき。未着手・monitoring 再開の note は、成立している停止理由をすべて優先順(halt → 新規着手抑止〔残置 worktree 上限等〕→ 前提 → 再開ゲート → 予算)に並べる。前提は、まだ決着していないもの(「前提 #A が未完了のため待機」)と、既に失敗・ブロックとして記録されたもの(「前提 #B は完了していない(失敗・ブロックとして記録済み)」)を分けて書く。予算の断片は単独でも他の理由との併記でも同じ文言(他の理由が残るノードは `starved` / `neededLimit` の対象に数えず、`held` では `budgetOnly: false`。詳細は references/agent-call-budget.md の「終了時の note の停止理由」) |
| `models` | 任意 | 未指定(`{}`) | **判断を伴う agent のモデルを段階ごとに上書きする**(`parseAgentModels`)。キーと対象の agent: `plan`(Plan。既定はモデルを渡さずセッションのモデルを継ぐ)・`implement`(Implement と回復の Implement)・`review`(push 前 Review)・`securityReview`(セキュリティ専用の Review。`securityReview` を有効にしたときだけ指定でき、無効のまま指定すると起動時エラー。既定は `models.review`、無ければ `sonnet`)・`fix`(Review ループと Merge ループの fix)・`prCreate`(push + PR 作成)・`baseMerge`(base 取り込み。#603 第 3 弾以降は `scripts/base-merge.sh` を 1 回実行して結果の 1 行を転記するだけの agent で、routing 照合・fetch・merge・push の検証はモデルによらずスクリプトが行う。判断を伴わないため、機械的な agent を対象外とする下の方針の例外として残す。費用を下げたいときに `haiku` を指定する用途を想定する)。`plan`・`securityReview` 以外の既定は `sonnet`。値は `agent()` の `model` に渡すエイリアス `haiku` / `sonnet` / `opus` / `fable` のいずれか。指定しなかった段階は既定のまま、同じ段階の agent が複数あればすべてに同じ値が効く。返却値 `models` に正規化した値(上の段階の順。未指定なら `{}`)を載せる。**状態ファイル・worktree 走査・Tree・Recover の安全確認・monitor・merge-exec・merge-verify・pr-bind・close などの機械的な agent は対象外**(呼び出し回数が多く上書きすると固定費が増えるうえ、マージの安全側の判定に関わる agent のモデルを呼び出し側が変えられる経路を作らないため)。Recover の判断(継続 / 破棄)は Plan と同じくセッションのモデルを継ぐ。オブジェクト以外・未知のキー(機械的な agent の段階を含む)・許可外の値・大文字違いは起動時エラーで停止する(エージェントを 1 回も呼ばない。誤記を黙って既定へ読み替えない) |
| `securityReview` | 任意 | `false` | **`true` または `{"enabled": true}` で、push 前 Review の各ラウンドにセキュリティ専用の Review エージェントを足す**(opt-in。`parseSecurityReview`)。通常の Review(品質とセキュリティを 1 体で見る)と同じ worktree 隔離・同じ比較基準と差分(`git diff origin/<base>...HEAD`)・同じ返却スキーマで、観点だけを認証・認可・入力検証とインジェクション・秘密情報と個人情報の露出・権限境界・OWASP Top 10 に絞ったエージェントを並列に起動し、互いの結果は見せない。判定は両方を合わせる: どちらかが `blocked` なら `blocked`、どちらかが `needs-fix` ならそのラウンドは通らず fix へ指摘のある側の指摘をすべて渡す、`highestSeverity` は両方の高い方(無効な結果・`needs-fix` なのに重要度 `none` の矛盾出力は medium 相当)。最終ラウンドで Low のみなら通す規則など既存の判定は合わせた結果に当てる。モデルの既定は通常の Review と同じ(`models.review`、無ければ `sonnet`)で、`models.securityReview` で変えられる。返却値 `securityReview`(`{ enabled, model }`)に載せ、ラウンドごとに両方の結果をログに出す。費用: 有効時は Review ラウンドごとに 1 エージェント(最大 3)と使い捨て worktree 1 件(最大 3)が増える。このため 1 issue の呼び出し数の目安(`agentCallLimit` の判定)を 30 → 32、`agentCallLimit` の下限を 81 → 83 に、新規着手 1 件の使い捨て worktree の予約を 3 件増やす。未指定・`false`・`{"enabled": false}` ではエージェントの呼び出しは今と同じ。それ以外の形(文字列・数値・配列・`enabled` 以外のキー・`enabled` が boolean でない)は起動時エラーで停止する。push 前に Review を通さない経路(PR 作成時の base 取り込み・Merge ループの base 取り込み)には足さない(Step 5 参照) |
**`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 件):
```html
<!-- 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](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](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](references/automerge-design.md)
### branch protection(マージ判定の本体。人間マージ・サーバー側 auto-merge の両運用で必要)
対象ベースブランチには**サーバー側 branch protection / ruleset を設定することを強く推奨**する(ランタイムゲートではなく運用推奨)。compromised なローカルエージェントもサーバー側ルールは迂回できない。
詳細: [references/automerge-design.md](references/automerge-design.md)
## フロー
### Step 1: ツリーを取得して依存グラフ付き実行キューを構築する(Tree)
gh CLI の sub-issues API で親イシュー配下の全ツリーを最下層まで再帰取得し、post-order DFS で実行キューを構築する。取得はキュー方式で、closed ノードも含めて全ノードの `sub_issues` を辿り、深さでは打ち切らない。打ち切るのは `sub_issues_summary.total` が 0 のノード(leaf と確定)だけで、訪問済み番号は再度辿らない。各ノードに `subIssuesTotal`(`sub_issues_summary.total`。closed を含む直下の子の総数)を記録し、ホストが全ノードで「返却ノード中の子の数 == `subIssuesTotal`」を照合する。不一致が 1 件でもあれば Workflow を停止する(fail-closed。取り損ねたノードを leaf と誤認して配下を黙って未実装にしない)。各 open イシューは本文を読んで機能的依存(`dependsOn`)を抽出する。
各 open イシューの本文からは opt-in テスト宣言マーカー(`<!-- optin-tests: ... -->`)も機械抽出し、ホスト側で許可形式かを再検証する(前掲「opt-in テストの宣言」節参照。宣言が無ければ後続処理は現行と変わらない)。
ツリー取得に続いて、直前 3 件の merged PR の check-runs から GitHub Actions 以外の外部チェック App(例: Cursor Bugbot)を観測する。**観測結果は参考値であり構成の確定情報ではない**。構成の確定は `args.externalChecks` の明示入力で行い、明示がない限り「確定不能」として後続の Merge ステップで自動マージを停止する。
```bash
# 各ノードのサブイシューを取得(--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 済みイシューは自動でスキップする(ラン開始時に完了扱い)。人手マージで自動 close した実装ノードのうち、状態が `blocked` / `monitoring` で worktree と PR を記録しているものは、PR のマージ済み(`MERGED`・結び付き・`headRefOid`)を確認できたときだけ、skip の直後に worktree を破棄して状態を `merged` へ更新する(`--head` で HEAD の一致を確かめ、違えば保持。確認できなければ status も worktree も触らない。1 ランの試行は 10 件まで(試行回数 `closedCleanupAttempts` の少ない候補から試すので、一過性の候補が先頭を占めても後ろの候補に順番が回る)、成立しなかった候補は `closedCleanupTried` に記録して次のランから飛ばす。`references/recovery.md` の「closed 後始末」)。ただし closed の子の配下に open の子孫がある場合、親の verify-close はその open 子孫(closed を貫通して最初に現れるもの)の完了も待つ。待たないと、配下が未完了のまま親の検証が走って `failed`(halt カウント)になる
- `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 宣言の行頭項目に限り、インライン記法には適用しない)。コードフェンス内(開始時の記号と長さを保持し、同種・同長以上の終了行でのみ閉じる)・引用行・インラインコード(同じ長さのバッククォート列同士を組にして内容ごと除去。改行をまたぐコードスパンも段落単位で除去し、ブロック境界はまたがない)は除外する(機能的に先行完了が必須のものだけを依存辺にするため)。インデントされた行はコードブロックとみなさず抽出する(リスト内容インデントは行単位で判定できず、取りこぼしより有界な過剰待機を選ぶ)。**兄弟イシューを工程・タスク ID で指す先行記述**も依存辺にする(Issue #657。受け入れ条件の「前の工程(TASK-97-5)の結果を踏まえている」のようにイシュー番号を書かない形)。ホストが兄弟(同じ親の子)のタイトル先頭のタスク ID(`type(scope)!: ` の後ろの `TASK-97-6` のような英大文字始まり + `-数字` 1〜4 組。兄弟の中で重複する ID と、1 つの親で 100 件を超える ID は使わない)から「ID → イシュー番号」の表を作って jq フィルタへ埋め込み、本文の「前の工程(ID)」「ID の結果を踏まえ」「ID を前提」「ID の後」「ID 完了後」「ID が完了して」等(ID の直後に先行を表す語)、`after` / `depends on` / `blocked by` / `builds on` / `requires` の直後の ID、依存見出し節の行頭の ID を番号へ解決する。ID を書かない「前の工程」は、自分のタイトルの ID の末尾の工程番号を 1 つ戻した兄弟(TASK-97-6 なら TASK-97-5)に解決する。ID だけの言及(「親 TASK-97「…」の工程 6/6」等)・表に無い ID・関連 / 参考 / 否定を含む行・コードと引用の中は依存にしない。工程を順に積む分解(工程 n が n-1 を踏まえる)のツリーでは、兄弟が直列になり並列度が下がる(本文の宣言どおりの順序で着手する)。`gh api repos/{owner}/{repo}/issues/<N> --jq`(REST の core 枠。GraphQL 枠を消費しない)が整数配列へ正規化した出力のみを扱い、本文テキストはエージェントのコンテキストへ入れない。40 件ずつのチャンクで、同時起動は 3 チャンクまで(`DECLARED_DEPS_CONCURRENCY`)で実行し、ホストが依頼した番号の全件返却と、jq がイシュー番号と deps から計算した検査値 `sig` を返却 deps から再計算した値との一致を照合する(ホストはシェルを持たずコマンド出力を直接照合できないため、転記の誤りを検査値で検出する。欠落・契約違反〔重複番号・非整数・deps 非配列・上限超過・sig 不一致〕は 1 回だけ再試行し、なお欠落すればラン開始前に停止する fail-closed。依存を取りこぼしたまま着手しないため。依頼外番号のエントリは採用せずログに残して無視する。Workflow ハーネスが中継するユーザー発言に複数のイシュー番号があると抽出エージェントが他ランの番号まで返すためで、sig が番号を含むので依頼番号へ化けず、依頼番号の欠落は従来どおり停止する。レート制限と確認できたチャンク(stderr のメッセージに rate limit / secondary rate limit / `RATE_LIMITED` がある、または HTTP 403・429 で `gh api rate_limit` の core の `remaining` が 0)は、エージェントが返す復帰までの秒数だけホストが待ってから未取得分を再試行する(権限不足・SSO などその他の 403 はレート制限扱いにせず、通常の失敗として再試行し、なお取れなければ停止する。secondary rate limit は core の reset と無関係なため `retry-after` の秒数、無ければ固定の 60 秒を待つ。待機は再試行回数を消費せず 1 チャンク 2 回まで。復帰が 900 秒を超える、または待機できない場合は待たず、一過性であることと再開可能時刻を示して停止する。他チャンクの失敗で全体が停止した後は、待機明けのチャンクもエージェントを呼ばない)。ツリー外前提の state 取得も同様〔レート制限の復帰待ちは対象外〕)
- 祖先イシューへの `dependsOn` は無視する(親は子の完了を待つ側のため)
- **ツリー外の前提イシューを待つ(fail-closed・既定で有効)**: `dependsOn`(Tree エージェントの抽出と本文宣言の和集合後の最終値)のうち、ツリー外(`args.parent` 配下に無い)の番号は毎ラン state だけを機械取得する(`plan:out-of-tree-deps-*`。`gh issue view <N> --json number,state` のみで本文は読まない。issue として取得できない番号は `gh pr view <N> --json number,state` で取り直す。1 ランあたり 200 件まで・40 件チャンク・1 回再試行)。open の前提を持つイシューは着手せず `blocked`(halt 非カウント)で「ツリー外の前提イシュー #N が open のため未着手(close 後に再実行すると着手する)」と記録し、それをツリー内で `dependsOn` に持つ後続も「前提イシューの失敗・ブロックにより未着手」で待つ。state を取得できなかった番号(gh の失敗・存在しない番号・上限超過)は open 扱いで待ち、ログに理由を残す。closed(番号が PR なら MERGED も)の前提は従来どおり制約なし。ルートの祖先(sub-issues の親チェーン。`plan:root-ancestors-<round>` が GraphQL の `Issue.parent` を 1 ラウンドあたり 8 段(`ROOT_ANCESTOR_DEPTH`)ずつ辿り、真の終端に届かなければ次ラウンドの起点を前ラウンドの最深ノードへ継ぎ足して最大 5 ラウンド(`ROOT_ANCESTOR_MAX_ROUNDS`)=最大 40 段まで `(number, parent)` の連鎖を確認する)は子の完了を待つ側のため待たない。連鎖が root から始まり各 `parent` が次の `number` に一致することをホストで検証し、取得失敗・不整合なら蓄積分も含めて祖先の除外なしで待つ側へ倒す(fail-closed)。安全上限の 5 ラウンドに達してもなお終端しない場合は、それより先の祖先確認を打ち切りツリー外前提として通常どおり state を見て待つ。祖先として除外した番号は、実際にツリー外依存に現れたものだけをログに出す。state の転記は jq が計算した検査値 `sig` をホストが照合し、不一致は再試行のうえ取得不能(待機)として扱う。state は状態ファイルに保存しないため、前提を close したあと同じ args で再実行すると再評価されて着手する。ラン中の前提完了プローブ(下記)はツリー外の番号を対象にしない(再実行で反映する)。人間担当 issue を別のトラッキングツリーで管理し、実装 issue 本文の依存節にその番号を書く運用を想定している
- 依存グラフに循環がある場合は 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 どちらの経路でもデータを失わない。対象ブランチの解決・WIP commit(フックを通す)・`origin/<base>` の取得・差分の有無は `scripts/recover-prep.sh` が決定的に行い(`references/script-trust-model.md` の第 9 弾)、Recover エージェントに残るのは継続か破棄かの判断と回復ブリーフの作成だけである。discard の場合は WIP commit を残した状態で branch を削除する。**削除後は reflog に残らない**(gc までの到達不能オブジェクトとして残るだけ)ため、`worktree-remove.sh` は消す前のブランチ先頭 sha を結果に出し、ホストがログに残す(`git branch <名前> <sha>` で gc までの間は復元できる)。
**削除ゲート(continue / discard 共通)**: Recover エージェントの返す `wipCommitted` は(`recover-prep.sh` の結果の行を写した)エージェントの返却値であり、誤判定・異常応答・プロンプトインジェクションで真を騙られ得る。加えて Recover は「フック失敗等で退避できなかった場合は `wipCommitted: false` を返して続行する」契約のため、`continue` も退避失敗時に返り得る。そのため **continue / discard いずれの経路でも** worktree の削除は次の 2 条件を**両方**満たす場合にのみ実行する。
1. **申告ゲート**: Recover エージェントが `wipCommitted: true` を返している(退避した場合、および退避すべき未 commit 変更が最初から無かった場合に true。フック失敗等で退避できなかった場合は false)
2. **事実ゲート**: 削除と同じプロセスで動く `scripts/worktree-remove.sh remove --mode discard` が、対象 worktree に未 commit 変更(`git -c core.excludesFile= status --porcelain=v2 -uall --ignore-submodules=none` の出力。利用者の status 設定とグローバルの除外ファイルに左右されない形)も初期化済み submodule も無いことを確認し、`--force` を使わずに削除できている。lock した Claude Code のセッションが生きている場合も削除せず保持する
どちらか一方でも満たさない場合(スクリプトが worktree を保持した場合を含む)、あるいは掃除自体が失敗した場合は 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 フェーズへ渡す
```bash
# 作業ブランチ作成例(並列時の衝突回避のためイシュー番号を含める)
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` スキルを参照。
**③セキュリティ専用の Review**(`args.securityReview` を有効にしたときだけ。既定は無効): 上の Review とは別のエージェントを各ラウンドで並列に起動し、同じ worktree 隔離・同じ比較基準と差分で、セキュリティ(認証・認可・入力検証とインジェクション・秘密情報と個人情報の露出・権限境界・OWASP Top 10)だけを見る。対象リポジトリの CLAUDE.md・rules・専門サブエージェントにセキュリティレビューの手順・観点があればそれに従う。互いの結果は見せない。返却は通常の Review と同じスキーマで、ホストが両方を合わせる(どちらかが `blocked` なら `blocked`、どちらかが `needs-fix` なら通らず fix へ指摘のある側の指摘をすべて渡す、`highestSeverity` は高い方)。以下の判定(最終ラウンドの Low の扱いを含む)は合わせた結果に当てる。PR 作成時(Step 5.5)と Merge ループの base 取り込みで行うコンフリクト解消には足さない: どちらも通常の Review を通さず push 後の CI・外部チェック・monitor が検証する既存の設計で、取り込む差分は base 側のレビュー済みのコミットとその解消であり、セキュリティだけ push 前のレビューを課すと 2 つの Review の対象が食い違うため。片方の Review エージェントが例外で終了した場合は、両方の決着を待ってから使い捨て worktree を台帳へ記録し(例外を投げた側はパス不明の 1 件として計上する)、通常の Review 単独の例外と同じ経路(そのイシューを `failed` で終える。fix へは進まない)で伝える。StructuredOutput 欠落(`null`)は従来どおり無効な結果として指摘あり扱いになる。
レビュー条件:
- `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 回のみ)。
```bash
# 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)
`scripts/observe-pr.sh --mode checks --until settled`(`gh pr checks --watch` の置き換え。Issue #603 第 8 弾)で 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](references/automerge-design.md) を参照。
**新規追加ファイルの衝突ゲート(Issue #658)**: 並列の兄弟 PR は各 worktree が `origin/<base>` から切られるため、連番付きのファイル(`docs/decisions/0063-*.md`・`migrations/0018_*.sql` 等)で同じ「次の番号」を選び、成果物の置き場ファイルを同じパスで新規追加し得る。番号違いの別名は git のコンフリクトにならず、CI・AI レビューも PR を 1 本ずつしか見ないので検出されない。そこで monitor が `ready` を返した後、`recoveryOnly` の判定とマージ実行(自動マージ・人手マージ待ちのどちらでも)の前に、`scripts/observe-pr.sh --mode added-files`(`merge:added-files:#N`)で自分の新規追加ファイル(changeType `ADDED`)を、同じランの先行 PR(`knownPrByIssue` と状態ファイルの `pr` のうち自分より PR 番号の小さいもの。近い順に 30 本まで)の新規追加ファイルと、base の同じディレクトリの既存ファイルに突き合わせる。連番 = basename の先頭の 3 桁以上の数字 + `-` / `_`、幹 = 最初の `.` より前で、同じディレクトリ・同じ連番・別の幹なら衝突(`0018_create.up.sql` と `0018_create.down.sql` のような同じ幹の組、自分が削除する base のファイルは衝突にしない)。勝ち負けは決定的で、base にあるものが常に先、それ以外は PR 番号の小さい方が先。後発の側だけを扱う: 連番の衝突は改番の fix(合成 finding。base と open PR のどちらでも使われていない番号へ改番し参照も直す)へ回し、2 回で直らなければ `blocked`。同じパスの新規追加は先行 PR のマージ前には統合できないので `blocked`(理由に先行 PR の番号。先行のマージ後の再実行では CONFLICTING → base 取り込みの経路で扱われる)。封筒の payload はパスの文字列を含まず件数と PR 番号だけで、ホストは nonce・入力のハッシュ・sha256 を照合する。判定は止める方向にしか効かないため、照合・取得の失敗(転記ミス・例外は sonnet で 1 回だけ取り直す)はゲートを飛ばして従来どおり進む(警告ログを残す)。ファイルが 100 件を超える PR は先頭 100 件だけで比べ、連番ファイルのディレクトリが 20 を超える PR は確かめない。補助として Plan と Implement のプロンプトに、連番付きのファイルは open PR の追加ファイルも見て未使用の番号を選ぶこと・置き場ファイルが既にあれば別名で新設しないことを書く(置き場を別名で新設する衝突は決定的に検出できないため、こちらだけが扱う)。
**クライアント側の自動マージは opt-in ランでのみ実行する(references/automerge-design.md の「クライアント側自動マージの設計」節参照):** `autoMerge: true` + `externalChecks` 確定(全 App の信頼済み context 宣言込み)のランでは、monitor の `ready` 判定後に merge-exec が検証スクリプト `scripts/merge-gate.sh`(`run-verified.sh` 経由。#603 第 6 弾)で 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 の実行権限を持たない。
- **観測と待機はスクリプト(`scripts/observe-pr.sh`、Issue #603 第 2 弾)**: PR の state・mergeable・チェック総数・required context の不足・外部 App のレビュー到着・未解決スレッド数は、monitor が `gh` / `jq` を組み立てずに `bash <skillDir>/scripts/run-verified.sh … observe-pr.sh --mode …` を実行し、1 行 JSON 封筒(整数と固定語彙の payload)を読む。待機(レビュー到着・required 揃い・チェック総数 0 件・CI の完了待ち `--mode checks --until settled`)の1 回の起動は、gh が遅い・ハングする場合を含めて壁時計 90 秒(`OBSERVE_PR_WALL_SEC`)+ 後始末の数秒以内に必ず返る(gh 1 回ごとに 25 秒の時間制限を付けてグループごと kill し、残り時間が 1 観測分に満たなければ次の観測を始めない。Bash ツールの既定タイムアウト 120 秒に収まる)。`pending` のたびに `--attempt` を 1 増やして同じコマンドを再実行する(最大 7 回 = およそ 10 分。CI の完了待ち `--until settled` だけは 25 秒間隔・4 観測で 1 起動およそ 83 秒、最大 30 回 = およそ 40 分で、旧 `gh pr checks --watch` の最長と同じ。回数の上限はスクリプトが強制する。エージェントが毎回自作していたループが `paste -sd+ | bc` の BSD 非互換で上限まで回り切った不具合の是正)。取得失敗は `err: 1` で返り、0 件・不足 0 に倒さない。ホストが封筒を照合して採用するのはチェック総数だけ(`checksObservation`。timeout かつ総数 0 を conflicting へ再判定する根拠)で、他の観測は monitor の助言的判定の材料にとどまる。push 直後の確認(`--mode push`。pr-create・fix のエージェントが実行)の封筒もホストは照合も採用もしない(`checksStarted` / `mergeableAfterPush` は従来どおり診断・分岐ヒント専用)。信頼モデルは [references/script-trust-model.md](references/script-trust-model.md)。
- **マージ実行エージェント(merge-exec)**: 監視が `ready` を返したときにホストが起動する。レビュー本文・Issue 本文・**チェック名**を一切読まず、**リポジトリ内ファイル(CLAUDE.md・`.claude/rules` 等 = PR 側で変更可能な未信頼テキスト)も読まない**(実装系エージェント向けの共通指示 COMMON を挿入せず、merge-verify と同一の最小指示で構成する)。**マージ直前の検証(G0・チェック・未解決スレッド・mergeable・外部チェック)は #603 第 6 弾から `scripts/merge-gate.sh` が行う**(`gh` / `jq` の手順はプロンプトに無く、判定語 `verdict`(merge-exec の reason と同じ語彙)を封筒で返す。merge-exec は `ok` のときだけ手順 3 の squash merge に進み、ホストは `merged: true / reason: merged` の受理前に封筒を nonce・sha256・入力・HEAD sha で照合する。マージの実行そのものはスクリプトに含めず merge-exec がプロンプトのコマンド形で行う。詳細は references/script-trust-model.md の第 6 弾の節)。以下は検証の中身(スクリプトが読むもの)を指す。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 取り込み予算の空費を防ぐ)。**required context の揃い待機(autoMerge ラン、Issue #577)**: monitor は ruleset の required context が HEAD 上に(check-run 名または commit status context として)揃うまで `ready` を返さず、最大 10 分の有界待機後も不足なら `timeout`(再監視)にする。速く終わる AI レビュー系チェックだけが先に揃った状態での早期 `ready` を防ぐ助言的判定で、発行元束縛は merge-exec (G0 (v-b)) が担う。merge-exec は束縛済み check-run の未発行のみ(integration_id が全て数値)なら `checks-not-green`(再監視)、integration_id が数値でない・検証不能なら `issuer-unbound`(終端)を返す。 **監視側の 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 に従い最終レポート確認時にユーザー承認のうえで実施する)。
完了待ちが終わった(`met`)だけで合格にしない。封筒の件数(`passed` / `pending` / `failed` / `total`)で全チェックの結論を確認する。pending が残ったまま待機上限(`exhausted`)なら `timeout`。`failed` があれば修正エージェント(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)**: CI の完了待ち(`--until settled`)は「**存在する**チェックが緑になったか」しか保証せず、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 を除外して再実行する。
```bash
# HEAD sha を取得(push のたびに取り直す)
HEAD_SHA=$(gh pr view <pr-number> --json headRefOid -q .headRefOid)
# CI の完了待ち(monitor が run-verified.sh 経由で実行する。--attempt を 1 から最大 30 回まで増やして再実行 = およそ 40 分)
bash "<skillDir>/scripts/run-verified.sh" <sha256> observe-pr.sh --nonce <nonce> --mode checks --pr <pr-number> --until settled --wait --attempt 1
# 封筒の payload: passed / pending / failed / total。pending が 0 件で met。failed が 0 件、pending が 0 件であること
# 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 競合の発生頻度が上がるため、この状態に遭遇しやすい。
- **検知コマンド(エージェントが実行してよいものと、人間の診断専用を明確に分ける)**:
```bash
# (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]+$`)以外の
# 出力も判定不能として扱う
```
```bash
# (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 回だけ起動する」設計に反する)。
```bash
# 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`)を起動する。routing 照合・fetch・checkout・マージ・push とその検証(push 前後のリモート head の突き合わせ)は `scripts/base-merge.sh`(`lib/push-verify.sh`。Issue #603 第 3 弾)が決定的に行い、エージェントは `run-verified.sh` 経由の 1 コマンドを実行して標準出力の最後の 1 行 `BASE_MERGE_RESULT {…}` を転記するだけである(封筒は使わない: ホストが新たに採用する値が無く、`pushed` は進捗にしか使わない。信頼モデルは [references/script-trust-model.md](references/script-trust-model.md))。スクリプトを実行できなかった(`run-verified.sh` の exit 3 〔ハッシュ不一致〕/ 4 〔欠落〕・スクリプトの exit 2 等で結果の行が無い)ときは、エージェントが `pushed: false`・`commitFailed: true` で返し、ホストは下記の `conflict` / `hookRejected` を伴わない `commitFailed` として `failed` 終端する(取り込み直しはしない)。品質問題ではないため `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 は `base-merge.sh` の埋め込み定数 `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 最新化ゲートが commitlint 設定を正当に読める権限で subject を決定し、マージコミットを作成・検証・push する)。`conflict` / `hookRejected` を伴わない `commitFailed`(branch / base fetch 失敗・checkout 失敗・subject 未確定等)は自動回復不能クラスとして `failed` 終端する。push 後の `mergeable` と check-run 起動の観測は、この経路では行わない(第 3 弾で、ログ専用だった `mergeableAfter` / `checksStarted` / `headSha` の 3 項目を返却スキーマごと廃止した。実際の判定は次ラウンドの monitor がサーバー側の実値を再観測して行う)。**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 防御は弱めない。monitor に限り、null・例外のとき 1 回だけ即時再試行し(再試行は gh run rerun・@cursor review 投稿等の書き込みを禁止した観測専用の指示付きで行う。)(書き込みを伴う fix は 1 回目が push 済みの可能性があり、再実行すると pushed:false の no-op で push を見失うため再試行しない)、それでも失敗した場合に `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: 親イシューを検証してクローズする
子を持つノード(親イシュー)は、配下のすべての子孫が完了した時点で以下を確認してクローズする。直下の子だけでなく、`sub_issues_summary.total > 0` の子を再帰的に辿り、子孫の最下層まで open が無いことを確認する。
```bash
# 1. 子孫の最下層まで open が無いか確認(--paginate で 100 件超も全ページ自動取得。
# total > 0 の子ごとに同じ呼び出しを繰り返す。全体を辿る場合は scripts/preview-tree.sh を使う)
gh api --paginate "repos/{owner}/{repo}/issues/<parent>/sub_issues?per_page=100" \
--jq '.[] | {number, state, total: .sub_issues_summary.total}'
# 2. 受入基準・チェックリストを読む
gh issue view <parent-number>
# 3. 受入基準を満たしていればクローズ
gh issue close <parent-number> --comment "配下のサブイシューがすべて実装・マージ完了。受入基準を確認してクローズ。"
```
open のサブイシューが残っている場合、または受入基準が未達の場合はクローズせず `failed` として記録する。親ノードは全子イシュー(closed の子の配下にある open の子孫を含む)が完了するまで投入されないため、子の並列実行完了後に検証される。
### 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` に記録される。前提の PR MERGED 判定は PR 番号の一致だけでなく、merge-verify と同じ PR と issue の結び付け照合(headRefName・base・同一リポジトリ・closingIssues)を満たす場合に限る(詳細は references/recovery.md)。
レポート出力テンプレート(処理結果サマリー・完了イシュー・失敗/未着手イシュー・対象外/未解決コメントの各節)と返却値フィールドの説明は以下を参照。
返却値 `mainWorktreeUntracked` にメイン worktree(リポジトリルート)の未追跡ファイル検査結果が入る。ラン開始時・終了時の観測差分から本ラン中に新規出現したファイルを検出し、**削除はせず警告のみ**行う(並行ランや人間の作業も拾い得るため帰属は推定)。`observed: false` は検査自体が不成立だったことを示し、その場合は `git status` で手動確認する。1 件以上検出した場合はレポートにも記載し、ユーザーへ手動確認・削除を促す。
返却値 `mainWorktreeHead` にメイン worktree の HEAD 検査結果(`observed` / `changed` / `shaChanged` / `baselineRef` / `endRef`)が入る。`changed: true` はラン中にブランチが切り替わった(または detached HEAD のまま動いた)ことを示す。**自動では戻さない**ので、`git status` で確認して手動で元のブランチへ戻す。`observed: false` は検査不成立のため手動確認する。
詳細: [references/report-format.md](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` でコメント・先頭インデント・トークン間の不要な半角スペースを除去した実行ファイルを再生成する(行番号は両者で一致し、列番号は一致しない)。ビルダーは生成物と開発ファイルを acorn のパーサ(`acorn.parse` の `onToken`。ブロックかオブジェクトか・条件の括弧・`for await` 等の文脈つき)で読んでトークン列(文字列・テンプレートの中身を含む)を照合する。読みは module と、Workflow ランタイムが実際に評価する形(先頭の `export const meta` の `export ` を外した本体を関数本体として評価 = script。`<!--` と行頭の `-->` が行末コメントになる)の 2 つで、両方で一致しなければならない。不一致・どちらかがパースできない・照合を実行できない・照合結果が想定の形でない場合は生成ファイルを書かずに中止する。照合を差し替える CLI 引数は無い。**中止になる開発ファイルの書き方**(実測): `}` の直後に置いた正規表現が `//` `/*` を含む(普通の `/re/.test(s)` は通る)、`for (` の括弧の中で変数 `of` を除算に使う行(`i < of / 2`)と同じ行に `//` を含むブロックコメントがある(同じ行に `/` を含むテンプレートがあり、その後に複数行のテンプレート等がある場合も)、`class A extends /[//]/…` のように `extends` の直後に `//` `/*` 入りの正規表現を置く(括弧で包めば通る)、`o?.in / 2` のように `?.` の直後のキーワード名プロパティの直後に `/` を置く(同梱 acorn が誤読してパースできない。`(o?.in) / 2` で避ける)、`with` 文、`<!--` を行末コメントとして使い後ろに `/*` を置く形。中止にはならないが生成物が縮まない形もある(意味は同じ。開発用コメントが配布物に残る): `}` の直後の `` /`/ `` や対にならないクォートを含む正規表現(`/a b['"]x/.test(s)`)、`of` の除算の行(行末の `//` コメントが残る。同じ行に `/` を含むテンプレートがあると以降のコメントとインデントも残る)、メソッド名が `for` の定義の括弧内で `of` を除算に使う形。**これらの正確な条件は `build-workflow.mjs` の `stripComments` の doc コメント(「残る制約」)を正とする**(ここでは代表的な形だけを挙げる)。同期漏れ・直接編集は CI の `tests/build-workflow.test.mjs`(鮮度ゲート)が検出する。
詳細: [references/verification.md](references/verification.md)
テストの流し方(全体・ファイル単位・Node 24 と 22 の並列)と、検査が弱くなっていないことを変異で確かめる実行器 `tests/tools/mutate.mjs` の使い方: [references/testing.md](references/testing.md)
## よくある失敗
| 問題 | 回避策 |
|------|--------|
| テスト失敗の原因を調査せず当て推量で修正を繰り返す | 対象リポジトリに `.claude/rules/debugging.md` があればその4フェーズ、無ければ同じ方針(調査→分析→仮説→修正)を踏む。3回失敗したら `blocked` にしてユーザーへ報告 |
| 完了待ちが終わった(met)だけで CI 合格と判断する | 封筒の件数(passed / pending / failed / total)で全チェックの結論を確認する。total 0 件は green とみなさない |
| 仕様準拠を確認せずにコード品質レビューへ移行する | 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](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[] | select(.name == "<job名>") | .conclusion'` で同一 job 名の conclusion が `success` であることを実測する。推測で flaky 扱いにしない |
**推奨投入単位**: 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 | 定型コマンド出力の転記(判断なし) |
| `plan:out-of-tree-deps-*` / `plan:root-ancestors-<round>`(ツリー外前提の state・ルートの祖先チェーンの機械取得) | haiku | low | 定型コマンド出力の転記(判断なし)。ツリー外の前提がある場合のみ起動。祖先チェーンはラウンドごとにラベルが変わる(`plan:root-ancestors-1` 等、最大 5 ラウンド) |
| `detect:external-checks`(外部チェック判定) | haiku | low | 定型コマンド集計 |
| `state:load` / `state:load-verify` / `state:load-fill` / `state:update` / `state:update-batch:<n>` / `state:cleanup` / `state:init-all` / `state:high-water` | haiku(未返却時 sonnet へ 1 回フォールバック) | low | jq の機械処理。StructuredOutput 未返却(例外・null・schema 不適合)が続く場合のみ同一プロンプトで sonnet へ 1 回フォールバックする。`state:load-verify` は読込結果を項目ごとの sha256 で照合する独立エージェント。`state:load-fill` は返らなかったキーをホスト組み立ての jq で 5 件ずつ再取得する(詳細は `references/recovery.md`)。`state:update-batch:<n>` は着手できないノードの blocked 記録を 100 件ずつ 1 回で書く(件数に比例して起動しない。後述) |
| `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 | 実装系・コスト最適化 |
| `pr-bind:#N`(monitoring 再開前・新規 PR の Merge ループ投入前の PR 照合) | sonnet | low | `gh pr view` の取得値の転記のみ(実在・headRefName・closingIssuesReferences の判定はホスト側) |
| `merge:#N`(CI/レビュー監視・マージ) | sonnet | medium | CI/レビュー判定・マージ可否ゲート |
| `merge:added-files:#N`(新規追加ファイルの衝突ゲートの観測。Issue #658) | haiku(転記ミス・例外・取得失敗のときだけ sonnet で 1 回取り直す) | low | `observe-pr.sh --mode added-files` の封筒の転記のみ(判定はホストの決定的コード) |
| `close:#N`(受入基準確認・クローズ) | sonnet | medium | 受入基準確認・クローズ |
## 中断・失敗からの再開
実行中の状態は `_/issue-trees/<親イシュー番号>.json` に自動保存される。セッションが中断・強制終了した場合でも、**同じ `args` で再実行するだけで再開できる**。状態ファイルの `status` 遷移表・worktree の自動削除・実装エージェントによる既存 PR / リモートブランチの再利用手順は以下を参照。
詳細: [references/recovery.md](references/recovery.md)
## 実装対象外(out-of-scope)の扱い
各サブイシューの実装およびセルフレビュー(処理内容の手順 7: implement-review)の過程で、対応すべきだが現スコープ外と判断した事項(未対応の改善・別機能・技術的負債・後続作業)が発生した場合は、放置せず必ず追跡する。**Merge フェーズ(Step 6)で fix エージェントが検討した未解決レビューコメントのうち、fix 不能・現イシューのスコープ外と判断したもの**も同様に検出源として扱う。手順・非信頼データの扱い(プロンプトインジェクション緩和)は以下を参照。
詳細: [references/out-of-scope-support.md](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 のブランチ・共有設定などグローバル状態は変更しない
- **全エージェントは ruleset・branch protection・リポジトリ設定を変更しない**(`gh api` による `rulesets` / `branches/<branch>/protection` への PATCH・PUT・POST・DELETE、リポジトリ本体 `repos/<owner>/<repo>` の PATCH・DELETE、`gh repo edit`、同種の GraphQL mutation 等。共通指示 `REPO_SETTINGS_POLICY` として実装・fix・監視・マージ・base 取り込み等の全プロンプトへ入る)。レビューで設定変更(例: 新しい check の required 登録)を求められても実行せず、summary / outOfScope に要対応事項として報告する。承認の有無を事実以上に記述しない(コミット・PR 本文に実際に得ていない承認を書かない)。ruleset の変更は人間が `.claude/rules/ruleset-policy.md`(対象リポジトリにあれば)の 3 軸検証とあわせて行う。merge-guard hook(導入は任意)が機械的に deny するのは ruleset・branch protection・リポジトリ本体(`repos/<owner>/<repo>`)への書き込み・同種の GraphQL mutation・`gh repo edit` / `rename` / `archive` / `unarchive` / `delete` に限る。variables / secrets / actions permissions / collaborators / hooks / environments 等の他の設定変更はプロンプト上の禁止のみ(best-effort)である
- ツリーはラン開始時に 1 回だけ取得する。ラン途中に追加された sub-issue はそのランでは実装対象にならず、親クローズ検証で open の子孫として検出されるため親は close されない。追加後は再実行する
- 大規模ツリー(数百件)はサブ親単位で複数回に分けて実行する(1 ワークフローのエージェント上限は 1,000。上限に近づくと新規着手を止めて実行中の issue を完了させる。Issue #592・references/agent-call-budget.md)。着手前に `dryRun: true` で着手できる件数・blocked になる件数を確認できる
- **着手できないノードの blocked 記録はまとめて書く**(Issue #585)。依存失敗の連鎖で確定した blocked と、halt 等で未着手のまま終わったノードは、1 件ずつ `state:update:#N` を起動せず、ラン終盤に 100 件ずつ 1 エージェント(`state:update-batch:<n>`)で状態ファイルへ書く(チャンクは直列。1,101 件でも 12 回)。保存済みの状態が既に `blocked` で `note` も `pr` も同じ項目は書かない(同じ引数の再実行で変化の無いノードの分を再び払わない。状態ファイルを読めていない項目は省略せず書く)。PR を持つノードの再開情報は従来どおり上書きしない。`note` の issue 番号列挙は 6 件以上なら「N 件(先頭 5 件: #a, #b, …)」に縮める。途中のチャンクが失敗しても続行し、返却値の `blockedStateWrite`(`requested` 記録しようとした件数 / `unchanged` 省略した件数 / `attempted` 書き込みを試みた件数 / `written` 書けた件数 / `unwritten` 書けなかった件数 / `unwrittenIssues` 先頭 20 件 / `chunks` / `failedChunks`)とログ(`⚠️ blocked の状態ファイル記録: N 件を書けなかった`)で、返却値(`done`)と状態ファイルの件数のずれが分かる。ずれた分は同じ引数で再実行すると書き直される
- `--no-verify` は絶対に使用しない(pre-commit フック回避禁止。対象リポジトリに `.claude/rules/conventional-commits.md` があればその規約に従う)
- シェルコマンドの変数は必ず `"${var}"` でクォートする(コマンドインジェクション対策)。GitHub API から取得した文字列はプロンプト埋め込み前にサニタイズされる
- 1 イシューの失敗では停止せず次へ進むが、3 イシュー連続失敗で新規着手を停止(halt)する
- マージ前に **CI は全チェックが success/neutral/skipped で完了(pending/failure 0 件)であること**を明示確認する(CI の完了待ちが終わっただけでは合格にせず、封筒の件数で全チェックの結論を確認する)
- マージ前に **チェックが 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` 一時ファイルのみ)。一時ファイルは `mktemp` の絶対パス、または scratchpad 内に `mktemp -d` で作った自分専用ディレクトリの中に置き、削除は自作物だけに限る。ラン開始時・終了時にメイン worktree の未追跡ファイルを検査し、本ラン中に新規出現したファイルがあれば最終レポート `mainWorktreeUntracked` とログで警告する(**自動削除はしない**。並行ランや人間の作業も拾い得るため帰属は推定)
- **ラン中のエージェントは main セッションと scratchpad を共有する**。ラン間で使うファイル(作業契約・ゲート用スクリプト・待機用スクリプト等)は scratchpad に置かず、`~/.cache/<名前>/` など scratchpad の外に置いてエージェントへそのパスを渡す(エージェントには自作物以外を削除・上書きしない規則を課しているが、規則に依存せず巻き込まれる余地を残さない)
- 隔離 worktree を持たないエージェント(計画・監視・マージ・状態書込み等)はメイン worktree で動くため、`git checkout` / `switch` / `reset` / `merge` / `rebase` / `stash` / `pull` / `commit` / `clean` 等でメイン worktree の HEAD・インデックス・作業ツリーを変更しない(`origin/<base>` の内容は `git show` / `git diff` / `git ls-tree` / `git log` で読む)。ラン開始時・終了時にメイン worktree の HEAD(ブランチ名と sha)を観測し、ブランチが変わっていれば(開始時から detached の場合は sha が動いたとき)最終レポート `mainWorktreeHead` とログで警告する(**自動では戻さない**)
## sandbox 環境での実行
このスキルはネットワーク越しの GitHub 操作(`git fetch` / `git push` / PR 作成・マージ)を必須とする。該当コマンドはコマンド単位で sandbox 無効にして実行する。ネットワーク遮断を解除できない環境では実行できない。
Ships with 136 supporting files:
- references/agent-call-budget.md
- references/automerge-design.md
- references/out-of-scope-support.md
- references/recovery.md
- references/report-format.md
- references/script-trust-model.md
- references/testing.md
- references/verification.md
- sample/args-example.json
- sample/issue-tree-example.json
- sample/state-example.json
- scripts/base-merge.sh
- scripts/build-workflow.mjs
- scripts/implement-issue-tree.js
- scripts/lib/added-files-lib.sh
- scripts/lib/base-merge-lib.sh
- scripts/lib/merge-gate-lib.sh
- scripts/lib/observe-checks-lib.sh
- scripts/lib/pr-push-lib.sh
- scripts/lib/push-verify.sh
- … and 116 more
Mirrored from the author's public source. Install counts from the open skills registry.