Home

2026-01-25 fullstack command plan

/fullstack コマンドサポート追加 実装計画

概要

Ghostrunner API に /fullstack コマンドの実行機能を追加する。現在 /plan コマンドのみサポートしている設計を汎用化し、複数のカスタムコマンド(/plan, /fullstack, /go, /nextjs 等)を実行できるようにする。

現状分析

現在の実装

ハードコードされた /plan コマンド:

  • internal/service/claude.go:45: prompt := fmt.Sprintf("/plan %s", args)
  • ハンドラー名: PlanHandler(/plan 専用)
  • エンドポイント: /api/plan 系のみ

ファイル構成:

internal/
├── handler/
│   └── plan.go      # PlanHandler(/plan 専用)
└── service/
    ├── claude.go    # ClaudeService(/plan をハードコード)
    └── types.go     # 型定義

設計方針

選択: 汎用コマンドAPI

理由:

  1. /go, /nextjs など将来のコマンド追加が容易
  2. コードの重複を避けられる
  3. APIが一貫したインターフェースになる
  4. 現時点で外部クライアントがないため、後方互換性の心配が不要

懸念点と解決策

懸念点 1: コマンドのバリデーション

問題: 任意のコマンドを実行できると、意図しないコマンド実行のリスクがある

解決策: 許可されたコマンドのホワイトリストを定義

var AllowedCommands = map[string]bool{
    "plan":      true,
    "fullstack": true,
    "go":        true,
    "nextjs":    true,
}

懸念点 2: /fullstack の長時間実行

問題: /fullstack は複数フェーズ(Backend → Frontend)を持ち、実行時間が非常に長くなる可能性がある

解決策:

  • 現在の 60 分タイムアウトを維持(十分な長さ)
  • ストリーミングAPI(SSE)を推奨し、リアルタイムで進捗を確認可能

懸念点 3: エンドポイント命名

問題: /api/plan/api/command への変更は大きな変更

解決策:

  • 今回は汎用化を優先し /api/command に統一
  • Phase 1 の段階で外部利用者はいないため、破壊的変更は許容

実装計画

ステップ 1: 型定義の更新

ファイル: internal/service/types.go

変更内容:

  • PlanResultCommandResult にリネーム
  • StreamEvent.Result フィールドの型を *CommandResult に更新
  • 新規: 許可コマンドの定数定義
// AllowedCommands は許可されたスラッシュコマンドのリストです
var AllowedCommands = map[string]bool{
    "plan":      true,
    "fullstack": true,
    "go":        true,
    "nextjs":    true,
}

// CommandResult はExecuteCommandの結果を表します(旧PlanResult)
type CommandResult struct {
    SessionID string     `json:"session_id"`
    Output    string     `json:"output"`
    Questions []Question `json:"questions,omitempty"`
    Completed bool       `json:"completed"`
    CostUSD   float64    `json:"cost_usd,omitempty"`
}

// StreamEvent はストリーミングイベントを表します
type StreamEvent struct {
    Type      string         `json:"type"`
    SessionID string         `json:"session_id,omitempty"`
    Message   string         `json:"message,omitempty"`
    ToolName  string         `json:"tool_name,omitempty"`
    ToolInput interface{}    `json:"tool_input,omitempty"`
    Result    *CommandResult `json:"result,omitempty"` // *PlanResult → *CommandResult
}

定数の配置:

  • internal/service/types.goAllowedCommands を定義(パッケージ変数)
  • ハンドラー層では service.AllowedCommands を参照してコマンド存在チェック
  • サービス層で最終的なバリデーションを実施

ステップ 2: サービス層の汎用化

ファイル: internal/service/claude.go

変更内容:

  • ClaudeService インターフェースに新規メソッド追加:
    • ExecuteCommand(ctx, project, command, args string) (*CommandResult, error)
    • ExecuteCommandStream(ctx, project, command, args string, eventCh chan<- StreamEvent) error
  • 既存の ExecutePlan / ExecutePlanStream は内部で ExecuteCommand を呼び出す形に変更(インターフェース互換性維持)
  • ContinueSession / ContinueSessionStream の戻り値型を *CommandResult に変更
  • 内部メソッド executeCommand, parseResponse の戻り値型を *CommandResult に変更
