Files
ClaudeDo/docs/plan.md
Mika Kuns b6897df86e chore: add gitignore and finalize initial plan
- .gitignore for .NET output, IDE files, runtime artifacts (todo.db, worktrees, sandbox, logs)
- docs/plan.md: .NET worker spawning Claude CLI, per-list working_dir + git worktrees, 3NF schema, SignalR IPC, Windows-Service deployment path
2026-04-13 11:14:46 +02:00

321 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 — `manual` | `queued` | `running` | `done` | `failed` (`running` bleibt persistiert für Crash-Recovery: stale `running`-Tasks werden beim Worker-Start auf `failed` gesetzt)
- `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.