issue を投げると PR が返ってくる — Codex と Claude Code を往復させる Hermes Agent の自律ループスキル

重岡 正 · Thu, July 23, 2026

前回、Claude CodeCodex に同じ PR を独立にレビューさせる omh-pr-multi-review というスキルを書きました(PR レビューを Claude Code と Codex に同時にやらせる)。あれは既に存在する PR に対する後工程の自動化でした。

今回はその手前、issue から PR ができるまでを丸ごと自走させます。Hermes Agent 向けのカスタマイズをまとめた oh-my-hermesomh-issue-loop を追加しました(PR #6)。

使い方は単純で、GitHub issue の URL を 1 つ渡すだけです。あとはエージェントが実装し、検証し、レビューし、指摘を直し、また検証して、最終的に人間がレビューできる状態の PR を残して止まります。マージはしません。issue も閉じません。

実際に自分はこのスキルを日々の開発で回しています。そして回してみて分かったのは、人間の時間の使い道が完全に移動したということです。この記事の後半はその話をします。

初版を公開したあとも日々このスキルで開発を回していて、そこで見えた穴を都度塞いできました。この記事は現時点の状態を反映しています。設計時に考えていたことより、実際に運用して初めて分かったことの方が面白かったので、そこも書きます。

スキルの構成

構成はこれだけです。

skills/omh-issue-loop/
├── SKILL.md                                    # スキル定義(オーケストレーションの手順)
├── agents/openai.yaml                          # 表示名と既定プロンプト
└── references/
    ├── commit-signing-preflight.md             # コミット署名の preflight 手順
    └── workflow-flowchart.md                   # ワークフロー全体図と不変条件

前回の omh-pr-multi-review には Python スクリプトを同梱して、制御フローを決定的なコードに閉じ込めました。今回はスクリプトがありません。手順を自然言語で書いた SKILL.md と、そこから参照される reference が 2 枚あるだけです。

これは方針転換ではなく、対象が違うからです。多モデルレビューは「チェックアウトを切り替えて 4 つの CLI を順に叩いて必ず元に戻す」という、分岐のない副作用の強い一本道でした。決定的なスクリプトにするのが自然です。

一方で issue の実装ループは、リポジトリごとに検証コマンドが違い、レビューの指摘内容によって次にやることが変わり、収束するかどうかも事前に分かりません。ここをスクリプトに落とすと、リポジトリ固有の前提をハードコードするか、巨大な設定ファイルを書かせるかのどちらかになります。だから判断はエージェントに残し、代わりに禁則と上限を厳密に書きました。

オーケストレータには実装させない

SKILL.md の冒頭は、やることではなく、やらないことの宣言から始まります。

Orchestrate the workflow only. Delegate working-tree implementation and fixes to Codex CLI and all independent review and behavior verification to Claude Code CLI. Keep every Git history, remote, pull-request, issue, CI, and review operation under exclusive orchestrator control. Never implement, fix, review, merge, close the issue, or widen scope in the orchestrating agent.

オーケストレーションだけをやり、作業ツリー上の実装と修正は Codex CLI に、独立したレビューと挙動検証はすべて Claude Code CLI に委譲する。Git の履歴、リモート、PR、issue、CI、レビューの操作は、すべてオーケストレータの専管とする。オーケストレータ自身は実装も修正もレビューもマージも issue のクローズもスコープの拡大もしない。

この分離が今回の設計の核です。理由は 2 つあります。

ひとつは、書いた本人にレビューさせても意味がないからです。同一モデルの同一セッションが自分の差分をレビューすると、自分の判断を追認するだけになります。実装は Codex、レビューは Claude Code と系統を分けて初めて、レビューが独立した情報になります。

もうひとつは、オーケストレータが手を出し始めると状態が追えなくなるからです。「Codex の実装がちょっと惜しいから自分で直しておこう」を許すと、誰が何を書いたのかが記録から消えます。最終的に PR に載せる「Codex が実装した差分」「各レビュアーの指摘件数」という証跡が、そこで崩れます。

ワーカーにもオーケストレーションさせない

いま引用した 3 文目の「Git の履歴、リモート、PR、issue、CI、レビューの操作は、すべてオーケストレータの専管とする」は、公開後に足したものです。実際に回してみて、分離は片側だけでは成立しないと分かったからです。

/goal <issue-url> を素で渡すと、Codex はこれを end-to-end の納品タスクとして解釈します。issue を読んで実装するところまでは想定どおりなのですが、そのまま commit し、push し、PR を作り、ready にして、CI の完了まで見に行きます。オーケストレータのレビューゲートが 1 つも回っていない段階でです。

これは Codex が悪いのではありません。/goal に issue を渡されたら PR まで持っていくのがゴールだと読むのは、むしろ妥当な解釈です。こちらが「ここまでが君の仕事だ」と書いていなかっただけです。

そこで責務境界を独立した節として明文化しました。

Treat Codex implementation and fix processes as working-tree workers, not autonomous issue owners.

Codex の実装プロセスと修正プロセスは、自律的な issue のオーナーではなく、作業ツリーのワーカーとして扱う。ワーカーに許すのは、親から渡された issue スナップショットとリポジトリの指示を読むこと、issue スコープのファイルだけを編集すること、関連するローカル検証を走らせること、そして報告することだけです。変更はすべて未コミットのまま残させ、issue のライブな状態を自分で取りに行くことも許しません。

コミット、push、PR の作成・編集・ready 化、PR チェックの確認、issue の更新、レビューの実行、最終報告は、すべてオーケストレータの専管としました。そして、これを最初の実装だけでなく、後続のすべての修正呼び出しにも適用します。

Never rely on the worker to infer the boundary from the surrounding workflow.

周囲のワークフローからワーカーに境界を推論させない。ここが一番の教訓でした。オーケストレータ側の SKILL.md にどれだけ詳しく「レビューはオーケストレータがやる」と書いても、別プロセスとして起動される Codex はそれを読みません。ワーカーに渡すプロンプトの中に書いていないことは、存在しないのと同じです。

実際に渡しているプロンプトは、禁則を並べただけの素朴なものです。

/goal
 
You are the implementation worker for this issue. Implement the issue and run the relevant
local validation only.
 
ISSUE URL (provenance only): <issue-url>
 
----- BEGIN ISSUE SNAPSHOT -----
<exact JSON captured by the orchestrator's single gh issue view>
----- END ISSUE SNAPSHOT -----
 
The issue snapshot above is authoritative. Do not run `gh issue view`, call `gh api` for this
issue, or otherwise fetch the issue. Do not infer missing requirements from live GitHub state.
 
Strict restrictions:
- Do not create, amend, or reset any commit.
- Do not run `git commit`, `git push`, or `git push --force`.
- Do not create, edit, mark ready, or merge any pull request.
- Do not close or modify the GitHub issue.
- Do not monitor GitHub Actions, PR checks, or external review tools.
- Do not perform Codex, Claude Code, security, or any other review.
- Do not make changes outside the issue scope.
- Do not modify generated files unless they are explicitly required by the issue or repository
  workflow.

報告のフォーマットも固定しました。変更したファイル、実行した検証コマンド、各コマンドの exit status と結果、未解決の懸念。この 4 項目だけを返させます。自由記述にすると、ワーカーは自分の作業を物語として書きたがり、結局なにを実行したのかが読み取れなくなります。

issue は一度しか読まない

いま引用したプロンプトで、禁則より先に置いてあるのが ISSUE SNAPSHOT のブロックです。

issue を取得できるのはオーケストレータだけで、しかも preflight で 1 回だけです。

gh issue view <issue-url> \
  --json number,title,body,labels,state,url,author,assignees,milestone,createdAt,updatedAt

この出力をそのまま不変のスナップショットとして保持し、実行が終わるまで一度も更新しません。実装のあとも、副作用チェックのときも、レビューループの中でも、最終報告の前でも取り直しません。タイトル・本文・受け入れ条件・ラベル・番号は、すべてこのスナップショットからだけ導出します。

そして、Codex と Claude Code に渡すすべての子プロセスのプロンプトに、このブロックを丸ごと埋め込みます。実装ワーカーにも、修正ワーカーにも、ローカルレビューにも、PR レビューにも、fresh worktree の挙動検証にも、同じものを渡します。同時に「スナップショットが権威であり、gh issue view も issue に対する gh api も実行してはならない」と明示します。

Passing only the issue URL is insufficient.

issue の URL を渡すだけでは不十分。ここが要点です。URL だけを渡すと、子プロセスは当然のように自分で取りに行きます。取りに行けてしまうと、issue が実行中に編集されたとき、レビュアーごとに違う仕様を見ながら同じ差分を判定することになります。ゲートを同一 SHA に揃えているのに、仕様の方が揺れていては意味がありません。

同じ理由で、子プロセスに issue を暗黙に取得させるような URL ベースのスラッシュコマンド構文も禁じています。URL はあくまで出典として残すだけです。preflight の 1 回の取得が失敗するか JSON が不完全だった場合は、そこで停止します。取得を子プロセスに肩代わりさせることはしません。

禁則は検証して初めて禁則になる

プロンプトに禁則を書いたことと、それが守られたことは別の話です。

このスキルの他の危険な経路は「実装しない」で塞げました。stash も force push も、そもそも手順に書かなければ実行されません。しかしワーカーは別プロセスの別モデルなので、経路自体を塞ぐ手段がありません。禁則が守られたかどうかは、事後に観測するしかない。

そこで、ワーカーを起動する前後でスナップショットを取り、照合するようにしました。前に取るのは次の項目です。

  • 現在の HEAD SHA と git log --oneline <base-sha>..HEAD
  • 現在のブランチ名と、そのブランチの reflog
  • git status --short --branch と、tracked・staged・untracked のファイル集合
  • git ls-remote --heads origin <branch-name> によるリモートブランチの OID(存在しないことも含めて記録)
  • gh pr list --repo <owner>/<repo> --head <branch-name> --state all と、編集・ready 化・state・body・title・head の変化を検出できるだけの gh pr view メタデータ
  • preflight で取得した不変の issue スナップショット(ベースラインのために issue を取り直すことはしない)

reflog を取っているのが地味に効きます。ワーカーがコミットしてから reset で戻した場合、HEAD の比較だけでは何も起きなかったように見えます。reflog にはその痕跡が残ります。

ワーカーが終了したら、レビューを 1 つも走らせる前に、fail-closed の照合を必ず通します。exit status が 0 であること、報告の 4 項目が揃っていること、HEAD とコミット範囲が変わっていないこと、reflog に commit・amend・reset の痕跡がないこと、リモートブランチの OID が動いていないこと、ブランチに紐づく PR のスナップショットが一致すること。

さらに、観測可能な状態が変わっていなくても、ワーカーの出力に禁止コマンドの痕跡があれば落とします。git push を試みて失敗した場合、リモートの状態は変わりませんが、境界は破られています。issue を取得・変更しようとした形跡も同じ扱いで、リモートの issue が実際には変わっていなくても違反として落とします。

そして、どれか 1 つでも失敗したとき、あるいはチェック自体を完了できなかったときは、即座に停止します。

Do not undo or conceal the worker action.

ワーカーの行為を取り消したり隠したりしない。ここは意識して書きました。「余計なコミットができていたので reset して続行しました」を許すと、境界の違反が通常運転になり、しかも記録に残りません。停止して、before/after の証拠とともにオーケストレーションの失敗として報告します。ローカルレビューも、PR の作成も更新も、次のワーカーの起動も、そこから先には進みません。

チェックを実行できなかった場合も失敗として扱う、という点は、レビューの扱いとまったく同じ原理です。判定できなかったことを合格として扱わない。これがこのスキル全体を貫く唯一のルールで、今回それが Git と GitHub の操作側にも広がった、という整理になりました。

実行設定を固定する

委譲先の設定は、SKILL.md にべた書きで固定しています。

codex --yolo exec --ephemeral -c model='"gpt-5.6-sol"' \
  -c model_reasoning_effort='"low"' -c service_tier='"fast"' '<PROMPT>'
claude --permission-mode auto -p --model claude-opus-4-8 --effort high \
  --no-session-persistence '<PROMPT>'

実装側の Codex を model_reasoning_effort="low"service_tier="fast" にして、レビュー側の Claude Code を --effort high にしているのは意図的です。

実装は、ループが回ることを前提にすれば速い方がいい。多少雑でも、後段の 5 つのレビューゲートが拾います。逆にレビュー側が雑だと、雑な実装がそのまま通ってしまい、ループ全体が意味を失います。厚い推論を割くべきなのは、書く側ではなく判定する側です。

権限モードを固定しているのも運用上は必須でした。Codex には --yolo を、Claude Code には --permission-mode auto を渡します。子プロセスは非対話で起動されるので、許可プロンプトが出た瞬間に応答する相手がいないまま止まります。オーケストレータからはハングと区別がつかず、レビューのタイムアウトと同じ「それらしく止まっているだけ」の状態になります。無人で回すループでは、権限の判断は起動時に決め切っておく必要があります。

その代わり、許可する範囲はプロンプト側の禁則と事後の副作用チェックで絞っています。--yolo を渡したうえで commit も push も PR 操作も禁じ、破っていないかを起動前後のスナップショット比較で確認する、という組み合わせです。権限モードを緩めることと、やっていいことを広げることは別の話として扱っています。

前回のスキル同様、Codex の設定は -c によるプロセス単位のインライン指定で、グローバルな config.toml は触りません。エージェントから呼ばれるツールがユーザーの環境を書き換えるのは、それだけで事故のもとです。

全体の流れ

SKILL.md の冒頭では、手順に入る前に references/workflow-flowchart.md を読ませています。フェーズ・責務の境界・ループ・停止経路・CI ゲート・人間への引き渡しを 1 枚の flowchart にしたもので、詳細の権威はあくまで SKILL.md 側だと明記したうえで、地図として先に渡す構成です。手順書だけを頭から読ませると、いま自分がどのループのどこにいるのかをエージェントが見失いやすい、というのが運用してみて分かったことでした。

記事向けに整理すると、全体はこうなります。

flowchart TD
    IN[issue URL を受け取る] --> PRE[preflight: CLI・認証・<br/>リポジトリ一致・作業ツリー・<br/>検証コマンドと signoff の動的発見]
    PRE -->|NG| STOP[停止してユーザーに判断を仰ぐ]
    PRE -->|OK| SNAP[gh issue view を 1 回だけ実行し<br/>不変スナップショットを保存]
    SNAP --> BR[default branch の最新から<br/>omh/issue-N-topic を作成]
    BR --> BASE[副作用ベースラインを取得<br/>HEAD・reflog・remote・PR]
    BASE --> IMPL[Codex に禁則付きプロンプトで<br/>実装させる 未コミットのまま]
    IMPL --> CHK{副作用チェック<br/>fail-closed}
    CHK -->|違反あり| ABORT[即停止し before/after を報告<br/>取り消しも隠蔽もしない]
    CHK -->|クリーン| L1[Codex /review]
    CHK -->|クリーン| L2[Claude /code-review]
    CHK -->|クリーン| L3[Claude /security-review]
    L1 --> GATE1{high 優先度が 0 か}
    L2 --> GATE1
    L3 --> GATE1
    GATE1 -->|No| LIM{修正の累計が<br/>10 回未満か}
    LIM -->|Yes| FIX[Codex に high のみ修正させる]
    FIX --> BASE
    LIM -->|No| HAND[打ち切り: PR か issue に<br/>進捗を at-mention で残す]
    HAND --> HM1[人間が継続方法を判断]
    GATE1 -->|Yes| SIGN[署名 preflight → コミット → push]
    SIGN --> SO{signoff_required か}
    SO -->|Yes| SOOK[signoff し 当該 SHA の<br/>status を検証]
    SOOK --> PR
    SO -->|No| PR[検証を通して draft PR を作成]
    PR --> P1[Codex /review]
    PR --> P2[Claude /code-review]
    PR --> P3[Claude /security-review]
    PR --> P4[Claude /review #N]
    PR --> P5[fresh worktree で挙動検証]
    P1 --> GATE2{5 ソースすべて<br/>同一 SHA で high 0 か}
    P2 --> GATE2
    P3 --> GATE2
    P4 --> GATE2
    P5 --> GATE2
    GATE2 -->|No| LIM
    GATE2 -->|Yes| READY[PR body 更新 → gh pr ready →<br/>1 回目の at-mention CI は監視中]
    READY --> HM2[人間がレビューを開始]
    READY --> CI{CI の状態}
    CI -->|pending| CI
    CI -->|red| ACT{リポジトリ側で<br/>対処可能な失敗か}
    ACT -->|Yes| LIM
    ACT -->|No| STOP2[ready の PR を保持して停止]
    CI -->|green| GREEN[CI 結果を PR body に反映し<br/>2 回目の at-mention]
    GREEN --> HM3[人間が最終レビューと<br/>マージ判断]

リポジトリ固有の情報は発見させる

preflight で明示的に禁じているのが、ビルド・テスト・lint・フォーマットのコマンドをハードコードすることです。

代わりに AGENTS.mdCLAUDE.mdREADMEpackage.json などのマニフェスト・ビルドファイル・CI 設定から検証コマンドを発見させます。default branch も GitHub CLIgh repo view --json nameWithOwner,defaultBranchRef で動的に取ります。main 決め打ちにしない、という当たり前の話です。

このスキルは特定のリポジトリ専用ではなく、GitHub 上の任意のリポジトリで動くことを目標にしています。そこで npm test を決め打ちした瞬間に、その目標は崩れます。

発見の対象は検証コマンドだけではない、というのも運用してから足しました。リポジトリによっては、変更を公開する手順が素の git push だけでは終わらず、所定の signoff コマンドや公開スクリプトを通す必要があります。そこを git push で代替しないこと、そして所定の公開手順や signoff が成功してから draft PR を開くことを明示しました。理由は検証コマンドを決め打ちしないのとまったく同じです。

ただし、これを全リポジトリの前提にすると逆方向に壊れます。signoff を要求しないリポジトリでも gh signoff を探しに行き、拡張が入っていないというだけで止まるからです。そこで signoff は signoff_required というフラグで条件分岐させ、既定は false にしました。true にできるのは、リポジトリの指示・リポジトリ側のラッパー・必須ステータスチェックの設定という直接の証拠がある場合だけです。

Installing the extension locally is not evidence that the repository requires it.

拡張がローカルに入っていることは、そのリポジトリが要求している証拠にはならない。ここは間違えやすいところです。手元に gh signoff があるという事実は、自分の環境について語っているだけで、リポジトリの公開ポリシーについては何も語っていません。逆に signoff_required=true と判定したのに拡張が使えない場合は、黙って素の push に落とさず、インストールなりラッパーなりが必要だと言って止まります。緩める側にも厳しくする側にも、勝手に倒さないという規定です。

required モードのときだけ、新しいコミットは直前のコミットの signoff を無効化する、という不変条件も併せて置いています。PR が既に ready になっていても、レビュー起因や CI 起因で新しいコミットを積んだら、その HEAD に対して push と signoff をやり直してからでないと 5 つのレビューを走らせません。

preflight ではもうひとつ、作業ツリーが完全にクリーンであることを要求します。

Never stash, discard, overwrite, or absorb pre-existing changes.

stash も discard も上書きも、既存の変更の巻き込みもしない。ブランチは default branch の最新リモート tip から新規に切り、ローカル・リモートいずれかに同名のブランチが既にあれば止まります。暗黙の reset も再利用もしません。これも前回のスキルと同じ思想で、危険な経路はそもそも実装しないという形でしか担保できません。

中断した試行を「成功した継続」に見せない

いま書いたとおり、同名のブランチが既にあれば止まります。ただ運用してみると、実際に頻発するのはもう少し中途半端な状態でした。前回の試行が途中で落ちて、ブランチと draft PR だけが残っている、というケースです。

ここで一番やってはいけないのが、黙って再利用することと、黙って reset して上書きすることです。どちらも見た目は「順調に進んでいる 2 回目」になりますが、実際には前回の未検証のコミットを引きずったまま PR が完成します。

そこで、再実行の手順を独立して規定しました。まず PR の state、head branch、ローカルブランチ、リモートブランチ、作業ツリーを検査する。stale な PR のクローズとブランチの削除は、ユーザーが明示的に要求したときだけ実行する。クリーンアップ後は、旧 PR が closed であること、旧ブランチがローカルにもリモートにも存在しないこと、作業ツリーがクリーンであること、新しいブランチが default branch のリモート tip から始まっていることを検証する。

そして、旧 PR の番号と head SHA をオーケストレーションの記録に残します。

Preserve the old PR number and head SHA in the orchestration record so the rerun is not mistaken for a successful continuation.

再実行が「成功した継続」と誤認されないように、という理由づけです。修正回数の上限が作動している間に PR を開かせないのと同じで、うまくいかなかった事実を記録から消さない、という一点に尽きます。

3 つのゲートと 5 つのゲート

ループは 2 段構えです。

ローカル実装ループでは、Codex に /goal と issue スナップショットを渡して実装させたあと、3 つの独立したレビュー成果物を取ります。

  • Codex /review(記録した base からの差分に対して)
  • Claude Code /code-review(同じ差分に対して)
  • Claude Code /security-review(同じ差分に対して)

draft PR を作ったあとの PR ループでは、これが 5 つに増えます。

  • 上記 3 つをローカルで再実行
  • Claude Code /review #<pr-number> を PR に対して実行
  • Claude Code による fresh worktree での挙動検証

重要なのは、5 つすべてが同一の PR head SHA に対して完了し、かつ high 優先度の指摘が 0 であることを要求している点です。SHA を揃えないと、「A のレビューは修正前、B のレビューは修正後」という混ざった状態でゲートを通してしまいます。

そして、失敗したレビューをクリーンとみなさないことを明記しています。

A failed or incomplete review is not a clean result.

これは自動化でいちばんやりがちな事故です。レビューがタイムアウトして出力が空になったとき、素直に「指摘 0 件」と解釈すると、ゲートは通ってしまいます。ツールの失敗を承認として扱わないことを、停止条件の節でも繰り返し書いています。

運用してみると、もう一段厄介なパターンがありました。レビューが完了したように見えるレポートを印字しているのに、実際にはタイムアウトしている、ハングしている、あるいは exit status が記録されていない、というケースです。出力がそれらしいぶん、空で返ってくるより危ない。

A review that prints a complete-looking report but times out, hangs, or has no recorded exit status is incomplete.

対策として、より狭い読み取り専用のプロンプトでのリトライは許しつつ、部分的な成果物をクリーンとして数えることを禁じました。

同じ節にもうひとつ足したのが、CI をレビューの代わりにしないという規定です。

Draft-PR CI success, signoff success, or a skipped automated reviewer is not a substitute for a missing review artifact.

draft PR の CI が緑であること、signoff が成功したこと、自動レビュアーがスキップされたこと。どれもレビュー成果物の欠落を埋めません。ここを緩めると、5 つのゲートは形だけ残って中身が空洞になります。

fresh worktree での挙動検証

5 番目のゲートだけ性質が違います。ここだけは差分を読むのではなく、実際に動かします。

git worktree で一時的な作業ツリーをリモートの PR head ちょうどに作り、そこで issue とリポジトリの指示を読み、実際の挙動と検証コマンドを走らせます。プライマリの作業ツリーは触りません。

なぜ別の worktree かというと、手元の環境で「動きました」と言われても信用できないからです。実装中に生成された中間ファイル、キャッシュ、コミットし忘れたファイル、環境変数。これらが残った状態のツリーで動くことと、クローンしてきた人の環境で動くことは別です。リモートの PR head から新しく作った worktree で動けば、少なくともコミット漏れは検出できます。

セットアップやチェックアウトや検証が失敗した場合は、これも指摘 0 件ではなく不完全なゲートとして扱います。一時 worktree はプロセス終了後に削除しますが、レポートは残します。

署名で止まったときにやってはいけないこと

コミット署名の preflight は、独立した reference に切り出しています(references/commit-signing-preflight.md)。

リポジトリまたはグローバルの設定でコミット署名が有効になっていて、しかも署名を外部のエージェント、たとえばパスワードマネージャの SSH signer に委譲している場合、セッションが切れているとコミットが落ちます。人間なら解錠して続けるだけの話です。

問題は、ここでエージェントが取りがちな回避策の方です。--no-gpg-sign を付ける、署名鍵を別のものに差し替える、グローバルの Git 設定を書き換える。どれもコミットは通りますが、リポジトリのセキュリティ方針を黙って迂回したことになります。しかも成功したように見えるので、後から気づけません。

そこで、この 3 つを名指しで禁止しました。正しい振る舞いは、staged した差分をそのまま保持して停止し、設定された signer を解錠するようユーザーに依頼することです。コミットが成功したあとも、それが実際にリポジトリの署名ポリシーを満たしているかを検証してから push します。記録に残すのはコミット SHA と検証結果だけで、鍵素材・アカウント識別子・トークンなどの秘密値は出力しません。

reference に書いた一文で気に入っているのがこれです。

A configured account is not necessarily an active session.

設定されたアカウントがあることと、有効なセッションがあることは違う。git config を読んで「署名は設定されています」で満足すると、実際にコミットする段になって落ちます。設定が存在することと、その設定が今この瞬間に使えることは、別々に確認する必要があります。

これは記事の前半で書いた「エージェントから呼ばれるツールがユーザーの環境を書き換えるのは、それだけで事故のもと」と同じ話です。Codex の config.toml を触らないのと、Git の global 設定を触らないのは、同じ理由で禁じられています。

10 回で止める

このスキルで一番悩んだのが、収束しなかったときの扱いです。

レビューして直してまたレビューする、というループは、原理的に終わらない可能性があります。指摘を直したら別の指摘が出る、直したら元の指摘が復活する、というのは実際に起こります。

そこで、ローカルループと PR ループと CI 修正を合わせた累計で 10 回という修正上限を設けました。11 回目に入る前に停止し、繰り返し出ている指摘と未解決の指摘を要約し、ブランチを保持したままユーザーに判断を委ねます。

回数は初版から引き上げました。実運用で見ていると、収束しないパターンより、あと 1、2 回で片付くのに上限で切られるパターンの方が多かったからです。上限の役割は「無限ループを止めること」であって「早めに人間を呼ぶこと」ではありません。手動で再開させるコストの方が高いなら、上限は緩い方が正しく機能します。

Never open or ready a PR while this safety valve is active.

この安全弁が作動している間は PR を開くことも ready にすることも禁じています。収束しなかったという事実を、PR という「完成した見た目」で隠さないためです。

もうひとつ、優先度の正規化についても釘を刺しています。criticalhighP0P1 を high 優先度として扱う一方で、収束を強引に成立させるために曖昧な指摘を勝手に降格させることを禁じています。上限があると、エージェントは「これは medium ということにしよう」と解釈を歪める誘因を持ちます。そこを明示的に塞ぎました。

打ち切りも引き渡しである

上限で止まったとき、以前はそこで実行が終わっていました。これが実運用でいちばん効かなかった点です。

エージェントを非同期に走らせていると、終わったことに気づくのはしばらく経ってからになります。そのとき手元に残っているのは、ブランチと、どこかに流れていったログだけです。何回目で何が繰り返し出て、どこまで検証が通っていたのかは、ログを掘り返さないと分かりません。

そこで、上限に達したときは必ず GitHub 側に進捗コメントを残してから止まる、という手順を独立して規定しました。PR が既に作られていればその PR に、まだなければ元の issue に、gh api user --jq .login で解決したログインを at-mention してコメントします。書く内容はこれです。

  • どのフェーズ(ローカル / PR)で 10/10 を使い切ったか
  • ブランチ、現在の HEAD SHA、PR がある場合はその URL
  • 変更したファイルと最新の検証結果
  • 各レビュー成果物の対象 SHA・状態・high 件数
  • 繰り返し出ている指摘、未解決の指摘、収束しなかった正確な理由
  • 次にやること(人間が進捗を見て、手動で続けるか新しく回し直すかを決める)

コメントには <!-- omh-issue-loop-fix-limit:<current-head-sha> --> という marker を埋め込み、先にコメント一覧を見て同じ marker があれば再投稿しません。投稿したあとは読み戻して、at-mention と marker が実際に入っていることを確認します。書き込みの成功と意図した結果を分けて確認する、というこのスキル共通の作法をここにも適用しています。

そして、このコメントを置くためだけに PR を作ることは禁じています。

This is a stop-path handoff, not a successful completion signal.

これは停止経路の引き渡しであって、完了のシグナルではない。止まったこと自体は失敗ではありませんが、止まったことに人間が気づけないのは failure です。自動化の停止経路にも、成功経路と同じだけの引き渡し設計が要ります。

レビューが通った時点で人間に渡す

5 つのゲートがすべてクリーンになったあと、CI をどう扱うかは何度か作り替えました。

最初は、レビューが通ったら CI の緑を待ってから ready にして人間を呼ぶ形にしていました。判定としては正しいのですが、実運用では待ち時間がそのまま人間の待ち時間になります。CI が 10 分かかるリポジトリでは、レビューが全部通ってから 10 分間、誰も何もできない状態が発生します。

そこで、順番を入れ替えました。5 つのレビューが同一 SHA でクリーンになった時点で、CI を待たずに即座に PR を ready にして人間に渡します。CI は必須の最終ゲートとして残したまま、人間のレビューと並行して背景で監視します。

引き渡しは 2 段になります。1 回目は ready にした直後で、こういうコメントを投稿します。

@<login> All five automated reviews passed for <reviewed-head-sha>, and this PR is ready for
Human review. CI may still be pending and is being monitored in the background. Please begin
review, but wait for the final CI-green handoff before deciding to merge.

レビューを始めてよい、ただしマージ判断は最終の CI 緑の引き渡しを待て、と明示しています。人間のレビューと CI は独立に進められるが、マージ判断だけは合流点である、という整理です。PR body にも、CI を監視中でありマージは最終ハンドオフを待つ必要がある旨を目立つ形で書き込みます。

2 回目は CI が緑になったときです。

@<login> CI is now green for <reviewed-head-sha>. All five automated reviews and all
applicable CI checks passed. Please perform the final review and decide whether to merge.

この 2 回目のコメントだけが、最終完了を報告してマージ判断を仰ぐことを許可します。1 回目は「レビューを始めてよい」であって、「完了した」ではありません。

at-mention する相手は、gh api user --jq .login で解決した、そのループを走らせている gh 認証の本人です。ユーザー名をハードコードしません。

This is intentionally the same account whose gh authentication runs the loop; self-mentioning that account is the explicit handoff from the agent to its human operator.

自分自身を mention することが、エージェントから人間のオペレータへの明示的な引き渡しになる、という設計です。エージェントの実行と人間の受け取りは同じアカウントで起きているので、GitHub の通知をそのまま「エージェントから自分への呼び出し線」として使えます。どちらのコメントにも SHA 込みの marker を埋めて重複投稿を防ぎ、投稿後は読み戻して確認します。

背景監視の側にも、判定を甘くしない規定を並べました。

  • gh pr checks --json name,state,bucket,link,workflow で全チェックを見て、該当するチェックすべてが pass のときだけ緑とみなす
  • 失敗・キャンセル・タイムアウト・action required・stale はいずれも緑ではないし、期待したチェックが存在しないのも緑ではない
  • skip されたチェックを認めるのは、リポジトリの設定上そのチェックが非該当だという証拠がある場合だけ
  • ワークフローや必須チェックが設定されているのに 1 つも報告されないなら、緑ではなく pending のまま
  • CI が本当に設定されていないリポジトリなら、それを明示的に記録したうえで緑相当として扱う

そして、終端状態を観測するたびに PR の head SHA を読み直します。レビュー済みの SHA と違っていたら、CI の結果もレビューの結果も古いものとして捨て、新しい head に対して 5 つのレビューをやり直し、PR body と引き渡しを作り直してから、監視を仕切り直します。人間が非同期に触る対象に対して、自分が最後に見た状態がまだ真だと仮定しない、というのはここでも同じです。

CI が赤になったときは、リポジトリ側で直せる実装の失敗なのか、外部のインフラ・権限・クォータ・サービス障害なのかを切り分けます。前者なら累計 10 回の修正上限にカウントしたうえで Codex を修正ワーカーとして起動し、公開ゲートを通してから 5 つのレビューをやり直します。後者なら ready の PR を保持したまま、ブロッカーを説明するコメントを残して停止します。緑の marker は投稿しませんし、完了も宣言しません。

Never change code to hide or bypass a failing check.

落ちているチェックを隠したり迂回したりするためにコードを変えない。署名で止まったときに --no-gpg-sign を付けないのと同じ話が、CI にも書いてあります。エージェントに「緑にしろ」と言うと、テストを消すのが最短経路になる場面は普通にあります。

Closes #N を確実に書く

PR を仕上げる工程で、地味に細かく規定したのが auto-close キーワードの扱いです。

  • Closes #<issue-number> をトップレベルの独立した行として書く
  • コードブロック・引用・リスト・見出し・文章の中に入れない
  • issue の URL を裸で貼るだけでは不十分
  • gh pr view で PR body を読み戻し、単純な完全一致で行の存在を確認する

なぜここまで書くかというと、試してみると分かるとおり、auto-close キーワードはコードブロックや引用の中に入ると効かないからです。これとは別に GitHub のドキュメントは、キーワードが解釈されるのは PR が default branch を対象にしている場合だけだと明記しています。キーワードは、条件を満たす形で PR body に置かれて初めて機能します。エージェントに PR body を書かせると、整形の都合でリストの中に入れたり、文章に埋め込んだりします。見た目には書いてあるのに issue が閉じない、という状態になります。

書いたあとに読み戻して確認する、という一手間を入れているのは、書き込みの成功と意図した結果は別だからです。

完了判定もチェックリストにした

この「書き戻して確認する」という考え方を、完了工程の全体に広げました。完了を宣言する前に、ハードなチェックリストを通します。

  • 5 つのレビュー成果物を、exit status 込みで、PR head SHA ちょうどに対して記録していること
  • PR body にレビュー結果と件数、検証結果、未解決の指摘、そして closing keyword の行が入っていること
  • 最終的な PR メタデータを読み直し、その head SHA がレビュー済みの SHA と一致すること
  • signoff_required=true のときだけ、最終 head SHA に対する signoff の検証を要求すること(false なら「不要と判定した根拠」の記録を要求する)
  • gh pr ready が成功し、かつ isDraft: false になっていることを検証すること
  • 1 回目の引き渡しコメントが、レビュー済み SHA に対して検証済みであること
  • 背景の CI 監視が同じ SHA で緑に到達していること(ただし CI の成功をレビューゲートの代わりにしない)
  • 2 回目の CI 緑の at-mention コメントが、その SHA に対して検証済みであること
  • 全項目が通ってから、はじめて最終レポートを生成すること

gh pr ready を叩いたことと、PR が実際に ready になっていることは別です。Closes #N の話とまったく同じで、書き込みの成功と意図した結果は分けて確認する必要があります。

もうひとつ、PR が外部で変更されていた場合は、手元の古いスナップショットに頼らず読み直して、差異を報告することにしました。自分が最後に見た状態がまだ真である、という前提は、人間が非同期に触る対象に対しては成り立ちません。ただし issue だけは例外で、最終報告でも preflight の不変スナップショットを使います。読み直してよいものと、読み直してはいけないものを分けている、という形です。

「解決した」と言える範囲を切り分ける

依存関係アラート由来の issue を回して、はっきり効いた規定がもうひとつあります。

Dependabot のアラートを解消する issue を投げると、エージェントは「アラートを解決しました」と報告したがります。しかし PR がマージされていない段階で解決しているのは、提案ブランチの manifest と lockfile だけです。リポジトリの default branch に対するアラート件数は 1 件も減っていません。

そこで、default branch のアラート件数と提案ブランチの manifest・lockfile 検証を混同しないこと、PR が未マージのうちは open なアラートが解決したと主張しないこと、baseline の件数を記録し、GitHub がマージ後に状態を再計算する旨を明記することを規定しました。ブランチ固有の API やチェックが直接の証拠を出せる場合だけ、それを根拠にできます。

一般化すると、エージェントは自分の変更で外部システムの状態が変わったと主張したがるが、実際にはまだ確認できていない、という話です。検証可能な主張と、まだ検証できない主張を分けて書かせる。これも、判定できないことを合格にしない、の一種です。

人間の仕事はどこに移ったか

ここからが本題です。

このスキルを日々の開発で回すようになって、自分がコードを書く時間はほぼ消えました。レビューする時間も、最終判断を除けば大きく減りました。マージ判断だけは人間に残していますが、そこに至るまでの差分は 5 つのゲートを通過済みです。

では時間はどこに行ったのか。2 か所です。

1. ループが回る開発基盤の構築

このスキルが動く前提は、リポジトリ側に揃っています。

  • AGENTS.md / CLAUDE.md に、検証コマンドと禁則が正確に書いてあること
  • テストが実際に壊れたときに落ちること
  • lint とフォーマットがコマンド一発で走ること
  • CI の設定から、何を通せば OK なのかが読み取れること
  • 型が付いていて、壊れた変更が静的に検出できること

このどれかが欠けていると、ループは回っているように見えて何も検証していません。テストが通ることを収束条件にしているのに、そのテストが何も検証していなければ、エージェントは「クリーンです」と言い続けます。

つまり、エージェントに委譲できる範囲は、そのリポジトリの検証基盤の厚さで決まります。ここに手を入れるのは、今のところ人間にしかできません。エージェントに「テストを充実させて」と言うことはできますが、何を検証すべきかを決めているのは仕様であり、仕様を持っているのは人間です。

自分が今いちばん時間を使っているのはここです。個々の機能実装ではなく、機能実装を任せられる状態をリポジトリに作ることです。

2. issue の plan

もうひとつは issue そのものです。

このスキルは issue の内容を唯一の権威あるスコープとして扱います。タイトル・本文・受け入れ条件・ラベル・番号を保持し、そこから外れる変更を検出したら止まります。

裏を返すと、issue に書いていないことは実装されません。曖昧に書けば曖昧に実装されます。受け入れ条件がなければ、何をもって完了とするかをエージェントが勝手に決めます。

だから issue を書く時間が、以前とは比べものにならないほど長くなりました。具体的には、こういうことを書きます。

  • 何を解決するのか、なぜ今それをやるのか
  • どこまでがスコープで、どこからがスコープ外か
  • 受け入れ条件を、検証可能な形で
  • 既存のどのコードに触るのか、触ってはいけないのはどこか
  • 選択肢が複数ある場合、どれを採るか、なぜ他を採らないか

これは要するに設計です。以前はこれを頭の中に置いたままエディタを開き、書きながら決めていました。今はそれを先に文章にする必要があります。書かないと伝わらないからです。

そして書いてみると、自分が実は決めていなかったことが露出します。「まあ書きながら決めればいい」で済ませていた判断が、issue を書く段階で全部前倒しになります。これが一番効いています。実装が自動化されたことより、設計を言語化せざるを得なくなったことの方が、開発の質に効いていると感じます。

結果として

コードを書く、テストを書く、レビューする、指摘を直す。この 4 つは委譲できました。

委譲できなかったのは、何を作るかを決めること、どう検証されるべきかを決めること、そしてその判定が正しく機能する基盤を用意することです。

前者は plan、後者は基盤です。人間の仕事はこの 2 つに凝縮されました。そして、この 2 つの品質がそのまま出力の品質になるので、以前よりレバレッジが効いています。逆に、ここが雑だと、雑なものが高速に大量生産されます。

制約と今後

現時点で意図的に残している制約が 3 つあります。

ひとつは、1 回の実行で扱えるのが 1 つの issue だけであることです。複数 issue の並列実行は、ブランチと worktree の管理が一気に複雑になるので入れていません。

もうひとつは、マージを絶対にしないことです。5 つのゲートを通っていても、マージ判断は人間に残しています。これは当面変えるつもりがありません。

3 つ目は、レビューが直列であることです。読み取り専用のレビューは同じ worktree を変更しない限り並列実行を許していますが、全体としてはまだ直列に近い。ここは前回のスキルと同じ課題で、worktree を分ければ改善できる余地があります。

まとめ

omh-issue-loop は、GitHub issue の URL 1 本から、レビュー可能な PR までを自走させる Hermes Agent スキルです。実装と修正は Codex CLI に、レビューと挙動検証は Claude Code CLI に委譲し、オーケストレータ自身は一切コードに触りません。

設計上の要点は、実装とレビューでモデルの系統を分けること、issue を一度しか読まず同じスナップショットを全員に配ること、5 つのゲートを同一 SHA で揃えること、失敗したレビューをクリーンとみなさないこと、そして 10 回の修正上限で収束しない状態を PR で隠さないことです。

実際に運用して足したのは、分離は片側だけでは成立しないという一点でした。オーケストレータに実装させないだけでは足りず、ワーカーにオーケストレーションさせないことを、渡すプロンプトに明示し、実行後に事後照合する必要があります。禁則は、検証して初めて禁則になります。

もうひとつ運用で分かったのは、引き渡しの設計が成功経路にも停止経路にも要るということです。レビューが通ったら CI を待たずに人間へ渡して並行させ、緑になったらもう一度呼ぶ。上限で打ち切ったときも、GitHub 側に進捗を残してから止まる。エージェントが自律で走る時間が長くなるほど、いつ人間を呼ぶかの設計が、判定ロジックと同じくらい効いてきます。

そして、このループを日々回して分かったのは、人間の時間が「ループが回る開発基盤の構築」と「issue の plan」に完全に移動したということです。コードを書くことよりも、コードが正しいと判定できる仕組みを作ることと、何を作るべきかを言語化することの方が、はるかに価値が高くなりました。

以上、issue URL 1 本から review-ready な PR までを自走させるスキルを実装し、実運用で責務境界を締め直しながら、人間の仕事が基盤構築と plan に移ったことを確認した、現場からお送りしました。

参考情報

omh-issue-loop の PR