Ein Task kann jetzt auf beliebig viele Vorgaenger warten. Die Einzelspalte tasks.depends_on_task_id wird durch die Beziehungstabelle task_dependencies ersetzt (Composite-PK, beide FKs ON DELETE CASCADE -- das reproduziert das alte ON DELETE SET NULL: geloeschter Vorgaenger = Kante weg = Task wieder pickbar). BlockedByTaskId bleibt unberuehrt. - Migration AddTaskDependencies kopiert jede bestehende depends_on_task_id verlustfrei in die Tabelle, bevor die Spalte fallengelassen wird - QueuePicker gatet per doppelt negiertem NOT EXISTS: kein Task mit 0 Kanten wird blockiert, alle Kanten muessen done sein, eine Kante auf einen verschwundenen Task blockiert weiter - SetDependsOnAsync ersetzt die ganze Menge; Zykluspruefung ist jetzt eine BFS ueber den Graphen, faengt also auch A->B->C->A - blockedReason nennt alle offenen Vorgaenger (External/TaskBlocking, von ExternalMcpService und TaskWaitMcpTools gemeinsam genutzt) - add_task/update_task nehmen dependsOnTaskIds; die alte Skalarform bleibt als Ein-Element-Menge inkl. ""-Clear-Sentinel gueltig - UI: Fan-in-Zeile bleibt flach und listet alle Vorgaenger im After-Label, die lineare Kette rendert unveraendert
146 lines
7.6 KiB
C#
146 lines
7.6 KiB
C#
using ClaudeDo.Data.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
|
|
|
namespace ClaudeDo.Data.Configuration;
|
|
|
|
public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
|
{
|
|
private static string StatusToString(TaskStatus v)
|
|
=> v switch
|
|
{
|
|
TaskStatus.Idle => "idle",
|
|
TaskStatus.Queued => "queued",
|
|
TaskStatus.Running => "running",
|
|
TaskStatus.WaitingForReview => "waiting_for_review",
|
|
TaskStatus.WaitingForChildren => "waiting_for_children",
|
|
TaskStatus.Done => "done",
|
|
TaskStatus.Failed => "failed",
|
|
TaskStatus.Cancelled => "cancelled",
|
|
_ => throw new ArgumentOutOfRangeException(nameof(v)),
|
|
};
|
|
|
|
private static TaskStatus StatusFromString(string v)
|
|
=> v switch
|
|
{
|
|
"idle" => TaskStatus.Idle,
|
|
"queued" => TaskStatus.Queued,
|
|
"running" => TaskStatus.Running,
|
|
"waiting_for_review" => TaskStatus.WaitingForReview,
|
|
"waiting_for_children" => TaskStatus.WaitingForChildren,
|
|
"done" => TaskStatus.Done,
|
|
"failed" => TaskStatus.Failed,
|
|
"cancelled" => TaskStatus.Cancelled,
|
|
_ => throw new ArgumentOutOfRangeException(nameof(v)),
|
|
};
|
|
|
|
private static readonly ValueConverter<TaskStatus, string> StatusConverter =
|
|
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)
|
|
{
|
|
builder.ToTable("tasks");
|
|
|
|
builder.HasKey(t => t.Id);
|
|
builder.Property(t => t.Id).HasColumnName("id");
|
|
builder.Property(t => t.Number).HasColumnName("number").IsRequired();
|
|
builder.Property(t => t.ListId).HasColumnName("list_id").IsRequired();
|
|
builder.Property(t => t.Title).HasColumnName("title").IsRequired();
|
|
builder.Property(t => t.Description).HasColumnName("description");
|
|
builder.Property(t => t.Status).HasColumnName("status").IsRequired()
|
|
.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.Result).HasColumnName("result");
|
|
builder.Property(t => t.ReviewFeedback).HasColumnName("review_feedback");
|
|
builder.Property(t => t.RoadblockCount).HasColumnName("roadblock_count").HasDefaultValue(0);
|
|
builder.Property(t => t.FailureReason).HasColumnName("failure_reason");
|
|
builder.Property(t => t.FailureTurnsUsed).HasColumnName("failure_turns_used");
|
|
builder.Property(t => t.FailureMaxTurns).HasColumnName("failure_max_turns");
|
|
builder.Property(t => t.UsageLimitAutoContinuedAt).HasColumnName("usage_limit_auto_continued_at");
|
|
builder.Property(t => t.LogPath).HasColumnName("log_path");
|
|
builder.Property(t => t.CreatedAt).HasColumnName("created_at").IsRequired();
|
|
builder.Property(t => t.StartedAt).HasColumnName("started_at");
|
|
builder.Property(t => t.FinishedAt).HasColumnName("finished_at");
|
|
builder.Property(t => t.CommitType).HasColumnName("commit_type").IsRequired().HasDefaultValue("chore");
|
|
builder.Property(t => t.Model).HasColumnName("model");
|
|
builder.Property(t => t.SystemPrompt).HasColumnName("system_prompt");
|
|
builder.Property(t => t.AgentPath).HasColumnName("agent_path");
|
|
builder.Property(t => t.MaxTurns).HasColumnName("max_turns");
|
|
builder.Property(t => t.PermissionMode).HasColumnName("permission_mode");
|
|
builder.Property(t => t.IsStarred).HasColumnName("is_starred").HasDefaultValue(false);
|
|
builder.Property(t => t.IsMyDay).HasColumnName("is_my_day").HasDefaultValue(false);
|
|
builder.Property(t => t.IsManual).HasColumnName("is_manual").HasDefaultValue(false);
|
|
builder.Property(t => t.ExpectsNoDiff).HasColumnName("expects_no_diff").HasDefaultValue(false);
|
|
builder.Property(t => t.Notes).HasColumnName("notes");
|
|
builder.Property(t => t.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
|
|
builder.Property(t => t.SessionSkills).HasColumnName("session_skills");
|
|
builder.Property(t => t.HandlerBaseCommit).HasColumnName("handler_base_commit");
|
|
builder.Property(t => t.HandlerHeadCommit).HasColumnName("handler_head_commit");
|
|
builder.Property(t => t.InteractiveSessionId).HasColumnName("interactive_session_id");
|
|
builder.Property(t => t.TicketRef).HasColumnName("ticket_ref");
|
|
|
|
builder.Property(t => t.ParentTaskId).HasColumnName("parent_task_id");
|
|
builder.Property(t => t.PlanningSessionId).HasColumnName("planning_session_id");
|
|
builder.Property(t => t.PlanningSessionToken).HasColumnName("planning_session_token");
|
|
builder.Property(t => t.PlanningFinalizedAt).HasColumnName("planning_finalized_at");
|
|
|
|
builder.Property(t => t.CreatedBy).HasColumnName("created_by");
|
|
|
|
builder.HasOne(t => t.Parent)
|
|
.WithMany(t => t.Children)
|
|
.HasForeignKey(t => t.ParentTaskId)
|
|
.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);
|
|
|
|
// DependsOn edges live in task_dependencies (a task can declare n predecessors) —
|
|
// configured in TaskDependencyEntityConfiguration, which also owns the Dependencies nav.
|
|
|
|
builder.HasOne(t => t.List)
|
|
.WithMany(l => l.Tasks)
|
|
.HasForeignKey(t => t.ListId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
|
|
builder.HasOne(t => t.Worktree)
|
|
.WithOne(w => w.Task)
|
|
.HasForeignKey<WorktreeEntity>(w => w.TaskId);
|
|
|
|
builder.HasIndex(t => t.Number).IsUnique().HasDatabaseName("idx_tasks_number");
|
|
builder.HasIndex(t => t.ListId).HasDatabaseName("idx_tasks_list_id");
|
|
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 => t.ParentTaskId).HasDatabaseName("idx_tasks_parent_task_id");
|
|
builder.HasIndex(t => t.BlockedByTaskId).HasDatabaseName("idx_tasks_blocked_by");
|
|
}
|
|
}
|