火曜日, 8月 19, 2025
火曜日, 8月 19, 2025
- Advertisment -
ホームニューステックニュースClaude Codeを24時間動かす技術

Claude Codeを24時間動かす技術



きっかけ・背景・課題

リファクタは作業自体はClaude CodeをはじめAIが得意とする作業ですが、対象ファイル数が数百あると、通常のClaude Codeの実行では、作業が途中で停止してしまうという問題がありました。その問題を解決するため、tmuxとPythonを組み合わせて、セッションを永続化し、停止したら自動再起動するスクリプトを書きました。今回、RailsのテストRspecの大規模なリファクタリングを行おうと思いました。リファクタの内容はコントローラー1に対して1ファイルにしていますが、これをアクションごとにテスト側のファイルを分割する作業です。

実装のポイント

  1. tmuxセッションでClaude Codeを実行
  2. Pythonスクリプトで出力を監視し、変更がなければ自動再起動
  3. タスクリストから順次処理し、処理済みタスクを自動削除
  4. 並列実行による効率化

全体アーキテクチャ

clauderというコマンドを自作して、以下の流れで動作します:

  1. ユーザーclauder コマンドを実行
  2. .zshrc内のclauder関数 が SESSION_NAME と COMMAND_FILE_PATH を受け取って Pythonスクリプトを起動
  3. .repeat_tmux.py(監視スクリプト) が以下を実行:

    • tmuxセッションの管理
    • 5秒ごとの出力差分監視
    • 変更がない場合の自動再起動
  4. tmuxセッション内でClaude Code が実行され:

    • order.txtの指示を読み取り
    • tasks.txtから1行目のファイルパスを取得
    • RSpecファイルの分割処理を実行
  5. ファイルシステムで管理されるデータ:
    • tasks.txt: 処理対象ファイルのリスト
    • order.txt: AIへの処理指示書
    • spec/requests/: 分割対象のRSpecファイル群

実装の詳細

1. .zshrc – clauder関数の定義

function clauder() {
  local SESSION_NAME=$1
  local COMMAND_FILE_PATH=$2
  python ~/.repeat_tmux.py $SESSION_NAME $COMMAND_FILE_PATH
}

シンプルなラッパー関数として定義し、Pythonスクリプトを呼び出します。

2. .repeat_tmux.py – 監視と自動再起動の実装

後ほど要点を解説しますが先にファイルの中身を紹介します。


import subprocess
import time
import os
import tempfile
import difflib
import argparse


def kill_tmux_session(session_name):
    """指定されたtmuxセッションをkillする"""
    try:
        
        result = subprocess.run(["tmux", "has-session", "-t", session_name],
                                capture_output=True, text=True)
        if result.returncode == 0:
            
            subprocess.run(["tmux", "kill-session", "-t",
                           session_name], check=True)
            print(f"tmuxセッション '{session_name}' をkillしました")
        else:
            print(f"tmuxセッション '{session_name}' は存在しません")
    except subprocess.CalledProcessError as e:
        print(f"tmuxセッション '{session_name}' のkill中にエラーが発生しました: {e}")
    except Exception as e:
        print(f"予期しないエラーが発生しました: {e}")


def start_new_tmux_session(session_name, command_file_path):
    """新しいtmuxセッションを起動し、指定されたファイルの内容をclaudeコマンドで実行する"""
    try:
        
        message = f"{command_file_path}を読み込んで、指示を実行してください。"

        
        claude_command = f'claude "{message}"'
        subprocess.run(["tmux", "new-session", "-d", "-s", session_name,
                       claude_command], check=True)
        time.sleep(10)  
        print(
            f"新しいtmuxセッション '{session_name}' を起動し、claudeコマンドを実行しました: {claude_command}")
    except FileNotFoundError:
        print(f"ファイル '{command_file_path}' が見つかりません")
        raise
    except Exception as e:
        print(f"tmuxセッション '{session_name}' の起動中にエラーが発生しました: {e}")
        raise


def capture_tmux_pane(session_name):
    """指定されたtmuxセッションのペインの内容をキャプチャして返す"""
    try:
        result = subprocess.run(
            ["tmux", "capture-pane", "-t", f"{session_name}:0.0", "-p"],
            capture_output=True,
            text=True
        )
        return result.stdout
    except Exception as e:
        print(f"ペインのキャプチャ中にエラーが発生しました: {e}")
        return None


