Add TUI kanban board, review workflow, and new task statuses

Replace task statuses (pending/running/succeeded/failed/cancelled) with
a kanban workflow: todo → in_progress → in_review → done/wontdo.

When a non-review agent task completes, the API auto-creates a child
review task and moves the parent to in_review. Only humans can move
tasks from in_review to done/wontdo via the TUI.

New components:
- cmd/tui: bubbletea kanban board with $EDITOR integration
- POST /api/v1/tasks/{id}/complete: agent completion callback
- Operator --api-url flag for completion callbacks
- ProviderQueue sets tasks to in_progress on pickup
- AgentTask reconciler calls /complete on job finish
This commit is contained in:
2026-06-12 22:47:40 +10:00
parent 1552c7fc66
commit 8f48dd838b
24 changed files with 1566 additions and 19 deletions
+69 -4
View File
@@ -2,6 +2,7 @@ package database
import (
"context"
"fmt"
"strconv"
"time"
@@ -24,7 +25,7 @@ func (db *DB) CreateTask(ctx context.Context, req models.CreateTaskRequest) (*mo
ExtraTools: req.ExtraTools,
ParentTaskID: req.ParentTaskID,
PoolRef: req.PoolRef,
Status: models.StatusPending,
Status: models.StatusTodo,
}
if task.ExtraTools == nil {
task.ExtraTools = []string{}
@@ -79,7 +80,7 @@ func (db *DB) GetTask(ctx context.Context, id string) (*models.Task, error) {
}
func (db *DB) ListPendingTasks(ctx context.Context) ([]models.Task, error) {
return db.listTasksByStatus(ctx, string(models.StatusPending))
return db.listTasksByStatus(ctx, string(models.StatusTodo))
}
func (db *DB) listTasksByStatus(ctx context.Context, status string) ([]models.Task, error) {
@@ -130,13 +131,13 @@ func (db *DB) ListTasks(ctx context.Context, status string, repository string) (
}
func (db *DB) UpdateTaskStatus(ctx context.Context, id string, req models.UpdateTaskRequest) error {
if req.Status == models.StatusRunning {
if req.Status == models.StatusInProgress {
_, err := db.Pool.Exec(ctx, `
UPDATE tasks SET status = $2, job_name = COALESCE(NULLIF($3, ''), job_name), started_at = NOW()
WHERE id = $1`, id, req.Status, req.JobName)
return err
}
if req.Status == models.StatusSucceeded || req.Status == models.StatusFailed {
if req.Status == models.StatusDone || req.Status == models.StatusWontdo {
_, err := db.Pool.Exec(ctx, `
UPDATE tasks SET status = $2, result = COALESCE(NULLIF($3, ''), result),
error_message = COALESCE(NULLIF($4, ''), error_message), completed_at = NOW()
@@ -147,6 +148,70 @@ func (db *DB) UpdateTaskStatus(ctx context.Context, id string, req models.Update
return err
}
func (db *DB) CompleteTask(ctx context.Context, id string, req models.CompleteTaskRequest) (*models.Task, error) {
task, err := db.GetTask(ctx, id)
if err != nil {
return nil, err
}
if req.ErrorMessage != "" {
_, err := db.Pool.Exec(ctx, `
UPDATE tasks SET status = 'todo', error_message = $2, completed_at = NOW()
WHERE id = $1`, id, req.ErrorMessage)
if err != nil {
return nil, err
}
task.Status = models.StatusTodo
task.ErrorMessage = req.ErrorMessage
return task, nil
}
if req.Result != "" {
_, err := db.Pool.Exec(ctx, `
UPDATE tasks SET result = $2 WHERE id = $1`, id, req.Result)
if err != nil {
return nil, err
}
task.Result = req.Result
}
if task.Command != "review" {
_, err := db.Pool.Exec(ctx, `
UPDATE tasks SET status = 'in_review', completed_at = NOW()
WHERE id = $1`, id)
if err != nil {
return nil, err
}
task.Status = models.StatusInReview
reviewTask := models.CreateTaskRequest{
Command: "review",
Repository: task.Repository,
Ref: task.Ref,
IssueNumber: task.IssueNumber,
PRNumber: task.PRNumber,
Body: task.Body,
Author: task.Author,
ParentTaskID: task.ID,
PoolRef: task.PoolRef,
}
if _, err := db.CreateTask(ctx, reviewTask); err != nil {
return nil, fmt.Errorf("create review task: %w", err)
}
return task, nil
}
_, err = db.Pool.Exec(ctx, `
UPDATE tasks SET status = 'in_review', completed_at = NOW()
WHERE id = $1`, id)
if err != nil {
return nil, err
}
task.Status = models.StatusInReview
return task, nil
}
func scanTasks(rows pgx.Rows) ([]models.Task, error) {
var tasks []models.Task
for rows.Next() {