LLM Agentに意味のある単位のコミットを強制させる

背景: LLM Agentに意味のある小さい単位でコミットして欲しい

  • Claude CodeなどのLLM Agentに対し「意味のある単位で小まめにコミットしてね〜」とinstructionに書いておいてもしばしば忘れられてしまう
  • その結果、複数の意味がある内容の大きなコミットがされてしまう
  • 大きな単位のコミットはレビューもしにくく、revertもしにくいため、よいことがない
  • 何とか大きいコミットを制限できないものか...、と考えた

解決方法: git addを封印し、専用のカスタムスラッシュコマンドを生やす

  • 意味の単位にgitのhunkを分割してステージングさせるsemantic_commitというカスタムスラッシュコマンドを作りました
    • インストールされるコマンドはこれです
  • Claude Codeのカスタムコマンドをパッケージマネージャ風に管理できるツール(cccsc)経由で入れることができます
    • npx cccsc add syou6162/claude-code-commands/semantic_commit
  • /cccsc:syou6162:claude-code-commands:semantic_commitで起動します
    • Claude Codeが現在の差分を見て、hunkを意味の単位の集合に分けます
    • 意味の単位の集合に対してメッセージを考え、コミットします
    • 差分がなくなるまで、意味の単位でコミットしてくれます
  • 依存ツールがあるので、以下も入れておいてください
    • brew install patchutils
    • go install github.com/syou6162/git-sequential-stage@latest
  • 以下の設定をして、semantic_commit以外でのgit addを封印する合わせ技で平穏が訪ずれました
    • settings.jsonに書いておきましょう
{
  "permissions": {
    "allow": [
      ...
      "Bash(git add -N:*)",
      "Bash(git diff:*)",
      "Bash(git apply:*)",
    ],
    "deny": [
      "Bash(git add:*)",
      "Bash(git commit -am:*)",
      "Bash(git commit --all:*)",
      "Bash(gh pr checkout:*)"
    ]
  }
}

実際の動作例(クリックで開きます)

> /cccsc:syou6162:claude-code-commands:semantic_commit is running…

✻ Thinking…

  ユーザーはsemantic_commitコマンドについて何かを伝えてきているようです。コマンドメッセージを見ると「semantic_commit is
  running…」と表示されているので、このコマンドが実行中のようです。

  コマンドの説明から:
  - 大きな変更を論理的な単位に分割してコミットするツール
  - LLMがgit diffを分析して意味のある最小単位を提案
  - git-sequential-stageツールによる自動化された逐次ステージング

  これは私が実行すべきタスクのようです。まず現在の状態を確認して、semantic commitを実行する必要があります。

  手順:
  1. リポジトリルートに移動
  2. 現在の変更を確認
  3. 差分を取得
  4. LLMで分析
  5. 適切な単位でコミット

  まずToDoリストを作成してタスクを管理しましょう。

⏺ semantic_commitコマンドを実行して、変更を意味のある最小単位でコミットします。まず現在の状態を確認して進めましょう。

⏺ Update Todos
  ⎿  ☐ リポジトリルートに移動して現在の状態を確認
     ☐ 差分を取得して.claude/tmp/current_changes.patchに保存
     ☐ 差分を分析して意味的にグループ化
     ☐ git-sequential-stageで選択したhunkをステージング&コミット

✻ Thinking…

  ToDoリストを作成しました。それでは最初のタスクから開始します。

⏺ Update Todos
  ⎿  ☐ リポジトリルートに移動して現在の状態を確認
     ☐ 差分を取得して.claude/tmp/current_changes.patchに保存
     ☐ 差分を分析して意味的にグループ化
     ☐ git-sequential-stageで選択したhunkをステージング&コミット

✻ Thinking…

  それではまず、リポジトリルートに移動して現在の状態を確認します。複数のコマンドを並列で実行します。

⏺ Bash(git rev-parse --show-toplevel)
  ⎿  /Users/yasuhisa.yoshida/work/claude-code-commands

⏺ Bash(pwd)
  ⎿  /Users/yasuhisa.yoshida/work/claude-code-commands

⏺ Bash(git status)
  ⎿  On branch main
     Your branch is up to date with 'origin/main'.

     … +6 lines (ctrl+r to expand)

✻ Thinking…

  現在リポジトリルートにいることが確認できました。README.mdが変更されています。次のステップに進みます。

⏺ Update Todos
  ⎿  ☒ リポジトリルートに移動して現在の状態を確認
     ☐ 差分を取得して.claude/tmp/current_changes.patchに保存
     ☐ 差分を分析して意味的にグループ化
     ☐ git-sequential-stageで選択したhunkをステージング&コミット

✻ Thinking…

  次に差分を取得する必要があります。まず新規ファイルがあるか確認し、その後差分を.claude/tmp/current_changes.patchに保存します。

⏺ Bash(git ls-files --others --exclude-standard | xargs -r git add -N)
  ⎿  (No content)

