# ToDo-App mit autonomem Agent-Worker — Design

## Context

Ziel: eine persönliche ToDo-App als Desktop-Anwendung, in der mehrere Listen verwaltet werden können. Ein Teil der Tasks soll autonom von Claude abgearbeitet werden (z.B. Recherche, Code-Aufgaben, Notizen-Verarbeitung). Die Autonomie läuft in einem getrennten Hintergrund-Prozess, damit die UI davon entkoppelt bleibt.

Motivation: Aufgaben "parken" und später von einem Agenten abarbeiten lassen, ohne dafür manuell einen Chat zu öffnen. Ergebnisse landen wieder an der Task, sodass die Liste die Single-Source-of-Truth bleibt.

Listen können ein eigenes `working_dir` (z.B. ein Projekt-Repo) haben. Tasks solcher Listen laufen in einem **dedizierten Git-Worktree**, der Worker commitet die Änderungen am Ende automatisch. Die UI zeigt Branch + Diff und erlaubt Merge/Keep/Discard.

## Architektur-Überblick

Monorepo (`C:\Private\ClaudeDo`, gehostet auf `git.kuns.dev`), zwei Prozesse, gekoppelt über eine gemeinsame SQLite-Datenbank:

```
┌─────────────────────┐        ┌──────────────────────────────┐
│  UI (Avalonia .NET) │        │  Worker (.NET 8 Console)     │
│  Listen, Tasks,     │◄──────►│  BackgroundService           │
│  Diff/Merge-Dialog  │ SQLite │  spawnt: claude -p ...       │
└─────────────────────┘  WAL   │  cwd = Worktree der Task     │
         │                     └──────────┬───────────────────┘
         │                                │ stdout (ndjson)
         └─────── todo.db ────────────────┘
                  (~/.todo-app/todo.db)

  Ziel-Repo (z.B. LagerApp):
    C:\Private\LagerApp\                              (main repo, unangetastet)
    C:\Private\.claudedo-worktrees\<list>\<task-id>\  (Worktree pro Task)
```

- **UI** (`ClaudeDo.Ui`, Avalonia MVVM): Listen & Tasks verwalten, Ergebnisse + Diffs anzeigen, Worktree-Aktionen.
- **Worker** (`ClaudeDo.Worker`, .NET 8 Console mit Kestrel + SignalR): hostet einen SignalR-Hub auf localhost, verwaltet die Queue intern, spawnt Claude CLI als Child-Prozess pro Task, streamt Events live an die UI, commitet, persistiert Ergebnisse.
- **SQLite im WAL-Mode** für persistente Daten (Listen, Tasks, Tags, Worktrees). Keine Koordinations-Felder — Live-Status läuft über SignalR.
- **SignalR über localhost-HTTP** (Default-Port `47821`, konfigurierbar) als Push-Channel zwischen Worker und UI. UI-Connection-State ⇒ "Worker online".

## Datenmodell

Schema in 3NF. Keine Mehrwert-Felder (z.B. JSON-Arrays), keine transitiven Abhängigkeiten: Worktree-Attribute wandern in eine eigene Tabelle, Tags sind über Junctions modelliert, Worker-Slots sind eigene Rows.

**lists**
- `id` TEXT PK (uuid)
- `name` TEXT NOT NULL
- `created_at` TIMESTAMP NOT NULL
- `working_dir` TEXT NULL — absoluter Pfad. Gesetzt + Git-Repo → Worktree-Modus.
- `default_commit_type` TEXT NOT NULL DEFAULT `'chore'`

