VS Code + Cline で ”Devin 風”開発を行う方法⑨ - ローカル優先・クラウド補完の自律開発ループを完成させる

前回は「LiteLLM」を使用して、ローカル LLM とクラウド LLM をモデル名で切り替える機能を追加しました。

今回はいよいよエスカレーション機能を実装して、エージェントループを完成させます。

※結論から先に申し上げると、前回追加した LiteLLM 側の補正や継続処理によって、当初用意していた 6 タスクはすべてローカル LLM だけで完了しました。

これはうれしい結果ではあるものの、そのままでは今回追加したクラウド LLM へのエスカレーション機能を確認できません。そこでまず、6 タスクが本当に正しく完了しているかを確認し、その後さらに難易度の高いタスクを追加して、必要に応じてクラウド LLM へ切り替わるかを検証しました。

この記事で使用している実行環境は以下のようになっています。

  • VS Code (Windows 版)・・・バージョン 1.136.2
  • Node.js (Windows 版)・・・バージョン 24.15.0
  • Cline CLI・・・バージョン 3.0.65
  • Kanban・・・バージョン 0.1.70
  • Python・・・バージョン 3.12.13
  • LangGraph・・・バージョン 1.2.10
  • LiteLLM・・・バージョン 1.98.0

※本記事でご紹介している内容は、確立された手法ではなく著者の独自の解釈に基づいています。その点をご理解のうえご参照ください。

本記事でご紹介したサンプルコードは記事末尾からダウンロードできます。

エスカレーション機能の追加とタスク検証について

すでに必要な基本機能はそろっているので、追加で以下の機能を実装しました。

  • ローカル LLM が規定回数 (デフォルトは 3 回) 失敗したらクラウド LLM に移行する
    これは LLM 名を配列で持ち、失敗回数によりインデックスを進める方式で実装しました。
  • 各タスクで初回の推論が停止した場合は、継続を促すプロンプトを追加する
    ローカル LLM は途中で停止する場合が多いので、2 回目以降のタスク作成時に基本のプロンプト (/worker-workflow などのワークフロー名) に継続を促す英文の 1 行メッセージを追加するように修正しました。

実際に動作させたところローカル LLM だけで、用意していた 6 個のタスクがすべて終了しました。これは今回の修正ではなく、前回追加した Tool Call 補正の効果によるところが大きいと思われます。

このままではエスカレーション機能の検証になっていませんが、折角なので本当に実装できているか確認しておきます。

テストに使用したタスク内容は以下のようになっています。

  • T0001: UserController::classic_login() へリネーム
    既存の登録後ログイン処理を、register() と login() で共有できる private メソッドへ整理します。
  • T0002: UserController::login() 実装
    Clean Architecture の UserLoginUseCase を使って login 処理を UserController に追加します。
  • T0003: Router へ login route 追加
    Clean Architecture 側の Router に POST login を追加します。
  • T0004: users.php の login POST を Router dispatch 化
    既存 MVC の login POST を Clean Architecture 側の Router へバイパスします。
  • T0005: 旧 controller login 使用箇所確認
    旧 MVC controller の login 呼び出しが残っていないか確認します。
  • T0006: Feature Test 作成
    login の Clean Architecture 移行後の HTTP レベルの動作をCodeception Feature Test で確認します。
T0001: UserController::classic_login() へリネーム

今回の一連のタスクは、クリーンアーキテクチャでログイン機能を実装することを目的としています。まずはすでに実装されている登録機能で使用されているヘルパー関数の名前と引数を変更して、これから追加するログイン関数で共通に使用できるように修正します。

修正する項目は以下のようになっています。

  • ヘルパー関数名を「login」から「classic_login」に変更
  • 関数の引数を「UserRegisterOutput」DTO から単なる文字列に変更
  • 引数の変更に伴い、関数内部での引数の参照を変更

意図したとおりに実装されているようです。

※差分の表示は「WinMerge」というアプリを使用しています。このアプリの設定方法につきましては こちら の記事をご参照ください。

T0002: UserController::login() 実装

すでに用意されている「UserLoginUseCase」関連ファイル (DTO、インターフェイス) を使用して、login 処理を UserController に追加します。

特に問題なく追加されたようです。

DTO (送受信用のデータ定義) のインポートやインジェクション (依存の注入) なども実装されているようです。

T0003: Router へ login route 追加

クリーンアーキテクチャ側のルーターに、ログイン処理のパスを追加します。「login」API に POST されるとログイン用のコントローラーを返すように実装されています。

T0004: users.php の login POST を Router dispatch 化

実装の方針として、動作しない状態を極力なくして少しずつ移植するいわゆる「Strangler Fig パターン」で実装を行います。そのため現在のルーティングの一部を変更してログイン処理をクリーンアーキテクチャのルーターにバイパスします。

