最近、いくつかのプロジェクトで同じリポジトリが二人の AI コーディングツールによって同時に変更され、さらに何人かが自分でコードを push していた… 最初は別に気にしていなかった。どうせコードを書くだけだし、と思っていたが、一通りやってみて分かったのは、問題はほぼコード自体になく、「誰がどのルールを読んだか」「誰がマシン上の何に触れられるか」といった協業の仕組みのほうにある、ということだった。ここに最終的に引いた境界線をいくつか記録しておく。

まずは逆転劇:いわゆる「多方」

別のリポジトリを見返していて面白いことに出くわした。git shortlog -sn を実行すると二行、二人の名前が出てきて、二人が push しているように見える。メールアドレスを調べると同一人物で、余分な一人は私が Web 画面で merge をクリックしたときのアカウント名だった。そして 78 個の commit のうち 63 個に Co-Authored-By: Claude が付いている——本当の「多方」は複数人ではなく、一人といくつかの AI セッションなのだ。並行作業の痕跡も .claude/worktrees/(あのディレクトリは空、git worktree list はメインリポジトリの一行だけ)にはなく、ブランチ名のほうにあった。claude/ プレフィックスのブランチが五つ、一度は二本が同時期に開かれて最後に互いにマージされた。

だから以下の境界線も、もともとは人と人のために定めたものだと思っていたが、振り返ってみるとむしろ「私と、私が開いたいくつかのセッション」のために定めたものだ——セッション同士には共通の記憶がない、この点は同僚よりも深刻なのだ。

ルールファイルは分裂する

あるチームリポジトリの取り決めはかなり網羅的に書かれていて、禁止リスト、バージョン差異、コミット規約が揃っている——だがそれはすべて一方の AI ツールのルールディレクトリの下に書かれていて、しかもすべて常時有効に設定されている。問題は、もう一方のツールがそのディレクトリをまったく読まないことだ。そちらには import の一行と、スキャフォールドが注入した汎用プロンプトしかない。二人の AI のコンテキストはまったく異なり、一方は「このマシンには別の本番プロジェクトがある」と知っていて、もう一方は何も知らない…

自分のリポジトリにもありありとした反面教師がある。acg-site が同時に三セットの取り決めファイルを維持していて、私は一つしか直しておらず、残り二つのバージョン番号とコマンド説明は古い値のまま止まっている——細かい計算は取り決めファイルについて書いたあの記事で既にやったので、繰り返さない。

結論はきっぱりしている。真の情報源は一つだけ残し、残りはすべて import でそこを指す。vns-next と thdl の CLAUDE.md はどちらも一行だけで、中身はすべて AGENTS.md にある。一箇所を直せば両方に同時に反映され、ドリフトの余地はない。

だがこの手は「同じ人が二つを維持する」問題しか解決しない。マルチ AI の場面にはもう一層ある。もう一方のツールはあなたが指すそのファイルをまったく読まないのだ。だから本当に確認すべきは「ルールを全部書いたか」ではなく、「各エージェントが起動時に一体何を読み込んだか」だ——ツールを一つずつ試すしかなく、近道はない。

第一条の境界:マシン上の他のものに触れてはならない

チームの agent を走らせるマシンでは同時にいくつかのプロジェクトが動いているので、「本番環境安全制約」というものをわざわざ書いて、常時有効にした。禁止の大意は次のとおりだ:

  • ポート番号でプロセスを kill してはならない
  • killall / pkill のような一括プロセス kill コマンドを使ってはならない
  • システムレベルの設定ディレクトリ配下のものを変更してはならない
  • いかなるコマンドも sudo してはならない
  • ポート競合は人に報告するだけで、自分で処理してはならない
  • データベース操作は本プロジェクトのあの庫だけに限る
  • 手を動かす前に影響範囲を確認し、疑問があればまず聞く

この幾つかは今では agent を入れたすべてのマシンにコピーしている… agent はポートが占有されているのを見ると、第一反応は占有プロセスを殺すことで、それが隣のプロジェクトの本番サービスだとはまったく知らないのだ。

第二条の境界:記憶で API を書いてはならない

