feat(planning): gate subtask queueing behind plan finalization
Planning subtasks are now "Draft" until their parent plan is finalized, then "Planned" (queueable). Finalizing a plan no longer auto-queues the child chain; the user sends the plan to the queue explicitly. - TaskStateService rejects a child entering Queued/Running unless its parent is Finalized; this single invariant covers UI, queue, RunNow and MCP paths - WorkerHub.SetTaskStatus routes Queued through the gated EnqueueAsync - Finalize call sites pass queueAgentTasks: false - PlanningChainCoordinator.QueuePlanAsync guards the chain build on Finalized - TaskRowViewModel derives Draft/Planned from ParentFinalized; gates CanSendToQueue / CanQueuePlan; view shows a PLANNED badge Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -115,7 +115,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
{
|
||||
try
|
||||
{
|
||||
await _planningChain.SetupChainAsync(parentTaskId, Context.ConnectionAborted);
|
||||
await _planningChain.QueuePlanAsync(parentTaskId, Context.ConnectionAborted);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
@@ -380,7 +380,11 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
{
|
||||
if (!Enum.TryParse<TaskStatus>(status, ignoreCase: true, out var parsed))
|
||||
throw new HubException($"unknown status: {status}");
|
||||
var result = await _state.ForceSetStatusAsync(taskId, parsed, Context.ConnectionAborted);
|
||||
// Queueing goes through the gated transition so draft subtasks can't be queued;
|
||||
// other statuses keep the unconditional "set status freely" affordance.
|
||||
var result = parsed == TaskStatus.Queued
|
||||
? await _state.EnqueueAsync(taskId, Context.ConnectionAborted)
|
||||
: await _state.ForceSetStatusAsync(taskId, parsed, Context.ConnectionAborted);
|
||||
if (!result.Ok) throw new HubException(result.Reason ?? "set status failed");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user