すでに実装されている登録機能 (オレンジの下線) 部分と同じように実装されています。

T0005: 旧 controller login 使用箇所確認

このタスクは「T0004」が正常に行われたかをチェックしています。他のタスクとは違い、これ以上修正する箇所がない事を確認します。レビュー結果は以下のようになりました。

## T0005: 旧 controller login 使用箇所確認

### ReviewTargets (レビュー対象)

- app/routes/users.php
- 必要に応じて検索結果のみ確認
- app/controllers/users.php

### ReviewCheckList (確認内容)

- Verify that all completion conditions are satisfied.

### ReviewFindings (レビュー発見事項)

- ワーカは「コード変更不要タスク」として処理した。
- 実際の差分も memory-bank/kanban 配下のメタファイルのみで、PHPファイルの変更はない。
- plan.md の完了条件(routes/users.php から \controller\users\login() の直接呼び出しが消えている)の確認結果として、変更不要であったことを確認。

### ArchitectureReview (アーキテクチャレビュー)

- N/A(コード変更不要タスク)

### SafetyReview (安全性レビュー)

- 問題なし。

### CodeQualityReview (コード品質レビュー)

- N/A(コード変更不要)

### KnownIssues (既知の問題)

- 特になし。

### RequiredFixes (修正要求)

- なし。

### Notes (補足)

- このタスクは確認のみのもので、実際のコード変更は行われなかった。
- worker-result.md の報告と実際の差分が一致していることを確認した。

### ReviewerResult

REVIEW_APPROVED
T0006: Feature Test 作成

ここまででログイン機能は実装できたので、正常に実装できたかテストケースを作成してテストを行います。テストは実際の HTTP 通信を使用して行います。

Codeception 用のテストケースを作成して、次の項目をテストします。

  • 正常系の login 処理
  • パスワード不一致
  • ユーザー名不一致

このタスクで以下のテストケースが作成されました。正常時のユーザー名とパスワードおよび、パスワード不一致時のユーザー名は実在の値に手動で変更しました。

<?php

declare(strict_types=1);

namespace Tests\Acceptance;

use Tests\Support\AcceptanceTester;

final class LoginCest
{
    public function _before(AcceptanceTester $I): void {}

    /**
     * 正しい認証情報でのログイン成功テスト
     */
    public function testLoginSuccess(AcceptanceTester $I): void
    {
        $I->amOnPage('/login');
        $I->see('ログイン');

        $I->sendPost('/login', [
            'username' => 'test1',
            'password' => 'test1',
        ]);

        $I->seeInCurrentUrl('/campgrounds');
        $I->see('Yelp Campへようこそ!');
    }

    /**
     * 間違ったパスワードでのログイン失敗テスト
     */
    public function testLoginWrongPassword(AcceptanceTester $I): void
    {
        $I->amOnPage('/login');

        $I->sendPost('/login', [
            'username' => 'test1',
            'password' => 'wrongpassword',
        ]);

        $I->seeInCurrentUrl('/login');
        $I->see('パスワードが一致しません。');
    }

    /**
     * ユーザーが存在しない場合のログイン失敗テスト
     */
    public function testLoginUserNotFound(AcceptanceTester $I): void
    {
        $I->amOnPage('/login');

        $I->sendPost('/login', [
            'username' => 'nonexistentuser',
            'password' => 'password123',
        ]);

        $I->seeInCurrentUrl('/login');
        $I->see('ユーザーが見つかりません。');
    }
}

VS Code でターミナル画面を開いて以下のように入力して PHP のビルトインサーバーを起動しておきます。

cd app
php -S localhost:8080

別のターミナルを開いて、以下のように入力してテストを実行します。

cd app
vendor\bin\codecept run Acceptance tests\Codeception\Acceptance\LoginCest.php

正常にテストが終了したようです。

PS D:\MAMP\htdocs\cline\YelpCampCleanArch> cd app
PS D:\MAMP\htdocs\cline\YelpCampCleanArch\app> vendor\bin\codecept.bat run Acceptance tests\Codeception\Acceptance\LoginCest.php
Codeception PHP Testing Framework v5.3.2 https://stand-with-ukraine.pp.ua

Tests.Acceptance Tests (3) ------------------------------------------------------------------------------
+ LoginCest: Test login success (4.48s)
+ LoginCest: Test login wrong password (2.17s)
+ LoginCest: Test login user not found (2.11s)
---------------------------------------------------------------------------------------------------------
Time: 00:13.004, Memory: 6.00 MB

OK (3 tests, 7 assertions)
PS D:\MAMP\htdocs\cline\YelpCampCleanArch\app> vendor\bin\codecept.bat run Acceptance tests\Codeception\Acceptance\LoginCest.php
Codeception PHP Testing Framework v5.3.2 https://stand-with-ukraine.pp.ua

Tests.Acceptance Tests (3) ------------------------------------------------------------------------------
+ LoginCest: Test login success (2.79s)
+ LoginCest: Test login wrong password (2.21s)
+ LoginCest: Test login user not found (2.14s)
---------------------------------------------------------------------------------------------------------
Time: 00:07.452, Memory: 6.00 MB

OK (3 tests, 7 assertions)

デバッガーを起動して確認してみると、確かに追加された関数が実行されているようです。

本当はどのページにリダイレクトされたか、エラーの場合はどのコードが返るのかまで見てほしいですが、ローカルなのでとりあえずは良しとします。

ここまでの確認で、当初用意していた 6 タスクは単に Kanban 上で完了扱いになっていただけではなく、実際のコード変更や HTTP レベルのテストも含めて問題なく完了していることを確認できました。

しかし、この結果ではエスカレーション機能そのものの検証にはなりません。そこで次は、より複雑な判断や修正を必要とするタスクを追加して、ローカル LLM だけでは処理しきれない場合にクラウド LLM へ切り替わるかを確認します。

エスカレーション機能の検証
ログアウト機能の追加

以前のタスクは修正するファイルまで指定していたので、少し簡単だったかもしれません。少し難易度を上げるために、今度はログアウト機能を一度に実装します。どのファイルを修正するかはあえて指定しません。以下のようなタスクを追加しました。

## T0007: ログアウト機能の実装

Status: TODO

### Purpose (目的)
ログアウト機能を Clean Architecture 側へ実装してください。

既存アプリの logout 処理を調査し、
現在の挙動を維持したまま必要な変更を行ってください。

### TargetFiles (変更対象)

### ReferenceFiles (参照ファイル)

### ImplementationDetails (実装内容)
- 実装方法や変更対象ファイルは、既存コードを調査して判断してください
- 既存 MVC の logout 処理は参照して構いません
- 既存の login / register の動作を壊さないでください
- 不要な変更は行わないでください

### CompletionConditions (完了条件)
- logout が実行できる
- logout 後は未認証状態になる
- 既存アプリと同じ遷移先になる
- login / register の既存動作が維持される
- 関連テストが成功する

### TestDetails (テスト観点)

実際に実行してみるとこれもローカル LLM だけで完了しました。特に修正するファイルを指定しませんでしたが、「コントローラー」、「新/旧ルーター」が修正されています。

デバッグ実行も問題なさそうです。

Campgrounds 機能の追加

すでにログイン機能ではありませんが、簡単な機能の追加はローカル LLM でも対応できそうなので、今度は Campgrounds 全体の実装を行います。Campgrounds 機能は登録や編集などの他にレビューの登録や削除などもあり、認証・認可などの処理も結構複雑です。

これらの機能を一気に実装すると検証が大変なので、まずはこれらの機能をユースケースごとに個別のタスクに分割する以下のようなタスクを追加して実行してみます。

## T0008: Campgrounds 機能の Clean Architecture 移行タスク作成

Status: TODO

### Purpose (目的)

既存の Campgrounds 関連機能を調査し、
Clean Architecture 側へ段階的に移行するための実装タスクを作成してください。

Campgrounds には登録、表示、編集、削除、レビュー、認証・認可など
複数の機能が含まれているため、一度に実装せず、
安全に移行できる単位へ分割してください。

### TargetFiles (変更対象)

- memory-bank/kanban/tasks.md

### ReferenceFiles (参照ファイル)

- 既存の Campgrounds 関連コード
- 既存の Reviews 関連コード
- 既存の Router / Controller / Model / Query
- 認証・認可処理
- T0001 ~ T0007 の実装結果
- kanban/history 配下の過去タスク履歴

### ImplementationDetails (実装内容)

- 既存コードを調査して Campgrounds 関連機能の全体像を把握してください
- 現在の MVC 側の処理を機能単位に整理してください
- Clean Architecture へ移行するために必要な作業を複数のタスクへ分割してください
- 各タスクは可能な限り独立して実行・レビューできる大きさにしてください
- タスク間に依存関係がある場合は、実行順序が分かるようにしてください
- 既存機能を壊さず段階的に移行できる順序を優先してください
- 既存の T0001 ~ T0007 と同じフォーマットでタスクを作成してください
- 実装そのものは行わず、今回はタスク作成だけを行ってください

### CompletionConditions (完了条件)