def get_diff_content(previous_content, current_content):
    """前回の内容と現在の内容の差分を取得し、行数も返す"""
    try:
        if previous_content is None:
            return "初回実行のため差分なし", 0

        
        previous_lines = previous_content.splitlines(keepends=True)
        current_lines = current_content.splitlines(keepends=True)

        
        diff = difflib.unified_diff(
            previous_lines,
            current_lines,
            fromfile='previous',
            tofile='current',
            lineterm=''
        )
        diff = [
            line for line in diff
            if line.startswith('+') or line.startswith('-')
            if not line.startswith('+++') and not line.startswith('---')
        ]

        diff_content = '\n'.join(diff)
        diff_lines = diff_content.split('\n')

        
        line_count = len(diff_lines)

        return diff_content, line_count
    except Exception as e:
        print(f"差分の取得中にエラーが発生しました: {e}")
        return None, 0


WAIT_SEC = 5


def main():
    
    parser = argparse.ArgumentParser(
        description='指定されたtmuxセッションを監視し、claudeコマンドを実行します。実行例: clauder  ')
    parser.add_argument('session_name', help='tmuxセッション名')
    parser.add_argument('command_file_path',
                        help='claudeに送信するメッセージが含まれるファイルのパス')
    args = parser.parse_args()

    
    kill_tmux_session(args.session_name)
    start_new_tmux_session(args.session_name, args.command_file_path)

    previous_content = None

    print(f"tmuxセッション '{args.session_name}' の監視を開始...")

    while True:
        try:
            
            current_content = capture_tmux_pane(args.session_name)

            if current_content is None:
                print("ペインのキャプチャに失敗しました")
                time.sleep(WAIT_SEC)
                continue

            
            if previous_content is not None and current_content == previous_content:
                print(
                    f"変更が検出されませんでした {time.strftime('%Y-%m-%d %H:%M:%S')}")
                
                kill_tmux_session(args.session_name)
                start_new_tmux_session(
                    args.session_name, args.command_file_path)
            else:
                print(
                    f"変更が検出されました {time.strftime('%Y-%m-%d %H:%M:%S')}")

                
                diff_content, line_count = get_diff_content(
                    previous_content, current_content)
                if diff_content:
                    print(f"差分行数: {line_count}")
                    if line_count > 0:
                        print("差分内容:")
                        print(diff_content)
                else:
                    print("差分が利用できません")

                previous_content = current_content

            
            time.sleep(WAIT_SEC)

        except KeyboardInterrupt:
            print("\nユーザーによって監視が停止されました")
            break
        except Exception as e:
            print(f"予期しないエラー: {e}")
            time.sleep(WAIT_SEC)


if __name__ == "__main__":
    main()

3. order.txt – AIへの処理指示書

参考として今回用いたorder.txtも記載しておきます。こちらもAIと壁打ちして生成させています。使途に応じた今まで通りのプロンプトにタスク管理にファイルシステムを使うイメージです。

# RSpecファイルのアクション別分割指示

あなたは RSpec ファイルをアクションごとに分割する作業を担当します。

## 作業手順

