Takeshi Ikemoto

医療 × 経営 × テクノロジー

·note記事·note·feature

【開発日誌】Claude Code × Python で実現する「最強の協働(Human-in-the-loop)」ワークフロー

業務効率化pythonClaudeCodeAI開発vibeCoding

はじめに:「運び屋」からの脱却

前回の記事までで、リポジトリ初期化のための「3段階プロンプト(概要生成→詳細設計→初期化)」は完成しました。これにより、AIによる設計品質は劇的に向上しました。

しかし運用には大きな課題が残りました。開発者が毎回ターミナルを開き、長いプロンプトをコピペし、AIの出力をコピーして次のプロンプトに貼り付けるという「人間がAIの出力を運ぶ運び屋」作業が必要だったのです。

今回の第5回では、この「プロンプト3本立て」を Claude Code CLI(`claude`)と Python でオーケストレーションし、「面倒なコピペ作業はゼロ、でも重要な判断は人間がやる」 という理想的な半自動化ワークフローを構築した話を紹介します。

Claude Code CLI × Python による「Human-in-the-loop」自動化

これまでの「人力オーケストレーション」のボトルネックは、プロンプトの連結作業にありました。

そこで目をつけたのが、Anthropic 社が提供する Claude Code CLI(`claude`)です。これを Python スクリプトから制御することで、「単純作業は機械に任せ、人間は監督に徹する」という分業体制を実現しました。

実装したアーキテクチャ(Human-in-the-loop)

今回作成した `init_repo_v2.py` は、以下のフローで動作します。

- User Input: 開発者は実行時に `--idea "..."` でアイデアを 1 行渡すだけ。

こうすることで、「プロンプトをコピペして貼り付ける」という虚無な作業はゼロにしつつ、「勝手に変なファイルを作られないか」という監視(承認)はしっかりできる、安全かつ高速な開発フローが実現しました。

コードの核心部分(イメージ)

# 1. 外部ファイルからプロンプトテンプレートを読み込む tmpl_1 = load_prompt("プロンプト1.md")

# 2. アイデアからサービス概要を作らせる(Step 1) # ユーザーのアイデアを埋め込んでClaudeに投げる prompt1 = tmpl_1.replace("{idea}", user_idea) run_claude(prompt1)

# 3. 人間による確認(Human-in-the-loop) # ここで生成された概要を見て、気に入らなければ修正指示を出せる confirm_overview()

# 4. その概要を使ってファイルを一括生成させる(Step 2 & 3) # 概要を次のプロンプトに埋め込んで自動実行 prompt_final = tmpl_final.replace("{overview}", service_overview) run_claude(prompt_final)

開発裏話:自動化を阻んだ「5つの壁」と解決策

一見スマートに見えるこの自動化スクリプトですが、完成までには泥臭い試行錯誤がありました。ここでは、開発中に直面した主なトラブルと解決策を簡潔に紹介します。

- AIが承認待ちで固まる壁 問題: 画面には何も出ないのに処理が進まない。裏でAIが「ファイル作っていい?」と許可を待ち続けていた。 解決: コマンドに `--dangerously-skip-permissions` を追加し、強制的に作業を進めさせることで解決(※Human-in-the-loop版では一部手動承認に戻しました)。

- 一発勝負すぎて怖い壁 問題: 最初の1行アイデアだけで最後まで突っ走るため、設計がズレると全滅する。 解決: 「サービス概要」ができた時点で一時停止し、人間が確認・修正指示(edit)を出せるステップを追加。

- ブラックボックスの壁 問題: PythonがAIの出力を奪ってしまい、作業中のログが見えず不安。 解決: ログは画面に垂れ流しつつ、肝心のデータ(概要)だけは `_temp_overview.md` という一時ファイル経由で受け渡す「バケツリレー方式」に変更。

- 終わらない親切心の壁 問題: 作業完了後もAIが「他に何かありますか?」と待機してしまい、スクリプトが終了しない。 解決: プロンプト末尾に「完了したら挨拶不要で直ちに終了せよ」という冷徹な指示を追加。

