Claude Codeに「add user authentication」と入力する開発者は、毎回異なる結果を得る。JWTかもしれない。セッションCookieかもしれない。リフレッシュトークンとPKCE付きの完全なOAuth2フローかもしれない。エージェントはあなたが何を求めているか分からない。あなたが伝えたのは方向であって、目的地ではないからだ。
私が見てきた中で、Claude Codeから一貫して出荷可能なアウトプットを得ている開発者には共通の習慣がある。エージェントに作業を渡す前にスペックを書くのだ。小説ではない。3行のコンテキストが付いたJiraチケットでもない。誰もコードを書き始める前に「完了」がどういう状態かを定義する具体的なドキュメントだ。
これは新しい知見ではない。スペック・ファーストの開発はAIの何十年も前から存在する。しかしエージェントにおいては、スペックを省略するコストはより高く、書くことの恩恵はより大きい。人間の開発者は実装の途中で立ち止まって「待って、パスワード認証のこと?それともSSO?」と聞ける。エージェントは黙ってどちらかを選び、進み続ける。気づいた時には間違ったものが出来上がっていて、捨てるべきコードのレビューに20分を費やしている。
この記事では、私がClaude Codeで毎日使っているスペック駆動のライフサイクルを解説する。エージェントが実行できるスペックの書き方、誤解を早期に検出するplan-before-codeチェックポイント、そして「コンパイルが通った」より厳格な検証プロトコルについてだ。
「とりあえず作って」がエージェントで失敗する理由
失敗モードを具体的に見よう。Claude Codeに曖昧な指示を出すと、3つのことがうまくいかない。
サイレントな仮定。 エージェントはスペックのあらゆる隙間を自身の仮定で埋める。その仮定が妥当なこともある。そうでないこともある。アウトプットを読むまでどちらか分からない。曖昧な指示だと、スペックを書くのにかかったであろう以上の注意深さでアウトプットを読むことになる。
再現不可能な結果。 同じ曖昧なプロンプトを2回実行すると、2つの異なる実装が得られる。変数名やフォーマットだけでなく、アーキテクチャの決定が異なる。ライブラリが異なる。エラーハンドリング戦略が異なる。アウトプットを再現できなければ、その周りに信頼できるプロセスは構築できない。
レビューがボトルネックになる。 エージェントがすべての決定を下すと、すべての決定を検証しなければならない。各選択を理解している400行のdiffは5分でレビューできる。エージェントがデータベーススキーマ、APIの形、エラーコード、バリデーションロジックを選んだ400行のdiffは、実装からスペックを逆算するため30分かかる。
解決策はより良いプロンプトではない。重要な決定を、エージェントが実行できるドキュメントに前倒しすることだ。
スペック駆動のライフサイクル
ワークフローには5つのフェーズがある。それぞれに明確なエントリ条件とエグジット条件がある。
フェーズ1:ブレインストーム。 問題空間を探索する。制約は何か?どんなアプローチがあるか?以前何を試したか?ここでは一人で、またはClaude Codeの会話モードで思考を声に出す。エグジット条件:望ましいアプローチがあり、トレードオフを理解している。
フェーズ2:レビュー。 アプローチをストレステストする。何がうまくいかない可能性があるか?どんなエッジケースがあるか?コードベースの既存の何かと衝突しないか?複数のエージェントで作業している場合、ここでアーキテクチャエージェントやセカンドオピニオンが価値を持つ。エグジット条件:アプローチが堅実だと確信している。
フェーズ3:スペック。 決定したことを書き出す。問題の記述、提案するアプローチ、変更するファイル、機械的に検証可能な受け入れ基準、テストプラン。これが契約だ。エグジット条件:誰か(人間またはエージェント)がこのスペックを読んで、何を構築し、どう検証するか正確に分かる。
フェーズ4:実装。 エージェントがスペックに対して実行する。曖昧なアイデアに対してではない。テスト可能な基準を持つ具体的なドキュメントに対してだ。エグジット条件:エージェントが完了を宣言し、検証の証拠を投稿している。
フェーズ5:検証。 あなた(またはQAエージェント)が実装がスペックに一致することを確認する。「正しそうに見えるか」ではなく「各受け入れ基準を満たしているか」だ。エグジット条件:すべての基準がチェックされ、失敗したものはフェーズ4に戻る。
重要なインサイト:フェーズ1-3は安い。中規模の機能で10-20分だ。フェーズ4は実装にかかる時間分。フェーズ5は5-10分。フェーズ1-3を省略しても10-20分の節約にはならない。間違った方向に進んだ作業のレビュー、デバッグ、やり直しの時間がかかる。
良いエージェントスペックの姿
実際のスペックテンプレートを示す。ユーザーストーリーではない。プロダクト要件ドキュメントでもない。エージェントに正確に何を構築するか伝える作業ドキュメントだ。
## Problem
The filter bar resets when switching workspaces. Users lose their
filter state and have to re-apply filters every time they switch.
## Approach
Persist filter state per-workspace in localStorage. Key the stored
state by workspace database path so filters don't bleed across
workspaces.
## Files to Modify
- lib/local-storage.ts: Add getWorkspaceFilters / setWorkspaceFilters
- components/filter-bar.tsx: Read initial state from localStorage,
write on every change
- hooks/use-workspace.ts: Trigger filter restore on workspace switch
## Acceptance Criteria
1. Select workspace A, set filters to status=open + type=bug
2. Switch to workspace B. Filters reset to defaults.
3. Switch back to workspace A. Filters restore to status=open + type=bug.
4. Close the browser tab, reopen. Filters for the active workspace
are still applied.
5. bd list --status=open --type=bug output matches the filtered table.
## Out of Scope
- Server-side filter persistence
- Filter presets / saved filter combinations
- URL-based filter state (query params)
## Test Plan
- Unit test: getWorkspaceFilters returns stored value for matching
workspace path
- Unit test: setWorkspaceFilters writes correct key format
- Manual test: steps 1-5 from acceptance criteria above
このスペックが何を含み、何を含まないかに注目してほしい。localStorageの仕組みは説明しない。Claude Codeは知っている。URL paramsではなくlocalStorageを選んだ理由は説明しない。それはブレインストームフェーズで行われた。エージェントが触るべきすべてのファイルをリストしている。つまりエージェントがこのリスト外のファイルを変更し始めたら、それは警告サインだ。スコープ外セクションがあり、エージェントのゴールドプレーティングを防ぐ。
受け入れ基準が最も重要な部分だ。各項目は観察可能な結果を伴う具体的なアクションだ。「フィルターが永続化されるべき」ではない。それは曖昧だ。「ワークスペースAに切り替え、フィルターがstatus=open + type=bugであることを確認」はテスト可能だ。エージェントが実行できる。QAレビュアーが検証できる。
Plan-before-codeパターン
実装ミスの大半をコードになる前に検出するプロトコルがある。エージェントに何かを書く前に実装計画を投稿させるのだ。
実践では、タスクへの構造化されたコメントとして現れる。エージェントはファイルを開く前に、何をするつもりか正確に書き出す。
PLAN: Persist filter state per-workspace using localStorage.
1. Add workspace-scoped storage helpers to lib/local-storage.ts:
- getWorkspaceFilters(dbPath: string): FilterState | null
- setWorkspaceFilters(dbPath: string, filters: FilterState): void
- Key format: "beadbox:filters:{dbPath}"
2. Update components/filter-bar.tsx:
- On mount, read stored filters for current workspace
- On every filter change, write to localStorage
- Accept databasePath as prop (currently not passed)
3. Update hooks/use-workspace.ts:
- On workspace switch, emit event that filter-bar listens for
- Filter-bar reads new workspace's stored filters on event
4. Threading databasePath:
- filter-bar needs databasePath from the workspace context
- Will add it to the WorkspaceProvider context value
Files: lib/local-storage.ts, components/filter-bar.tsx,
hooks/use-workspace.ts, components/workspace-provider.tsx
Test: Unit tests for storage helpers, manual test of all 5
acceptance criteria.
エージェントがこれを生成するのに約30秒。あなたが読むのに約2分。その2分で、実装後に修正するのに20分かかる問題を検出できる:
- エージェントはスペック外のファイルを触っているか?(workspace-provider.tsxの追加はスペックになかった。OKなのかスコープクリープか?)
- アプローチは合理的か?(ワークスペース切り替えにイベントエミッターを使うのはオーバーエンジニアリングかもしれない。よりシンプルなprop変更で済むかもしれない。)
- 欠けているステップはないか?(ワークスペースが削除された時の古いlocalStorageエントリのクリーンアップは?)
プランはチェックポイントだ。正しく見えるならエージェントに続行させる。間違って見えるならプランを修正する。どちらにしても20分ではなく2分の投資だ。
