本文へスキップコンテンツへスキップ
深掘り

複数のコーディングエージェントで1つのリポジトリ: ロックではなく宣言で

並列で動くコーディングエージェントは、どれもほかのエージェントが何を編集しているかを知らないため、マージの段階で衝突します。Foxl Codeのクラウドエージェントは、これから変更するファイルを宣言し、あなたのほかの稼働中のエージェントのうち、重なるパスをすでに宣言しているのがどれかを受け取るようになりました。宣言はどちらの場合も記録され、何もロックしません。衝突したエージェントには、持ち主に変更を送るか、待つか、あなたに尋ねるよう指示されます。出荷済みコマンドの実際の出力、globのルール、データベースのコスト、そしてエージェントが本当にルールに従うのか(まだわかっていません)を紹介します。

Foxl Team更新 13 分で読めます

角に2つのエージェントの札(青とオレンジ)が結びつけられたファイルと、その横に置かれた開いた南京錠。ファイルへの2つの宣言はどちらも記録され、何もロックされていません。
このページの内容
2026年10月27日 に、リリース済みの実装と残っているテストで内容を確認しました。

1つのリポジトリで複数のコーディングエージェントを動かすと、マージの段階で衝突します。どのエージェントも、ほかのエージェントが何を編集しているかを知らないからです。Foxl CodeはClaude CodeやCodexなどのコーディングエージェントをクラウドで動かしています。v0.9.3-beta.5からは、各エージェントに対し、編集する前にfoxl-agents claim <paths>を実行して、これから変更するパスを宣言(claim)するよう指示しています。同じユーザーのまだ動いている別のエージェント(稼働中のエージェント)が重なるパスをすでに宣言していれば、返答にそのエージェントと、それが残したメモが示されます。何もロックされません。衝突したエージェントには、必要な変更を宣言の持ち主に送るか、持ち主が宣言を解除するのを待つか、ユーザーに尋ねるよう指示されます。エージェント自身の宣言は、実行が終わると消えます。

並列で動くコーディングエージェントが衝突する理由

各エージェントは自分専用のチェックアウトの、自分専用のブランチで作業するので、2つのエージェントが同じファイルを変更するのを止めるものは何もありません。Gitがテキスト上の衝突を報告するのは2つ目のプルリクエストをマージするときで、そのときには両方のエージェントの作業が終わり、費用も支払い済みです。意味上の衝突、つまり2つのプルリクエストがそれぞれのチェックは通るのに、両方マージするとビルドが壊れるという衝突は、人が両方をマージするまで何も報告しません。宣言がこの種の衝突を捉えられるのは、2つのエージェントが同じパスに触れる場合だけです。

何が役立つかは、私たち自身の環境が教えてくれました。私たちはFoxlのリポジトリで15から20のClaude Codeセッションを並列に動かしながらFoxlを開発していて、14日間でセッションどうしが4,773件のメッセージを送り合いました。互いの変更を上書きせずに済んだのは、共有の担当リストのおかげです。1つのコーディネーターのセッションが、どのセッションがどのファイルを担当しているか(多くのセッションが触れるファイルについては、そのどの部分か)を記録し、担当していないファイルに変更が必要になったセッションは、自分で変更する代わりに担当者へ変更内容を送ります。リストができる前は、複数のセッションが同じファイルを同時に編集し、マージの段階で初めて気づいていました。リストにはリストなりのコストもありました。その14日間にセッションが作業を止められた123回のうち、33回はコーディネーターの返事待ちでした。

Foxl Codeにはすでにメッセージの仕組みがありました。各クラウドエージェントが、同じユーザーのほかの稼働中のエージェントを一覧し、メッセージを送れるfoxl-agentsコマンドです。宣言は、コーディネーターなしで担当リストを実現します。各エージェントが何に触れるかを伝え、データベースが1回のリクエストで答えます。

なぜロックではなく宣言なのか