- 立つ鳥跡を濁さず 当初はユーザー任せだった `.gitignore` の作成や一時ファイルの削除も、「そこまでやってこそ自動化だ」と思い直し、スクリプト内で完結するように実装しました。

成果と学び

このスクリプトにより、開発者は「1行のコマンドを叩くだけ」で、以下の成果物を得られるようになりました。

学び:

マニュアル: 新規サービス立ち上げ時のリポジトリ自動初期化手順

今後、新しいプロジェクトを開始する際は、以下の手順に従ってください。

1. 前提条件

以下のツールがインストールされ、認証済みであること。

2. セットアップ(初回のみ)

この開発日誌で紹介している自動化スクリプト `init_repo_v2.py` を、自分のホームディレクトリなど分かりやすい場所に保存します。また、プロンプト自体はメンテナンスしやすいように外部ファイルとして管理します。

要するに何をするの?

やっていることは非常にシンプルで、以下の2ステップだけです。

これだけです。これをコマンドで一気に行う手順が以下になります。

手順

GitHub 等から clone するのではなく、この記事のコードをコピーして使う場合は、以下の手順でファイルを作成してください。

※ 以下の操作はすべて、Cursor の画面下部にある「ターミナル」で行います。 (見当たらない場合は、メニューバーの `Terminal` > `New Terminal` で開けます)

1. 保存場所(フォルダ)を作る

まず、ターミナルで以下のコマンドを実行し、ファイルを保存するための「道具箱」を作ります。

# ホームディレクトリに Scripts と prompts フォルダを一気に作成 mkdir -p ~/Scripts/prompts

2. ファイルを作成してコードを貼り付ける

お好みのエディタ(Cursor や VS Code、メモ帳など)を使って、以下の場所に4つのファイルを作成し、記事内で紹介しているコードやプロンプトの中身をそれぞれコピペして保存してください。

3. 実行権限をつける

最後に、作成したスクリプトを実行できるようにします。 ※ これを行わないと、セキュリティ上の理由で「実行権限がありません」とエラーになる場合があります。

# chmod +x (Change Mode +Executable) = "実行してOK" という許可証を貼る chmod +x ~/Scripts/init_repo_v2.py

💡 なぜ「~/Scripts」なのか?

「道具箱」と「作業現場」を分けるためです。

3. プロジェクト初期化フロー

Step 1: ディレクトリ作成とGit初期化

まず、空のプロジェクトディレクトリ(作業現場)を作成し、Git管理下におきます。

mkdir my-new-service cd my-new-service git init

Step 2: 自動初期化スクリプトの実行

あなたのアイデアを引数に渡してスクリプト(道具)を呼び出します。

【重要】ここでは手動で Claude Code を起動する必要はありません! `python3` コマンドを実行すると、裏側で自動的に Claude Code(`claude` コマンド)が呼び出されて働きます。あなたはただ待っているだけで、チャット画面を見る必要すらありません。

また、スクリプトを今のフォルダにコピーする必要もありません。 下記のようにパス(住所)を指定すれば、Python が勝手に読み込みに行ってくれます。

# 「~/Scripts/init_repo_v2.py」と書くことで、 # 今どこにいても必ず「道具箱」の中にあるスクリプトを見に行って実行します。 python3 ~/Scripts/init_repo_v2.py --idea "子供の予防接種スケジュールを自動管理し、クリニックの予約もできるアプリ"※ Claude Codeの権限確認が表示される場合があります。自動化したい場合は `--dangerously-skip-permissions` フラグの使用を検討してください(スクリプト内の `subprocess` 呼び出しに追加が必要)。

Step 3: 生成結果の確認

スクリプト完了後、以下のファイルが生成されていることを確認します。

Step 4: 開発スタート

ここからはCursorを開き、AIと共に開発を進めます。

ここから初めて、手動で Claude Code を起動します。 家(リポジトリ)は全自動で建ったので、ここからはあなたと Claude(職人)が一緒に入って内装工事を始めるイメージです。

Cursor のターミナルで以下を実行してください。

claudeClaude Code が起動したら、以下のプロンプトをチャット欄に貼り付けて送信してください。これが「ロケットスタート」の合図になります。