- Campgrounds 関連機能が必要な単位に分割されている
- 登録・一覧・詳細・編集・削除が考慮されている
- Review の登録・削除など関連機能も考慮されている
- 認証・認可が必要な処理がタスクに反映されている
- Router / Controller / UseCase / Repository など必要な変更が考慮されている
- 既存 MVC からの段階的な切り替え方法が考慮されている
- 必要な HTTP レベルのテストが各タスクに含まれている
- タスク間の依存関係と実行順序が分かる
- memory-bank/kanban/tasks.md に新しいタスクが追加されている

### TestDetails (テスト観点)

- 今回は実装を行わないため、コード実行テストは不要
- 作成された各タスクについて、目的・変更対象・完了条件・テスト観点が明確か確認する
- 一つのタスクに責務が集中しすぎていないか確認する

さすがにクラウド LLM に移行するかと思われましたが、今度もまたローカル LLM で処理が完了しました。T0009 ~ T0017 の 9 個のタスクが追加されました。

生成されたタスクのタイトルは以下のようになっていました。

  • T0009: CampGround エンティティと Repository interface 作成
  • T0010: CampGround Repository 実装 (PDO)
  • T0011: Campground 登録機能の実装
  • T0012: Campground 一覧・詳細表示機能の実装
  • T0013: Campground 編集機能の実装
  • T0014: Campground 削除機能の実装
  • T0015: Review 登録・削除機能の実装
  • T0016: Campgrounds Router 追加と既存 routes の置き換え
  • T0017: Campgrounds Feature Test 作成

生成されたタスクの全文は以下のようになっています。

クリックすると展開します。tasks.md (T0009 ~ T0017)
## T0009: CampGround エンティティと Repository interface 作成

Status: DONE

### Purpose (目的)

Campgrounds ドメインのエンティティとリポジトリインターフェースを作成してください。

既存の User エンティティ(T0001〜T0007)の実装を参考にし、
Clean Architecture のパターンに従って実装してください。

### TargetFiles (変更対象)

- app/src/domain/Entities/CampGround.php
- app/src/domain/Repositories/CampGroundRepositoryInterface.php
- app/src/domain/Entities/Review.php
- app/src/domain/Repositories/ReviewRepositoryInterface.php

### ReferenceFiles (参照ファイル)

- app/src/domain/Entities/User.php
- app/src/domain/Repositories/UserRepositoryInterface.php
- app/models/campground.php
- app/models/review.php
- app/models/images.php

### ImplementationDetails (実装内容)

- CampGround エンティティを作成する
  - id, title, price, description, location, author, geometry を持つ
  - isOwner(): bool メソッドを実装する
  - getGeoData(): array メソッドを実装する(JSON データ生成)
  - static create(): self ファクトリメソッドを実装する
- CampGroundRepositoryInterface を作成する
  - save(CampGround): CampGround
  - findById(int): ?CampGround
  - findAll(): array
  - update(CampGround): ?CampGround
  - delete(int): bool
- Review エンティティを作成する
  - id, body, rating, camp_id, author を持つ
  - isOwner(): bool メソッドを実装する
  - static create(): self ファクトリメソッドを実装する
- ReviewRepositoryInterface を作成する
  - save(Review): Review
  - findById(int): ?Review
  - findByCampId(int): array
  - delete(int): bool
  - deleteByCampId(int): bool
- ImageSchema は既存の app/models/images.php を参照するが、
  Clean Architecture 側では別の実装を検討する(必要に応じて)

### CompletionConditions (完了条件)

- CampGround エンティティが存在する
- CampGroundRepositoryInterface が存在する
- Review エンティティが存在する
- ReviewRepositoryInterface が存在する
- 各メソッドのシグネチャが正しい
- syntax error がない

### TestDetails (テスト観点)

- 今回はエンティティとインターフェースのみなので、コード実行テストは不要
- メソッドシグネチャと戻り値の型が正しいか確認する

## T0010: CampGround Repository 実装(PDO)

Status: FAILED

### Purpose (目的)

CampGround リポジトリの PDO 実装を作成してください。

既存の PdoUserRepository(T0001〜T0007)の実装を参考にし、
CampGroundQuery の処理をリポジトリパターンへ移行します。

### TargetFiles (変更対象)

- app/src/infrastructure/Db/CampGround/PdoCampGroundRepository.php

### ReferenceFiles (参照ファイル)

- app/src/infrastructure/Db/User/PdoUserRepository.php
- app/db/campgrounds.query.php
- app/models/campground.php

### ImplementationDetails (実装内容)

- PdoCampGroundRepository を作成する
  - CampGroundRepositoryInterface を実装する
  - DataSource を使用して DB アクセスする
  - findById, findAll, save, update, delete を実装する
  - CampGroundSchema::cast() を使って結果をエンティティに変換する