この罠はよくある。訓練データの中の API と、あなたの package.json の中のそのバージョンはまったく別物だ… バージョン差異を ❌→✅ の対照表に書く方法は別の二篇で例を挙げたので、ここでは協業の面でより重要なあの半分だけを語る。

対照表はあのルールファイルの中では実は最後の見せ場で、冒頭にまず据えられているのは三段階の SOP だ。まず resolve-library-id、次に query-docs、ドキュメントの結果を基準にコードを書き、さらにどの場面が「必ずドキュメントを調べる」かを明記する。先にプロセス、次に誤例、この順序がなかなか肝心だ——誤例は有限で、やがて古くなるが、プロセスならあなたが予見しなかったライブラリまでカバーできる。すべてのライブラリの罠を全員のために列挙することなど不可能だ。

もっと楽な道もある。新しいスキャフォールドは自分でリポジトリに「自分の訓練データを信じるな、node_modules の中のドキュメントを読め」という一段を差し込んでくれ、BEGIN/END のマーカーまで付いていて、はっきりとツールによる上書き用に残されている。取り決めを人が書く必要がなく、維持コストはゼロ、こういう安上がりは使えるなら使うべきだ〜

逆に言えば、禁止にも理由が必要だ。冒頭のリポジトリの docs/ARCHITECTURE.md:115 は技術スタックの表に挟まれた一行がある。クライアントの型はバックエンドの OpenAPI で生成され、tRPC/Prisma は導入しない。その直後に理由が続く——バックエンドは Rust axum の REST で、JS バックエンド契約などそもそも存在しない。この二つは TS エコシステムのデフォルトの答えで、AI はついでにインストールしてしまう。「tRPC を使うな」とだけ書いても止められず、次のセッションはいつでもこの条項は時代遅れだと思えるが、理由が付いていればこそ戒律ではなく事実判断になる。同じドキュメントの :148 はミラー側の権限まで制限している。もう一方は pydantic で DTO をミラーする「提出のみに用い、構造は永続化しない」——コピーはあってよいが、コピーが事実の情報源になってはならない。

第三条の境界:AI による大改修は人が読み切れなければならない

別のチームのリポジトリの PR テンプレートには、冒頭にこんなコメントがある。要旨は、複数人+AI の協働场景では PR が壊れたコードを本番に送り込む前の唯一の人間ゲートなので、正直にチェックを入れてほしい、というものだ。セルフチェックリストのうち二項目は明確に AI を狙っている。一つは PR のサイズを抑え、大きな変更は分割することを求める。理由は、人間が数分で読み切れなければならないからだ。もう一つは、提出者がその中の AI が書いた部分を一行ずつ確認し、責任を負うことを求めている。

この二項目は、どんな lint ルールよりも役に立つ。lint は構文レベルしか捕まえられない。このロジックが通ってはいるが間違っている、という類は捕まえられないのだ…

ゲートは pre-commit に置くか CI に置くか

同じリポジトリの選択は、pre-commit で全量の型チェック+全量の単体テストを走らせるというものだった。理由は設定のコメントに書いてある。CI は主干へのマージ時にしか発火せず、プレリリースブランチは直接プッシュで PR を開けないため、CI だけに頼ると破壊がマージまで露見しないのだ…

真似する価値のある細部が一つある。型チェックはプロジェクト全体が対象で、単一ファイルだけを渡すわけにはいかない。だからフックでは固定コマンドを返し、マッチしたファイル名はそのまま無視する。さらにシークレットスキャンの層も掛けており、ローカルではステージング分を、CI ではリポジトリ全体をスキャンする。

自分のいくつかのリポジトリと見比べると、ゲートの強度がひどくバラバラだ:

プロジェクトゲート強度
acg-siteCI の七段階(依存インストール → db:generate → Biome フォーマットチェック → ESLint → tsc → 単体テスト → build)+ pre-commit は lint-staged★★★★★
bbpslint に --max-warnings 0 を追加したが、typecheck は一か月後にようやく補った★★
vns-next"check": "biome check && tsc --noEmit" が一本あるだけで、CI も hook もない★★
acgn-flowtypecheck スクリプトすらなく、唯一の静的検証は eslint .★

bbps のケースが最もよく物語っている。lint は 2025-11 にはもうあったが、--max-warnings 0 は 2026-02 まで持ち越され、さらに typecheck と check は一か月後の 1d953fe9 でようやく package.json に補われた… 補ったその日に型の問題を一つ捕まえている——comments-section.tsx の中の、型宣言のないサードパーティ CSS のインポートで、その場でコメントによる免除を加えることになった。

そのリポジトリには、私が思いもしなかったゲートがもう一つある。言語をまたぐ契約を専門に守るもので、CI でコード生成をもう一度走らせ、その結果をコミット済みのものと比較する。

- run: pnpm gen:client
- name: Fail if OpenAPI/TS client drifted from committed source
  run: git diff --exit-code apps/api/openapi.json packages/api-client/src/schema.d.ts

一バイトでも違えば赤信号だ。docs/ARCHITECTURE.md:176 に「バックエンドを変える=一か所を変えればクライアントは自動で更新される」と原則として書いてあるが、この二行こそがそれを破れないものにしている。bbps と同じ手口で、このゲートを導入したコミットには「以前ドリフトしていたいくつかのエンドポイントを戻す」とついでに書かれている——デプロイのその場で既存のドリフトを捕まえたのだ。

ついでに、Biome と ESLint がどう役割分担して喧嘩しないようにしているか。acg-site の biome.json:20-25 は linter と assist を丸ごと無効化しており、Biome はフォーマット専用、lint はすべて ESLint に任せている。CLAUDE.md:109 にも明記してあるので、次に AI が Biome を見てまた linter を有効化したりはしない~

事故から 28 分後、教訓はスクリプトになった

これはそのリポジトリで見た中で最も見事な箇所だ。その日の午後、まずバックエンドが落ちた。コメント構造が自分自身を参照しており(replies: Vec<CommentOut>)、Rust の型システム上は完全に合法で、cargo build、clippy -D warnings、cargo test はすべて緑。しかし起動時に OpenAPI を生成すると無限再帰になり、スタックオーバーフロー、クラッシュループ、502 から始まって最終的にサイト全体が 500 に——純粋にドキュメント層のものが本番を落としたのだ。九分後に二件目。新しく追加したトップレベルページがミドルウェアのホワイトリストに入っておらず、i18n にローカライズ済みルートとみなされ、実行時に 404。しかも next build はルートが存在すると表示し、CI はすべて緑だった。

さらに十九分後、三件目のコミットがこの二つの教訓をデプロイスクリプト内の二つのスモークテストに変えた。13 行の shell だ:

# ①:真跑一次二进制,拦「build 全绿但启动就崩」
SQLX_OFFLINE=true timeout 30 target/release/api --openapi >/dev/null 2>&1 \
  || { echo "✗ 冒烟失败——中止部署,未传镜像"; exit 1; }

