VS Code + Cline で ”Devin 風”開発を行う方法② - 実践編

前回は、VS Code と Cline を使って、いわゆる「Devin 風」の開発環境を構築しました。しかし、実際の開発現場では「新しく作る」よりも「既存のコードを改善する」場面の方が圧倒的に多いと思われます。

そこで今回は、以前の記事でクリーンアーキテクチャを使用してリファクタリングを行った PHP アプリを題材に、残りの部分を AI エージェントと一緒に仕上げていきます。

単なるコード修正ではなく

  • タスクを分解して段階的に改善する方法
  • AI にどこまで任せられるのか
  • 人間が介入すべきポイント

といった実務的な観点で、「Devin 風開発」をどこまで再現できるのかを検証していきます。

結論から先に言うと、AI エージェントは万能ではないが、タスクを分解して使えば実用レベルで十分使えると思いました。

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

  • VS Code (Windows 版)・・・バージョン 1.116.0
  • MAMP (Windows 版)・・・バージョン 5.0.6
  • Cline・・・バージョン 2.13.0
  • Kanban・・・バージョン 0.1.53 (今回は使用していません)

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

ベースになるアプリケーションを準備します

前回のリファクタリングでは WSL2 を使用しましたが、今回は MAMP 環境上で進めていきます。実際に試されたい方はこのセクションの内容を参考にしてセットアップしてください。今回のリファクタリングでベースになるアプリは こちら からダウンロードできます。

詳しくはこちら
・必要なツールのインストール

まず初めに、こちらの記事を参考に MAMP のセットアップを行います。その後、Windows のターミナルを開いて以下のコマンドを入力して、コマンドラインから PHP と MySQL を実行できるようにします。

[Environment]::SetEnvironmentVariable("PATH",$env:PATH + ";C:\MAMP\bin\php\php8.2.14","User")
[Environment]::SetEnvironmentVariable("PATH",$env:PATH + ";C:\MAMP\bin\mysql\bin","User")

PHP の設定ファイルもコピーして、PHP のビルトインサーバーでも Xdebug でデバッグできるようにしておきます。

copy C:\MAMP\conf\php8.2.14\php.ini C:\MAMP\bin\php\php8.2.14

追加で以下のツールをインストールします。すでにインストールされている場合はスキップしてください。

  • Composer
  • Node.js (npm)
  • git

Composerこちら のリンクを参考にしてインストールして下さい。記事作成時とインストール画面が多少異なりますが、すべてデフォルトでインストールします。(Xdebug は前のステップでインストール済みなのでスキップします。)

Node.js (npm)こちら のリンクからインストーラーがダウンロードきます。

Gitこちら のリンクからダウンロードできます。ご使用の PC に入っていない場合はインストールして下さい。

・ベースアプリを展開します

ダウンロードしたベースアプリを任意のフォルダに展開します。ベースアプリのフォルダ構成は以下のようになっています。今回リファクタリングを行うベースアプリはすべて「app/」フォルダ内に入っています。それ以外は VS Code でのデバッグに必要な設定ファイルや Cline の制御用フォルダなどになっています。まずはVS Code を起動して「YelpCampCleanArch」フォルダを開いておきます。

my-workspace/
└─YelpCampCleanArch/
    ├─ .vscode/
    ├─ .clinerules/
    ├─ app/
    ├─ docs/
    ├─ kanban/
    ├─ memory-bank/
    └─ README.md
・オプションのツールや API キーをインストールします

必須ではありませんが、ベースアプリでサムネイル画像や地図を表示したい場合は以下のツールや API キーをインストールします。

  • Imagick
  • MapBox API キー

Imagick をインストールするには、ターミナル画面で以下のコマンドを実行します。

winget install ImageMagick.Q16

MapBox API キーの取得方法は こちら のサイトをご参照ください。取得した API キーは「YelpCampCleanArch\app\.env」というファイルを開いて、「MAPBOX_TOKEN」キーの値として貼り付けます。

MAPBOX_TOKEN=pk.eyJ1I...
・データベースを作成します

VS Code で Ctrl + @ と入力してターミナル画面を表示します。表示されたターミナル画面の「V」マークボタンをクリックします。

表示されたメニューから「Command Prompt」を選択します。

表示されたコマンドプロンプトに、以下のように入力してデータベースとユーザーを作成します。

cd app
mysql -uroot -proot < create-database.sql
mysql -uroot -proot < create-user.sql 

ベースアプリの実行に必要なパッケージをインストールします。

cd app
composer install

