diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-03-08 03:34:45 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-08 06:33:02 +0000 |
| commit | f887a6387946c8fc54f83a1f11a86d6c9d68dc50 (patch) | |
| tree | 5635025ce6300e1a8baedf123d05401d362247f6 | |
| parent | 306482ddc04c6bd6284f52727f396b19e6b8e867 (diff) | |
refactor(executor): update runners and tests for generic agents
| -rw-r--r-- | internal/executor/claude.go | 40 | ||||
| -rw-r--r-- | internal/executor/executor_test.go | 35 | ||||
| -rw-r--r-- | internal/executor/gemini.go | 1 | ||||
| -rw-r--r-- | internal/executor/preamble.go | 9 |
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. |