**tasks**
- `id` TEXT PK (uuid)
- `list_id` TEXT NOT NULL REFERENCES `lists(id)` ON DELETE CASCADE
- `title` TEXT NOT NULL
- `description` TEXT NULL
- `status` TEXT NOT NULL — Lifecycle-only: `idle` | `queued` | `running` | `done` | `failed` | `cancelled` (`running` bleibt persistiert für Crash-Recovery: stale `running`-Tasks werden beim Worker-Start auf `failed` gesetzt). Planungs-Hierarchie und Chain-Blocking laufen über zwei separate Felder.
- `planning_phase` TEXT NOT NULL DEFAULT `'none'` — Parent-only Marker: `none` | `active` (Planung läuft) | `finalized` (Plan committed, Children existieren). Ein Parent kann `status='idle'` sein und gleichzeitig `planning_phase='finalized'` (für Re-Runs).
- `blocked_by_task_id` TEXT NULL REFERENCES `tasks(id)` ON DELETE SET NULL — Vorgänger in einem sequenziellen Subtask-Chain. Ein `queued`-Row mit `blocked_by_task_id IS NOT NULL` wird vom Picker übersprungen.
- `scheduled_for` TIMESTAMP NULL — "nicht vor"
- `result` TEXT NULL (Markdown)
- `log_path` TEXT NULL — Pfad zur ndjson-Log-Datei
- `created_at` TIMESTAMP NOT NULL
- `started_at` TIMESTAMP NULL
- `finished_at` TIMESTAMP NULL
- `commit_type` TEXT NOT NULL DEFAULT `'chore'` — Conventional-Commit-Prefix (wird beim Anlegen aus `list.default_commit_type` befüllt, danach unabhängig).

**tags**
- `id` INTEGER PK AUTOINCREMENT
- `name` TEXT NOT NULL UNIQUE — z.B. `agent`, `manual`, `code`, `research`

**list_tags** (Junction, Liste hat 0..n Tags)
- `list_id` TEXT NOT NULL REFERENCES `lists(id)` ON DELETE CASCADE
- `tag_id` INTEGER NOT NULL REFERENCES `tags(id)` ON DELETE CASCADE
- PK `(list_id, tag_id)`

**task_tags** (Junction, Task hat 0..n Tags; überschreibt die List-Tags nicht automatisch — die Vereinigung aus `list_tags` ∪ `task_tags` bildet die effektive Tag-Menge der Task; Ausschluss über noch zu spezifizierenden Negations-Mechanismus, bis dahin additiv)
- `task_id` TEXT NOT NULL REFERENCES `tasks(id)` ON DELETE CASCADE
- `tag_id` INTEGER NOT NULL REFERENCES `tags(id)` ON DELETE CASCADE
- PK `(task_id, tag_id)`

**worktrees** (1:1 mit Tasks, die im Worktree-Modus laufen)
- `task_id` TEXT PK REFERENCES `tasks(id)` ON DELETE CASCADE — Task hat höchstens einen Worktree.
- `path` TEXT NOT NULL — absoluter Pfad zum Worktree.
- `branch_name` TEXT NOT NULL — z.B. `claudedo/<task-id-kurz>`.
- `base_commit` TEXT NOT NULL — SHA des `working_dir`-HEAD beim Anlegen.
- `head_commit` TEXT NULL — SHA nach Auto-Commit (NULL bis Commit erfolgt).
- `diff_stat` TEXT NULL — Output von `git diff --stat base..head`.
- `state` TEXT NOT NULL DEFAULT `'active'` — `active` | `merged` | `discarded` | `kept`.
- `created_at` TIMESTAMP NOT NULL

Die Abwesenheit einer `worktrees`-Zeile entspricht dem alten Wert `'none'`. Non-Worktree-Tasks (Liste ohne `working_dir`) erzeugen keine Zeile.

**Hinweis:** Es gibt keine `worker_heartbeat`- oder `worker_slots`-Tabelle. Worker-Online-Status und aktive Slots laufen über SignalR (siehe Sektion "IPC").

## Tag-Modell (Startset)

- Tags leben in `tags`, Zuordnung via `list_tags` und `task_tags` (Junctions).
- Effektive Tag-Menge einer Task = `list_tags(task.list_id) ∪ task_tags(task.id)` (additiv).
- Minimal-Startset in `tags`:
  - `manual` → Worker ignoriert, reine Notiz/Checkliste.
  - `agent` → Worker pickt auf, wenn Status `queued`.
- Weitere Profile (`code`, `research` …) später als zusätzliche Rows in `tags` + Handler im Worker.

## IPC (SignalR)

