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
125 lines
6.1 KiB
C#
125 lines
6.1 KiB
C#
using System.ComponentModel.DataAnnotations.Schema;
|
|
|
|
namespace ClaudeDo.Data.Models;
|
|
|
|
public enum TaskStatus
|
|
{
|
|
Idle,
|
|
Queued,
|
|
Running,
|
|
WaitingForReview,
|
|
WaitingForChildren,
|
|
Done,
|
|
Failed,
|
|
Cancelled,
|
|
}
|
|
|
|
public enum PlanningPhase
|
|
{
|
|
None,
|
|
Active,
|
|
Finalized,
|
|
}
|
|
|
|
public sealed class TaskEntity
|
|
{
|
|
public required string Id { get; init; }
|
|
// Global, monotonically increasing, never-reused display/lookup handle shown as `#123`.
|
|
// Allocated by TaskNumberAllocator from AppSettingsEntity.NextTaskNumber -- never MAX(number)+1,
|
|
// which would reissue a deleted task's number. The GUID Id stays the real identity everywhere else.
|
|
public int Number { get; set; }
|
|
public required string ListId { get; init; }
|
|
public required string Title { get; set; }
|
|
public string? Description { get; set; }
|
|
public TaskStatus Status { get; set; } = TaskStatus.Idle;
|
|
public PlanningPhase PlanningPhase { get; set; } = PlanningPhase.None;
|
|
public string? BlockedByTaskId { get; set; }
|
|
|
|
// User/MCP-declared predecessors, distinct from BlockedByTaskId (the planning chain's own
|
|
// internal link): the picker skips a Queued task until EVERY predecessor reached Done.
|
|
// Unlike the chain, a Failed/Cancelled dependency does NOT cascade or auto-resolve -- the
|
|
// dependent just stays blocked and reports why (see TaskStateService.SetDependsOnAsync and
|
|
// QueuePicker). Only loaded where a read path Includes it; the picker gates in raw SQL and
|
|
// never relies on this collection.
|
|
public ICollection<TaskDependencyEntity> Dependencies { get; set; } = new List<TaskDependencyEntity>();
|
|
|
|
[NotMapped]
|
|
public IReadOnlyList<string> DependsOnTaskIds =>
|
|
Dependencies.Select(d => d.DependsOnTaskId).ToList();
|
|
public DateTime? ScheduledFor { get; set; }
|
|
public string? Result { get; set; }
|
|
public string? ReviewFeedback { get; set; }
|
|
public int RoadblockCount { get; set; }
|
|
// Denormalized from the failing run, same pattern as RoadblockCount, so get_task/
|
|
// batch_get_tasks can report why a Failed task stopped without a second query.
|
|
// Null on a non-Failed task, or on a Failed task predating this field ("unknown").
|
|
public string? FailureReason { get; set; }
|
|
public int? FailureTurnsUsed { get; set; }
|
|
public int? FailureMaxTurns { get; set; }
|
|
// Set once QueueService's UsageLimitAutoContinueCoordinator fires ContinueTask for a
|
|
// FailureReason=="usage_limit" task. Guards against firing it a second time for the same
|
|
// task — never reset automatically, so auto-continue never loops on a repeat limit hit.
|
|
public DateTime? UsageLimitAutoContinuedAt { get; set; }
|
|
public string? LogPath { get; set; }
|
|
public required DateTime CreatedAt { get; init; }
|
|
public DateTime? StartedAt { get; set; }
|
|
public DateTime? FinishedAt { get; set; }
|
|
public string CommitType { get; set; } = CommitTypeRegistry.DefaultType;
|
|
public string? Model { get; set; }
|
|
public string? SystemPrompt { get; set; }
|
|
public string? AgentPath { get; set; }
|
|
public int? MaxTurns { get; set; }
|
|
// Null = inherit the list's config, then AppSettingsEntity.DefaultPermissionMode.
|
|
// One of PermissionModeRegistry.Modes.
|
|
public string? PermissionMode { get; set; }
|
|
public bool IsStarred { get; set; }
|
|
public bool IsMyDay { get; set; }
|
|
// Manual = a reminder only the user can do. Automation skips it (queue picker, daily prep,
|
|
// list handler) and the Claude affordances are hidden; a hand-driven ConPTY session is still
|
|
// allowed. New tasks in a manual list default to true.
|
|
public bool IsManual { get; set; }
|
|
// Set on verification/audit/research tasks whose correct outcome is an empty diff (e.g. "build
|
|
// and test, don't change code"). Lets the merge-helper session tell that apart from a task that
|
|
// silently failed to commit -- both look identical to preview_merge_set otherwise.
|
|
public bool ExpectsNoDiff { get; set; }
|
|
public string? Notes { get; set; }
|
|
public int SortOrder { get; set; }
|
|
public string? SessionSkills { get; set; }
|
|
|
|
// Verknüpftes Ticket im externen Ticketsystem, Format "<provider>:<id>" (aktuell immer
|
|
// "bandel:<ticketId>"). Gesetzt vom Ticket-Import, sonst null. Der Provider-Präfix existiert,
|
|
// damit ein zweites Ticketsystem später keine Datenmigration braucht.
|
|
public string? TicketRef { get; set; }
|
|
|
|
// Review range for a worktree-less task hosting an interactive "list handler" run
|
|
// (Mission Control's "Let Claude handle it"): the handler commits its own changes
|
|
// straight to the list's working dir, so there is no per-task worktree to diff. These
|
|
// capture the repo's HEAD at session start / submit-for-review instead, so the normal
|
|
// diff/get_task_diff paths can show `HandlerBaseCommit..HandlerHeadCommit` over the
|
|
// list's working dir exactly like a merged task's commit-range diff.
|
|
public string? HandlerBaseCommit { get; set; }
|
|
public string? HandlerHeadCommit { get; set; }
|
|
|
|
// The claude session id an embedded ConPTY interactive task session is (or was last)
|
|
// running under -- generated up front and persisted before launch so a closed/aborted
|
|
// session can be resumed even if the process never got past startup. Cleared implicitly
|
|
// whenever the task's worktree is recreated (a fresh worktree has nothing to resume into).
|
|
public string? InteractiveSessionId { get; set; }
|
|
|
|
public string? ParentTaskId { get; set; }
|
|
public string? PlanningSessionId { get; set; }
|
|
public string? PlanningSessionToken { get; set; }
|
|
public DateTime? PlanningFinalizedAt { get; set; }
|
|
|
|
public string? CreatedBy { get; set; }
|
|
|
|
// Navigation properties
|
|
public ListEntity List { get; set; } = null!;
|
|
public WorktreeEntity? Worktree { get; set; }
|
|
public ICollection<TaskRunEntity> Runs { get; set; } = new List<TaskRunEntity>();
|
|
public ICollection<SubtaskEntity> Subtasks { get; set; } = new List<SubtaskEntity>();
|
|
|
|
public TaskEntity? Parent { get; set; }
|
|
public ICollection<TaskEntity> Children { get; set; } = new List<TaskEntity>();
|
|
}
|