Merge branch 'main' into feat/poweruse-list-handler

Kollision mit #264 (Handoff-Budget im Brief sichtbar machen): beide Seiten haben
HandoffRoundTracker, HandoffMcpTools und InteractiveLaunchSpecService angefasst.

Der Konflikt war inhaltlich, nicht nur textuell. #264 liest den Cap an zwei
Stellen aus der Konstante (GetStatus und das roundInfo-Rendering), waehrend
dieser Branch ihn zum Parameter aus app_settings gemacht hat. Ein reiner
Text-Merge haette kompiliert und trotzdem gelogen: Register haette den
konfigurierten Cap durchgesetzt, waehrend der Brief dem Modell weiter "2 Runden
uebrig" gemeldet haette, obwohl es 39 hat.

Aufgeloest so, dass der Cap eine einzige Quelle hat:
- GetStatus(taskId, maxHandoffs) nimmt den Cap als Parameter, mit demselben
  Clamp wie Register. Die Rechnerei liegt damit an einer Stelle.
- GetHandoffStatus liest das Setting und gibt es weiter.
- Das roundInfo-Rendering benutzt GetStatus statt eigener Arithmetik.

Zwei Tests nageln genau diesen Drift fest, damit er beim naechsten Merge nicht
unbemerkt zurueckkommt: einer auf GetHandoffStatus, einer auf den Brief.

MaxHandoffs -> DefaultMaxHandoffs in #264s vier Test-Referenzen; die laufen alle
mit Default-Settings, die Semantik bleibt.
This commit is contained in:
Mika Kuns
2026-09-02 15:48:43 +02:00
41 changed files with 2323 additions and 225 deletions
@@ -0,0 +1,13 @@
namespace ClaudeDo.Data.Models;
/// <summary>
/// One declared "task must run after task" edge. A task can carry n of them; the queue picker
/// claims it only once every predecessor reached <see cref="TaskStatus.Done"/>.
/// Deliberately separate from <see cref="TaskEntity.BlockedByTaskId"/>, the planning chain's own
/// internal single link — see ClaudeDo.Worker/CLAUDE.md → Status Model.
/// </summary>
public sealed class TaskDependencyEntity
{
public required string TaskId { get; init; }
public required string DependsOnTaskId { get; init; }
}
+13 -6
View File
@@ -1,3 +1,5 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace ClaudeDo.Data.Models;
public enum TaskStatus
@@ -33,12 +35,17 @@ public sealed class TaskEntity
public PlanningPhase PlanningPhase { get; set; } = PlanningPhase.None;
public string? BlockedByTaskId { get; set; }
// A user/MCP-declared predecessor, distinct from BlockedByTaskId (the planning chain's own
// internal link): the picker also skips a Queued task while this is set and the referenced
// task's Status isn't 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).
public string? DependsOnTaskId { 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; }