tdd について
tddは、テスト作成、実装、リファクタリングを別々のエージェントに割り当てるテスト駆動開発スキルです。テストを先に書く開発者向けに、対話、自動実行、再開、ドライランの各モードを備えています。
主な機能
- 01RED-GREEN-REFACTORサイクルの実行
- 02テスト作成と実装エージェントの分離
- 03アーキテクチャ層に沿ったスライス順序
- 04対話、自動実行、再開モード
- 05ドライランでのプロンプト生成と検証
活用シーン
- 01新機能の実装前にテストを整備
- 02修正前に不具合を再現するテストを作成
- 03コードを書かずにTDDスクリプトとプロンプトを確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ glebis/ claude-skills --skill tddインストール後、Claude Code で/tddを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsonreferences/agent_prompts.mdanti_patterns.mdframework_configs.mdlayer_guide.mdscripts/discover_docs.shextract_api.shrun_tests.shtests/conftest.pytest_discover_docs.pytest_extract_api.pytest_prompts.pytest_run_tests.pytests/fixtures/go_project/calculator.gotests/fixtures/python_project/README.mdtests/fixtures/python_project/docs/API.mdopenapi.yamltests/fixtures/python_project/src/calculator.pymodels.pytests/fixtures/ts_project/README.mdtests/fixtures/ts_project/src/calculator.tstests/snapshots/implementer.txtrefactorer.txttest_writer.txt詳細
テスト駆動開発 — マルチエージェントオーケストレーション
個別のサブエージェントを使って、規律ある RED-GREEN-REFACTOR サイクルを徹底します。核心となる工夫は、テストライターには実装コードを一切見せず、実装担当者には仕様を一切見せないことです。これにより、LLM が実装の意図をテスト設計に漏らすことを防ぎます。
使用するタイミング
- ユーザーが TDD、テストファースト、またはレッド・グリーン・リファクタリングのワークフローを求めている場合
- ユーザーが機能の説明やバグ報告とともに
/tddを指定した場合 - ユーザーが最初からテストカバレッジを確保して機能を追加したい場合
- ユーザーが再現テストを先に作成してからバグを修正したい場合
呼び出しモード
| 呼び出し | 動作 |
|---|---|
/tdd <feature> | 対話モード — スライスおよび各 RED チェックポイントで承認を得るために一時停止します |
/tdd --auto <feature> | 自律モード — 一時停止せずにすべてのスライスを実行します。回復不能なエラーが発生した場合にのみ停止します |
/tdd --resume | プロジェクトのルートにある .tdd-state.json から再開します |
/tdd --dry-run <feature> | 検証モード — Phase 0 と Phase 1 を最後まで実行し、すべてのプロンプトを表示しますが、Task() の呼び出しは省略します。コードは一切書き込みません。 |
--auto モードでは、すべての [HUMAN CHECKPOINT] 手順を省略します。代わりにステータス行を表示します。
[auto] RED slice 1/4: "validates email format" — test failing as expected
[auto] GREEN slice 1/4: passing (attempt 1)
[auto] REFACTOR slice 1/4: 1 suggestion applied, 0 skipped
次の場合にのみ停止し、ユーザーに確認します。
- 5 回試行しても実装に失敗した場合
- 3 回試行してもリグレッションを自動修正できない場合
- 続行不可能なスクリプトエラーが発生した場合(バイナリが見つからない、権限が拒否されるなど)
--dry-run モードでは、サブエージェントを実行したりコードを書き込んだりせずに、オーケストレーションパイプライン全体を検証します。
- Phase 0 を最後まで実行:フレームワークを検出し、ベースラインを確認し、API を抽出し、ドキュメントを検出して、状態ファイルを作成します
- Phase 1 を最後まで実行:スライスに分解します(ユーザーの承認は引き続き必要です)
- 各スライスについて:3 つのエージェント(テストライター、実装担当者、リファクタラー)のプロンプトを、実際の変数を使ってすべて生成します。生成したプロンプトと文字数をユーザーに表示します。
Task()の呼び出しは行いません。テストファイルは書き込まず、実装コードも生成しません。- 検証:すべてのテンプレート変数が解決されていること(
{UNRESOLVED}のプレースホルダーがないこと)、すべてのスクリプトがエラーなく実行できること、状態ファイルが正しい形式であることを確認します。 - 概要を報告:
DRY RUN COMPLETE: {feature name}
Phase 0:
Framework: {framework}
Language: {language}
Baseline: {pass|greenfield}
API surface: {line count} lines
Doc context: {line count} lines (or "none")
Phase 1:
Slices: {N} ({layer breakdown})
Prompts rendered: {N * 3} (all variables resolved)
Test Writer: {char count} chars
Implementer: {char count} chars
Refactorer: {char count} chars
State file: .tdd-state.json written
No code was modified.
このモードは、次の用途に便利です。
- プロジェクトの環境でスクリプトが動作することの検証
- TDD 全体の実行を開始する前のプロンプト内容の確認
- 副作用を発生させずにスキルの変更をテストすること
アーキテクチャの概要
ORCHESTRATOR (you, reading this file)
├─ Phase 0: Setup — detect framework, extract API, create state file
├─ Phase 1: Decompose into vertical slices → user approves
│
├─ FOR EACH SLICE:
│ ├─ Phase 2 (RED): Task(Test Writer) ← spec + API only
│ ├─ Phase 3 (GREEN): Task(Implementer) ← failing test + error only
│ └─ Phase 4 (REFACTOR): Task(Refactorer) ← all code + green results
│
└─ Summary
コンテキストの境界(重要な制約)
| エージェント | 閲覧できる情報 | 閲覧できない情報 |
|---|---|---|
| テストライター | スライスの仕様、公開 API のシグネチャ、フレームワークの規約、レイヤーの制約 | 実装コード、他のスライス、実装計画 |
| 実装担当者 | 失敗するテストコード、テスト失敗の出力、ファイルツリー、既存のソースコード、レイヤーの制約 | 元の仕様、スライスの説明、今後の計画 |
| リファクタラー | すべての実装とすべてのテスト、成功結果、変更したレイヤー | 元の仕様、分解の根拠 |
ワークフロー
フェーズ 0:セットアップ(セッションごとに 1 回)
ステップ 1:フレームワークとテストランナーを検出します。
Check for: package.json (jest/vitest), pyproject.toml/pytest.ini (pytest),
go.mod (go test), Cargo.toml (cargo test), Gemfile (rspec), composer.json (phpunit)
判別できない場合は、「テストの実行コマンドは何ですか?(例:npm test、pytest)」と尋ねます。
ステップ 2:ソースファイルから言語を検出します(エージェントのプロンプト用)。
TypeScript (.ts/.tsx), JavaScript (.js/.jsx), Python (.py), Go (.go), Rust (.rs), Ruby (.rb), PHP (.php)
ステップ 3:テストがすべて成功するベースラインを確認します。
bash ~/.claude/skills/tdd/scripts/run_tests.sh {FRAMEWORK} "{TEST_COMMAND}"
JSON 出力を解析します。
statusが"pass"の場合:続行します。statusが"fail"の場合:停止します。「既存のテストが失敗しています。TDD はテストがすべて成功するベースラインから開始します。」statusが"error"かつtotalが 0 の場合:グリーンフィールドプロジェクトです。テストはまだありません。問題ありません。続行します。
ステップ 4:公開 API の範囲を抽出します。
bash ~/.claude/skills/tdd/scripts/extract_api.sh {SOURCE_DIR}
出力を保存します。これがテスト作成者に表示されます。空の場合(グリーンフィールド)は想定どおりです。
ステップ 5:プロジェクトのドキュメントを検出します。
bash ~/.claude/skills/tdd/scripts/discover_docs.sh {PROJECT_ROOT} --lang {LANGUAGE}
以下を検索します。
- ドキュメントファイル:README、ARCHITECTURE.md、docs/ フォルダー、DESIGN.md、SPEC ファイル、ADR
- API 仕様:OpenAPI/Swagger、GraphQL スキーマ、.proto ファイル
- ソースの docstring:JSDoc、Python の docstring、Go の doc コメント、Rust の
///コメント
出力を {DOC_CONTEXT} として保存します。これは以下で使用します。
- フェーズ 1 — ドキュメントに記載された振る舞いと API コントラクトに基づいてスライスを分解します
- フェーズ 2 — テスト作成者がコードのシグネチャだけでなく、文書化された意図に沿ったテストを作成します
空の場合(ドキュメントが見つからない場合)も問題ありません。ドキュメントのコンテキストなしで続行します。
ステップ 6:プロジェクトのルートに状態ファイル .tdd-state.json を作成します。
{
"feature": "user's feature description",
"framework": "jest|vitest|pytest|go|cargo|rspec|phpunit",
"language": "typescript|javascript|python|go|rust|ruby|php",
"test_command": "the full test command",
"source_dir": "src/",
"doc_context": "output from discover_docs.sh (or empty string)",
"auto_mode": false,
"dry_run": false,
"slices": [],
"current_slice": 0,
"phase": "setup",
"layer_map": {},
"files_modified": [],
"test_files_created": []
}
slices 配列内の各スライスには、"domain"、"domain-service"、"application"、または "infrastructure" の layer フィールドが含まれます。レイヤーの割り当て方法については、フェーズ 1 を参照してください。
layer_map はディレクトリのプレフィックスをレイヤーに対応付けます。フェーズ 1 でプロジェクトの構造に基づいて作成します。
{
"layer_map": {
"src/domain/": "domain",
"src/services/": "domain-service",
"src/application/": "application",
"src/infrastructure/": "infrastructure",
"src/adapters/": "infrastructure",
"src/controllers/": "infrastructure"
}
}
プロジェクトに明確なディレクトリとレイヤーの対応関係がない場合(フラットな構造の場合)は、layer_map を {} に設定し、パスベースの検証を省略します。
ステップ 5a(layer_map の自動検出):layer_map が空の場合、ソースディレクトリをスキャンして、DDD/レイヤードアーキテクチャで一般的なディレクトリ名を探し、自動で値を設定します。
Common directory → layer mappings (check if directories exist):
*/domain/ → "domain"
*/models/ → "domain" (ORM models often serve as domain entities)
*/entities/ → "domain"
*/value_objects/ → "domain"
*/services/ → "application" (unless clearly infrastructure)
*/application/ → "application"
*/use_cases/ → "application"
*/core/ → "application"
*/infrastructure/ → "infrastructure"
*/adapters/ → "infrastructure"
*/controllers/ → "infrastructure"
*/api/ → "infrastructure"
*/bot/ → "infrastructure" (Telegram/Discord bot handlers)
*/handlers/ → "infrastructure"
*/repositories/ → "infrastructure" (concrete repo implementations)
ソースツリーに実際に存在するディレクトリだけを追加します。一致するディレクトリが 2 つ未満の場合は、layer_map を空のままにします(フラットなプロジェクト)。自動検出したマップをユーザーに提示し、確認を求めます。
Auto-detected layer map from directory structure:
src/models/ → domain
src/services/ → application
src/core/ → application
src/bot/ → infrastructure
src/api/ → infrastructure
Does this mapping look correct? (adjust if needed)
状態を更新します:"phase": "setup"。状態ファイルにすぐ書き込みます。
フェーズ 1:仕様の分解
ユーザーの機能リクエストを受け取り、順序付けされた垂直スライスに分解します。各スライスは、テスト可能な振る舞いを 1 つ表します。
ドキュメントのコンテキストを使用:分解する際は、フェーズ 0 のステップ 5 で取得した {DOC_CONTEXT} を相互参照します。ドキュメントには、スライスの境界を決める際に考慮すべき意図された振る舞い、エッジケース、API コントラクトが記載されていることがよくあります。ドキュメントに具体的なエラーケース、検証ルール、または振る舞いの要件が記載されている場合は、スライスの候補として検討します。
内側から外側へのスライス順序
すべてのスライスを特定した後、アーキテクチャのレイヤーに従って内側から外側へ並べ替えます。これにより、各スライスは前のスライスで実装された(モックではない)実際の実装を基盤として構築できます。
- ドメインモデルのスライスを最初に配置します — 純粋なロジックで、依存関係もモックも不要です
- ドメインサービスのスライスを配置します — 実際のドメインオブジェクトを使用する、複数の集約にまたがる操作です
- アプリケーションサービス/ユースケースのスライスを配置します — ポート用のインメモリフェイクを使用したオーケストレーションです
- インフラストラクチャアダプターのスライスを最後に配置します — リポジトリ、外部 API、フレームワークアダプターです
各スライスに domain、domain-service、application、または infrastructure の layer タグを割り当てます。分類には references/layer_guide.md のヒューリスティックを使用します。
なぜ内側から外側へ進めるのですか? ドメインスライスによって作成された実際のオブジェクトを、後続のスライスで直接使用できます。これにより、モックを最小限に抑え、統合上の問題を早期に発見できます。また、インフラストラクチャに関する判断を行う前に、ビジネスルールが実装され、テストされるようになります。
すべてのコードが 1 つのレイヤーに収まるシンプルなプロジェクトでは、すべてのスライスに layer: "application" を割り当て、順序は変更しません。このガイダンスは、そのような場合にも柔軟に適用されます。
スライスの順序に関するエッジケース
インフラストラクチャのみの機能(例:「メールプロバイダーの再試行ロジックを追加する」、「Postgres から MySQL に切り替える」):
- 機能にドメインまたはアプリケーションの振る舞いの変更がまったくない場合、すべてのスライスを
infrastructureにできます。これは有効です。内側のレイヤーはすべてスキップしてください。 - 次のように提示します:「これはインフラストラクチャのみの変更です。すべてのスライスがインフラストラクチャレイヤーです。」
ポートインターフェースがない場合(domain-service が、まだ存在しないポートを必要とする場合):
- ポートを必要とする最初のスライスで、実装の一部としてインターフェースを作成します。実装者は内側のレイヤーにファイルを作成できます(domain/domain-service は独自のポートを定義できます)。
- 例:
RegistrationService用のdomain-serviceスライスで、GREEN の一部としてdomain/ports/UserRepositoryインターフェースを作成します。
横断的なスライス(スライスが複数のレイヤーにまたがる場合):
- 触れるレイヤーのうち、最も内側のレイヤーをタグ付けします。実装者はそのレイヤーと、それより内側の任意のレイヤーにファイルを作成できます。
- 例:新しいドメインイベントも導入するユースケースは
applicationとタグ付けしますが、domain/events/にファイルを作成します。
ユーザーに次のように提示します:
I've broken this into N vertical slices (ordered inside-out):
Domain:
1. [behavior] — [what the test verifies]
Domain Services:
2. [behavior] — [what the test verifies]
Application:
3. [behavior] — [what the test verifies]
Infrastructure:
4. [behavior] — [what the test verifies]
Each slice follows RED -> GREEN -> REFACTOR before moving to the next.
Does this decomposition look right?
すべてのスライスが1つのレイヤーに属する場合は、レイヤー見出しを省略し、フラットなリストとして提示します。
ユーザーの承認を待ちます(--auto モードでも、スライスの分解には常に承認が必要です)。
状態を更新します:スライス配列(各要素に layer フィールドを含む)を書き込み、"phase": "decomposed" に設定します。
ドライランのフェーズ上書き(フェーズ 2~4)
--dry-run モードでは、フェーズ 2~4 を完全に置き換え、各スライスについて以下を実行します:
- API サーフェスを更新します(
extract_api.sh)。 - すべての変数を埋めた テストライターのプロンプトを生成します。
### Test Writer Prompt (slice N)見出しの下に出力します。 - プレースホルダーのテストコードを使用して 実装者のプロンプトを生成します。
{FAILING_TEST_CODE}には"(dry-run: test code would be generated by Test Writer)"を、{TEST_FAILURE_OUTPUT}には"(dry-run: no test output)"を使用します。 - プレースホルダー値を使用して リファクタラーのプロンプトを生成します。
{GREEN_TEST_OUTPUT}には"(dry-run: no green output)"を、{ALL_TEST_CODE}には"(dry-run: code from Test Writer)"を、{ALL_IMPLEMENTATION_CODE}には"(dry-run: code from Implementer)"を使用します。 - 生成した各プロンプトについて、
{UNRESOLVED_VARIABLE}のパターンが残っていないことを確認します(正規表現:\{[A-Z][A-Z_]+\})。未解決の変数があれば、エラーとして報告します。 - 各プロンプトの文字数を出力します。
- 次のスライスに進みます(
Task()呼び出し、ファイル書き込み、テスト実行は行いません)。
すべてのスライスを処理したら、ドライランの概要を出力して終了します。状態ファイルはクリーンアップしないでください。後続の --resume で利用できます。
フェーズ 2:RED — 失敗するテストを1つ書く
ステップ 1:API サーフェスを更新します(スライスの実装に伴って変化します):
bash ~/.claude/skills/tdd/scripts/extract_api.sh {SOURCE_DIR}
ステップ 2:references/agent_prompts.md の「Test Writer Agent」セクションからプロンプトテンプレートを読み込みます。以下を埋めてプロンプトを作成します:
{SLICE_SPEC}:現在のスライスの振る舞いの説明{LANGUAGE}:フェーズ 0 で検出した言語{FRAMEWORK}:検出したフレームワーク名{API_SURFACE}:extract_api.sh の出力{DOC_CONTEXT}:discover_docs.sh の出力(フェーズ 0、ステップ 5)。現在のスライスに関連するセクションのみを含めます。出力全体が大きい場合は、キーワードの一致で絞り込みます。{TEST_FILE_PATH}:テストを配置する場所(プロジェクトの規約に従います){EXISTING_TEST_CONTENT}:テストファイルが存在する場合はその現在の内容、存在しない場合は「テストファイルはまだありません。」{FRAMEWORK_SKELETON}:references/framework_configs.mdにある該当するスケルトン{LAYER}:フェーズ 1 で付与したスライスのレイヤータグ{LAYER_TEST_CONSTRAINTS}:レイヤー固有のテスト制約(agent_prompts.md -> Layer-Specific Constraint Lookup を参照)
ステップ 3:テストライターエージェントを起動します:
Task(subagent_type="general-purpose", prompt=<constructed prompt>)
Step 4:agent_prompts.md の parse_agent_json ロジックを使用して JSON レスポンスを解析します。
- Markdown のコードフェンスがあれば削除します
- JSON の直接解析を試みます
- 失敗した場合は、最初の
{と最後の}を見つけ、その部分文字列の解析を試みます - それでも無効な場合は、末尾に「JSON オブジェクトのみを返してください。」を追加して、Task 呼び出しを 1 回再試行します
- それでも失敗する場合は、生のレスポンスからテストコードを手動で抽出します
Step 5:テストコードをテストファイルに書き込みます。ファイルが存在する場合は、テスト関数を追記し、インポートをマージします。新規の場合は、エージェントの imports_needed と test_code を使って作成します。
Step 5a(書き込み後のテストスメルスキャン):実行前に、テストコードをスキャンして一般的なスメルを検出します。
| スメル | 検出方法 | アクション |
|---|---|---|
| Assertion Roulette | 同じテスト関数内に、メッセージのない単独の assert 文が複数(3 件以上)ある | ユーザーに警告します(ブロックしません)。「テストにはメッセージのない assert が N 件あります。デバッグしやすくするため、失敗メッセージの追加を検討してください。」 |
| Unknown Test | テスト名が汎用的である:test_1、test_it、test_works、test_example、test_thing に一致する | 末尾に「動作仕様として読める、説明的なテスト名を使用してください(例:test_rejects_empty_email)。」を追加して、Test Writer を再起動します。 |
| Tautological assertion | assert True、関数に None を返す経路がない場合の assert result is not None、唯一のアサーションとしての assert isinstance(result, X) | 末尾に「アサーションがトートロジーになっています。関数が何かを返すことだけでなく、実際の動作や値をテストしてください。」を追加して、Test Writer を再起動します。 |
Step 5b(書き込み後のレイヤーリント):テストコードをスキャンし、レイヤー違反となるパターンを検出します。
| レイヤー | テストコードで禁止されるパターン |
|---|---|
domain | jest.mock(、vi.mock(、Mock(、mock.patch、unittest.mock、gomock、mockery — ドメインテストではモックライブラリを使用してはいけません |
domain-service | ドメインオブジェクトに対する同じモックパターン(ポート/リポジトリのモックは許可されます) |
application | 禁止パターンなし(ポートのモックは想定されています) |
infrastructure | 禁止パターンなし |
禁止パターンが見つかった場合:
- 問題のあるモック/パターンをテストから削除します
- 末尾に「モックライブラリを使用しないでください。これは {LAYER} レイヤーのテストです。実際のドメインオブジェクトを使用してください。」を追加して、Test Writer を再起動します
- 2 回目の試行でも禁止パターンが使用されている場合は、ユーザーに確認します
Step 6:テストを実行して、失敗することを確認します(セットアップエラーではなく、アサーションエラーを想定します)。
bash ~/.claude/skills/tdd/scripts/run_tests.sh {FRAMEWORK} "{TEST_COMMAND_FOR_SPECIFIC_TEST}"
Step 7:セマンティック検証で結果を評価します。
| 結果 | アクション |
|---|---|
status: "fail"、アサーションエラー | 適切な RED — 期待される動作がまだ存在しないため、テストが失敗しています。続行します。 |
status: "fail"、ImportError / ModuleNotFoundError | セットアップの問題であり、適切な RED ではありません。 テスト対象のモジュールをインポートすることさえできません。修正方法:インポートが解決されるように、最小限のスタブ(空のクラス/関数)を作成してから再実行します。今度はアサーションで失敗するはずです。 |
status: "fail"、メソッドがないことによる AttributeError | インポートエラーと同様です。クラスは存在しますが、メソッドがありません。アサーションも失敗する場合は、許容される RED です。続行します。 |
status: "pass" | 動作はすでに存在しています。記録:「テストは成功しました — スライスをスキップします(実装済み)。」current_slice をインクリメントし、次のスライスに進みます。 |
status: "error"、SyntaxError | 修正:テストに誤記があります。raw_tail を読み、テストファイルを直接修正します。再実行します。2 回修正してもエラーが続く場合は、ユーザーに確認します。 |
status: "error"、コンパイル/フレームワークエラー | 修正:インポートの誤り、フィクスチャの不足、またはフレームワークの設定ミスです。raw_tail を読み、テストファイルを直接修正します。2 回修正してもエラーが続く場合は、ユーザーに確認します。 |
Step 8(対話モードのみ — --auto ではスキップ):ユーザーに次の内容を提示します。
RED: Test written and failing as expected.
Test: {test_name}
File: {test_file_path}
Failure: {failure message from JSON}
This test verifies: {test_description from agent response}
Proceed to GREEN phase? (or adjust the test?)
GREEN に進む前に、ユーザーの承認を待ちます。
状態を更新:"phase": "red" とし、test_files_created にテストファイルを追加します。状態をすぐに書き込みます。
Phase 3: GREEN — 最小限の実装
ステップ 1:失敗したテストファイルと、テスト失敗時の出力(RED フェーズの run_tests.sh 実行結果に含まれる raw_tail 全体)を読みます。
ステップ 2:ソースファイルのファイルツリーを作成します(テストファイル、node_modules などは含めません)。
find {SOURCE_DIR} -type f \( -name '*.ts' -o -name '*.js' -o -name '*.py' -o -name '*.go' -o -name '*.rs' -o -name '*.rb' -o -name '*.php' \) | grep -v test | grep -v spec | grep -v node_modules | grep -v __pycache__ | grep -v vendor | grep -v target | grep -v dist | grep -v build | head -50
ステップ 3:テストがインポートまたは参照する既存のソースファイルを読みます。
ステップ 4:references/agent_prompts.md の「Implementer Agent」セクションからプロンプトテンプレートを読みます。以下を入力します。
{LANGUAGE}:検出された言語{FAILING_TEST_CODE}:テストファイルの完全な内容{TEST_FAILURE_OUTPUT}:run_tests.shの JSON 出力に含まれるraw_tail{FILE_TREE}:ステップ 2 のソースファイル一覧{EXISTING_SOURCE}:関連するソースファイルの内容(ある場合。新規プロジェクトでは空の場合があります){LAYER}:フェーズ 1 のスライスのレイヤータグ{LAYER_DEPENDENCY_CONSTRAINT}:レイヤー固有の依存関係の制約(agent_prompts.mdの「Layer-Specific Constraint Lookup」を参照)
再試行時(試行回数が 1 を超える場合)は、{?PREVIOUS_ATTEMPT} セクションにも以下を入力します。
{PREVIOUS_ATTEMPT_DESCRIPTION}:失敗した試行のexplanationフィールド{PREVIOUS_ATTEMPT_ERROR}:失敗した試行後のテスト実行から得られたraw_tail
重要:スライスの仕様、機能の説明、今後の計画は含めないでください。Implementer はテストだけをもとに作業します。
ステップ 5:Implementer エージェントを起動します。
Task(subagent_type="general-purpose", prompt=<constructed prompt>)
ステップ 6:JSON レスポンスを解析します。レイヤー境界を検証してから、ファイルの変更を適用します。
ステップ 6a(レイヤーパスの検証):layer_map が空でない場合、レスポンス内の各ファイルパスが現在のスライスのレイヤーに該当するか確認します。
For each file in response.files:
inferred_layer = lookup file.path against layer_map (longest prefix match)
if inferred_layer exists AND inferred_layer != current_slice.layer:
if inferred_layer is OUTER relative to current_slice.layer:
REJECT: "Implementer created/modified {file.path} which belongs to
the {inferred_layer} layer, but this is a {current_slice.layer} slice.
Inner layers must not depend on outer layers."
→ Re-launch Implementer with appended constraint:
"Do NOT create or modify files in {inferred_layer} directories.
This slice is {current_slice.layer} only."
if inferred_layer is INNER relative to current_slice.layer:
ALLOW: outer layers may touch inner-layer files (e.g., adding a port interface)
「外側」のチェックにおけるレイヤーの順序:domain < domain-service < application < infrastructure。
layer_map が空の場合(フラットなプロジェクト)は、この検証を省略します。
ステップ 6b:検証済みのファイル変更を適用します。
レスポンスの files 配列に含まれる各ファイルについて:
actionが"create"または"overwrite"の場合:Write ツールを使用して、完全な内容でファイルを作成または上書きします。actionが"edit"の場合(200 行を超える既存ファイルに使用):Edit ツールでold_string→new_stringを適用します。Implementer が周辺のコンテキストを含む変更済み関数のみを返すため、挿入箇所または置換する関数を特定し、それに応じて Edit ツールを使用します。編集対象を特定できない場合は、ファイル全体を読み込んで Write を使用します。- 200 行を超える既存ファイルに対して、Implementer が完全な内容を返した場合(action = "overwrite")は、変更されていないコードの誤ったフォーマット変更を防ぐため、差分のみを適用できる Edit ツールの使用を優先します。
ステップ 7:対象のテストを実行します。
bash ~/.claude/skills/tdd/scripts/run_tests.sh {FRAMEWORK} "{TEST_COMMAND_FOR_SPECIFIC_TEST}"
ステップ 8:再試行ループ(テストがまだ失敗する場合):
attempt = 1
max_attempts = 5
previous_explanation = null
previous_error = null
while status != "pass" AND attempt <= max_attempts:
previous_explanation = explanation from last Implementer response
previous_error = raw_tail from last test run
Launch FRESH Task(Implementer) with:
- same test code + file tree + existing source (re-read!)
- NEW failure output
- PREVIOUS_ATTEMPT section filled in
Apply changes (Write tool for each file)
Re-run test
attempt += 1
if still failing after max_attempts:
STOP. Present to user:
"Implementation failed after 5 attempts. Last error: {raw_tail}"
Ask: "Adjust the test, try a different approach, or debug manually?"
再試行ごとに、前回の試行の説明とエラーのみを渡して、新しい Task 呼び出しを行います。これにより、別の方法を試すのに十分なコンテキストを提供しながら、Implementer が深入りしすぎるのを防ぎます。
ステップ 9:対象のテストが成功したら、テストスイート全体を実行します。
bash ~/.claude/skills/tdd/scripts/run_tests.sh {FRAMEWORK} "{FULL_TEST_COMMAND}" --all
ステップ 10:リグレッションに対処します。
| 結果 | 対応 |
|---|---|
| すべて成功 | REFACTOR に進む |
| リグレッションが見つかった | 自動修正:リグレッションのテスト失敗を渡して、新しい Implementer を起動します。変更を適用し、テストスイート全体を再実行します。最大 3 回まで繰り返します。3 回のリグレッション修正を試しても失敗する場合は、停止してユーザーに提示します。 |
ステップ 11(インタラクティブモードのみ。--auto では省略):ユーザーに提示します。
GREEN: Test passing with minimal implementation.
Implementation: {explanation from agent response}
Files changed: {list}
All tests: {passed} passing, {failed} failing
Proceed to REFACTOR phase? (or adjust?)
状態を更新:"phase": "green" に設定し、files_modified を更新します。直ちに状態を書き込みます。
ステップ 12(domain/domain-service スライスのみ):REFACTOR の前にレイヤーの純粋性を確認します。
domain または domain-service レイヤーのスライスに含まれる新規または変更済みの各ファイルについて:
- インポートのスキャン:すべての import/require 文を読みます。各インポート対象モジュールを
layer_mapと照合します。外側のレイヤーからのインポートがあれば、違反として報告します。 - コンストラクターの確認:コンストラクターが外側のレイヤーで型指定されたパラメーターを受け取っていないことを確認します(ORM セッション、HTTP クライアント、フレームワーク設定など)。
- 静的呼び出しの確認:外側のレイヤーのコードを静的メソッド呼び出しで呼び出していないことを確認します。
- 違反が見つかった場合は、REFACTOR に進む前に依存関係をポートインターフェースに移動して修正します。
ステップ 13:リポジトリ全体のインポートスキャン(すべてのレイヤーを対象に、スライスごとに 1 回実行):
セッション中に変更されたファイルだけでなく、すべてのソースファイルをスキャンして、依存関係の方向に関する違反を確認します。
# For each source file, extract imports and check against layer_map
# Language-specific patterns:
# Python: from X import Y, import X
# TypeScript/JS: import ... from 'X', require('X')
# Go: import "X"
各ファイルについて:
layer_mapからレイヤーを特定します(一致しない場合はスキップします)。- 各インポートについて、インポート対象モジュールのレイヤーを
layer_mapから特定します。 - インポート対象のレイヤーがファイルのレイヤーよりも外側の場合は、違反です。
REFACTOR の前に、違反をユーザーに報告します。
Layer scan found N dependency direction violation(s):
- domain/user.py imports infrastructure/db.py (domain → infrastructure)
- domain/services/registration.py imports adapters/email.py (domain-service → infrastructure)
--auto モードでは、自動修正を試みます(具象インポートをポートインターフェースに置き換えます)。対話モードでは、違反を提示して、どのように進めるかユーザーに確認します。
これは、リポジトリ全体のスキャンを行い、Refactorer のインポートチェック(セッションのファイルのみを対象とします)を補完します。静的ツールでは約 23% の違反を見逃します(Pruijt et al., 2017)。テキストと構造のチェックを組み合わせることで、検出範囲が広がります。
フェーズ 4:REFACTOR
ステップ 1:すべてのコンテキストを収集します。
- このセッション中に作成または変更されたすべてのテストファイル
- このセッション中に変更されたすべてのソースファイル
- すべてのテストが成功したことを示す出力
ステップ 2:references/agent_prompts.md の「Refactorer Agent」セクションからプロンプトテンプレートを読み込み、次の値を入力します。
{LANGUAGE}:検出された言語{GREEN_TEST_OUTPUT}:すべてのテストが成功したことを示す完全なテスト出力{ALL_TEST_CODE}:すべてのテストファイルの内容{ALL_IMPLEMENTATION_CODE}:変更されたすべてのソースファイルの内容{SLICE_LAYERS}:ここまでに完了したすべてのスライスから、重複のないレイヤーをカンマ区切りで列挙したもの
ステップ 3:Refactorer エージェントを起動します。
Task(subagent_type="general-purpose", prompt=<constructed prompt>)
ステップ 4:JSON レスポンスを解析します。suggestions が空の場合は、ステップ 6 に進みます。
提案は優先度の高い順に、1 つずつ適用します。
各提案について:
- コードを変更します(各ファイルの
old_codeをnew_codeに置き換える Edit ツールを使用します)。 - プロジェクトのリンター/フォーマッターのチェックを実行します(プロジェクト設定から検出します)。
- Python:
python -m black --check {files} && python -m flake8 {files} && python -m mypy {files} - TypeScript/JS:
npx eslint {files}またはnpx tsc --noEmit - Go:
go vet ./... - Rust:
cargo clippy - lint に失敗した場合は、直ちに変更を元に戻し、この提案をスキップします(テスト失敗時と同様です)。
- Python:
- テストスイート全体を実行します。
- テストが 1 つでも失敗した場合は、直ちに変更を元に戻し(編集前のファイルを読み直して Write で書き戻します)、この提案をスキップします。
- すべてのテストと lint に合格した場合は、変更を維持します。
ステップ 5(対話モードのみ。--auto ではスキップ):次を提示します。
REFACTOR: Code improved, all tests still passing.
Applied: {list of accepted suggestions}
Skipped: {list of reverted suggestions, if any}
All tests: {count} passing
[Moving to slice N of M] or [All slices complete]
--auto モードでは、次の 1 行を出力します。
[auto] REFACTOR slice N/M: {applied_count} applied, {skipped_count} skipped
状態を更新します:"phase": "refactor"。状態を直ちに書き込みます。
フェーズ 5:次のスライスまたは完了
残りのスライスがある場合は、状態の current_slice を増やして、フェーズ 2 に戻ります。
すべてのスライスが完了した場合は、概要を提示します。
TDD Complete: {feature name}
Slices implemented: N
Tests written: N
Files created/modified: {list}
All tests passing: yes
クリーンアップとして、.tdd-state.json を削除します(--auto モードでは通知せずに削除し、対話モードではユーザーに確認します)。
再開のサポート
ユーザーが /tdd --resume を実行した場合:
- プロジェクトのルートから
.tdd-state.jsonを読み込みます。 - 現在の状態を報告します:「'{feature}' の TDD セッションが見つかりました。現在、{N}/{total} スライス目の {phase} フェーズです。」
- 現在のスライスの現在のフェーズから再開します。
- 状態の
auto_modeが true の場合は、自動モードを継続します。
エッジケース
グリーンフィールドプロジェクト
ソースファイル、テスト、テスト設定がない場合は、適切に対処します。
- フェーズ 0 ステップ 3:run_tests.sh が
status: "error"とtotal: 0を返した場合、テストファイルがあるか確認します。なければグリーンフィールドプロジェクトとして続行します。 - フェーズ 0 ステップ 4:extract_api.sh は空の出力を返します。
"(No existing API — this is a new project)"を Test Writer に渡します。 - フェーズ 2:Test Writer はテストファイルをゼロから作成します。テストフレームワークの設定(例:
jest.config.js、pytest.ini)が必要になる場合があります。最初のテスト実行がテスト失敗ではなくフレームワークエラーで失敗した場合は、最小限のフレームワーク設定を作成して再試行します。
バグ修正の TDD
- バグを示すテストを作成します(バグの存在を示して失敗するはずです)。
- 失敗が報告されたバグと一致することを確認します — 人間によるチェックポイントです。
- 修正します:テストを通すための最小限のコードを記述します(通常どおり GREEN フェーズ)。
- 検証します:リグレッションがないことを確認します。
既存コード(特性テスト)
- 現在の動作に対するテストを作成します(特性テストとして成功するはずです)。
- 望ましい動作に合わせてテストを変更します(失敗するはずです)。
- GREEN -> REFACTOR に進みます。
ユーザーが提供したテスト
ユーザーがテストコードを提供した場合:
- 実行して失敗することを確認します(RED を確認)。
- フェーズ 3(GREEN)に進みます — ユーザーが提供したテストを正とします。
- 確認せずに変更しないでください。
不安定なテスト
テストが成功したり失敗したりする場合は、停止して報告し、続行する前に不安定なテストを修正します。
失敗時の復旧リファレンス
| 失敗 | フェーズ | 復旧 |
|---|---|---|
| Test Writer が無効な JSON を返す | RED | フェンスの除去と部分文字列の抽出で解析します。「Return ONLY JSON.」を付けて一度再試行します。最後に手動で抽出します。 |
| 失敗するはずのテストが成功する | RED | 「すでに実装済み」と記録し、そのスライスをスキップして次に進みます。 |
| テストに構文/コンパイルエラーがある | RED | raw_tail を読み、テストファイルを直接修正します。最大 2 回再試行します。その後、ユーザーに確認します。 |
| Implementer が無効な JSON を返す | GREEN | Test Writer と同じ JSON 復旧を行います。 |
| 実装後もテストが失敗する | GREEN | 再試行ループ:前回の試行内容を含めて Implementer を最大 5 回、新たに呼び出します。その後、ユーザーに確認します。 |
| 全テストスイートでリグレッションが発生する | GREEN | 自動修正:リグレッション失敗を含めて Implementer を新たに呼び出します。最大 3 回試行します。その後、ユーザーに確認します。 |
| Refactorer の提案でテストが失敗する | REFACTOR | 直ちに元に戻し、その提案をスキップして次に進みます。 |
| run_tests.sh がタイムアウトする | 任意 | タイムアウトを延長します。続く場合は、テストのパフォーマンスについてユーザーに確認します。 |
run_tests.sh が "error" を返す | 任意 | 原因を調べるため raw_tail を読みます。スクリプトエラー(バイナリがない、パスが不正など)の場合は修正して再試行します。コンパイルエラーは実装エラーとして扱います。 |
| extract_api.sh が空を返す | RED | グリーンフィールドプロジェクトでは通常の動作です。「(No existing API)」のメッセージを渡します。 |
| エージェントの応答が完全に空 | 任意 | Task 呼び出しを一度再試行します。それでも空の場合は、ユーザーに確認します。 |
レイヤーリファレンス
レイヤーの定義、依存関係のルール、レイヤー別のテスト戦略、検出ヒューリスティクスについては references/layer_guide.md を参照してください。
避けるべきアンチパターン
references/anti_patterns.md を参照してください。特に重要なもの:
- テストを通すためにテストを変更しないでください(テストではなく実装を変更します)
- テストより先に実装を書かないでください
- すべてのテストを一度に書かないでください(垂直スライスで進めます)
- 実装の詳細をテストしないでください
- RED フェーズをスキップしないでください
- ドメインコードからインフラストラクチャをインポートしないでください(依存関係の方向違反)
- ドメインオブジェクトをモックしないでください — 実際のインスタンスを構築します
フレームワークのクイックリファレンス
セットアップの詳細については、references/framework_configs.md をご覧ください。
| フレームワーク | 単一テストの実行 | すべて実行 | 監視モード |
|---|---|---|---|
| Jest | npx jest --testPathPattern=<file> -t "<name>" | npx jest | npx jest --watch |
| Vitest | npx vitest run <file> -t "<name>" | npx vitest run | npx vitest |
| pytest | pytest <file>::<test_name> -v | pytest -v | pytest-watch |
| Go | go test -run <TestName> ./... | go test ./... | — |
| Cargo | cargo test <test_name> | cargo test | cargo watch -x test |
| RSpec | rspec <file>:<line> | rspec | guard |
| PHPUnit | phpunit --filter <test_name> | phpunit | — |