8.1 KiB
Waiting for Review — Task State — Design
Date: 2026-06-01
Status: Approved (brainstorming)
Scope: ClaudeDo.Data (TaskEntity, EF config + migration), ClaudeDo.Worker (TaskStateService, TaskRunner, QueueService, WorkerHub, ExternalMcpService), ClaudeDo.Ui (StatusColorConverter, TaskRowViewModel, views), CLAUDE.md docs
Problem
A successful task run currently transitions straight to Done and is considered complete. There is no gate for a human (or another agent) to review the result before it is accepted. We want review to be a mandatory step: after a successful run a task waits for an explicit approval, and a reviewer can send it back with feedback for another turn.
Goals
- Add a
WaitingForReviewlifecycle state that a task enters automatically after a successful run. - Reviewer can approve (→
Done), reject-and-re-run (→Queued, resuming the same Claude session with required feedback), reject-and-park (→Idle), or cancel (→Cancelled). - Reject-and-re-run reuses the existing session-resume mechanism so the agent continues with full context.
- Both the desktop UI and the external MCP surface can perform review actions.
Non-Goals
- No change to the failure path: a failed run still goes straight to
Failed, never toWaitingForReview. - No change to planning-phase finalization. A planning parent that generates child tasks keeps its current behavior and does not route through review. Only ordinary executable runs (
Running→ success) are affected. - No change to worktree state flow (
Active | Merged | Discarded | Kept). - No change to the in-run auto-retry-on-failure behavior; only the final successful completion routes to review.
Design
1. State machine
Changed/added transitions in bold:
| From | To | Trigger |
|---|---|---|
| Idle | Queued | enqueue (unchanged) |
| Queued | Running | queue picker claim (unchanged) |
| Running | WaitingForReview | successful run (was → Done) |
| Running | Failed | failed run (unchanged) |
| Running | Cancelled | cancel during run (unchanged) |
| WaitingForReview | Done | approve |
| WaitingForReview | Queued | reject + required feedback → resume re-run |
| WaitingForReview | Idle | reject → park for manual edit |
| WaitingForReview | Cancelled | abandon an almost-done task |
| Done | Failed | Cancelled | Idle | reset (unchanged) |
2. Data model
ClaudeDo.Data:
TaskStatusenum (Models/TaskEntity.cs): addWaitingForReviewafterRunning.- EF string converter (
Configuration/TaskEntityConfiguration.cs): mapWaitingForReview⇄"waiting_for_review"(TEXT column, no schema constraint to change). - New nullable column
ReviewFeedback : string?onTaskEntity. Holds the reviewer's rejection comment until the re-run consumes it, then it is cleared. Persisted so it survives a worker restart and is visible to the UI. - One EF migration: add the
review_feedbackcolumn. No backfill — the new status value and column are only written going forward.
3. Worker — status transitions (State/TaskStateService.cs)
TaskStateService remains the sole owner of status writes. New/changed methods:
SubmitForReviewAsync(taskId)—Running→WaitingForReview. SetsFinishedAtandResultexactly asCompleteAsyncdoes today. Called byTaskRunneron success instead ofCompleteAsync. (CompleteAsyncis retained for the approve path.)ApproveReviewAsync(taskId)—WaitingForReview→Done.RejectToQueueAsync(taskId, feedback)—WaitingForReview→Queued. Rejects empty/whitespace feedback with a failedTransitionResult. StoresfeedbackinReviewFeedback. Wakes the queue.RejectToIdleAsync(taskId)—WaitingForReview→Idle. Parks for manual editing; leavesResultintact, clearsReviewFeedback.CancelAsync— extend the allowed source states to includeWaitingForReview.
Each transition broadcasts TaskUpdated as today. Invalid source states return a failed TransitionResult (no throw), matching existing convention.
4. Resume-aware re-run (Queue/QueueService.cs)
The queue picker still atomically claims a Queued, unblocked task (UPDATE … SET status='running' … RETURNING *). The RETURNING row already carries ReviewFeedback. After a successful claim, QueueService branches:
ReviewFeedbackset + latest run has aSessionId→TaskRunner.ContinueAsync(task, feedback)—--resume {sessionId}withfeedbackas the next-turn prompt.ReviewFeedbackset, no priorSessionId(edge case) →TaskRunner.RunAsyncwith the feedback appended to the task prompt, so the comment is not lost.- No
ReviewFeedback→ normalTaskRunner.RunAsync(fresh session).
ReviewFeedback is cleared once consumed (single UPDATE), so a later re-run does not re-apply stale feedback.
5. External MCP surface (External/ExternalMcpService.cs)
- New tool
review_task(taskId, decision, feedback?),decision ∈ {approve, reject_rerun, reject_park, cancel}.feedbackis required whendecision = reject_rerun(validation error otherwise). Maps onto theTaskStateServicemethods in §3. This lets automation / other agents act as reviewers. get_task_status_values— addWaitingForReviewwith a description covering the four exit actions.list_tasksstatus-filter parsing and validation message — includeWaitingForReview.get_tasklifecycle description text — update toIdle → Queued → Running → WaitingForReview → Done | Failed | Cancelled.update_task_statusstays restricted toIdleandQueued; all review decisions go throughreview_task(keeps the "set status freely" affordance and the review affordance distinct).
6. Worker hub (Hub/WorkerHub.cs + Hub/HubBroadcaster.cs)
New hub methods called by the UI, each delegating to TaskStateService:
ApproveReview(taskId)RejectReviewToQueue(taskId, feedback)RejectReviewToIdle(taskId)
Cancel already exists. No new broadcast events — TaskUpdated covers it.
7. UI (ClaudeDo.Ui)
Converters/StatusColorConverter.cs: add awaiting_for_reviewcase. Snap to an existing color token from the scale; final visual pass is left to the user (per project convention — centralize/tokenize, user does the visual pass).ViewModels/Islands/TaskRowViewModel.cs: addIsWaitingForReviewcomputed property and commands Approve, RejectRerun, RejectPark, Cancel (the last reuses the existing cancel command). Commands are enabled only whenStatus == WaitingForReview.- Reject-Rerun opens a small flyout/dialog with a required multi-line feedback text box; on confirm it calls
RejectReviewToQueue(taskId, feedback). - Wire the commands to the new SignalR client methods.
8. Docs
Update the status flow in:
- root
CLAUDE.md— "Task status flow" line. src/ClaudeDo.Data/CLAUDE.md— TaskEntity status list.src/ClaudeDo.Worker/CLAUDE.md— status-model transition table.
Testing
ClaudeDo.Worker.Tests (real SQLite + real git, existing harness):
SubmitForReviewAsync: a successful run lands inWaitingForReview, notDone.ApproveReviewAsync:WaitingForReview→Done.RejectToQueueAsync: empty feedback rejected; valid feedback stored inReviewFeedbackand status →Queued.RejectToIdleAsync: →Idle,Resultpreserved,ReviewFeedbackcleared.CancelAsyncfromWaitingForReview→Cancelled.- Invalid source states (e.g. approve from
Idle) return a failedTransitionResult. - Resume-aware re-run: a task with
ReviewFeedback+ a priorSessionId, when claimed, resumes the session with the feedback as the prompt and clearsReviewFeedback. review_taskMCP tool: each decision maps to the correct transition;reject_rerunwithout feedback errors.
Open questions
None outstanding. Planning-task exclusion (Non-Goals) is the one assumption to verify against the planning-finalization code path during implementation; if planning finalization shares CompleteAsync, route only the executable-run success site through SubmitForReviewAsync.