1. **tasks.txt から1行目のファイルパスを取得**
   ```bash
   TARGET_FILE=$(head -n 1 tasks.txt)
   echo "処理対象: $TARGET_FILE"
  1. tasks.txt から処理済みファイルを削除

    tail -n +2 tasks.txt > tasks_tmp.txt && mv tasks_tmp.txt tasks.txt
    
  2. 対象ファイルの詳細分析

    • ファイル内容を読み取り
    • 含まれるアクション(index, show, create, update, destroy, new, edit等)を特定
    • 各アクションのテストケース数をカウント
  3. 分割前の動作確認

    
    docker compose exec web bundle exec rspec $TARGET_FILE --format progress
    
    • 実行結果(パス数、失敗数、総件数)をメモ
  4. アクション別ファイル分割実行
    例:spec/requests/users_spec.rb

    spec/requests/users/index_spec.rb
    spec/requests/users/show_spec.rb  
    spec/requests/users/create_spec.rb
    spec/requests/users/update_spec.rb
    spec/requests/users/destroy_spec.rb
    
  5. 分割後の動作確認

    
    for file in spec/requests/users/*_spec.rb; do
      echo "Testing: $file"
      docker compose exec web bundle exec rspec "$file" --format progress
    done
    
    • 分割前後でテスト件数が一致することを確認
    • 全てのテストがパスすることを確認
  6. 元ファイルの削除

    • 分割完了後、元のファイルを削除
  7. rubocop & コミット

    • docker compose exec web bundle exec rubocop -A “[FILEPATH]”
    • git add [FILEPATH] (削除した方もgit add忘れないで)
    • git commit -m “test: [FILEPATH]をアクション毎によりファイル分割”
  8. 分割結果の報告

    • 分割されたファイル一覧
    • テスト件数の確認結果
    • 動作確認の結果

注意事項

  • 必ず docs/rspec.md の方針に従って分割してください
  • 分割前後でテスト件数が変わらないことを確認してください
  • コメントも必ず移行してください
  • 全てのテストがパスすることを確認してください
  • 日本語でのテスト記述を保持してください
  • ファイル名は {controller名}/{action名}_spec.rb の形式にしてください

エラー対応

  • テスト失敗がある場合は、原因を調査して修正
  • テスト件数が合わない場合は、分割ロジックを見直し
  • ファイルが見つからない場合は、tasks.txt の更新状況を確認

完了条件

  1. 元ファイルが正常にアクション別に分割されている
  2. 分割前後でテスト件数が一致している
  3. 全てのテストが正常にパスしている
  4. tasks.txt から処理済みファイルが削除されている
  5. どうしてもrspecが解決できない移行不可ファイルが見つかったら、該当のファイルは回復、新ファイルは削除して、tasks.errors.txtに追記してtasks.txtからは除去しておいてください。

1つのファイルの分割が完了したら作業終了。次のファイルは別のAIインスタンスが処理します。


### 4. tasks.txt - 処理対象ファイルリスト

今回はファイル名ですが、URLであったりタスクに応じた自由な文字列のリストでも勿論可です。

spec/requests/users_spec.rb
spec/requests/cards_spec.rb
…中略…


## クイックスタート / 最短手順

### 1. 必要なファイルの準備

```bash
# .zshrcに関数を追加
echo 'function clauder() {
  local SESSION_NAME=$1
  local COMMAND_FILE_PATH=$2
  python ~/.repeat_tmux.py $SESSION_NAME $COMMAND_FILE_PATH
}' >> ~/.zshrc

# Pythonスクリプトを配置
cp .repeat_tmux.py ~/

# 処理対象ファイルリストを作成
AIに抽出させます

# 指示書を作成(order.txtの内容をコピー)

2. 実行

session名を明示することでtmuxを他の用途(別件のclauderなど)でも使えるようにしています。

clauder split-rspec order.txt

実装の要点

監視メカニズムの実装

5秒毎にClaude Codeのセッションをキャプチャして秒数、トークン数など含め描画の更新が一切ストップしていたら作業が停止した見なし再起動します。


if previous_content is not None and current_content == previous_content:
    print(f"変更が検出されませんでした {time.strftime('%Y-%m-%d %H:%M:%S')}")
    
    kill_tmux_session(args.session_name)
    start_new_tmux_session(args.session_name, args.command_file_path)

このシンプルな仕組みにより、Claude Codeが停止した場合でも自動的に再起動されます。

タスクキューの実装


TARGET_FILE=$(head -n 1 tasks.txt)


tail -n +2 tasks.txt > tasks_tmp.txt && mv tasks_tmp.txt tasks.txt

指示の途中変更

処理が開始すると、初期のorder.txtがもたらした実行結果への不満があるかと思います。その際は実行中でも後述するorder.txtを修正したりgitコマンドで編集を取り消してtasks.txtに消えたファイル名を積んで再タスク化などすることで次からは改善されたプロンプトで実行が継続します。

まとめ

Claude Codeを24時間稼働させる仕組みを構築することで、大規模なコードリファクタリングを自動化できました。



Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -