summaryrefslogtreecommitdiff
path: root/internal/api/validate.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/validate.go')
-rw-r--r--internal/api/validate.go125
1 files changed, 125 insertions, 0 deletions
diff --git a/internal/api/validate.go b/internal/api/validate.go
new file mode 100644
index 0000000..d8ebde9
--- /dev/null
+++ b/internal/api/validate.go
@@ -0,0 +1,125 @@
+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)
+}