AIエージェント開発・運用の事故録 #06
CLOSEDは
MERGEDではない
「対策済み」を5層のEvidence ladderで確かめる
公開日:2026年8月25日 著者:飯田 友広(Tomohiro Iida)
この記事の結論
Pull RequestがCLOSEDであることは、その変更がmainへ入った証拠になりません。
PR state、mergedAtとmerge commit、main ancestry、取り込み、必要な挙動の5層をEvidence ladderとして分けます。
別PRによる置き換えは、元のPRをmergedと書き換えず、別の状態として検証済み証拠を要求します。
層を飛ばした「対策済み」は証拠として扱わず、どの層まで確認したかを報告に明示します。
AIエージェントに実装を任せると、進捗はPull Requestの一覧で見ることになります。閉じたPRが並び、未対応の課題が減っていくように見えます。
ところが、GitHubのPull RequestがCLOSEDであることは、その変更がmainへ入ったことを意味しません。mergeされて閉じたPRも、mergeされずに閉じたPRも、一覧の上では同じ「閉じたPR」に見えます。
2026-08-25に当社リポジトリで実測したところ、番号#1423から#1550までのclosed Pull Request 57件のうち、stateがCLOSEDでmergedAtがnullのものは15件ありました。この15件はいずれも、そのPR自身からはmainへ1行も入っていません。
母集団を「直近60件」ではなく固定の番号範囲で示すのは、移動窓では同じ数字が再現しないためです。同じ日に時間をあけて--limit 60を2回実行したところ、新しいmergeが進んだぶん窓がずれ、対象の集合が変わりました。公開する実測値は、あとから同じ範囲を指定して数え直せる形にします。
結論を先に書きます。
「対策済み」は、Pull Requestの開閉ではなく5層のEvidence ladderで確かめます。
PR state、mergedAtとmerge commit、main ancestry、patchまたは意味的な取り込み、そして必要な挙動の存在。この5つは別々の状態であり、上の層が満たされても下の層は自動的には満たされません。
Incident Card
INCIDENT:
CLOSEDのまま残ったPull Requestを「対策済み」として扱う判断が、AIエージェント運用の中で起こりうる状態になっていた
SYMPTOM:
PR一覧では課題が閉じているのに、mainにも本番にも該当の変更が入っていない
FALSE_ASSUMPTION:
Pull Requestが閉じている以上、その中身はmainへ取り込まれている
ROOT_CAUSE:
Pull Requestのライフサイクル(open / closed)と、mainの内容(変更が存在するか)を同一の状態として扱っていた
IMMEDIATE_FIX:
stateとmergedAtを別々に取得し、mergedAtがnullなら「そのPRからは届いていない」と確定させる
SYSTEM_FIX:
PR state、merge commit、main ancestry、取り込み、必要な挙動の5層をEvidence ladderとして定義し、層ごとに別の証拠を要求する
REMAINING_RISK:
別PRによる意味的な取り込みは自動判定できず、検証済みの証拠を人が別途与える必要がある「対策済み」という一語の中に、5つの状態が入っている
「あの不具合は対策済みです」という報告は、次のどれを指しているのかを含んでいません。
- 修正を書いてPull Requestを出した
- そのPull Requestを閉じた
- そのPull Requestをmergeした
- 別のPull Requestが同じ内容を取り込んだ
- 現在のmainにその変更が存在する
- 本番でその挙動が実際に直っている
AIエージェントの報告文だけを読むと、この6つは1つに見えます。GitHubとGitの側では、6つとも独立に成立したり不成立だったりします。
実際のPR状態を見ると分かりやすくなります。次はいずれも2026-08-25にリポジトリで実測した値です。
| PR | state | mergedAt | 意味 |
|---|---|---|---|
| #1173 | CLOSED | null | このPRからmainへは入っていない |
| #1207 | CLOSED | null | 同上 |
| #1425 | CLOSED | null | タイトルに「superseded by #1437」と明記 |
| #1437 | MERGED | 2026-08-24 | 実際にmainへ入ったのはこちら |
#1425だけを見ると「閉じたので対応済み」と読めますが、そのPRからmainへは1行も届いていません。変更を届けたのは#1437です。逆に#1173と#1207は、閉じただけで別PRによる置き換えも自動では保証されていません。
誤った前提:閉じたPull Requestは、取り込まれている
この前提が壊れる場面は、少なくとも4種類あります。
- close — 方針変更、要件消滅、budget切れで、mergeせずに閉じた
- superseded — 同じ意図を持つ別PRへ引き継ぎ、元のPRを閉じた
- 別PR統合 — 複数PRを1本へまとめ、個々のPRは閉じた
- 手動移植 — 手元でcherry-pickや書き直しを行い、元のPRは閉じた
1はmainに何も入りません。2から4はmainに何かが入りますが、入ったものが元のPRの内容と同じとは限りません。範囲が狭まっている、条件が変わっている、テストだけ落とされている、といった差が起こります。
「閉じた」という1ビットの情報では、この4つを区別できません。
根本原因:PRのライフサイクルと、mainの内容を同一視していた
原因はAIの判断力ではなく、状態モデルの設計です。Pull Requestは作業の入れ物のライフサイクルを表します。mainはコードの内容を表します。この2つを1つの「完了」という語に押し込むと、どちらの意味で完了なのかを誰も特定できなくなります。
AIエージェントを使うと、この構造的な問題が表に出やすくなります。会話は自然言語で進み、報告は要約され、PR番号だけが引き継がれるためです。
その場の対処:stateとmergedAtを分けて取得する
最初の対処は1コマンドで済みます。
gh pr view <N> --json state,mergedAt,title
state = "CLOSED" かつ mergedAt = null
→ このPRからmainへは届いていない(PR_NOT_DELIVERED)
state = "MERGED" かつ mergedAt != null
→ このPRはmergeされた(次の層へ進む)stateだけを読んでも判定できません。MERGEDもGitHubの内部表現では閉じたPRだからです。判定に使うのはmergedAtとmerge commitです。
仕組みへの変換:Evidence ladderを5層で定義する
当社のリポジトリでは、この判定をscripts/agent-os/delivery-truth.mjsのDelivery Truth Gateへ移しています。層は次の順に上がります。下の層を飛ばして上の層を主張しません。
| 層 | 確かめること | 取得方法 |
|---|---|---|
| layer1 PR state | openか、closedか、mergedか | gh pr view --json state |
| layer2 merge commit | mergedAtとmerge commitが存在するか | gh pr view --json mergedAt,mergeCommit |
| layer3 main ancestry | そのcommitが現在のmainから到達可能か | git merge-base --is-ancestor またはcompare API |
| layer4 patch inclusion | 意図した差分または等価な変更がmainにあるか | 差分の再取得と検証済み証拠 |
| layer5 behavior | 必要な挙動が実際に存在するか | テスト・実行・本番観測 |
Delivery Truth Gateは、この層を状態コードとして返します。
CLOSED_NOT_MERGED… 閉じたが届いていない。prDeliveredはfalseMERGED_NOT_TARGET_BASE… mergeされたが、対象branchはmainではないCANDIDATE_UNVERIFIED… mergeされたが、承認済み候補と同一と確認できていないMAIN_CHANGE_UNVERIFIED… mainに変更があるか未検証EQUIVALENT_CHANGE_ON_MAIN… 別経路の等価な変更がmainにある
supersededを「mergeされた」と書き換えない
4つ目のコードが要点です。閉じたPRの意図が別PRで満たされている場合でも、prDeliveredはfalseのまま、changeSourceはSEMANTIC_EQUIVALENTになります。等価であるという判断は自動では成立せず、equivalentEvidenceVerifiedという別の検証済み証拠を明示的に渡した場合にだけ通ります。
if pr.state == CLOSED and pr.mergedAt == null:
if equivalent_evidence_verified:
state = EQUIVALENT_CHANGE_ON_MAIN
pr_delivered = false # ここをtrueに書き換えない
else:
state = CLOSED_NOT_MERGED
code = PR_NOT_DELIVERED「別PRで直っているので実質mergeです」と要約した瞬間に、どのPRのどの差分が入ったのかを後から追えなくなります。届いた事実と、届けた経路は分けて記録します。
mergeされていても、現在のmainにあるとは限らない
3層目も自動では満たされません。stacked PRの土台へmergeした場合、対象baseはmainではありません。mergeしたあとにrevertされた場合、merge commitは履歴にあっても内容は戻っています。そこでmerge後の判定では、merge commitが現在のmainから到達可能かを別途確認します。
同じ考え方はrollback判定にも入っています。scripts/deploy-policy/verify-rollback-target.mjsは、戻し先のcommitに対してgit merge-base --is-ancestorを実行し、origin/mainの履歴に無ければ「未マージのものへは戻さない」として失敗させます。
再利用できる確認手順
自組織へ移すときは、次の順で確かめます。層を飛ばさないことが要点です。
1. gh pr view <N> --json state,mergedAt,mergeCommit,baseRefName
mergedAt == null -> STOP: このPRからは届いていない
2. baseRefName == "main" -> falseならSTOP: 対象baseが違う
3. git merge-base --is-ancestor <mergeCommit> origin/main
非ゼロ終了 -> STOP: 現在のmainの履歴に無い
4. 意図した差分または等価物が現在のmainにあるかを再取得して確認
等価判定は検証済み証拠を要求する
5. 必要な挙動をテストまたは実観測で確認報告に書く語も、層に対応させて分けます。
| 書いてよい語 | 満たしている層 |
|---|---|
| PRを出した | 0(まだ届いていない) |
| PRを閉じた | 1 |
| mergeした | 2 |
| 現在のmainにある | 3 |
| 意図した変更が入っている | 4 |
| 対策済み | 5 |
Evidence
| 内容 | 分類 | 根拠 |
|---|---|---|
| #1423から#1550までのclosed PR 57件のうち15件がstate CLOSEDかつmergedAtがnull(2026-08-25実測) | OBSERVED | gh pr list --state closed --limit 300 --json number,state,mergedAtを番号範囲で絞って集計 |
移動窓(--limit 60)の件数は同日中でも再現しない | OBSERVED | 同日に2回実行し、対象集合が変化したことを確認 |
| PR #1173、#1207、#1423、#1425はいずれもCLOSEDでmergedAtがnull | OBSERVED | gh pr view <N> --json state,mergedAt(2026-08-25実測) |
| PR #1425のタイトルは「superseded by #1437」で、#1437は2026-08-24にMERGED | OBSERVED | gh pr view 1425 / 1437 --json title,state,mergedAt |
| CLOSEDかつ未mergeをPR_NOT_DELIVERED、prDelivered falseとして扱う | IMPLEMENTED | scripts/agent-os/delivery-truth.mjs |
| 等価な変更がmainにある場合も、mergeされたとは書き換えず別の証拠を要求する | IMPLEMENTED | scripts/agent-os/delivery-truth.mjsのequivalentEvidenceVerified |
| merge後はmerge commitが現在のmainから到達可能かを別途確認する | IMPLEMENTED | scripts/agent-os/delivery-truth.mjsのevaluateControllerPostMergeTruth |
main ancestryをgit merge-base --is-ancestorで確認し、履歴に無ければ失敗させる | IMPLEMENTED | scripts/deploy-policy/verify-rollback-target.mjs |
| 各判定は実際のPR番号を冠した回帰テストで固定されている | IMPLEMENTED | scripts/agent-os/__tests__/delivery-truth.test.mjs |
| Delivery TruthはController統合の終端ゲートとして実行される | IMPLEMENTED | .github/workflows/agent-integration-controller.yml |
| 5層の分離により誤った完了報告の発見が早くなる | INFERRED | 導入前後の件数比較は未計測です |
適用限界
- ここで扱うのはGitHubのPull Requestを前提にした判定です。他のホスティングでは、
mergedAtに相当する値と履歴到達性の確認手段を個別に確定させる必要があります。 - 4層目の「意味的な取り込み」は自動判定できません。当社の運用では、等価と判断した根拠を人が明示的に与えた場合にだけ通す設計にしています。これは当社の運用目安であり、普遍的な基準ではありません。
- 5層目の本番での挙動確認は、シリーズの別の回で扱います。本記事はmainに変更が存在するかどうかまでを主に扱います。
- 実測したPR状態は2026-08-25時点のものです。PRは後から再オープンされる場合があるため、判断のたびにlive stateを取り直します。
残るリスク
mergedAtが入っていても、その後にrevertされていれば現在のmainには変更がありません。層3と層4を省略できません。- 別PRによる取り込みは、範囲が狭まったまま「等価」と判断される場合があります。等価判定の根拠自体をレビュー対象にする必要があります。
- 手動移植は、元のPRのテストまで移植されたかどうかが記録に残りにくく、層5で初めて差が出ます。
- PR本文や過去の会話に書かれた状態は、live stateの代用になりません。判断のたびに再取得します。
まとめ
Pull Requestの開閉は、作業の入れ物のライフサイクルです。mainの内容は、コードの状態です。この2つを1つの「対策済み」に押し込むと、どちらの意味で完了なのかを誰も特定できなくなります。
PR state -> merge commit -> main ancestry -> 取り込み -> 挙動
下の層を飛ばして、上の層を主張しないCLOSEDはMERGEDではありません。閉じた事実は、届いた証拠ではありません。
よくある質問
PRがCLOSEDなら、どうやってmergeされたか分かりますか
gh pr view <N> --json state,mergedAtでmergedAtを見ます。MERGEDのPRも内部的には閉じたPRなので、stateだけでは区別できません。mergedAtがnullなら、そのPRからmainへは届いていません。
mergedAtが入っていれば、対策済みと言ってよいですか
まだ言えません。mergeした対象branchがmainとは限らず、merge後にrevertされている場合もあります。merge commitが現在のmainから到達可能かをgit merge-base --is-ancestorなどで確認してから次の層へ進みます。
別のPRで同じ修正が入った場合、元のPRはmerged扱いにしてよいですか
しません。当社の実装では、等価な変更がmainにある状態をEQUIVALENT_CHANGE_ON_MAINという別の状態として扱い、元のPRのprDeliveredはfalseのままにします。等価であるという判断には、別途検証済みの証拠を要求します。
AIが「対策済みです」と報告した場合、何を確認すればよいですか
どの層まで確かめたかを聞きます。PRを出しただけなのか、mergeまで確認したのか、現在のmainで差分を再取得したのか、挙動を実行したのかで意味が変わります。層を指定しない完了報告は、証拠として扱いません。
5層すべてを毎回確認する必要がありますか
変更のリスクによって変えます。当社では、認証・課金・データ削除・本番設定に触る変更は5層すべてを要求し、影響範囲の小さい変更は層3までで運用しています。判定に迷う場合は高い側へ倒します。
この記事の著者

