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:
mika kuns
2026-05-29 14:41:48 +02:00
parent 09a930e28e
commit ce79a2d0fe
10 changed files with 223 additions and 9 deletions

View File

@@ -244,6 +244,16 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
foreach (var r in Items)
r.HasQueuedSubtasks = parentsWithQueuedKids.Contains(r.Id);
// A subtask is "Planned" (queueable) once its planning parent is finalized;
// until then it is a "Draft".
var finalizedParents = Items
.Where(r => r.PlanningPhase == PlanningPhase.Finalized)
.Select(r => r.Id)
.ToHashSet();
foreach (var r in Items)
r.ParentFinalized = !string.IsNullOrEmpty(r.ParentTaskId)
&& finalizedParents.Contains(r.ParentTaskId!);
Regroup();
UpdateSubtitle();
}
@@ -645,7 +655,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
await _worker.ResumePlanningSessionAsync(row.Id);
break;
case UnfinishedPlanningModalResult.FinalizeNow:
await _worker.FinalizePlanningSessionAsync(row.Id);
await _worker.FinalizePlanningSessionAsync(row.Id, queueAgentTasks: false);
break;
case UnfinishedPlanningModalResult.Discard:
await TryDiscardPlanningWithRetryAsync(row.Id);
@@ -713,7 +723,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
private async Task FinalizePlanningSessionAsync(TaskRowViewModel? row)
{
if (row is null) return;
try { await _worker!.FinalizePlanningSessionAsync(row.Id, queueAgentTasks: true); }
try { await _worker!.FinalizePlanningSessionAsync(row.Id, queueAgentTasks: false); }
catch { }
}