// ClaudeService インターフェース
type ClaudeService interface {
    // 新規メソッド(汎用コマンド実行)
    ExecuteCommand(ctx context.Context, project, command, args string) (*CommandResult, error)
    ExecuteCommandStream(ctx context.Context, project, command, args string, eventCh chan<- StreamEvent) error

    // 既存メソッド(互換性維持、内部でExecuteCommandを呼び出す)
    ExecutePlan(ctx context.Context, project, args string) (*CommandResult, error)
    ExecutePlanStream(ctx context.Context, project, args string, eventCh chan<- StreamEvent) error

    // セッション継続
    ContinueSession(ctx context.Context, project, sessionID, answer string) (*CommandResult, error)
    ContinueSessionStream(ctx context.Context, project, sessionID, answer string, eventCh chan<- StreamEvent) error
}

// ExecuteCommand はカスタムコマンドを実行します
func (s *claudeServiceImpl) ExecuteCommand(ctx context.Context, project, command, args string) (*CommandResult, error) {
    log.Printf("[ClaudeService] ExecuteCommand started: project=%s, command=%s, args=%s", project, command, truncateLog(args, 100))

    // コマンドバリデーション
    if !AllowedCommands[command] {
        return nil, fmt.Errorf("command not allowed: %s", command)
    }

    // プロンプト構築: "/<command> <args>"
    prompt := fmt.Sprintf("/%s %s", command, args)
    return s.executeCommand(ctx, project, prompt, "")
}

// ExecutePlan は互換性のために維持(内部でExecuteCommandを呼び出す)
func (s *claudeServiceImpl) ExecutePlan(ctx context.Context, project, args string) (*CommandResult, error) {
    return s.ExecuteCommand(ctx, project, "plan", args)
}

エラーメッセージの統一:

  • サービス層(claude.go): 英語のエラーメッセージ(ログ・内部エラー)
  • ハンドラー層(command.go): 日本語のエラーメッセージ(APIレスポンス)

ステップ 3: ハンドラー層の汎用化

ファイル: internal/handler/command.go(新規作成)

設計方針:

  • 既存の plan.go は維持(Git 履歴維持、互換性維持)
  • command.go を新規作成し、汎用コマンドハンドラーを実装

変更内容:

  • CommandRequest を定義(command フィールド追加)
  • CommandResponse を定義
  • CommandHandler を実装
  • バリデーションにコマンドチェックを追加
// CommandRequest は/api/commandリクエストの構造体です
type CommandRequest struct {
    Project string `json:"project"` // プロジェクトのパス
    Command string `json:"command"` // 実行するコマンド(plan, fullstack, go, nextjs)
    Args    string `json:"args"`    // コマンドの引数
}

// CommandResponse は/api/commandレスポンスの構造体です
type CommandResponse struct {
    Success   bool               `json:"success"`
    SessionID string             `json:"session_id,omitempty"`
    Output    string             `json:"output,omitempty"`
    Questions []service.Question `json:"questions,omitempty"`
    Completed bool               `json:"completed"`
    CostUSD   float64            `json:"cost_usd,omitempty"`
    Error     string             `json:"error,omitempty"`
}

// CommandHandler はCommand関連のHTTPハンドラを提供します
type CommandHandler struct {
    claudeService service.ClaudeService
}

バリデーション順序:

  1. JSON リクエストボディのバインド
  2. project フィールドの必須チェック・パス検証
  3. command フィールドの必須チェック・許可リストチェック
  4. args フィールドの必須チェック

エラーは最初に検出された時点で即座に返す(fail-fast)

ステップ 4: ルーティングの更新

ファイル: cmd/server/main.go

変更内容:

  • /api/command 系のエンドポイントを追加
  • 既存の /api/plan は維持(後方互換性)
// 依存性の組み立て
claudeService := service.NewClaudeService()
planHandler := handler.NewPlanHandler(claudeService)
commandHandler := handler.NewCommandHandler(claudeService)

