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
This commit is contained in:
63
.gitignore
vendored
Normal file
63
.gitignore
vendored
Normal file
@@ -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
|
||||
322
docs/plan.md
322
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\<list>\<task-id>\ (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/<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)
|
||||
|
||||
- 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:<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`).
|
||||
- **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/<task-id>/`, 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<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.
|
||||
- **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 <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).
|
||||
|
||||
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= ".\<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)
|
||||
|
||||
@@ -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
|
||||
/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
|
||||
/worker TypeScript / Node
|
||||
/src
|
||||
db.ts, poller.ts, runner.ts, state.ts, config.ts, index.ts
|
||||
/logs runtime, gitignored
|
||||
package.json, tsconfig.json
|
||||
/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
|
||||
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 <id>`), 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<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` → 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.
|
||||
|
||||
Reference in New Issue
Block a user