複数のsandboxを同時に立ち上げて開発していると、それぞれの作業環境が独立しているのは便利な一方で、別のsandboxが何をしているのか分からなくなる。
例えば、一つのsandboxで調査して設計方針を決め、別のsandboxで実装を始めるとする。新しい側には前の会話がなく、なぜその方針になったかも、何が未解決なのかも分からない。結果として同じ調査をやり直したり、すでに決めたことを再検討したりする。別のworkerが編集中のファイルに気づかず、作業がぶつかることもある。
会話履歴はsandbox間で共有されないので、毎回人間が背景を説明し直すことになる。かといって、会話を丸ごとコピーするのは量が多く、次のworkerに必要な情報を探しにくい。
そこで、共有する情報を会話ではなく、開発コンテキストに限定することにした。決定事項、現在の作業、変更したファイル、次の人への注意点が分かれば、次のsandboxは同じ前提から始められる。これを実現するCLIツールとして ctx-sync を作った。
アイデア:会話ではなく、作業に必要な事実を共有する
最初に考えたのは、別のsandboxへ会話をそのまま渡すことではない。会話には試行錯誤や一時的な仮説、デバッグログなどが大量に含まれる。これを全部同期しても、新しいworkerが読むには重いし、共有する必要のない情報まで混ざる。
共有情報を次の三層に分けた。
| 共有する情報 | 例 |
|---|---|
| Project | 目的、現在の構成、採用済みの設計判断 |
| Coordination | workerの状態、作業内容、変更ファイル、引き継ぎ事項 |
| Local | 会話、推論、試しただけの案、一時的なデバッグメモ |
共有するのは「なぜこの設計に決まったか」「今どこまで終わっているか」「次の人が注意すること」。推論過程や会話ログそのものは各sandboxのローカルに置き、他のworkerが行動するのに必要な事実だけを残す。
実装:GitHub GistをGitリポジトリとして使う
この共有状態をどこへ置くか。プロジェクトのGitリポジトリへ全部入れると、workerごとの一時的な進捗まで通常のソース管理へ混ざる。独自サーバーを立てると、認証や運用が必要になる。
そこで共有先にはGitHub Gistを使うことにした。GistはGitリポジトリとしてcloneできるので、特別なサーバーを用意せず、既存のGit認証を使って複数のマシンやsandboxから同期できる。
ctx-syncはRust製のctx-sync CLIで、プロジェクト側にはGistを特定する.ctx-sync.tomlを置く。ローカルのcloneやworker identityはプロジェクトディレクトリの外へ保存し、既定ではmacOSのApplication SupportまたはLinuxの~/.local/state/ctx-syncを使う。
Gist側は次のようなファイル構成になっている。
00-meta.json
10-project.md
20-architecture.md
30-decision-<date>-<number>-<slug>.md
40-worker-<short-id>.md
設計判断は一つずつ別ファイルにして、古い判断を書き換えるのではなく、新しい判断を追加して置き換える。各workerは自分の40-worker-*.mdだけを更新する。この分離により、複数のworkerが同時に進めていても、別のworkerの作業状態を上書きしにくくしている。
作業開始と引き継ぎ
新しいsandboxで作業を始めるときは、ctx-sync agent startを実行する。
ctx-sync agent start
-> 必要ならこのcheckoutをGistへattach
-> worker identityを登録
-> 最新の共有contextをpull
-> architecture、決定事項、作業中worker、注意点を表示
新規プロジェクトなら、最初にGistを作ってworkerを登録する。
ctx-sync init --project example --create-gist --secret
ctx-sync register sandbox-a
ctx-sync agent start
生成された.ctx-sync.tomlをプロジェクトへcommitしておけば、別のworktreeやsandboxから同じGistを見つけられる。参加する側はfresh checkoutでctx-sync agent start --name sandbox-bを実行すればよい。
作業を終えるときは、何を変更したか、重要なファイル、未解決の問題などをworkerのhandoffへ記録する。
ctx-sync agent finish --summary "設定読み込みを追加" \
--attention "CLI互換性のテストが未実施"
次のsandboxがagent startすると、最新contextをpullしてからonboarding情報を受け取る。各workerが個別に長い引き継ぎ文を書いて会話へ貼る代わりに、一定の形式で共有状態を更新する流れ。
既存プロジェクトへ追加する場合は、ctx-sync bootstrapでリポジトリから共有候補を集められる。--applyを付けてもすぐにGistへ同期するのではなく、まず共有文書へローカル出力する。中身を人間が確認してからctx-sync syncで公開する。
同時更新は自動で潰さない
複数sandboxが同じGistを更新する以上、競合は起こりうる。
ctx-syncの同期はGitのfetch、rebase、pushを行う。別workerが先にpushしていた場合はrebaseしてからpushを試す。fetchからpushまでの間に再び更新されてnon-fast-forwardになったら、最大3回までやり直す。
同じファイルを別々に編集してrebase conflictになった場合は、自動でどちらかを採用しない。rebaseをabortして競合したファイルを表示し、人間が解決できる状態で停止する。共有contextは後続の作業判断に使われるので、内容を黙って上書きするより、競合を見える形にする方を選んだ。
各workerが別々のファイルを所有するのは、競合自体をなくす仕組みではない。ただ、独立して更新できる単位を分け、競合が起きた場合にもどこが衝突したか分かりやすくする。
何でも共有しない
Gistをsecretとして作っても、URLを知っている人は読める。認証情報やAPI tokenを保管する場所ではない。
handoffとdecision addでは、sk-、ghp_、github_pat_や秘密鍵ヘッダーなど、明らかなcredentialらしき文字列を警告する。ただし、これはパターン検出であって秘密情報を完全に見つける仕組みではない。共有前に内容を確認する必要がある。
そのため、ctx-syncが同期するのは作業に必要な短い事実だけにする。デバッグログや会話を丸ごと貼るのではなく、「このエラーは再現済み」「このファイルは別workerが変更中」「この設計判断は採用済み」のように、次の作業で使える形にまとめる。
オーケストレーターではない
ctx-syncはworkerを起動したり、タスクを割り当てたり、sandboxの実行順を決めたりするツールではない。全workerの会話を集めて監視する仕組みでもない。
やることは、共有contextをGitで同期し、workerごとの状態を読み書きすること。Codex向けには、作業開始時に共有状態を読み、設計判断を残し、終了時にhandoffを書くためのSkillも用意した。プロジェクトのAGENTS.mdへ共有ルールを追記するコマンドもあるが、既存の指示を置き換えず、追加部分を確認してからcommitできる。
複数のsandboxを同時に動かすと、会話履歴を共有することより、各workerが同じ前提で作業を始められることの方が重要になる。会話そのものを同期する案ではなく、作業に必要な事実を選んで残す案にして、GistとGitで同期する形にした。ctx-syncはそのための小さな共有ノートとして作った。まずは決定事項と引き継ぎを同期するところから使ってみる。