summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/elaborate.go20
-rw-r--r--internal/api/elaborate_test.go8
-rw-r--r--internal/api/server.go11
-rw-r--r--internal/api/server_test.go25
-rw-r--r--internal/api/templates.go14
-rw-r--r--internal/api/templates_test.go13
-rw-r--r--internal/api/validate.go32
-rw-r--r--internal/api/validate_test.go8
8 files changed, 84 insertions, 47 deletions
diff --git a/internal/api/elaborate.go b/internal/api/elaborate.go
index 8a18dee..907cb98 100644
--- a/internal/api/elaborate.go
+++ b/internal/api/elaborate.go
@@ -18,7 +18,7 @@ func buildElaboratePrompt(workDir string) string {
if workDir != "" {
workDirLine = fmt.Sprintf(` "project_dir": string — use %q for tasks that operate on this codebase, empty string otherwise,`, workDir)
}
- return `You are a task configuration assistant for Claudomator, an AI task runner that executes tasks by running Claude as a subprocess.
+ return `You are a task configuration assistant for Claudomator, an AI task runner that executes tasks by running Claude or Gemini as a subprocess.
Your ONLY job is to convert any user request into a Claudomator task JSON object. You MUST always output valid JSON. Never ask clarifying questions. Never explain. Never refuse. Make reasonable assumptions and produce the JSON.
@@ -27,9 +27,10 @@ Output ONLY a valid JSON object matching this schema (no markdown fences, no pro
{
"name": string — short imperative title (≤60 chars),
"description": string — 1-2 sentence summary,
- "claude": {
- "model": string — "sonnet" unless the task obviously needs opus,
- "instructions": string — detailed, step-by-step instructions for Claude,
+ "agent": {
+ "type": "claude" | "gemini",
+ "model": string — "sonnet" for claude, "gemini-2.0-flash" for gemini,
+ "instructions": string — detailed, step-by-step instructions for the agent,
` + workDirLine + `
"max_budget_usd": number — conservative estimate (0.25–5.00),
"allowed_tools": array — only tools the task genuinely needs
@@ -44,13 +45,14 @@ Output ONLY a valid JSON object matching this schema (no markdown fences, no pro
type elaboratedTask struct {
Name string `json:"name"`
Description string `json:"description"`
- Claude elaboratedClaude `json:"claude"`
+ Agent elaboratedAgent `json:"agent"`
Timeout string `json:"timeout"`
Priority string `json:"priority"`
Tags []string `json:"tags"`
}
-type elaboratedClaude struct {
+type elaboratedAgent struct {
+ Type string `json:"type"`
Model string `json:"model"`
Instructions string `json:"instructions"`
ProjectDir string `json:"project_dir"`
@@ -149,12 +151,16 @@ func (s *Server) handleElaborateTask(w http.ResponseWriter, r *http.Request) {
return
}
- if result.Name == "" || result.Claude.Instructions == "" {
+ if result.Name == "" || result.Agent.Instructions == "" {
writeJSON(w, http.StatusBadGateway, map[string]string{
"error": "elaboration failed: missing required fields in response",
})
return
}
+ if result.Agent.Type == "" {
+ result.Agent.Type = "claude"
+ }
+
writeJSON(w, http.StatusOK, result)
}
diff --git a/internal/api/elaborate_test.go b/internal/api/elaborate_test.go
index 09f7fbe..b33ca11 100644
--- a/internal/api/elaborate_test.go
+++ b/internal/api/elaborate_test.go
@@ -53,7 +53,8 @@ func TestElaborateTask_Success(t *testing.T) {
task := elaboratedTask{
Name: "Run Go tests with race detector",
Description: "Runs the Go test suite with -race flag and checks coverage.",
- Claude: elaboratedClaude{
+ Agent: elaboratedAgent{
+ Type: "claude",
Model: "sonnet",
Instructions: "Run go test -race ./... and report results.",
ProjectDir: "",
@@ -94,7 +95,7 @@ func TestElaborateTask_Success(t *testing.T) {
if result.Name == "" {
t.Error("expected non-empty name")
}
- if result.Claude.Instructions == "" {
+ if result.Agent.Instructions == "" {
t.Error("expected non-empty instructions")
}
}
@@ -127,7 +128,8 @@ func TestElaborateTask_MarkdownFencedJSON(t *testing.T) {
task := elaboratedTask{
Name: "Test task",
Description: "Does something.",
- Claude: elaboratedClaude{
+ Agent: elaboratedAgent{
+ Type: "claude",
Model: "sonnet",
Instructions: "Do the thing.",
MaxBudgetUSD: 0.5,
diff --git a/internal/api/server.go b/internal/api/server.go
index 833be8b..3d7cb1e 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -37,6 +37,7 @@ type Server struct {
logger *slog.Logger
mux *http.ServeMux
claudeBinPath string // path to claude binary; defaults to "claude"
+ geminiBinPath string // path to gemini binary; defaults to "gemini"
elaborateCmdPath string // overrides claudeBinPath; used in tests
validateCmdPath string // overrides claudeBinPath for validate; used in tests
scripts ScriptRegistry // optional; maps endpoint name → script path
@@ -56,7 +57,7 @@ func (s *Server) SetNotifier(n notify.Notifier) {
s.notifier = n
}
-func NewServer(store *storage.DB, pool *executor.Pool, logger *slog.Logger, claudeBinPath string) *Server {
+func NewServer(store *storage.DB, pool *executor.Pool, logger *slog.Logger, claudeBinPath, geminiBinPath string) *Server {
wd, _ := os.Getwd()
s := &Server{
store: store,
@@ -68,6 +69,7 @@ func NewServer(store *storage.DB, pool *executor.Pool, logger *slog.Logger, clau
logger: logger,
mux: http.NewServeMux(),
claudeBinPath: claudeBinPath,
+ geminiBinPath: geminiBinPath,
workDir: wd,
}
s.routes()
@@ -346,7 +348,7 @@ func (s *Server) handleCreateTask(w http.ResponseWriter, r *http.Request) {
var input struct {
Name string `json:"name"`
Description string `json:"description"`
- Claude task.ClaudeConfig `json:"claude"`
+ Agent task.AgentConfig `json:"agent"`
Timeout string `json:"timeout"`
Priority string `json:"priority"`
Tags []string `json:"tags"`
@@ -362,7 +364,7 @@ func (s *Server) handleCreateTask(w http.ResponseWriter, r *http.Request) {
ID: uuid.New().String(),
Name: input.Name,
Description: input.Description,
- Claude: input.Claude,
+ Agent: input.Agent,
Priority: task.Priority(input.Priority),
Tags: input.Tags,
DependsOn: []string{},
@@ -372,6 +374,9 @@ func (s *Server) handleCreateTask(w http.ResponseWriter, r *http.Request) {
UpdatedAt: now,
ParentTaskID: input.ParentTaskID,
}
+ if t.Agent.Type == "" {
+ t.Agent.Type = "claude"
+ }
if t.Priority == "" {
t.Priority = task.PriorityNormal
}
diff --git a/internal/api/server_test.go b/internal/api/server_test.go
index c3b12ce..cd415ae 100644
--- a/internal/api/server_test.go
+++ b/internal/api/server_test.go
@@ -84,8 +84,12 @@ func testServer(t *testing.T) (*Server, *storage.DB) {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
runner := &mockRunner{}
- pool := executor.NewPool(2, runner, store, logger)
- srv := NewServer(store, pool, logger, "claude")
+ runners := map[string]executor.Runner{
+ "claude": runner,
+ "gemini": runner,
+ }
+ pool := executor.NewPool(2, runners, store, logger)
+ srv := NewServer(store, pool, logger, "claude", "gemini")
return srv, store
}
@@ -118,7 +122,8 @@ func TestCreateTask_Success(t *testing.T) {
payload := `{
"name": "API Task",
"description": "Created via API",
- "claude": {
+ "agent": {
+ "type": "claude",
"instructions": "do the thing",
"model": "sonnet"
},
@@ -160,7 +165,7 @@ func TestCreateTask_InvalidJSON(t *testing.T) {
func TestCreateTask_ValidationFailure(t *testing.T) {
srv, _ := testServer(t)
- payload := `{"name": "", "claude": {"instructions": ""}}`
+ payload := `{"name": "", "agent": {"type": "claude", "instructions": ""}}`
req := httptest.NewRequest("POST", "/api/tasks", bytes.NewBufferString(payload))
w := httptest.NewRecorder()
srv.Handler().ServeHTTP(w, req)
@@ -207,7 +212,7 @@ func TestListTasks_WithTasks(t *testing.T) {
for i := 0; i < 3; i++ {
tk := &task.Task{
ID: fmt.Sprintf("lt-%d", i), Name: fmt.Sprintf("T%d", i),
- Claude: task.ClaudeConfig{Instructions: "x"}, Priority: task.PriorityNormal,
+ Agent: task.AgentConfig{Type: "claude", Instructions: "x"}, Priority: task.PriorityNormal,
Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "linear"},
Tags: []string{}, DependsOn: []string{}, State: task.StatePending,
}
@@ -244,7 +249,7 @@ func createTaskWithState(t *testing.T, store *storage.DB, id string, state task.
tk := &task.Task{
ID: id,
Name: "test-task-" + id,
- Claude: task.ClaudeConfig{Instructions: "do something"},
+ Agent: task.AgentConfig{Type: "claude", Instructions: "do something"},
Priority: task.PriorityNormal,
Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "linear"},
Tags: []string{}, DependsOn: []string{}, State: task.StatePending,
@@ -606,7 +611,7 @@ func TestRunTask_RetryLimitReached_Returns409(t *testing.T) {
tk := &task.Task{
ID: "retry-limit-1",
Name: "Retry Limit Task",
- Claude: task.ClaudeConfig{Instructions: "do something"},
+ Agent: task.AgentConfig{Instructions: "do something"},
Priority: task.PriorityNormal,
Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "linear"},
Tags: []string{},
@@ -647,7 +652,7 @@ func TestRunTask_WithinRetryLimit_Returns202(t *testing.T) {
tk := &task.Task{
ID: "retry-within-1",
Name: "Retry Within Task",
- Claude: task.ClaudeConfig{Instructions: "do something"},
+ Agent: task.AgentConfig{Instructions: "do something"},
Priority: task.PriorityNormal,
Retry: task.RetryConfig{MaxAttempts: 3, Backoff: "linear"},
Tags: []string{},
@@ -694,7 +699,7 @@ func TestDeleteTask_Success(t *testing.T) {
srv, store := testServer(t)
// Create a task to delete.
- created := createTestTask(t, srv, `{"name":"Delete Me","claude":{"instructions":"x","model":"sonnet"}}`)
+ created := createTestTask(t, srv, `{"name":"Delete Me","agent":{"type":"claude","instructions":"x","model":"sonnet"}}`)
req := httptest.NewRequest("DELETE", "/api/tasks/"+created.ID, nil)
w := httptest.NewRecorder()
@@ -729,7 +734,7 @@ func TestDeleteTask_RunningTaskRejected(t *testing.T) {
tk := &task.Task{
ID: "running-task-del",
Name: "Running Task",
- Claude: task.ClaudeConfig{Instructions: "x", Model: "sonnet"},
+ Agent: task.AgentConfig{Instructions: "x", Model: "sonnet"},
Priority: task.PriorityNormal,
Tags: []string{},
DependsOn: []string{},
diff --git a/internal/api/templates.go b/internal/api/templates.go
index 0139895..024a6df 100644
--- a/internal/api/templates.go
+++ b/internal/api/templates.go
@@ -27,7 +27,7 @@ func (s *Server) handleCreateTemplate(w http.ResponseWriter, r *http.Request) {
var input struct {
Name string `json:"name"`
Description string `json:"description"`
- Claude task.ClaudeConfig `json:"claude"`
+ Agent task.AgentConfig `json:"agent"`
Timeout string `json:"timeout"`
Priority string `json:"priority"`
Tags []string `json:"tags"`
@@ -46,13 +46,16 @@ func (s *Server) handleCreateTemplate(w http.ResponseWriter, r *http.Request) {
ID: uuid.New().String(),
Name: input.Name,
Description: input.Description,
- Claude: input.Claude,
+ Agent: input.Agent,
Timeout: input.Timeout,
Priority: input.Priority,
Tags: input.Tags,
CreatedAt: now,
UpdatedAt: now,
}
+ if tmpl.Agent.Type == "" {
+ tmpl.Agent.Type = "claude"
+ }
if tmpl.Priority == "" {
tmpl.Priority = "normal"
}
@@ -98,7 +101,7 @@ func (s *Server) handleUpdateTemplate(w http.ResponseWriter, r *http.Request) {
var input struct {
Name string `json:"name"`
Description string `json:"description"`
- Claude task.ClaudeConfig `json:"claude"`
+ Agent task.AgentConfig `json:"agent"`
Timeout string `json:"timeout"`
Priority string `json:"priority"`
Tags []string `json:"tags"`
@@ -114,7 +117,10 @@ func (s *Server) handleUpdateTemplate(w http.ResponseWriter, r *http.Request) {
existing.Name = input.Name
existing.Description = input.Description
- existing.Claude = input.Claude
+ existing.Agent = input.Agent
+ if existing.Agent.Type == "" {
+ existing.Agent.Type = "claude"
+ }
existing.Timeout = input.Timeout
existing.Priority = input.Priority
if input.Tags != nil {
diff --git a/internal/api/templates_test.go b/internal/api/templates_test.go
index bbcfc87..474c5d4 100644
--- a/internal/api/templates_test.go
+++ b/internal/api/templates_test.go
@@ -34,7 +34,8 @@ func TestCreateTemplate_Success(t *testing.T) {
payload := `{
"name": "Go: Run Tests",
"description": "Run the full test suite with race detector",
- "claude": {
+ "agent": {
+ "type": "claude",
"model": "sonnet",
"instructions": "Run go test -race ./...",
"max_budget_usd": 0.50,
@@ -65,7 +66,7 @@ func TestCreateTemplate_Success(t *testing.T) {
func TestGetTemplate_AfterCreate(t *testing.T) {
srv, _ := testServer(t)
- payload := `{"name": "Fetch Me", "claude": {"instructions": "do thing", "model": "haiku"}}`
+ payload := `{"name": "Fetch Me", "agent": {"type": "claude", "instructions": "do thing", "model": "haiku"}}`
req := httptest.NewRequest("POST", "/api/templates", bytes.NewBufferString(payload))
w := httptest.NewRecorder()
srv.Handler().ServeHTTP(w, req)
@@ -107,14 +108,14 @@ func TestGetTemplate_NotFound(t *testing.T) {
func TestUpdateTemplate(t *testing.T) {
srv, _ := testServer(t)
- payload := `{"name": "Original Name", "claude": {"instructions": "original"}}`
+ payload := `{"name": "Original Name", "agent": {"type": "claude", "instructions": "original"}}`
req := httptest.NewRequest("POST", "/api/templates", bytes.NewBufferString(payload))
w := httptest.NewRecorder()
srv.Handler().ServeHTTP(w, req)
var created storage.Template
json.NewDecoder(w.Body).Decode(&created)
- update := `{"name": "Updated Name", "claude": {"instructions": "updated"}}`
+ update := `{"name": "Updated Name", "agent": {"type": "claude", "instructions": "updated"}}`
req2 := httptest.NewRequest("PUT", fmt.Sprintf("/api/templates/%s", created.ID), bytes.NewBufferString(update))
w2 := httptest.NewRecorder()
srv.Handler().ServeHTTP(w2, req2)
@@ -132,7 +133,7 @@ func TestUpdateTemplate(t *testing.T) {
func TestUpdateTemplate_NotFound(t *testing.T) {
srv, _ := testServer(t)
- update := `{"name": "Ghost", "claude": {"instructions": "x"}}`
+ update := `{"name": "Ghost", "agent": {"type": "claude", "instructions": "x"}}`
req := httptest.NewRequest("PUT", "/api/templates/nonexistent", bytes.NewBufferString(update))
w := httptest.NewRecorder()
srv.Handler().ServeHTTP(w, req)
@@ -145,7 +146,7 @@ func TestUpdateTemplate_NotFound(t *testing.T) {
func TestDeleteTemplate(t *testing.T) {
srv, _ := testServer(t)
- payload := `{"name": "To Delete", "claude": {"instructions": "bye"}}`
+ payload := `{"name": "To Delete", "agent": {"type": "claude", "instructions": "bye"}}`
req := httptest.NewRequest("POST", "/api/templates", bytes.NewBufferString(payload))
w := httptest.NewRecorder()
srv.Handler().ServeHTTP(w, req)
diff --git a/internal/api/validate.go b/internal/api/validate.go
index 0fcdb47..07d293c 100644
--- a/internal/api/validate.go
+++ b/internal/api/validate.go
@@ -12,7 +12,7 @@ import (
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.
+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.
@@ -48,7 +48,7 @@ func (s *Server) validateBinaryPath() string {
if s.validateCmdPath != "" {
return s.validateCmdPath
}
- return s.claudeBinaryPath()
+ return s.claudeBinPath
}
func (s *Server) handleValidateTask(w http.ResponseWriter, r *http.Request) {
@@ -59,11 +59,13 @@ func (s *Server) handleValidateTask(w http.ResponseWriter, r *http.Request) {
var input struct {
Name string `json:"name"`
- Claude struct {
+ 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:"claude"`
+ } `json:"agent"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
@@ -73,17 +75,27 @@ func (s *Server) handleValidateTask(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
return
}
- if input.Claude.Instructions == "" {
+ if input.Agent.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)
+ agentType := input.Agent.Type
+ if agentType == "" {
+ agentType = "claude"
}
- if len(input.Claude.AllowedTools) > 0 {
- userMsg += fmt.Sprintf("\n\nAllowed tools: %v", input.Claude.AllowedTools)
+
+ 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)
diff --git a/internal/api/validate_test.go b/internal/api/validate_test.go
index 5a1246b..c3d7b1f 100644
--- a/internal/api/validate_test.go
+++ b/internal/api/validate_test.go
@@ -23,7 +23,7 @@ func TestValidateTask_Success(t *testing.T) {
wrapperJSON, _ := json.Marshal(wrapper)
srv.validateCmdPath = createFakeClaude(t, string(wrapperJSON), 0)
- body := `{"name":"Test Task","claude":{"instructions":"Run go test ./... and report results."}}`
+ body := `{"name":"Test Task","agent":{"instructions":"Run go test ./... and report results."}}`
req := httptest.NewRequest("POST", "/api/tasks/validate", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
@@ -46,7 +46,7 @@ func TestValidateTask_Success(t *testing.T) {
func TestValidateTask_MissingInstructions(t *testing.T) {
srv, _ := testServer(t)
- body := `{"name":"Test Task","claude":{"instructions":""}}`
+ body := `{"name":"Test Task","agent":{"instructions":""}}`
req := httptest.NewRequest("POST", "/api/tasks/validate", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
@@ -61,7 +61,7 @@ func TestValidateTask_MissingInstructions(t *testing.T) {
func TestValidateTask_MissingName(t *testing.T) {
srv, _ := testServer(t)
- body := `{"name":"","claude":{"instructions":"Do something useful."}}`
+ body := `{"name":"","agent":{"instructions":"Do something useful."}}`
req := httptest.NewRequest("POST", "/api/tasks/validate", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
@@ -77,7 +77,7 @@ func TestValidateTask_BadJSONFromClaude(t *testing.T) {
srv, _ := testServer(t)
srv.validateCmdPath = createFakeClaude(t, "not valid json at all", 0)
- body := `{"name":"Test Task","claude":{"instructions":"Do something useful."}}`
+ body := `{"name":"Test Task","agent":{"instructions":"Do something useful."}}`
req := httptest.NewRequest("POST", "/api/tasks/validate", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()