1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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"`
ProjectDir string `json:"project_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.ProjectDir != "" {
userMsg += fmt.Sprintf("\n\nWorking directory: %s", input.Claude.ProjectDir)
}
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)
}
|