diff --git a/docs/superpowers/specs/2026-08-07-handler-run-links-design.md b/docs/superpowers/specs/2026-08-07-handler-run-links-design.md new file mode 100644 index 00000000..38b8cd49 --- /dev/null +++ b/docs/superpowers/specs/2026-08-07-handler-run-links-design.md @@ -0,0 +1,167 @@ +# Handler-Run: Verknüpfung zu den behandelten Tasks + +**Date:** 2026-08-07 +**Status:** Design approved (Mika), implementation pending +**Verified against:** commit `c792765` + +## Problem + +Ein "Let Claude handle it"-Run besitzt seit 2026-08-05 einen echten Task (`IsManual=true`, +`HandlerBaseCommit`/`HandlerHeadCommit`, Diff über Commit-Range). Was fehlt: **welche Tasks der Run +behandelt hat, ist nirgends persistiert.** Die Auswahl lebt nur in der ConPTY-Session und im +Transcript; `HandoffMcpTools.HandoffListHandler` (`src/ClaudeDo.Worker/External/HandoffMcpTools.cs:28-45`) +bekommt `survivingTaskIds` als flüchtige Liste. + +Folge: Nachdem ein Run durch ist und der Diff sichtbar wird, lässt sich nicht mehr nachvollziehen, +*was alles gemacht werden sollte* und *welcher Task was produziert hat*. Duplikate, die der Handler +in Phase 1 gecancelt hat, verschwinden vollständig aus dem Blickfeld. + +Zweitens zeigt der Handler-Task in der Liste das Badge **MANUAL**, weil er `IsManual=true` setzt — +irreführend, denn es ist kein manueller Reminder. + +## Ist-Zustand + +### Es gibt kein Task-Kind + +`TaskEntity` hat **kein `Kind`/`Type`-Enum**. Task-"Arten" sind heute Feld-Kombinationen: + +| Feld | Bedeutung | +|---|---| +| `IsManual` | manueller Reminder — Queue/Daily-Prep/Refine überspringen ihn | +| `ParentTaskId` | Kind einer Planning-/Improvement-Session | +| `PlanningPhase` | Planning-Parent | +| `BlockedByTaskId` | Kettenglied, Queue-Picker überspringt es | +| `HandlerBaseCommit` | worktree-loser List-Handler-Host (`src/ClaudeDo.Data/Models/TaskEntity.cs:60-61`) | + +Ein Handler-Task ist also allein durch `HandlerBaseCommit != null` identifiziert. + +### `ParentTaskId` ist belegt + +`TaskRepository.CreateChildAsync` (`src/ClaudeDo.Data/Repositories/TaskRepository.cs:306`) setzt es +für Planning-Kinder; `TaskRowViewModel.IsChild`/`ShowAsChild` +(`src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs:59,66`) hängen daran und rücken die Zeile +im Baum ein. Ein Recycling für Handler→behandelte Tasks würde die Auswahl optisch unter den Handler +schieben und mit echten Planning-Kindern kollidieren. + +### Badge-Infrastruktur existiert + +`TaskRowView.axaml:129-144` rendert DRAFT / PLANNED / PLANNING / MANUAL über +`Border Classes="badge "`. Basis-Style und Varianten liegen in +`src/ClaudeDo.Ui/Design/IslandStyles.axaml:963-990`, die Brushes als theme-fähige Tokens in +`Tokens.axaml`. Loc-Keys: `tasks.badgeManual`, `tasks.manualTip` (en.json:163-164). + +### Kinder-Panel existiert + +`DetailsIslandViewModel.LoadChildOutcomesAsync` +(`src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs:704-748`) lädt +`Where(t => t.ParentTaskId == parentTaskId)` in `ChildOutcomes` (`:248`) und rendert pro Zeile +Id/Titel/Status/RoadblockCount/WorktreeState via `ChildOutcomeRowViewModel`; Refresh läuft über +`TaskUpdated`/`WorktreeUpdated` (`:814-833`). + +## Entscheidungen + +| Frage | Entscheidung | Begründung | +|---|---|---| +| Neues `TaskKind`-Enum? | **Nein** | Es gäbe kein Enum zu erweitern — es wäre das erste überhaupt, inkl. Migration und Rückwirkung auf Queue/Filter/UI. Der Bedarf ist eine Beziehung, kein Typ. | +| `ParentTaskId` wiederverwenden? | **Nein** | belegt durch Planning-Kinder, kollidiert mit Einrückungs-Logik | +| 1:n oder n:m? | **1:n**, eine nullable Spalte | Historie "welcher Run hat den Task mal berührt" bringt nichts, wenn ohnehin der letzte Run derjenige ist, dessen Diff man ansieht. Join-Tabelle = doppelter Code für einen Randfall. | +| Wann stempeln? | **Beim Anlegen des Handler-Tasks** | Die UI kennt die Auswahl bereits. Erfasst auch die Tasks, die der Handler in Phase 1 als Duplikat cancelt — genau das "was sollte alles gemacht werden". Ein Stempeln erst in `handoff_list_handler` würde Dedupe-Verlierer verlieren und bei Abbruch vor Phase 2 gar nichts verknüpfen. | +| Umfang der Anzeige | **Nur Liste + Endstatus** | Kein Phasen-Protokoll, kein Per-Task-Diff im Panel — der Diff hängt ohnehin am jeweiligen Task. | + +## Design + +### 1. Daten + +Neue nullable Spalte auf `TaskEntity`: + +```csharp +/// Id des Handler-Task-Runs, der diesen Task behandelt hat (null = keiner). +public string? HandlerTaskId { get; set; } +``` + +Konfiguration in `TaskEntityConfiguration`: `HasIndex(t => t.HandlerTaskId)`, kein FK-Constraint +(konsistent mit `BlockedByTaskId`-Handhabung; ein gelöschter Handler-Task soll die behandelten Tasks +nicht kaskadierend anfassen). EF-Core-Migration `AddHandlerTaskId`. + +Ein zweiter Run über dieselben Tasks überschreibt die Zuordnung — gewollt (1:n). + +### 2. Schreiben + +Die Auswahl wird durchgereicht: UI → `IWorkerClient.CreateMergeHelperTaskAsync` → +`WorkerHub.CreateMergeHelperTask` (`src/ClaudeDo.Worker/Hub/WorkerHub.cs:827-838`) → +`InteractiveLaunchSpecService.CreateMergeHelperTaskAsync`. Nach dem Anlegen des Handler-Tasks setzt +eine neue Repository-Methode die Zuordnung in einem Batch-Update: + +```csharp +Task SetHandlerTaskIdAsync(IReadOnlyList taskIds, string handlerTaskId, CancellationToken ct); +``` + +Der Handler-Task selbst bekommt **kein** `HandlerTaskId` (kein Selbstbezug). Unbekannte Ids werden +still übersprungen. + +### 3. Badge + +`TaskRowViewModel`: + +```csharp +public bool IsHandlerRun => !string.IsNullOrEmpty(HandlerBaseCommit); +public string? HandlerBadge => IsHandlerRun ? Loc.T("tasks.badgeHandler") : null; +public string? ManualBadge => IsManual && !IsHandlerRun ? Loc.T("tasks.badgeManual") : null; +``` + +`HandlerBaseCommit` muss dafür auf das Row-ViewModel und in dessen Mapping aufgenommen werden. +HANDLER hat Vorrang vor MANUAL — beide Badges nie gleichzeitig. + +In `TaskRowView.axaml` analog zu `:141-144` ein `Border Classes="badge handler"` mit +`ToolTip.Tip="{loc:Tr tasks.handlerTip}"`. In `IslandStyles.axaml` eine `.badge.handler`-Variante +mit `{DynamicResource HandlerBadgeBrush}`, Token in `Tokens.axaml` für Light und Dark. + +Neue Loc-Keys in en.json **und** de.json (Parität ist testgeprüft): + +- `tasks.badgeHandler` — "HANDLER" / "HANDLER" +- `tasks.handlerTip` — "Handler run — lists the tasks it processed" / "Handler-Run — listet die + Tasks, die er bearbeitet hat" + +### 4. Anzeige + +Im Detail-Bereich eines Handler-Tasks eine Liste der behandelten Tasks, parallel zum bestehenden +Kinder-Panel: + +- Neue Collection `HandledTasks` auf `DetailsIslandViewModel`, befüllt von `LoadHandledTasksAsync` + mit `Where(t => t.HandlerTaskId == taskId)`, sortiert wie die Kinder-Liste. +- Zeilen wiederverwenden `ChildOutcomeRowViewModel` (Id, Titel, Status, RoadblockCount, + WorktreeState) — keine neue Row-Klasse. +- Refresh über dieselben `TaskUpdated`-Events wie `ChildOutcomes`; der bestehende + `RefreshChildOutcomeAsync`-Pfad (`:814-833`) wird um die zweite Collection erweitert. +- Sichtbar nur wenn `HandledTasks.Count > 0`. +- Klick auf eine Zeile springt zum Task — gleiche Interaktion wie bei den Kindern. + +### 5. Fehlerfälle + +- Handler-Task gelöscht → `HandlerTaskId` der behandelten Tasks zeigt ins Leere; die Tasks bleiben + normal nutzbar, das Panel existiert schlicht nicht mehr. Kein Cleanup nötig. +- Behandelter Task gelöscht → verschwindet aus der Liste (Query läuft live gegen die Tasks). +- Leere Auswahl → kein Stempeln, Panel bleibt unsichtbar. + +## Tests + +| Ebene | Test | +|---|---| +| Data | `SetHandlerTaskIdAsync` stempelt alle übergebenen Ids, ignoriert unbekannte, überschreibt eine vorhandene Zuordnung | +| Worker | `CreateMergeHelperTaskAsync` stempelt die übergebene Auswahl und **nicht** den Handler-Task selbst | +| Ui | `TaskRowViewModel`: HANDLER schlägt MANUAL (`IsManual=true` + `HandlerBaseCommit` gesetzt → nur HANDLER) | +| Ui | `DetailsIslandViewModel`: `HandledTasks` lädt nach `HandlerTaskId`, aktualisiert sich auf `TaskUpdated` | +| Localization | Parität en/de — deckt der bestehende Test automatisch ab | + +## Bewusst nicht enthalten + +- Kein `TaskKind`-Enum. +- Keine n:m-Historie über mehrere Runs. +- Kein Phasen-Protokoll (Dedupe-Begründungen, Umformulierungen) — nur das Ergebnis. +- Kein Per-Task-Diff im Panel; der Diff bleibt am jeweiligen Task. +- Kein Badge auf den *behandelten* Tasks. + +## Offen + +- **Sichtprüfung durch Mika:** Badge-Farbe im Light- und Dark-Theme, Position des Panels im + Detail-Bereich, Verhalten bei vielen behandelten Tasks (Scroll).