Worker hostet `Microsoft.AspNetCore.SignalR` über Kestrel auf `http://127.0.0.1:<port>/hub` (Default `47821`, in `worker.config.json` überschreibbar). Bindung **nur** an Loopback. Kein Auth-Layer.

**Hub: `WorkerHub`**

Server-Methoden (UI ruft auf):
- `GetActive()` → `[{ slot: "queue"|"override", taskId, startedAt }]` — initialer State-Sync nach Connect.
- `RunNow(taskId)` → triggert sofortigen Override-Run; wirft, wenn Override-Slot belegt oder Task nicht existiert.
- `CancelTask(taskId)` → killt laufenden CLI-Child-Prozess der Task.
- `WakeQueue()` → Worker prüft Queue sofort (für instant pickup nach Task-Anlage).
- `Ping()` → `"pong"` + Worker-Version (Sanity-Check).

Client-Methoden (Worker pusht an UI):
- `TaskStarted(slot, taskId, startedAt)`
- `TaskFinished(slot, taskId, status, finishedAt)` — Status `done` | `failed`.
- `TaskMessage(taskId, ndjsonLine)` — streamt jedes Claude-Event live (für Live-Log in TaskDetail).
- `WorktreeUpdated(taskId)` — Auto-Commit fertig, UI lädt `worktrees`-Row neu.
- `TaskUpdated(taskId)` — generisches Signal: UI lädt Task neu (z.B. nach Result-Persist).

**Connection-State** der UI ⇒ "Worker online" (kein Heartbeat in DB nötig).

**UI-Verhalten bei offline Worker:**
- StatusBar: "Worker offline".
- "Run Now" deaktiviert (RPC nicht möglich).
- Task-Anlage funktioniert weiter (DB-only); wird beim nächsten Worker-Start aufgenommen.

## Queue-Semantik

- **Default**: Tasks mit Tag `agent` und Status `queued` werden **sequenziell** abgearbeitet (FIFO, nach `created_at`). Worker-interner In-Memory-Slot `_queueSlot`.
- **Schedule**: `scheduled_for` gesetzt → Worker überspringt, bis Zeit erreicht.
- **Override**: UI ruft `RunNow(taskId)` per SignalR. Worker startet die Task in `_overrideSlot` parallel zur Queue. **Max. 1 Queue + 1 Override = 2 gleichzeitige Runs.**
- Zweiter `RunNow` während Override-Slot belegt → SignalR-Methode wirft, UI zeigt "Override bereits aktiv".
- **Wake-up**: Bei Task-Anlage ruft die UI `WakeQueue()`, damit der Worker nicht erst auf den nächsten Tick warten muss.

## Worker-Komponenten (.NET)

**Projekt:** `ClaudeDo.Worker` — `Microsoft.NET.Sdk.Web` Console-App (für Kestrel + SignalR), Teil von `ClaudeDo.sln`, referenziert `ClaudeDo.Data`.

