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
43 lines
1.6 KiB
C#
43 lines
1.6 KiB
C#
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
|
|
|
namespace ClaudeDo.Worker.External;
|
|
|
|
/// <summary>
|
|
/// Renders the `blockedReason` string shared by <see cref="ExternalMcpService"/> and
|
|
/// <see cref="TaskWaitMcpTools"/>. Lives here so the two can't drift on the wording an agent
|
|
/// parses.
|
|
/// </summary>
|
|
internal static class TaskBlocking
|
|
{
|
|
/// <summary>
|
|
/// The predecessors of <paramref name="dependencyIds"/> that have not reached Done — an id
|
|
/// missing from <paramref name="statuses"/> counts as unmet (the picker treats a dangling
|
|
/// edge as blocking too).
|
|
/// </summary>
|
|
public static List<(string Id, TaskStatus? Status)> Unmet(
|
|
IEnumerable<string> dependencyIds, IReadOnlyDictionary<string, TaskStatus> statuses)
|
|
{
|
|
var unmet = new List<(string Id, TaskStatus? Status)>();
|
|
foreach (var id in dependencyIds)
|
|
{
|
|
if (statuses.TryGetValue(id, out var status))
|
|
{
|
|
if (status != TaskStatus.Done) unmet.Add((id, status));
|
|
}
|
|
else
|
|
{
|
|
unmet.Add((id, null));
|
|
}
|
|
}
|
|
return unmet;
|
|
}
|
|
|
|
/// <summary>Names every open predecessor, not just the first — a task can wait on n of them.</summary>
|
|
public static string Describe(IReadOnlyList<(string Id, TaskStatus? Status)> unmet)
|
|
{
|
|
var noun = unmet.Count == 1 ? "task" : "tasks";
|
|
var parts = unmet.Select(u => $"{u.Id} (status: {(u.Status?.ToString() ?? "not found")})");
|
|
return $"Blocked: depends on {noun} {string.Join(", ", parts)}.";
|
|
}
|
|
}
|