summaryrefslogtreecommitdiff
path: root/internal/api/validate.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-06 23:55:07 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-06 23:55:07 +0000
commitfd42a54d96fcd3342941caaeb61a4b0d5d3f1b4f (patch)
tree0b9ef3b7f0ac3981aa310435d014c9f5e21089d4 /internal/api/validate.go
parent7d4890cde802974b94db24071f63e7733c3670fd (diff)
recover: restore untracked work from recovery branch (no Gemini changes)
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 <noreply@anthropic.com>
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)
+}