- `Program.cs` — `WebApplication.CreateBuilder` + `AddSignalR()` + `AddHostedService<QueueService>()`. `app.MapHub<WorkerHub>("/hub")` auf konfiguriertem Loopback-Port. Bindung: `app.Urls.Add($"http://127.0.0.1:{cfg.Port}")`.
- `Hub/WorkerHub.cs` — implementiert die in der IPC-Sektion beschriebenen Methoden. Delegiert `RunNow`/`CancelTask`/`WakeQueue` an `QueueService`. `GetActive()` liest aus den In-Memory-Slots.
- `Hub/HubBroadcaster.cs` — Wrapper um `IHubContext<WorkerHub>` mit typisierten `TaskStarted`/`TaskFinished`/`TaskMessage`/`WorktreeUpdated`/`TaskUpdated`-Methoden. Wird in `TaskRunner` und `ClaudeProcess` injiziert.
- `Services/QueueService.cs` — `BackgroundService`, interner Timer (z.B. 30s als Backstop, primärer Trigger ist `WakeQueue`). Wählt nächste Task (Tag `agent` via `task_tags`/`list_tags`, `scheduled_for ≤ now`, `_queueSlot` frei). Hält In-Memory-Slots `_queueSlot`, `_overrideSlot`. Sendet `TaskStarted`/`TaskFinished` über `HubBroadcaster`.
- `Services/StaleTaskRecovery.cs` — beim Start: alle `tasks.status='running'` → `'failed'`, Begründung "worker restart".
- `Runner/TaskRunner.cs` — orchestriert eine Task-Ausführung (siehe Lifecycle unten).
- `Runner/WorktreeManager.cs` — kapselt `git worktree add/remove`, `git rev-parse`, `git diff --stat`, `git add -A && git commit`.
- `Runner/ClaudeProcess.cs` — kapselt `System.Diagnostics.Process`:
  - Cmd: `claude -p --output-format stream-json --verbose --dangerously-skip-permissions`
  - `WorkingDirectory = worktreePath` (bzw. Sandbox-Dir bei Listen ohne `working_dir`).
  - Prompt über **stdin** (vermeidet Quoting-Probleme mit Zeilenumbrüchen).
  - Async stdout-Reader, jede Zeile = 1 JSON-Event → an `LogWriter`, `MessageParser` **und** `HubBroadcaster.TaskMessage(taskId, line)`.
  - stderr in Log mit `[stderr]`-Prefix.
  - Finales `type:"result"` → `.result`-Feld extrahieren (Markdown).
  - Exit != 0 oder fehlendes Result → Task `failed`, Fehler ins `result`-Feld.
- `Runner/LogWriter.cs` — streamt ndjson nach `~/.todo-app/logs/<task-id>.ndjson`.
- `Runner/CommitMessageBuilder.cs` — baut Conventional-Commit-Message (siehe unten).
- `Config/WorkerConfig.cs` — lädt `~/.todo-app/worker.config.json`:
  ```json
  {
    "db_path": "~/.todo-app/todo.db",
    "sandbox_root": "~/.todo-app/sandbox",
    "log_root": "~/.todo-app/logs",
    "worktree_root_strategy": "sibling",
    "central_worktree_root": "~/.todo-app/worktrees",
    "queue_backstop_interval_ms": 30000,
    "signalr_port": 47821,
    "claude_bin": "claude"
  }
  ```
  Kein API-Key — CLI nutzt die bestehende User-Session.

**Dependencies:** `Microsoft.AspNetCore.App` (Kestrel + SignalR via `Microsoft.NET.Sdk.Web`), `Microsoft.Extensions.Hosting`, `Microsoft.Data.Sqlite` (via `ClaudeDo.Data`), `System.Text.Json`.

## Task-Lifecycle (Worktree-Modus)

Nur wenn `list.working_dir` gesetzt und ein Git-Repo ist:

1. **Worktree anlegen** (`WorktreeManager.CreateAsync`):
   - `base = git -C <working_dir> rev-parse HEAD`
   - `branch = claudedo/<task-id-kurz>`
   - `worktreePath = <working_dir>/../.claudedo-worktrees/<list-slug>/<task-id>/`
   - `git -C <working_dir> worktree add -b <branch> <worktreePath> <base>`
   - Insert in `worktrees`: `task_id`, `path`, `branch_name`, `base_commit`, `state='active'`, `created_at=now`.
2. **Claude-Run** via `ClaudeProcess.RunAsync(prompt, worktreePath, ...)`.
3. **Auto-Commit** (`WorktreeManager.CommitAsync`), nur wenn Run erfolgreich UND Änderungen vorhanden:
   - `git -C <worktreePath> add -A`
   - Nichts staged → skippen (`head = base`).
   - Sonst: `git commit -m <conventional-message>` (Author aus User-Git-Config).
   - `head = rev-parse HEAD`, `diff_stat = git diff --stat base..head`.
   - Update `worktrees`: `head_commit`, `diff_stat`.
4. **Status finalisieren:** `tasks.status='done'`, `tasks.result` = Claude-Result-Markdown, `tasks.finished_at=now`.
5. **Kein Auto-Cleanup.** `worktrees.state` bleibt `'active'`, bis der User über die UI eine Aktion wählt.

