# 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\\\ (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/`. - `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:/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()`. `app.MapHub("/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` 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/.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 rev-parse HEAD` - `branch = claudedo/` - `worktreePath = /../.claudedo-worktrees///` - `git -C worktree add -b ` - 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 add -A` - Nichts staged → skippen (`head = base`). - Sonst: `git commit -m ` (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//`, 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: ``` 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 merge --ff-only ` (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= ".\" 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("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 `), 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=`, Task "add hello.txt with content hi". - Nach Run: Worktree unter `/../.claudedo-worktrees///`, `hello.txt` drin, 1 Commit `chore(): add hello.txt` auf Branch `claudedo/`. `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" → `` 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.