11 KiB
Waiting for Review — Task State — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a WaitingForReview lifecycle state that standalone tasks enter after a successful run, with approve / reject-rerun / reject-park / cancel exits, exposed via UI and MCP.
Architecture: New enum value + nullable ReviewFeedback column. TaskStateService gains review transitions. TaskRunner.HandleSuccess routes standalone-task success to review. QueueService.RunInSlotAsync resumes the Claude session when re-running a rejected task. New MCP review_task tool + UI commands.
Tech Stack: .NET 8, EF Core (SQLite, TEXT enum), SignalR, Avalonia MVVM, xUnit.
Scope decision (locked): Only standalone tasks (ParentTaskId == null) route to WaitingForReview. Planning child tasks continue to Done on success so the sequential planning chain (which advances on terminal states) is unaffected. Flagged for user confirmation.
Task 1: Data layer — enum, converter, column
Files:
-
Modify:
src/ClaudeDo.Data/Models/TaskEntity.cs -
Modify:
src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs -
Create: EF migration via CLI
-
Step 1: Add
WaitingForReviewtoTaskStatusenum (afterRunning) and addpublic string? ReviewFeedback { get; set; }toTaskEntity. -
Step 2: In
TaskEntityConfiguration, addTaskStatus.WaitingForReview => "waiting_for_review"toStatusToStringand"waiting_for_review" => TaskStatus.WaitingForReviewtoStatusFromString; map the column:builder.Property(t => t.ReviewFeedback).HasColumnName("review_feedback"); -
Step 3: Create migration:
dotnet ef migrations add AddReviewFeedback --project src/ClaudeDo.Data/ClaudeDo.Data.csproj. Verify it only adds thereview_feedbackTEXT column (nullable). Ifdotnet efunavailable, hand-write the migration + designer following the latest migration inMigrations/. -
Step 4: Build
dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj. Expected: success. -
Step 5: Commit.
Task 2: Worker — review transitions in TaskStateService
Files:
- Modify:
src/ClaudeDo.Worker/State/TaskStateService.cs - Modify:
src/ClaudeDo.Worker/State/Interfaces/ITaskStateService.cs(add new method signatures) - Test:
tests/ClaudeDo.Worker.Tests/...(state transition tests)
New methods (all return TransitionResult, broadcast TaskUpdated):
-
SubmitForReviewAsync(taskId, finishedAt, result, ct)— guardStatus == Running; setStatus=WaitingForReview, FinishedAt, Result. Does NOT callOnChildTerminalAsync(review is non-terminal; only invoked for standalone tasks anyway). -
ApproveReviewAsync(taskId, ct)— guardStatus == WaitingForReview; setStatus=Done. -
RejectToQueueAsync(taskId, feedback, ct)— reject empty/whitespace feedback (TransitionResult(false, "Feedback is required to reject for re-run.")); guardStatus == WaitingForReview; setStatus=Queued, ReviewFeedback=feedback;_waker.Wake(). -
RejectToIdleAsync(taskId, ct)— guardStatus == WaitingForReview; setStatus=Idle, ReviewFeedback=null(leaveResultintact). -
ClearReviewFeedbackAsync(taskId, ct)— setReviewFeedback=null(no status change, no guard); used by the runner after consuming feedback. -
Extend
CancelAsyncguard:(Status == Running || Status == Queued || Status == WaitingForReview). -
Step 1: Write failing tests in a new
tests/ClaudeDo.Worker.Tests/State/ReviewTransitionTests.cs(follow existing TaskStateService test setup). Cover: submit-for-review from Running; approve from WaitingForReview→Done; reject-to-queue stores feedback + status Queued; empty feedback rejected; reject-to-idle clears feedback + keeps Result; cancel from WaitingForReview→Cancelled; invalid (approve from Idle) returns!Ok. -
Step 2: Run tests, expect FAIL (methods missing).
-
Step 3: Implement the methods + interface signatures + CancelAsync guard.
-
Step 4: Run tests, expect PASS.
-
Step 5: Commit.
Task 3: Worker — route standalone success to review
Files:
-
Modify:
src/ClaudeDo.Worker/Runner/TaskRunner.cs(HandleSuccess) -
Step 1: In
HandleSuccess, after commit, branch:var finishedAt = DateTime.UtcNow; if (task.ParentTaskId is null) { await _state.SubmitForReviewAsync(task.Id, finishedAt, result.ResultMarkdown, CancellationToken.None); await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (waiting for review)", WorkerLogLevel.Success, DateTime.UtcNow); await _broadcaster.TaskFinished(slot, task.Id, "waiting_for_review", finishedAt); } else { await _state.CompleteAsync(task.Id, finishedAt, result.ResultMarkdown, CancellationToken.None); await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (done)", WorkerLogLevel.Success, DateTime.UtcNow); await _broadcaster.TaskFinished(slot, task.Id, "done", finishedAt); } -
Step 2: Build worker. Expected: success.
-
Step 3: Commit.
Task 4: Worker — resume-aware re-run in QueueService
Files:
-
Modify:
src/ClaudeDo.Worker/Queue/QueueService.cs(RunInSlotAsync) -
Test:
tests/ClaudeDo.Worker.Tests/... -
Step 1: In
RunInSlotAsync, after loadingtask:if (!string.IsNullOrWhiteSpace(task.ReviewFeedback)) { var feedback = task.ReviewFeedback!; string? sessionId; using (var ctx = _dbFactory.CreateDbContext()) sessionId = (await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId, ct))?.SessionId; await _state.ClearReviewFeedbackAsync(taskId, ct); // inject ITaskStateService if (sessionId is not null) { await _runner.ContinueAsync(taskId, feedback, "queue", ct); return; } task.Description = string.IsNullOrWhiteSpace(task.Description) ? $"Reviewer feedback: {feedback}" : $"{task.Description}\n\nReviewer feedback: {feedback}"; } await _runner.RunAsync(task, "queue", ct);Inject
ITaskStateService _stateintoQueueService(add to ctor + DI already provides it). -
Step 2: Build worker, expect success.
-
Step 3: Commit.
Task 5: MCP — review_task tool + status reference
Files:
-
Modify:
src/ClaudeDo.Worker/External/ExternalMcpService.cs -
Step 1: Add
review_tasktool:[McpServerTool, Description( "Review a task that is WaitingForReview. decision: 'approve' (→ Done), " + "'reject_rerun' (→ Queued, resumes the agent session with feedback — feedback required), " + "'reject_park' (→ Idle for manual editing), 'cancel' (→ Cancelled). ")] public async Task<TaskDto> ReviewTask(string taskId, string decision, string? feedback, CancellationToken cancellationToken) { var task = await _tasks.GetByIdAsync(taskId, cancellationToken) ?? throw new InvalidOperationException($"Task {taskId} not found."); TransitionResult r = decision.ToLowerInvariant() switch { "approve" => await _state.ApproveReviewAsync(taskId, cancellationToken), "reject_rerun" => await _state.RejectToQueueAsync(taskId, feedback ?? "", cancellationToken), "reject_park" => await _state.RejectToIdleAsync(taskId, cancellationToken), "cancel" => await _state.CancelAsync(taskId, DateTime.UtcNow, cancellationToken), _ => throw new InvalidOperationException($"Unknown decision '{decision}'. Use approve, reject_rerun, reject_park, or cancel."), }; if (!r.Ok) throw new InvalidOperationException(r.Reason ?? "Review action failed."); return ToDto((await _tasks.GetByIdAsync(taskId, cancellationToken))!); } -
Step 2: Add
WaitingForReviewtoGetTaskStatusValueslist; update the validation strings inListTasksand the lifecycle text inGetTask/UpdateTaskStatusto includeWaitingForReview. -
Step 3: Build worker, expect success.
-
Step 4: Commit.
Task 6: UI — client + hub methods
Files:
-
Modify:
src/ClaudeDo.Worker/Hub/WorkerHub.cs -
Modify:
src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs -
Modify:
src/ClaudeDo.Ui/Services/WorkerClient.cs -
Modify:
tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs -
Step 1: Hub: add
ApproveReview(taskId),RejectReviewToQueue(taskId, feedback),RejectReviewToIdle(taskId),CancelReview(taskId)— each calls the matching_statemethod viaHubGuard-style mapping (if (!result.Ok) throw new HubException(...)). -
Step 2:
IWorkerClient+WorkerClient: addApproveReviewAsync,RejectReviewToQueueAsync(taskId, feedback),RejectReviewToIdleAsync,CancelReviewAsyncinvoking the hub methods. Add no-op/stub impls toStubWorkerClient. -
Step 3: Build App + Ui.Tests. Expected: success.
-
Step 4: Commit.
Task 7: UI — converter, row VM, view buttons
Files:
-
Modify:
src/ClaudeDo.Ui/Converters/StatusColorConverter.cs -
Modify:
src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs -
Modify:
src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs(commands) -
Modify: the task row/detail AXAML to surface Approve / Reject / Park / Cancel when
IsWaitingForReview -
Step 1:
StatusColorConverter: add"waiting_for_review" => Brushes.MediumPurple,(placeholder — user does visual pass). -
Step 2:
TaskRowViewModel: addpublic bool IsWaitingForReview => Status == TaskStatus.WaitingForReview;, raise it inOnStatusChanged, and add(TaskStatus.WaitingForReview, _) => "review"toStatusChipClass. -
Step 3:
TasksIslandViewModel: add relay commandsApproveReview,RejectReviewRerun(prompts for feedback),RejectReviewPark,CancelReviewoperating on the selected/target row, calling the new client methods. -
Step 4: Add buttons to the relevant view bound to those commands, visible when
IsWaitingForReview. Reject-rerun uses a text-input flyout/dialog for required feedback. -
Step 5: Build App + Ui.Tests. Expected: success. (Visual layout: flagged for user's visual pass — cannot render here.)
-
Step 6: Commit.
Task 8: Docs + full verification
Files:
-
Modify: root
CLAUDE.md,src/ClaudeDo.Data/CLAUDE.md,src/ClaudeDo.Worker/CLAUDE.md -
Step 1: Update status flow lines + worker transition table to include
WaitingForReviewand the new transitions. -
Step 2: Build all projects (csproj individually —
.slnxneeds .NET 9) and rundotnet test tests/ClaudeDo.Worker.Tests,tests/ClaudeDo.Ui.Tests,tests/ClaudeDo.Data.Tests. Expected: all green. -
Step 3: Commit.
Self-Review notes
- Spec coverage: §1 state machine → Tasks 2,3; §2 data → Task 1; §3 transitions → Task 2; §4 resume → Task 4; §5 MCP → Task 5; §6 hub → Task 6; §7 UI → Tasks 6,7; §8 docs → Task 8; testing → Tasks 2,4,8.
- Method names consistent across tasks:
SubmitForReviewAsync,ApproveReviewAsync,RejectToQueueAsync,RejectToIdleAsync,ClearReviewFeedbackAsync(state);ApproveReview/RejectReviewToQueue/RejectReviewToIdle/CancelReview(hub);ApproveReviewAsync/RejectReviewToQueueAsync/RejectReviewToIdleAsync/CancelReviewAsync(client).