**Fehlerpfade:**
- Run failed / Exit != 0 → `tasks.status='failed'`, **kein Auto-Commit**, `worktrees`-Row bleibt mit `state='active'` zur Inspektion.
- `working_dir` ist kein Git-Repo → Task failed mit klarer Meldung; keine `worktrees`-Row, keine Fallback-Sandbox.
- Worktree-Anlegen failed (Branch/Dir existiert) → Task failed; keine `worktrees`-Row geschrieben; Runner räumt nicht auf.

**Non-Worktree-Modus** (`list.working_dir` NULL): cwd = `~/.todo-app/sandbox/<task-id>/`, kein Git, Standard-Toolset wie bisher.

## Conventional Commit Message

Format: `{type}({list-slug}): {title}`

- `type` = `task.commit_type` (Default `chore`).
- `{list-slug}` = `list.name` lowercased, whitespace→`-`, nicht-alphanumerisch entfernt.
- `{title}` = `task.title`, truncated auf 60 Zeichen.
- Body (optional, zweizeilig nach Leerzeile):
  ```
  {task.description, max 400 chars}

  ClaudeDo-Task: <task-id>
  ```

Beispiel: `feat(lager-app): add barcode scan retry logic`

## UI-Komponenten (Avalonia)

- **MainWindow** mit 2-Pane-Layout: links Listen, rechts Tasks der gewählten Liste.
- **ListEditor** — Feld `Working Directory` (Ordner-Picker), Default `Commit Type` Dropdown.
- **TaskItemView** zeigt Titel, Tags, Status (Badge), "Run Now"-Button (→ ruft `WorkerHub.RunNow(taskId)`; deaktiviert wenn Worker offline).
- **TaskDetailPane** — Description, Result-Markdown (AvaloniaEdit oder Markdown.Avalonia), Link zum ndjson-Log.
  - Bei vorhandener `worktrees`-Row mit `state='active'`: Branch, `diff_stat`, Buttons **Open Worktree**, **Show Diff**, **Merge into main**, **Keep as branch**, **Discard**.
  - **Merge into main**: `git -C <working_dir> merge --ff-only <branch>` (UI warnt bei non-ff) → `worktrees.state='merged'`, Worktree + Branch entfernen.
  - **Keep as branch**: Worktree entfernen, Branch behalten → `worktrees.state='kept'`.
  - **Discard**: `git worktree remove --force` + `git branch -D` → `worktrees.state='discarded'`.
- **TaskEditor** — Dropdown `Commit Type` (Default aus Liste).
- **StatusBar** — Worker-Status aus SignalR-Connection-State (Connected ⇒ online), aktive Tasks aus In-Memory-State (initial via `GetActive()`, danach via `TaskStarted`/`TaskFinished`-Events).
- **TaskDetailPane** zeigt bei laufender Task den Live-Stream der `TaskMessage`-Events (rolling Log).
- **WorkerClient** (`ClaudeDo.Ui/Services/WorkerClient.cs`) — kapselt `HubConnection`, Auto-Reconnect, exponiert `IObservable<…>`-Events bzw. `INotifyPropertyChanged`-Properties für die ViewModels.
- **SettingsDialog** — Default-Tags, Pfad zur todo.db, SignalR-Port (muss zum Worker passen).

DB-Zugriff via Microsoft.Data.Sqlite + Repository-Layer (`TaskRepository`, `ListRepository`). Git-Operationen (UI + Worker) über gemeinsamen `GitService` in `ClaudeDo.Data`. MVVM via CommunityToolkit.Mvvm.

## Worker als Windows-Service (Ziel-Deployment)

Initial läuft der Worker als Console-Prozess (lokales Dev-Setup). Im Endzustand soll er als **Windows-Service** automatisch starten.

**Code-seitig:**
- Paket `Microsoft.Extensions.Hosting.WindowsServices` referenzieren.
- In `Program.cs`: `builder.Host.UseWindowsService(o => o.ServiceName = "ClaudeDoWorker")`.
- Logging zusätzlich über `EventLog` (`builder.Logging.AddEventLog(...)`), damit Service-Fehler im Windows Event Viewer landen.
- Alle Pfade in `worker.config.json` **absolut** auflösen (`%USERPROFILE%` / `~` expandieren) — der Service-Working-Directory ist standardmäßig `C:\Windows\System32`.
- `StaleTaskRecovery` (siehe oben) sorgt nach Service-Restart automatisch für das Aufräumen hängender `running`-Tasks.
- Restart-Verhalten via `sc.exe failure`-Konfig oder beim Install.