✻ Thinking…

  新規ファイルはなかったようです。次にコンテキスト付きの差分を取得してファイルに保存します。

⏺ Bash(git diff HEAD > .claude/tmp/current_changes.patch)
  ⎿  (No content)

✻ Thinking…

  差分ファイルが作成されました。内容を確認してみましょう。

⏺ Read(.claude/tmp/current_changes.patch)
  ⎿  Read 85 lines (ctrl+r to expand)

✻ Thinking…

  README.mdに大きな変更が加えられているようです。最初はほぼ空のファイルだったのが、完全なREADMEファイルになっています。これは単一の論理的な変更(READMEの初期作成)として
  扱えそうです。

  ただし、内容を詳しく見ると:
  1. プロジェクトの概要説明(Overview、Installation、Usage)
  2. 利用可能なコマンドの説明(Available Commands)
  3. 新しいコマンドの追加方法(Adding New Commands)
  4. ライセンス情報

  これらは異なる意味的なセクションとして分けることも可能ですが、READMEの初期作成という観点では1つのコミットとして扱うのが適切かもしれません。

  実際のhunk数を確認しましょう。

⏺ Bash(grep -c '^@@' .claude/tmp/current_changes.patch)
  ⎿  1

✻ Thinking…

  1つのhunkしかないようです。これはREADME.mdのほぼ全体が1つの変更として扱われているためです。この場合、semantic
  commitとしては、README全体を1つのコミットとして扱うのが適切です。

  ToDoリストを更新して、分析結果を反映します。

⏺ Update Todos
  ⎿  ☒ リポジトリルートに移動して現在の状態を確認
     ☒ 差分を取得して.claude/tmp/current_changes.patchに保存
     ☐ 差分を分析して意味的にグループ化
     ☐ git-sequential-stageで選択したhunkをステージング&コミット

✻ Thinking…

  分析結果:
  - README.mdファイルに対する1つの大きなhunk
  - 内容は完全なREADMEファイルの初期作成
  - 複数のセクションが含まれているが、「READMEの初期作成」という1つの論理的な変更として扱うのが適切

  これを1つのコミットとして処理します。

⏺ Update Todos
  ⎿  ☒ リポジトリルートに移動して現在の状態を確認
     ☒ 差分を取得して.claude/tmp/current_changes.patchに保存
     ☒ 差分を分析して意味的にグループ化
     ☐ git-sequential-stageで選択したhunkをステージング&コミット

✻ Thinking…

  それでは、git-sequential-stageを使用してステージングとコミットを行います。README.mdの1つのhunkをステージングします。

⏺ Bash(git-sequential-stage -patch=".claude/tmp/current_changes.patch" -hunk="README.md:1")
  ⎿  Successfully staged specified hunks

✻ Thinking…

  成功しました。次にコミットメッセージを作成してコミットします。これはREADMEファイルの初期作成なので、適切なコミットメッセージを作成します。

⏺ Bash(git commit -m "docs: Add comprehensive README for claude-code-commands…)
  ⎿  [main c7ed7e8] docs: Add comprehensive README for claude-code-commands
      1 file changed, 76 insertions(+), 1 deletion(-)

✻ Thinking…

  コミットが成功しました。残りの変更があるか確認します。

⏺ Bash(git diff HEAD | wc -l)
  ⎿  0

✻ Thinking…

  残りの変更はありません。すべての変更がコミットされました。ToDoリストを更新して完了とします。

参考: 試行錯誤の過程

  • 単純なgit addpermissions.denyに入れてしまえばいいのでは?と考えた
  • 人間でいうgit add -ptigを使った選択的なhunkのステージングを強制できればLLM Agentもうまく動けそう
    • 忘れることがなければ、hunkを意味の単位で取り扱うということ自体をこなす能力はありそうだった
  • しかし、git add -pなどはinteractiveなツールであり、コマンド経由でタスクをするLLM Agentと相性が悪い...
    • => じゃあ、LLM Agentが扱いやすい形でhunkを意味の単位で選択できるやり方を探ろう!
  • hunkを作るにはgit diff HEADで十分である
    • 新規のファイルに対してはgit add -Nすればよい
  • hunkを選択するためのツールとしてはfilterdiffという枯れたツールがあり、これを使えばよさそう
    • このツールは知らなかったけど、Claude Codeとのやり取りの中で教えてもらった
    • filterdiffを使うと、Claude Codeは「この意味だとfile1のhunk 1,3,4とfile2のhunk 1,2、あの意味だとfile2のhunk 3, 5」といった具合にhunkを分類することができる!
  • しかし、そのままだと難しい
    • hunkをgit applyしていく際に後段の行番号がずれてしまうため
    • hunkを一つずつ逐次的にgit applyするコマンドラインツールを作ってやることにした
  • syou6162/git-sequential-stage
    • 逐次的にやるとhunkも毎回変わってしまうが、その辺をうまく取り扱う方法としてgit patch-idというのがあるのを教えてもらった(参考)
  • 成果物としてはこのカスタムスラッシュコマンドとなる