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