- 画像の保存・削除もリポジトリ内で処理する
  - ImageQuery を内部で呼び出す
- トランザクション処理を実装する
  - save と update ではトランザクションを使用する

### CompletionConditions (完了条件)

- PdoCampGroundRepository が存在する
- CampGroundRepositoryInterface を実装している
- findById, findAll, save, update, delete が実装されている
- トランザクション処理が正しい
- syntax error がない

### TestDetails (テスト観点)

- リポジトリの単体テストは行わない(軽量確認を優先)
- php-lint で構文チェックを行う

## T0011: Campground 登録機能の実装

Status: TODO

### Purpose (目的)

Campground 登録(Create)機能を Clean Architecture 側へ実装してください。

既存の app/controllers/campgrounds.php の createCampground() を
Clean Architecture パターンへ移行します。

### TargetFiles (変更対象)

- app/src/application/UseCases/Campground/CampgroundRegisterInput.php
- app/src/application/UseCases/Campground/CampgroundRegisterOutput.php
- app/src/application/UseCases/Campground/CampgroundRegisterUseCase.php
- app/src/application/UseCases/Campground/CampgroundRegisterUseCaseInterface.php
- app/src/adapter/Controllers/Campground/CampgroundController.php
- app/src/infrastructure/Bootstrap/container.php

### ReferenceFiles (参照ファイル)

- app/controllers/campgrounds.php
- app/models/campground.php
- app/src/application/UseCases/User/UserRegisterUseCase.php
- app/src/adapter/Controllers/UserController.php

### ImplementationDetails (実装内容)

- CampgroundRegisterInput を作成する
  - title, location, price, description, campgroundImages を持つ
  - バリデーションロジックを含む
- CampgroundRegisterOutput を作成する
  - id(): int を持つ
- CampgroundRegisterUseCaseInterface を作成する
- CampgroundRegisterUseCase を実装する
  - CampGroundRepository を DI する
  - 入力バリデーション
  - 地理座標変換(forwardGeocode)
  - CampGround::create() でエンティティ生成
  - リポジトリへ保存
  - 画像処理(ImageSchema の追加)
  - 例外をスローまたは Output を返す
- CampgroundController を作成する
  - CampgroundRegisterUseCaseInterface を DI する
  - public register(): void メソッドを実装する
  - POST リクエストから入力データを取得
  - UseCase を実行
  - 成功時は flash message + redirect
  - 失敗時は flash message + redirect
- container.php に route と DI 設定を追加する

### CompletionConditions (完了条件)

- CampgroundRegisterUseCase が存在する
- CampgroundController に register() が存在する
- POST /campgrounds が Clean Architecture 側へ流れる
- 登録成功時に flash message と redirect が行われる
- 登録失敗時にエラーメッセージが表示される
- syntax error がない

### TestDetails (テスト観点)

- Codeception で登録機能の HTTP テストを作成する
  - 正常系: 登録成功
  - 異常系: 必須項目欠落
  - 異常系: プライスが数値以外

## T0012: Campground 一覧・詳細表示機能の実装

Status: TODO

### Purpose (目的)

Campground 一覧(Index)と詳細(Show)表示機能を Clean Architecture 側へ実装してください。

既存の view/campgrounds/index.php と view/campgrounds/show.php の
データ取得処理を Clean Architecture 側へ移行します。

### TargetFiles (変更対象)

- app/src/application/UseCases/Campground/CampgroundListUseCase.php
- app/src/application/UseCases/Campground/CampgroundListUseCaseInterface.php
- app/src/application/UseCases/Campground/CampgroundListOutput.php
- app/src/application/UseCases/Campground/CampgroundDetailUseCase.php
- app/src/application/UseCases/Campground/CampgroundDetailUseCaseInterface.php
- app/src/application/UseCases/Campground/CampgroundDetailOutput.php
- app/src/adapter/Controllers/Campground/CampgroundController.php
- app/src/infrastructure/Bootstrap/container.php

### ReferenceFiles (参照ファイル)

- app/views/campgrounds/index.php
- app/views/campgrounds/show.php
- app/db/campgrounds.query.php
- app/db/reviews.query.php

### ImplementationDetails (実装内容)

- CampgroundListUseCase を作成する
  - findAll() を実行
  - 各 campground に画像情報を付与
  - Output モデルに詰めて返す
- CampgroundDetailUseCase を作成する
  - findById() を実行
  - 関連レビューを取得
  - Output モデルに詰めて返す
- CampgroundController に list() と detail() メソッドを追加する
  - GET /campgrounds -> list()
  - GET /campgrounds/{id} -> detail()
- container.php に route と DI 設定を追加する

### CompletionConditions (完了条件)

