summaryrefslogtreecommitdiff
path: root/internal/task
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-04 21:25:34 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-04 21:25:34 +0000
commit6511d6e0ff139495413c7848a9b4aabb9d9ee4e2 (patch)
tree95bd6a0efc0ace206a5716da62a5956491cb46e7 /internal/task
parent3962597950421e422b6e1ce57764550f5600ded6 (diff)
Add READY state for human-in-the-loop verification
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 <noreply@anthropic.com>
Diffstat (limited to 'internal/task')
-rw-r--r--internal/task/task.go11
-rw-r--r--internal/task/task_test.go14
2 files changed, 21 insertions, 4 deletions
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 {