// APIルーティング
api := r.Group("/api")
{
    // 汎用コマンドAPI(推奨)
    api.POST("/command", commandHandler.Handle)
    api.POST("/command/stream", commandHandler.HandleStream)
    api.POST("/command/continue", commandHandler.HandleContinue)
    api.POST("/command/continue/stream", commandHandler.HandleContinueStream)

    // 旧API(互換性維持)
    api.POST("/plan", planHandler.Handle)
    api.POST("/plan/stream", planHandler.HandleStream)
    api.POST("/plan/continue", planHandler.HandleContinue)
    api.POST("/plan/continue/stream", planHandler.HandleContinueStream)
}

ステップ 5: フロントエンド(web/index.html)の更新

変更内容:

  • API エンドポイントを /api/command に変更
  • リクエストボディに command フィールドを追加
  • UI にコマンド選択機能を追加(ドロップダウン)
  • プロンプト表示を /${command} ${args} 形式に変更

コマンド選択UI:

<select id="command-select">
    <option value="plan">/plan - 実装計画作成</option>
    <option value="fullstack">/fullstack - フルスタック実装</option>
    <option value="go">/go - Go バックエンド実装</option>
    <option value="nextjs">/nextjs - Next.js フロントエンド実装</option>
</select>

ステップ 6: doc.go の更新

ファイル: internal/handler/doc.go, internal/service/doc.go

変更内容:

  • ドキュメントを新しいAPI仕様に合わせて更新
  • 許可コマンドリストを記載
  • /api/command エンドポイントの仕様を追加

APIリファレンス(変更後)

POST /api/command

カスタムコマンドを実行します。

リクエスト:

{
    "project": "/path/to/project",
    "command": "fullstack",
    "args": "仕様書の内容..."
}

レスポンス:

{
    "success": true,
    "session_id": "session-xxx",
    "output": "実行結果...",
    "questions": [],
    "completed": true,
    "cost_usd": 0.05
}

POST /api/command/stream

カスタムコマンドをストリーミングで実行します(SSE)。

POST /api/command/continue

セッションを継続して回答を送信します。

リクエスト:

{
    "project": "/path/to/project",
    "session_id": "session-xxx",
    "answer": "ユーザーの回答"
}

POST /api/command/continue/stream

セッションをストリーミングで継続します(SSE)。


許可コマンドリスト

コマンド説明
plan実装計画の作成
fullstackバックエンド + フロントエンドの実装
goGo バックエンドのみの実装
nextjsNext.js フロントエンドのみの実装

変更ファイル一覧

ファイル変更内容
internal/service/types.go型リネーム(PlanResult→CommandResult)、StreamEvent.Result型更新、許可コマンド定義追加
internal/service/claude.goExecuteCommand追加、既存メソッドの戻り値型変更、ExecutePlanは内部委譲
internal/handler/command.go新規作成: CommandHandler、CommandRequest、CommandResponse
internal/handler/plan.go戻り値型を service.CommandResult に変更
internal/handler/doc.goドキュメント更新(/api/command 追加)
internal/service/doc.goドキュメント更新(ExecuteCommand 追加)
cmd/server/main.goCommandHandler 追加、/api/command ルーティング追加
web/index.htmlコマンド選択UI追加、APIエンドポイント変更

テスト計画

1. 単体テスト(service 層)

ファイル: internal/service/claude_test.go

テストケース:

  • TestExecuteCommand_AllowedCommands - 許可されたコマンド(plan, fullstack, go, nextjs)
  • TestExecuteCommand_DisallowedCommand - 不許可コマンド(例: "rm", "invalid")
  • TestExecuteCommand_EmptyCommand - 空のコマンド

2. 単体テスト(handler 層)

ファイル: internal/handler/command_test.go

テストケース:

  • TestCommandHandler_ValidRequest - 正常なリクエスト
  • TestCommandHandler_InvalidCommand - 不正なコマンド
  • TestCommandHandler_MissingProject - project フィールドなし
  • TestCommandHandler_MissingArgs - args フィールドなし

3. 統合テスト

  • /api/command/plan 実行
  • /api/command/fullstack 実行
  • セッション継続のテスト

4. 手動テスト

  • ブラウザから各コマンド実行
  • SSE ストリーミングの動作確認