docs/project-status.md を読み込んでください。 その内容に基づいて、現在のプロジェクトの状況(フェーズ、目的)を把握し、 「次に私が実行すべき最初のタスク」を具体的に提案してください。`docs/project-status.md` は、いわば「プロジェクトの引き継ぎ書」です。ここには「ここまで何が終わっていて、次は何をやるべきか」が全て自動で記録されています。 そのため、このファイルを読ませるだけで、Claude は一瞬で状況を理解し、「では、まずはこのファイルを編集しましょう」と的確な指示を出し始めてくれます。

付録:Python スクリプトの完全版 (init_repo_v2.py)

最後に、本記事で作成した Python スクリプトの完全版を掲載します。 このファイルを `~/Scripts/init_repo_v2.py` として保存し、実行権限を与えて (`chmod +x`) 使用してください。

#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ プロジェクト初期設定自動化スクリプト (v2.1 - Human-in-the-loop Edition)

Claude Code CLI (`claude`) を活用し、プロンプトの生成からファイル作成までを 完全自動で行うためのスクリプトです。

プロンプト定義は外部のMarkdownファイル(~/Scripts/prompts/*.md)から読み込みます。

Usage: python3 ~/Scripts/init_repo_v2.py --idea "あなたのプロジェクトアイデア" """

import argparse import subprocess import sys import re from pathlib import Path

# デフォルトのプロンプトディレクトリ SCRIPT_DIR = Path(__file__).resolve().parent PROMPTS_DIR = SCRIPT_DIR / "prompts"

# ファイル名の定義 PROMPT_FILE_1 = "初期リポジトリ完全自動化プロンプト1.md" PROMPT_FILE_2 = "初期リポジトリ完全自動化プロンプト2.md" PROMPT_FILE_3 = "初期リポジトリ完全自動化プロンプト3.md"

def load_prompt(filename: str) -> str: """指定されたMarkdownファイルを読み込んで文字列として返す""" path = PROMPTS_DIR / filename if not path.exists(): # フォールバック: ~/Scripts/prompts/ も探す fallback_path = Path.home() / "Scripts" / "prompts" / filename if fallback_path.exists(): path = fallback_path else: print(f"Error: プロンプトファイルが見つかりません: {path}", file=sys.stderr) sys.exit(1)

return path.read_text(encoding="utf-8")

def create_gitignore(): """標準的な .gitignore ファイルを作成する""" gitignore_content = """# Dependencies node_modules .pnp .pnp.js

# Testing coverage .nyc_output

# Production dist build

# Misc .DS_Store .env .env.local .env.development.local .env.test.local .env.production.local

# Logs npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log*

# Editor directories and files .idea .vscode *.swp *.swo

# Temporary files _temp_overview.md """ Path(".gitignore").write_text(gitignore_content, encoding="utf-8") print(">>> .gitignore を作成しました。")

def run_claude(prompt: str, verbose: bool = True, capture: bool = True) -> str: """Claude CLIを実行し、結果を返す""" if verbose: print(">>> Claude に指示を送信中...")

# claude CLIに -p オプションでプロンプトを渡す # --dangerously-skip-permissions: 権限確認をスキップして自動実行 try: cmd = ["claude", "-p", prompt, "--dangerously-skip-permissions"]

# capture=True の場合は出力を取得して返す(Step 1用) if capture: result = subprocess.run(cmd, capture_output=True, text=True, check=True) output = result.stdout if verbose: print(">>> Claude からの応答を受信しました。") return output

# capture=False の場合はリアルタイムに表示(Step 2, 3用) else: subprocess.run(cmd, check=True) return ""

except subprocess.CalledProcessError as e: print(f"Error executing claude command: {e}", file=sys.stderr) sys.exit(1) except FileNotFoundError: print("Error: 'claude' command not found. Please install Claude Code CLI.", file=sys.stderr) sys.exit(1)

def main(): parser = argparse.ArgumentParser(description="Repository Initialization v2 (Auto)") parser.add_argument("--idea", type=str, required=True, help="Project idea") args = parser.parse_args()

print(f"=== プロジェクト初期化 (自動モード v2.1) を開始します ===") print(f"Idea: {args.idea}\n")

# .gitignore の作成 if not Path(".gitignore").exists(): create_gitignore()

# プロンプト読み込み try: tmpl_1 = load_prompt(PROMPT_FILE_1) tmpl_2 = load_prompt(PROMPT_FILE_2) tmpl_3 = load_prompt(PROMPT_FILE_3) except Exception as e: print(f"Initialization failed: {e}", file=sys.stderr) sys.exit(1)

# Step 1: サービス概要の生成 print("--- Step 1: サービス概要の生成 ---")

# プロンプト1の構築 base_prompt1 = tmpl_1.replace("[ここにあなたのプロジェクトアイデアを記述]", args.idea) base_prompt1 = base_prompt1.replace("{idea}", args.idea)

# ファイル経由で受け渡すための指示を追加 prompt1 = base_prompt1 + "\n\n【重要】生成した「## サービス概要」の内容を、現在のディレクトリの `_temp_overview.md` というファイルに保存してください。マークダウン形式で保存し、余計な解説は含めないでください。"

# リアルタイム表示で実行 run_claude(prompt1, capture=False)

# 生成されたファイルを読み込む temp_overview_path = Path("_temp_overview.md") if temp_overview_path.exists(): service_overview = temp_overview_path.read_text(encoding="utf-8") else: print("Error: _temp_overview.md が生成されませんでした。", file=sys.stderr) sys.exit(1)

print("\n[生成された概要 (from _temp_overview.md)]") print(service_overview) print("\n----------------------------------\n")

# ユーザー確認&修正ループ(Human-in-the-loop) while True: choice = input(">>> この概要でプロジェクトを初期化してよろしいですか? (y:はい / n:中止 / e:修正指示): ").lower().strip()

if choice == 'y': break elif choice == 'n': print("初期化を中止します。") sys.exit(0) elif choice == 'e': feedback = input(">>> 修正指示を入力してください: ").strip() if not feedback: continue

print("\n>>> 修正指示に基づいて概要を再生成しています...") refine_prompt = f""" 以下のサービス概要(_temp_overview.md)に対して、ユーザーから修正指示がありました。 内容を更新し、再度 `_temp_overview.md` を上書き保存してください。

現在の概要: {service_overview}

ユーザーからの修正指示: {feedback} """ run_claude(refine_prompt, capture=False)

if temp_overview_path.exists(): service_overview = temp_overview_path.read_text(encoding="utf-8")

print("\n[再生成された概要]") print(service_overview) print("\n----------------------------------\n") else: print("y, n, または e を入力してください。")

# Step 2: ドメイン固有ファイルの生成 print("--- Step 2: ドメイン固有ファイルの生成 ---")

base_prompt2 = tmpl_2.replace("[ここにステップ1で生成したサービス概要をコピペ]", service_overview) prompt2 = base_prompt2 + "\n\n【重要】ファイルの生成が完了したら、直ちに終了してください。"

print(">>> Claude にドメイン固有ファイルの生成を指示します...") run_claude(prompt2, capture=False)

print("\n[ドメイン固有ファイルの生成完了]\n")

# Step 3: 全体初期化(最終プロンプト) print("--- Step 3: 最終リポジトリ初期化 (共通ルール + 統合) ---")

base_prompt3 = tmpl_3.replace("[ここにステップ1で生成した「## サービス概要(ドメイン前提)」ブロックをそのままコピペ]", service_overview)

# 完了時の居座りを防ぐための終了指示 prompt3 = base_prompt3 + "\n\n【重要】全てのファイルの作成・書き込みが完了したら、これ以上の対話を行わずに直ちに終了してください。質問や確認は不要です。"

print(">>> Claude に全体初期化を指示します...") run_claude(prompt3, capture=False)

# クリーンアップ if temp_overview_path.exists(): temp_overview_path.unlink() print("\n>>> 一時ファイル (_temp_overview.md) を削除しました。")

print("\n=== 全工程が完了しました ===")

if __name__ == "__main__": main()