Files
ClaudeDo/docs/superpowers/specs/2026-08-07-handler-run-links-design.md
T

8.1 KiB

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 <variant>". 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:

/// <summary>Id des Handler-Task-Runs, der diesen Task behandelt hat (null = keiner).</summary>
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.CreateMergeHelperTaskAsyncWorkerHub.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:

Task<int> SetHandlerTaskIdAsync(IReadOnlyList<string> taskIds, string handlerTaskId, CancellationToken ct);

Der Handler-Task selbst bekommt kein HandlerTaskId (kein Selbstbezug). Unbekannte Ids werden still übersprungen.

3. Badge

TaskRowViewModel:

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).