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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os/exec"
"strings"
"time"
)
const validateTimeout = 20 * time.Second
// claudeJSONResult is the top-level object returned by `claude --output-format json`.
type claudeJSONResult struct {
Result string `json:"result"`
IsError bool `json:"is_error"`
}
// extractJSON returns the first top-level JSON object found in s, stripping
// surrounding prose or markdown code fences the model may have added.
func extractJSON(s string) string {
start := strings.Index(s, "{")
end := strings.LastIndex(s, "}")
if start == -1 || end == -1 || end < start {
return s
}
return s[start : end+1]
}
const validateSystemPrompt = `You are a task instruction reviewer for Claudomator, an AI task runner that executes tasks by running Claude or Gemini 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.claudeBinPath
}
func (s *Server) handleValidateTask(w http.ResponseWriter, r *http.Request) {
if s.elaborateLimiter != nil && !s.elaborateLimiter.allow(realIP(r)) {
writeJSON(w, http.StatusTooManyRequests, map[string]string{"error": "rate limit exceeded"})
return
}
var input struct {
Name string `json:"name"`
Agent struct {
Type string `json:"type"`
Instructions string `json:"instructions"`
ProjectDir string `json:"project_dir"`
WorkingDir string `json:"working_dir"` // legacy
AllowedTools []string `json:"allowed_tools"`
} `json:"agent"`
}
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.Agent.Instructions == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "instructions are required"})
return
}
agentType := input.Agent.Type
if agentType == "" {
agentType = "claude"
}
projectDir := input.Agent.ProjectDir
if projectDir == "" {
projectDir = input.Agent.WorkingDir
}
userMsg := fmt.Sprintf("Task name: %s\nAgent: %s\n\nInstructions:\n%s", input.Name, agentType, input.Agent.Instructions)
if projectDir != "" {
userMsg += fmt.Sprintf("\n\nWorking directory: %s", projectDir)
}
if len(input.Agent.AllowedTools) > 0 {
userMsg += fmt.Sprintf("\n\nAllowed tools: %v", input.Agent.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)
}
|