Claude Code のサブエージェントを並列で走らせて、1日で静的サイトに6ページ出荷した。動いた。ただ、壊れたところは全部「共有資源」だった。
この記事は成功談ではなく、同じ日に踏んだ地雷の記録です。並列でエージェントを回そうとしている人が、同じ穴に落ちないために書いています。
何をやったか
資格試験(宅建)の学習サイトに、○×形式・計算形式の問題ページを同じ日に6枚追加しました。
- サイトは静的HTML(GitHubリポジトリ → Cloudflare Pages)。別に Next.js + OpenNext on Cloudflare Workers のサイトももう1本あり、そちらでも別のエージェントが並走していた
- サブエージェント6体を並列起動。1体=1ページを担当し、「作問 → e-Gov法令APIで条文原文を取得して照合 → HTML実装 → 自分が触ったファイルだけcommit → push → 本番URLにcurlして実測」まで一気通貫でやらせた
- 出荷結果(記事執筆時点で再curlして確認)
| ページ | 問数 | 本番サイズ |
|---|---|---|
| クーリング・オフ | ○×17問 | 25,678 bytes |
| 農地法 | ○×14問 | 25,394 bytes |
| 報酬計算 | 4択13問 | 29,339 bytes |
| 意思表示 | ○×18問 | 28,991 bytes |
| 建蔽率・容積率 | 4択13問 | 32,643 bytes |
| 開発許可 | ○×22問 | 35,122 bytes |
合計97問。全ページ HTTP 200。
表のページ名は実物へのリンクです。バイト数は記事執筆時点の実測値で、その後も加筆しているので現在の値とは一致しません(追試するなら curl -s <URL> | wc -c で今の値が取れます)。
完了条件を「本番URLへの curl 実測」に固定したのが唯一うまくいった設計です。git push は完了ではない。過去に「autoDeploy: true と書いてあるのに webhook が未接続で、push しても本番が10分経っても古いまま」という事故を踏んでいるので、各エージェントの完了条件を
- 本番URLが HTTP 200
-
<title>が意図どおり -
中身の要素数(設問の
data-a属性の個数)が期待値と一致
まで下ろしました。「200が返る」だけだとソフト404を通してしまいます。
では、壊れたところの話をします。
地雷1:ビルド成果物ディレクトリを消し合う
一番わかりやすく落ちたのがこれです。Next.js + OpenNext のリポジトリ側で起きました。
✘ [ERROR] The entry-point file at ".open-next/worker.js" was not found.
wrangler deploy が突然これで落ちる。ビルドは成功していたのに。
原因:自分のビルドが終わった直後に、別のエージェントが同じリポジトリで opennextjs-cloudflare build を開始した。OpenNext のビルドは .open-next/ を最初にクリーンするので、こちらのデプロイ対象が消えた。
冷静に考えれば当たり前で、.open-next/・dist/・.next/・node_modules/ は単一プロセス前提のグローバル可変状態です。ワークツリーが同じなら、エージェントが増えた瞬間に競合します。gitのブランチを分けても、ビルド出力先が同じなら意味がない。
対処(採用したもの):
# ビルド前に他のビルドが走っていないか確認する
ps aux | grep -E "opennext|wrangler" | grep -v grep
これを「ビルド前の必須手順」としてエージェントのプロンプトに書きました。
そしてもう一つ重要な回復手順があります。事故のとき、相手のビルドは自分のコミットを含む同じHEADから作られていました。つまり相手のデプロイに自分の変更も乗っていた。だから正しい対処は「もう一度ビルドし直す」ではなく、
- 相手の deploy 完了を待つ
- 本番URLに curl して、自分の変更が反映されているか実測する
- 反映されていれば、自分は何もしない
でした。並列環境では「自分のデプロイが失敗した」と「自分の変更が本番に出ていない」は別の事象です。ここを混同すると、無駄な再ビルドでもう一度衝突を起こします。
根本的にやるなら、リポジトリごとにワークツリーを分ける(git worktree)か、ビルドをミューテックスで直列化するかですが、今回は「1リポジトリ1ビルダー」を運用で守る方を選びました。
地雷2:git add -A が他人の作業中ファイルを巻き込む
別の日にやらかした事故ですが、今回の並列運用の前提になっているので書きます。
未コミットの変更を保全しようとして git add -A した結果、別セッションが編集途中だった server.js を巻き込んでコミットしました。相手のローカルから編集中の内容が一時的に消えます。
対処:
-
git add -Aを禁止。自分が触ったファイルだけを明示的に add する -
git statusに身に覚えのない変更があったら、他エージェントを疑って止まる - push 前に
git pull --rebase --autostash(相手のWIPを退避してrebase、あとで復元)
ここまでは普通です。問題は共有ファイルでした。
6体それぞれが、ハブページ(ichimon/index.html)・記事一覧(blog/index.html)・sitemap.xml に自分の1行だけを追加する必要がある。でも同じファイルを他の5体も同時に編集している。ファイル単位の add では相手の編集中の行まで staging されてしまう。
採用した手口:ワークツリーを触らずに、HEADのblobに1行だけ挿入したものを直接インデックスに置く。
# 1) HEAD時点の内容を取り出す
git show HEAD:ichimon/index.html > /tmp/base.html
# 2) 自分の1行だけを挿入した内容を作る(sed / python なんでもよい)
python3 - <<'EOF'
src = open('/tmp/base.html').read()
line = '<li><a href="/ichimon/ishihyoji">意思表示の一問一答</a></li>\n'
open('/tmp/mine.html','w').write(src.replace('<!-- CARDS -->', '<!-- CARDS -->\n' + line))
EOF
# 3) blobを作ってインデックスに直接置く(作業ツリーは触らない)
BLOB=$(git hash-object -w /tmp/mine.html)
git update-index --cacheinfo 100644,$BLOB,ichimon/index.html
# 4) この状態でcommit
git commit -m "add ishihyoji card"
git update-index --cacheinfo <mode>,<sha>,<path> は、作業ツリーの内容と無関係にインデックスを書き換えるので、他エージェントが編集中の同ファイルのWIPを巻き込まずに、自分の差分だけをコミットできます。
副作用として、コミット後は作業ツリーとインデックスがズレます(git status に相手のWIPが差分として残る)。これはそのまま放置するのが正解で、git checkout で消してはいけません。相手の作業です。
地雷3:ブラウザのタブを奪い合う
ブラウザ自動化MCP(Claude in Chrome)を使って375px幅の表示検証をさせていたら、別のエージェントが人間の開いていた Search Console のタブを乗っ取りました。同じ日に2体が同じ事故を起こしています。
原因:多くのブラウザ自動化ツールは tabId を省略できます。省略すると「いま最前面にあるタブ」を操作する。エージェントは自分が最前面だと思っているが、実際には別のエージェントや人間が操作中のタブが前面にいる。
対処:
- 各エージェントは起動時に必ず自分のタブを作る(
tabs_create相当) - 以後の
navigate/click/read_pageの全呼び出しにtabIdを明示的に渡す。省略を禁止する - 終了時に自分が作ったタブだけを閉じる。人間や他エージェントのタブは触らない
- エラーで中断した場合も閉じる。閉じ忘れが積もるとタブ上限に達して、後続エージェントの検証が丸ごと落ちます(実際に3回潰れました)
- 奪ってしまったら、元のURLへ navigate で戻す
「デフォルト引数が暗黙のグローバル状態を指している」という、地雷1とまったく同じ形です。並列化すると、APIの省略可能な引数は全部おかしくなると思っておいたほうがいい。
地雷4:一番怖いのは「エラーにならない衝突」
ここからが本題です。
6体は全員、指示どおり正しく動きました。全員が「自分が触ったファイルだけ」をコミットし、全員が本番curlで200を確認し、誰一人としてエラーを出していない。CIも通る。
それでも壊れました。
ハブページに、同義の見出しが2つできたのです。
<h2>論点別の一問一答(頻出テーマを深掘り)</h2>
→ クーリングオフ / 農地法 / 開発許可 / 意思表示 の4枚
<h2>論点別の問題</h2>
→ 報酬計算 / 建蔽率・容積率 の2枚
○×形式のページを作った4体は前者に、計算形式のページを作った2体は後者に、それぞれ「妥当な判断で」カードを追加した。結果、同じカテゴリの6枚が2箇所に分散し、読者からは別カテゴリに見える状態になった。
これは分散データベースの write skew とまったく同じ形です。個々のトランザクションは制約を守っている。合成した結果だけが制約を破っている。しかもどのトランザクションも失敗していないので、どこにもエラーが出ない。
git のコンフリクトは「同じ行を触ったとき」しか教えてくれません。違う行に書いた2つの正しい決定が矛盾するケースは、git は検出できない。テストも通る。気づくのは人間が本番を見たときだけです。
対処:
-
共有ファイルの「構造」を変更する権利を1体に集約する。他のエージェントは既存の構造に追記するだけ(新しい
<h2>を作ってはいけない) - 構造を作る担当を先に走らせ、それが終わってから残りを並列で走らせる(構造フェーズと追記フェーズを直列に分ける)
- それでも起きるので、最後に整理役のエージェントを1体走らせる。今回は後追いの整理エージェントが6枚を1つのリストに統合しました(カードの削除はせず統合のみ、という指示つきで)。統合後のハブがこのページで、いま
<h2>は「分野別の一問一答」「論点別の問題(頻出テーマを深掘り)」の2つだけです。事故当時は後者が同義の2見出しに割れて、6枚がそこに分散していました
そして、これを見つけた3体のエージェントが、自分では直さずに「申し送り(要判断)」として報告してきたのは正解でした。自ファイル外の構造変更は、並列環境では別の衝突を生みます。「気づいたが直さない」を報告として許可しておくのは、並列運用ではかなり効きます。
地雷5:教訓を書いても再発する
ここが個人的に一番の学びでした。
事故のたびに lessons_learned.md という教訓ファイルに追記していました。読まれませんでした。
理由は単純で、サブエージェントは最小コンテキストで起動するからです。トークンを節約するために「必要なファイルだけ読め」と指示している。そのエージェントは、自分がこれから踏む地雷を知らないので、教訓ファイルを読む理由がない。「関連ファイルを読め」と書いても、何が関連かは踏んでみるまで分からない。
教訓の置き場所を、「読むかもしれないファイル」から「入力に必ず入るブロック」に移しました。
具体的には、サブエージェント指示のテンプレートを作り、削除禁止の共通ルールとして教訓を埋め込みました。実際に効いた項目:
### 安全装置
6. 既存の意思決定と矛盾する指示だと気づいたら、実行せず報告して止まる。
### git
8. `git add -A` を使わない。自分が触ったファイルを指定して commit する
### ブラウザ
10-b. ブラウザ作業は必ず「自分のタブ」を作ってから始める。
以後 navigate・click・read_page 等に必ず tabId を渡す。
10-c. ビルド成果物ディレクトリは共有資源。ビルド前に他が同じリポジトリで
作業中か確認し、作業中ならビルドせずに待つか、相手のデプロイに相乗りする
(相手のHEADに自分のコミットが入っていれば本番反映は済む=curlで実測する)
特に6番が実際に事故を止めました。「運営者表記を全サイトで統一しろ」という私の指示に対して、あるエージェントが「その指示は、別ドメインを意図的に分離している既存の決定と矛盾します」と実行せずに報告してきた。私が忘れていた過去の設計判断でした。
「止まっていい」「疑っていい」を明示的に権限として渡さないと、エージェントは指示を実行してしまう。 これは並列かどうかに関係なく、エージェント運用の一番大事な設定だと思っています。
一般化するとこうです:教訓は、エージェントの入力に必ず入る場所に置かないと存在しないのと同じ。ドキュメントに書くのは人間のための行為であって、エージェントのための行為ではない。
おまけ:「作らない」も出荷である
並列で作れるようになると、作るコストが劇的に下がります。1日で6ページ出せる。すると相対的に「作る理由の検証コスト」が高くつくようになる。何も考えないと、誰も欲しがっていないものを高速で量産することになります。
同じ日、別のエージェントには「新商品を作れ」ではなく「まず需要を測れ」と指示しました。やったのは note の検索APIで、狙っている棚の有料記事のスキ数分布を実測することです。
# note検索API(人気順)で棚のシグナルを取る
# GET https://note.com/api/v3/searches?context=note&q=<クエリ>&sort=popular
# → 各記事を個別GETして likeCount を取得
結果:
| 棚 | 有料記事の最高スキ数 |
|---|---|
| 比較基準にした別ジャンル(実売が確認できている棚) | 217 |
| 狙っていた棚A | 44 |
| 狙っていた棚B | 5 |
自分が既にその棚に置いている商品は販売0本でした。
→ 新商品は作らない判断をして、既存商品の改修に工数を回しました。「実売が確認できない棚に2品目を置くのは、過去の失敗の再生産」という判断です。
エージェントを並列で回す環境では、「作らない」という結論を成果として記録できるようにしておくのが要ると思っています。そうしないと、エージェントは何か作らないと仕事をした気がしないので、必ず何か作ってきます。
まとめ:並列エージェントのチェックリスト
- 共有資源を先に棚卸しする — ビルド出力ディレクトリ、gitインデックス、ブラウザのタブ、複数体が追記するHTML。「単一プロセス前提のグローバル可変状態」を全部書き出す
- 共有資源には所有者を1体決める。他は追記のみ。構造変更フェーズと追記フェーズを直列に分ける
- 完了条件を本番実測に固定する。push は完了ではない。200 だけでも足りない(中身の要素数まで見る)
- 教訓はドキュメントではなくプロンプトに埋める。読まれないファイルに書いた教訓は存在しない
- 「止まる」「疑う」「作らない」を権限として明示的に渡す
エラーになる衝突は、いずれ直ります。エラーにならない衝突(地雷4)のほうがずっと高くつきます。
こうしたエージェント運用の設計まわりを、もう少し体系立てて書いたものがあります(有料note)。
📝 Claude Codeに「会社」を任せたら、人間の僕がボトルネックだった——3日放置しても事業が止まらない仕組みの全実装
ここで書いた「複数エージェントを同じリポジトリで並走させる」構成は、もともと自分のサイト(毎日ラボ)を作るために組んだものです。同じやり方でのツール制作(¥55,000〜)や、メディア運用の伴走(月額 ¥110,000〜)も仕事として引き受けています。範囲と料金の目安はこちらにまとめました → https://mainichi-lab.com/contact?utm_source=qiita&utm_medium=article&utm_campaign=work_inquiry&utm_content=qiita_001_contact