以上でセットアップは完了です。VS Code のターミナル画面で「app」フォルダに移動して、PHP のビルトインサーバーを起動します。

cd app
php -S localhost:8080

ブラウザーを起動して URL 欄に「localhost:8080」と入力してエンターキーを押すとベースアプリが実行されます。「localhost:8080/seeds」にアクセスすると、50 件の架空のキャンプ場とユーザー名「test」、パスワード「test」というユーザーが登録されます。

動作環境について

本記事では LLM の実行環境として、「Ollama」を使用したローカル環境で実行しています。クラウドモデル (Claude や GPT など) でも同様に進めることが可能です。クラウドモデルの方が高性能で快適に実行できますが、最低限 $5 程度の課金 (クレジット) が必要なケースが多いようです。

今回はコストを抑えるためにローカル LLM を使用していますが、精度を重視する場合はクラウドモデルの方が安定します。

Ollama のインストーラーは こちら からダウンロードできます。デフォルト設定でインストールしてください。起動前に Windows のターミナル画面などで以下のコマンドを実行しておくと、GPU の専用メモリ (VRAM) が効率的に利用できます。

[Environment]::SetEnvironmentVariable("OLLAMA_MAX_LOADED_MODELS","1","User")
[Environment]::SetEnvironmentVariable("OLLAMA_KEEP_ALIVE","30m","User")

Cline の API 設定画面の API Provider のドロップダウンから「Ollama」を選択すると接続されます。

Cline の設定方法
・Cline のカスタマイズ方法

公式ドキュメントによると Cline の動作をカスタマイズするには、以下のような方法があるようです。

機能概要用途
ルールCline の動作を定義する・コーディング規約の定義
・プロジェクトの制約
・チームの慣習 など
スキル専門知識をオンデマンドで提供・専門知識
・複雑な手続き
・組織的な専門知識 など
ワークフロー段階的なタスクの自動化・反復的なプロセス
・リリース手順
・セットアップスクリプト など
フック特定イベントでのカスタムロジックの挿入・検証
・強制
・監視
・自動化トリガー など
.clineignoreファイルアクセスの制御・依存関係
・ビルド成果物
・大きなデータファイル
などを除外。LLM 版の .gitignore のようなファイル

これらの機能を記述するファイルはそれぞれ以下の場所に格納されるようです。グローバルロケーションはすべてのプロジェクトで有効で、プロジェクトロケーションはローカルフォルダー内に配置して、そのプロジェクトでのみ有効になります。

機能グローバルロケーションプロジェクトロケーション
ルール~/Cline/Rules/.clinerules/
スキル~/.agents/skills/.agents/skills/
ワークフロー~/Cline/Workflows/.clinerules/workflows/
フック~/Documents/Cline/Hooks/.clinerules/hooks/
.clineignore該当なし.clineignore

※ Windows のグローバルロケーションは「~」の部分が「C:\Users\<ユーザー名>\」となります。

・ルールの追加方法

Cline を使用すると簡単に設定ファイルを追加できます。(VS Code のエクスプローラーなどで規定の場所に直接ファイルを作成しても認識されます。)

  1. モデル表示の左側にある「Manage Cline Rules & Workflows」ボタンを押します。
  2. 「Rules」タブを選択します。
  3. 入力欄にルールファイル名を入力します。有効な拡張子は「md」または「txt」になっています。(省略も可能)
  4. リターンキーを押すか、「+」ボタンを押すとファイルが作成されます。

ワークフローやスキルなどもこの設定内のタブを切り替えることで作成可能です。また、設定ファイル名の右側にあるトグルコントロールでそれぞれの設定を個別に有効・無効化することも可能です。

memory-bank ファイルを作成します

こちらの公式ドキュメントで解説されている、メモリーバンクファイルを作成します。

LLM などの推論モデルは「コンテキスト」と呼ばれる作業用の記憶領域を持っていますが、コンテキストの容量を超えると、古い情報が要約・削除されるなどして前の情報を保持できなくなります。メモリーバンクは、この制限を補うために用意されている、AI が参照できる外部的な記憶の仕組みであり、プロジェクトに関する情報を継続的に保持することができます。今回は、Cline で推奨されている以下のファイル群を作成していきます。

YelpCampCleanArch/
└─ memory-bank/
   ├─ projectbrief.md
   ├─ productContext.md
   ├─ systemPatterns.md
   ├─ techContext.md
   ├─ activeContext.md
   └─ progress.md

各ファイルの依存関係は以下のようになっています。

