refactor(worker): remove MessageParser (replaced by StreamAnalyzer)
This commit is contained in:
92
docs/improvement-plan.md
Normal file
92
docs/improvement-plan.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# ClaudeDo — Improvement Plan (Session 2026-04-13)
|
||||
|
||||
Erfasst während manuellem Walkthrough der App. Priorisiert nach Schmerz/Aufwand.
|
||||
|
||||
---
|
||||
|
||||
## P1 — UX-Blocker (sollten zuerst)
|
||||
|
||||
### IP-1: UI ↔ Worker Auto-Reconnect
|
||||
**Symptom:** Wenn UI vor Worker startet, bleibt die Verbindung tot. Manueller UI-Restart nötig.
|
||||
**Soll:** SignalR-Client mit `WithAutomaticReconnect()` + Reconnect-Versuche im Hintergrund (exponential backoff). Status-Bar zeigt "verbinde…" während Retry.
|
||||
**Dateien:** `src/ClaudeDo.Ui/Services/WorkerClient.cs` (oder wo `HubConnection` gebaut wird)
|
||||
**Aufwand:** klein (~30 Zeilen, primär `HubConnectionBuilder`-Konfig + Reconnect-Handler)
|
||||
**Risiko:** klein
|
||||
|
||||
### IP-2: Listen-Modus „Notes" (non-autonomous)
|
||||
**Symptom:** Jede Liste ist Agent-gesteuert. Keine reine Notiz-Liste möglich.
|
||||
**Soll:** Neues Feld `lists.kind` (`agent` | `notes`).
|
||||
- `agent`: aktuelles Verhalten (Worker pickt Tasks)
|
||||
- `notes`: Worker ignoriert die Liste komplett, UI versteckt Run-/Schedule-/Worktree-Felder, Tasks haben nur Title + Description + done-Checkbox.
|
||||
**Dateien:**
|
||||
- Schema: neue Spalte + Migration (siehe IP-9)
|
||||
- `Data/Entities/TaskList.cs`, `Repositories/ListRepository.cs`
|
||||
- `Worker/Queue/QueueService.cs` (Filter `WHERE list.kind = 'agent'`)
|
||||
- UI: `ListEditorView` (Radio/ComboBox), `TaskListView` (conditional Columns), `TaskDetailView` (verstecken)
|
||||
**Aufwand:** mittel (~Schema + Repo + UI an mehreren Stellen)
|
||||
**Risiko:** mittel — bestehende Listen müssen Default `agent` bekommen
|
||||
|
||||
### IP-3: Doppelklick öffnet Edit-Dialog
|
||||
**Symptom:** Edit nur über separaten Button/Menüpunkt.
|
||||
**Soll:** `DoubleTapped`-Handler auf ListBox-Items (Listen-Pane) und auf TaskRows (Task-Pane) → öffnet jeweiligen Editor.
|
||||
**Dateien:** `Views/MainWindow.axaml(.cs)`, `Views/TaskListView.axaml(.cs)`
|
||||
**Aufwand:** klein (~10–15 Zeilen pro Stelle)
|
||||
**Risiko:** klein
|
||||
|
||||
### IP-4: Tag-Multi-Select statt Freitext
|
||||
**Symptom:** Tags müssen getippt werden, keine Auto-Vervollständigung, Typos möglich.
|
||||
**Soll:** Multi-Select-Control:
|
||||
- Zeigt alle in DB existierenden Tags (DISTINCT aus `lists.tags` ∪ `tasks.tags`)
|
||||
- Erlaubt Anlegen neuer Tags (Free-Text-Add)
|
||||
- Chip/Token-Darstellung der ausgewählten Tags
|
||||
**Dateien:**
|
||||
- *neu* `Views/Controls/TagPicker.axaml` (wiederverwendbar)
|
||||
- `ListEditorView`, `TaskEditorView` einbinden
|
||||
- Repo-Methode `GetAllKnownTagsAsync()`
|
||||
**Aufwand:** mittel (Custom-Control lohnt sich, da 2× verwendet)
|
||||
**Risiko:** klein
|
||||
|
||||
### IP-5: Rechtsklick-Kontextmenü
|
||||
**Symptom:** Quick-Actions nur über Buttons im Detail-Pane oder Toolbar.
|
||||
**Soll:**
|
||||
- **Liste:** Edit, Delete, New Task, ggf. „Mark all done" (für Notes-Listen aus IP-2)
|
||||
- **Task:** Edit, Delete, Run Now, Show Diff, Merge, Cancel (je nach Status)
|
||||
- Items kontext-sensitiv enabled/disabled je nach Task-Status & List-Kind
|
||||
**Dateien:** `Views/MainWindow.axaml` (List-Pane), `Views/TaskListView.axaml` (Task-Pane)
|
||||
**Aufwand:** klein–mittel — Avalonia `ContextMenu` + Command-Bindings
|
||||
**Risiko:** klein
|
||||
|
||||
---
|
||||
|
||||
## P2 — Folge-Arbeiten (durch P1 ausgelöst)
|
||||
|
||||
### IP-6: Schema-Migration-Mechanismus
|
||||
**Trigger:** IP-2 fügt eine Spalte zu `lists` hinzu. Aktuell `schema.sql` ist Drop-and-Create-Style.
|
||||
**Soll:** Mini-Migrations-System: `migrations/0001_initial.sql`, `0002_lists_kind.sql`, … + `_schema_version` Tabelle.
|
||||
**Aufwand:** klein–mittel
|
||||
**Querverweis:** `open.md` Sektion 7 (Schulden-Tabelle: „Embedded schema.sql ohne Versionierung")
|
||||
|
||||
### IP-7: Status-Bar zeigt Reconnect-State
|
||||
**Trigger:** IP-1 — User soll sehen, dass Verbindung gerade aufgebaut wird (statt nur „offline").
|
||||
**Soll:** States: `connected` | `connecting` | `reconnecting` | `offline`. Farb-codiert.
|
||||
**Datei:** `ViewModels/StatusBarViewModel.cs`
|
||||
**Aufwand:** klein
|
||||
|
||||
### IP-8: Tag-Repository für `GetAllKnownTagsAsync`
|
||||
**Trigger:** IP-4 braucht eine Quelle aller bekannten Tags.
|
||||
**Soll:** Methode in `ListRepository`/`TaskRepository` ODER neuer `TagRepository`. SQL: `SELECT DISTINCT trim(value) FROM lists, json_each(lists.tags) UNION ...`.
|
||||
**Aufwand:** klein
|
||||
|
||||
---
|
||||
|
||||
## Empfohlene Reihenfolge
|
||||
|
||||
1. **IP-1** (Auto-Reconnect) — sofortiger UX-Win, isoliert, klein
|
||||
2. **IP-3** (Doppelklick) — trivial, sofort spürbar
|
||||
3. **IP-5** (Kontextmenü) — kompakt, hebt Bedienkomfort deutlich
|
||||
4. **IP-6** (Migrations) — Voraussetzung für IP-2
|
||||
5. **IP-2** (Notes-Mode) — größerer Brocken, braucht Schema-Migration
|
||||
6. **IP-8 → IP-4** (Tag-Repo, dann Multi-Select-Control)
|
||||
7. **IP-7** (Reconnect-Status in StatusBar) — Polish nach IP-1
|
||||
|
||||
Block 1 (IP-1, IP-3, IP-5) ist ein realistischer Session-Block.
|
||||
193
docs/open.md
Normal file
193
docs/open.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# ClaudeDo — Offene Punkte
|
||||
|
||||
Stand: 2026-04-13 nach Slice F. Branch `main` @ `48e4aab`. Alle Tests grün (38/38), Build 0 Warnings.
|
||||
|
||||
Dieses Dokument listet alles, was noch fehlt — gruppiert nach Aufwand/Risiko und mit konkreten Datei-Pointern, damit wir es in der IDE der Reihe nach durchgehen können.
|
||||
|
||||
---
|
||||
|
||||
## 1. Verification (vor allem anderen)
|
||||
|
||||
Die in `plan.md` definierten Verification-Steps sind teilweise nur durch Build/Tests abgedeckt. Diese sollten manuell einmal durchlaufen werden, BEVOR wir Polish bauen — damit wir wissen, was tatsächlich kaputt ist.
|
||||
|
||||
| # | Plan | Status | Was tun |
|
||||
|---|------|--------|---------|
|
||||
| 1 | Schema-Init | Auto verifiziert (Worker startet ohne Crash, WAL-Files entstehen) | OK |
|
||||
| 1a | SignalR-Endpoint | Manuell verifiziert (HTTP 400 auf `/hub` ohne Handshake) | OK |
|
||||
| 1b | Hub-Roundtrip `Ping` | **Nicht getestet** | Test-Client schreiben oder UI starten und im Log nach "pong" schauen |
|
||||
| 2 | `claude --version` Preflight | **Nicht implementiert** | `Worker/Program.cs`: vor `app.Run()` einmal `claude --version` shellen und bei Exit≠0 abbrechen |
|
||||
| 3 | Smoke-Spawn (`claude -p` mit Prompt "ping") | **Nicht getestet** | Integrationstest schreiben oder einmal manuell laufen lassen |
|
||||
| 4 | E2E Happy Path (Non-Worktree) | **Nicht getestet** | UI starten → Liste "Test" anlegen → Task mit Tag `agent` + Status `queued` + Description "Schreibe ein Haiku über Intralogistik" → Run abwarten → Result prüfen |
|
||||
| 5 | Worktree Happy Path | **Nicht getestet** | Manueller Test mit echtem Repo (z.B. einem temp-Repo) |
|
||||
| 6 | No-Changes-Run | **Nicht getestet** | Prompt der nichts ändert → `head_commit` bleibt NULL |
|
||||
| 7 | Kein Git-Repo | **Nicht getestet** | working_dir auf `C:\Temp` → Task `failed`, keine `worktrees`-Row |
|
||||
| 8 | Merge-UI | **Nicht getestet** (UI ruft `GitService.MergeFfOnlyAsync`, aber nie ausgeführt) | Manuell |
|
||||
| 9 | Override-Parallelität | Tests vorhanden für Slot-Logik, **End-to-End nicht** | UI: zwei Tasks queuen, `Run Now` auf der zweiten → beide laufen parallel |
|
||||
| 10 | Schedule | Logik per Test abgedeckt, **End-to-End nicht** | Task mit `scheduled_for = now+2min` |
|
||||
| 11 | Worker-Offline-Erkennung | UI hat Status-Bar, aber **nicht visuell verifiziert** | Worker killen, schauen ob Status auf "offline" wechselt |
|
||||
| 12 | Live-Stream | **Nicht getestet** | Während Run TaskDetail öffnen, beobachten ob ndjson-Zeilen erscheinen |
|
||||
| 13 | Wake-up (UI ruft `WakeQueue` nach Anlage) | Implementiert in `TaskListViewModel`, **nicht visuell verifiziert** | Tasks nach Anlage in <1s gepickert |
|
||||
|
||||
**Vorschlag:** Wir machen einmal Step 4 (Haiku-Happy-Path) gemeinsam — wenn das läuft, ist die ganze Pipeline lebendig.
|
||||
|
||||
---
|
||||
|
||||
## 2. UI-Polish (kritisch für Benutzbarkeit)
|
||||
|
||||
Im aktuellen Stand kompiliert die UI, aber mehrere Stellen sind als `// TODO` markiert. Reihenfolge nach Schmerz:
|
||||
|
||||
### 2.1 Folder-Picker für `Working Directory`
|
||||
- **Datei:** `src/ClaudeDo.Ui/Views/ListEditorView.axaml` + `src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs`
|
||||
- **Aktuell:** plain `TextBox` — Pfad muss getippt werden.
|
||||
- **Soll:** Button "…" daneben → öffnet `IStorageProvider.OpenFolderPickerAsync`, schreibt Pfad ins Feld.
|
||||
- **Aufwand:** klein, ~30 Zeilen.
|
||||
|
||||
### 2.2 Delete-Confirmation
|
||||
- **Dateien:** `MainWindowViewModel.DeleteList`, `TaskListViewModel.DeleteTask`
|
||||
- **Aktuell:** löscht direkt ohne Rückfrage. Datenverlust-Risiko.
|
||||
- **Soll:** Mini-Dialog "Wirklich löschen?" mit Ja/Nein.
|
||||
- **Aufwand:** klein, generisches `ConfirmDialog` lohnt sich (1× bauen, mehrfach nutzen).
|
||||
|
||||
### 2.3 Markdown-Rendering für Result + Description
|
||||
- **Datei:** `src/ClaudeDo.Ui/Views/TaskDetailView.axaml`
|
||||
- **Aktuell:** `TextBox IsReadOnly="True"` mit Plaintext.
|
||||
- **Soll:** `Markdown.Avalonia` Package einbinden und auf `MarkdownScrollViewer` umstellen.
|
||||
- **Aufwand:** mittel — Package + ein paar XAML-Anpassungen. Theme-Integration kann nerven.
|
||||
|
||||
### 2.4 Live-Log Auto-Scroll
|
||||
- **Datei:** `src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs` (oder im VM)
|
||||
- **Aktuell:** ndjson-Zeilen werden angehängt, aber Scrollposition bleibt stehen.
|
||||
- **Soll:** Bei jeder neuen Zeile `ScrollViewer.ScrollToEnd()` solange User nicht manuell hochgescrollt hat (Sticky-Bottom-Pattern).
|
||||
- **Aufwand:** klein, ein attached behavior reicht.
|
||||
|
||||
### 2.5 Diff-Viewer
|
||||
- **Datei:** `TaskDetailViewModel.ShowDiffAsync`
|
||||
- **Aktuell:** `Process.Start("cmd", "/k git diff …")` — separates Konsolenfenster, hässlich.
|
||||
- **Soll:** entweder unified-diff inline anzeigen (`git diff` Output in `TextBox` mit Mono-Font + Color für +/-) oder einen externen Diff-Tool-Hook (`git difftool`).
|
||||
- **Aufwand:** mittel. MVP: einfach nur den Diff-Output in einem Modal.
|
||||
|
||||
### 2.6 Status-Bar Active-Tasks Live-Update
|
||||
- **Datei:** `StatusBarViewModel`
|
||||
- **Risiko:** das Slot-State-Update kommt vom WorkerClient, aber `RunNowCommand.NotifyCanExecuteChanged` triggert nicht pro Item bei `IsConnected`-Wechsel (vom Slice-F-Agent dokumentiert).
|
||||
- **Soll:** Über `WeakReferenceMessenger` (CommunityToolkit.Mvvm) eine Connection-Change-Message verteilen, an die alle `TaskItemViewModel` lauschen.
|
||||
- **Aufwand:** klein, aber muss sauber gemacht werden.
|
||||
|
||||
### 2.7 Settings-Dialog
|
||||
- **Datei:** *neu* — `Views/SettingsDialog.axaml` + VM
|
||||
- **Aktuell:** `~/.todo-app/ui.config.json` muss von Hand editiert werden.
|
||||
- **Soll:** Dialog mit Feldern: DB-Pfad, SignalR-Port, Default-Tags. Persistiert zurück in JSON.
|
||||
- **Aufwand:** mittel. Achtung: Port-Wechsel braucht Worker-Restart.
|
||||
|
||||
---
|
||||
|
||||
## 3. Worker-Robustheit
|
||||
|
||||
### 3.1 CLI-Preflight beim Worker-Start
|
||||
- **Datei:** `src/ClaudeDo.Worker/Program.cs`
|
||||
- **Soll:** vor `app.Run()` `claude --version` ausführen; bei Fehler `app.Logger.LogCritical` + `Environment.Exit(1)`.
|
||||
- **Aufwand:** klein, ~20 Zeilen. Liefert Verification Step 2.
|
||||
|
||||
### 3.2 Worktree-Cleanup beim Anlege-Failed
|
||||
- **Datei:** `src/ClaudeDo.Worker/Runner/WorktreeManager.cs`
|
||||
- **Aktuell:** Wenn `WorktreeAddAsync` zwischen `CreateAsync`-Schritten failt (z.B. Branch existiert schon), bleibt evtl. ein halbangelegter Worktree-Dir auf der Platte.
|
||||
- **Soll:** try/finally — bei Fehler `git worktree remove --force` als Best-Effort-Cleanup.
|
||||
- **Aufwand:** klein.
|
||||
|
||||
### 3.3 Logging über `Microsoft.Extensions.Logging` strukturieren
|
||||
- **Datei:** alle Worker-Komponenten
|
||||
- **Aktuell:** ILogger wird benutzt, aber kein File-Sink konfiguriert.
|
||||
- **Soll:** Optional Serilog oder einfach `AddFile` (Karambolage.Extensions.Logging.File) — Service-Modus braucht persistente Logs außerhalb der Console.
|
||||
- **Aufwand:** klein.
|
||||
|
||||
### 3.4 Tag-Negation / Exclusion (Plan-TODO)
|
||||
- **Plan-Sektion:** "Tag-Modell"
|
||||
- **Aktuell:** Tags sind rein additiv (`list_tags ∪ task_tags`).
|
||||
- **Soll:** Mechanismus, um auf Task-Ebene einen List-Tag auszuschließen. Z.B. neue Tabelle `task_tag_exclusions` ODER ein Prefix `!tag` im task_tags-Eintrag.
|
||||
- **Aufwand:** mittel — Schema + Repo + Tests + UI.
|
||||
|
||||
---
|
||||
|
||||
## 4. Service-Deployment (Plan-Sektion „Worker als Windows-Service")
|
||||
|
||||
### 4.1 Windows-Service-Hosting in Code
|
||||
- **Datei:** `src/ClaudeDo.Worker/Program.cs`
|
||||
- **Pakete:** `Microsoft.Extensions.Hosting.WindowsServices`
|
||||
- **Soll:**
|
||||
```csharp
|
||||
builder.Host.UseWindowsService(o => o.ServiceName = "ClaudeDoWorker");
|
||||
builder.Logging.AddEventLog(...);
|
||||
```
|
||||
- **Aufwand:** klein.
|
||||
|
||||
### 4.2 Pfad-Auflösung absolut machen
|
||||
- Bereits in `WorkerConfig.Load` per `Paths.Expand` gemacht — verifizieren, dass auch `cfg.ClaudeBin` ggf. in Service-PATH gefunden wird.
|
||||
|
||||
### 4.3 Install-Skripte / Doku
|
||||
- **Datei:** *neu* — `docs/install-service.md` oder `scripts/install-service.cmd`
|
||||
- **Inhalt:** `dotnet publish` + `sc.exe create` + `sc.exe failure` + Hinweis auf `obj=` (User-Account) wegen Claude-CLI-Session.
|
||||
- **Aufwand:** klein.
|
||||
|
||||
### 4.4 (später) Installer-Projekt
|
||||
- WiX/MSIX, registriert Service + UI-Shortcut. Plan-Sektion „Offene Punkte".
|
||||
|
||||
---
|
||||
|
||||
## 5. Tests / CI
|
||||
|
||||
### 5.1 GitHub-Actions / Gitea-Actions Pipeline
|
||||
- **Datei:** *neu* — `.gitea/workflows/ci.yml` (oder `.github/workflows/ci.yml`)
|
||||
- **Inhalt:** `dotnet restore` → `dotnet build --no-restore` → `dotnet test --no-build`. Auf Push + PR.
|
||||
- **Aufwand:** klein.
|
||||
|
||||
### 5.2 Echter SignalR-Roundtrip-Test
|
||||
- **Datei:** *neu* — `tests/ClaudeDo.Worker.Tests/Hub/WorkerHubTests.cs`
|
||||
- **Soll:** mit `WebApplicationFactory` + `HubConnectionBuilder` testen, dass `Ping`, `GetActive`, `RunNow`-Throw-Verhalten korrekt sind. Plan-Verification 1b + 9.
|
||||
- **Aufwand:** mittel.
|
||||
|
||||
### 5.3 Smoke-Test gegen echten `claude`
|
||||
- **Datei:** *neu* — `tests/ClaudeDo.Worker.Tests/Runner/ClaudeProcessSmokeTest.cs`
|
||||
- **Soll:** Real-CLI-Test, der mit `[Fact(Skip="..."]` ausgegraut bleibt und nur lokal aktiviert wird, wenn `CLAUDE_AUTHENTICATED=1` Env-Var gesetzt ist.
|
||||
- **Aufwand:** klein.
|
||||
|
||||
---
|
||||
|
||||
## 6. Dokumentation
|
||||
|
||||
### 6.1 README.md
|
||||
- Komplett fehlt. Mind. 1× kurz: was ist es, wie starten (Worker + UI), wo Config.
|
||||
- **Aufwand:** klein.
|
||||
|
||||
### 6.2 `docs/architecture.md`
|
||||
- In `plan.md` schon teilweise enthalten — kann entweder konsolidiert oder explizit ausgegliedert werden.
|
||||
|
||||
### 6.3 ADRs für die getroffenen Entscheidungen
|
||||
- Z.B. „SignalR vs. SQLite-Polling für IPC", „Worktree pro Task", „SignalR über Loopback ohne Auth".
|
||||
- **Aufwand:** klein, hilfreich für später.
|
||||
|
||||
---
|
||||
|
||||
## 7. Bekannte Code-Schulden / Smells
|
||||
|
||||
| Stelle | Issue |
|
||||
|---|---|
|
||||
| `WorkerHub.GetActive` returnt `IReadOnlyList<object>` mit anonymen Typen | Sollte ein expliziter DTO sein (`ActiveTaskDto`), den Worker UND Ui teilen. Aktuell duplizieren beide das Schema. |
|
||||
| `TaskRunner` führt eine `if (list.WorkingDir != null)` Verzweigung mitten in der Methode | Strategy-Pattern (`IRunStrategy`: SandboxStrategy, WorktreeStrategy) wenn die Methode wächst. Aktuell noch klein genug. |
|
||||
| `App.Services` als public static `ServiceProvider` | Service-Locator-Antipattern. Toleriert, weil nur in `App.OnFrameworkInitializationCompleted` verwendet. Falls mehr Code drauf zugreift → echtes DI durchziehen. |
|
||||
| Embedded `schema.sql` ohne Versionierung | Solange das Schema nicht in Production läuft, OK. Sobald User-Daten existieren → `migrations/` Folder + Version-Tabelle. |
|
||||
| CRLF-Warnings beim Commit | `.gitattributes` mit `* text=auto eol=lf` (oder explizit pro Sprache) wäre sauberer. |
|
||||
|
||||
---
|
||||
|
||||
## Empfohlene Reihenfolge für die nächste Session
|
||||
|
||||
1. **Verification Step 4** zusammen durchspielen → falls etwas grundlegend kaputt ist, jetzt finden, nicht später.
|
||||
2. **CLI-Preflight (3.1)** + **Folder-Picker (2.1)** + **Delete-Confirm (2.2)** — kleine, isolierte Wins.
|
||||
3. **Auto-Scroll (2.4)** + **Active-Tasks Live-Update (2.6)** — User-Experience im Detail-Pane.
|
||||
4. **Markdown-Rendering (2.3)** — größer, lohnt sich aber für Lesbarkeit.
|
||||
5. **Worktree-Cleanup (3.2)** — Robustheit, bevor wir Worktrees ernsthaft nutzen.
|
||||
6. **CI-Pipeline (5.1)** — automatisches Sicherheitsnetz für alles weitere.
|
||||
7. **Service-Deployment (4)** — wenn die App lokal stabil läuft.
|
||||
8. **Settings-Dialog (2.7)** + **Diff-Viewer (2.5)** — Polish.
|
||||
9. **Tag-Negation (3.4)** — wenn der Bedarf konkret wird.
|
||||
|
||||
Punkte 1–3 sind ein realistischer Block für eine Session.
|
||||
2311
docs/superpowers/plans/2026-04-14-worker-cli-modernization.md
Normal file
2311
docs/superpowers/plans/2026-04-14-worker-cli-modernization.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,514 @@
|
||||
# Worker CLI Modernization
|
||||
|
||||
**Date:** 2026-04-14
|
||||
**Status:** Approved
|
||||
**Scope:** ClaudeDo.Worker — CLI invocation, execution tracking, per-task configuration, multi-turn support
|
||||
|
||||
## Problem
|
||||
|
||||
The Worker currently invokes Claude CLI with hardcoded flags (`-p --output-format stream-json --verbose --dangerously-skip-permissions`). There is no way to configure model, system prompt, or agent per list or task. Execution is single-shot with no retry or follow-up capability. Results are stored as a single markdown blob on the `tasks` row with no structured metadata, token usage, or turn count.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Per-list configuration (model, system prompt, agent file) with per-task overrides
|
||||
2. Execution history — each CLI invocation tracked as its own `task_runs` row
|
||||
3. Multi-turn support — manual continue and auto-retry via `--resume`
|
||||
4. Structured output alongside markdown via `--json-schema`
|
||||
5. Agent file management — filesystem-based `.md` agents with UI to browse/create/edit
|
||||
6. Richer stream parsing — token usage, turn count, session ID, retry events
|
||||
|
||||
## Non-Goals (Deferred)
|
||||
|
||||
- `--bare` mode (forces API key; user relies on OAuth/keychain auth)
|
||||
- `--allowedTools` / permission modes (keep `--dangerously-skip-permissions`)
|
||||
- Schema migration framework (use `IF NOT EXISTS` / `INSERT OR IGNORE` for additive changes)
|
||||
|
||||
---
|
||||
|
||||
## 1. Schema Changes
|
||||
|
||||
### 1.1 New table: `list_config`
|
||||
|
||||
One-to-one with `lists`. Stores per-list defaults for CLI invocation.
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS list_config (
|
||||
list_id TEXT PRIMARY KEY REFERENCES lists(id) ON DELETE CASCADE,
|
||||
model TEXT NULL, -- 'opus-4-6' | 'sonnet-4-6' | 'haiku-4-5'
|
||||
system_prompt TEXT NULL, -- appended via --append-system-prompt
|
||||
agent_path TEXT NULL -- path to agent .md file, passed via --agents
|
||||
);
|
||||
```
|
||||
|
||||
### 1.2 New columns on `tasks`
|
||||
|
||||
Per-task overrides. All nullable — NULL means "use list default".
|
||||
|
||||
```sql
|
||||
ALTER TABLE tasks ADD COLUMN model TEXT NULL;
|
||||
ALTER TABLE tasks ADD COLUMN system_prompt TEXT NULL;
|
||||
ALTER TABLE tasks ADD COLUMN agent_path TEXT NULL;
|
||||
```
|
||||
|
||||
Since schema uses `IF NOT EXISTS` and is re-applied on startup, these are added via `ALTER TABLE ... ADD COLUMN` wrapped in a try/catch (SQLite raises "duplicate column" if already present — safe to ignore).
|
||||
|
||||
### 1.3 New table: `task_runs`
|
||||
|
||||
One row per CLI invocation. Supports multi-turn and retry tracking.
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS task_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
run_number INTEGER NOT NULL, -- 1, 2, 3... sequential per task
|
||||
session_id TEXT NULL, -- Claude CLI session ID (for --resume)
|
||||
is_retry INTEGER NOT NULL DEFAULT 0, -- 0 = normal/continue, 1 = auto-retry
|
||||
prompt TEXT NOT NULL, -- the prompt sent for this run
|
||||
result_markdown TEXT NULL, -- free-form result from 'result' field
|
||||
structured_output TEXT NULL, -- JSON from 'structured_output' field
|
||||
error_markdown TEXT NULL, -- error output on failure
|
||||
exit_code INTEGER NULL, -- CLI exit code
|
||||
turn_count INTEGER NULL, -- number of agent loop turns
|
||||
tokens_in INTEGER NULL, -- total input tokens
|
||||
tokens_out INTEGER NULL, -- total output tokens
|
||||
log_path TEXT NULL, -- NDJSON log file for this run
|
||||
started_at TIMESTAMP NULL,
|
||||
finished_at TIMESTAMP NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_task_runs_task_id ON task_runs(task_id);
|
||||
```
|
||||
|
||||
### 1.4 Denormalized fields on `tasks`
|
||||
|
||||
Keep existing `result`, `log_path`, `started_at`, `finished_at` on the `tasks` table. After each run completes, update them with the latest run's values. This preserves backward compatibility for UI queries that read `tasks` directly.
|
||||
|
||||
### 1.5 Model validation
|
||||
|
||||
Valid model values: `opus-4-6`, `sonnet-4-6`, `haiku-4-5`. Validated at the application layer (repository/service), not via SQL CHECK constraint, to allow easy future additions.
|
||||
|
||||
---
|
||||
|
||||
## 2. Agent File Management
|
||||
|
||||
### 2.1 Directory
|
||||
|
||||
Agents live in `~/.todo-app/agents/`. The directory is created on Worker startup if absent.
|
||||
|
||||
### 2.2 File format
|
||||
|
||||
Standard Claude agent markdown with YAML frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: .NET Developer
|
||||
description: Senior .NET developer focused on clean architecture
|
||||
---
|
||||
|
||||
You are a senior .NET developer. Follow existing project patterns...
|
||||
```
|
||||
|
||||
### 2.3 AgentFileService
|
||||
|
||||
New service in `ClaudeDo.Worker` (not a repository — operates on filesystem, not DB):
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `ScanAsync()` | Returns `List<AgentInfo>` — parse frontmatter for name/description from all `*.md` in agents dir |
|
||||
| `ReadAsync(string path)` | Full file content |
|
||||
| `WriteAsync(string path, string content)` | Create or overwrite |
|
||||
| `DeleteAsync(string path)` | Remove file |
|
||||
|
||||
### 2.4 AgentInfo DTO
|
||||
|
||||
```csharp
|
||||
public sealed record AgentInfo(string Name, string Description, string Path);
|
||||
```
|
||||
|
||||
### 2.5 Discovery
|
||||
|
||||
- Worker scans on startup and exposes agents via a new SignalR method `GetAgents()`.
|
||||
- UI calls `GetAgents()` to populate dropdowns.
|
||||
- A `RefreshAgents()` hub method triggers a re-scan (for after UI creates/edits a file).
|
||||
|
||||
---
|
||||
|
||||
## 3. CLI Invocation Changes
|
||||
|
||||
### 3.1 Current invocation
|
||||
|
||||
```
|
||||
claude -p --output-format stream-json --verbose --dangerously-skip-permissions
|
||||
```
|
||||
|
||||
Prompt written to stdin. Single-shot, no config, no structured output.
|
||||
|
||||
### 3.2 New invocation
|
||||
|
||||
Built dynamically per run by `ClaudeArgsBuilder`:
|
||||
|
||||
```
|
||||
claude -p
|
||||
--output-format stream-json
|
||||
--verbose
|
||||
--dangerously-skip-permissions
|
||||
--model <resolved-model> # if set
|
||||
--append-system-prompt <resolved-prompt> # if set
|
||||
--agents '[{"file":"<resolved-agent-path>"}]' # if set
|
||||
--json-schema <schema-json> # always
|
||||
--resume <session-id> # only for multi-turn/retry
|
||||
```
|
||||
|
||||
### 3.3 Config resolution
|
||||
|
||||
```
|
||||
resolved_model = task.model ?? list_config.model ?? null (omit --model)
|
||||
resolved_prompt = task.system_prompt ?? list_config.system_prompt ?? null (omit --append-system-prompt)
|
||||
resolved_agent = task.agent_path ?? list_config.agent_path ?? null (omit --agents)
|
||||
```
|
||||
|
||||
### 3.4 Structured output schema
|
||||
|
||||
Passed via `--json-schema` on every invocation:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"summary": { "type": "string" },
|
||||
"files_changed": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"commit_type": { "type": "string" }
|
||||
},
|
||||
"required": ["summary"]
|
||||
}
|
||||
```
|
||||
|
||||
The CLI returns this in the `structured_output` field of the JSON result event. The markdown result remains in the `result` field.
|
||||
|
||||
### 3.5 ClaudeArgsBuilder
|
||||
|
||||
New class, single responsibility for argument construction:
|
||||
|
||||
```csharp
|
||||
public sealed class ClaudeArgsBuilder
|
||||
{
|
||||
// Returns the full argument string for ProcessStartInfo.Arguments
|
||||
public string Build(ClaudeRunConfig config);
|
||||
}
|
||||
|
||||
public sealed record ClaudeRunConfig(
|
||||
string? Model,
|
||||
string? SystemPrompt,
|
||||
string? AgentPath,
|
||||
string? ResumeSessionId
|
||||
);
|
||||
```
|
||||
|
||||
Testable in isolation — no process spawning, just string building.
|
||||
|
||||
---
|
||||
|
||||
## 4. Stream Parsing
|
||||
|
||||
### 4.1 StreamAnalyzer (replaces MessageParser)
|
||||
|
||||
Processes each NDJSON line and accumulates metrics:
|
||||
|
||||
| Responsibility | How |
|
||||
|---|---|
|
||||
| Extract result markdown | Look for `type: "result"`, read `.result` field |
|
||||
| Extract structured output | Same event, read `.structured_output` field |
|
||||
| Extract session ID | Read `.session_id` from the result event |
|
||||
| Count turns | Count events where `.type == "assistant"` |
|
||||
| Accumulate tokens | Sum `.usage.input_tokens` and `.usage.output_tokens` from each turn |
|
||||
| Track retries | Count `system/api_retry` events (informational logging) |
|
||||
|
||||
### 4.2 StreamResult
|
||||
|
||||
```csharp
|
||||
public sealed class StreamResult
|
||||
{
|
||||
public string? ResultMarkdown { get; set; }
|
||||
public string? StructuredOutputJson { get; set; }
|
||||
public string? SessionId { get; set; }
|
||||
public int TurnCount { get; set; }
|
||||
public int TokensIn { get; set; }
|
||||
public int TokensOut { get; set; }
|
||||
public int ApiRetryCount { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Extended RunResult
|
||||
|
||||
```csharp
|
||||
public sealed class RunResult
|
||||
{
|
||||
public required int ExitCode { get; init; }
|
||||
public string? ResultMarkdown { get; init; }
|
||||
public string? ErrorMarkdown { get; init; }
|
||||
public string? StructuredOutputJson { get; init; }
|
||||
public string? SessionId { get; init; }
|
||||
public int TurnCount { get; init; }
|
||||
public int TokensIn { get; init; }
|
||||
public int TokensOut { get; init; }
|
||||
|
||||
public bool IsSuccess => ExitCode == 0 && ResultMarkdown is not null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Multi-Turn & Auto-Retry
|
||||
|
||||
### 5.1 Execution flow
|
||||
|
||||
```
|
||||
Task queued
|
||||
-> Run 1 (run_number=1, is_retry=0)
|
||||
-> Resolve config (list defaults + task overrides)
|
||||
-> Build CLI args (no --resume on first run)
|
||||
-> Spawn claude, stream output, parse via StreamAnalyzer
|
||||
-> Create task_runs row with all metrics
|
||||
-> Update denormalized tasks fields
|
||||
|
||||
If failure (exit_code != 0):
|
||||
-> Auto-retry: Run 2 (run_number=2, is_retry=1)
|
||||
-> Prompt: "The previous attempt failed with:\n\n{error_markdown}\n\nTry again and fix the issues."
|
||||
-> Uses --resume <session_id> from Run 1
|
||||
-> Same worktree, same config
|
||||
-> Create new task_runs row
|
||||
-> If still fails: mark task Failed, stop
|
||||
|
||||
If success (exit_code == 0):
|
||||
-> Auto-commit in worktree if changes
|
||||
-> Mark task Done
|
||||
|
||||
User triggers "Continue" on finished/failed task:
|
||||
-> New run (run_number=N+1, is_retry=0)
|
||||
-> User-provided follow-up prompt
|
||||
-> Uses --resume <session_id> from last run
|
||||
-> Task status -> Running -> Done/Failed
|
||||
```
|
||||
|
||||
### 5.2 Rules
|
||||
|
||||
- Max 1 auto-retry per task execution (no retry loops)
|
||||
- Auto-retry reuses the session via `--resume` (full context of prior failure)
|
||||
- Manual continue works on both Done and Failed tasks
|
||||
- Each run gets its own log file: `{task_id}_run{N}.ndjson`
|
||||
- Worktree commit happens only after a successful run
|
||||
- If Run 1 has no session_id (edge case: CLI crashed before producing one), skip auto-retry
|
||||
|
||||
### 5.3 Continue via SignalR
|
||||
|
||||
New hub method: `ContinueTask(string taskId, string followUpPrompt)` -> returns `string runId`
|
||||
|
||||
Validation:
|
||||
- Task must exist
|
||||
- Task must not be currently running
|
||||
- Previous run must have a session_id
|
||||
|
||||
---
|
||||
|
||||
## 6. TaskRunner Refactoring
|
||||
|
||||
### 6.1 Current flow (TaskRunner.RunAsync)
|
||||
|
||||
1. Load list, create worktree/sandbox, mark running
|
||||
2. Build prompt from title + description
|
||||
3. Call `_claude.RunAsync(prompt, dir, logPath, taskId, callback, ct)`
|
||||
4. Handle result: commit on success, mark done/failed
|
||||
|
||||
### 6.2 New flow
|
||||
|
||||
```csharp
|
||||
public async Task RunAsync(TaskEntity task, string slot, CancellationToken ct)
|
||||
{
|
||||
// 1. Load list + list_config
|
||||
// 2. Resolve config (merge list_config + task overrides)
|
||||
// 3. Create worktree/sandbox (unchanged)
|
||||
// 4. Execute run (see RunOnceAsync below)
|
||||
// 5. If failed and no prior retry: auto-retry
|
||||
// 6. Final status update
|
||||
}
|
||||
|
||||
public async Task ContinueAsync(string taskId, string followUpPrompt, string slot, CancellationToken ct)
|
||||
{
|
||||
// 1. Load task, last run (for session_id)
|
||||
// 2. Mark task running
|
||||
// 3. Execute run with --resume
|
||||
// 4. Commit if success + worktree
|
||||
// 5. Final status update
|
||||
}
|
||||
|
||||
private async Task<RunResult> RunOnceAsync(
|
||||
TaskEntity task, string slot, string runDir, ClaudeRunConfig config,
|
||||
int runNumber, bool isRetry, string prompt, CancellationToken ct)
|
||||
{
|
||||
// 1. Create task_runs row (started_at = now)
|
||||
// 2. Build log path: {task_id}_run{runNumber}.ndjson
|
||||
// 3. Build CLI args via ClaudeArgsBuilder
|
||||
// 4. Spawn ClaudeProcess
|
||||
// 5. Stream lines to LogWriter + StreamAnalyzer + HubBroadcaster
|
||||
// 6. Build RunResult from StreamAnalyzer
|
||||
// 7. Update task_runs row (finished_at, metrics, result)
|
||||
// 8. Update denormalized tasks fields
|
||||
// 9. Return RunResult
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 ClaudeProcess changes
|
||||
|
||||
Simplified — receives pre-built args, no longer constructs its own:
|
||||
|
||||
```csharp
|
||||
public async Task<RunResult> RunAsync(
|
||||
string arguments, // pre-built by ClaudeArgsBuilder
|
||||
string prompt, // written to stdin
|
||||
string workingDirectory,
|
||||
Func<string, Task> onStdoutLine,
|
||||
CancellationToken ct)
|
||||
```
|
||||
|
||||
The `StreamAnalyzer` instance is owned by the caller (TaskRunner), not ClaudeProcess. ClaudeProcess just feeds lines via the callback.
|
||||
|
||||
---
|
||||
|
||||
## 7. Repository Changes
|
||||
|
||||
### 7.1 New: TaskRunRepository
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `AddAsync(TaskRunEntity)` | Insert new run |
|
||||
| `UpdateAsync(TaskRunEntity)` | Update after completion |
|
||||
| `GetByTaskIdAsync(string taskId)` | All runs for a task, ordered by run_number |
|
||||
| `GetLatestByTaskIdAsync(string taskId)` | Most recent run (for session_id lookup) |
|
||||
| `GetByIdAsync(string runId)` | Single run |
|
||||
|
||||
### 7.2 Extended: ListRepository
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `GetConfigAsync(string listId)` | Returns `ListConfigEntity?` |
|
||||
| `SetConfigAsync(ListConfigEntity)` | Upsert via INSERT OR REPLACE |
|
||||
|
||||
### 7.3 New models
|
||||
|
||||
```csharp
|
||||
public sealed class TaskRunEntity
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string TaskId { get; init; }
|
||||
public required int RunNumber { get; init; }
|
||||
public string? SessionId { get; set; }
|
||||
public required bool IsRetry { get; init; }
|
||||
public required string Prompt { get; init; }
|
||||
public string? ResultMarkdown { get; set; }
|
||||
public string? StructuredOutputJson { get; set; }
|
||||
public string? ErrorMarkdown { get; set; }
|
||||
public int? ExitCode { get; set; }
|
||||
public int? TurnCount { get; set; }
|
||||
public int? TokensIn { get; set; }
|
||||
public int? TokensOut { get; set; }
|
||||
public string? LogPath { get; set; }
|
||||
public DateTime? StartedAt { get; set; }
|
||||
public DateTime? FinishedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ListConfigEntity
|
||||
{
|
||||
public required string ListId { get; init; }
|
||||
public string? Model { get; set; }
|
||||
public string? SystemPrompt { get; set; }
|
||||
public string? AgentPath { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. SignalR Hub Changes
|
||||
|
||||
### 8.1 New server methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `ContinueTask(string taskId, string followUpPrompt)` | Trigger follow-up run. Returns `string runId`. Throws if running or no session. |
|
||||
| `GetAgents()` | Returns `List<AgentInfo>` from AgentFileService scan |
|
||||
| `RefreshAgents()` | Re-scan agents directory |
|
||||
|
||||
### 8.2 Updated broadcasts
|
||||
|
||||
| Event | Change |
|
||||
|-------|--------|
|
||||
| `TaskStarted(slot, taskId, runId, runNumber, startedAt)` | Added `runId`, `runNumber` |
|
||||
| `TaskFinished(slot, taskId, runId, status, finishedAt)` | Added `runId` |
|
||||
| `TaskMessage(taskId, runId, ndjsonLine)` | Added `runId` |
|
||||
| `RunCreated(taskId, runId, runNumber, isRetry)` | New — signals retry/continue started |
|
||||
|
||||
### 8.3 Unchanged
|
||||
|
||||
`Ping`, `GetActive`, `CancelTask`, `WakeQueue`, `WorktreeUpdated`, `TaskUpdated` — no changes.
|
||||
|
||||
---
|
||||
|
||||
## 9. File Structure (New/Changed)
|
||||
|
||||
```
|
||||
src/ClaudeDo.Worker/
|
||||
Runner/
|
||||
ClaudeArgsBuilder.cs NEW — CLI argument construction
|
||||
StreamAnalyzer.cs NEW — replaces MessageParser
|
||||
StreamResult.cs NEW — accumulated stream metrics
|
||||
RunResult.cs CHANGED — extended with tokens, turns, session_id
|
||||
ClaudeProcess.cs CHANGED — simplified, takes pre-built args
|
||||
TaskRunner.cs CHANGED — retry/continue logic, config resolution
|
||||
MessageParser.cs DELETED — replaced by StreamAnalyzer
|
||||
Services/
|
||||
AgentFileService.cs NEW — filesystem agent management
|
||||
|
||||
src/ClaudeDo.Data/
|
||||
Models/
|
||||
TaskRunEntity.cs NEW
|
||||
ListConfigEntity.cs NEW
|
||||
AgentInfo.cs NEW — DTO (name, description, path)
|
||||
Repositories/
|
||||
TaskRunRepository.cs NEW
|
||||
ListRepository.cs CHANGED — GetConfigAsync, SetConfigAsync
|
||||
|
||||
schema/
|
||||
schema.sql CHANGED — list_config table, task_runs table, tasks columns
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Testing Strategy
|
||||
|
||||
### 10.1 Unit tests (new)
|
||||
|
||||
| Test class | Covers |
|
||||
|------------|--------|
|
||||
| `ClaudeArgsBuilderTests` | Arg construction with all config combos, omitted flags for null values |
|
||||
| `StreamAnalyzerTests` | Turn counting, token accumulation, result extraction, session_id, retry events, malformed input |
|
||||
| `AgentFileServiceTests` | Scan, frontmatter parsing, read/write/delete, missing directory handling |
|
||||
|
||||
### 10.2 Unit tests (updated)
|
||||
|
||||
| Test class | Changes |
|
||||
|------------|---------|
|
||||
| `TaskRunnerTests` | New: auto-retry flow, continue flow, config resolution |
|
||||
| `QueueServiceTests` | New: continue task routing |
|
||||
|
||||
### 10.3 Integration tests (new)
|
||||
|
||||
| Test class | Covers |
|
||||
|------------|--------|
|
||||
| `TaskRunRepositoryTests` | CRUD, ordering, latest-by-task queries |
|
||||
| `ListRepositoryConfigTests` | GetConfig, SetConfig upsert behavior |
|
||||
|
||||
### 10.4 Existing tests (MessageParserTests)
|
||||
|
||||
Removed along with `MessageParser`. Equivalent coverage moves to `StreamAnalyzerTests`.
|
||||
Reference in New Issue
Block a user