1. 序論:コミットログでは説明できなくなったこと
AIコーディングツールをチームに導入すると、コードは速く増えていく一方で、そのコードがどのような過程を経て生まれたのかは、むしろ見えにくくなります。以前はコミットログをざっと確認するだけで、誰が何にどれほど取り組んでいたのか、おおよそ読み取れました。今では同じログを見ても、人が1時間悩んだ結果なのか、エージェントが3分で作ったものなのか区別できません。リポジトリは同じでも、その中で起きていることが見えなくなったのです。
目標は2つに定めました。エージェントが作ったコードであっても、後から加わった人がリポジトリだけを受け取ってすぐに把握し、引き継げるようにすること。そして、どのプロジェクトのリポジトリであっても、分析対象に登録された瞬間から、別途設定なしで指標と推移が出るようにすることです。その後のすべての選択は、この2つによって決まりました。
2. 何を数えるのか
2.1 判別ではなく申告にする
最初はコードの変更内容を分析し、AIが書いた部分を見分けようとしましたが、すぐに断念しました。コミットに記録されているのは作成者、時刻、変更内容、メッセージだけで、「この行はどのように作られたのか」を記録する場所はありません。AIが下書きを作り、人が手を加えてコミットしたコードは、人が手で入力したコードとバイト単位で同一です。判別の精度が低いのではなく、判別すべき対象がデータに存在しないのです。
そこで判別を諦め、申告を受け付けることにしました。ツールが残したもの、または人が記入したものだけを数えます。その代わり、指標名を「AI使用率」ではなく「申告されたAI」と明確に定めました。申告しなかったらどうするのかという疑問が出てきますが、これは欠陥ではなく、もう1つの指標として扱うことにしました。申告率が低いという事実そのものが、規約が守られていないことを示すシグナルだからです。
2.2 単位をコミットではなくセッションにする
より重要な決定は、何を1つの作業とみなすかでした。エージェントとの作業は一度で終わりません。指示し、結果を確認し、修正を依頼し、再び受け取ります。この過程が3、4個のコミットに分かれて残ります。
コミットを単位に数えると、「エージェントが3つのコミットを作成した」という事実だけが残ります。本当に知りたいのは、「この作業は何回で終わったのか」です。一度で終わった作業と、5回やり直した作業は、同じ3コミットでもまったく異なる話です。そこでセッション識別子をコミットにも一緒に残し、同じセッションのコミットを1つの作業としてまとめることにしました。
|
見る単位 |
答えられる質問 |
|---|---|
|
コミット |
エージェントが参加したコミットは何件か |
|
セッション |
1つの作業は何回で終わったのか/どの作業でやり直しが多いのか |
3. 採用した方法
3.1 コミットメッセージをインターフェースとして使う
人が書いたコードなら、理解できなければ質問できます。エージェントが書いたコードでは、そうはいきません。セッションが終わると文脈は消え、次に同じツールを呼び出しても、前回なぜそのようにしたのかを答えられません。だからこそ、エージェントが関わったコードほど、説明がコードと同じ場所に残っていなければなりません。後から加わった人がリポジトリだけを受け取っても把握できるようにするには、それしか方法がありません。
記録を別のストレージに蓄積したり、ツールのAPIから取得したりする方法もありましたが、コミットメッセージを選びました。どのツールを使っていてもコミットは残るためツールを選ばず、記録がコードとともに移動して複製やミラーリングにも付いていき、原本がそのまま保存されるため、後から解釈ルールを変更しても再度読み取れるからです。形式には、Gitがすでに備えているトレーラーの慣例をそのまま使いました。
[SPEC-008] feat(member): 세션에서 사용자 신원을 읽도록 변경
X-Agent: dev
X-Agent-Session: 3ae7dbc4
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ここに1つ落とし穴がありました。Co-authored-byはもともと人同士の共同作業を示す規格なので、そのまま使うと、2人でペア作業したコミットまでAIコミットとして扱われます。広く使われている規格を別の用途に再利用すると、よく起こる問題です。そこで、既知のツールのメールドメインである場合に限ってエージェントとして認めるように区別しました。
3.2 規約を人ではなくツールに組み込む
ここが最も予想外だった部分です。コミット規約はもともと、あまり守られません。文書に定めてオンボーディングで説明しても、数週間もすれば曖昧になります。人の誠実さに頼るルールは、たいていそうなります。ところがエージェントは、プロジェクトの規約ファイルを毎回読み、そのとおりに従います。記憶しているのではなく、作業するたびに読み直すため、時間が経っても曖昧になりません。
## 커밋 메시지 규약
[<백로그>] <type>(<scope>): <제목>
X-Agent: <역할> # dev · review · test · fix · chore 중 하나
X-Agent-Session: <식별자> # 세션 내내 같은 값을 쓴다
- 제목 한 줄이면 끝이다. 본문을 쓰지 않는다.
- 한 커밋에 역할 두 개를 담지 않는다.
コミット規約という概念自体は新しいものではありません。誰もが重要だと分かっていながら、守られていなかっただけです。変わったのは規約ではなく、それを施行する主体です。だからこそ、規約ファイルをどこに置くかが重要になります。リポジトリ内に置けば、規約の変更もコード変更と同じようにレビューを受け、バージョンとして残り、コードが変わったのに規約がそのままであれば、同じコミット内で目に留まります。Wikiに置かれていたなら、コードと食い違っていても誰にも気づかれないまま過ぎていたでしょう。
ただし、必ず従わなければならないルールが1つあります。人が直接作成するコミットには、申告トレーラーを付けないことです。便利だからと付け始めると、人の作業とエージェントの作業が混ざり、その瞬間、このデータで答えようとしていた問いが丸ごと意味を失います。規約を作るときは、何を書くかと同じくらい、何を書かないかも決めなければなりません。
役割を5種類に限定し、1つのコミットには1つだけ記載させたのも同じ理由です。2つ書きたくなったなら、コミットを分けるべきだというサインです。リストにない値が入ってきても破棄せず、そのまま数えます。規約が守られていないという事実そのものが見るべきシグナルであり、その割合が規約遵守率の指標になるからです。
3.3 判定は保存せず、導出する
実装を終えて画面を開くと、数字が予想よりはるかに小さくなっていました。原因はデータではなく設計でした。コミットエンティティは不変として設計されており、一度取り込まれると値を変更しません。それなのに、判定結果をそのエンティティのフィールドに保存していました。パーサーを組み込む前に取り込まれたコミットは、そのフィールドが永遠に空のままだったのです。
|
方法 |
内容 |
問題 |
|---|---|---|
|
マイグレーションバッチ |
過去の行を走査してフィールドを埋める |
ルールが変わるたびに再実行する必要がある |
|
フィールドの更新を許可 |
エンティティを可変に変更する |
判定1つのために不変設計を諦める |
|
集計時に導出 |
保存せず、毎回判定する |
集計するたびにパースのコストがかかる |
3つ目を選びました。判定ルールは今後も変わり続けるでしょう。ルールが変わるたびに過去をマイグレーションする構造は、長くは持ちません。導出する形にしておけば、再集計を1回行うだけで、過去にも新しいルールが適用されます。懸念していたパースコストは、改めて確認すると集計時間に有意な差を生じさせず、保存して節約しようとしていたコストは、そもそも節約する価値のないものでした。
4. 成果物
4.1 新しいリポジトリが追加されてもそのまま動く
ここまでの選択は別々に決めたように見えますが、1つに集約されます。リポジトリを新たに分析対象として登録したとき、ほかに何をしなければならないのか、ということです。
|
選択 |
だから新しいリポジトリでは |
|---|---|
|
記録をコミットメッセージに残す |
リポジトリを追加するだけで記録も一緒に取り込まれる |
|
判定を保存せず、導出する |
登録するとすぐに、過去のコミットまで遡って集計される |
|
ベンダー・タイプ一覧をポリシーデータとして保持する |
デフォルト値ですぐに動作し、必要に応じて上書きします |
結果として、新しいリポジトリを登録する際に別途準備するものはありません。そのチームがコミット規約を守ってさえいれば、登録した瞬間から指標が表示され、過去のコミットも合わせて計算されるため、最初の画面ですぐに推移を確認できます。反対に、記録を別のリポジトリに蓄積していたなら、リポジトリごとに連携作業が必要だったでしょうし、判定結果を保存していたなら、新たに追加したリポジトリの過去は永遠に空白のままだったはずです。
4.2 0になった理由を画面で区別できるようにする
申告されたAI利用が0のメンバーを一覧から非表示にするか、そのまま表示するかも決める必要がありました。非表示にしたほうが画面はすっきりします。しかし非表示にすると、「使っていない」と「使っているが記録していない」が画面上では同じになります。前述のとおり申告率そのものを指標にすることにしたのに、画面がその2つを一緒に扱ってしまえば、その決定は意味を失います。そこで0を非表示にせず、なぜ0なのかを画面上で直接示すことにしました。
No AI use was reported in this period.
Commits alone cannot tell not-used from not-declared.
同じ原則を、指標を読み込めなかった場合にも適用しました。取得に失敗した場合は0として表示せず、読み込めなかったと記載します。体温計が36.5を示している状態と、故障して目盛りが止まっている状態はまったく異なりますが、数字だけでは区別できません。取得に失敗した場合と値が0の場合も同じです。
5. 結論
エージェントをチームに導入することは、ツールを1つ追加で使うこととは異なります。開発プロセスに新しい参加者が加わるということであり、その参加者が何をしたのかを残す場所を新たに作らなければなりません。今回の作業の大部分は、パースや集計ではなく、何を残し、何を残さないかを決めることに費やされました。
最も実用的な発見は、規約を人ではなくツールに組み込めるということでした。規約が重要だという話は以前からありましたが、常に人の誠実さに頼る話であり、そのためたいてい守られませんでした。重要なのは規約そのものではなく、規約が置かれる場所へと移ったのです。ただし、人の作業とエージェントの作業を混在させないというルールだけは、人が守らなければなりません。
結局、やりたかったことは単純です。エージェントが作成したコードであっても、後から来た人がリポジトリを受け取るだけですぐに引き継げ、リポジトリを分析に接続した瞬間に、何の準備もなく結果と推移が見える状態です。そのためには、説明はコードと同じ場所にあり、規約はリポジトリ内にあり、解釈はリポジトリの外でいつでもやり直せなければなりません。
参考資料
Git – git-interpret-trailers — https://git-scm.com/docs/git-interpret-trailers
GitHub Docs – 複数の著者によるコミットの作成 — https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/creating-a-commit-with-multiple-authors
Conventional Commits 1.0.0 — https://www.conventionalcommits.org/ko/v1.0.0/
toffeeman