- .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
321 lines
22 KiB
Markdown
321 lines
22 KiB
Markdown
# 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.
|