出展:docs.cline.bot
・memory-bank.md ファイルを作成します

公式ドキュメントにある、各メモリーバンクファイルの仕様を説明している文章を日本語に翻訳したものを「.clinerules\memory-bank.md」ファイルとして保存します。

# Cline のメモリーバンク

私は Cline です。優秀なソフトウェアエンジニアですが、1つ特徴があります。それは「セッションが切り替わると記憶が完全にリセットされる」という点です。
これは制限ではなく、完璧なドキュメントを維持するための仕組みです。
リセット後、私はプロジェクトを理解し作業を継続するために、メモリーバンクに完全に依存します。すべてのタスクの開始時に、必ずすべてのメモリーバンクファイルを読む必要があります。これは必須であり、省略することはできません。

---

## メモリーバンクの構成

メモリーバンクは、memory-bank/配下にMarkdown形式で記述されたコアファイルと追加のコンテキストファイルで構成されます。これらのファイルは明確な階層構造を持ち、互いに補完し合います。

---

### ■ コアファイル(必須)

1. `projectbrief.md`
- すべてのファイルの基礎となるドキュメント
- 存在しない場合はプロジェクト開始時に作成する
- プロジェクトの基本要件と目標を定義する
- プロジェクト範囲の基準となる

---

2. `productContext.md`
- このプロジェクトの存在理由
- 解決する問題
- どのように動作するべきか
- ユーザー体験の目標

---

3. `activeContext.md`
- 現在の作業の焦点
- 最近の変更内容
- 次に行う作業
- 現在の意思決定や検討事項
- 重要なパターンや方針
- 学びやプロジェクトの知見

---

4. `systemPatterns.md`
- システムのアーキテクチャ
- 重要な技術的意思決定
- 使用している設計パターン
- コンポーネント間の関係
- 重要な実装経路

---

5. `techContext.md`
- 使用している技術
- 開発環境
- 技術的制約
- 依存関係
- ツールの使用パターン

---

6. `progress.md`
- 現在うまくいっていること
- まだ実装されていないこと
- 現在の状況
- 既知の問題
- 意思決定の変遷

---

### ■ 追加コンテキスト

必要に応じて memory-bank/ 配下に追加ファイルやフォルダを作成して構いません。

例:
- 複雑な機能のドキュメント
- 外部連携仕様
- API仕様書
- テスト戦略
- デプロイ手順

---

## ドキュメントの更新

メモリーバンクは以下のタイミングで更新します:

1. 新しいパターンを発見したとき
2. 大きな変更を実装した後
3. ユーザーから「update memory bank」と指示されたとき(必ず全ファイルを確認する)
4. 文脈の明確化が必要なとき

---

## 重要

記憶がリセットされるたびに、私は完全に初期状態からスタートします。
メモリーバンクは、過去の作業とつながる唯一の手段です。
そのため、正確かつ明確に維持されなければなりません。私の能力は、このメモリーバンクの質に完全に依存しています。
・language.md ファイルを作成します

デフォルト状態では、各マークダウンファイルが英語で作成されてしまうため、応答メッセージやマークダウンファイルが日本語で作成されるように修正します。「.clinerules\language.md」というファイルを作成して以下の内容を記述します。

# 言語ルール

## 出力言語
- すべての出力は必ず日本語で行うこと  
- 英語への切り替えは禁止する(明示的な指示がある場合を除く)  

## ファイル生成
- 生成するすべてのドキュメントは日本語で記述する  

## 補足
- コードおよび技術用語は英語のままで問題ない  

ファイルが作成できたら、Cline 画面を開いて「Plan」モードを選択します。プロンプトの入力欄に「init memory bank」と入力してエンターキーを押します。

処理が終了すると、「memory-bank」フォルダ内に 6 個のファイルが作成されます。英語で作成されてしまった場合は、「メモリーバンクファイルを日本語で書き換えてください。」などと入力して変換します。

Cline の設定ファイルを作成します

それでは実際に Cline の動作設定を行っていきます。

方針としては、逆説的になりますが「できるだけ何も考えさせない」ようにして、可能な限り AI エージェントの裁量を削り落とします。このような設定を行うことで、こちらの意図しない動作を抑制します。ある程度見通しが立ってきたら少しずつ裁量を持たせていくようにします。

・ルールファイルを作成します

ルールファイルには AI エージェントの行動制約 (やっていいこと/禁止されていること) などを記述します。

YelpCampCleanArch/
└─ .clinerules/
   └─ rules.md

