package api import ( "bytes" "context" "encoding/json" "fmt" "net/http" "os/exec" "time" ) const validateTimeout = 20 * time.Second const validateSystemPrompt = `You are a task instruction reviewer for Claudomator, an AI task runner that executes tasks by running Claude as a subprocess. Analyze the given task name and instructions for clarity and completeness. Output ONLY a valid JSON object (no markdown fences, no prose, no explanation): { "clarity": "clear" | "warning" | "blocking", "ready": boolean — true if task can proceed without clarification, "summary": string — 1-2 sentence assessment, "questions": [{"text": string, "severity": "blocking" | "minor"}], "suggestions": [string] } clarity definitions: - "clear": instructions are specific, actionable, and complete - "warning": minor ambiguities exist but task can reasonably proceed - "blocking": critical information is missing; task cannot succeed without clarification` type validateResult struct { Clarity string `json:"clarity"` Ready bool `json:"ready"` Questions []validateQuestion `json:"questions"` Suggestions []string `json:"suggestions"` Summary string `json:"summary"` } type validateQuestion struct { Severity string `json:"severity"` Text string `json:"text"` } func (s *Server) validateBinaryPath() string { if s.validateCmdPath != "" { return s.validateCmdPath } return s.claudeBinaryPath() } func (s *Server) handleValidateTask(w http.ResponseWriter, r *http.Request) { var input struct { Name string `json:"name"` Claude struct { Instructions string `json:"instructions"` WorkingDir string `json:"working_dir"` AllowedTools []string `json:"allowed_tools"` } `json:"claude"` } 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.Name == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"}) return } if input.Claude.Instructions == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "instructions are required"}) return } userMsg := fmt.Sprintf("Task name: %s\n\nInstructions:\n%s", input.Name, input.Claude.Instructions) if input.Claude.WorkingDir != "" { userMsg += fmt.Sprintf("\n\nWorking directory: %s", input.Claude.WorkingDir) } if len(input.Claude.AllowedTools) > 0 { userMsg += fmt.Sprintf("\n\nAllowed tools: %v", input.Claude.AllowedTools) } ctx, cancel := context.WithTimeout(r.Context(), validateTimeout) defer cancel() cmd := exec.CommandContext(ctx, s.validateBinaryPath(), "-p", userMsg, "--system-prompt", validateSystemPrompt, "--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("validate: claude subprocess failed", "error", err, "stderr", stderr.String()) writeJSON(w, http.StatusBadGateway, map[string]string{ "error": fmt.Sprintf("validation failed: %v", err), }) return } var wrapper claudeJSONResult if err := json.Unmarshal(stdout.Bytes(), &wrapper); err != nil { s.logger.Error("validate: failed to parse claude JSON wrapper", "error", err, "stdout", stdout.String()) writeJSON(w, http.StatusBadGateway, map[string]string{ "error": "validation failed: invalid JSON from claude", }) return } var result validateResult if err := json.Unmarshal([]byte(extractJSON(wrapper.Result)), &result); err != nil { s.logger.Error("validate: failed to parse validation result", "error", err, "result", wrapper.Result) writeJSON(w, http.StatusBadGateway, map[string]string{ "error": "validation failed: claude returned invalid result JSON", }) return } writeJSON(w, http.StatusOK, result) }