**Install:**
- Veröffentlichen mit `dotnet publish -c Release -r win-x64 --self-contained false`.
- Service registrieren:
  ```cmd
  sc.exe create ClaudeDoWorker binPath= "C:\Path\To\ClaudeDo.Worker.exe" start= auto
  sc.exe failure ClaudeDoWorker reset= 60 actions= restart/5000/restart/10000/restart/30000
  ```
- Später optional: kleines `ClaudeDo.Installer`-Projekt (WiX oder MSIX), das das auch macht.

**Auth-Konflikt mit "User-CLI-Session" beachten:**
Der Worker-Service läuft per Default unter `LocalSystem` — der hat **keinen Zugriff** auf die `~/.claude/`-Session des interaktiven Users, in der der CLI-Login liegt. Optionen:

1. **Empfohlen:** Service unter dem **User-Account** laufen lassen (`sc.exe config ClaudeDoWorker obj= ".\<username>" password= "..."` oder via `services.msc` → "Log On As"). Dann greift die bestehende `claude login`-Session des Users. Voraussetzung: User-Account hat das Recht "Log on as a service".
2. **Fallback:** Wieder auf API-Key wechseln (`ANTHROPIC_API_KEY` als Umgebungsvariable des Service oder im `worker.config.json`). Dann ist der Service unabhängig vom User-Profil — verliert aber den Vorteil "kein Key-Handling".

Entscheidung wird beim Service-Deployment getroffen, bleibt für die initiale Console-Variante irrelevant. Service-Modus erfordert keine Schema- oder API-Änderungen am Worker.

**SignalR im Service-Modus:** Bindung bleibt `127.0.0.1:47821`. Da die UI auf demselben Rechner läuft, ist Loopback-Erreichbarkeit gegeben — Windows-Firewall greift bei Loopback nicht.

## Project-Layout (Monorepo)

**Repo: `ClaudeDo`** auf `git.kuns.dev`, lokal `C:\Private\ClaudeDo`

```
/ClaudeDo
  ClaudeDo.sln
  /src
    /ClaudeDo.App              Avalonia Entry (App.axaml, Program.cs)
    /ClaudeDo.Ui               Views, ViewModels, Worktree-Dialoge
    /ClaudeDo.Data             Repositories, Models, SqliteConnectionFactory, GitService
    /ClaudeDo.Worker           Console/BackgroundService
  /tests
    /ClaudeDo.Worker.Tests     xUnit, In-Memory-SQLite + temp-Repo für Worktree-Tests
  /schema
    schema.sql                 Single source of truth für DB-Schema
    migrations/                (später, falls nötig)
  /docs
    plan.md, architecture.md
  .gitignore                   bin/, obj/, *.db, worker.config.json, logs/
```

Vorteil Monorepo: gemeinsames `schema.sql`, atomische Änderungen über UI+Worker, ein Clone reicht, einheitlicher .NET-Stack.

## Verification

1. **Schema-Init**: Worker erstellt `todo.db` bei erstem Start mit WAL, legt alle Tabellen an (`lists`, `tasks`, `tags`, `list_tags`, `task_tags`, `worktrees`). Initial werden in `tags` die Rows `'agent'` und `'manual'` eingefügt. → `sqlite3 todo.db ".schema"` zeigt die erwartete Struktur in 3NF.
1a. **SignalR-Endpoint**: Worker startet, `curl -i http://127.0.0.1:47821/hub` antwortet (HTTP 400 Negotiate-Fehler ohne SignalR-Handshake ist OK — Hauptsache Port lauscht und nur auf Loopback).
1b. **Hub-Roundtrip** (UI- oder Test-Client): `connection.InvokeAsync<string>("Ping")` liefert `"pong"`.
2. **CLI-Preflight**: `claude --version` läuft, User eingeloggt. Worker-Startup-Check failed laut, wenn nicht erfüllt.
3. **Smoke-Spawn** (Unit-Test): `claude -p --output-format stream-json --dangerously-skip-permissions`, Prompt `"ping"` via stdin → `result`-Message erhalten + geparst.
4. **End-to-End Happy Path (Non-Worktree)**:
   - UI: Liste "Test" (kein `working_dir`), Task mit Tag `agent` + Status `queued`, Description "Schreibe eine Haiku über Intralogistik".
   - Worker erkennt Task binnen 5s (Log: `picked up task <id>`), startet CLI-Run, schreibt Result zurück.
   - UI zeigt Status `done` + Result-Markdown.