- CampgroundListUseCase が存在する
- CampgroundDetailUseCase が存在する
- GET /campgrounds が一覧を表示する
- GET /campgrounds/{id} が詳細を表示する
- レビュー情報が詳細ページに表示される
- syntax error がない

### TestDetails (テスト観点)

- Codeception で一覧・詳細の HTTP テストを作成する
  - 一覧表示が正常に動作する
  - 詳細表示が正常に動作する
  - レビューが表示される

## T0013: Campground 編集機能の実装

Status: TODO

### Purpose (目的)

Campground 編集(Update)機能を Clean Architecture 側へ実装してください。

既存の app/controllers/campgrounds.php の updateCampground() を
Clean Architecture パターンへ移行します。

### TargetFiles (変更対象)

- app/src/application/UseCases/Campground/CampgroundUpdateInput.php
- app/src/application/UseCases/Campground/CampgroundUpdateOutput.php
- app/src/application/UseCases/Campground/CampgroundUpdateUseCase.php
- app/src/application/UseCases/Campground/CampgroundUpdateUseCaseInterface.php
- app/src/adapter/Controllers/Campground/CampgroundController.php
- app/src/infrastructure/Bootstrap/container.php

### ReferenceFiles (参照ファイル)

- app/controllers/campgrounds.php
- app/models/campground.php
- app/db/images.query.php

### ImplementationDetails (実装内容)

- CampgroundUpdateUseCase を作成する
  - findById() で既存データを取得
  - 存在しない場合は例外をスロー
  - 入力データをマージ
  - 画像の追加・削除処理
  - リポジトリへ保存
- CampgroundController に update() メソッドを追加する
  - PUT /campgrounds/{id} を処理
  - 所有者確認(認可)
  - UseCase を実行
  - 成功/失敗時の flash message + redirect
- container.php に route と DI 設定を追加する

### CompletionConditions (完了条件)

- CampgroundUpdateUseCase が存在する
- CampgroundController に update() が存在する
- PUT /campgrounds/{id} が Clean Architecture 側へ流れる
- 編集成功時に flash message と redirect が行われる
- 非所有者の場合は権限エラーが表示される
- syntax error がない

### TestDetails (テスト観点)

- Codeception で編集機能の HTTP テストを作成する
  - 正常系: 編集成功
  - 異常系: 非所有者
  - 異常系: 存在しない campground

## T0014: Campground 削除機能の実装

Status: TODO

### Purpose (目的)

Campground 削除(Delete)機能を Clean Architecture 側へ実装してください。

既存の app/controllers/campgrounds.php の deleteCampground() を
Clean Architecture パターンへ移行します。

### TargetFiles (変更対象)

- app/src/application/UseCases/Campground/CampgroundDeleteUseCase.php
- app/src/application/UseCases/Campground/CampgroundDeleteUseCaseInterface.php
- app/src/adapter/Controllers/Campground/CampgroundController.php
- app/src/infrastructure/Bootstrap/container.php

### ReferenceFiles (参照ファイル)

- app/controllers/campgrounds.php
- app/db/campgrounds.query.php

### ImplementationDetails (実装内容)

- CampgroundDeleteUseCase を作成する
  - findById() で既存データを取得
  - 存在しない場合は例外をスロー
  - 所有者確認(認可)
  - リポジトリへ削除依頼
  - 関連画像・レビューも削除されることを確認
- CampgroundController に delete() メソッドを追加する
  - DELETE /campgrounds/{id} を処理
  - 所有者確認(認可)
  - UseCase を実行
  - 成功時は flash message + redirect
  - 失敗時は flash message + redirect
- container.php に route と DI 設定を追加する

### CompletionConditions (完了条件)

- CampgroundDeleteUseCase が存在する
- CampgroundController に delete() が存在する
- DELETE /campgrounds/{id} が Clean Architecture 側へ流れる
- 削除成功時に flash message と redirect が行われる
- 非所有者の場合は権限エラーが表示される
- syntax error がない

### TestDetails (テスト観点)

- Codeception で削除機能の HTTP テストを作成する
  - 正常系: 削除成功
  - 異常系: 非所有者
  - 異常系: 存在しない campground

## T0015: Review 登録・削除機能の実装

Status: TODO

### Purpose (目的)

Review 登録(Create)と削除(Delete)機能を Clean Architecture 側へ実装してください。

既存の app/controllers/reviews.php の createReview() と deleteReview() を
Clean Architecture パターンへ移行します。

### TargetFiles (変更対象)

- app/src/application/UseCases/Campground/ReviewRegisterUseCase.php
- app/src/application/UseCases/Campground/ReviewRegisterUseCaseInterface.php
- app/src/application/UseCases/Campground/ReviewDeleteUseCase.php
- app/src/application/UseCases/Campground/ReviewDeleteUseCaseInterface.php
- app/src/adapter/Controllers/Campground/CampgroundController.php
- app/src/infrastructure/Bootstrap/container.php