ロックが機能するのは、すべてのエージェントがそれに従い、すべてのエージェントがそれを解除する場合だけです。クラッシュしたり、予算を使い切ったり、キャンセルされたりしたコーディングエージェントは何も解除しないので、ロックにはタイムアウトが必要になります。そして実際の実行に足りるだけ長いタイムアウト(私たちの実行は7時間を超えることがあります)は、そのあいだ全員を止めてしまいます。互いに相手のファイルの1行ずつを必要とする2つのエージェントは、永遠に待ち合うことになります。しかも、ロックを拒否されたエージェントには、止まる以外にやることが残りません。

宣言は、これらをそれぞれ回避します。クラッシュした持ち主は、実行が稼働中でなくなった時点で数に入らなくなるので、タイムアウトを待つ必要はありません。ほかの宣言と重なる宣言も拒否されず、記録されたうえで、その重なりが持ち主のタスクID、タイトル、宣言したパス、メモとともに、問い合わせたエージェントに返されます。そして衝突したエージェントには、まず必要な変更を持ち主に送るよう指示されるので、互いのファイルを必要とする2つのエージェントにも、待つ以外にやることがあります。もっとも、両方が結局待ってしまうのを防ぐものはありません。すべてのエージェントが受け取る指示は、一字一句このとおりです。

- Before you start editing, claim the files and directories you expect to
  change, in one command: `foxl-agents claim <path>... --note "<why>"`. A
  directory claims everything under it; `*`, `?` and `**` globs work. Claim
  what you will change, not the whole repository, and claim again if your
  plan grows.
- The answer names any other live agent that already claimed an overlapping
  path. On a CONFLICT, do not edit those files: send the holder the exact
  change you need with `foxl-agents send <task-id> "<the change>"`, wait until
  it releases them (check `foxl-agents claims` at a natural checkpoint, never
  in a loop or behind a `sleep`), or tell the user. A claim is information,
  not a lock.
- `foxl-agents release <path>...` drops a claim you no longer need. Every
  claim you hold is dropped when your run ends.

これらの行は、各CLIが読み込むユーザーレベルの指示ファイルに追記されます。Claude CodeならCLAUDE.md、Codex、Kiro、Cursor、Hermes、OpenCodeならAGENTS.mdです。同じ仕組みを6つの異なるCLIに届ける必要があるため、foxl-agentsはMCPサーバーではなくシェルコマンドになっています。どのコーディングCLIもシェルコマンドは実行できるので、それぞれにサーバーを読み込む設定をしなくても、1つのスクリプトが6つすべてで動きます。

エージェントが実行するものと、目にするもの

以下は、出荷済みのコマンドの実際の出力です。エージェントではなくスクリプトから、デモ用リポジトリの2つのデモタスクについて実行しました。タスクAはレート制限を追加するCodexのタスク、タスクBはusersエンドポイントにページネーションを追加するClaude Codeのタスクです。サーバー、そのデータベース、コマンドは、クラウドエージェントが使うものと同じです。

ターミナルの出力。タスクAが「adding a token bucket per API key」というメモ付きでsrc/api/middleware.tsとsrc/api/routes.tsを宣言し、重なるパスを宣言した稼働中のエージェントはほかにないと返されます。続いてタスクBがディレクトリsrc/api/を宣言すると、タスクA、そのタイトル、2つのファイル、メモを示すCONFLICTが返され、そのあとに、持ち主に変更を送る、解除を待つ、またはユーザーに伝えるという指示が続きます。稼働中の宣言の一覧には、両方のタスクの宣言が表示されています。
図1. 出荷済みのサーバーをworkerd(CloudflareのWorkersランタイム)でローカルに動かし、出荷済みのコマンドを実行して取得した出力(2026年10月23日)。タスクBの宣言も記録されています。

