diff --git a/docs/open.md b/docs/open.md index 366d660f..8e63125b 100644 --- a/docs/open.md +++ b/docs/open.md @@ -44,6 +44,28 @@ verifiziert und deshalb hier entfernt. Es bleiben die Entscheidungen, die daran --- +## Offene Verifikation (2026-08-27, Ticketsystem-Anbindung) + +Tasks 1–9 sind implementiert und gemergt (Build + Tests grün, `_broadcaster.TaskUpdated` genau +einmal in `TaskStateService.cs`), aber **kein Smoke-Test gegen die echte Bandel-API ist +gelaufen**. Die fünf Punkte aus der „Manuellen Verifikation" des Specs +(`docs/superpowers/specs/2026-08-27-ticketsystem-integration-design.md`): + +- [ ] PAT in den Settings hinterlegen, „Verbindung testen" → korrekter Benutzername + Scopes. +- [ ] Liste mit Projekt `393 Bandel.LagerApp` verknüpfen, „Update from Ticketsystem" → nur + eigene, offene Tickets erscheinen als Tasks. +- [ ] Einen Task laufen lassen → Ticket steht im Ticketsystem auf `InBearbeitung`. +- [ ] Approve/Merge → Ticket steht auf `Fertig`. +- [ ] Base-URL leeren → Menüeintrag verschwindet, keine Netzwerkaufrufe mehr. + +**Wichtig:** die Response-Shapes der Bandel-API (`BandelEnvelope`, `TicketIdentity`, +`TicketBoardItem`, `SidebarDepartment`/`SidebarProject`, `TicketDtos.cs`) wurden aus dem +**Quellcode** von `Bandel.APIs` abgeleitet, **nie live gegen die echte API verifiziert** — es gab +keinen einzigen echten Netzwerkaufruf während der Implementierung. Die Punkte 1–4 oben sind +zugleich der erste Test dieser Annahme: schlägt einer davon mit einem Deserialisierungs- oder +404/Formatfehler fehl, ist das erwartbar und kein Regressions-Bug — dann müssen die DTOs in +`src/ClaudeDo.Worker/Tickets/TicketDtos.cs` an die tatsächliche Antwort angepasst werden. + ## Offene Verifikation (2026-07-27) - **List handler (2026-07-27)** — visual pass: the Broom button is gone from the lists footer, diff --git a/src/ClaudeDo.Data/CLAUDE.md b/src/ClaudeDo.Data/CLAUDE.md index 961e083f..6fdac5e2 100644 --- a/src/ClaudeDo.Data/CLAUDE.md +++ b/src/ClaudeDo.Data/CLAUDE.md @@ -4,15 +4,16 @@ Shared data layer: models, repositories, SQLite infrastructure, and git operatio ## Models -- **TaskEntity** — Id, ListId, Title, Description, Status, PlanningPhase, BlockedByTaskId (FK to predecessor in a chain), DependsOnTaskId (FK to a user/MCP-declared predecessor, distinct from BlockedByTaskId), ScheduledFor, Result, ReviewFeedback, LogPath, timestamps, CommitType, Model / SystemPrompt / AgentPath / MaxTurns / PermissionMode (nullable overrides), IsStarred, IsMyDay, IsManual, Notes, ParentTaskId, PlanningSessionId / PlanningSessionToken / PlanningFinalizedAt, CreatedBy, HandlerBaseCommit / HandlerHeadCommit, InteractiveSessionId, Number (global immutable human-readable alias). +- **TaskEntity** — Id, ListId, Title, Description, Status, PlanningPhase, BlockedByTaskId (FK to predecessor in a chain), DependsOnTaskId (FK to a user/MCP-declared predecessor, distinct from BlockedByTaskId), ScheduledFor, Result, ReviewFeedback, LogPath, timestamps, CommitType, Model / SystemPrompt / AgentPath / MaxTurns / PermissionMode (nullable overrides), IsStarred, IsMyDay, IsManual, Notes, ParentTaskId, PlanningSessionId / PlanningSessionToken / PlanningFinalizedAt, CreatedBy, HandlerBaseCommit / HandlerHeadCommit, InteractiveSessionId, TicketRef, Number (global immutable human-readable alias). + - `TicketRef` (`ticket_ref`, nullable string) — link to an external ticket, format `:` (currently always `bandel:`). Set by the ticket import, otherwise null. → `ClaudeDo.Worker/CLAUDE.md` Tickets section. - Status / PlanningPhase / BlockedByTaskId / DependsOnTaskId semantics + allowed transitions: `ClaudeDo.Worker/CLAUDE.md` → Status Model. - `HandlerBaseCommit`/`HandlerHeadCommit` = the review range for a **worktree-less "list handler" host task** ("Let Claude handle it"), which commits straight into the list's working dir instead of a per-task worktree. Everything that reads a task's diff falls back to this pair whenever `Worktree` is null → [conpty-sessions](../../docs/explore-notes/conpty-sessions.md). - `InteractiveSessionId` = the claude session id an embedded ConPTY interactive task session runs under, persisted by `InteractiveLaunchSpecService` before launch so a closed/aborted session can be resumed → [conpty-sessions](../../docs/explore-notes/conpty-sessions.md). - `Number` (INTEGER NOT NULL, unique index `idx_tasks_number`) — a global, monotonically increasing display alias for tasks. **Never reused**: deleting a task leaves a gap in numbering, ensuring that a task number that appears in a log or report always points to the same task (if it exists). Allocated by `TaskNumberAllocator.AddWithNumberAsync` on every insert via a persistent counter (`app_settings.next_task_number`). **Why not `MAX(number) + 1`:** deleting the highest-numbered task would free its number for reuse, breaking the immutability guarantee. The allocator uses an `UPDATE…RETURNING` statement to claim numbers atomically, retrying up to 5 times if a uniqueness collision occurs (the counter advances on each attempt, so retries always allocate fresh numbers). The one-time backfill for pre-existing rows lived in the `AddTaskNumbers` migration, squashed away 2026-08-26. - Legacy status values `Manual`/`Planning`/`Planned`/`Draft`/`Waiting` were retired. The backfill migration was squashed away (2026-08-26) — a database still holding those values can't be upgraded, see Schema below. - **ListEntity** — Id, Name, WorkingDir, DefaultCommitType, CreatedAt, IsManual (reminder list — tasks created here default to `IsManual`) -- **ListConfigEntity** — ListId (PK, 1:1), Model, SystemPrompt, AgentPath, MaxTurns, SessionSkills, VerifyCommand, PermissionMode (all nullable) + SerializeOnFileOverlap (bool). `VerifyCommand` is an optional post-merge gate; null/blank = no gate → [review-merge](../../docs/explore-notes/review-merge.md). `PermissionMode` is null = inherit the global default; resolution order task → list → global lives in `EffectiveRunConfigResolver`. - - ⚠️ `ListRepository.SetConfigAsync` copies the entity **verbatim**, so every writer must carry the fields it doesn't own (`SessionSkills`, `SerializeOnFileOverlap`, `PermissionMode`) or they silently reset. `UpdateListConfigDto.SerializeOnFileOverlap` is tri-state (`null` = keep stored) for exactly that reason — only the list-settings modal sends an explicit value. +- **ListConfigEntity** — ListId (PK, 1:1), Model, SystemPrompt, AgentPath, MaxTurns, SessionSkills, VerifyCommand, PermissionMode, TicketProjectId (all nullable) + SerializeOnFileOverlap (bool). `VerifyCommand` is an optional post-merge gate; null/blank = no gate → [review-merge](../../docs/explore-notes/review-merge.md). `PermissionMode` is null = inherit the global default; resolution order task → list → global lives in `EffectiveRunConfigResolver`. `TicketProjectId` (`ticket_project_id`) links the list to a project in the external ticket system; null = not linked → `ClaudeDo.Worker/CLAUDE.md` Tickets section. + - ⚠️ `ListRepository.SetConfigAsync` copies the entity **verbatim**, so every writer must carry the fields it doesn't own (`SessionSkills`, `SerializeOnFileOverlap`, `PermissionMode`, `TicketProjectId`) or they silently reset. `UpdateListConfigDto.SerializeOnFileOverlap` and `TicketProjectId` are tri-state (`null` = keep stored) for exactly that reason — only the list-settings modal sends an explicit value. This bit `PermissionMode` for real: its update-branch copy was missing from `SetConfigAsync` until `f0a3a186`, so a list's permission-mode override only ever stuck on the list's *first* save, never on a later one. - **WorktreeEntity** — TaskId (PK, 1:1), Path, BranchName, BaseCommit, HeadCommit, DiffStat, MergeCommit (nullable — SHA of the merge commit this branch produced; the only thing making `revert_merge` possible without searching `git log`), State (`Active|Merged|Discarded|Kept`) - **TaskRunEntity** — per-run record: session_id, turns, result, structured output, exit code, log path, nullable `Model` (what the run actually executed with), and `TokensIn`/`TokensOut`/`CacheReadTokens`/`CacheWriteTokens`. ⚠️ Token fields come from the **session transcript**, not the stream-json event, as a per-run delta → [usage-monitoring](../../docs/explore-notes/usage-monitoring.md). - **PrimeScheduleEntity** — Id, Days (`[Flags] PrimeDays` weekday bitmask, column `days_of_week`), TimeOfDay, Enabled, LastRunAt, PromptOverride, Kind (`PrimeActionKind`, column `action_kind`, default `ping`), CreatedAt. Recurs on selected weekdays; no date range. `PromptOverride`'s role depends on `Kind`: ignored for `Ping`, appended to the daily-prep prompt for `FillMyDay`, and the entire prompt for `Custom`. diff --git a/src/ClaudeDo.Worker/CLAUDE.md b/src/ClaudeDo.Worker/CLAUDE.md index e8cbc1c1..fd759e70 100644 --- a/src/ClaudeDo.Worker/CLAUDE.md +++ b/src/ClaudeDo.Worker/CLAUDE.md @@ -36,6 +36,10 @@ Worker/ Prime/ — "Prime Claude" schedules: PrimeScheduler, PrimeRunner, PrimePrompts, DailyPrepPrompt, PrimeScheduleValidation, NextDueCalculator, PrimeScheduleSignal Online/ — optional Online Inbox sync (off by default; zero network when disabled) + Tickets/ — Bandel ticket-system integration (per-list import + status write-back), + inactive without ticket_api_base_url: TicketSystemConfig/TicketClientFactory, + BandelTicketClient, TicketDtos, TicketStatusMap, TicketStatusSync, + TicketImportService Usage/ — OAuth usage monitor, gate, throttle, per-session token reader; TokenTracker/ = the external analytics backend (cost + per-model/per-task breakdown) ``` @@ -46,7 +50,7 @@ subfolder within their area; the namespace stays the area namespace. ## Architecture - **Program.cs** — loads config, inits schema, registers DI, configures SignalR on `/hub`, binds to `127.0.0.1:47821` -- **TaskStateService** — the **only** component that writes `Status`, `PlanningPhase`, `BlockedByTaskId`, `DependsOnTaskId`. All transitions return a `TransitionResult` (no exceptions on invalid moves). Wakes the queue and broadcasts `TaskUpdated` automatically; advances the planning chain on child terminal transitions. `SetDependsOnAsync` rejects a self-reference, an unknown dependency id, or a link that would create a cycle (walks the proposed predecessor's own `DependsOnTaskId` chain). +- **TaskStateService** — the **only** component that writes `Status`, `PlanningPhase`, `BlockedByTaskId`, `DependsOnTaskId`. All transitions return a `TransitionResult` (no exceptions on invalid moves). Wakes the queue and advances the planning chain on child terminal transitions. Every transition broadcasts via a private `NotifyAsync(taskId)` — never call `_broadcaster.TaskUpdated` directly from this class, `NotifyAsync` also drives `TicketStatusSync.SyncAsync` (see Tickets below) and a direct call skips that transition's ticket write-back. `SetDependsOnAsync` rejects a self-reference, an unknown dependency id, or a link that would create a cycle (walks the proposed predecessor's own `DependsOnTaskId` chain). - **IQueueWaker / IQueuePicker / QueueService** — waker is a singleton `SemaphoreSlim`; picker performs the atomic `Queued → Running` claim filtered by `BlockedByTaskId IS NULL`, `is_manual = 0`, schedule, and (`DependsOnTaskId IS NULL` OR the dependency's `Status = 'done'`); QueueService is a thin `BackgroundService` looping on the waker, dispatching via `TaskRunner`. Per tick it also applies the usage throttle and gate → [usage-monitoring](../../docs/explore-notes/usage-monitoring.md). - **RunCancellationRegistry** — taskId → running-run CTS. Lets `TaskStateService.CancelAsync` kill a cancelled task's process without a DI cycle. - **OverrideSlotService** — owns `RunNow` / `ContinueTask`; goes through `TaskStateService.StartRunningAsync` (caller-driven, serialized by slot lock). @@ -245,10 +249,12 @@ non-obvious, behaviour-changing. A fixed bug is git history, not a finding. - `db_path`, `sandbox_root`, `log_root` - `worktree_root_strategy` (`sibling` | `central`), `central_worktree_root` - `queue_backstop_interval_ms` (30000) — also the gate/throttle recovery timer -- `signalr_port` (47821), `claude_bin` — `claude_bin` is the one field the UI can write - (Settings → Dateien → CLAUDE CLI, hub `GetClaudeBin`/`SetClaudeBin`). It edits the DI-singleton - `WorkerConfig` in place *and* persists via `SaveClaudeBin`, so it applies to the next spawn - without a restart. Every other field here is hand-edit only. +- `signalr_port` (47821), `claude_bin`, `ticket_api_base_url` — the two UI-writable fields + (`claude_bin`: Settings → Dateien → CLAUDE CLI, hub `GetClaudeBin`/`SetClaudeBin`; + `ticket_api_base_url`: Settings → Ticketsystem, hub `GetTicketSettings`/`SetTicketApiBaseUrl`). + Both edit the DI-singleton `WorkerConfig` in place *and* persist via their own `SaveKey` call + (`SaveClaudeBin`/`SaveTicketApiBaseUrl`), so each applies without a restart. Every other field + here is hand-edit only. - `usage_poll_interval_seconds` (60, clamped to min 15 on load) - `online_inbox` — `enabled` (false by default; when false the entire `Online/` stack is not registered), `api_base_url` (must be HTTPS or loopback, validated at startup), `poll_interval_seconds` (60), `zitadel.authority`/`client_id`/`scopes`. The refresh token is **not** in this file — DPAPI-encrypted at `~/.claudeDo/online-inbox.token`. @@ -258,8 +264,35 @@ Per-list config (`list_config` in DB) provides defaults for `model`, `system_pro `get_effective_run_config` can't drift). `verify_command` and `serialize_on_file_overlap` are list-only — no task-level override. `verify_command` is written via `set_list_config`'s `verifyCommand` parameter (`ConfigMcpTools`) and the UI's list config editor; -`serialize_on_file_overlap` and `permission_mode` are UI-only (`set_list_config` preserves but -does not expose them). +`serialize_on_file_overlap`, `permission_mode`, and `ticket_project_id` are UI-only +(`set_list_config` preserves but does not expose them). + +## Tickets (Bandel ticket system) + +Optional, off by default. `TicketSystemConfig.IsConfigured` is the single gate (`worker.config.json`'s +`ticket_api_base_url` set **and** a PAT present) — false costs nothing: no DB query, no network. The +PAT is DPAPI-encrypted at `~/.claudeDo/ticket.pat` via `DpapiTokenStore` (same class the Online Inbox +refresh token uses, one instance per file, DI markers `OnlineRefreshTokenStore`/`TicketPatStore` +distinguish the two singletons). The token never touches `worker.config.json` or the DB. + +- **Import** (`TicketImportService`, hub `ImportTickets`) — per-list, manual only (no polling): pulls + the linked project's board (`ListConfigEntity.TicketProjectId`), keeps tickets that are `StatusId + == 1` (Offen) *and* assigned to the PAT owner (case-insensitive name match), skips any `TicketRef` + already present on the list, and creates one `Idle` task per remainder via `TaskRepository.AddAsync` + (so it gets a `Number` like every other task). Already-imported tickets are never updated — a + re-import would overwrite task notes. +- **Write-back** (`TicketStatusSync`) hangs off `TaskStateService.NotifyAsync` — the one place every + status transition passes through. Mapping: `Running`/`WaitingForReview` → ticket status `2 + InBearbeitung`, `Done` → `3 Fertig`, everything else writes nothing (`1 Offen` is never written + back — it's the entry state). A task without a `TicketRef`, or a `TicketRef` not shaped + `bandel:`, is a no-op before any DB read. Per-process de-dupe (`taskId → last-written status`) + skips the redundant `Running → WaitingForReview` PATCH. Never throws — a ticket-system failure logs + a warning and does not block the status transition or the queue. +- `BandelTicketClient` wraps the four REST calls (`GET /api/ticketsystem/pat/me`, `GET + /api/Navigation/sidebar`, `GET /api/Board/project/{id}`, `PATCH /api/Ticket/{id}/status`); every + response is unwrapped from `BandelEnvelope` and a non-2xx/`success:false` throws + `TicketApiException` with a readable message. ⚠️ The endpoint shapes were taken from the + `Bandel.APIs` source, never confirmed against a live call — see `docs/open.md`. ## Notes