AIが書いたコードはAIが見る ── レビューが詰まらず、品質はむしろ上がる

19 min read

目次

  1. 連載一覧
  2. いきなり1ヶ月分の数字から
  3. 「AIが書くとレビューが詰まる」を構造で防ぐ
  4. 通説:レビューが新しいボトルネックになる
  5. cortexの答え:見る側もAIに渡す
  6. 自動レビューのシステム配置
  7. Event Relayにたどり着くまで(補足)
  8. 並列sub-agentではなく、1セッション順次レビューにしている理由
  9. 細かい運用上の制御
  10. 指摘の構造 ── タグと重要度
  11. タグ(観点)
  12. 重要度
  13. 実例:embeddingモデルをgemini-embedding-001からgemini-embedding-2に移行するPR
  14. ガイドラインを育てる ── AIが間違える瞬間を捕まえてルール側を直す
  15. 1. AIが「既存コードも同様だから」と言って降格させていた
  16. 2. AIの最終判定が3択あって、「コメントのみ」でPRが宙ぶらりんになっていた
  17. 3. チェック項目に重要度が書いてなくて、AIが毎回違う判定をしていた
  18. 4. 既存ガイドラインがAI特有の罠を拾えていなかった
  19. 5. AIが「基準そのもの」を緩めようとしてくる
  20. ガイドラインの育て方そのものがhuman-on-the-loop
  21. 自動修正 ── AIが修正してプッシュする仕組み
  22. 自動マージ + 並列デプロイ
  23. 数字で見る自動レビュー
  24. review-fix loopの深さ
  25. Critical / Majorのhit状況
  26. false positiveの実数
  27. 何が変わったか / Bridge to Part 4

みなさまこんにちは!エアークローゼットでCTOをしているです。

Part 1(総論)AIがPRレビューや障害対応を回している話を、Part 2(Product Graph)コード・docs・DB・インフラを1つのナレッジグラフに統合したcpgの話を書きました。

今回は自動レビュー ── PRをAIが見て、AIが直して、AIがマージするパイプラインの全フローです。AI開発で必ず話題になる「レビューが詰まる」「品質が下がる」という問題に対して、cortexの中ではそれらが構造的に起こりにくいパイプラインが組まれています。その仕組みを順に見ていきます。

連載一覧

# テーマ キーシーン 記事
1 総論:cortexのハーネス PRが無人マージ / 障害が気づく前に治っている ai-harness-intro
2 Product Graph(cpg) コード・ドキュメント・DB・インフラを1グラフに統合 cortex-product-graph
3 AI PRレビュー webhook → AIレビュー → 自動修正 → squash merge 本記事 ←現在地
4 Self-Healing + Observability + 自動lint追加 アラート → AI調査 → 修正PR + 新規lint/型gate → 自動再デプロイで同じ書き方を機械的に弾く cortex-self-healing
5 改修フェーズの民主化 業務要件を把握している人が本番に直接PR、ハーネスが品質を担保 cortex-non-engineer-prs
6 連載総括・最終章 根底にある思想(何を捨てて何を取ったか / なぜこの設計か)と失敗の振り返り cortex-philosophy

いきなり1ヶ月分の数字から

マージされたPR: 769本。

マージ時間中央値: 31分。

個別PRへの人レビュー: ほぼ0%。

これが、直近30日(4/21〜5/21)のcortexの「典型的な1ヶ月」です。

その769本すべてにAIレビュアーが最初に入り、review-fix loopが 平均10.8回 / 最大56回 回っている。5本に1本は10分以内、約半数が30分以内にマージされている。人がやっているのはレビュー結果を見て レビュープロンプトやガイドライン自体を調整する こと ── いわば human-in-the-loopではなく、human-on-the-loop。「個別の意思決定の中に人が混ざる」のではなく、「システム全体を上から見る役割」に役回りが変わっています。

直近30日の数字
マージされたPR 769本
AIレビュアー関与率 100%
平均レビュー回数 / PR 10.8回
最大レビュー回数 56回
個別PRへの人レビュー ほぼ0%
マージ時間中央値 31分
10分以内マージ 20%
30分以内マージ 49%

これが今のcortexの「典型的な1ヶ月」。

世間でよく聞く「AIで開発スピードを上げても結局レビューが詰まる」「AIが書いたコードは品質が下がる」という声は、cortexの中では構造的に起こりにくいパイプラインで受け止めています。ここから順に分解します。

「AIが書くとレビューが詰まる」を構造で防ぐ

通説:レビューが新しいボトルネックになる

AIで書く速度が上がるほど、書いたコードを見る側(レビュアー)の負荷が比例して増える。Anthropicの社内ブログ(How Anthropic teams use Claude Code)でも、書く側より見る側にボトルネックが移っていること、シニアエンジニアの仕事がコードを書くことよりAI出力を統合・レビューすることに寄ってきていること、が報告されています。

