feat(data): add Idle/Cancelled status, PlanningPhase enum, BlockedByTaskId field
Slice 1 of the worker-state-and-queue-consolidation refactor — additive only,
no caller changes. Introduces the new orthogonal status model:
- TaskStatus gains canonical Idle and Cancelled values; legacy values
(Manual, Planning, Planned, Draft, Waiting) stay around until slice 6.
- New PlanningPhase enum (None/Active/Finalized) for parent tasks.
- New BlockedByTaskId FK on TaskEntity for sequential chain ordering;
ON DELETE SET NULL so orphaned children become pickable.
- EF migration adds planning_phase and blocked_by_task_id columns plus
the idx_tasks_blocked_by index. Also picks up an unrelated drift in
app_settings.default_permission_mode that had been changed in code
(commit 14cc9fb) without a migration.
This commit is contained in:
@@ -9,32 +9,65 @@ namespace ClaudeDo.Data.Configuration;
|
|||||||
public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
||||||
{
|
{
|
||||||
private static string StatusToString(TaskStatus v)
|
private static string StatusToString(TaskStatus v)
|
||||||
=> v == TaskStatus.Manual ? "manual"
|
=> v switch
|
||||||
: v == TaskStatus.Queued ? "queued"
|
{
|
||||||
: v == TaskStatus.Running ? "running"
|
TaskStatus.Idle => "idle",
|
||||||
: v == TaskStatus.Done ? "done"
|
TaskStatus.Queued => "queued",
|
||||||
: v == TaskStatus.Failed ? "failed"
|
TaskStatus.Running => "running",
|
||||||
: v == TaskStatus.Planning ? "planning"
|
TaskStatus.Done => "done",
|
||||||
: v == TaskStatus.Planned ? "planned"
|
TaskStatus.Failed => "failed",
|
||||||
: v == TaskStatus.Draft ? "draft"
|
TaskStatus.Cancelled => "cancelled",
|
||||||
: v == TaskStatus.Waiting ? "waiting"
|
// Legacy values — kept for compat until slice 6 retires them.
|
||||||
: throw new ArgumentOutOfRangeException(nameof(v));
|
TaskStatus.Manual => "manual",
|
||||||
|
TaskStatus.Planning => "planning",
|
||||||
|
TaskStatus.Planned => "planned",
|
||||||
|
TaskStatus.Draft => "draft",
|
||||||
|
TaskStatus.Waiting => "waiting",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(v)),
|
||||||
|
};
|
||||||
|
|
||||||
private static TaskStatus StatusFromString(string v)
|
private static TaskStatus StatusFromString(string v)
|
||||||
=> v == "manual" ? TaskStatus.Manual
|
=> v switch
|
||||||
: v == "queued" ? TaskStatus.Queued
|
{
|
||||||
: v == "running" ? TaskStatus.Running
|
"idle" => TaskStatus.Idle,
|
||||||
: v == "done" ? TaskStatus.Done
|
"queued" => TaskStatus.Queued,
|
||||||
: v == "failed" ? TaskStatus.Failed
|
"running" => TaskStatus.Running,
|
||||||
: v == "planning" ? TaskStatus.Planning
|
"done" => TaskStatus.Done,
|
||||||
: v == "planned" ? TaskStatus.Planned
|
"failed" => TaskStatus.Failed,
|
||||||
: v == "draft" ? TaskStatus.Draft
|
"cancelled" => TaskStatus.Cancelled,
|
||||||
: v == "waiting" ? TaskStatus.Waiting
|
// Legacy values — kept for compat until slice 6 retires them.
|
||||||
: throw new ArgumentOutOfRangeException(nameof(v));
|
"manual" => TaskStatus.Manual,
|
||||||
|
"planning" => TaskStatus.Planning,
|
||||||
|
"planned" => TaskStatus.Planned,
|
||||||
|
"draft" => TaskStatus.Draft,
|
||||||
|
"waiting" => TaskStatus.Waiting,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(v)),
|
||||||
|
};
|
||||||
|
|
||||||
private static readonly ValueConverter<TaskStatus, string> StatusConverter =
|
private static readonly ValueConverter<TaskStatus, string> StatusConverter =
|
||||||
new(v => StatusToString(v), v => StatusFromString(v));
|
new(v => StatusToString(v), v => StatusFromString(v));
|
||||||
|
|
||||||
|
private static string PhaseToString(PlanningPhase v)
|
||||||
|
=> v switch
|
||||||
|
{
|
||||||
|
PlanningPhase.None => "none",
|
||||||
|
PlanningPhase.Active => "active",
|
||||||
|
PlanningPhase.Finalized => "finalized",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(v)),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static PlanningPhase PhaseFromString(string v)
|
||||||
|
=> v switch
|
||||||
|
{
|
||||||
|
"none" => PlanningPhase.None,
|
||||||
|
"active" => PlanningPhase.Active,
|
||||||
|
"finalized" => PlanningPhase.Finalized,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(v)),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly ValueConverter<PlanningPhase, string> PhaseConverter =
|
||||||
|
new(v => PhaseToString(v), v => PhaseFromString(v));
|
||||||
|
|
||||||
public void Configure(EntityTypeBuilder<TaskEntity> builder)
|
public void Configure(EntityTypeBuilder<TaskEntity> builder)
|
||||||
{
|
{
|
||||||
builder.ToTable("tasks");
|
builder.ToTable("tasks");
|
||||||
@@ -46,6 +79,9 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
|||||||
builder.Property(t => t.Description).HasColumnName("description");
|
builder.Property(t => t.Description).HasColumnName("description");
|
||||||
builder.Property(t => t.Status).HasColumnName("status").IsRequired()
|
builder.Property(t => t.Status).HasColumnName("status").IsRequired()
|
||||||
.HasConversion(StatusConverter);
|
.HasConversion(StatusConverter);
|
||||||
|
builder.Property(t => t.PlanningPhase).HasColumnName("planning_phase").IsRequired()
|
||||||
|
.HasConversion(PhaseConverter).HasDefaultValue(PlanningPhase.None);
|
||||||
|
builder.Property(t => t.BlockedByTaskId).HasColumnName("blocked_by_task_id");
|
||||||
builder.Property(t => t.ScheduledFor).HasColumnName("scheduled_for");
|
builder.Property(t => t.ScheduledFor).HasColumnName("scheduled_for");
|
||||||
builder.Property(t => t.Result).HasColumnName("result");
|
builder.Property(t => t.Result).HasColumnName("result");
|
||||||
builder.Property(t => t.LogPath).HasColumnName("log_path");
|
builder.Property(t => t.LogPath).HasColumnName("log_path");
|
||||||
@@ -73,6 +109,12 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
|||||||
.HasForeignKey(t => t.ParentTaskId)
|
.HasForeignKey(t => t.ParentTaskId)
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
// BlockedBy: predecessor in a sequential chain. SetNull on delete so child becomes pickable.
|
||||||
|
builder.HasOne<TaskEntity>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(t => t.BlockedByTaskId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
builder.HasOne(t => t.List)
|
builder.HasOne(t => t.List)
|
||||||
.WithMany(l => l.Tasks)
|
.WithMany(l => l.Tasks)
|
||||||
.HasForeignKey(t => t.ListId)
|
.HasForeignKey(t => t.ListId)
|
||||||
@@ -97,5 +139,6 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
|||||||
builder.HasIndex(t => t.Status).HasDatabaseName("idx_tasks_status");
|
builder.HasIndex(t => t.Status).HasDatabaseName("idx_tasks_status");
|
||||||
builder.HasIndex(t => new { t.ListId, t.SortOrder }).HasDatabaseName("idx_tasks_list_sort");
|
builder.HasIndex(t => new { t.ListId, t.SortOrder }).HasDatabaseName("idx_tasks_list_sort");
|
||||||
builder.HasIndex(t => t.ParentTaskId).HasDatabaseName("idx_tasks_parent_task_id");
|
builder.HasIndex(t => t.ParentTaskId).HasDatabaseName("idx_tasks_parent_task_id");
|
||||||
|
builder.HasIndex(t => t.BlockedByTaskId).HasDatabaseName("idx_tasks_blocked_by");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddPlanningPhaseAndBlockedBy : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "blocked_by_task_id",
|
||||||
|
table: "tasks",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "planning_phase",
|
||||||
|
table: "tasks",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "none");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "app_settings",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "default_permission_mode",
|
||||||
|
value: "auto");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx_tasks_blocked_by",
|
||||||
|
table: "tasks",
|
||||||
|
column: "blocked_by_task_id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_tasks_tasks_blocked_by_task_id",
|
||||||
|
table: "tasks",
|
||||||
|
column: "blocked_by_task_id",
|
||||||
|
principalTable: "tasks",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_tasks_tasks_blocked_by_task_id",
|
||||||
|
table: "tasks");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "idx_tasks_blocked_by",
|
||||||
|
table: "tasks");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "blocked_by_task_id",
|
||||||
|
table: "tasks");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "planning_phase",
|
||||||
|
table: "tasks");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "app_settings",
|
||||||
|
keyColumn: "id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "default_permission_mode",
|
||||||
|
value: "bypassPermissions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,7 +84,7 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
DefaultClaudeInstructions = "",
|
DefaultClaudeInstructions = "",
|
||||||
DefaultMaxTurns = 100,
|
DefaultMaxTurns = 100,
|
||||||
DefaultModel = "sonnet",
|
DefaultModel = "sonnet",
|
||||||
DefaultPermissionMode = "bypassPermissions",
|
DefaultPermissionMode = "auto",
|
||||||
WorktreeAutoCleanupDays = 7,
|
WorktreeAutoCleanupDays = 7,
|
||||||
WorktreeAutoCleanupEnabled = false,
|
WorktreeAutoCleanupEnabled = false,
|
||||||
WorktreeStrategy = "sibling"
|
WorktreeStrategy = "sibling"
|
||||||
@@ -225,6 +225,10 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("agent_path");
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<string>("BlockedByTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("blocked_by_task_id");
|
||||||
|
|
||||||
b.Property<string>("CommitType")
|
b.Property<string>("CommitType")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -285,6 +289,13 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("planning_finalized_at");
|
.HasColumnName("planning_finalized_at");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningPhase")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("none")
|
||||||
|
.HasColumnName("planning_phase");
|
||||||
|
|
||||||
b.Property<string>("PlanningSessionId")
|
b.Property<string>("PlanningSessionId")
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("planning_session_id");
|
.HasColumnName("planning_session_id");
|
||||||
@@ -327,6 +338,9 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BlockedByTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_blocked_by");
|
||||||
|
|
||||||
b.HasIndex("ListId")
|
b.HasIndex("ListId")
|
||||||
.HasDatabaseName("idx_tasks_list_id");
|
.HasDatabaseName("idx_tasks_list_id");
|
||||||
|
|
||||||
@@ -519,6 +533,11 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
{
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BlockedByTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
.WithMany("Tasks")
|
.WithMany("Tasks")
|
||||||
.HasForeignKey("ListId")
|
.HasForeignKey("ListId")
|
||||||
|
|||||||
@@ -2,17 +2,31 @@ namespace ClaudeDo.Data.Models;
|
|||||||
|
|
||||||
public enum TaskStatus
|
public enum TaskStatus
|
||||||
{
|
{
|
||||||
Manual,
|
// Lifecycle (canonical values).
|
||||||
|
Idle,
|
||||||
Queued,
|
Queued,
|
||||||
Running,
|
Running,
|
||||||
Done,
|
Done,
|
||||||
Failed,
|
Failed,
|
||||||
|
Cancelled,
|
||||||
|
|
||||||
|
// Legacy values — kept for backwards compatibility while the worker
|
||||||
|
// layer is migrated to (Status, PlanningPhase, BlockedByTaskId).
|
||||||
|
// Removed in slice 6 of the worker-state-and-queue-consolidation refactor.
|
||||||
|
Manual,
|
||||||
Planning,
|
Planning,
|
||||||
Planned,
|
Planned,
|
||||||
Draft,
|
Draft,
|
||||||
Waiting,
|
Waiting,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum PlanningPhase
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Active,
|
||||||
|
Finalized,
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class TaskEntity
|
public sealed class TaskEntity
|
||||||
{
|
{
|
||||||
public required string Id { get; init; }
|
public required string Id { get; init; }
|
||||||
@@ -20,6 +34,8 @@ public sealed class TaskEntity
|
|||||||
public required string Title { get; set; }
|
public required string Title { get; set; }
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public TaskStatus Status { get; set; } = TaskStatus.Manual;
|
public TaskStatus Status { get; set; } = TaskStatus.Manual;
|
||||||
|
public PlanningPhase PlanningPhase { get; set; } = PlanningPhase.None;
|
||||||
|
public string? BlockedByTaskId { get; set; }
|
||||||
public DateTime? ScheduledFor { get; set; }
|
public DateTime? ScheduledFor { get; set; }
|
||||||
public string? Result { get; set; }
|
public string? Result { get; set; }
|
||||||
public string? LogPath { get; set; }
|
public string? LogPath { get; set; }
|
||||||
|
|||||||
Reference in New Issue
Block a user