refactor(data): retire legacy TaskStatus values and backfill existing rows

Slice 6 of the worker state and queue consolidation refactor.

* Drop Manual, Planning, Planned, Draft, Waiting from the TaskStatus enum
  and from the EF value converter; only the lifecycle values remain
  (Idle, Queued, Running, Done, Failed, Cancelled).
* Add migration RetireLegacyTaskStatus that rewrites existing rows:
  manual/draft -> idle, planning -> idle+planning_phase=active,
  planned -> idle+planning_phase=finalized, waiting -> queued+blocked_by
  derived from sort_order via a CTE with LAG().
* Reroute every call site that compared/set legacy values to the new
  three-field model (Status + PlanningPhase + BlockedByTaskId), including
  the planning repo helpers, MCP services, the planning chain coordinator,
  and the UI view-models. TaskRowViewModel now exposes PlanningPhase to
  drive the planning badge.
* Refresh Worker/CLAUDE.md and Data/CLAUDE.md, the docs/plan.md status
  section, and the planning verification notes in docs/open.md.
This commit is contained in:
Mika Kuns
2026-04-27 15:28:55 +02:00
parent ff7c239959
commit dc3fc443b4
37 changed files with 306 additions and 229 deletions

View File

@@ -176,7 +176,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
ct.ThrowIfCancellationRequested();
static bool IsPlanningStatus(TaskStatus s) => s == TaskStatus.Planning || s == TaskStatus.Planned;
static bool IsPlanningParent(TaskEntity t) => t.PlanningPhase != PlanningPhase.None;
IEnumerable<TaskEntity> filtered = list.Kind switch
{
@@ -185,10 +185,10 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
ListKind.Smart when list.Id == "smart:planned" => all.Where(t => t.ScheduledFor != null),
ListKind.Virtual when list.Id == "virtual:queued" => all.Where(t =>
(t.Status == TaskStatus.Queued && t.ParentTaskId == null) ||
(IsPlanningStatus(t.Status) && all.Any(c => c.ParentTaskId == t.Id && (c.Status == TaskStatus.Queued || c.Status == TaskStatus.Waiting)))),
(IsPlanningParent(t) && all.Any(c => c.ParentTaskId == t.Id && c.Status == TaskStatus.Queued))),
ListKind.Virtual when list.Id == "virtual:running" => all.Where(t =>
(t.Status == TaskStatus.Running && t.ParentTaskId == null) ||
(IsPlanningStatus(t.Status) && all.Any(c => c.ParentTaskId == t.Id && c.Status == TaskStatus.Running))),
(IsPlanningParent(t) && all.Any(c => c.ParentTaskId == t.Id && c.Status == TaskStatus.Running))),
ListKind.Virtual when list.Id == "virtual:review" => all.Where(t => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active && t.ParentTaskId == null),
ListKind.User => all.Where(t => $"user:{t.ListId}" == list.Id),
_ => Enumerable.Empty<TaskEntity>(),
@@ -437,7 +437,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
if (entity != null)
{
entity.Status = row.Done ? TaskStatus.Done : TaskStatus.Manual;
entity.Status = row.Done ? TaskStatus.Done : TaskStatus.Idle;
row.Status = entity.Status;
await db.SaveChangesAsync();
}
@@ -493,18 +493,16 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
if (entity is null) return;
// For a planning parent the dequeue button targets queued/waiting children,
// not the parent itself (whose Status is Planning/Planned).
if (entity.Status == TaskStatus.Planning || entity.Status == TaskStatus.Planned
|| entity.PlanningPhase != PlanningPhase.None)
// For a planning parent the dequeue button targets queued children
// (chain-blocked or not), not the parent itself.
if (entity.PlanningPhase != PlanningPhase.None)
{
var children = await db.Tasks
.Where(t => t.ParentTaskId == row.Id
&& (t.Status == TaskStatus.Queued || t.Status == TaskStatus.Waiting))
.Where(t => t.ParentTaskId == row.Id && t.Status == TaskStatus.Queued)
.ToListAsync();
foreach (var c in children)
{
c.Status = TaskStatus.Manual;
c.Status = TaskStatus.Idle;
c.BlockedByTaskId = null;
}
await db.SaveChangesAsync();
@@ -513,7 +511,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
var childRow = Items.FirstOrDefault(r => r.Id == c.Id);
if (childRow is not null)
{
childRow.Status = TaskStatus.Manual;
childRow.Status = TaskStatus.Idle;
childRow.BlockedByTaskId = null;
}
}
@@ -521,9 +519,9 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
}
else
{
entity.Status = TaskStatus.Manual;
entity.Status = TaskStatus.Idle;
await db.SaveChangesAsync();
row.Status = TaskStatus.Manual;
row.Status = TaskStatus.Idle;
}
Regroup();
UpdateSubtitle();
@@ -565,7 +563,8 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
[RelayCommand]
private async Task OpenPlanningSessionAsync(TaskRowViewModel? row)
{
if (row is null || row.Status != TaskStatus.Manual) return;
if (row is null) return;
if (row.Status != TaskStatus.Idle || row.PlanningPhase != PlanningPhase.None) return;
ForegroundHelper.AllowAny();
try { await _worker!.StartPlanningSessionAsync(row.Id); }
catch { }