diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4a416c --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +# .NET build output +bin/ +obj/ +[Bb]uild/ +[Bb]uildResults/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio / Rider / VS Code +.vs/ +.vscode/ +.idea/ +*.suo +*.user +*.userosscache +*.sln.docstates +*.userprefs + +# Test results +[Tt]est[Rr]esult*/ +*.coverage +*.coveragexml +coverage*.json +coverage*.xml +coverage*.info + +# Publish output +publish/ +*.[Pp]ublish.xml +PublishProfiles/ + +# NuGet +*.nupkg +*.snupkg +.nuget/ +packages/ +!packages/build/ +project.lock.json +project.fragment.lock.json +artifacts/ + +# Avalonia / XAML designer +*.designer.cs + +# Project-specific +*.db +*.db-shm +*.db-wal +worker.config.json +sandbox/ +.claudedo-worktrees/ + +# OS +Thumbs.db +ehthumbs.db +Desktop.ini +.DS_Store + +# Misc +*.log +*.tmp +*.bak diff --git a/docs/plan.md b/docs/plan.md index 52993bf..e8a2338 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -6,79 +6,259 @@ Ziel: eine persönliche ToDo-App als Desktop-Anwendung, in der mehrere Listen ve 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 -Ein Monorepo (`C:\Private\ClaudeDo`, gehostet auf `git.kuns.dev`), zwei Prozesse, gekoppelt über eine gemeinsame SQLite-Datenbank: +Monorepo (`C:\Private\ClaudeDo`, gehostet auf `git.kuns.dev`), zwei Prozesse, gekoppelt über eine gemeinsame SQLite-Datenbank: ``` -┌─────────────────────┐ ┌──────────────────────┐ -│ UI (Avalonia .NET) │ │ Worker (Node/TS) │ -│ MVVM, Dialogs, │◄──────►│ Claude Agent SDK │ -│ Queue-View │ SQLite │ Queue-Polling │ -└─────────────────────┘ WAL └──────────────────────┘ - │ │ - └─────── todo.db ───────────────┘ +┌─────────────────────┐ ┌──────────────────────────────┐ +│ 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** (`todo-app-ui`, Avalonia MVVM): Listen & Tasks verwalten, Status ändern, Ergebnisse anzeigen. -- **Worker** (`todo-app-worker`, TypeScript): pollt die DB, arbeitet `queued` Tasks ab, schreibt Ergebnisse zurück. -- **SQLite im WAL-Mode** für sauberes concurrent read/write zwischen beiden Prozessen. +- **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 -Drei Tabellen in `todo.db`: +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, uuid), `name` (TEXT), `tags` (TEXT, JSON-Array), `created_at` +- `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, uuid), `list_id` (FK), `title`, `description` -- `tags` (JSON-Array, überschreibt/ergänzt List-Tags) -- `status`: `manual` | `queued` | `running` | `done` | `failed` -- `priority`: `normal` | `override` (→ läuft parallel zur Queue) -- `scheduled_for` (TIMESTAMP, nullable — "nicht vor") -- `result` (TEXT, Markdown), `log_path` (TEXT, Pfad zur Session-Log-Datei) -- `created_at`, `started_at`, `finished_at` +- `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). -**worker_state** -- `id` (1), `last_heartbeat`, `active_queue_task_id`, `active_override_task_id` -- Dient UI als Signal, ob Worker läuft und welche Task aktiv ist. +**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) -- Liste hat Default-Tags, Task erbt und kann ergänzen/überschreiben. -- Minimal-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 Tags + Handler im Worker. +- 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`). +- **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-Button "Jetzt ausführen" setzt `priority = override`. Worker startet parallel zur laufenden Queue-Task. **Max. 1 Queue + 1 Override = 2 gleichzeitige Runs.** -- Ein zweites Override während ein Override läuft → Meldung "Override bereits aktiv". +- **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 (TypeScript) +## Worker-Komponenten (.NET) -- `db.ts` — better-sqlite3, Prepared Statements, WAL-Mode aktivieren. -- `poller.ts` — alle 5s: nächste `queued` Task auswählen (Tag `agent`, `scheduled_for` ≤ jetzt, kein anderer Queue-Run aktiv), sowie pending `override`. -- `runner.ts` — startet Agent-SDK-Run, streamt Messages in Log-Datei, speichert Ergebnis. - - Input: Task-Titel + Description als User-Prompt. - - Standard-Toolset: Read/Write in `~/.todo-app/sandbox//`, keine Netzwerk-Tools by default (später per Tag erweiterbar). -- `state.ts` — schreibt Heartbeat alle 5s in `worker_state` (UI erkennt so, ob Worker läuft). -- Konfig via `~/.todo-app/worker.config.json`: `ANTHROPIC_API_KEY`, `model`, `sandbox_root`, `poll_interval_ms`. +**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. -- **TaskItemView** zeigt Titel, Tags, Status (Badge), "Run Now"-Button (→ setzt `override`). -- **TaskDetailPane** — erweiterbare Ansicht mit Description, Result-Markdown (AvaloniaEdit oder Markdown.Avalonia), Link zum Log. -- **StatusBar** — zeigt Worker-Status aus `worker_state.last_heartbeat` (< 15s = online). -- **SettingsDialog** — Default-Tags, Poll-Interval, Pfad zur todo.db. +- **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). -Zugriff auf DB via Microsoft.Data.Sqlite + schmaler Repository-Layer (`TaskRepository`, `ListRepository`). MVVM via CommunityToolkit.Mvvm. +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) @@ -86,43 +266,55 @@ Zugriff auf DB via Microsoft.Data.Sqlite + schmaler Repository-Layer (`TaskRepos ``` /ClaudeDo - /ui Avalonia .NET 8 - ClaudeDo.sln - /src - /ClaudeDo.App App.axaml, Program.cs (Entry) - /ClaudeDo.Ui Views, ViewModels - /ClaudeDo.Data Repositories, Models, SqliteConnectionFactory - /tests - /worker TypeScript / Node - /src - db.ts, poller.ts, runner.ts, state.ts, config.ts, index.ts - /logs runtime, gitignored - package.json, tsconfig.json + 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) + schema.sql Single source of truth für DB-Schema + migrations/ (später, falls nötig) /docs - README.md, architecture.md - .gitignore bin/, obj/, node_modules/, logs/, *.db, worker.config.json + 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. +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 Tabellen an. → `sqlite3 todo.db ".schema"` zeigt erwartete Tabellen. -2. **End-to-End Happy Path**: - - UI: Liste "Test" anlegen, Task mit Tag `agent` + Status `queued` erzeugen, description: "Schreibe eine Haiku über Intralogistik". - - Worker erkennt Task binnen 5s (Log: `picked up task `), startet Agent-Run, schreibt Result zurück. - - UI zeigt Status `done` + Result-Markdown nach Reload/Refresh. -3. **Override-Parallelität**: Zwei `queued` Tasks anlegen → nur eine läuft. Bei zweiter "Run Now" klicken → beide laufen parallel, `worker_state` zeigt beide IDs. -4. **Schedule**: Task mit `scheduled_for = now + 2min` → Worker wartet; nach Ablauf startet sie. -5. **Worker-Offline-Erkennung**: Worker-Prozess killen → UI-StatusBar wechselt innerhalb 15s auf "offline". -6. **Unit-Tests** (Worker): `poller` Selection-Logik (Priorität Override, Schedule-Filter, Sequenzialität) mit In-Memory-SQLite. +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` → Bash/Git-Tools, `research` → WebFetch). +- 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). -- Encrypted API-Key-Storage (erstmal Plaintext-JSON mit User-File-ACL). +- 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.