### ReferenceFiles (参照ファイル)

- app/controllers/reviews.php
- app/models/review.php
- app/db/reviews.query.php

### ImplementationDetails (実装内容)

- ReviewRegisterUseCase を作成する
  - CampGroundRepository を DI(存在確認用)
  - ReviewRepository を DI
  - 入力バリデーション
  - Review::create() でエンティティ生成
  - リポジトリへ保存
- ReviewDeleteUseCase を作成する
  - findById() で既存データを取得
  - 存在しない場合は例外をスロー
  - 所有者確認(認可)
  - リポジトリへ削除依頼
- CampgroundController に registerReview() と deleteReview() メソッドを追加する
  - POST /campgrounds/{id}/reviews -> registerReview()
  - DELETE /campgrounds/{id}/reviews/{review_id} -> deleteReview()
- container.php に route と DI 設定を追加する

### CompletionConditions (完了条件)

- ReviewRegisterUseCase が存在する
- ReviewDeleteUseCase が存在する
- POST /campgrounds/{id}/reviews が Clean Architecture 側へ流れる
- DELETE /campgrounds/{id}/reviews/{review_id} が Clean Architecture 側へ流れる
- レビュー登録成功時に flash message と redirect が行われる
- レビュー削除成功時に flash message と redirect が行われる
- 非所有者の場合は権限エラーが表示される
- syntax error がない

### TestDetails (テスト観点)

- Codeception でレビュー機能の HTTP テストを作成する
  - 正常系: レビュー登録成功
  - 正常系: レビュー削除成功
  - 異常系: 非所有者
  - 異常系: 必須項目欠落

## T0016: Campgrounds Router 追加と既存 routes の置き換え

Status: TODO

### Purpose (目的)

Clean Architecture 側の Router に Campgrounds ルートを追加し、
既存 MVC の routes/campgrounds.php からバイパスするように変更してください。

### TargetFiles (変更対象)

- app/src/infrastructure/Bootstrap/container.php
- app/routes/campgrounds.php

### ReferenceFiles (参照ファイル)

- app/src/infrastructure/Http/Router.php
- app/routes/campgrounds.php
- app/controllers/campgrounds.php

### ImplementationDetails (実装内容)

- container.php に CampgroundController の DI 設定を追加する
- Router の routes 配列に Campgrounds ルートを追加する
  - GET /campgrounds
  - POST /campgrounds
  - GET /campgrounds/new
  - GET /campgrounds/{id}
  - PUT /campgrounds/{id}
  - DELETE /campgrounds/{id}
  - GET /campgrounds/{id}/edit
  - POST /campgrounds/{id}/reviews
  - DELETE /campgrounds/{id}/reviews/{review_id}
- routes/campgrounds.php を変更し、
  対応するルートは $router->dispatch() へバイパスする
- GET /campgrounds/new と GET /campgrounds/{id}/edit は
  既存の view 表示を維持する(または Clean Architecture 側で実装)

### CompletionConditions (完了条件)

- CampgroundController が Router 経由で dispatch される
- 全てのルートが正しく動作する
- 既存の campgrounds GET ルートが維持されている
- syntax error がない

### TestDetails (テスト観点)

- Codeception で各ルートの HTTP テストを作成する
  - 全てのルートが正しく dispatch される

## T0017: Campgrounds Feature Test 作成

Status: TODO

### Purpose (目的)

Campgrounds 機能の Clean Architecture 移行後の HTTP レベルの動作を
Codeception Feature Test で確認してください。

### TargetFiles (変更対象)

- app/tests/Codeception

### ReferenceFiles (参照ファイル)