タスクBは、ディレクトリを示す末尾のスラッシュ付きでsrc/api/を宣言し、サーバーはそれをsrc/api/**として保存しました。スラッシュがなくても、コマンドが自分で/**を付け足していたはずです。コマンドはチェックアウトの中で動くので、ファイルとディレクトリを区別できる唯一の場所にいるからです。コマンドはまた、作業ディレクトリがどこであっても、すべてのパスをリポジトリのルートからの相対パスに書き換え、チェックアウトの外のパスは何かを送る前に拒否します。

衝突の返答にはタスクAの2つのファイルとメモが示され、タスクBはそのメモを見て判断します。この例なら、ミドルウェアには手を付けずに、そこで必要な1つの変更をタスクAに送ることもできますし、エンドポイント自体のハンドラーから取りかかり、タスクAが解除したら改めて確認することもできます。

2つのglobが重なるかどうかはどう判定するのか

2つの宣言は、両方に一致するパスが少なくとも1つあれば重なっているとみなします。文法は、あえて小さくしています。

  • 普通のパスは、ワイルドカードのないglobです。
  • *は1つのパスセグメントの中で一致します。
  • ?はちょうど1文字に一致します。
  • ちょうど**だけのセグメントは、任意の数のセグメント全体に一致します。
  • それ以外の文字はすべて文字どおりに扱うので、[id].tsxは文字クラスではなくファイル名です。

出荷済みのマッチャーが、いくつかの組み合わせに対して返す結果は次のとおりです。

宣言A宣言B重なり
src/api/**src/api/users.tsあり
src/*.tssrc/api/users.tsなし。*は1つのセグメント内にとどまる
src/**/*.test.tssrc/api/*.tsあり。どちらもsrc/api/x.test.tsに一致する
docs/**src/**なし
src/app/[id]/page.tsxsrc/app/a/page.tsxなし。[id]は文字どおりに扱われる
**/*.mddocs/guide.mdあり