Cline の設定画面を利用して「rules.md」というファイルを作成して以下の内容を書き込みます。

# プロジェクトルール

## 基本ルール
- 既存の挙動を変更しない
- 小さく安全な変更を優先する
- 無関係なファイルを変更しない
- 一度に大規模な変更を行わない

## 出力ルール
- すべての説明は日本語で行う
- 変更前に方針を説明する

## リファクタリング方針
- 明示的に許可されていない限り、既存の挙動を変更しない
- 全体を書き換えるのではなく、責務の分離・抽出を優先する
- ファイル名・クラス名は既存の命名規則に合わせる(必要な場合のみ変更)
- アーキテクチャが不明確な場合は、編集前に2〜3案を提示する

## Strangler Fig パターンに関する制約
- 既存の MVC アプリは Strangler Fig パターンで段階的に置き換える
- 既存実装を一括で書き換えない
- 旧実装は参照専用とし、明示的な指示がない限り変更しない

## 変更禁止領域
以下の既存 MVC 領域は、明示的な指示がない限り変更しないこと
- `app/controllers/`
- `app/db/`
- `app/models/`
- `app/routes/`
- `app/utils/`
- `app/views/`

## 実装先の制約
- 新しい実装は `app/src/` 配下に追加する
- Controller は `Adapter/Controllers/` に配置する
- UseCase は `Application/UseCases/` 配下に配置する
- Entity は `Domain/Entities/` に配置する
- Repository interface は `Domain/Repositories/` に配置する
- Repository の実装は `Infrastructure/Db/` 配下に配置する
- 既存 MVC 領域には新しいロジックを追加しない

## 補足
- 既存コードの責務は分析してよいが、変更は行わない
- 必要な接続ポイントがある場合は、変更前に必ず方針を説明する

## テストコマンド
- `cd app 2>$null; vendor/bin/pest`
- `cd app -ErrorAction SilentlyContinue; vendor/bin/pest`
・スキルファイルを作成します

スキルファイルは専門的な知識や具体的な手続きなどを記述します。明示的な呼び出しはできないようです。必要に応じて AI エージェントが自動的に呼び出します。

YelpCampCleanArch/
└─ .agents/
   └─ skills/
      ├─ analyze-project/
      │  └─ SKILL.md
      └─ refactor-usecase/
         └─ SKILL.md

Cline の設定画面を利用して「analyze-project」というスキルを作成します。スキルの場合はルールのようなファイル名ではなくて「analyze-project」というフォルダ名を入力して作成します。SKILL.md ファイルは自動で作成されます。SKILL.md ファイルに以下の内容を書き込みます。このスキルはプロジェクト解析を行うスキルになっています。Cline での管理に必要なので、ファイルの先頭部分にある「name:」宣言などをしているセクションは削除しないでください。

---
name: analyze-project
description: プロジェクトの構造解析を行います
---

# スキル:プロジェクト構造の分析

## 目的
リファクタリングを行う前に、現在のプロジェクト構造を正しく把握する。

## 実行内容
このスキルを使用する場合、以下を実施すること。

1. ディレクトリ構成および主要なエントリーポイントを確認する  
2. 以下の要素を特定する  
   - コントローラ(controllers)  
   - サービス / ユースケース(services / use cases)  
   - インフラ層 / データベースアクセス(infrastructure / database)  
   - ビュー / テンプレート(views / templates)  
   - 設定ファイル(configuration)  
3. 現在のアーキテクチャを簡潔に要約する  
4. 責務分離の観点で問題がありそうな箇所を指摘する  
5. 安全に実施可能なリファクタリング候補を最大3つ提案する(小さい順)  
6. 明示的な指示がない限り、ファイルの変更は行わない  

## 出力形式
以下の形式で出力すること。

- 現在の構造の要約  
- 想定されるアーキテクチャ上の問題点  
- リファクタリング候補(最大3つ)

さらにもう一つ「refactor-usecase」という、ユースケース単位のリファクタリングスキルを追加します。

---
name: refactor-usecase
description: ユースケースへのリファクタリングを行います
---

# スキル:単一ユースケースのリファクタリング

## 目的
大規模な書き換えを行わず、1つのユースケースを安全に改善する。

## 実行内容
このスキルを使用する場合、以下を実施すること。

1. 関連するファイルを事前に確認する  
2. 改善対象となる「単一のユースケース」または責務を特定する  
3. 以下を説明する  
   - 現在の実装内容  
   - 改善したいポイント  
   - 変更対象となる最小限のファイル  