5. **Worktree-Happy-Path** (Integrations-Test mit temp-Repo):
   - Liste mit `working_dir=<temp-repo>`, Task "add hello.txt with content hi".
   - Nach Run: Worktree unter `<temp-repo>/../.claudedo-worktrees/<slug>/<id>/`, `hello.txt` drin, 1 Commit `chore(<slug>): add hello.txt` auf Branch `claudedo/<id-kurz>`. `worktrees`-Row für die Task: `head_commit != base_commit`, `diff_stat` gesetzt, `state='active'`.
6. **No-Changes-Run**: Prompt, der nichts ändert → `tasks.status='done'`, kein Commit, `worktrees.head_commit IS NULL` (oder = `base_commit`).
7. **Kein Git-Repo**: `working_dir` auf normalen Ordner → Task failed mit klarer Meldung, **keine** `worktrees`-Row.
8. **Merge-UI**: nach erfolgreichem Run Button "Merge into main" → `<working_dir>` HEAD enthält den Commit, Branch + Worktree weg, `worktrees.state='merged'`.
9. **Override-Parallelität**: Zwei `queued` Tasks → nur eine läuft. Bei zweiter "Run Now" klicken → `WorkerHub.RunNow` succeeded, beide laufen parallel, UI bekommt zwei `TaskStarted`-Events (slots `queue` + `override`), in Worktree-Listen zwei getrennte Worktrees. Dritter `RunNow` → SignalR-Exception "override slot busy".
10. **Schedule**: Task mit `scheduled_for = now + 2min` → Worker wartet; nach Ablauf startet sie.
11. **Worker-Offline-Erkennung**: Worker-Prozess killen → SignalR-Connection bricht ab, UI-StatusBar wechselt sofort auf "offline" (kein Polling-Lag). "Run Now"-Buttons werden disabled. Laufende Task bleibt `running` → beim nächsten Worker-Start setzt `StaleTaskRecovery` sie auf `failed`, `worktrees`-Row bleibt zur Inspektion. UI reconnected automatisch und ruft erneut `GetActive()` für den State-Sync.
12. **Live-Stream**: Während eine Task läuft, UI öffnet TaskDetailPane → ndjson-Events erscheinen in Echtzeit (jedes `TaskMessage`-Event wird angehängt).
13. **Wake-up**: Task in UI anlegen mit Tag `agent`, Status `queued` → UI ruft `WakeQueue()` → Worker startet die Task in < 1s (nicht erst nach Backstop-Intervall).
14. **Unit-Tests** (Worker): `QueueService`-Selection-Logik (Override-Slot-Belegung, Schedule-Filter, Sequenzialität) mit In-Memory-SQLite. Hub-Methoden mit `TestServer` + `HubConnectionBuilder`.

## Offene Punkte für später (nicht Scope dieses Plans)

- Zusätzliche Tag-Profile mit spezialisierten Toolsets (`code` → engere Permissions, `research` → Netzwerk).
- MCP-Server-Integration für externe Dienste.
- Notification bei Task-fertig (Windows Toast).
- Non-ff Merges (Rebase-/Squash-Option im UI).
- Bulk-Discard alter Worktrees.
- Anzeige der ndjson-Message-Chronik im UI.
- Windows Job Objects für garantierten Child-Cleanup beim Worker-Crash.
- Installer-Projekt (`ClaudeDo.Installer`, WiX/MSIX), das den Service registriert + UI shortcut anlegt.
