diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-09 03:59:51 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-09 03:59:51 +0000 |
| commit | 0fc0c1e6ee0f106da7b91da7847c163936128051 (patch) | |
| tree | eb4c137070c0b3e8d8ffe956645a41cc89ab2a8e | |
| parent | ad00e1865c19afd40c8d159dbb55668ed6b51b12 (diff) | |
style: gofmt executor.go, executor_test.go, task.go
Pre-existing gofmt debt on all three files (confirmed dirty before
this session's nested-subtask-completion commit too, not a
regression) -- cleaned up while already touching these files.
| -rw-r--r-- | internal/executor/executor.go | 38 | ||||
| -rw-r--r-- | internal/executor/executor_test.go | 32 | ||||
| -rw-r--r-- | internal/task/task.go | 65 |
3 files changed, 70 insertions, 65 deletions
diff --git a/internal/executor/executor.go b/internal/executor/executor.go index 2505280..88a5783 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -11,6 +11,7 @@ import ( "sync" "time" + "github.com/google/uuid" "github.com/thepeterstone/claudomator/internal/event" "github.com/thepeterstone/claudomator/internal/llm" "github.com/thepeterstone/claudomator/internal/retry" @@ -18,7 +19,6 @@ import ( "github.com/thepeterstone/claudomator/internal/storage" "github.com/thepeterstone/claudomator/internal/story" "github.com/thepeterstone/claudomator/internal/task" - "github.com/google/uuid" ) // Store is the subset of storage.DB methods used by the Pool. @@ -88,8 +88,8 @@ type Pool struct { runners map[string]Runner store Store logger *slog.Logger - depPollInterval time.Duration // how often waitForDependencies polls; defaults to 5s - requeueDelay time.Duration // how long to wait before requeuing a blocked-per-agent task; defaults to 30s + depPollInterval time.Duration // how often waitForDependencies polls; defaults to 5s + requeueDelay time.Duration // how long to wait before requeuing a blocked-per-agent task; defaults to 30s mu sync.Mutex active int @@ -101,15 +101,15 @@ type Pool struct { // so repeated round_robin tier resolutions (see selectRung) actually // rotate across candidates rather than always picking the first one. roleTierIndex map[string]int - closed bool // set to true when Shutdown has been called - resultCh chan *Result - startedCh chan string // task IDs that just transitioned to RUNNING - workCh chan workItem // internal bounded queue; Submit enqueues here - doneCh chan struct{} // signals when a worker slot is freed - workerWg sync.WaitGroup // tracks in-flight execute/executeResume goroutines - dispatchDone chan struct{} // closed when the dispatch goroutine exits - Classifier *Classifier - LLM *llm.Client + closed bool // set to true when Shutdown has been called + resultCh chan *Result + startedCh chan string // task IDs that just transitioned to RUNNING + workCh chan workItem // internal bounded queue; Submit enqueues here + doneCh chan struct{} // signals when a worker slot is freed + workerWg sync.WaitGroup // tracks in-flight execute/executeResume goroutines + dispatchDone chan struct{} // closed when the dispatch goroutine exits + Classifier *Classifier + LLM *llm.Client // Budget gates paid providers against a rolling window; nil disables gating. Budget BudgetGate } @@ -166,9 +166,11 @@ func (p *Pool) dispatch() { p.active++ p.mu.Unlock() if item.exec != nil { - p.workerWg.Add(1); go func(i workItem) { defer p.workerWg.Done(); p.executeResume(i.ctx, i.task, i.exec) }(item) + p.workerWg.Add(1) + go func(i workItem) { defer p.workerWg.Done(); p.executeResume(i.ctx, i.task, i.exec) }(item) } else { - p.workerWg.Add(1); go func(i workItem) { defer p.workerWg.Done(); p.execute(i.ctx, i.task) }(item) + p.workerWg.Add(1) + go func(i workItem) { defer p.workerWg.Done(); p.execute(i.ctx, i.task) }(item) } break } @@ -255,10 +257,10 @@ func (p *Pool) Cancel(taskID string) bool { // resumablePoolStates are the task states that may be submitted for session resume. var resumablePoolStates = map[task.State]bool{ - task.StateBlocked: true, - task.StateTimedOut: true, - task.StateCancelled: true, - task.StateFailed: true, + task.StateBlocked: true, + task.StateTimedOut: true, + task.StateCancelled: true, + task.StateFailed: true, task.StateBudgetExceeded: true, } diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go index d89a9eb..e2b9b44 100644 --- a/internal/executor/executor_test.go +++ b/internal/executor/executor_test.go @@ -109,7 +109,7 @@ func makeTask(id string) *task.Task { now := time.Now().UTC() return &task.Task{ ID: id, Name: "Test " + id, - Agent: task.AgentConfig{Type: "claude", Instructions: "test"}, + Agent: task.AgentConfig{Type: "claude", Instructions: "test"}, Priority: task.PriorityNormal, Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "linear"}, Tags: []string{}, @@ -1206,10 +1206,13 @@ func TestPool_Submit_ParentNotBlocked_NoTransition(t *testing.T) { // minimalMockStore is a standalone Store implementation for unit-testing Pool // methods that do not require a real SQLite database. type minimalMockStore struct { - mu sync.Mutex - tasks map[string]*task.Task - executions map[string]*storage.Execution - stateUpdates []struct{ id string; state task.State } + mu sync.Mutex + tasks map[string]*task.Task + executions map[string]*storage.Execution + stateUpdates []struct { + id string + state task.State + } questionUpdates []string summaryUpdates []struct{ taskID, summary string } changestatCalls []struct { @@ -1245,7 +1248,7 @@ func (m *minimalMockStore) ListSubtasks(parentID string) ([]*task.Task, error) { return nil, nil } func (m *minimalMockStore) ListExecutions(_ string) ([]*storage.Execution, error) { return nil, nil } -func (m *minimalMockStore) CreateExecution(e *storage.Execution) error { return nil } +func (m *minimalMockStore) CreateExecution(e *storage.Execution) error { return nil } func (m *minimalMockStore) CreateExecutionAndSetRunning(e *storage.Execution) error { return nil } @@ -1257,7 +1260,10 @@ func (m *minimalMockStore) UpdateTaskState(id string, newState task.State) error return m.updateStateErr } m.mu.Lock() - m.stateUpdates = append(m.stateUpdates, struct{ id string; state task.State }{id, newState}) + m.stateUpdates = append(m.stateUpdates, struct { + id string + state task.State + }{id, newState}) if t, ok := m.tasks[id]; ok { t.State = newState } @@ -1298,10 +1304,10 @@ func (m *minimalMockStore) UpdateExecutionChangestats(execID string, stats *task m.mu.Unlock() return nil } -func (m *minimalMockStore) RecordAgentEvent(_ storage.AgentEvent) error { return nil } -func (m *minimalMockStore) GetProject(_ string) (*task.Project, error) { return nil, nil } -func (m *minimalMockStore) CreateTask(_ *task.Task) error { return nil } -func (m *minimalMockStore) CreateEvent(_ *event.Event) error { return nil } +func (m *minimalMockStore) RecordAgentEvent(_ storage.AgentEvent) error { return nil } +func (m *minimalMockStore) GetProject(_ string) (*task.Project, error) { return nil, nil } +func (m *minimalMockStore) CreateTask(_ *task.Task) error { return nil } +func (m *minimalMockStore) CreateEvent(_ *event.Event) error { return nil } func (m *minimalMockStore) GetActiveRoleConfig(_ string) (*storage.RoleConfigRow, error) { return nil, sql.ErrNoRows } @@ -1709,7 +1715,7 @@ func TestPool_MaxPerAgent_BlocksSecondTask(t *testing.T) { runners := map[string]Runner{"claude": runner} logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) pool := NewPool(2, runners, store, logger) // pool size 2, but maxPerAgent=1 - pool.requeueDelay = 50 * time.Millisecond // speed up test + pool.requeueDelay = 50 * time.Millisecond // speed up test tk1 := makeTask("mpa-1") tk2 := makeTask("mpa-2") @@ -1838,7 +1844,6 @@ func TestPool_ConsecutiveFailures_ResetOnSuccess(t *testing.T) { } } - // TestPool_DependsOn_NoDeadlock verifies that a task waiting for a dependency // does NOT hold the per-agent slot, allowing the dependency to run first. func TestPool_DependsOn_NoDeadlock(t *testing.T) { @@ -1975,4 +1980,3 @@ func TestPool_Shutdown_TimesOut(t *testing.T) { } close(unblock) // cleanup } - diff --git a/internal/task/task.go b/internal/task/task.go index a95107a..3745e94 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -7,14 +7,14 @@ import ( type State string const ( - StatePending State = "PENDING" - StateQueued State = "QUEUED" - StateRunning State = "RUNNING" - StateReady State = "READY" - StateCompleted State = "COMPLETED" - StateFailed State = "FAILED" - StateTimedOut State = "TIMED_OUT" - StateCancelled State = "CANCELLED" + StatePending State = "PENDING" + StateQueued State = "QUEUED" + StateRunning State = "RUNNING" + StateReady State = "READY" + StateCompleted State = "COMPLETED" + StateFailed State = "FAILED" + StateTimedOut State = "TIMED_OUT" + StateCancelled State = "CANCELLED" StateBudgetExceeded State = "BUDGET_EXCEEDED" StateBlocked State = "BLOCKED" ) @@ -28,8 +28,8 @@ const ( ) type AgentConfig struct { - Type string `yaml:"type" json:"type"` - Model string `yaml:"model" json:"model"` + Type string `yaml:"type" json:"type"` + Model string `yaml:"model" json:"model"` // Role, when non-empty, names an internal/role.RoleConfig to dispatch // through: internal/executor.Pool.execute() resolves tier 0 of the // active role_configs row's EscalationLadder for the initial dispatch @@ -37,7 +37,7 @@ type AgentConfig struct { // walks the ladder on failure (retry-then-escalate). Tasks with // Role == "" (every existing YAML/chatbot task shape) are completely // unaffected — this is purely additive. - Role string `yaml:"role,omitempty" json:"role,omitempty"` + Role string `yaml:"role,omitempty" json:"role,omitempty"` ContextFiles []string `yaml:"context_files" json:"context_files"` Instructions string `yaml:"instructions" json:"instructions"` ContainerImage string `yaml:"container_image" json:"container_image"` @@ -56,7 +56,6 @@ type AgentConfig struct { MaxTokens int `yaml:"max_tokens,omitempty" json:"max_tokens,omitempty"` } - type RetryConfig struct { MaxAttempts int `yaml:"max_attempts" json:"max_attempts"` Backoff string `yaml:"backoff" json:"backoff"` // "linear", "exponential" @@ -84,34 +83,34 @@ type Interaction struct { } type Task struct { - ID string `yaml:"id" json:"id"` - ParentTaskID string `yaml:"parent_task_id" json:"parent_task_id"` - Name string `yaml:"name" json:"name"` - Description string `yaml:"description" json:"description"` - Project string `yaml:"project" json:"project"` // Human-readable project name + ID string `yaml:"id" json:"id"` + ParentTaskID string `yaml:"parent_task_id" json:"parent_task_id"` + Name string `yaml:"name" json:"name"` + Description string `yaml:"description" json:"description"` + Project string `yaml:"project" json:"project"` // Human-readable project name RepositoryURL string `yaml:"repository_url" json:"repository_url"` - Agent AgentConfig `yaml:"agent" json:"agent"` - Timeout Duration `yaml:"timeout" json:"timeout"` - Retry RetryConfig `yaml:"retry" json:"retry"` - Priority Priority `yaml:"priority" json:"priority"` - Tags []string `yaml:"tags" json:"tags"` - DependsOn []string `yaml:"depends_on" json:"depends_on"` + Agent AgentConfig `yaml:"agent" json:"agent"` + Timeout Duration `yaml:"timeout" json:"timeout"` + Retry RetryConfig `yaml:"retry" json:"retry"` + Priority Priority `yaml:"priority" json:"priority"` + Tags []string `yaml:"tags" json:"tags"` + DependsOn []string `yaml:"depends_on" json:"depends_on"` // AcceptanceCriteria, when non-empty, states what this task's work must // satisfy — set by a decomposing parent via spawn_subtask's // acceptance_criteria parameter (mirrors story.Story.AcceptanceCriteria // exactly). Unused by execution itself; a later phase's arbitrated-review // generalization reads it when evaluating this task's work, falling back // to the enclosing story's own AcceptanceCriteria when empty. - AcceptanceCriteria []string `yaml:"acceptance_criteria" json:"acceptance_criteria"` - BranchName string `yaml:"-" json:"branch_name,omitempty"` - State State `yaml:"-" json:"state"` - RejectionComment string `yaml:"-" json:"rejection_comment,omitempty"` - QuestionJSON string `yaml:"-" json:"question,omitempty"` - ElaborationInput string `yaml:"-" json:"elaboration_input,omitempty"` - Summary string `yaml:"-" json:"summary,omitempty"` - Interactions []Interaction `yaml:"-" json:"interactions,omitempty"` - CreatedAt time.Time `yaml:"-" json:"created_at"` - UpdatedAt time.Time `yaml:"-" json:"updated_at"` + AcceptanceCriteria []string `yaml:"acceptance_criteria" json:"acceptance_criteria"` + BranchName string `yaml:"-" json:"branch_name,omitempty"` + State State `yaml:"-" json:"state"` + RejectionComment string `yaml:"-" json:"rejection_comment,omitempty"` + QuestionJSON string `yaml:"-" json:"question,omitempty"` + ElaborationInput string `yaml:"-" json:"elaboration_input,omitempty"` + Summary string `yaml:"-" json:"summary,omitempty"` + Interactions []Interaction `yaml:"-" json:"interactions,omitempty"` + CreatedAt time.Time `yaml:"-" json:"created_at"` + UpdatedAt time.Time `yaml:"-" json:"updated_at"` // NeedsReview is set by internal/scheduler.Scheduler when it resumes a // BLOCKED role-typed task past its ask_user-timeout with a system-authored // fallback answer rather than a real human answer — a flag for a human to |
