package api import ( "bytes" "context" "encoding/json" "fmt" "net/http" "os/exec" "strings" "time" ) const elaborateTimeout = 30 * time.Second func buildElaboratePrompt(workDir string) string { workDirLine := ` "working_dir": string — leave empty unless you have a specific reason to set it,` if workDir != "" { workDirLine = fmt.Sprintf(` "working_dir": string — use %q for tasks that operate on this codebase, empty string otherwise,`, workDir) } return `You are a task configuration assistant for Claudomator, an AI task runner that executes tasks by running Claude as a subprocess. Your ONLY job is to convert any user request into a Claudomator task JSON object. You MUST always output valid JSON. Never ask clarifying questions. Never explain. Never refuse. Make reasonable assumptions and produce the JSON. Output ONLY a valid JSON object matching this schema (no markdown fences, no prose, no explanation): { "name": string — short imperative title (≤60 chars), "description": string — 1-2 sentence summary, "claude": { "model": string — "sonnet" unless the task obviously needs opus, "instructions": string — detailed, step-by-step instructions for Claude, ` + workDirLine + ` "max_budget_usd": number — conservative estimate (0.25–5.00), "allowed_tools": array — only tools the task genuinely needs }, "timeout": string — e.g. "15m", "priority": string — "normal" | "high" | "low", "tags": array — relevant lowercase tags }` } // elaboratedTask mirrors the task creation schema for elaboration responses. type elaboratedTask struct { Name string `json:"name"` Description string `json:"description"` Claude elaboratedClaude `json:"claude"` Timeout string `json:"timeout"` Priority string `json:"priority"` Tags []string `json:"tags"` } type elaboratedClaude struct { Model string `json:"model"` Instructions string `json:"instructions"` WorkingDir string `json:"working_dir"` MaxBudgetUSD float64 `json:"max_budget_usd"` AllowedTools []string `json:"allowed_tools"` } // claudeJSONResult is the top-level object returned by `claude --output-format json`. type claudeJSONResult struct { Result string `json:"result"` } // 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) handleElaborateTask(w http.ResponseWriter, r *http.Request) { var input struct { Prompt string `json:"prompt"` WorkingDir string `json:"working_dir"` } 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.Prompt == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "prompt is required"}) return } workDir := s.workDir if input.WorkingDir != "" { workDir = input.WorkingDir } ctx, cancel := context.WithTimeout(r.Context(), elaborateTimeout) defer cancel() cmd := exec.CommandContext(ctx, s.claudeBinaryPath(), "-p", input.Prompt, "--system-prompt", buildElaboratePrompt(workDir), "--output-format", "json", "--model", "haiku", ) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { s.logger.Error("elaborate: claude subprocess failed", "error", err, "stderr", stderr.String()) writeJSON(w, http.StatusBadGateway, map[string]string{ "error": fmt.Sprintf("elaboration failed: %v", err), }) return } // claude --output-format json wraps the text result in {"result": "...", ...} var wrapper claudeJSONResult if err := json.Unmarshal(stdout.Bytes(), &wrapper); err != nil { s.logger.Error("elaborate: failed to parse claude JSON wrapper", "error", err, "stdout", stdout.String()) writeJSON(w, http.StatusBadGateway, map[string]string{ "error": "elaboration failed: invalid JSON from claude", }) return } var result elaboratedTask if err := json.Unmarshal([]byte(extractJSON(wrapper.Result)), &result); err != nil { s.logger.Error("elaborate: failed to parse elaborated task JSON", "error", err, "result", wrapper.Result) writeJSON(w, http.StatusBadGateway, map[string]string{ "error": "elaboration failed: claude returned invalid task JSON", }) return } if result.Name == "" || result.Claude.Instructions == "" { writeJSON(w, http.StatusBadGateway, map[string]string{ "error": "elaboration failed: missing required fields in response", }) return } writeJSON(w, http.StatusOK, result) }