飯田 友広
代表取締役
Netsujo株式会社 代表取締役。京都発のWeb3・AI実装スタートアップを2023年6月に創業。Webサイトを営業基盤として捉え、経営・営業・検索・生成AI・コンバージョン・計測を横断して課題と改善優先順位を整理する「Netsujo SIGNAL」を設計・運営。京都ビッグデータ活用プラットフォーム参画(小規模企業会員・ベンチャー)、同プラットフォーム発のワーキンググループ「Chain Up KYOTO」参画(2026-03-10)、IVS2026サイドイベント「なぜ京都でWeb3.0ビジネスなのか」を京都府庁旧議場で開催(2026-07-02・Netsujoとして主催、企画・登壇・運営/京都府デジタル政策推進課は共催)。京都美術工芸大学での講義・龍谷大学でのセミナー実績、ITコミュニティ「みやこでIT」(connpassメンバー614名・イベント167件・2019年2月から運営)運営。NPO法人NEMTUS理事、BAR KRYPTO運営。ソーシャル企業認証「S認証」認証企業(2026年2月認証・2026年4月公表)。技術領域はWeb3/ブロックチェーン/DID/NFT/生成AI/コミュニティ運営。
プロフィールを見るこの記事が向いている方
AIエージェントの完了報告とPull Request一覧で進捗を管理している開発責任者
閉じたPRと実際にmainへ入った変更の対応が追えなくなっているチーム
「対策済み」の定義を層で分け、報告と証拠を結び付けたい方
— 壁打ち相談
読者のよくある相談
記事を読んだ後に「自分の状況だとどう判断すべきか」を整理するための壁打ち相談を受け付けています。下記のような相談例が当てはまる方は、お気軽にご連絡ください。
Q. PRが閉じているのに、不具合が直っていない
stateとmergedAtを分けて取得し、mergedAtがnullなら未到達として確定させます。
Q. 別PRで直したはずだが、どの差分が入ったか追えない
等価な変更は別状態として記録し、元のPRをmerged扱いへ書き換えません。
Q. mergeしたのに現在のmainに変更が見当たらない
merge commitが現在のmainから到達可能かを履歴で確認します。
上記いずれかが該当する場合、初回30分の壁打ち相談で論点整理に対応します。記事に書ききれない個別事情を踏まえた判断材料が必要な段階こそ、壁打ちが活きやすいフェーズです。
関連するサービス
AI導入・開発運用設計のご相談
AI開発の完了判定を、5層のEvidenceで設計する
役割分離、CI、merge判定、デプロイ、本番Evidenceを一つの運用として設計します。