無人で回したエージェントが、夜のうちに 40 ファイル近くを書き換えていました。朝いちばんに開いて、全部の diff を上から読もうとして、すぐに諦めました。チャットログも長く、どこで何を判断したのかを追うだけで時間が溶けていきます。
個人開発で複数のアプリとサイトを並行して回していると、こうした無人実行の「翌朝のレビュー」は毎日の作業になります。問題は変更の量そのものではなく、人間が再びその文脈に入り直すときのコストです。ここでは、全 diff を上から読まずに済むよう、変更をリスク階級で束ねた再入場ダイジェストを設計します。
量ではなく「再入場コスト」が敵
無人実行のレビューがつらいのは、diff が多いからではありません。どこから見れば安全かが分からないからです。40 ファイルのうち、本当に目を凝らすべきは数ファイルで、残りは整形やリネームのような確認不要のノイズだったりします。
ところが diff は、重要な一行も、空白の調整も、同じ見た目で並べてきます。人間はそれを上から順に読み、集中力を後半まで保てません。必要なのは、変更を「人が見るべき順番」に並べ替える層です。
もう一つ、私が長く踏んでいた罠があります。分からなくなるとチャットログを遡る癖です。エージェントの思考ログは、判断の理由を探すには向いていますが、何が起きたかを確かめる用途にはまったく向いていません。理由の記録と結果の記録を混ぜて読むから、再入場のコストが跳ね上がります。見るべきは結果、つまりリポジトリの状態と外部への副作用だけです。
リスク階級で束ねる
私は変更を四つの階級に束ねています。見る順番は、影響が取り返しにくいものからです。
| 階級 | 含まれる変更 | 見る順番 | 確認の深さ |
| 不可逆・外部影響 | push、デプロイ、課金、データ削除、外部 API への書き込み | 最初 | 原物を一行ずつ |
| 契約面 | 公開 API、スキーマ、設定値、依存バージョン | 次 | 変更理由と後方互換 |
| 内部実装 | 関数の中身、テスト、リファクタ | その次 | テストの通過を確認 |
| ノイズ | 整形、リネーム、コメント、import 並べ替え | 最後(飛ばし可) | 件数だけ確認 |
この並べ替えだけで、レビューの体感は大きく変わります。集中力がいちばん高い最初の数分を、いちばん取り返しのつかない変更に充てられるからです。AdMob の設定や課金まわりの一行は、この「不可逆・外部影響」の先頭に来ます。
境界にある変更をどう扱うかが、この表の実用性を決めます。CI のワークフロー定義は、内部実装のように見えて、実際にはデプロイを起動する不可逆の入口です。ロックファイルは行数こそ多いものの、依存の実体を決める契約面です。マイグレーションは、適用済みかどうかで階級がまるごと変わります。
私が置いている裁定規則は二つだけです。迷ったら一段上の階級に置くこと。そして削除を含む変更は、種別に関係なく一段上げること。復元が要る側に倒しておけば、外したときの損害が小さくて済みます。
階級は人手で付けない — パス規則で機械分類する
階級分けを毎朝手で判断していると、それ自体が再入場コストになります。分類はパスとファイル名の規則で機械に任せ、人間は結果を眺めるだけにします。次のスクリプトは、前回レビュー地点からの変更一覧を読み、四階級に振り分けて出力します。
// scripts/classify-changes.mjs
// 使い方: node scripts/classify-changes.mjs reviewed/antigravity
import { execSync } from "node:child_process";
const base = process.argv[2] ?? "HEAD~1";
// 上から順に評価し、最初に一致した規則の階級を採用する
const RULES = [
{ cls: "irreversible", re: /^(\.github\/workflows\/|wrangler\.toml$|.*\/migrations\/|infra\/|scripts\/deploy)/ },
{ cls: "irreversible", re: /(stripe|billing|webhook|checkout)/i },
{ cls: "contract", re: /^(src\/config\/|.*\/api\/|.*\.d\.ts$|package(-lock)?\.json$|.*schema.*\.(sql|ts|json)$)/ },
{ cls: "internal", re: /^(src\/|app\/|lib\/|tests?\/)/ },
{ cls: "noise", re: /(\.md$|\.snap$|\.lock$|^public\/)/ },
];
const ORDER = ["irreversible", "contract", "internal", "noise"];
const UP = { noise: "internal", internal: "contract", contract: "irreversible", irreversible: "irreversible" };
const raw = execSync(`git diff --name-status ${base}..HEAD`, { encoding: "utf8" }).trim();
const buckets = { irreversible: [], contract: [], internal: [], noise: [] };
for (const line of raw ? raw.split("\n") : []) {
const [status, ...paths] = line.split("\t");
const path = paths.at(-1);
let cls = RULES.find((r) => r.re.test(path))?.cls ?? "contract"; // 未知は契約面に寄せる
if (status.startsWith("D") || status.startsWith("R")) cls = UP[cls]; // 削除・リネームは一段上げる
buckets[cls].push(`${status}\t${path}`);
}
for (const cls of ORDER) {
const files = buckets[cls];
if (cls === "noise") {
console.log(`\n## noise: ${files.length} 件(件数のみ確認)`);
continue;
}
console.log(`\n## ${cls}: ${files.length} 件`);
files.forEach((f) => console.log(" " + f));
}
規則を上から評価して最初の一致を採る形にしているのは、判定の理由を後から説明できるようにするためです。分類が外れたときに直すのは規則の一行であって、その日の判断ではありません。未知のパスを contract(契約面)に寄せているのも同じ考え方で、知らないものを安全側の中位に置いておけば、静かに読み飛ばされる事故が起きません。
ノイズ階級だけは件数しか出しません。中身を出すと、結局そこも読んでしまうからです。飛ばす対象を目に入れないところまでが、この層の仕事だと考えています。
前回レビュー地点を、エージェントごとに持つ
毎朝、変更全体を見るのは無駄です。見るべきは「前回自分がレビューした地点より後」の差分だけ。そこで、レビューを終えた地点に印を残します。
# レビューを終えたら、その地点に印を残す
git tag -f reviewed/antigravity HEAD
git push -f origin reviewed/antigravity
# 翌朝、前回レビュー地点以降だけを見る
git diff reviewed/antigravity..HEAD --stat
git log reviewed/antigravity..HEAD --oneline
タグを「前回レビュー地点」として動かしていくと、無人実行が何度走っても、人間はつねに「前回以降の差分」だけを相手にできます。差分の母数が小さく保たれるほど、リスク階級の束ね直しも軽くなります。
これが崩れるのは、エージェントを並列で走らせ始めたときでした。地点を一つしか持っていないと、先に片方をレビューして印を進めた瞬間、もう片方の未レビュー分が印より前に沈んで見えなくなります。実際に一度、これで小さな設定変更を見落としました。
対処は単純で、印をエージェント単位に分けるだけです。reviewed/nightly-refactor、reviewed/link-audit のように名前を分け、レビュー対象はそのエージェントのコミットに限定します。
AGENT="nightly-refactor"
BASE=$(git rev-parse -q --verify "reviewed/${AGENT}" || git merge-base origin/main HEAD)
# そのエージェントのコミットだけを対象にする(committer 名で絞る)
git log --committer="agent/${AGENT}" --oneline "${BASE}..HEAD"
node scripts/classify-changes.mjs "$BASE"
git rev-parse -q --verify で失敗したときに merge-base へ落とす形にしているのは、rebase や履歴の書き換えで印が指すコミットが消えた場合の保険です。印が壊れているのに気づかず「差分ゼロ」と読んでしまうのが、この仕組みで唯一こわい失敗です。復旧が必要な日には差分が大きく出ますが、見落とすよりはるかにましだと考えています。
ダイジェストはエージェントに「観点」で書かせる
変更の要約自体は、エージェントに書かせて構いません。ただし「何を変えたか」を箇条書きさせても、それは diff の言い換えにしかなりません。書かせるべきは、人間のレビュー観点です。
実行の最後に、次の三つを必ず出力させます。
- 不可逆・外部影響に当たる操作の一覧(なければ「なし」と明記する)。
- 契約面で後方互換を壊しうる変更と、その理由。
- 人間が最初に見るべきファイルと行の指定。
エージェント側の指示書には、出力そのものではなく、この観点を固定しておきます。私は無人タスクの指示書の末尾に、次の断片を置いています。
実行の最後に「再入場ダイジェスト」を出力してください。
- 不可逆・外部影響(push / デプロイ / 課金 / 削除 / 外部APIへの書き込み)に
当たる操作を列挙する。実施していなければ「なし」と明記する。推測で補わない。
- 契約面の変更は「変更前→変更後」と理由、既存利用者への影響を一行で添える。
- 人間が最初に見るべきファイルを最大3つ、行番号付きで挙げる。
- 判断に迷って保留した箇所があれば、迷った理由ごと書く。省略しない。
最後の一行を足したのは、迷いを黙って握り潰されるほうが困ると気づいたからです。判断を保留したまま進んだ箇所は、翌朝いちばん確認したい場所でもあります。
## 再入場ダイジェスト(前回レビュー地点以降)
- 不可逆・外部影響: なし(push は未実行、デプロイなし)
- 契約面: pricing.ts の Article 価格を 250→280 に変更(理由: 指示書の改定。後方互換: 既存購入には影響なし)
- 最初に見る: src/config/pricing.ts L42 / src/lib/premium.ts L18
- 保留: 旧価格を参照するテストが1件あり、意図的に未修正のまま残した
- ノイズ: 31 ファイルの整形のみ(要確認なし)
このダイジェストがあると、再入場は「最初に見る」に挙がった数ファイルから始められます。全体像はリスク階級が、詳細はそこから辿る、という二段構えになります。
事実は機械、理由はエージェント
ダイジェストを全部エージェントに書かせると、件数やファイル名までが生成物になります。数字が一つずれているだけでレビュー全体の信頼が落ちるので、私は途中で分担を変えました。事実は機械が git から取り、理由だけをエージェントに書かせます。
#!/usr/bin/env bash
# scripts/digest.sh — 事実部分だけを組み立てる
set -euo pipefail
AGENT="${1:-antigravity}"
BASE=$(git rev-parse -q --verify "reviewed/${AGENT}" || git merge-base origin/main HEAD)
echo "## 再入場ダイジェスト(${AGENT} / ${BASE:0:7}..HEAD)"
echo
echo "### 変更規模"
git diff --shortstat "${BASE}..HEAD"
echo
echo "### 階級別"
node scripts/classify-changes.mjs "$BASE"
echo
echo "### 外部への副作用(原物)"
echo "- origin/main との差: $(git rev-list --count "origin/main..HEAD") commits ahead"
echo "- 押されたタグ: $(git ls-remote --tags origin | grep -c 'refs/tags' || true)"
出力の骨格が機械側で決まっていると、エージェントに求めるのは空欄を埋める作業だけになります。理由・後方互換・保留の三つは人間の言葉が要るところなので、そこはエージェントの担当のままにしています。
分担を変えてから、ダイジェストの数字を疑う時間がなくなりました。生成物を信じるかどうかを毎回考えなくて済むのは、思っていたより大きな違いでした。
自己申告を、どこまで信じるか
ここがいちばん大切な落とし穴です。エージェントの自己申告ダイジェストは、内部実装やノイズの分類には十分に使えます。ですが、不可逆・外部影響の階級だけは、自己申告を鵜呑みにしてはいけません。
エージェントが「push は未実行」と書いていても、私は必ず git log origin/main と実際のデプロイ履歴を自分の目で確かめます。理由は単純で、いちばん取り返しのつかない操作ほど、誤分類されたときの損害が大きいからです。ダイジェストは入口の地図として使い、不可逆な操作だけは原物で裏を取る。この一線を引いておくと、ダイジェストを信頼しつつ事故を防げます。
裏取りは 30 秒で終える形にしておく
原物確認を「気をつける」で運用すると、忙しい朝に飛ばします。飛ばせないようにするには、一つのコマンドで終わる形にしておくことです。
#!/usr/bin/env bash
# scripts/verify-irreversible.sh — 不可逆側だけを原物で確認する
set -euo pipefail
git fetch --quiet origin --tags --prune
echo "== remote main の直近(自分以外の push がないか)=="
git log origin/main --since="18 hours ago" --pretty=" %h %an %s"
echo "== ローカルと remote の乖離 =="
git rev-list --left-right --count origin/main...HEAD | awk '{print " behind="$1" ahead="$2}'
echo "== 直近のデプロイ実行 =="
gh run list --limit 5 --json displayTitle,status,conclusion,createdAt \
--jq '.[] | " \(.createdAt[0:16]) \(.conclusion // .status) \(.displayTitle)"'
echo "== 追跡外の生成物が残っていないか =="
git status --porcelain --untracked-files=all | head -20
ここで見ているのは、エージェントの言葉ではなく、リモートと CI の記録です。--left-right --count を使っているのは、ahead/behind を一目で見たいからで、behind が付いていれば自分の手元が古い状態でレビューしていることになります。
最後の未追跡ファイル確認は、あとから足しました。生成された秘密情報の断片や一時ファイルが手元に残っていたことがあり、それは diff にも履歴にも出てこないからです。
分類を外した日のこと
規則ベースの分類は、外れます。私が実際に外した二件を残しておきます。
一件目は、ロックファイルをノイズに入れていたときです。整形と同じ扱いで飛ばしていたら、依存のメジャーバージョンが一つ上がっていました。ビルドは通っていたので、気づいたのは数日後です。以来、ロックファイルは契約面に置いています。行数の多さと重要度は無関係だと、ここで理解しました。
二件目は、リネームの裏に隠れた削除です。生成ファイルの大量リネームに紛れて、ルートが一つ消えていました。--name-status の R は見た目が穏やかなので、目が滑ります。いまは削除とリネームを機械的に一段上げる規則を入れてあります。先ほどのスクリプトの UP は、この失敗の産物です。
どちらも共通しているのは、外れ方が静かだったことです。誤分類は警告を出しません。だからこそ、規則を直すたびに「どの失敗から来た行か」をコメントに残すようにしています。
何がどれだけ短くなったか
主観だけでは判断できないので、二週間ぶんの朝の作業を計って比べました。私一人の記録で、対象は同じ規模の無人実行です。厳密な計測ではありませんが、傾向としては十分に読み取れました。
| 作業 | 導入前 | 導入後 | 変化の理由 |
| どこから見るかの判断 | 約 8 分 | 1 分未満 | 階級順に並んで届く |
| diff の読み込み | 約 25 分 | 約 9 分 | ノイズ階級を開かない |
| 不可逆操作の裏取り | 実施しない日あり | 約 40 秒 | 1コマンドに集約 |
| チャットログの遡り | 約 12 分 | ほぼ発生せず | 保留理由が先に届く |
減った時間そのものより、裏取りを飛ばす日がなくなったことのほうが効きました。安心して飛ばせる範囲が決まると、人は速く正確に読めるようになります。
一方で、増えた手間もあります。分類規則の保守です。プロジェクト構成が変わるたびに数行を直す必要があり、これを放置すると分類が静かに劣化します。私は月に一度、規則表を上から読み直す時間を取っています。
次の一手として、いま無人で回しているタスクの出力末尾に、ダイジェストの四項目(不可逆・契約面・最初に見る・保留)を必ず書かせる一文を足してみてください。分類スクリプトはその後で構いません。翌朝の再入場が、どこから手を付けるかで迷わなくなります。