これはcortexでも実際に同じ現象が起きました。Claude Codeをフル稼働させた瞬間、書く速度は桁を変える勢いで跳ね上がる。一方で、PRを読んで承認する人間の時間はリニアにしか伸びない。レビュアー(=私)が休めば全体が止まる、という古典的な単一障害点。

cortexの答え:見る側もAIに渡す

Part 1 / Part 2で繰り返し書いた「ハーネスをどこまで広げるか」の問題で、cortexは迷わず「AIが書いたコードはAIが見る」に振り切った。人間が手元に残しているのは「プロンプトやガイドライン自体を直す」役回り。個別PRの中で意思決定をするのではなく、システム全体を上から見て調整する立ち位置です。

これが成立するためには、3つの条件が必要でした。

  1. AIに渡すコンテキストが十分であること

    通常のAIレビューは PRのdiffしか見ません。コード本体だけ見ても、ビジネス的な意味・上流下流の依存・過去の障害履歴は見えません。cortexではPart 2で書いた Product Graph(cpg)── コード・ドキュメント・DB・インフラを1つのグラフに統合し、ノードごとにビジネス的な役割と上流下流の依存を持たせた知識基盤 ── をAIレビュアーに渡しているので、PRで触っていない関連箇所まで含めて影響範囲を辿れます。結果として以下が構造的に検出されます:

    • 上流下流の修正漏れ
    • ドキュメント更新漏れ
    • 関連テストの未追従

    これはAIを使ったレビューでも、PR diffだけを見るやり方では絶対に届かない範囲です。

  2. 指摘の品質が「思いつきベース」にならないこと

    レビューが日替わりだとチームは混乱するし、AIに対しても「正解」が定義できない。これは明文化されたレビューガイドラインをAIに必ず引用させる判定基準として渡すことで担保します(後述、これは別リポジトリで公開しました)。

  3. 誤指摘でマージブロックが連発しないこと

    false positiveを全部Criticalにすると現場が壊れる。これは重要度の階層化(Critical / Major / Minor / Nit)と降格禁止ルールで抑えています。

要は、Part 2で書いたcpgが「AIに渡すコンテキスト」、レビューガイドラインが Guides(事前制御)として「AIに何をさせるか」、severity階層と降格禁止ルールが Sensors(事後制御)として「AIに何をさせないか」を、それぞれ担当しています。Martin Fowlerが提唱したGuides / Sensorsの分類(Part 1で触れた)にそのまま乗る構造です。

もう一つ補足すると、これら3層の手前にファイルあたり500行までというlintを効かせていて、PRに含まれるファイルが必ずAIの1セッションで読み切れるサイズに保たれています。これだけでも自動レビューが破綻しにくく、人レビューと違って見落としも起きにくい。これ以外にも多数のlintを敷いていますが、全体像は Part 4(Self-Healing + Observability + 自動lint追加)で扱います。

自動レビューのシステム配置

実装は各開発者のPC上で動くスクリプトです。GitHub webhookは社内で運用している Event Relayサーバで受けてFirestoreに永続化し、各PCのスクリプトは SSEクライアントとしてEvent Relayにつないでイベントを受け取る構成。再接続時はLast-Event-IDで未送信分が再送されるので取りこぼしゼロ、GitHub admin権限も1回のwebhook登録だけで済みます。配信はEvent Relayが集約、レビュー判定や修正処理は各PCで実行、というのが基本ルートです。

立ち位置を補足すると、レビュアーモードのスクリプトを動かすPCは常時起動(レビューがいつ来てもすぐ反応できる体制)、authorモードのスクリプトはPR作者が自分で作業している裏で動く(PR作者は普段の開発でPCを立ち上げているので、そのプロセスとして同居)という運用です。長時間オフラインだった場合も、Event RelayがFirestoreで保留しているので接続が戻った時点で再配信されます。

Event Relayにたどり着くまで(補足)

実はこの構成、最初からこうだったわけではありません。

smee.io / Cloudflare Tunnelいずれも コネクション切れや配信欠落 が頻発して、見落としが何度かありました。自前Event Relayに切り替えてからは、Firestore永続化 + Last-Event-IDでの再送 で取りこぼしゼロ、かつ複数用途に使いまわせる汎用layerになっています。

実際、次回Part 4で扱う Self-HealingのGrafana webhook受け取りも、まったく同じEvent Relay経由 です。GitHub / Grafana / その他webhook系を1つのrelayに集約して、各PCのSSEクライアントが必要なイベントをsubscribeする形に整理できる。汎用webhook relayは1つ持っておくと、想定外の用途でも横展開しやすいインフラ だと感じています。