- app/routes/campgrounds.php
- app/src/adapter/Controllers/Campground/CampgroundController.php
- app/src/application/UseCases/Campground/*.php

### ImplementationDetails (実装内容)

- Codeception を使用する
- 実際の HTTP リクエスト経由で campgrounds をテストする
- localhost:8080 で起動しているテスト用アプリケーションを使用する
- 以下のテストを作成する:
  - campground list success
  - campground detail success
  - campground create success
  - campground create validation error
  - campground update success
  - campground update unauthorized
  - campground delete success
  - campground delete unauthorized
  - review register success
  - review register validation error
  - review delete success
  - review delete unauthorized
- Laravel/Pest の HTTP helper は使用しない
- Controller / UseCase の Unit Test への置き換えは禁止

### CompletionConditions (完了条件)

- 全ての Feature Test が存在する
- Codeception コマンドで実行可能
- 全テストが成功する

### TestDetails (テスト観点)

- redirect
- flash message
- 認証・認可
- 画像アップロード
- レビュー表示

生成されたタスクを実行してみると 1 つ問題が発生しました。「T0009」タスクは問題なく終了しましたが、「T0010」タスクを実行すると推論がループしているようです。

タスクを監視するため設定ファイルにカウンター変数を追加して、in_progress 状態が 30 回 (最短で約 5 分) 連続したら強制的に finished に移行するように修正しました。(本来なら GPU の稼働状況なども考慮するべきかもしれません。)

また、個別タスクのループだけではなく、エージェントループがローカル LLM だけでは終わらない現象も発生しましたので、エージェントループが 3 回以上ループしたら強制的にクラウド LLM に移行するように修正しました。

さらに無制限な課金を抑えるために、クラウド LLM を使用している場合でも 3 回以上実行しても完了しない場合は、LangGraph ループ自体を終了するように修正しました。

以上の修正を行ってから実行してみると「T0016」タスクでクラウド LLM にエスカレーションすることが確認できました。

その後は再びローカル LLM に戻り、そのまま最後のタスクまで完了しました。

まとめ

今回の検証では、当初想定していた以上にローカル LLM が多くのタスクを処理できることが分かりました。最初に用意したログイン関連のタスクだけでなく、変更対象を細かく指定しなかったログアウト機能の実装や、Campgrounds 機能を Clean Architecture 化するためのタスク分割まで、ほとんどローカル LLM だけで処理できています。

一方で、処理がループしてしまうケースや、ローカル LLM だけでは完了できないタスクもありました。そこで一定回数以上処理が進まない場合にはクラウド LLM へ切り替える仕組みを追加したところ、実際にエスカレーションが発生し、その後は再びローカル LLM に戻して処理を継続できました。

この結果から、すべての処理を最初から高性能なクラウド LLM に任せなくても、普段はローカル LLM を使用し、難しい場面だけクラウド LLM に補助してもらう構成でも、ある程度実用的な開発ループを構築できそうです。

Devin のような完成されたサービスは非常に高性能ですが、内部でどのような判断や再試行、モデル切り替えが行われているかは利用者から見えにくい部分もあります。その点、自作したエージェントループでは、タスクの入力、実行結果、レビュー結果、再試行回数などを自分の好きな形で保存できます。今回のように history フォルダへ実行履歴を残しておけば、後から失敗原因を調査したり、過去の実装を次のタスクの参考情報として利用したりすることもできます。

また、今回試してみて感じたのは、「すべてを AI に任せればよい」というわけでもないという点です。完成したコードを人間が確認する必要があるのであれば、目的や完了条件、テスト観点などは、ある程度人間側で指定できた方が確認しやすくなります。あまりに抽象的な要求だけを与えて完全に自動生成してしまうと、最終的には他人が書いたコードを後から読み解くのとあまり変わらなくなってしまいます。

そのため、実際の開発では「人間が目的と制約を決め、AI が調査・実装・テストを進める」という役割分担が、今のところ扱いやすいように感じました。

今回は Campgrounds 機能について、比較的大きな要求から複数の実装タスクを生成するところまでは確認できました。ただし、Devin のようにさらにあいまいな要求から仕様を整理し、適切な粒度へ自動的に分解していくには、Planner に相当する機能をもう少し強化する必要がありそうです。

本シリーズでは、VS Code と Cline を出発点として、LangGraph、LiteLLM、ローカル LLM を組み合わせながら、コードの実装、レビュー、再試行、モデル切り替えまでを行う開発ループを少しずつ構築してきました。最初に想像していたよりも手間は掛かりましたが、自分で組み立ててみたことで、AI エージェントがどのような仕組みで動いているのかもかなり見えてきたように思います。

完全自動の開発環境というところまではまだ距離がありますが、少なくとも「AI がコードを書き、テストし、失敗したらやり直し、難しい場合は別のモデルに切り替える」という一連の流れは、自分の PC 上でもかなりの部分まで再現できました。

まだまだ不安定ですが、今回作成したスクリプトは こちら からダウンロードできます。よろしければご覧ください。

使用するモデルを変更するには「kanban\python\lang_graph\config.py」ファイルの「default_ollama_model」と「default_cloud_model」を変更してください。

def get_settings() -> Settings:
    _load_env()
    default_ollama_model = os.getenv("OLLAMA_MODEL", "ollama-qwen3.6:35b-a3b")
    default_cloud_model = os.getenv("CLOUD_MODEL", "chatgpt-gpt-5.5")

以上でこのシリーズは一旦終了です。ここまでお読みいただき、ありがとうございました。興味がありましたら、ぜひご自身の環境でも試してみてください。