summaryrefslogtreecommitdiff
path: root/internal/api/validate.go
blob: a3b2cf057015b583e52c65a2a5a13ce7247c639f (plain)
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
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 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.claudeBinaryPath()
}

func (s *Server) handleValidateTask(w http.ResponseWriter, r *http.Request) {
	var input struct {
		Name  string `json:"name"`
		Agent struct {
			Type         string   `json:"type"`
			Instructions string   `json:"instructions"`
			WorkingDir   string   `json:"working_dir"`
			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"
	}

	userMsg := fmt.Sprintf("Task name: %s\nAgent: %s\n\nInstructions:\n%s", input.Name, agentType, input.Agent.Instructions)
	if input.Agent.WorkingDir != "" {
		userMsg += fmt.Sprintf("\n\nWorking directory: %s", input.Agent.WorkingDir)
	}
	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)
}