レビュアーのPCで動くスクリプトがイベントを受け取ったら、claude -pをspawnして9観点(Graph / Architecture / Security / Test / Doc / Impact / Observability / AI-Antipattern / Recurrence)を順にチェックし、AIが末尾に書いた最終判定マーカーを読み取ってgh pr reviewAPPROVE / REQUEST_CHANGESを投稿します。

自動レビュー全フロー — PR起票から自動デプロイまで人なしで完結

ポイントを少しだけ補足します。

並列sub-agentではなく、1セッション順次レビューにしている理由

最初は9観点を並列のsub-agentに分ける案を試しましたが、実運用に乗せたら以下の問題が出ました:

1セッション順次に切り替えると、cpg / ガイドラインの読み込みは1回だけで済み、前の観点のfindingsを保ったまま次の観点に進めるので 観点間の整合性も自然に拾える。出力も1ストリームなので、最終判定マーカーを末尾に1つ書くだけで集約完了です。

あわせて CLAUDE.md をレビュー時専用に差し替える 設計にしてあります。cortexのデフォルト CLAUDE.md は開発作業向けのコンテキスト(Product Graph操作方法・本番データ保護ルール・各種MCP使用順序など)が大量に書かれていて、レビュアーAIにとってはほぼノイズです。起動時にレビュー専用 CLAUDE.md(severity階層 / 降格禁止ルール / 最終判定マーカー仕様などを核に据えたもの)に差し替えると、AIの注意がレビュー目的に集中 して脱線が減ります。

無駄なコンテキスト注入を削ることで、判定精度とtokenコストの両方が同時に改善する ── というのがこのあたりの設計のベースです。

細かい運用上の制御

実運用ではいくつかの「絞り」を効かせています:

指摘の構造 ── タグと重要度

自動レビューの出力はタグ + severity + 具体例の3点セットで構造化されています。

タグ(観点)

タグ 観点 主な対象
[Graph] Product Graph整合性 @graph-* JSDoc、依存ノード、ドキュメント整合性
[Doc] ドキュメント整合性 コード変更に対するドキュメント追従、配置
[Impact] 影響範囲分析 上流下流の修正漏れ、via: フィールド不整合
[Security] セキュリティ 認証・認可・入力検証・機密情報
[Architecture] Composable Architecture app/package境界、依存方向
[Test] テスト品質 カバレッジ・matcher・命名
[Observability] 観測性 ログ・通知の構造化・truncate禁止
[AI-Antipattern] AI生成コードの罠 幻覚API、フォールバック濫用、デッドコード
[Recurrence] 再発防止 障害修正時の判定(lint化 / 横展開 / ガイドライン追加)

重要度

severity 基準 アクション
Critical セキュリティ、データ破壊、本番障害、ドキュメント不整合、@graph-*欠落、品質基準の緩和 REQUEST_CHANGES
Major 仕様逸脱、Composable Arch違反、テスト欠如 REQUEST_CHANGES
Minor 命名改善、保守性、軽微なリファクタ REQUEST_CHANGES(resolve必須)
Nit スタイル好み、表記揺れ APPROVE(コメントのみ)

最重要ルールは「降格禁止」です:

これはseverity.mdに明記されていて、AIが引用しながらREQUEST_CHANGESを返してきます。

実例:embeddingモデルをgemini-embedding-001からgemini-embedding-2に移行するPR

実際の自動レビューコメントを見たほうが早いので、典型例を1つ貼ります。これは2026-05-19にマージされたfeature PR(feat(meet): dual-write embeddings to new 'embedding' column (v2))で、起票からマージまで 1.5時間で6回イテレートした記録です。

