| name | cross-review |
| description | PR を codex / gemini 両方にレビューさせ、両方 APPROVE まで /ndf:review → /ndf:fix を自動ループ。サブエージェント分離・PR ローテーション・nit 集約でメイン context 消費を最小化 |
| argument-hint | [PR番号] [--max-rounds N] [--rotate-after K] [--only codex|gemini] |
| disable-model-invocation | true |
| allowed-tools | ["Bash","Read","Edit","Write","Glob","Grep"] |
クロスレビュー収束ループ
PR を codex / gemini 両方 にレビューさせ、両者が APPROVE を返すまで
/ndf:review と /ndf:fix を自動で回す。
詳細手順は docs/ 配下に、主要コマンドは scripts/ 配下に分割している:
メインセッションからは $SCRIPTS/state.py <subcommand> 形式で呼ぶだけで、
state.json の読み書きや AI launcher 起動・完了待ちは全て委譲される。
設計方針
長丁場が予想されるため メインセッションの context 消費を最小化 する:
| 観点 | 方針 |
|---|
| レビュー投稿 | AI 自身が gh api で PR に直接投稿。メインはペイロードを保持しない |
| 修正 | 必ずサブエージェント (general-purpose) で実行。メイン context に diff は載せない |
| ユーザ問い合わせ | 自動判断を最大化(critical/major/minor は自動修正、nit は最後にまとめて 1 回だけ問い合わせ) |
| 状態の永続化 | /tmp/cross-review-pr<番号>-state.json に集約。中断・再開可能 |
| 長尺PR対策 | --rotate-after ラウンドで PR をローテーション(squash + 新ブランチ + 新 PR) |
| 振動検知 | 同じ指摘が 2 round で 50%以上重複したら中断 |
引数
| 引数 | 意味 | 既定 |
|---|
[PR番号] | 対象 PR(省略時は直前 PR / 現在ブランチ) | — |
--max-rounds N | 全体最大ラウンド数(PR ローテーションを含む通算) | 6 |
--rotate-after K | この round 数で未収束なら PR ローテーション | 5 |
--only codex / --only gemini | 片方だけで回す(デバッグ用) | 両方 |
例:
/ndf:cross-review 123
/ndf:cross-review 123 --max-rounds 4 --rotate-after 2
/ndf:cross-review 123 --only codex
前提
/ndf:review が AI 直接投稿(外部 AI 自身が gh api で投稿)に対応
/ndf:fix が サブエージェント起動 + 重要度ベース自動修正 + Resolve Conversation に対応
codex / gemini CLI が動作し、gh CLI が認証済み
Agent(subagent_type="general-purpose", ...) でサブエージェントを起動可能
事前確認(state.py init が自動実施)
ループ開始前に 4 つのプリチェック が必要だが、すべて scripts/state.py init
が内部で実施する。メインは結果を KEY=VALUE 形式で受け取るだけで良い。
| # | 対策 | スクリプト側で何をするか |
|---|
| 1 | 自分の PR 判定(422 回避) | gh api user と gh pr view --json author を比較し is_own_pr / event_downgrade を state.json に書く |
| 2 | worktree 分離 | git worktree add /work/worktrees/pr<PR> <head> を冪等実行 |
| 3 | gemini trusted directory | launch-gemini.sh が GEMINI_CLI_TRUST_WORKSPACE=true + --skip-trust を必ず併用。さらに tmp dir は ~/.gemini/tmp/<workspace>/ を採用し、gemini の workspace 制約 (workspace 外の read_file / write_file がブロックされる) を回避 |
| 4 | 既存コメント差分 | gh api .../comments --paginate を $TMP_DIR/cross-review-pr<PR>-existing-comments.txt に保存し、gemini プロンプトには 内容をインライン埋め込み、codex プロンプトには path を渡す |
intent / posted_as の両保持(最重要)
GitHub は 自分の PR には REQUEST_CHANGES でレビューを投稿できない
(HTTP 422)。state.json には 両方 を保持する:
"codex": {
"intent": "REQUEST_CHANGES",
"posted_as": "COMMENT",
"comments": 5, "review_url": "..."
}
state.py judge は intent を見るので、ダウングレード投稿してもループは続行する。
全体フロー
flowchart TD
Start([事前確認 / loop 開始前に 1 回だけ]):::phase --> Init["worktree 作成 + state.json 初期化<br/>・自分の PR 判定 → event downgrade 設定<br/>・/work/worktrees/pr<PR> を用意<br/>・既存コメントスナップショット保存"]
Init --> Round["Round N start<br/>current_pr = PR#"]:::phase
Round -.並列バックグラウンド.-> Codex["/ndf:review <PR> codex<br/>(AI が gh api で直接投稿)<br/>body 先頭: cross-review / round N / codex / intent<br/>→ result.json (intent + posted_as)"]
Round -.並列バックグラウンド.-> Gemini["/ndf:review <PR> gemini<br/>--skip-trust 必須<br/>body 先頭: cross-review / round N / gemini / intent<br/>→ result.json (intent + posted_as)"]
Codex --> Decide{"判定 (intent ベース)"}
Gemini --> Decide
Decide -->|両方 APPROVE / SKIP| Approved([final = approved]):::ok
Decide -->|一方でも REQUEST_CHANGES| Fix["Agent (general-purpose)<br/>/ndf:fix <PR> --defer-nit を worktree 内で実行<br/>・critical/major/minor 修正 + push<br/>・reply + resolveReviewThread<br/>・deferred/rejected は reply のみ<br/>→ /tmp/fix-pr<#>-result.json"]
Fix --> Check{収束チェック}
Check -->|max-rounds 到達| MaxR([final = max_rounds]):::stop
Check -->|振動検知 50% 重複| Osc([final = oscillation]):::stop
Check -->|CI failure code-related| Err([final = error]):::stop
Check -->|"CI failure meta-only (Assignees 等)"| Round
Check -->|round_in_pr >= rotate-after| Rotate["PR rotation<br/>squash + 新ブランチ + 新 PR"]
Check -->|それ以外| Round
Rotate --> Round
Approved --> Nit[最後に 1 回<br/>deferred nit 一覧をユーザに問い合わせ]
MaxR --> Nit
Osc --> Nit
Err --> Nit
classDef phase fill:#eef,stroke:#557
classDef ok fill:#dfd,stroke:#383
classDef stop fill:#fdd,stroke:#933
実行ステップ概要(メインの bash 骨組み)
各ステップの詳細は docs/ 参照。メインは以下のテンプレートで scripts/ を呼ぶだけ:
SCRIPTS="$CLAUDE_PLUGIN_ROOT/skills/cross-review/scripts"
STATE_PR=$INITIAL_PR
eval "$("$SCRIPTS/state.py" init "$STATE_PR" \
--max-rounds "$MAX_ROUNDS" --rotate-after "$ROTATE_AFTER" \
${ONLY:+--only "$ONLY"})"
export CROSS_REVIEW_TMP_DIR="$TMP_DIR"
cd "$WORKTREE"
while :; do
eval "$("$SCRIPTS/state.py" start-round "$STATE_PR")"
[ "$ONLY" != "gemini" ] && "$SCRIPTS/launch-codex.sh" "$STATE_PR" "$ROUND"
[ "$ONLY" != "codex" ] && "$SCRIPTS/launch-gemini.sh" "$STATE_PR" "$ROUND"
"$SCRIPTS/monitor.py" "$STATE_PR" "${ONLY:-both}" || handle_review_failure $?
[ "$ONLY" != "gemini" ] && "$SCRIPTS/state.py" read-result "$STATE_PR" codex
[ "$ONLY" != "codex" ] && "$SCRIPTS/state.py" read-result "$STATE_PR" gemini
if "$SCRIPTS/state.py" judge "$STATE_PR"; then break; fi
"$SCRIPTS/state.py" check-oscillation "$STATE_PR" || [ $? -eq 2 ] || exit 4
"$SCRIPTS/state.py" merge-fix "$STATE_PR"
if "$SCRIPTS/state.py" should-rotate "$STATE_PR"; then
eval "$("$SCRIPTS/rotate-pr.sh" "$STATE_PR")"
"$SCRIPTS/state.py" set-current-pr "$STATE_PR" "$NEW_PR"
fi
done
"$SCRIPTS/state.py" report "$STATE_PR"
各ステップの内容と契約(state.json / result.json スキーマ等)の詳細は:
レビュー出力の制約
目的: PR 上に Resolve 義務を伴うインラインコメントを増やさない。
修正アクションを伴わない記述は一切出さない ことを両 launcher プロンプトで強制する。
1. body 先頭 identifier prefix(必須)
人間アカウントから AI が投稿するため、GitHub UI 上では誰のレビューか分からない。
body 先頭に必ず以下を入れる:
## 🤖 cross-review | round 1 | codex | REQUEST_CHANGES
書式: ## 🤖 cross-review | round <N> | <agent> | <event>
<agent>: codex / gemini のいずれか
<event>: AI の本来の判定(REQUEST_CHANGES / APPROVE / COMMENT)
posted_as ではなく intent を書く
2. インラインコメントの最小化(最重要)
インラインコメントは GitHub 上で Resolve 操作が必須 になるため、本当に直すものだけ作る:
| 重要度 | インライン化 | 説明 |
|---|
critical / major | ✅ する | 修正必須 |
minor | ✅ する | 明らかな改善のみ。判断が割れるなら出さない |
nit | ❌ 出さない | 好み・スタイルはコメント化禁止。気になっても無視する |
1 インラインコメント = 1 修正アクション を厳守。
コメント本文は [重要度 / カテゴリ] 修正提案 の 1 文で完結させ、
コード引用ブロック(...)や現状説明だけのコメントは作らない。
3. body(総評)に書かないこと
- ❌ 「良い点」/「Strengths」/「Positives」/「評価できる点」セクション — 一切書かない
- ❌ 個別ファイル・関数の褒め言葉
- ❌ 「特に問題ありません」「概ね良好です」等の評価文
- ❌ 対応不要な観察コメント(「〜のようです」「〜と思われます」止まり)
body に書くのは 設計レベル・PR 横断の修正提案 のみ。
書くことが無ければ body は ## 🤖 cross-review ... の prefix 行 + 1 行サマリのみで良い。
4. event 判定
APPROVE — 修正必須の指摘なし(minor 以下しか無い場合も APPROVE で良い)
REQUEST_CHANGES — critical / major の指摘あり
COMMENT — 基本使わない。雑感だけの投稿は禁止
CI failure の分類(誤中断防止)
「CI 失敗 → 即 final=error」は乱暴。scripts/state.py merge-fix が
fix 戻り値ファイル (/tmp/fix-pr<PR>-result.json) を受け取った際に
ci_failed_checks を以下で分類する:
| 分類 | パターン | 振る舞い |
|---|
| code-fail | pint / larastan / phpstan / test / lint / type / build / ruff / eslint / tsc / mypy | final=error で中断 (exit 3) |
| meta-only | check_pr_requirements / assignees / reviewers / labels / meta | ci_note に記録して継続 |
| 不明 | 上記以外 | 保守的に code-fail 扱い |
PR メタデータ系の check(Assignees / Reviewers / Labels)は 継続、
pint / larastan / test / build などは 中断 を原則とする。
アンチパターン
- ❌ 修正をメインセッション内で行う — context が一気に膨れる。必ずサブエージェント
- ❌ AI に Markdown だけ返させる — メインがパース・投稿する設計は禁物。AI 直接投稿
- ❌ nit を都度ユーザに問う — 必ずバッチ集約して最後に 1 回
- ❌
max-rounds なしで回す — 無限ループの温床
- ❌ PR ローテーションを忘れる — 100+ コメントの巨大 PR になる
- ❌ CI 失敗を一律で中断 — コード関連/メタチェックを分類(上記参照)
- ❌ 自分の PR に
REQUEST_CHANGES で投稿 — 必ず 422。事前判定 + COMMENT ダウングレード
- ❌
gemini --yolo だけで起動 — trusted directory で YOLO 無効化。--skip-trust 併用
- ❌
pgrep -fa <prompt> で完了判定 — gemini は long prompt が引数に乗り検知失敗。pidfile 必須
- ❌ sentinel 単独で完了判定 — codex がクラッシュすると永遠に出ない。
monitor.py の多軸判定 (pidfile / sentinel / 早期エラー / stall / hard timeout / result.json) を使うこと
- ❌ タイムアウトなしで wait — ハング検知不能。
monitor.py の hard timeout (30 分既定) + stall timeout (10 分既定) を必ず効かせる
- ❌ fix サブエージェントが Resolve をスキップ — reply だけでは未対応扱い。Resolve まで実行
- ❌ review body に identifier prefix を付け忘れる — GitHub UI 上で誰のレビューか不明になる
メイン context 節約の工夫
- 大きいファイルはメイン context に載せない: payload / err.log / diff は
すべて
/tmp/ に置き、メインは state.json と result.json だけ読む
- サブエージェント分離: 修正は別 context window で実行
- PR ローテーション: 1 PR あたりの会話履歴を抑える
- AI 直接投稿: 中間ペイロードがメインを通らない
- state.json で再開可能: メインが落ちても次回起動時に続きから
作業完了報告(必須)
ループ終了後、メインからユーザへの報告:
-
最終ステータス: approved / max_rounds / oscillation / error
-
総ラウンド数 / PR 数: 例: 5 rounds / 2 PRs (rotated 1 回)
-
PR 履歴: 各 PR 番号 + closed/open 状態 + round 数
-
各ラウンドのサマリ表:
| round | PR | codex | gemini | fix | CI |
|---|
| 1 | #123 | REQ (5) | REQ (3) | abc123 (5 fixed, 2 deferred) | ✅ |
| 2 | #123 | REQ (2) | APP | def456 (2 fixed) | ✅ |
| 3 | #145 | APP | APP | — | — |
-
残 deferred nit リスト(ユーザ判断要)
-
rejected 件数(bot 誤指摘で却下したもの)
-
最終 PR URL
詳細は PR 上のインラインコメントと state.json に残っているため、本報告では
繰り返さない。
関連
/ndf:review — 単発レビュー(AI 直接投稿対応)
/ndf:fix — 修正対応(サブエージェント起動対応)
/ndf:codex — codex CLI 呼び出し手順
/ndf:gemini — gemini CLI 呼び出し手順
/ndf:resolve-pr-comments — Resolve Conversation の詳細
general-purpose エージェント — fix 実行用サブエージェント