4. 対象のユースケースのみに限定してリファクタリングを行う  
5. 明示的な指示がない限り、既存の挙動は維持する  
6. コードの結合度が高すぎる場合は作業を中断し、より小さな単位での分離案(抽出)を提案する  
7. 実装後、以下をまとめる  
   - 何を変更したか  
   - 依然として結合が残っている箇所  
   - 次に行うべき改善案  

## 推奨リファクタリングパターン
- メソッドの抽出(extract method)  
- サービス / ユースケースの抽出(extract service / use case)  
- データベースアクセスの分離(isolate database access)  
- 可能な範囲で、コントローラからバリデーションロジックを分離  

## 禁止・回避事項
- プロジェクト全体に及ぶ書き換え  
- 多数のファイルを一度にリネームすること  
- 必要性が明確でないルーティングやブートストラップの変更  
リファクタリングをはじめます

環境が整ったので、リファクタリングを始めていきます。まずは手始めに、ログイン機能から実装します。動作確認も兼ねて小さく刻みたいので、実装範囲はアプリケーション層のユースケースまでの実装とします。セッションの確立などは行いません。以下の手順でリファクタリングします。

  1. 作業ブランチの作成
  2. 作業手順 (activeContext.md ファイル) の作成
  3. リファクタリング実行
  4. 作業手順の確認と修正
・作業ブランチの作成

VS Code のターミナル画面で「git init」と入力して Git リポジトリを初期化してから、初回のコミットまで済ませておきます。

その後、Cline の Act モードで「refactor/login というブランチを作成してください。」と入力して作業用のブランチを作成します。

・作業手順 (activeContext.md ファイル) の作成

メモリーバンクフォルダ内の作業手順 (activeContext.md) ファイルに以下の内容を記述します。

## 目的
既存の MVC コントローラ(users.php の login 処理)から、
クリーンアーキテクチャの UseCase を切り出すための設計ドキュメントを作成する。

## ソース
- `app\controllers\users.php`
- `login` 関数

