■ 1. 記事の主旨
- マージ時にdiffを読まないことが多くなった:
- 場合によっては動作確認もしない
- それでも簡単なバグはほぼ出ない
- 理由:
- レビューでしか捕まらない問題を、先に減らす仕組みを整えたため
- この記事は、その「先に減らす」ために何を整えたかを説明するもの
- 背景:
- エージェントを並列で回すようになり、コミットは1日500を超えた
- 前作「ターミナルを自作したら、1日のコミット数が500を超えて、生産性がバグった話」の続編にあたる
■ 2. 前提:人間のレビューはスケールしない
- 並列でエージェントを走らせると、生成されるコード量が人間の読める速度を超える:
- 問題は量そのものではなく、読む速度が量に追いつかなくなったこと
- 選択肢は2つ:
- 生成を減らして読める量に合わせる
- 読まなくても壊れないようにする
- 後者を選択、これは「壊れたら赤くなって止まる」仕組みを積み上げる作業
- 紹介する設定はMulmoTerminal / MulmoClaudeで実際に稼働しており、両方ともMITで公開している
■ 3. CLAUDE.md:規約を人の記憶でなくファイルに置く
- 規約の置き場所は2層:
- グローバル(~/.claude/CLAUDE.md): 全プロジェクトに効くルール、公開済み
- リポジトリ固有(各リポジトリのCLAUDE.md): そのリポでしか通用しない事実
- リポジトリ側はほとんど不要:
- 開発スタック(yarn、TypeScript、Vitest、同じESLint構成、同じCIの形、同じテストの置き方)をほぼ揃えているため
- リポジトリ側に本当に書く価値があるのはディレクトリ構成の理由付け程度
- 例:共有コードを置く場所と、同期維持コメント付きコピーを禁止する理由
- もう一つ価値があるのは、そのリポでしか起きない罠
- 例:MulmoTerminalでは型チェックコマンドが3つに分かれており、揃えないとローカルでは通ってもCIで落ちる
- リポジトリ側CLAUDE.mdが長くなることへの解釈:
- 規約が足りないのではなく、スタックの揃え方が足りないサインと考えている
- 同じ注意書きを複数リポに書き写すより、先にスタックを揃える方が早い
- 規約は増やすものでなく、書かなくて済むよう環境を寄せていくもの
- 長い規約は人間にもAIにも読まれない、実際に効くのは毎回必ず踏む数行だけ
- 誰のために書いているか:
- ここに書いてある規約は基本的に全部AIにやらせるためのもの
- 人間が読んで守るための規約ではなく、著者自身がやることは想定していない
- AI向けの規約は、いつ読むか・何をすればよいかまで書き切る必要がある
- 行間を読んでくれることを期待した書き方は通用しない
- 人間が読んで守る前提を捨てれば、規約はいくら細かくても構わなくなる
- グローバル側は「短い本体+深いドキュメント」の構成:
- 本体には1行のポインタだけを置き、詳細はdocs/に逃がしている
- 現在配置されているのはdocs/testing.md、docs/debugging-methodology.md、docs/windows-gotchas.md、docs/cross-platform-ci.md、docs/web-debugging.mdの5つ
- ポインタには「いつ読むか」を添える
- 例:テストを書く前に読む、Windows障害デバッグ前に必読
- 常に全部を読ませようとすると結局どれも効かなくなる、規約は量でなく「踏む確率」で効く
■ 4. ESLint:大きすぎる・複雑すぎるを機械が拒否する
- コードレビューで指摘したくなる内容(長い、ネストが深い、引数が多い、分岐が多すぎる)の大半は機械が言えると気づいた
- 主要ルールをすべてerrorに設定:
- max-lines-per-function: 60行
- complexity: 20
- max-depth: 4
- max-params: 6(こちらはwarnのまま)
- max-nested-callbacks: 4
- 加えてeslint-plugin-sonarjsのcognitive-complexityをerror(閾値15)に設定、循環的複雑度より人間の読みにくさに近い指標
- MulmoClaude側はさらに厳しくmax-lines-per-function 50、complexity 15
- 大事なのは例外の作法:
- インラインのeslint-disableを許すと規約が死ぬため許可しない
- 例外は設定ファイルに理由付きで記載する運用
- 例:Vueコンポーネントの
<style>ブロックを原則禁止しTailwindユーティリティで代替、どうしても書けないものだけ理由付きで許可リスト化し、理由が消えたらエントリも消す- max-paramsのみwarnにとどめている理由も設定ファイルに明記
- spawnClaudePtyの7引数はhot pathであり、5箇所の呼び出しをオプションオブジェクト化する労力に見合わないため
- 解消したらerrorへ格上げする方針
- プリセットの選び方:
- typescript-eslintはrecommendedでなくstrictを採用
- sonarjsとsecurityはrecommendedのみが最上位設定(strictが存在しない)であり、手加減ではなく取り得る最も厳しい設定
- その上で個別ルールをさらに追加
- sonarjs/cognitive-complexity: 人間の読みにくさに近い指標
- sonarjs/no-ignored-exceptions: catchして握りつぶす箇所を検出
- sonarjs/assertions-in-tests: アサーションのないテストを検出、以前は人間が目で見つけていたものをCIが担うようになった
- 外すルールにも理由を記載
- MulmoTerminalではsonarjs/no-os-command-from-pathをbin/でのみ無効化、ユーザーがインストールしたCLIをPATHから起動するのがツールの前提であり、このルールと矛盾するため
- 型をもう一つの規約として使う:
- CLAUDE.mdが自然言語規約なら、型は機械が強制する規約
- diffを読まなくなった以上、読む役を型に担わせるしかないと考えている
- no-explicit-any、no-non-null-assertion、consistent-type-assertionsをerrorに設定
- グローバルCLAUDE.mdにも明記
- asキャストは使わず型ガードを書く
- Zodスキーマからはz.infer
で導出し、同じ型を二重定義しない - anyは使わない
- lint/型エラーをeslint-disable、@ts-ignore、@ts-expect-errorで黙らせず根本を直す
- 最後のルールが特に効く、放置するとエージェントはエラーを消す最短経路(@ts-ignore等)を選ぶため
- 型情報を使うlintは「コストに見合うものだけ」採用:
- projectServiceを有効にした型チェックパスは重い
- 実測では5ルールでも44ルールでもコストはほぼ同じで、プログラム構築自体が全コスト、未型付けパスに対し全体で約26秒かかる
- にもかかわらずstrictTypeCheckedを全部は入れていない
- 残りはスタイル系ルールが支配的で、restrict-template-expressions単体で1213件中439件を占め、本当に必要なルールを埋もれさせるため
- 型情報でなければ捕まえられない2種類に絞って採用
- 構造的に見えないany(型のないライブラリの値、JSON.parse()、二段キャストなど)
- 構文ルールでは原理的に捕まえられない間違い(await付け忘れ、同期専用APIへの非同期コールバック、文字列化ミスなど)
- 該当するno-base-to-string、no-floating-promises、no-misused-promises、await-thenableの4ルールは、バックログをゼロにしてからerrorに格上げした
- 「誰も読まない警告リストに1件足す」ことを許さない運用、既存のバックログがないため新規混入は即CI失敗になる
- 人間なら耐えられない厳しさが、AIなら成立する:
- この設定は人間のチームなら3日で緩和PRが飛んでくるレベル
- 人間が書いていれば「いちいちうるさい」となるが、AIは指摘されたら直すだけで文句を言わない
- 従来lintの厳しさは「機械の正しさ」と「開発者の忍耐」のトレードオフであり、緩める判断は技術的でなく社会的な判断だった
- 社会的コストがゼロになった以上、天秤は厳しくする側にしか傾かない
- diffを読まない著者にとって、厳しいlintは贅沢でなく必需品
- だから型のない言語を選びたくない:
- 静的解析が効かない言語では、この記事の内容の半分が成立しない
- 「読まなくても壊れない」を支えるのは書いた瞬間に機械が読んでくれることであり、型がないとその最初の読み手がいなくなる
- 型を書く面倒のコストは、今は著者でなくAIが払っている
- ESLintとSonarJSをCIにどう置くか(ゲートかレポートか、既存負債の返し方)は別記事に切り出してある
■ 5. テスト:できる限りpure関数にしてエッジケースを潰す
- 実績数値:
- MulmoTerminal: specファイル294、テストケース3,373、ソースファイル407(ソース1ファイルあたり約8ケース)
- MulmoClaude: specファイル799
- エージェントが書くためテスト作成コスト自体は安く、高いのは「壊れたことに気づかないコスト」の方
- 設計の方が本質:
- テストを増やす前に、テストできる形にすることを優先した
- ルール(フィルタ、並び順、上限、検証、整形、保持期間)は専用ファイルのpure関数に切り出す
- ファイル読み書き、プロセス起動、ソケット、HTTPは呼び出し側に残す
- アプリを起動しないと到達できないルールはテストされないため
- 純度を保てない境界(now()、isValidId、hasTmux、ホームディレクトリのパスなど)では依存を引数で渡す
- import時に時計やプロセスを掴むモジュールは開発者のマシンに触らずテストできず、これは設計の欠陥とみなす
- これは著者の心がけではなく規約(CLAUDE.md、docs/testing.md)に明記されており、エージェントがそれに従って書く
- 新しいコードだけの話ではない:
- 規約には既存コードにも適用すべきと明記されている
- 大きなファイルを触る際は、埋もれたpureなルールを見つけて切り出しテストを付けることが求められる
- テスタブルであることは新規行だけを縛るルールでなく、コードベースの性質として取り戻し続けるものと位置付けている
- 定期的にコードベース全体を見直させ、Claude Code組み込みの/code-reviewや/simplifyコマンドで切り出しとリファクタリングを行わせている(自作スキルでなく標準機能)
- これも人間はやらない
- カバーするケースのパターン:
- 正常系、エッジケース、コーナーケース、境界値、空、null/undefined、不正入力、エラー、否定、リグレッションの10種類を明示している
- テストが本当に落ちるか確かめる:
- 新しいテストは対象を壊したときに赤くなるかを確認する運用が最近習慣化した
- 条件を反転させる、ガードを消す、修正をrevertするなどして赤くなることを確認してから戻す
- 壊れたコードでも通るテストは何か別のものをテストしており、確認しない限り気づけない
- 何を先にテストするか:
- 優先順位は「どれだけ静かに失敗するか」で決めている
- 例外を投げる関数は自分で報告してくれるため優先度が低い
- 怖いのはそれっぽい間違った値を返すもの
- 例:ずれたインデックス、バイト数と文字数の取り違え、ずれた日付、拡張子の大文字小文字違い、ゆるすぎるバリデータ、プロトタイプチェーンを読んでしまう検索
- これらはユーザーが気づくまで見えないため先に潰す
■ 6. DRY:重複は「気をつける」ではなく機械が数える
- 重複はメンテコストに直結する、同じロジックが3箇所にあると修正が3箇所必要になり、たいてい2箇所しか直らない
- CIに2本のスキャンを追加:
- duplication-scan(jscpd公式): コピペの検出
- dead-code-scan(knip公式): どこからもimportされていないexportや、誰も呼ばなくなったヘルパーの検出
- どちらもビルドを落とさない設計だが方法が異なる:
- dead-code-scanは明示的にcontinue-on-error: trueを設定
- duplication-scanは閾値を設定せず走らせ、結果をSARIFでCode Scanningに上げる(アラートとして残るがジョブは失敗しない)
- 理由:
- knipは全てのエントリポイントを推論できず(tsx経由のCLIサブコマンド、codegenなど)、少数の誤検知が想定されるため
- knipにはbase-branch diffingがなく、報告はPRの差分でなく全体インベントリであるため、レビュー補助でありゲートではない
- 初日からブロックすると「無視の作法」が育ってしまう、これはESLintの例外運用と同じ構造
- 補足:
- ESLintのno-unused-varsはファイル内しか見ず、重複スキャンはコピペしか見ない
- どこからも呼ばれなくなったexportはどちらの網にも引っかからない孤児であり、リファクタが最後の呼び出し元を消したときに残る典型例
- knipはその隙間を埋めるために導入した
■ 7. CIをmac / Linux / Windowsで回す
- 構成:
- PRごと: ubuntu-latest + macOS-latest
- Windows: 毎日03:00 JST実行に加えmainへのpush時にも実行
- 3OSで回す最大の理由:
- チームにLinux使いもWindows使いもおらず全員がMacユーザー
- Linux/WindowsのCIが唯一の実機であり、手元にない環境のバグはローカルで再現できない
- リリース前だけでなく、ユーザーからのバグレポート再現にも有効な想定外の副産物になっている
- macOSをPRごとに回す理由:
- 冗長性のためではなく、Dockerサンドボックスがdarwin限定機能のため
- Keychainからの認証情報エクスポートやマウント構築のコードパスがmacOSランナーでしか実際には走らないため
- WindowsをPRから外した理由:
- NTFSのtar展開、Defender、2回目のtscなどで遅く、全PRがそのコストを払う価値に見合わないため
- Windowsが守っている範囲(PATHの大文字小文字分岐、;区切りと\区切り、bareコマンド名の解決、powershell.exeの起動、postinstallの実行など)をワークフロー先頭にコメントで明記している
- 「何を守っているか書けるなら頻度は落としてよい、逆に書けないゲートはたぶん不要か、必要なのに守れていないかのどちらか」という考え方
- Windows固有の罠の例:fs.watchが8.3短縮パスでプロセスを丸ごとabortさせる(catch不可)、path.resolve("/etc")がC:\etcになりPOSIXパス一覧が静かに全部マッチしなくなる
- Windows専用のテストケース:
- 16個のspecファイルがwin32を意識したケースを持つ
- 目的は「Windowsで動くこと」だけでなく、他の変更でWindowsをデグレさせないことがより大きい
- パス処理や区切り文字はLinux前提の一見きれいなリファクタで簡単に壊れ、Mac上では誰も気づけない
- プラットフォームを引数で渡す書き方を採用し、process.platformを直接見ずWindows挙動をMac上でテストできるようにしている
- これは「境界では依存を引数で渡す」設計方針の実例でもある
■ 8. Claude Codeで実装しCodexがレビュー、OKが出るまで繰り返す
- 基本方針:
- 実装したのと別のAIにレビューさせるというだけ
- Claude Codeが書きCodexが読む、逆のパターンもある
- 同じモデルに自分のコードを読ませるとたいてい「良いと思います」と返る、これは同じ判断でそう書いたため
- 別のモデルは別の前提で読むため指摘の質が変わり、導入後バグらしいバグがほとんど出なくなった
- 仕組み:
- GitHub Actionsで全PRにCodexの自動レビューを実行し、CODEX VERDICT: LGTM/CHANGES REQUESTEDという決まったマーカーで判定を返す
- 発火条件は意図的にゆるく、draftやdocsのみの差分でも走らせる、レビューするか判断するコストの方がレビュー自体より高いため
- 連続pushはconcurrencyで最新コミット1回分のレビューに畳まれるため無駄打ちにならない
- 現在の形に至る経緯:
- Yohei Nakajima氏(BabyAGIの作者)のプロジェクトでCodeRabbitが使われているのを見たのがきっかけ
- PR数が少ない当初は無料プランで十分だったが、PR頻度が増えると1時間に1回という制限が足りなくなった
- レビューの本体は指摘→修正→再レビューの往復であり、1往復ごとに1時間待つと3往復で3時間かかる
- この制限はリポジトリ単位で人数分ではなく、著者(日本在住)と共同開発者の中島聡氏(Microsoftでウィンドウズ95やInternet Explorerを手がけ、現在もシアトル在住、Yohei氏の父)がアメリカ在住のため、両者の作業時間が重なった日はレビュー枠の奪い合いになった
- 間違ったコミットでトリガーすると1回分のレビュー枠が消え、また1時間待つことになった
- まずローカルでClaude CodeからCodexを呼び出すスキルを書いて試したところ調子がよかった
- CIでも動くのではと考えCodexのAPIキーを設定しワークフロー化したところ機能し、現在の形になった
- この「手元で回す」用のスキルは現在codex-local-reviewとして存在し、ローカル版が先にありCI版は後から追加された
- CI内で待ちなしにレビューが回ることの重要性:
- レビューの価値は1回の指摘でなく往復の回数にあり、1往復のコストが上がると質そのものが落ちる
- 「待つのが面倒だからこの指摘は次でいいか」が始まった時点で終わりだと考えている
- 人間が「レビューして」と頼む工程や順番待ちが1つでも挟まると自動化の輪から外れる
- push後に勝手にレビューが始まり戻ってきたら指摘が積まれているという形でなければレビューは自動化に組み込めない
- CodeRabbitも併用:
- 現在はMulmoTerminal / MulmoClaudeの両方でCodexと並行して稼働
- 見ている角度が違うため拾う問題も異なる
- レビュアーは数を増やすより種類を増やす方が効く、同じ穴を3回見るより違う穴を2回見る方が価値がある
- 空港でレビューしていた頃の話:
- 2人しかいないチームでは、どちらかが旅行に出ると人力レビューが完全に止まる問題があった
- 以前は空港や機内Wi-Fiでdiffを開きコメントを返していた
- AIレビュー導入がまず効いたのはここで、誰かが移動していてもレビューが止まらなくなった
- これがほんの半年前の出来事だったことに書きながら驚いている
- ここで一番大事なこと:
- ボットの指摘を機械的に全部適用してはいけない
- 複数ボットを走らせると指摘が矛盾することがある(CodexとCodeRabbitが逆のことを言うなど)、両方満たそうとすると誰も読めないコードになる
- 指摘を3つに分類する運用にしている
- 本物の修正: 直してテストを足す
- 妥当なnitpick: 安ければ直す、そうでなければ「意図的」と返す
- 誤検知/古い情報: 確認して理由を書いて飛ばす
- 最後に何を直し何を意図的に飛ばしたかをPRにコメントし、人間がボットのスレッドを全部読み直さずに済むようにしている
- 「OKが出るまで繰り返す」とはこの分類作業を繰り返すことを意味する
- ループ自体をスキル化:
- 手作業での繰り返しは疲れるため、ループそのものを公開リポジトリのskills/配下にスキルとして用意しており現在3種類ある
- codex-local-review: PR化前に作業ツリーやbranchのdiffをローカルでCodexに読ませる、GitHubを往復しないため速い
- codex-cross-review: GitHubのPRに対しcodex execを回して指摘を受け、評価・修正・再レビュー要求までを行う、最大5イテレーションの安全キャップ付きでイテレーションごとに状態ファイルを残す
- gh-review-loop: GitHub側のボット(CodexのActions、CodeRabbitなど)が最新コミットに出した指摘を読み、修正・push・再レビュー待ちを行う、gh pr viewに出ないインラインスレッドまで読むのが特徴
- マージ条件は「全ボットがsign off+CIがグリーン+人間が確認」としているが、最後の「人間が確認」は最近ほとんど形骸化しボタンを押すだけになっている
- それも全自動にしたいと考えており、少し怖さはあるものの人間の確認はすでに機械の結論をなぞるだけになっている
- それでも人間を残しているのは品質の問題でなく責任の所在の問題だと考えている
- 全自動化に進む場合、次にやるべきは「もっと賢いレビュー」でなく、何かあったときに確実に戻せること(ロールバックと後から追える記録)だとしている
- その先:判断そのものを学習させる:
- 現在ボット指摘の3分類判断はまだ著者自身が行っている
- この判断は過去の自分の判断の積み重ねでできており、学習させた「判断AI」に寄せられるはずだと考えている
- レビューを機械に移し、マージを機械に移し、最後に判断の基準を移すのが自然な順番だとしている
■ 9. 人間が残った場所
- 自動化を進めた結果、著者が手を動かしているのは次の3つのみになった:
- UI(見ないと分からないもの)
- 複雑なもの(仕様そのものが怪しいとき)
- 人間がテストするしかないもの(体験、手触り、通知のタイミング)
- 著者の仕事は「読む」から「見る」に変わった、diffを読むのでなく動いている画面を見るようになった
- 「GUIは操作するものでなく見るものになっていく」と考えるようになったと述べている(詳細は別記事に譲るとしている)
■ 10. 並列運用を支える道具:MulmoTerminal
- 壊れないことと回せることは別の問題:
- エージェントを6本並列で走らせるとターミナルが6枚になり、どれが終わったか自分待ちかを目でスキャンする時間がセッション数に比例して増える
- lintも型もテストもCIもこの問題は解決しない
- MulmoTerminalの紹介:
- ブラウザで動くターミナル、npx mulmoterminal@latestを実行するとhttp://localhost:34567が開く
- Node 22.9+とclaude CLIがあれば設定不要
- @latestを付けるのは、npxが過去にダウンロードしたバージョンをキャッシュから使い回すことがあるため
- 機能:
- グリッドで全セッションを一望でき、セルの枠色が状態(作業中/自分待ち/完了)を表す
- 入力待ちで音が鳴り、画面外で詰まったセッションに気づける
- スマホへのWeb Push通知により外出中でも詰まった1本だけ対応できる
- 裏側はtmuxで動いており、ブラウザを閉じてもサーバを再起動してもセッションは生きたまま
- セルごとにgit worktreeを割り当て、複数エージェントが同じリポジトリを同時に触っても衝突しない、グリッドからcommit/push/PRまで可能
- Claude CodeとCodexの両方に対応し、モデルもセッション単位で選べる
- この記事で紹介した運用は、この道具なしでは回らないとしている、厳しいlintも3OSのCIもレビューの自動ループも並列実行を前提にしているため
- MITライセンスで公開しており、使い方は日本語ガイドにまとめてある
■ 11. やっていないことのほうが本質だった
- レビューをやめられたのは頑張ったからでなく、頑張らなくても壊れないようにしたから
- 超人的なエンジニアになったわけでも手を動かす量が増えたわけでもなく、壊れたときに勝手に赤くなるものの数が増えただけ
- ボトルネックの所在が移動した:
- 昔: 書く速度がボトルネックだった
- 少し前: 判断する速度がボトルネックになった
- 今: 統合する速度(CI、レビュー、マージ、コンフリクト)がボトルネック
- 並列で回せる数の上限は注意力でなく道具がどれだけ状態を持ってくれるかで決まる
- 同様に、読まなくて済む量は壊れたときに赤くなる仕組みの量で決まる
- レビューをやめたのでなく、レビューを機械に移しただけだとまとめている
- 今後の目標は自分の判断をAIに学習させ、その判断で最終的にマージまで自動で回すこと:
- ボットのどの指摘を直しどれを「意図的」と返したか、どのPRをマージしどれを差し戻したかの履歴はすべてGitHubに残る過去の判断ログである
■ 12. で、楽になったのか
- 自動化を進めても楽にはならず、むしろ忙しくなったと述べている
- 思いついたことがその日のうちに動くようになり、動くと触り触るとまた思いつくというアイデアの回転が上がった分やることが増えた
- 減ったのは「待っている時間」と「読んでいる時間」であり、仕事そのものではなかった
- 奪われたのは仕事でなく「時間がないからできない」という言い訳の方だったとしている
■ 13. 読者への問いかけ
- 自分の書いた(書かせた)コードをまだ全部読んでいるかを読者に問いかけている
- 読んでいないなら代わりに何が守ってくれているか、読んでいるならいつまで読めそうかを知りたいとしている
■ 14. 真似するための実践ガイド
- 記事に出てきた内容は3つのリポジトリで実際に動いており、そのままコピーすれば同じように動くとしている:
- isamu/claude: 全プロジェクト共通のCLAUDE.md / docs/ / レビュー用skills/
- receptron/MulmoTerminal: eslint.config.js / .github/workflows/ / リポジトリ側CLAUDE.md
- receptron/MulmoClaude: eslint.config.mjs / .github/workflows/
- 導入は全部いっぺんに行う必要はなく、効く順に並べると次の通り:
- 別モデルによるレビューをCIに入れるが最も効く、ワークフローを1本コピーするだけで翌日から指摘が流れてくる
- ESLintのサイズ・複雑さ・型の締め付け、既存コードが赤くなるためまずwarnで入れゼロにしてからerrorに上げる(drainしてからratchet)
- ルールをpure関数に切り出してテストする、設計の話のため時間がかかるが最も寿命が長い
- CIを3OSに広げる、手元にない環境を持つにはこれしか方法がない
- 逆に最初にやらなくてよいのは重複・デッドコードのスキャン:
- これは量が増えてから効くもので、初日から入れても「回避の作法」を覚えるだけだったとしている
- 関連記事として、並列運用を扱う6本のシリーズの一部であることを紹介している