# ②:顶层页必须在 middleware 白名单里,否则运行时 404
for _d in apps/web/src/app/*/; do
  _n=$(basename "$_d"); case "$_n" in "[locale]"|dash) continue ;; esac
  [ -f "${_d}page.tsx" ] || continue
  grep -q "\"/$_n\"" apps/web/src/middleware.ts || { echo "✗ /$_n 未放行"; exit 1; }
done

泥臭いったらない。一つはバイナリを走らせ、一つは grep するだけ。だが秒単位で、決定的で、サービスを起動する必要も DB に接続する必要もなく、しかもインターセプト地点はイメージを転送する前——壊れた成果物はそもそもこのマシンから出られない。上の tsc、clippy、lint はどれも一段ずつ正式なものだが、実際にサイトを落とした二回は、すべてこの十三行が止めたのだ。

AI の成果物をどうレビューするか

あちらの二つの例が強く印象に残っている。一つは AI が大きく変更した後に、専ら粗を探すためのレビューで、十数個の確定した欠陥を一気に炙り出した。サーバーサイドリクエストフォージェリ、設定可能なアドレスがサーバーから直接 fetch される。バッチインポートがアトミックでなく、失敗したレコードまで成功数に数えられる。データダッシュボードがローリング 168 時間と自然日の二種類の七日間ウィンドウを混用している。キャッシュに書き込んだ後タグを付けていないため、後から古いデータを読んでしまう。こういう類は tsc も lint も一つも止められないのだ。

もう一つは、ある状態値がどのコードからも一度も書き込まれたことがなく、多段階のプログレスバーが永遠に最後の一歩まで到達しないというもの——四つの丸を描画しているのに、状態はせいぜい三つ目までしか進まない。本番 DB でこの値を使っている行数を数えたら、0 だった。

なかなか得難い回もあった。要件がほとんど記述されておらず、AI が防御的な解釈をしたうえで、コミットメッセージに「今後の仕様がこの解釈と食い違う場合、今回の変更は無関係な動作に等しい」と書いたのだ。これは正しく直すより貴重だ~

自分のリポジトリにも、人間のレビューで拾い上げて戻したものがある。acg-site の c2193c3 だ。ある PR でローカル開発用のフォールバックポートを :3000 から :4000 に変えたのだが、同じ PR の中で他の 8 か所は .env.example を付けてもなお :3000 のままだった。「ついでにコミットしてしまった汚い変更」と判定し、リポジトリ全体の一貫性を保つため差し戻した。

AI の比率が八割というあのリポジトリはもっと細かい。複数の視点で同じコードを独立にレビューし、出力は番号付きで段階分けしたリスト(H1/M1/L1)になる。結論も単純な合格か不合格かの二択ではなく、go-with-notes という段階がある——マージはできるが、この数点はフォローが必要だ、というものだ。捕まえたのはどれも lint では届かないタイミングの問題だった。並行処理下で先に読んでから書くことで互いに上書きし合う、effect の再実行が保存していない編集を吹き飛ばす、楽観的更新が失敗してもロールバックしない、といったものだ。

反面もここにある。この流れはリポジトリの中に一言も見つからない。常設のルールファイルも、CI job も、agent の定義もなく、数個の commit message の中にしか生きていない——どれだけ三段式で書いても、「教訓:」の段落や再現コマンドを添えても、次のセッションがたまたま git log をめくったときにしか手に入らないのだ。

worktree 並列化のいくつかの副作用

thdl では git worktree list で三行が見える。メインのリポジトリが main 上にあり、残りの二つが .claude/worktrees/heuristic-bardeen-567381 と .claude/worktrees/sweet-poincare-f3b7bc で、ランダム背景のあの機能はそのうちの一つで作った。便利だが、副作用が三つある。

worktree は完全な checkout なので、lint やフォーマットに引っかかる

acg-site の eslint.config.mjs:22-25 はそのまま無視リストに加えている:

// IDE local history
".history/**",
// Claude Code worktrees
".claude/**",

biome.json の files.includes でも同様に除外している。加えないと、lint を一度走らせるだけで同じコードを二度スキャンすることになる…

lockfile が複数あると、バンドラがワークスペースのルートを選び間違える(worktree は原因の一つにすぎない)

bbps の 449d72b3 は next.config.ts に turbopack: { root: process.cwd() } を追加した。コメントは「lockfile が複数あるときの Turbopack のルートディレクトリ推定警告を避ける」で、その頃はまだ worktree とは関係なかった。home の 54b4590 は同じ類の問題にぶつかったが方向は逆で、Next に親ディレクトリの pnpm-lock.yaml からワークスペースルートを推定させないようにする必要があった。worktree はさらにもう一つの発生源にすぎない。

誤ってコミットしやすい

home では一度、.claude/worktrees/ 配下のとあるサブディレクトリを gitlink としてコミットしてしまい、ついでに 12 個の .history/ ファイルと一つの .env まで持ち込んだ——git add -A の典型的な副産物で、commit message には一言も書かれていなかった。その後 .history を .gitignore に追加したものの、gitignore は追跡済みのファイルを untrack しないので、今日に至るまでその一群はバージョン管理の中に横たわっている。なんなんだよ、毎回自分に -A は使うなと言い聞かせているのに…

だから worktree のディレクトリは .gitignore と各種 ignore リストの両方に忘れずに加えること。一つ欠ければそれだけで落とし穴になる~

結局のところ、これらの項目はどれもコードの問題ではなく、誰が何を読めて、誰が何に触れるかという話だ。ツールがどれだけ増えても、この二つのことはやはり人間が決めなければならないのだね~