From 6511d6e0ff139495413c7848a9b4aabb9d9ee4e2 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Wed, 4 Mar 2026 21:25:34 +0000 Subject: Add READY state for human-in-the-loop verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Top-level tasks now land in READY after successful execution instead of going directly to COMPLETED. Subtasks (with parent_task_id) skip the gate and remain COMPLETED. Users accept or reject via new API endpoints: POST /api/tasks/{id}/accept → READY → COMPLETED POST /api/tasks/{id}/reject → READY → PENDING (with rejection_comment) - task: add StateReady, RejectionComment field, update ValidTransition - storage: migrate rejection_comment column, add RejectTask method - executor: route top-level vs subtask to READY vs COMPLETED - api: /accept and /reject handlers with 409 on invalid state Co-Authored-By: Claude Sonnet 4.6 --- internal/task/task.go | 11 +++++++---- internal/task/task_test.go | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) (limited to 'internal/task') diff --git a/internal/task/task.go b/internal/task/task.go index d360a07..3e74a82 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -8,6 +8,7 @@ const ( StatePending State = "PENDING" StateQueued State = "QUEUED" StateRunning State = "RUNNING" + StateReady State = "READY" StateCompleted State = "COMPLETED" StateFailed State = "FAILED" StateTimedOut State = "TIMED_OUT" @@ -53,9 +54,10 @@ type Task struct { Priority Priority `yaml:"priority" json:"priority"` Tags []string `yaml:"tags" json:"tags"` DependsOn []string `yaml:"depends_on" json:"depends_on"` - State State `yaml:"-" json:"state"` - CreatedAt time.Time `yaml:"-" json:"created_at"` - UpdatedAt time.Time `yaml:"-" json:"updated_at"` + State State `yaml:"-" json:"state"` + RejectionComment string `yaml:"-" json:"rejection_comment,omitempty"` + CreatedAt time.Time `yaml:"-" json:"created_at"` + UpdatedAt time.Time `yaml:"-" json:"updated_at"` } // Duration wraps time.Duration for YAML unmarshaling from strings like "30m". @@ -90,7 +92,8 @@ func ValidTransition(from, to State) bool { transitions := map[State][]State{ StatePending: {StateQueued, StateCancelled}, StateQueued: {StateRunning, StateCancelled}, - StateRunning: {StateCompleted, StateFailed, StateTimedOut, StateCancelled, StateBudgetExceeded}, + StateRunning: {StateReady, StateCompleted, StateFailed, StateTimedOut, StateCancelled, StateBudgetExceeded}, + StateReady: {StateCompleted, StatePending}, StateFailed: {StateQueued}, // retry StateTimedOut: {StateQueued}, // retry } diff --git a/internal/task/task_test.go b/internal/task/task_test.go index a8e0a84..6498271 100644 --- a/internal/task/task_test.go +++ b/internal/task/task_test.go @@ -45,6 +45,7 @@ func TestValidTransition_DisallowedTransitions(t *testing.T) { {"completed to queued", StateCompleted, StateQueued}, {"failed to completed", StateFailed, StateCompleted}, {"timed out to completed", StateTimedOut, StateCompleted}, + {"ready to queued", StateReady, StateQueued}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -55,6 +56,19 @@ func TestValidTransition_DisallowedTransitions(t *testing.T) { } } +func TestValidTransition_ReadyState(t *testing.T) { + valid := []struct{ from, to State }{ + {StateRunning, StateReady}, + {StateReady, StateCompleted}, + {StateReady, StatePending}, + } + for _, tt := range valid { + if !ValidTransition(tt.from, tt.to) { + t.Errorf("expected transition %s -> %s to be valid", tt.from, tt.to) + } + } +} + func TestDuration_UnmarshalYAML(t *testing.T) { var d Duration unmarshal := func(v interface{}) error { -- cgit v1.2.3