重なりを理由に宣言が拒否されることはありませんが、いくつかの形は拒否されます。

  • **や**/*のように、**を含むワイルドカードだけでできたglob。リポジトリ全体を宣言することになるからです。
  • ..のパスセグメント、バックスラッシュ、制御文字。
  • 先頭の!。ここでは否定の意味を持たないので、否定のように見えてはいけないからです。

1つのタスクが持てる宣言は最大50件で、それぞれ最大200文字、メモは最大280文字です。

比較には上限があり、変わった宣言の組み合わせがあってもサーバーのCPUを大きく消費しないようにしています。先頭の文字どおりのセグメントが異なる2つの宣言(docs/**とsrc/**)は、すぐに除外されます。それ以外の場合、サーバーはセグメントの組を、そしてセグメントの中では文字の組を、メモ化しながらたどります。1つのリクエスト内のすべての比較は50,000ステップの予算を共有し、予算が届かなかった組は捨てられるのではなく、重なりの可能性ありとして報告されます。そのため、予算を使い切ったリクエストでもエージェントには知らされます。

宣言が終わるとき

宣言は1回の実行に属します。実行が終わると、タスクは実行の完了を記録するのと同じステップで、自分が行った宣言をすべて取り下げます。そのステップまでたどり着かなかった実行には、読み取り時の2つのチェックで対処します。持ち主がもう稼働中でない宣言は無視され、実行が続きうる最長時間である7時間20分より古い宣言も無視されます。宣言の公開範囲はメッセージと同じです。同じリポジトリで同じGitHub Appのインストールを通じて作業している、1人のユーザーの稼働中のエージェントどうしだけが、互いの宣言を見られます。

2つのエージェントと1つの宣言テーブル。エージェントAが2つのファイルを宣言し、重なりはないと返されます。エージェントBがそれらを含むディレクトリを宣言すると、その宣言は書き込まれ、エージェントAを示す衝突が返されます。エージェントBには、エージェントAに変更を送る、エージェントAの解除を待つ、ユーザーに尋ねる、という3つの選択肢があります。実行が終わると、その宣言は削除されます。
図2. 衝突は、問い合わせたエージェントに返されます。重なりを理由に拒否されるものはなく、ロックを待つものもありません。

無料のデータベースプランでのコスト

宣言は1つのテーブルに保存され、インデックスは(ユーザー、リポジトリ、作成時刻)の1つだけで、それをポーリングするものはありません。どの読み取りも直近7時間20分に作成された宣言だけを求め、作成時刻はインデックスの最後の列なので、古い行はデータベースがスキャンする範囲から外れます。誰も解除しなかった古い宣言は、1,000件あっても2,000件あっても、読み取りに1行を加えるだけです。両方とも計測しました。

これを計測するのは、FoxlがCloudflareの無料プランで動いているからです。このプランでは、アカウント内のすべてのD1データベースが1日あたり500万行の読み取りと100,000行の書き込みを共有し、アカウントには1日100,000件のWorkerリクエストが割り当てられます。以下の数字は、CloudflareのランタイムであるworkerdでローカルにD1を動かして得たもので、本番のD1自身のメトリクスではまだ確認していません。

呼び出し読み取る行数書き込む行数
N個のパスを宣言リポジトリ上の稼働中の宣言1件につき約2行、さらにパス1つにつき約1行と、固定の読み取りが数行パス1つにつき2行
宣言の一覧1行+稼働中の宣言1件につき2行0
解除パス1つにつき約1行解除した行1つにつき1行
一度も宣言しなかった実行の終了00

全ユーザー合わせて1日約50件のクラウドタスクがあり、それぞれが宣言の呼び出しを2回行うとすると、1日100回の呼び出しになります。1回の呼び出しはWorkerリクエスト2件分(code.foxl.aiで1件、サーバーへの内部ホップで1件)なので約200リクエスト、無料プランの100,000件の0.2%です。仮にそのすべての呼び出しが、忙しいユーザーの最悪のケース(1つのリポジトリで10のエージェントがそれぞれ5件の宣言を持つ場合)と同じコストだったとしても、1日の読み取りは約11,000行(500万の0.2%)、書き込みは約830行(100,000の0.8%)です。

エージェントは本当にルールに従うのか

まだわかりません。このフェーズは、それを確かめるために作っています。私たち自身のローカルのセッションでも、同じルールを守らせるにはコーディネーターが必要でした。ですから、指示にそう書いてあるというだけで、6つの異なるCLIが編集前に宣言し、衝突したら引き下がるとは考えていません。衝突が起きるたびに監査記録が書き込まれ(各サーバーインスタンスは、同じタスクと持ち主の組み合わせについて10分以内の重複を省きます)、エージェントが別のエージェントに送るメッセージも同様に記録されます。エージェントが持ち主にメッセージを送ったかどうかは監査ログに、宣言を解除したかどうかは宣言のテーブルに、それでもファイルを編集したかどうかはプルリクエストの差分に表れます。

次のフェーズは、その結果しだいです。エージェントが宣言を守るなら、1つの依頼を複数のエージェントに分けるFoxl Codeの部分で、各サブタスクにファイルの一覧を渡し、重なるサブタスクは同時ではなく順番に実行する計画です。2026年10月23日の時点で、本番のテーブルは空です。この機能をデプロイしてから、クラウドのタスクが1つも始まっていないからです。

似たものを作るなら、私たちが残したい性質は3つです。宣言は必ず記録されること、重なりは持ち主とメモ付きで返されること、そして宣言は実行が続くあいだだけ有効であることです。Foxl Codeの公開時の記事では、エージェントのループそのもの、つまり各ステップを信じるのではなく、目標に向けて状態をすり合わせていく仕組みを解説しています。

参考資料

  1. Cloudflare D1の料金と無料プランの制限ドキュメント
  2. Foxl v0.9.3-beta.5のリリースリリース