初回レビュー(07:35

:

dual-writeの実装方針・冪等なmigration script・Promise.all並列化いずれも妥当です。以下3点のCriticalと2点のMinorを修正してからmergeをお願いします。


Critical

[Graph] @graph-businessタグが欠落(×3)

graph-integrity.md はアプリ層のトップレベル宣言に @graph-business を必須としています(severity: Critical)。 今回追加した以下3宣言すべてに欠落しています(別スレッドで詳細コメントあり)。 既存コードも同様に欠落していますが、「既存パターンに従った追加」は降格理由になりません(severity.md)。

  • generateEmbeddingV2(v2 embedding生成関数)
  • EMBEDDING_MODEL_V2(モデル名のconfig)
  • EMBEDDING_LOCATION_V2(リージョンのconfig)

[Graph] embedMeetContent@graph-connectsgenerateEmbeddingV2 を反映していない

embedMeetContent のJSDocに @graph-connects generateEmbedding [calls] Embedding生成 はありますが、generateEmbeddingV2 の呼び出しが追加されたにもかかわらず対応する @graph-connects が追記されていません。 グラフノードからは generateEmbeddingV2 へのエッジが見えない状態になります。

   * @graph-connects generateEmbedding [calls] Embedding生成
+  * @graph-connects generateEmbeddingV2 [calls] v2 Embedding生成(dual-write)
   * @graph-connects insertMeetChunks [calls] BQにチャンクデータ挿入

[Doc] 対応するBigQueryスキーマドキュメントが未更新

対応するドキュメントの「BigQueryスキーマ」節に新規 embedding カラムが記載されていません。 graph-integrity.md および severity.md はドキュメント不整合をCriticalと定義しています。

 | `created_at`  | TIMESTAMP   | 作成日時                               |
+| `embedding`   | FLOAT64[]   | Embeddingベクトル(v2: gemini-embedding-2)|

Minor

[Test] textEmbeddingV2 の値がテストで検証されていない

objectContaining は余分なフィールドを許容するため、v2値が渡されなくてもテストは通過します。

         textEmbedding: [0.1, 0.2, 0.3],
+        textEmbeddingV2: [0.1, 0.2, 0.3],

[Test] v2のみnullを返すシナリオが未テスト

generateEmbeddingV2: mockGenerateEmbedding としてv1と同一モックを使用しているため、「v2がnullを返しv1は成功する」ケースが独立して検証されていません。


<!-- VERDICT:REQUEST_CHANGES -->

ポイントは細部の精度です。

このあとPR作者(=多くの場合、作者のPCで動く別のAI)が修正をpush → 再レビュー。Critical 3件すべての本質が解消されていることを次のレビューで確認し、新たに次のMajor / Criticalを1つ指摘 → 修正 → 確認 → ... を 6イテレーション、計1.5時間で繰り返して、最終的にAPPROVE → 自動マージです。

このイテレーションの流れを時系列にすると、こうなります。

review-fix loopの実例 — embeddingモデル移行PR / 1.5時間で6イテレーション

人がレビュアーだったら、Critical 3件指摘して翌日対応待ち、再レビューがさらに翌日 ── 1本のPRで2-3日かかるところを、cortexは 90分で完結させています。

人レビューと自動レビューの差は、単に「速い」というだけではありません。1つのAIセッションが9観点を順に拾い、ガイドラインを必ず引用しながら判定するので、人が集中力で見落としやすい "深い指摘"(ドキュメント整合性 / 再発防止判断 / weak matcher等)がこぼれにくい。これをBefore / Afterで並べるとこうなります。

Before / After — 人レビュー時代vs cortexの自動レビュー時代

これが「レビュー詰まり」が構造的に起こらない理由です。

ガイドラインを育てる ── AIが間違える瞬間を捕まえてルール側を直す

ここまで何度か触れてきたレビューガイドラインですが、そのガイドライン自体は静的なドキュメントではありません。自動レビューを運用していると、定期的に「AIがこの種類の判断でミスる」というパターンが見えてきます。そのたびに、個別PRにコメントして上書きするのではなく、ガイドラインを書き換えて次回以降のAIに正しく振る舞わせる ── これが human-on-the-loop の実態です。

実際にcortexで起きた失敗と、それぞれをどう構造で塞いだかをいくつか並べておきます。

1. AIが「既存コードも同様だから」と言って降格させていた

初期、AIは指摘を出した直後に「ただし既存コードも同様に違反しているため、本PRではNitとします」と自分で降格していました。結果、新規追加コードに対する違反指摘が次々とNitに落ち、Approveが量産される状態に。

severity.mdに「降格禁止ルール」を明記して塞ぎました:

「既存パターンに従った追加」を理由にした降格禁止:既存コードがガイドラインに違反している場合、それに従った新規コードも同じ重要度で指摘する。「次回リファクタリング時に検討」のような先送りコメントは認めない。

これだけでは足りず、運用していくと「別PRで対応」「次のセッションで対応」「スコープ外」「段階的に」という別の言い訳パターンも出てきたので、これも降格禁止カテゴリとして追加。さらに「コード内にTODO/FIXMEを残しておく」で先送りするケースも明文化して禁止しました。「典型的な逃げ方を先回りで全部潰す」という発想です。

2. AIの最終判定が3択あって、「コメントのみ」でPRが宙ぶらりんになっていた

AIがレビュー末尾に出力する最終判定は元々 APPROVE / REQUEST_CHANGES / COMMENT(承認 / 修正要求 / コメントのみ)の3択でした。Minorしかない場合などにAIが COMMENT を選ぶと、スクリプトは何のアクションも取らず、PRはレビュー待ちのまま放置 ── 結局あとから人が拾わないと動かない、というアンチパターンが頻発。

判定ロジックを 2択に統一 しました。Minor以上は全部 REQUEST_CHANGES、最終判定が欠落していた場合も安全側で REQUEST_CHANGES、Nitのみor指摘なし(CI pass時)だけ APPROVE。「判定がぶれるならfail-safeに振って止める側(REQUEST_CHANGES)に倒す」という設計に振り切ったのが効きました。

3. チェック項目に重要度が書いてなくて、AIが毎回違う判定をしていた

graph-integrity.mdtesting.md などの各ガイドラインは、当初は箇条書きベースのチェックリストでした。「テストの命名が説明的か」「Mockは最小限か」みたいな項目が並んでいるだけで、項目ごとの重要度が書いていなかった。結果、同じ違反でもPRや起動セッションによってMajor扱いだったりNit扱いだったりと判定がぶれていました。

全ガイドラインのチェックリストを severity / scope / 観点 の表形式に統一 しました:

severity scope 観点
Critical 全PR @graph-business 欠落
Major アプリ層のみ テスト欠如
Minor 共通パッケージのみ 引数3つ超過
Nit 全PR 命名の表記揺れ

scope 列はそのチェックがどのパスに適用されるかを機械判定するためのもので、AIレビュアーはscopeに該当しないPRでは該当項目を発火させません。表に並べただけですが、判定の再現性が大きく上がりました。

4. 既存ガイドラインがAI特有の罠を拾えていなかった

しばらく運用していると、AI生成コード特有のアンチパターン ── 存在しないAPIを呼ぶ(幻覚API。user.findOrCreate() のような正しそうだが未定義のメソッド呼び出し)、エラーを握り潰してフォールバック値を返す(上流APIが落ちたら無言で空配列を返す等)、使われない関数を残す(refactorで旧実装がdead code化)、要求範囲を超えて勝手に修正を広げる(1関数の修正依頼で同ファイル全体reformat)、不要な後方互換コードを足す(内部関数なのにdeprecated aliasを新設)── が、security.mdtesting.mdでは捉えきれないことに気づきました。「AIだからこそやらかすミス」のクラスターが存在する。

これに対応して ai-antipattern.mdを新設 しました。レビューでも [AI-Antipattern] というタグで明示的に拾うようになっています。AIに対するレビューは、AI特有の罠を知ったうえで設計する必要がある ── これは人間のレビュー観点をそのままAIに移植するだけでは出てこない発想です。

5. AIが「基準そのもの」を緩めようとしてくる

最後にして最重要のパターン。AIが修正PRを書くとき、ガイドライン違反を直す代わりに、稀にガイドライン側を緩めるPRを書いてきました。たとえば:

しかも理由として「既存実装がすでに違反しているので、基準側を実装に合わせる」という、形式的には筋が通る論理を組み立ててくる。これを放置すると、AIが品質基準そのものをじわじわ下げていくことになります。

severity.md に「品質基準の緩和」をCritical扱いとして追加して塞ぎました:

ガイドライン文書・lintルール・カバレッジ閾値など品質基準を緩める変更を含むPRは、自動レビューがApproveせず必ず REQUEST_CHANGES で差し戻す。人間レビュアーのApproveを必須とする。「既存実装が違反しているので基準を合わせる」を緩和の正当化理由として認めない。

ここだけは明示的に AIに自律的なApprove権限を渡さない 領域として線を引いてあります。基準の変更可否は人間が判断する。これは「AIがAIをレビューする」というアーキテクチャに対する メタレベルの安全弁 です。

ガイドラインの育て方そのものがhuman-on-the-loop

並べてみると、共通しているのは「AIが間違ったときに、個別PRで上書きするのではなく、次回以降に伝播する形でガイドラインを直している」という動き方です。

このループが回っている限り、ガイドラインは AIの実運用で見えた失敗パターンを吸収しながら育つ生きたドキュメントになります。「最初に完璧なガイドラインを書く」のではなく、「AIが間違える瞬間を捕まえて、その瞬間に対応するルールを書く」。これが、人がloopの中に居続けなくても品質が下がらない仕組みの中身です。

そしてもう一つ。今のところ「AIが間違える瞬間」を観測してガイドラインを書き換えるトリガーは人間の判断が中心ですが、この メンテ自体も徐々にAIに渡されつつあります。本番障害を起点にAIが原因調査から修正PR起票まで完結させる Self-Healing(次回Part 4で扱います)では、修正PRに [Recurrence] 観点でのlint化 / ガイドライン追加 / 横展開のいずれかを書くことが必須化されていて、結果として AIが自分の判定基準を自分で増やしていく 方向に進んでいます。ガイドラインのメンテすら人手から離れていく ── という流れも、次回でもう少し詳しく触れます。

自動修正 ── AIが修正してプッシュする仕組み

REQUEST_CHANGESが立つと、今度は PR作者のPCで動いている同じスクリプトのauthorモードがイベントを受け取って起動します。

[REQUEST_CHANGES 検知]
   ↓ Event Relay経由でSSE push
[PR作者のPCでauthorモード起動]
   ↓ origin/mainをworktreeにマージ(lockfileは先行解消、
     残るコンフリクトはAIが解消)
   ↓ 自動レビューのコメントをコンテキストとして読み込む
   ↓ claude -p をworktree内でspawn
   ↓ 変更をcommit + push
   ↓ 新SHAをEvent Relay経由でレビュアーのPCに配信 → 再レビュー

ここのキモは2点。

自動マージ + 並列デプロイ

自動レビューがAPPROVEを返し、CIも全部greenになったら、auto-merge scriptが動き、PRはsquashでマージされます。

[自動レビュー APPROVE + CI green]

auto-merge script
   ↓ squash merge to main

[main更新]

Turborepo build (affected packages only)

Pulumi up (複数スタックを並列でデプロイ)
   ├─ APIサーバ群
   ├─ pipeline群
   ├─ MCPサーバ群
   └─ インフラ

[デプロイ完了]

cpg index再構築(差分のあったノードだけembedding再生成、Part 2参照)

pulumi up <stack1> <stack2> ...で並列実行できるようにしてあるので、9スタックを同時にデプロイしても約8-12分で全部終わります。マージから本番反映までで言うと、平均10-15分。

これもSelf-Healing PRと組み合わせると効果が大きい。障害アラート → Self-Healingが原因特定 → 修正PR起票 → 自動レビューpass → 自動マージ → 自動デプロイが、人の介入なしで一周まわります(Part 4で扱います)。

数字で見る自動レビュー

冒頭で挙げた数字をもう少し分解しておきます。

review-fix loopの深さ

直近30日の769本のPRの中で、1本のPRあたりのレビュー数は平均10.8回、最大56回。10回以上のレビューが走っているということは、初回レビューでほぼ確実に何か指摘が出ているということです。

さきほど例に出したembeddingモデル移行PRも6回のreview-fix iterationを経てマージされており、これが平均的なPRの姿です。人レビュアーがいたら数日かかる修正サイクルを、cortexは数分〜数十分単位で回している

Critical / Majorのhit状況

自動レビューが最初に出す指摘でよく見るのは:

これらは人レビュアーがやろうとしても見落とすことが多いカテゴリです(特にドキュメント整合性と再発防止判断)。AIに渡したことで、見落としが構造的に減ります。

false positiveの実数

完全にゼロではない。「これはMajorではなくNitでは?」というようなケースは月に数件あります。対応は前述のとおりで、個別PRにコメントせず、ガイドライン側を書き換えて次回以降の判定を直すという回し方です。

何が変わったか / Bridge to Part 4

cortexのエンジニアの役割は、ここ半年で「書く側」「見る側」の両方から「判断する側」に移った。

エンジニアの手元に残っているのは「そもそも何を作るか(product / 要件決定)」「そもそもこの方向で正しいか(アーキテクチャ判断)」「新しいガイドラインをどこに加えるか」「レビュー結果を見てプロンプトやガイドラインを直す」── 抽象度が高い、個別の意思決定ではなく、システム全体を上から見る役割に寄っています。human-in-the-loopからhuman-on-the-loopへ、と言い換えてもいい。

世間で言われる「AIで品質が下がる」「レビューが詰まる」という現象は、ハーネスを書く側だけに広げて、見る側を人に残したまま放置するから起こる。書く速度だけ上がって、見る側がそのままなら、確かに詰まる。確かに見落とす。

cortexは逆です。書く側よりも先に、見る側にハーネスを広げた。Anthropicが報告している「書く側より見る側にボトルネックが移る」という知見はそのまま正しい。だからこそ「見る側もAIに渡す」が、cortexが選んだ答えになる。

「AIが書いたコードはAIが見る」── これがcortexの自動レビューの核心です。品質低下とレビュー詰まりは、ハーネスをどこまで広げたかで決まる現象であって、AI開発そのものの宿命ではない。


次回 Part 4 ── Self-Healing + 再発防止の仕組み では、本番のアラート(OTel/Loki/Mimir/Tempo/Faroで観測)を起点にAIが原因調査 → 修正PR + 新規lint/型gateの追加 → 自動レビューに乗せて自動マージ → 自動再デプロイで完結させる仕組みを取り扱います。修正と同時に再発防止のガードレールが自動で増えていくので、同種alertが構造的に再発できなくなる。自動レビューがPR時点の品質を守るのに対し、Part 4は production時点の品質を守りつつ、品質ゲート自体を育てる 役割です。

冒頭の数字にはSelf-Healing PR(本番アラートをAIが原因特定→修正→自動マージ→自動デプロイで一周まわす仕組みの出力)も含まれています。「障害は気づく前に直っている」が、cortexの現状です。

comments (15)

  1. Mykola Kondratiukvia dev.to

    running at scale requires betting humans will notice when to intervene - that's the real tradeoff. curious what signals surface a PR for actual human review in practice.

    1. Ryosuke Tsujivia dev.to

      Great question — and I think the framing already points at the right concern. "Humans noticing when to intervene" is exactly the model that stops being reliable at this scale; once you're past ~700 PRs/month, attention-based oversight breaks. What we do instead is make escalation structural, not attentional. The concrete signals that surface a PR for human review: Quality-bar relaxation PRs — anything that lowers a lint rule, coverage threshold, or guideline binding is classified Critical in severity.md. The AI is forbidden from approving it; a human reviewer's approve is required. This is the meta-safety valve in the post. Repeated auto-fix failures — if the author-mode AI can't pass CI after a few attempts, the PR gets a needs-human label. Alert-Fix "can't fix" verdicts — when AI investigation can't pin down a root cause, it posts the investigation log to Slack and explicitly hands off (Part 4 of the series goes deeper on this). PR-author opt-out on auto-merge — authors can disable auto-merge per PR for changes they want to land carefully. None of these depend on a human happening to notice — they're explicit triggers built into the pipeline. The deeper bet is that humans aren't watching individual PRs at all. They're watching aggregate AI behavior and tuning the guidelines (the policy layer). When the AI misjudges a class of issue, the right intervention is rewriting the rule, not stepping into the PR. That's what human-on-the-loop means in practice here.

    2. Mykola Kondratiukvia dev.to

      structural escalation is the right frame - what signals break the glass for you? my attempt was blast-radius tags at design time, which helps but just shifts the attention problem upstream

    3. Ryosuke Tsujivia dev.to

      Right — that's exactly the meta-problem. Any tag-based mechanism just relocates "who attends to the tag." Our break-the-glass signals are deliberately action-coupled rather than attention-coupled: Explicit AI handoff — Alert-Fix posts its investigation log to Slack and says "I couldn't fix this safely" when it can't pin down a root cause. No flag to scan; the system tells you it's stuck, with everything it tried. Limit violations — repeated CI-fix failures, unresolvable conflicts, etc. get a needs-human label. That's literally the queue. Categories pre-defined as human-only — quality-bar relaxation (lint/coverage downgrades), credential handling, production data ops. Classified Critical in severity.md such that the AI is forbidden from approving them. Not "AI flags for human review" — "AI cannot approve, so the PR sits until a human does." The shape we landed on: the signal doesn't say "pay attention," it says "this is blocked until you act," and the context the human needs (diff, severity rationale, investigation log) is delivered with the signal. The queue is short and each item arrives with its own briefing. To address the "attention shifts upstream" worry directly: the same problem applies at design-time too — blast-radius tags help only if a human is going to read every tag. The way we keep it bounded is the [Recurrence] loop: when a class of issue trips the human-only gate twice, the next PR is required to add a generation-time constraint (lint / type / CI guard) in the same PR that fixes the symptom. So the "human-only" categories shrink in absolute terms over time, even while throughput grows. Your blast-radius tagging would slot into this nicely as one more pre-classified category — the question is whether the tag triggers attention or triggers action.

    4. Mykola Kondratiukvia dev.to

      action-coupled is cleaner - context and signal arrive together. what happens when the handoff message itself gets buried in a busy channel?

    5. Ryosuke Tsujivia dev.to

      Right — that's the natural next concern. The answer: the Slack message isn't the source of truth, it's just the notification. The actual queue lives on the PR itself. When a PR breaks the glass: A needs-human label is applied (or it sits in REQUEST_CHANGES state). Auto-merge is disabled. The relevant Slack channel gets a thread with the investigation log / verdict / diff. Optionally, the assigned reviewer gets a Slack DM via our notification relay. The label + PR state is the durable queue. We treat "open PRs with needs-human label" (and "PRs awaiting my review") as the canonical pending-attention surface — the same way you'd treat a bug tracker. It doesn't depend on anyone happening to see a chat message in the moment. So Slack is the push layer; the PR list is the pull layer. Both exist on purpose: push catches you when you're online; pull catches you when you come back from PTO or finally clear the queue. Slack channels are also severity-routed (#cortex-fatal is kept intentionally low-volume so critical items don't drown in the warning channel). That said — this pattern is tuned for "will be picked up within hours / a day," not "must be picked up in 60 seconds." Anything that time-sensitive probably shouldn't sit behind an AI handoff in the first place; it belongs in pager-style escalation. The break-the-glass pattern handles the 'AI couldn't, but it's not on fire' band, which turns out to be most of what shows up.

    6. Mykola Kondratiukvia dev.to

      the PR-as-queue pattern is solid when the agent’s whole world is the repo. the one that broke this assumption for me: a contract validation agent that ran pre-PR. needs-human had nowhere to land until we added a draft PR just to carry the label. works but feels like fighting the abstraction.

    7. Ryosuke Tsujivia dev.to

      Yeah, that's a real gap — agreed. PR-as-queue works when the unit of work is the PR; once you have agents whose unit of work is something else (pre-PR validation, long-lived branches, sandbox-style sessions), you have to either pick a different queue substrate or wrap the output in a PR shape just to give it somewhere to live. We hit something similar with an annotation agent that runs on a long-lived branch that never merges. Ended up with a single permanently-open PR acting as the carrier for review comments and labels — same vibe as your draft-PR workaround. Works, but clearly fighting the abstraction. Good exchange — useful pressure-test on how far the framing actually generalizes. Thanks for the back-and-forth.

    8. Mykola Kondratiukvia dev.to

      wrapping in PR shape is a real workaround until you need rollback that doesn't map cleanly to git. what substrate did your annotation agent end up on? sounds like you found the edge of the same pattern.

    9. Ryosuke Tsujivia dev.to

      Good direct question. The annotation agent ended up on: A long-lived branch that never merges, holding the annotation state A permanently-open PR from that branch as the review / label surface An indexing pipeline (BigQuery + embeddings) that auto-converges on whatever the git state of the branch is So git stays the source of truth and side effects converge on it. "Rollback" for annotations is just git revert on the annotation branch; the indexing pipeline catches up. PR-as-queue still carries the human attention, label, and conversation; the indexing handles the state convergence. But your "rollback doesn't map cleanly to git" point is real, and I think it's exactly the edge of this pattern. It works as long as: Side effects are idempotent (re-index on revert recovers state) External systems can replay from git (deploy + redeploy = rollback) There's a compensating action for the truly irreversible parts Once any of those fail — an already-mutated third-party state, an event already emitted to a fanout, a destructive DDL applied to prod data — git alone can't roll you back and PR-as-queue stops carrying enough state. At that point you need event sourcing, separate state stores, or compensating-action graphs. The pattern is great for "work that converges on git"; weaker for "work whose effects escape git." So yeah — same edge, approached from a different direction. Useful to name it explicitly.

    10. Mykola Kondratiukvia dev.to

      makes sense once the branch is a mutable store rather than a log. trying to serialize annotation state back into commits would get noisy fast.

    11. Ryosuke Tsujivia dev.to

      Yeah, exactly that. We rebase / squash that branch liberally and let the PR's review threads carry the conversational state — commits aren't trying to be a record. Trade-off is losing commit-level audit on the branch, which works for annotations and wouldn't for something like financial state. "Mutable store vs log" is the cleaner framing.

    12. Mykola Kondratiukvia dev.to

      that recoverable/not line is the whole call - annotations tolerate lossy history fine, financial state can't.

  2. Theo Valmisvia dev.to

    769 PRs/month is the scale at which review-time controls start to bend. The interesting design question is which constraints belong at generation time (so the agent can never produce a violating PR) vs. which belong at review time (where humans still adjudicate judgment calls). The mix is what determines whether the quality bar actually rises.

    1. Ryosuke Tsujivia dev.to

      Yes, this is the right design question. The generation-time vs review-time split is essentially Martin Fowler's Guides / Sensors taxonomy, and you're right that the mix is what determines whether the bar rises or just holds. Cortex's bet is to push as much as possible to generation time — but with an explicit mechanism for the mix to shift over time. Generation time (proactive): Lint, type checks, a 90% coverage gate, and a 500-lines-per-file cap — all enforced pre-commit / in CI. cpg context fed to the author AI (same graph the reviewer would query), so the author already has the context the reviewer is going to evaluate against. Author-side CLAUDE.md encodes architectural rules so violations rarely get written in the first place. Review time (reactive): 9-dimension AI review with a severity hierarchy and strict no-downgrade rules. Severity gates merge based on the verdict marker. The piece that I think actually moves the bar over time is what we call the [Recurrence] loop: bug-fix PRs are required by the review guideline (recurrence-prevention.md) to pick one of {add lint rule, horizontal rollout to other call sites, add guideline item, or "single occurrence — nothing"}. When a class of issue trips up review twice, the rule is required to migrate from review-time to generation-time in the same PR that fixes the symptom. So "whether the quality bar rises" isn't a static-mix question — every review-time catch becomes a candidate for promotion into a generation-time constraint, and the loop keeps converting Sensors into Guides over time. Part 4 of the series gets into this in more detail. Thanks for the framing — this is exactly the lens that surfaces what's actually load-bearing.