## ユースケース
- ベースパス: `app\src\application\UseCases\User\`
- 入力ファイル:`UserLoginInput.php`
- 出力ファイル:`UserLoginOutput.php`
- ユースケースインターフェイス: `UserLoginUseCaseInterface.php`
- ユースケース:`UserLoginUseCase.php`

## 参照ファイル
- ベースパス: `app\src\application\UseCases\User\`
- 入力ファイル:`UserRegisterInput.php`
- 出力ファイル:`UserRegisterOutput.php`
- ユースケース:`UserRegisterUseCase.php`
- ユースケースインターフェイス: `UserRegisterUseCaseInterface.php`
- 例外ファイル:`app\src\application\Exception\ApplicationException.php`
- ユーザー情報ファイル:`app\src\domain\Entities\User.php`

## 名前空間
- `UserRepositoryInterface`: `Domain\Repositories\UserRepositoryInterface`
- `ApplicationException`: `Application\Exception\ApplicationException`

## コーディング規約
- `final class` を使用する
- コンストラクタプロモーション形式で `private` プロパティを定義する
- プロパティは公開しない
- setter は作成しない
- 値の参照は getter メソッドで行う

## 生成予定ファイル
- app\src\application\UseCases\User\UserLoginInput.php
- app\src\application\UseCases\User\UserLoginOutput.php
- app\src\application\UseCases\User\UserLoginUseCaseInterface.php
- app\src\application\UseCases\User\UserLoginUseCase.php
- それ以外の一切のファイルは変更や生成は不可
・リファクタリング実行

ここまでの仕様でどこまで実装できるのか確認します。Cline の Act モードに移行してプロンプト入力欄に「activeContext.md ファイルの内容を実装してください。」などと入力すると実装処理が開始されます。処理が終了すると想定通り 4 つのファイルが作成されているようです。

内容を見てみると、こちらは入力用の DTO ですが、何もパラメータを指定しなかったのにユーザー名 ($username) とパスワード ($password) が定義されているのはさすがです。

<?php

namespace Application\UseCases\User;

class UserLoginInput
{
    private string $username;
    private string $password;

    public function __construct(string $username, string $password)
    {
        $this->username = $username;
        $this->password = $password;
    }

    public function getUsername(): string
    {
        return $this->username;
    }

    public function getPassword(): string
    {
        return $this->password;
    }
}

しかしよく見ると、「コンストラクタプロモーション」形式で実装を指示したのに、以下のような形式にはなっていないようです。

<?php

declare(strict_types=1);

namespace Application\UseCases\User;

/**
 * ユーザー登録ユースケースの入力データ
 * (Controller や CLI などの外側の層から渡される)
 */
final class UserRegisterInput
{
    public function __construct(
        private string $username,
        private string $email,
        private string $plainPassword
    ) {}

    public function username(): string
    {
        return $this->username;
    }

    public function email(): string
    {
        return $this->email;
    }

    public function plainPassword(): string
    {
        return $this->plainPassword;
    }
}

ユースケースファイルもプレースホルダが定義されていて、実際の処理は実装されていないようです。

<?php

namespace Application\UseCases\User;

class UserLoginUseCase implements UserLoginUseCaseInterface
{
    public function execute(UserLoginInput $input): UserLoginOutput
    {
        // Placeholder implementation - replace with actual login logic
        $token = bin2hex(random_bytes(16)); // Generate a random token for now
        return new UserLoginOutput($token);
    }
}
・作業手順の確認と修正

そこで Cline の Plan モードに戻り、プロンプトで「不明な個所はありますか?」などと入力すると以下のような応答が得られます。

不明な点はいくつかあります。

1.  `app\controllers\users.php` の `login` 関数はどのような処理を行っていますか? 具体的な処理内容を教えていただけますでしょうか。
2.  `UserLoginInput.php` と `UserLoginOutput.php` の具体的なフィールド構成はどのようなものですか?
3.  `Domain\Repositories\UserRepositoryInterface` で定義されているメソッドの内容を教えていただけますでしょうか。

不明点をいくつか答えて、最終的に以下のような作業手順が作成できました。

以下のソースからクリーンアーキテクチャのユースケースを作成するプランを日本語で考えてください。

## ソース
- `app\controllers\users.php`
- `login` 関数

## ユースケース
- ベースパス: `app\src\application\UseCases\User\`
- 入力ファイル:`UserLoginInput.php`
  - `username`, `password` getter 関数, setter なし
- 出力ファイル:`UserLoginOutput.php`
  - `username` getter 関数, setter なし
- ユースケースインターフェイス: `UserLoginUseCaseInterface.php`
  - `public function execute(UserLoginInput $input): UserLoginOutput`
- ユースケース:`UserLoginUseCase.php`
  - `__construct(private UserRepositoryInterface $userRepository)`
  - `execute(UserLoginInput $input): UserLoginOutput`
    - `$user = $this->userRepository->findByUsername($input->username())`
    - `password_verify($input->password(), $user->passwordHash())`
    - 上記の処理がエラーの場合: `throw new ApplicationException` メッセージは日本語で

## 参照ファイル
- ベースパス: `app\src\application\UseCases\User\`
- 入力ファイル:`UserRegisterInput.php`
- 出力ファイル:`UserRegisterOutput.php`
- ユースケース:`UserRegisterUseCase.php`
- ユースケースインターフェイス: `UserRegisterUseCaseInterface.php`
- 例外ファイル:`app\src\application\Exception\ApplicationException.php`
- ユーザー情報ファイル:`app\src\domain\Entities\User.php`

## 名前空間
- `UserRepositoryInterface`: `Domain\Repositories\UserRepositoryInterface`
- `ApplicationException`: `Application\Exception\ApplicationException`

## コーディング規約
- `final class` を使用する
- コンストラクタプロモーション形式で `private` プロパティを定義する
  - 実装方法は次のファイルを参照: `app\src\application\UseCases\User\UserRegisterOutput.php`
- プロパティは公開しない
- setter は作成しない
- 値の参照は getter メソッドで行う

## コンストラクタプロモーション形式
```php
final class UserRegisterOutput
{
    public function __construct(
        private string $username
    ) {}

