summaryrefslogtreecommitdiff
path: root/internal/api/elaborate.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/elaborate.go')
-rw-r--r--internal/api/elaborate.go244
1 files changed, 0 insertions, 244 deletions
diff --git a/internal/api/elaborate.go b/internal/api/elaborate.go
deleted file mode 100644
index e8930a6..0000000
--- a/internal/api/elaborate.go
+++ /dev/null
@@ -1,244 +0,0 @@
-package api
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "net/http"
- "os/exec"
- "strings"
- "time"
-)
-
-const elaborateTimeout = 30 * time.Second
-
-// claudeJSONResult is the top-level object returned by `claude --output-format json`.
-type claudeJSONResult struct {
- Result string `json:"result"`
- IsError bool `json:"is_error"`
-}
-
-// geminiJSONResult is the top-level object returned by `gemini --output-format json`.
-type geminiJSONResult struct {
- Response string `json:"response"`
-}
-
-// extractJSON returns the first top-level JSON object found in s, stripping
-// surrounding prose or markdown code fences the model may have added.
-func extractJSON(s string) string {
- start := strings.Index(s, "{")
- end := strings.LastIndex(s, "}")
- if start == -1 || end == -1 || end < start {
- return s
- }
- return s[start : end+1]
-}
-
-func (s *Server) claudeBinaryPath() string {
- if s.elaborateCmdPath != "" {
- return s.elaborateCmdPath
- }
- if s.claudeBinPath != "" {
- return s.claudeBinPath
- }
- return "claude"
-}
-
-func (s *Server) geminiBinaryPath() string {
- if s.geminiBinPath != "" {
- return s.geminiBinPath
- }
- return "gemini"
-}
-
-// elaboratedStorySubtask is a leaf unit within a story task.
-type elaboratedStorySubtask struct {
- Name string `json:"name"`
- Instructions string `json:"instructions"`
-}
-
-// elaboratedStoryTask is one independently-deployable unit in a story plan.
-type elaboratedStoryTask struct {
- Name string `json:"name"`
- Instructions string `json:"instructions"`
- AcceptanceCriteria string `json:"acceptance_criteria"`
- Subtasks []elaboratedStorySubtask `json:"subtasks"`
-}
-
-// elaboratedStoryValidation describes how to verify the story was successful.
-type elaboratedStoryValidation struct {
- Type string `json:"type"`
- Steps []string `json:"steps"`
- SuccessCriteria string `json:"success_criteria"`
-}
-
-// elaboratedStory is the full implementation plan produced by story elaboration.
-type elaboratedStory struct {
- Name string `json:"name"`
- BranchName string `json:"branch_name"`
- Tasks []elaboratedStoryTask `json:"tasks"`
- Validation elaboratedStoryValidation `json:"validation"`
-}
-
-func buildStoryElaboratePrompt() string {
- return `You are a software architect. Given a goal, analyze the codebase at /workspace and produce a structured implementation plan as JSON.
-
-Output ONLY valid JSON matching this schema:
-{
- "name": "story name",
- "branch_name": "story/kebab-case-name",
- "tasks": [
- {
- "name": "task name",
- "instructions": "detailed instructions including file paths and what to change",
- "acceptance_criteria": "specific, verifiable conditions a separate reviewer can check — e.g. 'run go test ./... and verify all pass; confirm GET /api/foo returns 200 with expected JSON shape'",
- "subtasks": [
- { "name": "subtask name", "instructions": "..." }
- ]
- }
- ],
- "validation": {
- "type": "build|test|smoke",
- "steps": ["step1", "step2"],
- "success_criteria": "what success looks like"
- }
-}
-
-Rules:
-- Tasks must be independently buildable (each can be deployed alone)
-- Subtasks within a task are order-dependent and run sequentially
-- Instructions must include specific file paths, function names, and exact changes
-- Instructions must end with: git add -A && git commit -m "..." && git push origin <branch>
-- Validation should match the scope: small change = build check; new feature = smoke test
-- acceptance_criteria must be concrete and verifiable by a separate agent — no vague assertions like "code looks good"`
-}
-
-func (s *Server) elaborateStoryWithClaude(ctx context.Context, workDir, goal string) (*elaboratedStory, error) {
- cmd := exec.CommandContext(ctx, s.claudeBinaryPath(),
- "-p", goal,
- "--system-prompt", buildStoryElaboratePrompt(),
- "--output-format", "json",
- "--model", "haiku",
- )
- if workDir != "" {
- cmd.Dir = workDir
- }
-
- var stdout, stderr bytes.Buffer
- cmd.Stdout = &stdout
- cmd.Stderr = &stderr
-
- err := cmd.Run()
-
- output := stdout.Bytes()
- if len(output) == 0 {
- if err != nil {
- return nil, fmt.Errorf("claude failed: %w (stderr: %s)", err, stderr.String())
- }
- return nil, fmt.Errorf("claude returned no output")
- }
-
- var wrapper claudeJSONResult
- if jerr := json.Unmarshal(output, &wrapper); jerr != nil {
- return nil, fmt.Errorf("failed to parse claude JSON wrapper: %w (output: %s)", jerr, string(output))
- }
- if wrapper.IsError {
- return nil, fmt.Errorf("claude error: %s", wrapper.Result)
- }
-
- var result elaboratedStory
- if jerr := json.Unmarshal([]byte(extractJSON(wrapper.Result)), &result); jerr != nil {
- return nil, fmt.Errorf("failed to parse elaborated story JSON: %w (result: %s)", jerr, wrapper.Result)
- }
- return &result, nil
-}
-
-func (s *Server) elaborateStoryWithGemini(ctx context.Context, workDir, goal string) (*elaboratedStory, error) {
- combinedPrompt := fmt.Sprintf("%s\n\n%s", buildStoryElaboratePrompt(), goal)
- cmd := exec.CommandContext(ctx, s.geminiBinaryPath(),
- "-p", combinedPrompt,
- "--output-format", "json",
- "--model", "gemini-2.5-flash-lite",
- )
- if workDir != "" {
- cmd.Dir = workDir
- }
-
- var stdout, stderr bytes.Buffer
- cmd.Stdout = &stdout
- cmd.Stderr = &stderr
-
- if err := cmd.Run(); err != nil {
- return nil, fmt.Errorf("gemini failed: %w (stderr: %s)", err, stderr.String())
- }
-
- var wrapper geminiJSONResult
- if err := json.Unmarshal(stdout.Bytes(), &wrapper); err != nil {
- return nil, fmt.Errorf("failed to parse gemini JSON wrapper: %w (output: %s)", err, stdout.String())
- }
-
- var result elaboratedStory
- if err := json.Unmarshal([]byte(extractJSON(wrapper.Response)), &result); err != nil {
- return nil, fmt.Errorf("failed to parse elaborated story JSON: %w (response: %s)", err, wrapper.Response)
- }
- return &result, nil
-}
-
-func (s *Server) handleElaborateStory(w http.ResponseWriter, r *http.Request) {
- var input struct {
- Goal string `json:"goal"`
- ProjectID string `json:"project_id"`
- }
- if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
- return
- }
- if input.Goal == "" {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "goal is required"})
- return
- }
- if input.ProjectID == "" {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "project_id is required"})
- return
- }
-
- proj, err := s.store.GetProject(input.ProjectID)
- if err != nil {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"})
- return
- }
-
- // Update git refs without modifying the working tree.
- if proj.LocalPath != "" {
- gitCmd := exec.Command("git", "-C", proj.LocalPath, "fetch", "origin")
- if err := gitCmd.Run(); err != nil {
- s.logger.Warn("story elaborate: git fetch failed", "error", err, "path", proj.LocalPath)
- }
- }
-
- ctx, cancel := context.WithTimeout(r.Context(), elaborateTimeout)
- defer cancel()
-
- result, err := s.elaborateStoryWithClaude(ctx, proj.LocalPath, input.Goal)
- if err != nil {
- s.logger.Warn("story elaborate: claude failed, falling back to gemini", "error", err)
- result, err = s.elaborateStoryWithGemini(ctx, proj.LocalPath, input.Goal)
- if err != nil {
- s.logger.Error("story elaborate: fallback gemini also failed", "error", err)
- writeJSON(w, http.StatusBadGateway, map[string]string{
- "error": fmt.Sprintf("elaboration failed: %v", err),
- })
- return
- }
- }
-
- if result.Name == "" {
- writeJSON(w, http.StatusBadGateway, map[string]string{
- "error": "elaboration failed: missing required fields in response",
- })
- return
- }
-
- writeJSON(w, http.StatusOK, result)
-}