リポジトリを 2 つに増やした日のことを、今でもよく覚えています。1 つ目のリポジトリで丁寧に育てた AGENTS.md を、2 つ目へコピーして貼り付けました。3 つ目を作ったときは、もうどちらが新しいのか分からなくなっていました。エージェントは各リポジトリで律儀に「指示通り」動くのですが、その指示自体が少しずつズレていく。気づいたときには、同じ「敬体で書く」というルールが、片方では更新されもう片方では古いまま、という状態になっていました。
私は現在、技術スタックの近い 4 つのサイトを独立したリポジトリとして並行運用しています。monorepo にまとめてしまえば設定の共有は簡単ですが、デプロイ単位・公開タイミング・障害の影響範囲を切り離したいという事情から、あえて別々のリポジトリにしています。この「独立しているが、エージェントには同じ規律で動いてほしい」という要求が、マルチリポ・ガバナンスという設計課題です。
ここから扱うのは、Antigravity のエージェントを複数の独立リポジトリで運用するときに、振る舞いの一貫性をどう担保するかという問いです。実際に使っている同期スクリプトとドリフト検出の実装込みで整理します。公式ドキュメントが触れていない、「規約が静かに腐っていく」という長期運用特有の問題への対処が中心です。
なぜ monorepo ではなく、わざわざ複数リポジトリなのか
最初に前提を揃えておきます。「設定を共有したいなら monorepo にすればいい」という指摘は正論で、実際それが最も素直な解です。pnpm workspace や Nx で 1 つの AGENTS.md をルートに置けば、全プロジェクトが同じ規約を見ます。
それでも独立リポジトリを選ぶ合理的な理由はいくつもあります。私の場合は次の 3 つでした。
第一に、障害の影響範囲を物理的に分離したかったこと。1 サイトの設定ミスやビルド破壊が、他の 3 サイトの CI を巻き込むのを避けたい。第二に、デプロイ権限とトークンをサイトごとに完全分離したかったこと。1 つのトークンが漏れても被害が 1 リポジトリに留まる設計です。第三に、公開タイミングとコミット履歴を独立させたかったこと。サイトごとに別人格として育てているので、履歴が混ざるのは美しくない、という個人開発者の美意識もあります。
つまりマルチリポは「設定共有を犠牲にしてでも分離したい」ときの選択です。だからこそ、犠牲にした設定共有を別の仕組みで取り戻す必要が出てきます。それがガバナンス層です。
規約はコピーした瞬間から腐り始める
複数リポジトリでエージェントを動かすと、必ずドリフト(drift、緩やかなズレ)が発生します。私が観測したドリフトは大きく 3 種類でした。
ひとつは ルールのバージョンドリフトです。あるリポジトリで AGENTS.md に新ルールを足しても、他にコピーし忘れる。エージェントはコピーされていないリポジトリでは古いルールのまま動きます。
ふたつめは ゲートスクリプトのドリフトです。品質チェックの Python スクリプトを 1 箇所で直しても、他の N-1 箇所が古いまま。同じ違反が片方では弾かれ、片方では通ってしまいます。
みっつめが厄介で、暗黙の慣習ドリフトです。AGENTS.md には書かれていないけれど、あるリポジトリのコミット履歴を見てエージェントが学習する「空気」のような規約。これはファイル比較では検出できません。
実装上の注意点として、この記事で対処するのは前者ふたつ、つまりファイルとして可視化できるドリフトです。本番運用ではこの 2 種類こそが事故に直結します。みっつめは後述する「正本の一元化」で間接的に抑えます。
設計の核:3 層に分けて、正本を 1 つにする
私が落ち着いた設計は、エージェントが参照する規約を 3 層に分けることでした。
**第 1 層は共有正本(canonical)**です。全リポジトリ共通のルール、品質ゲート、文体規約。これは 1 箇所だけに「正本」として存在し、各リポジトリへは配布物としてコピーされます。手で編集してよいのは正本だけ、というのが鉄則です。
第 2 層はサイト固有設定です。カテゴリ定義、デプロイ先、サイト名など、リポジトリごとに必ず異なる値。これは各リポジトリが自分で持ちます。
第 3 層は合成された AGENTS.md です。第 1 層と第 2 層を結合して生成される、エージェントが実際に読むファイル。これは生成物なので手では編集しません。
この分離の効きどころは、「手で触ってよい場所」が明確になることです。ドリフトの大半は「配布物を直接編集してしまう」ことから生まれます。正本と配布物を構造的に分ければ、エージェント自身にも「ここは生成物だから触らない」と伝えられます。
ディレクトリ構成は次のようにしました。正本は 4 リポジトリのどれでもない、運用専用の場所に置きます。
ops/ # 運用専用の正本置き場(どのサイトのリポでもない)
├── canonical/
│ ├── AGENTS.base.md # 全リポジトリ共通の規約(第1層)
│ ├── gates/
│ │ ├── article_gate.py # 品質ゲート(正本)
│ │ └── frontmatter_integrity.py
│ └── manifest.json # 配布先リポジトリと配置パスの定義
└── scripts/
├── distribute.sh # 正本を各リポへ配布
└── audit-drift.sh # 配布物と正本の差分を監査
manifest で「どこに何を配るか」を宣言する
配布の心臓部は manifest です。「どのファイルを、どのリポジトリの、どのパスへ置くか」を宣言的に書きます。手続きをスクリプトにベタ書きすると、リポジトリが増えるたびにスクリプトを直すことになり、それ自体がドリフト源になります。宣言と実行を分離するのが要点です。
{
"repos": [
{ "name": "site-a", "path": "/home/ops/repos/site-a" },
{ "name": "site-b", "path": "/home/ops/repos/site-b" },
{ "name": "site-c", "path": "/home/ops/repos/site-c" },
{ "name": "site-d", "path": "/home/ops/repos/site-d" }
],
"artifacts": [
{ "src": "canonical/gates/article_gate.py", "dest": "_gates/article_gate.py" },
{ "src": "canonical/gates/frontmatter_integrity.py", "dest": "_gates/frontmatter_integrity.py" }
],
"agents_md": {
"base": "canonical/AGENTS.base.md",
"site_overlay": "AGENTS.site.md",
"dest": "AGENTS.md"
}
}
artifacts は単純コピーする配布物、agents_md は「共通ベース + サイト固有オーバーレイ」を合成して出力するファイルです。この 2 系統に分けておくと、ほとんどの規約資産はこのどちらかに収まります。
配布スクリプト:合成して、生成物だと明示する
配布の実装です。ポイントは、合成した AGENTS.md の先頭に「これは生成物です。編集は正本側で」という見出しを必ず差し込むこと。エージェントにも人間にも、ここを直接いじってはいけないと伝えるためです。
#!/usr/bin/env bash
# distribute.sh — 正本を manifest に従って各リポジトリへ配布する
set -euo pipefail
OPS_DIR="$(cd "$(dirname "$0")/.." && pwd)"
MANIFEST="$OPS_DIR/canonical/manifest.json"
repo_count=$(jq '.repos | length' "$MANIFEST")
for i in $(seq 0 $((repo_count - 1))); do
repo_path=$(jq -r ".repos[$i].path" "$MANIFEST")
repo_name=$(jq -r ".repos[$i].name" "$MANIFEST")
[ -d "$repo_path/.git" ] || { echo "skip (no repo): $repo_name"; continue; }
# 1) 単純コピーする配布物
art_count=$(jq '.artifacts | length' "$MANIFEST")
for j in $(seq 0 $((art_count - 1))); do
src=$(jq -r ".artifacts[$j].src" "$MANIFEST")
dest=$(jq -r ".artifacts[$j].dest" "$MANIFEST")
mkdir -p "$repo_path/$(dirname "$dest")"
cp "$OPS_DIR/$src" "$repo_path/$dest"
done
# 2) AGENTS.md を base + site overlay で合成
base=$(jq -r '.agents_md.base' "$MANIFEST")
overlay=$(jq -r '.agents_md.site_overlay' "$MANIFEST")
dest=$(jq -r '.agents_md.dest' "$MANIFEST")
{
echo "<!-- GENERATED FILE — DO NOT EDIT. Edit the canonical source in ops/canonical/ -->"
echo ""
cat "$OPS_DIR/$base"
if [ -f "$repo_path/$overlay" ]; then
echo ""
echo "<!-- ===== site-specific overlay ($repo_name) ===== -->"
echo ""
cat "$repo_path/$overlay"
fi
} > "$repo_path/$dest"
echo "distributed -> $repo_name"
done
このスクリプトの本質は、AGENTS.md を「書く」ものから「生成する」ものへ格下げしたことです。生成物になった瞬間、それを手で編集する動機が消え、ドリフトの最大の発生源が塞がれます。宮大工だった私の祖父は、寸法の基準となる「規矩(きく)」を何より大切にしたと聞きます。基準が 1 つだから、何枚の板を切っても狂わない。正本を 1 つに保つというのは、それと同じ発想だと感じています。
ドリフト検出:配布物が正本と一致しているか監査する
配布だけでは不十分です。誰か(自分も含む)が配布物を直接編集してしまうと、次の配布まで気づけません。そこで、配布物と正本のハッシュを突き合わせる監査スクリプトを CI と push 前に走らせます。
#!/usr/bin/env bash
# audit-drift.sh — 各リポの配布物が正本と一致しているか検査する
set -euo pipefail
OPS_DIR="$(cd "$(dirname "$0")/.." && pwd)"
MANIFEST="$OPS_DIR/canonical/manifest.json"
drift=0
hash_of() { sha256sum "$1" | awk '{print $1}'; }
repo_count=$(jq '.repos | length' "$MANIFEST")
art_count=$(jq '.artifacts | length' "$MANIFEST")
for i in $(seq 0 $((repo_count - 1))); do
repo_path=$(jq -r ".repos[$i].path" "$MANIFEST")
repo_name=$(jq -r ".repos[$i].name" "$MANIFEST")
[ -d "$repo_path/.git" ] || continue
for j in $(seq 0 $((art_count - 1))); do
src=$(jq -r ".artifacts[$j].src" "$MANIFEST")
dest=$(jq -r ".artifacts[$j].dest" "$MANIFEST")
deployed="$repo_path/$dest"
if [ ! -f "$deployed" ]; then
echo "MISSING $repo_name:$dest"; drift=1; continue
fi
if [ "$(hash_of "$OPS_DIR/$src")" != "$(hash_of "$deployed")" ]; then
echo "DRIFT $repo_name:$dest が正本と一致しません"; drift=1
fi
done
done
if [ "$drift" -ne 0 ]; then
echo "❌ ドリフトを検出しました。distribute.sh を再実行して揃えてください。"
exit 1
fi
echo "✅ 全リポジトリの配布物が正本と一致しています。"
ハッシュ一致という単純な判定にしているのは、意図的です。意味的な差分(「内容は同じだが整形が違う」など)まで許容し始めると、判定が緩み、結局ドリフトを見逃します。配布物はバイト単位で正本と一致していなければならない、という厳しさが、長期運用では効きます。緩い基準は静かに崩れます。
実際に効いた数字と、効かなかった工夫
この仕組みを 4 リポジトリで回した結果を、正直に書きます。
導入前、規約変更を 4 リポジトリへ反映するのに、手作業で平均 18 分かかっていました。1 つずつクローンを開いて、編集して、コミットして、を繰り返すからです。distribute.sh 導入後は、正本を 1 回直して配布を流すだけになり、所要時間は約 40 秒になりました。手作業に対しておよそ 27 倍の短縮です。月に 6〜8 回の規約変更があるので、月あたり 2 時間前後の手作業が消えた計算です。
数字より大きかったのは、「反映し忘れ」がゼロになったことです。導入前の 3 か月で、4 リポジトリのうち少なくとも 1 つにルールが反映されていない状態を、私は 5 回作っていました。audit-drift.sh を push 前ゲートに組み込んでからは、不一致があれば push が止まるので、忘れること自体が不可能になりました。
一方で、効かなかった工夫もあります。当初は「意味的に等価ならドリフトと見なさない」賢い比較を作ろうとしました。結果は失敗です。AGENTS.md は Markdown なので、整形ゆれを吸収しようとすると判定が複雑になり、かえってバグの温床になりました。賢くしようとした分だけ脆くなる、という典型でした。最終的に「バイト一致か否か」だけに割り切ったほうが、運用は安定しました。
エージェント自身にガバナンスを尊重させる
ここまではスクリプトの話でしたが、Antigravity のエージェントに作業させる以上、エージェント側にもガバナンスを理解させる必要があります。私が AGENTS.base.md(正本)に必ず入れている一節を共有します。
## このリポジトリの規約ファイルについて
- `AGENTS.md` と `_gates/` 配下は**生成物・配布物**です。直接編集しないでください。
- 規約を変更したい場合は、変更の意図だけを述べてください。
正本は別の運用リポジトリ(ops/canonical/)にあり、そこで一元管理されます。
- push 前には必ず `audit-drift.sh` 相当のチェックが走ります。
配布物を書き換えるとこのチェックで停止します。
これを入れておくと、エージェントが「規約を改善しようとして配布物を直接書き換える」という、善意のドリフトを防げます。エージェントは指示に忠実なので、「ここは触らない場所だ」と明示するだけで、驚くほど素直に守ってくれます。逆に言えば、明示しなければエージェントは良かれと思って触ります。境界線は、書いておかないと存在しないのと同じです。
どこから始めるべきか
もし今、2 つ以上の独立リポジトリで Antigravity を使っていて、規約のコピペに疲れているなら、最初の一歩は「正本を 1 つ決める」ことだけで十分です。完璧な manifest も配布スクリプトも、最初は要りません。私はこの順序を強く推奨します。いきなり自動化から入るより、正本の所在を固めるほうを先にすることをお勧めします。
具体的には、いちばん規約が育っているリポジトリの AGENTS.md を運用用の場所へ移し、それを正本と宣言します。他のリポジトリの AGENTS.md は、その正本からの生成物だと頭の中で位置づけ直す。この「正本はどれか」を決めるだけで、次に規約を直すとき「どこを直せばいいか」で迷わなくなります。配布の自動化は、リポジトリが 3 つを超えて手作業が痛くなってから足せば十分間に合います。
私自身、4 サイトを別々に育てながら、それでも 1 つの規律で貫きたいという矛盾した願いを、この層別設計でなんとか両立させています。分けたいけれど揃えたい——その両方を諦めないための設計だと考えています。同じようにリポジトリが増えて困っている方の、設計の足がかりになれば嬉しいです。