    public function username(): string
    {
        return $this->username;
    }
}
```

## 生成可能ファイル
- app\src\application\UseCases\User\UserLoginInput.php
- app\src\application\UseCases\User\UserLoginOutput.php
- app\src\application\UseCases\User\UserLoginUseCaseInterface.php
- app\src\application\UseCases\User\UserLoginUseCase.php
- それ以外の一切のファイルは変更や生成は不可

Cline の Act モードに移行して以下のプロンプトを入力します。

activeContext.md ファイルの内容を実装してください。

「gemma3:12b」というローカルモデルでは作成できませんでしたが、「qwen3.5:9b」というモデルを使用したら意図した形になりました。モデルによって得意なコーディング領域が微妙に異なるのかもしれません。

ユースケースも上手く実装されたようです。コメントも自動で追加してくれます。

<?php

declare(strict_types=1);

namespace Application\UseCases\User;

use Application\Exception\ApplicationException;
use db\UserQuery;
use Domain\Entities\User;
use Domain\Repositories\UserRepositoryInterface;

/**
 * ログインユースケースの実装
 * 
 * 既存の MVC アーキテクチャの影響を受けず、
 * クリーンアーキテクチャに従って実装されます。
 */
final class UserLoginUseCase implements UserLoginUseCaseInterface
{
    public function __construct(
        private readonly UserRepositoryInterface $userRepository
    ) {}

    /**
     * ログインを実行する
     * 
     * @param UserLoginInput $input ログイン入力データ
     * @return UserLoginOutput ログイン結果
     * @throws ApplicationException ユーザー名またはパスワードが間違っている場合
     */
    public function execute(UserLoginInput $input): UserLoginOutput
    {
        // ユーザー名で検索
        $user = $this->userRepository->findByUsername($input->username());

        if ($user === null) {
            // ユーザーが存在しない場合
            throw new ApplicationException('ユーザー名またはパスワードが間違っています。');
        }

        // パスワード認証
        if (!password_verify($input->password(), $user->passwordHash())) {
            throw new ApplicationException('ユーザー名またはパスワードが間違っています。');
        }

        // ログイン成功
        return new UserLoginOutput($user->username());
    }
}
テストを実装して実行します

Cline の Plan モードか Act モードにして以下のプロンプトを入力します。Plan モードのプロンプトで入力した場合は Act モードに切り替えると処理が実行されます。

UserLoginUseCase.php のテストケースを作成して検証してください。
app\tests\Pest\Unit\Application\UseCases\User\UserRegisterUseCaseTest.php ファイルを参照して
app\tests\Pest\Unit\Application\UseCases\User フォルダ以下に作成してください。
テストコマンドは `cd app -ErrorAction SilentlyContinue; vendor/bin/pest` です。

まず最初にテストコードを作成して、次にテストを実行します。最初は失敗しますが、Cline が対処方法を自動的に考えて最終的にはテストが成功します。

最初にテストが失敗した原因は、モックが返すパスワードがハッシュ化されていないためにログイン処理でパスワードの検証が失敗したようです。次にモックのパスワードを「password_hash」関数でハッシュしてからセットしています。特に何も指示していないのに、自動的に考えてテストコードを修正しました。

<?php

use Domain\Entities\User;
use Domain\Repositories\UserRepositoryInterface;
use Application\UseCases\User\UserLoginInput;
use Application\UseCases\User\UserLoginOutput;
use Application\UseCases\User\UserLoginUseCase;
use Application\Exception\ApplicationException;

it('logs in a user successfully', function () {

    // -------- Mock Repository --------
    $repo = $this->createMock(UserRepositoryInterface::class);

    // bcrypt ハッシュを作成(パスワード検証用)
    $hashedPassword = password_hash('secret123', PASSWORD_DEFAULT);

    // findByUsername → ユーザーを返す
    $repo->method('findByUsername')
        ->willReturn(
            new User(
                username: 'alice',
                email: 'alice@example.com',
                passwordHash: $hashedPassword
            )
        );

    // -------- UseCase --------
    $useCase = new UserLoginUseCase($repo);

    $input = new UserLoginInput(
        username: 'alice',
        password: 'secret123'
    );

    $output = $useCase->execute($input);

    // -------- Assertions --------
    expect($output)->toBeInstanceOf(UserLoginOutput::class);
    expect($output->username())->toBe('alice');
});

it('throws when user not found', function () {

    // -------- Mock Repository --------
    $repo = $this->createMock(UserRepositoryInterface::class);

    // findByUsername → null(ユーザーが存在しない)
    $repo->method('findByUsername')
        ->willReturn(null);

    $useCase = new UserLoginUseCase($repo);

    $input = new UserLoginInput(
        username: 'alice',
        password: 'secret123'
    );

    // -------- Exception Expectation --------
    $this->expectException(ApplicationException::class);

    $useCase->execute($input);
});

it('throws when password is invalid', function () {

    // -------- Mock Repository --------
    $repo = $this->createMock(UserRepositoryInterface::class);

    // bcrypt ハッシュを作成(正しいパスワード用)
    $hashedPassword = password_hash('secret123', PASSWORD_DEFAULT);

    // findByUsername → ユーザーを返すが、パスワードは異なる
    $repo->method('findByUsername')
        ->willReturn(
            new User(
                username: 'alice',
                email: 'alice@example.com',
                passwordHash: $hashedPassword
            )
        );

    $useCase = new UserLoginUseCase($repo);

    $input = new UserLoginInput(
        username: 'alice',
        password: 'wrong_password'
    );

    // -------- Exception Expectation --------
    $this->expectException(ApplicationException::class);

    $useCase->execute($input);
});

以上で最初のリファクタリングは終了です。すべて自動的とはいきませんでしたが、とりあえずコードの自動生成からテストまで回すことができました。

まとめ

今回の検証では、VS Code + Cline を使った “Devin 風開発” を、既存コードのリファクタリングという実務に近い形で試してみました。

その中で見えてきたのは、「AI に全部任せる」というよりも、今は「人間と AI で役割を分担する開発スタイル」が現実的であるという点です。

タスクを分解して段階的に改善する

今回のリファクタリングでは、いきなりログイン機能全体を作り直すのではなく、

  • 対象を「ログイン機能」に限定する
  • さらに「アプリケーション層の UseCase の実装まで」に絞る
  • セッション管理や画面表示は対象外とする

といった形で、非常に小さな単位に分割しました。

さらに、activeContext.md に「対象ファイル」「責務」「制約」を明示したことで、AI の作業範囲をコントロールすることができました。

このように、「どこまでやるか」を人間が先に決めることで、AI の出力は安定しやすくなります。

AI にどこまで任せられるのか

今回の結果として、AI に任せられる範囲はかなり広いと感じました。

特に以下の点は非常に強力です。

  • DTO / UseCase のコード生成
  • テストコードの自動生成
  • テスト失敗時の原因分析と修正

実際に、モックのパスワードがハッシュ化されていない問題を、AI が自動的に検出し、password_hash() を使う形に修正するところまで実行できました。

一方で、以下のような点はまだ不安定です。

  • コンストラクタプロモーションなどの細かいコーディング規約の遵守
  • 意図した設計の完全再現 (プレースホルダ実装になるケース)
  • モデルによる出力品質の差 (gemma と qwen で結果が異なる)

つまり AI は、「たたき台の生成」や「反復処理」は非常に得意ですが、「厳密な仕様の完全実装」は条件が揃わないと外すことがあります。

人間が介入すべきポイント

今回の作業で明確になったのは、人間が介入すべきポイントは「コードを書く部分」ではないということです。

重要なのは以下の 3 点です。

  • 作業範囲を定義する (activeContext.md の設計など)
  • 出力結果のズレを検出する (レビュー)
  • 不明点や不足情報を補う (再プロンプト)

例えば今回も、

  • コンストラクタプロモーションになっていない
  • UseCase がプレースホルダのまま

といった問題を人間が検出し、Plan モードで再整理することで改善できました。

実際に使ってみて感じた課題

今回の検証では、AI エージェントによる開発の可能性を感じる一方で、いくつかの実務的な課題も見えてきました。

  • memory-bank などのドキュメントやコード内のコメントが英語で生成されることがある
    プロンプトの一部が英語であった場合など、出力言語が揺れることがあります。
  • Plan モードでもファイルが生成されることがある
    プロンプトなどに「生成」や「実装」などが含まれていると、本来「設計・分析のみ」の Plan モードでもファイルが生成されることがあるようです。
  • 変更禁止ファイルも修正しようとする
    特に参照しているクラスや関数などが見つけられなかった場合など、自動的な補完機能によって、変更禁止ファイルであっても修正する場合があるようです。
  • 自動生成は得意だが、制限をかけるのが難しい
    今回の検証で一番感じたのは、AI は「作ること」は非常に得意だが、「作らせないこと」は難しいという点です。
  • プロンプトが長い
    直接コードを作成した方が早いのでは?と思うこともありますが、テンプレート化することで、2 回目以降からは恩恵があります。

上記のほとんどの課題は、強めに何度も指示することでかなり改善されるようです。

結論

AI エージェントは、「自動で全部作るツール」ではなく、

「小さく分割されたタスクを高速に前進させるツール」

として使うのが最も効果的です。

  • 人間:設計・分解・判断
  • AI:実装・試行・修正

この役割分担ができたとき、初めて「Devin 風開発」に近い体験が実現できると感じました。

完全自動にはまだ距離がありますが、適切に使えば、開発速度と試行回数を大きく引き上げることができるのは間違いありません。

次回は、Kanban 機能を活用した複数エージェントの連携などについて掘り下げていきます。

今回は以上になります。よろしかったらお試しください。