summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-08 03:34:45 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-08 06:33:02 +0000
commitf887a6387946c8fc54f83a1f11a86d6c9d68dc50 (patch)
tree5635025ce6300e1a8baedf123d05401d362247f6
parent306482ddc04c6bd6284f52727f396b19e6b8e867 (diff)
refactor(executor): update runners and tests for generic agents
-rw-r--r--internal/executor/claude.go40
-rw-r--r--internal/executor/executor_test.go35
-rw-r--r--internal/executor/gemini.go1
-rw-r--r--internal/executor/preamble.go9
4 files changed, 48 insertions, 37 deletions
diff --git a/internal/executor/claude.go b/internal/executor/claude.go
index b97f202..86a2ba5 100644
--- a/internal/executor/claude.go
+++ b/internal/executor/claude.go
@@ -56,9 +56,9 @@ func (r *ClaudeRunner) binaryPath() string {
// It retries up to 3 times on rate-limit errors using exponential backoff.
// If the agent writes a question file and exits, Run returns *BlockedError.
func (r *ClaudeRunner) Run(ctx context.Context, t *task.Task, e *storage.Execution) error {
- if t.Claude.WorkingDir != "" {
- if _, err := os.Stat(t.Claude.WorkingDir); err != nil {
- return fmt.Errorf("working_dir %q: %w", t.Claude.WorkingDir, err)
+ if t.Agent.WorkingDir != "" {
+ if _, err := os.Stat(t.Agent.WorkingDir); err != nil {
+ return fmt.Errorf("working_dir %q: %w", t.Agent.WorkingDir, err)
}
}
@@ -95,7 +95,7 @@ func (r *ClaudeRunner) Run(ctx context.Context, t *task.Task, e *storage.Executi
)
}
attempt++
- return r.execOnce(ctx, args, t.Claude.WorkingDir, e)
+ return r.execOnce(ctx, args, t.Agent.WorkingDir, e)
})
if err != nil {
return err
@@ -208,21 +208,21 @@ func (r *ClaudeRunner) buildArgs(t *task.Task, e *storage.Execution, questionFil
"--output-format", "stream-json",
"--verbose",
}
- permMode := t.Claude.PermissionMode
+ permMode := t.Agent.PermissionMode
if permMode == "" {
permMode = "bypassPermissions"
}
args = append(args, "--permission-mode", permMode)
- if t.Claude.Model != "" {
- args = append(args, "--model", t.Claude.Model)
+ if t.Agent.Model != "" {
+ args = append(args, "--model", t.Agent.Model)
}
return args
}
- instructions := t.Claude.Instructions
- allowedTools := t.Claude.AllowedTools
+ instructions := t.Agent.Instructions
+ allowedTools := t.Agent.AllowedTools
- if !t.Claude.SkipPlanning {
+ if !t.Agent.SkipPlanning {
instructions = withPlanningPreamble(instructions)
// Ensure Bash is available so the agent can POST subtasks and ask questions.
hasBash := false
@@ -244,33 +244,33 @@ func (r *ClaudeRunner) buildArgs(t *task.Task, e *storage.Execution, questionFil
"--verbose",
}
- if t.Claude.Model != "" {
- args = append(args, "--model", t.Claude.Model)
+ if t.Agent.Model != "" {
+ args = append(args, "--model", t.Agent.Model)
}
- if t.Claude.MaxBudgetUSD > 0 {
- args = append(args, "--max-budget-usd", fmt.Sprintf("%.2f", t.Claude.MaxBudgetUSD))
+ if t.Agent.MaxBudgetUSD > 0 {
+ args = append(args, "--max-budget-usd", fmt.Sprintf("%.2f", t.Agent.MaxBudgetUSD))
}
// Default to bypassPermissions — claudomator runs tasks unattended, so
// prompting for write access would always stall execution. Tasks that need
// a more restrictive mode can set permission_mode explicitly.
- permMode := t.Claude.PermissionMode
+ permMode := t.Agent.PermissionMode
if permMode == "" {
permMode = "bypassPermissions"
}
args = append(args, "--permission-mode", permMode)
- if t.Claude.SystemPromptAppend != "" {
- args = append(args, "--append-system-prompt", t.Claude.SystemPromptAppend)
+ if t.Agent.SystemPromptAppend != "" {
+ args = append(args, "--append-system-prompt", t.Agent.SystemPromptAppend)
}
for _, tool := range allowedTools {
args = append(args, "--allowedTools", tool)
}
- for _, tool := range t.Claude.DisallowedTools {
+ for _, tool := range t.Agent.DisallowedTools {
args = append(args, "--disallowedTools", tool)
}
- for _, f := range t.Claude.ContextFiles {
+ for _, f := range t.Agent.ContextFiles {
args = append(args, "--add-dir", f)
}
- args = append(args, t.Claude.AdditionalArgs...)
+ args = append(args, t.Agent.AdditionalArgs...)
return args
}
diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go
index 6d13873..2f205f8 100644
--- a/internal/executor/executor_test.go
+++ b/internal/executor/executor_test.go
@@ -64,7 +64,7 @@ func makeTask(id string) *task.Task {
now := time.Now().UTC()
return &task.Task{
ID: id, Name: "Test " + id,
- Claude: task.ClaudeConfig{Instructions: "test"},
+ Agent: task.AgentConfig{Type: "claude", Instructions: "test"},
Priority: task.PriorityNormal,
Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "linear"},
Tags: []string{},
@@ -77,8 +77,9 @@ func makeTask(id string) *task.Task {
func TestPool_Submit_TopLevel_GoesToReady(t *testing.T) {
store := testStore(t)
runner := &mockRunner{}
+ runners := map[string]Runner{"claude": runner}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
- pool := NewPool(2, runner, store, logger)
+ pool := NewPool(2, runners, store, logger)
tk := makeTask("ps-1") // no ParentTaskID → top-level
store.CreateTask(tk)
@@ -104,8 +105,9 @@ func TestPool_Submit_TopLevel_GoesToReady(t *testing.T) {
func TestPool_Submit_Subtask_GoesToCompleted(t *testing.T) {
store := testStore(t)
runner := &mockRunner{}
+ runners := map[string]Runner{"claude": runner}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
- pool := NewPool(2, runner, store, logger)
+ pool := NewPool(2, runners, store, logger)
tk := makeTask("sub-1")
tk.ParentTaskID = "parent-99" // subtask
@@ -132,8 +134,9 @@ func TestPool_Submit_Subtask_GoesToCompleted(t *testing.T) {
func TestPool_Submit_Failure(t *testing.T) {
store := testStore(t)
runner := &mockRunner{err: fmt.Errorf("boom"), exitCode: 1}
+ runners := map[string]Runner{"claude": runner}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
- pool := NewPool(2, runner, store, logger)
+ pool := NewPool(2, runners, store, logger)
tk := makeTask("pf-1")
store.CreateTask(tk)
@@ -151,8 +154,9 @@ func TestPool_Submit_Failure(t *testing.T) {
func TestPool_Submit_Timeout(t *testing.T) {
store := testStore(t)
runner := &mockRunner{delay: 5 * time.Second}
+ runners := map[string]Runner{"claude": runner}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
- pool := NewPool(2, runner, store, logger)
+ pool := NewPool(2, runners, store, logger)
tk := makeTask("pt-1")
tk.Timeout.Duration = 50 * time.Millisecond
@@ -168,8 +172,9 @@ func TestPool_Submit_Timeout(t *testing.T) {
func TestPool_Submit_Cancellation(t *testing.T) {
store := testStore(t)
runner := &mockRunner{delay: 5 * time.Second}
+ runners := map[string]Runner{"claude": runner}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
- pool := NewPool(2, runner, store, logger)
+ pool := NewPool(2, runners, store, logger)
ctx, cancel := context.WithCancel(context.Background())
tk := makeTask("pc-1")
@@ -188,8 +193,9 @@ func TestPool_Submit_Cancellation(t *testing.T) {
func TestPool_Cancel_StopsRunningTask(t *testing.T) {
store := testStore(t)
runner := &mockRunner{delay: 5 * time.Second}
+ runners := map[string]Runner{"claude": runner}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
- pool := NewPool(2, runner, store, logger)
+ pool := NewPool(2, runners, store, logger)
tk := makeTask("cancel-1")
store.CreateTask(tk)
@@ -209,8 +215,9 @@ func TestPool_Cancel_StopsRunningTask(t *testing.T) {
func TestPool_Cancel_UnknownTask_ReturnsFalse(t *testing.T) {
store := testStore(t)
runner := &mockRunner{}
+ runners := map[string]Runner{"claude": runner}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
- pool := NewPool(2, runner, store, logger)
+ pool := NewPool(2, runners, store, logger)
if ok := pool.Cancel("nonexistent"); ok {
t.Error("Cancel returned true for unknown task")
@@ -220,8 +227,9 @@ func TestPool_Cancel_UnknownTask_ReturnsFalse(t *testing.T) {
func TestPool_AtCapacity(t *testing.T) {
store := testStore(t)
runner := &mockRunner{delay: time.Second}
+ runners := map[string]Runner{"claude": runner}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
- pool := NewPool(1, runner, store, logger)
+ pool := NewPool(1, runners, store, logger)
tk1 := makeTask("cap-1")
store.CreateTask(tk1)
@@ -265,8 +273,9 @@ func (m *logPatherMockRunner) Run(ctx context.Context, t *task.Task, e *storage.
func TestPool_Execute_LogPathsPreSetBeforeRun(t *testing.T) {
store := testStore(t)
runner := &logPatherMockRunner{logDir: t.TempDir()}
+ runners := map[string]Runner{"claude": runner}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
- pool := NewPool(2, runner, store, logger)
+ pool := NewPool(2, runners, store, logger)
tk := makeTask("lp-1")
store.CreateTask(tk)
@@ -296,8 +305,9 @@ func TestPool_Execute_LogPathsPreSetBeforeRun(t *testing.T) {
func TestPool_Execute_NoLogPather_PathsEmptyBeforeRun(t *testing.T) {
store := testStore(t)
runner := &mockRunner{} // does NOT implement LogPather
+ runners := map[string]Runner{"claude": runner}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
- pool := NewPool(2, runner, store, logger)
+ pool := NewPool(2, runners, store, logger)
tk := makeTask("nolp-1")
store.CreateTask(tk)
@@ -313,8 +323,9 @@ func TestPool_Execute_NoLogPather_PathsEmptyBeforeRun(t *testing.T) {
func TestPool_ConcurrentExecution(t *testing.T) {
store := testStore(t)
runner := &mockRunner{delay: 50 * time.Millisecond}
+ runners := map[string]Runner{"claude": runner}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
- pool := NewPool(3, runner, store, logger)
+ pool := NewPool(3, runners, store, logger)
for i := 0; i < 3; i++ {
tk := makeTask(fmt.Sprintf("cc-%d", i))
diff --git a/internal/executor/gemini.go b/internal/executor/gemini.go
index e4bd50e..3cabed5 100644
--- a/internal/executor/gemini.go
+++ b/internal/executor/gemini.go
@@ -10,7 +10,6 @@ import (
"strings"
"sync"
"syscall"
- "time"
"github.com/thepeterstone/claudomator/internal/storage"
"github.com/thepeterstone/claudomator/internal/task"
diff --git a/internal/executor/preamble.go b/internal/executor/preamble.go
index 71f8233..b20f7ea 100644
--- a/internal/executor/preamble.go
+++ b/internal/executor/preamble.go
@@ -23,11 +23,12 @@ Before doing any implementation work:
1. Estimate: will this task take more than 3 minutes of implementation effort?
-2. If YES — break it down into subtasks:
- - Create 3–7 discrete subtasks using the claudomator CLI, for example:
- claudomator create "Subtask name" --instructions "..." --working-dir "/path" --parent-id "$CLAUDOMATOR_TASK_ID" --server "$CLAUDOMATOR_API_URL"
- - Do NOT pass --start. Tasks will be queued and started in order by the operator.
+2. If YES — break it down:
+ - Create 3–7 discrete subtasks by POSTing to $CLAUDOMATOR_API_URL/api/tasks
+ - Each subtask POST body should be JSON with: name, agent.instructions, agent.working_dir (copy from current task), agent.model, agent.allowed_tools, and agent.skip_planning set to true
+ - Set parent_task_id to $CLAUDOMATOR_TASK_ID in each POST body
- After creating all subtasks, output a brief summary and STOP. Do not implement anything.
+ - You can also specify agent.type (either "claude" or "gemini") to choose the agent for subtasks.
3. If NO — proceed with the task instructions below.