はじめに
3年ぶりくらいの記事です。
エージェント作業に限った話ではないですが、コミットを見やすくしましょうという話です。
エージェント(Claude Code を想定)を使った開発を中期的に(数か月)続けていると、リファクタの介入をすべきタイミングが訪れます。
開発当初の設計が中盤になると通用しなくなるからです。
逆に中盤を見据えた実装を最初からしてしまうと、それはオーバーエンジニアリングになります。
開発の自然な流れとして、コードの規模の拡大に応じた設計の更新、実装の別ファイルへの切り出し、コンポーネント化は不可避でしょう。
そんなわけで今回は、UI の移動・統合を扱ったリファクタを例に、こうするとエージェントの作業をレビューしやすかったよという実録になります。
今回の結論は以下です。
- 機械的作業とエージェンティック作業のコミットを分離する
- 機械的作業: カット&ペーストなど
- エージェンティック作業: 個人的な造語。AIが判断を伴って編集する作業
見やすいコミットログがもっと世に増えますように。
背景
前提として、私は Qt/QML を使った C++ デスクトップアプリを開発しています。
今回は QML (GUI側の言語)を例に書きます。言語はやや特殊かもしれませんが、作業自体は他の言語でも起こると思っています。
設定ウィンドウについて、タブ構成を再編成するリファクタリングを行いました。
概念的にはブロックを移動するだけの単純な作業です。ちなみにファイルは1000行ほどでした。
SettingsWindow.qml
Hoge タブ {
|----------|
| hoge1 | <- Piyo の中、一番上に移動したい
|----------|
|----------|
| hoge2 |
|----------|
|----------|
| hoge3 |
|----------|
}
Fuga タブ { ... }
Piyo タブ { <- Hoge と Fuga の間に移動したい
|----------|
| piyo1 |
|----------|
}
Other タブ {
|----------|
| other1 | <- Piyo の中、piyo1 の上に移動したい
|----------|
}
しかしこれを一括でやると、git diff 上では大変なことになりました。
一括でやらせて失敗
最初はたかをくくっていたため、コミット的には1つにまとめていい粒度に感じましたし、エージェントに1作業でやらせました。
概念的には以下のようになっただけのはずです。ですが、git 差分は地獄のようでした。
SettingsWindow.qml
Hoge タブ {
|----------|
| hoge2 |
|----------|
|----------|
| hoge3 |
|----------|
}
Piyo タブ {
|----------|
| hoge1 |
|----------|
|----------|
| other1 |
|----------|
|----------|
| piyo1 |
|----------|
}
Fuga タブ { ... }
以下は diff の一部です。
変更していないはずの Fuga タブが変更に現れ、Piyo タブの変更は Fuga タブの上下に分かれ、diff の長さは数百行になりました。
--- a/SettingsWindow.qml
+++ b/SettingsWindow.qml
+ // Piyo タブ
+ ScrollView {
+ ...
+ GroupBox {
+ title: "hoge1"
+ ...
+
- // Fuga タブ(変更していないのに差分に巻き込まれる)
- ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
GroupBox {
- title: "fuga1" ← 変えてないのに差分扱い
+ title: "other1"
Layout.fillWidth: true
}
- }
- }
-
- // Piyoタブ
- ScrollView {
- ...
- ColumnLayout {
- ...
+ GroupBox {
+ title: "fuga1"
...
(... Fuga の追加と Piyo の削除がストライプ状に混在して数百行続く ...)
重要なのはこれが、自分が IDE 上でコードを移動させたのでなく、エージェントが Edit ツールか何かで編集した ということです。
私には、この数百行の diff の中にたった1つのタイポ(ハルシネーション、ノイズ)が含まれてないことは保証できません。
というか diff が見づらすぎて元のコードから内容的な変更が0であるかどうかも判断付きません。
Fuga が Huga になっているかもしれません。
この変更には責任が持てないと判断し、会話履歴を巻き戻し、別の方針で変更することにしました。
「どのような手段で変更されたのか」をコミット単位で区別する
私はエージェントのコーディングは既存の情報に対し破壊的な変更であると考えています。
言い方を変えると、エージェントが編集する前と編集した後で、コードの情報量が維持される保証はありません。どれだけ rules を設定しても。
エージェントの作業は情報量を変化させます。であれば、その変更は確認が容易であるべきです。
情報量が変化する変更と、変化しない変更でコミットを分けることにしました。
- コミット1:機械的なカット&ペースト(情報量が変化しない)
- コミット2:動作のための調整(情報量が変化する)
(もちろん、カット&ペースト、構造の移動が変化を生むこともままあります。言語への理解が前提です)
コミット1:機械的なカット&ペースト
コードの移動だけを行い、内容は一切変更しません。この時点では一旦ビルドが通らなくなります。
今回は、ファイルが 1000 行くらいで長くなってきていたので、git 差分も崩れにくいですし、外部ファイルに切り出しました。
(残念ながら)この作業は手作業でやりました。空ファイル作成とかはエージェントにやってもらいましたが…。
Piyo1.qml
|----------|
| hoge1 |
|----------|
|----------|
| other1 |
|----------|
Piyo2.qml
|----------|
| piyo1 |
|----------|
SettingsWindow.qml
Hoge タブ {
|----------|
| hoge2 |
|----------|
|----------|
| hoge3 |
|----------|
}
Fuga タブ { ... }
Piyo タブ {
}
Other タブ {
}
コミット2:動作のための調整
移動したコードを実際に動作させるための修正です。import の追加や、切り出したクラスの使用、cmake へのファイル追加。
Gihub Desktop において、「Hide whitespace changes」をオンにして見ると、インデント調整は全部消えて、意味のある変更だけ が残ります。
Piyo1.qml:
--- a/Piyo1.qml
+++ b/Piyo1.qml
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import AppSettings
+
+ColumnLayout {
+ spacing: 8
+
GroupBox {
title: "入力デバイス"
...
}
+}
Piyo2.qml も同様(import 追加 + ColumnLayout ラッパー追加のみ)。
SettingsWindow.qml:
--- a/SettingsWindow.qml
+++ b/SettingsWindow.qml
+ // Piyo タブ
+ ScrollView {
+ ...
+ Piyo1 {
+ Layout.fillWidth: true
+ }
+ Piyo2 {
+ Layout.fillWidth: true
+ }
+ }
+ }
// Fuga タブ(変更なし)
- // Piyo タブ
- ScrollView {
- ...
- }
-
- // Other タブ
- ScrollView {
- ...
- }
diff が小さく、すべてが意味のある変更です。「移動したコードがちゃんと動くか」だけに集中してレビューできます。
おわりに
「機械的作業」と「エージェンティック作業」を分けようね、という話をしました。
より本質的には、情報量を変える作業と変えない作業は履歴上で分離しようね、という話です。
別にこれは今に始まった話ではなく、アトミックコミットのような考え方として昔からあります。
また今回はファイルを切り分けましたが、そもそも最初の図の <- xxx に移動したい という矢印ごとにコミットを分ければ、おそらくは git 差分もそこまで酷いことにはならなかったでしょう。
私はこういうやり方で今回上手くいったよ、という報告であり、ベストプラクティスとも思いません。
ですが少なくとも、コミットを見やすくすることは、人間にもエージェントにも優しいはずです。
git 差分を小さくすることで、未来のエージェントが過去の diff を参照したときのコンテキストへの悪影響を抑えられます。
レビュワーには「今回はリファクタで、全体差分でなくコミットごとの変更を見たほうがレビューしやすいです」と伝えられます。
Claude Code はとても楽しいので、また何か書きます。