From fd42a54d96fcd3342941caaeb61a4b0d5d3f1b4f Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Fri, 6 Mar 2026 23:55:07 +0000 Subject: recover: restore untracked work from recovery branch (no Gemini changes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recovered files with no Claude→Agent contamination: - docs/adr/002-task-state-machine.md - internal/api/logs.go/logs_test.go: task-level log streaming endpoint - internal/api/validate.go/validate_test.go: POST /api/tasks/validate - internal/api/server_test.go, storage/db_test.go: expanded test coverage - scripts/reset-failed-tasks, reset-running-tasks - web/app.js, index.html, style.css: frontend improvements - web/test/: active-tasks-tab, delete-button, filter-tabs, sort-tasks tests Manually applied from server.go diff (skipping Claude→Agent rename): - taskLogStore field + validateCmdPath field - DELETE /api/tasks/{id} route + handleDeleteTask - GET /api/tasks/{id}/logs/stream route - POST /api/tasks/{id}/resume route + handleResumeTimedOutTask - handleCancelTask: allow cancelling PENDING/QUEUED tasks directly Co-Authored-By: Claude Sonnet 4.6 --- internal/api/validate.go | 125 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 internal/api/validate.go (limited to 'internal/api/validate.go') 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) +} -- cgit v1.2.3