金曜の夜、私は自分のコミット履歴をさかのぼりながら、少し落ち着かない気持ちになっていました。エージェントが書いたコードのうち、翌週の私が手を入れて直したものが、思っていたよりずっと多かったのです。
その週、Antigravity のエージェントにはずいぶん助けられたはずでした。緑のテストログも並んでいましたし、体感としては生産的でした。ところが履歴を丁寧に読むと、「エージェントが実装 → 数日後に私が同じ箇所を書き直す」という往復が、特定の種類の作業に集中していました。
私自身、個人開発で複数の壁紙アプリを回しており、AdMob まわりの細かな実装はエージェントに任せがちでした。だからこそ、この往復の多さは他人事ではありませんでした。
委譲の線引きを、私はそれまで勘で引いていました。「これは任せられる」「これは自分でやったほうがいい」。その勘の精度を、初めて数字で問い直した夜でした。
「速く終わった」と「うまくいった」は別の指標
エージェントに作業を委ねると、まず目に入るのは速度です。指示から実装までの時間、あるいは一晩で消化したタスク数。これらは気持ちよく、報告映えもします。
けれど速度は、成果の質を何も保証しません。速く出てきたコードが、翌週に自分の手で書き直されているなら、その委譲は差し引きで時間を生んでいない可能性があります。むしろ、レビューと手直しの二度手間ぶん、赤字かもしれません。
私はこの「事後に人手で直された割合」を、やり直し率(rework rate)と呼んで独立に測ることにしました。速度とは別の軸です。速くても、やり直し率が高ければ、その領域はまだ委ねる時期ではない。地味ですが、委譲の意思決定にはこちらの指標のほうがずっと効きます。
やり直し率をどう定義するか
言葉として「やり直し」は曖昧です。数字にするには、観測可能な事象へ落とす必要があります。私は次のように定義しました。
あるエージェント・コミットが「やり直された」とは、その後の一定期間内に、同じファイル群を触る人手のコミットが発生したこと。ここで期間は「72時間以内」または「同じファイルを触る次の5コミット以内」のいずれか短いほうを使います。
この定義は完璧ではありません。人手コミットが機能追加であって手直しではない場合も拾ってしまいます。ですが、実運用ではノイズよりシグナルのほうが勝ちました。理由は後半で触れます。まずは、雑でも観測可能な定義から始めることが大切だと考えています。
もう一段、精度を上げる補助シグナルも足しました。revert コミット、コミットメッセージ中の fix・revert・rework といった語です。これらは修正らしさの重み付けに使います。
前提:エージェントの仕事に「印」をつける
やり直し率を測るには、まず「どのコミットがエージェントの成果か」を後から判別できる必要があります。私は Antigravity のエージェントに実装させる際、コミットの trailer に一行の印を残す運用にしました。
feat(wallpaper): add adaptive brightness sampler
Antigravity-Task: brightness-sampler
Antigravity-Agent: implement
Antigravity-Category: feature-impl
Antigravity-Category が肝です。ここに「feature-impl」「bugfix」「refactor」「config」「boilerplate」といったタスク種別を入れておきます。あとで、この種別ごとにやり直し率を集計するためです。
trailer は git interpret-trailers でも機械的に付けられますし、エージェントのコミットメッセージ規約に組み込んでおけば自動で入ります。人手コミットには印を付けません。印のないコミットが、そのまま「人手」として扱われます。
実装:git 履歴からやり直し率を出す
ここからが本題です。git 履歴を読み、エージェント・コミットごとに「その後、同じファイルを人手が触ったか」を判定し、種別ごとに率を出します。信頼区間も添えます。少数のサンプルで率だけ見ると、簡単に錯覚するからです。
#!/usr/bin/env python3
"""rework_rate.py — エージェント成果のやり直し率をgit履歴から算出する。"""
import subprocess, json, math
from collections import defaultdict
WINDOW_HOURS = 72 # この時間内の人手修正をやり直しとみなす
WINDOW_COMMITS = 5 # かつ、同一ファイルを触る次のNコミット以内
def git(*args):
out = subprocess.run(["git", *args], capture_output=True, text=True, check=True)
return out.stdout
def load_commits():
"""新しい順にコミットのメタデータとファイル一覧を読む。"""
fmt = "%H%x1f%ct%x1f%B%x1e"
raw = git("log", "--no-merges", f"--pretty=format:{fmt}", "--name-only")
commits = []
for block in raw.split("\x1e"):
block = block.strip("\n")
if not block:
continue
head, *file_lines = block.split("\n")
sha, ts, body = head.split("\x1f")
files = {f for f in file_lines if f.strip()}
cat, agent = None, False
for line in body.splitlines():
if line.startswith("Antigravity-Category:"):
cat = line.split(":", 1)[1].strip()
if line.startswith("Antigravity-Agent:"):
agent = True
commits.append({"sha": sha, "ts": int(ts), "files": files,
"category": cat, "agent": agent, "body": body})
return commits
def is_rework(body):
low = body.lower()
return any(k in low for k in ("fix", "revert", "rework", "hotfix"))
def compute(commits):
ordered = list(reversed(commits)) # 古い順に走査したい
stats = defaultdict(lambda: {"total": 0, "reworked": 0})
for i, c in enumerate(ordered):
if not c["agent"] or not c["category"]:
continue
stats[c["category"]]["total"] += 1
deadline = c["ts"] + WINDOW_HOURS * 3600
touched_after, reworked = 0, False
for later in ordered[i + 1:]:
if later["ts"] > deadline:
break
if not (later["files"] & c["files"]):
continue
touched_after += 1
if not later["agent"] and is_rework(later["body"]):
reworked = True
break
if touched_after >= WINDOW_COMMITS:
break
if reworked:
stats[c["category"]]["reworked"] += 1
return stats
def wilson(reworked, total, z=1.96):
"""Wilson信頼区間の下限・上限。少数標本での過信を防ぐ。"""
if total == 0:
return (0.0, 0.0, 0.0)
p = reworked / total
denom = 1 + z * z / total
center = (p + z * z / (2 * total)) / denom
half = (z * math.sqrt(p * (1 - p) / total + z * z / (4 * total * total))) / denom
return (p, max(0.0, center - half), min(1.0, center + half))
if __name__ == "__main__":
stats = compute(load_commits())
rows = []
for cat, s in sorted(stats.items()):
p, lo, hi = wilson(s["reworked"], s["total"])
rows.append({"category": cat, "n": s["total"],
"rework_rate": round(p, 3),
"ci_low": round(lo, 3), "ci_high": round(hi, 3)})
print(json.dumps(rows, ensure_ascii=False, indent=2))
出力はこうなります。カテゴリごとに、サンプル数 n、やり直し率、そして信頼区間の下限・上限が並びます。
[
{ "category": "boilerplate", "n": 41, "rework_rate": 0.049, "ci_low": 0.013, "ci_high": 0.163 },
{ "category": "feature-impl", "n": 33, "rework_rate": 0.394, "ci_low": 0.245, "ci_high": 0.564 },
{ "category": "bugfix", "n": 18, "rework_rate": 0.278, "ci_low": 0.126, "ci_high": 0.509 },
{ "category": "refactor", "n": 12, "rework_rate": 0.500, "ci_low": 0.251, "ci_high": 0.749 }
]
なぜ信頼区間を必ず添えるか
上の refactor は率 0.50 ですが、n はわずか 12 です。信頼区間は 0.25〜0.75 と、ほとんど「何も分からない」に近い幅です。ここで率だけを見て「リファクタは任せられない」と結論すると、たった数件の偶然に振り回されます。
一方 boilerplate は n が 41 あり、区間上限でも 0.16。これは「かなり安心して委ねてよい」と読めます。feature-impl は n も区間下限も高く、こちらは明確に注意領域です。
私はこの区間を見るようになってから、数字を語りすぎない癖がつきました。サンプルが薄い領域については、「まだ判断しない」という判断を選べるようになったのです。
決定ルール:委ねる・要レビュー・自分でやる
率と区間が出たら、あとは行動へ落とします。私は信頼区間の下限を基準にしています。楽観に倒れないためです。 私は下限 0.15 未満を「委ねてよい」ラインとして推奨しています。
| 信頼区間の下限 | 方針 | 運用 |
| 0.15 未満 | 委ねる | エージェントに任せ、レビューは差分の目視のみ |
| 0.15〜0.35 | 要レビュー | 委ねるが、マージ前に人手の受け入れ確認を必須化 |
| 0.35 以上 | 自分でやる | 委譲を一旦止め、原因を切り分けてから再挑戦 |
閾値そのものは、事業やリスク許容度で変わります。課金に直結するコードなら 0.15 でも高すぎるかもしれません。逆に使い捨てのプロトタイプなら 0.5 でも構わないでしょう。大切なのは、閾値を一度言語化し、同じ物差しで全カテゴリを測ることだと考えています。
実際に一つのカテゴリを引き戻した記録
私の手元では、feature-impl の区間下限が長く 0.24 前後で高止まりしていました。決定ルールでは「要レビュー」ですが、内訳を掘ると、やり直しの多くが「仕様の解釈違い」に集中していました。エージェントの実装力ではなく、私の指示の曖昧さが原因だったのです。
そこで委譲を止めるのではなく、委譲の前段を変えました。機能実装を任せる前に、受け入れ基準を三行の箇条書きで先に書き、それをコミットの trailer にも残す。この一手間を挟んだ二週間で、feature-impl のやり直し率は 0.39(39%)から 0.19(19%)へ下がりました。区間下限も 0.24 から 0.10 台へ。課金に直結する本番コードほど、この一手間が効くと感じています。
数字は「任せるな」ではなく「渡し方を変えろ」と言っていたわけです。もし勘だけで進めていたら、私はおそらく「機能実装はまだ早い」と丸ごと引き戻し、せっかくの委譲を一つ失っていたでしょう。
この計測が向く場面と、向かない場面
計測は万能の物差しではありません。領域ごとに、合う形と合わない形があります。運用しながら、その線引きも一緒に学んでいきました。
向く場面:成果がファイルに落ちる作業
この手法は、コミット単位で成果がファイルに落ちる作業に向きます。実装・修正・リファクタのように、後から誰が触ったかを git で追える領域です。本番運用のコードに近いほど、やり直しの一件一件が重くなるぶん、計測の価値も上がります。
向かない場面:差分に現れない作業
逆に、調査・設計・対話的な壁打ちのように、成果がファイル差分に現れにくい作業には向きません。そこは別の指標——たとえば提案の採用率や、対話後に方針が変わった回数——で測るべきだと考えています。
落とし穴:印付けの規律が崩れると静かに嘘をつく
もう一つの注意点は、印付けの規律です。エージェント・コミットに trailer を残す運用が崩れると、この計測は静かに嘘をつきます。印のないコミットが人手扱いになるため、印を付け忘れたエージェントの成果が、そのまま「やり直した人手」として二重に数えられてしまう落とし穴があるのです。
この罠への対処は単純です。私は Antigravity のコミット規約テンプレートに trailer を最初から埋め込み、人手では消さないことを自分との約束にしています。
おわりに
委譲の線引きは、勘でもできます。経験を積めば、勘はそれなりに当たります。ただ私は、勘の当たり外れを後から数字で確かめられるようにしたことで、ずいぶん気持ちが楽になりました。外した線を、翌週には引き直せるからです。
やり直し率は派手な指標ではありません。けれど、エージェントとどう働くかを静かに教えてくれる相棒のような数字だと、いまは感じています。もし委譲の判断に迷っておられるなら、まずは自分のコミット履歴に一行の印を足すところから始めてみてください。実装の一助になれば幸いです。お読みいただき、ありがとうございました。