実装順序

  1. internal/service/types.go - 型定義の更新
  2. internal/service/claude.go - サービス層の汎用化
  3. internal/handler/plan.go - 戻り値型の変更
  4. internal/handler/command.go - 新規作成
  5. cmd/server/main.go - ルーティングの更新
  6. web/index.html - フロントエンドの更新
  7. internal/*/doc.go - ドキュメントの更新
  8. ビルド・動作確認

完了条件

  • 全ファイルの変更完了
  • go build ./... が成功
  • go vet ./... が成功
  • /plan コマンドが /api/command 経由で動作
  • /fullstack コマンドが /api/command 経由で動作
  • /plan コマンドが /api/plan(旧API)経由で動作(後方互換性)
  • SSE ストリーミングが正常動作
  • セッション継続が正常動作
  • ドキュメント(doc.go)が最新の API 仕様を反映

バックエンド実装レポート

実装日

2026-01-25

実装者

Claude

実装サマリー

/fullstack コマンドサポートのバックエンド実装が完了した。 /api/plan 専用だったAPIを汎用化し、/api/command エンドポイントを新設した。 許可コマンドのホワイトリスト(plan, fullstack, go, nextjs)により、セキュアなコマンド実行を実現した。

変更ファイル一覧

ファイル変更内容
internal/service/types.goAllowedCommands 定義追加、PlanResult -> CommandResult リネーム、StreamEvent.Result 型更新
internal/service/claude.goExecuteCommand / ExecuteCommandStream メソッド追加、既存 ExecutePlan は内部で ExecuteCommand に委譲
internal/handler/command.go新規作成: CommandHandler, CommandRequest, CommandContinueRequest, CommandResponse
cmd/server/main.goCommandHandler の初期化追加、/api/command 系ルーティング追加
internal/handler/doc.goCommandHandler のドキュメント追加、許可コマンドリスト記載
internal/service/doc.goExecuteCommand / ExecuteCommandStream のドキュメント追加、AllowedCommands 記載
docs/BACKEND_API.md新規作成: API仕様書

新規APIエンドポイント

エンドポイントメソッド説明
/api/commandPOSTコマンドの同期実行
/api/command/streamPOSTコマンドのストリーミング実行(SSE)
/api/command/continuePOSTセッション継続
/api/command/continue/streamPOSTセッション継続のストリーミング実行(SSE)

許可コマンドリスト

コマンド説明
plan実装計画の作成
fullstackバックエンド + フロントエンドの実装
goGo バックエンドのみの実装
nextjsNext.js フロントエンドのみの実装

計画からの変更点

  • 特になし(計画通りに実装)

実装時の課題

ビルド・テストで苦戦した点

  • 特になし

技術的に難しかった点

  • 特になし

残存する懸念点

  • 単体テスト・統合テストが未実装(テスト計画には記載あり)
  • フロントエンド(web/index.html)の更新が未完了

後方互換性

既存の /api/plan 系エンドポイントは維持され、内部で ExecuteCommand(ctx, project, "plan", args) を呼び出す形に変更されている。 外部からの利用に影響はない。

検証結果

go build ./...   -> 成功
go vet ./...     -> 成功
go fmt           -> フォーマット問題なし

動作確認フロー

1. サーバー起動
   $ go run ./cmd/server

2. /api/command エンドポイントのテスト(curl)
   $ curl -X POST http://localhost:8080/api/command \
     -H "Content-Type: application/json" \
     -d '{"project": "/path/to/project", "command": "plan", "args": "implement feature X"}'

3. /api/command/stream エンドポイントのテスト(curl)
   $ curl -X POST http://localhost:8080/api/command/stream \
     -H "Content-Type: application/json" \
     -d '{"project": "/path/to/project", "command": "fullstack", "args": "implement feature X"}'

4. 期待される動作
   - 正常なレスポンス(success: true)が返る
   - ストリーミングの場合はSSEイベントが順次送信される
   - 不正なコマンドの場合は 400 エラー("許可されていないコマンドです")

デプロイ後の確認事項

  • /api/command エンドポイントが正常に動作する
  • /api/command/stream でSSEストリーミングが動作する
  • 既存の /api/plan エンドポイントが引き続き動作する(後方互換性)
  • 不正なコマンドが拒否される

課題・残作業

  • フロントエンド実装が残っている: web/index.html のコマンド選択UI追加とAPIエンドポイント変更が未完了
  • テスト未実装: 単体テスト・統合テストは今後の実装課題

フロントエンド実装レポート

実装日

2026-01-25

実装者

Claude

実装サマリー

web/index.html にコマンド選択UIを追加し、APIエンドポイントを /api/command 系に変更した。 これにより、ブラウザから /plan, /fullstack, /go, /nextjs の各コマンドを選択して実行できるようになった。

変更ファイル一覧

ファイル変更内容
web/index.htmlコマンド選択ドロップダウン追加、APIエンドポイント変更、リクエストボディに command フィールド追加、UIテキスト更新

実装内容

1. コマンド選択ドロップダウン追加

フォームに <select> 要素を追加し、以下のコマンドを選択可能にした。

<select id="command" name="command">
    <option value="plan">/plan - 実装計画作成</option>
    <option value="fullstack">/fullstack - フルスタック実装</option>
    <option value="go">/go - Go バックエンド実装</option>
    <option value="nextjs">/nextjs - Next.js フロントエンド実装</option>
</select>

2. APIエンドポイント変更

変更前変更後
/api/plan/stream/api/command/stream
/api/plan/continue/stream/api/command/continue/stream

3. リクエストボディ更新

command フィールドを追加し、選択されたコマンドをリクエストに含めるようにした。

// 変更前
body: JSON.stringify({ project, args })

// 変更後
body: JSON.stringify({ project, command, args })

4. UIテキスト更新

要素変更前変更後
ボタンテキストExecute /planExecute Command
ラベルPlan ArgumentsArguments
プロンプト表示/plan ${args}/${command} ${args}

計画からの変更点

  • 特になし(計画通りに実装)

実装時の課題

ビルド・テストで苦戦した点

  • 特になし(静的HTMLファイルのためビルド不要)

技術的に難しかった点

  • 特になし

残存する懸念点

  • 単体テスト・統合テストが未実装(テスト計画には記載あり)
  • コマンド選択のデフォルト値が plan 固定(ユーザー設定の永続化なし)

動作確認フロー

1. サーバー起動
   $ go run ./cmd/server

2. ブラウザで http://localhost:8080 にアクセス

3. Project Path を入力(例: /Users/user/myproject)

4. Command ドロップダウンからコマンドを選択
   - /plan: 実装計画作成
   - /fullstack: フルスタック実装
   - /go: Go バックエンド実装
   - /nextjs: Next.js フロントエンド実装

5. Arguments に引数を入力(例: ユーザー認証機能を追加)

6. "Execute Command" ボタンをクリック

7. 期待される動作
   - プロンプト表示: "/<選択したコマンド> <引数>" 形式で表示
   - SSEストリーミングでイベントが順次表示される
   - 質問が表示された場合は回答を入力可能
   - 完了時に結果が表示される

デプロイ後の確認事項

  • コマンド選択ドロップダウンが正常に表示される
  • 各コマンド(plan, fullstack, go, nextjs)が選択・実行できる
  • プロンプト表示が /<command> <args> 形式で正しく表示される
  • SSEストリーミングが正常に動作する
  • セッション継続が正常に動作する

完了

全ての実装が完了した。

完了条件の確認:

  • 全ファイルの変更完了
  • go build ./... が成功
  • go vet ./... が成功
  • /plan コマンドが /api/command 経由で動作
  • /fullstack コマンドが /api/command 経由で動作
  • /plan コマンドが /api/plan(旧API)経由で動作(後方互換性)
  • SSE ストリーミングが正常動作
  • セッション継続が正常動作
  • ドキュメント(doc.go)が最新の API 仕様を反映

ファイル移動: 本仕様書を 保守/実装/完了/ に移動してください。

mv "保守/実装/修正/2026-01-25_fullstack_command_plan.md" "保守/実装/完了/2026-01-25_fullstack_command_plan.md"