docs(tickets): document cross-project ticket import and open checks

This commit is contained in:
mika kuns
2026-10-01 08:45:54 +02:00
parent cb775958a7
commit 7724f5eb70
3 changed files with 30 additions and 7 deletions
+3 -2
View File
@@ -1,8 +1,8 @@
# External MCP tool surface # External MCP tool surface
> **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative. > **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative.
> Last verified against commit `38af549` (2026-08-11). > Last verified against commit `cb775958` (2026-10-01).
> Drift check: `git log --oneline 38af549..HEAD -- src/ClaudeDo.Worker/External` > Drift check: `git log --oneline cb775958..HEAD -- src/ClaudeDo.Worker/External`
> Stable structure only (no line numbers). See docs/explore-notes/README.md. > Stable structure only (no line numbers). See docs/explore-notes/README.md.
Covers `src/ClaudeDo.Worker/External/` — the always-on MCP tools ClaudeDo exposes to general Covers `src/ClaudeDo.Worker/External/` — the always-on MCP tools ClaudeDo exposes to general
@@ -99,6 +99,7 @@ Daily prep: `GetDailyPrepCandidates`, `SetMyDay`.
| `QueueStateMcpTools` | `GetQueueState` | | `QueueStateMcpTools` | `GetQueueState` |
| `AttachmentMcpTools` | `AddTaskAttachment`, `ListTaskAttachments`, `RemoveTaskAttachment` | | `AttachmentMcpTools` | `AddTaskAttachment`, `ListTaskAttachments`, `RemoveTaskAttachment` |
| `ListHandlerMcpTools` | `StartListHandler` | | `ListHandlerMcpTools` | `StartListHandler` |
| `TicketMcpTools` | `GetTicketImportCandidates` (open assigned ticket-system items not yet imported + all lists + fallback list; `available=false` + `reason` when no ticket system), `ImportTicket` (one Idle task per item; no `list` → fallback list; `created=false` always carries `skippedReason`) |
## Per-tool behaviour worth knowing ## Per-tool behaviour worth knowing
+11
View File
@@ -74,6 +74,17 @@ Bleibt manuell zu prüfen (UI-Pfad, nie sichtbar gelaufen):
- [ ] Base-URL leeren → Menüeintrag verschwindet, keine Netzwerkaufrufe mehr. - [ ] Base-URL leeren → Menüeintrag verschwindet, keine Netzwerkaufrufe mehr.
- [ ] Sichtprüfung des Settings-Tabs + des Ticket-Projekt-Dropdowns (nie gerendert gesehen). - [ ] Sichtprüfung des Settings-Tabs + des Ticket-Projekt-Dropdowns (nie gerendert gesehen).
## Offene Verifikation (2026-10-01, Ticket-Import über alle Projekte)
Build + Unit-Tests grün, **nicht visuell/live verifiziert**:
- [ ] Sichtprüfung: Button „Tickets + Tag planen" im Prep-Panel (nur sichtbar mit Base-URL + Token),
Fallback-Listen-Dropdown in Settings → Ticketsystem, Radio „Tickets + Tag planen" im Prime-Tab.
- [ ] E2E: Button einmal gegen das echte Ticketsystem klicken — Items landen in sinnvollen Listen, die
Fallback-Liste fängt den Rest, danach ist MyDay gefüllt; ein zweiter Klick importiert nichts Neues.
- [ ] `PATCH /api/Ticket/{id}/status` auf einem **Task** (nicht Ticket) ist ungetestet — nachdem der erste
importierte Task den Status wechselt, im Worker-Log auf eine `TicketStatusSync`-Warnung prüfen.
## Offene Verifikation (2026-07-27) ## Offene Verifikation (2026-07-27)
- **Empty-Repo-Hardening (2026-07-30)** — Worker/App neu starten (laufende Instanz hat den - **Empty-Repo-Hardening (2026-07-30)** — Worker/App neu starten (laufende Instanz hat den
+16 -5
View File
@@ -144,21 +144,25 @@ a follow-up prompt to an existing session via `--resume <session_id>`.
## Prime Claude ## Prime Claude
Every schedule carries a `PrimeActionKind` (`Ping` — the default for new schedules — `FillMyDay`, Every schedule carries a `PrimeActionKind` (`Ping` — the default for new schedules — `FillMyDay`,
or `Custom`). `PrimeRunner.FireAsync` holds one `SemaphoreSlim` gate for **all** kinds, then `ImportTickets`, or `Custom`). `PrimeRunner.FireAsync` holds one `SemaphoreSlim` gate for **all** kinds, then
branches: branches:
| Kind | Prompt | Args | Log + `Prep*` events | | Kind | Prompt | Args | Log + `Prep*` events |
|---|---|---|---| |---|---|---|---|
| `Ping` | `PrimePrompts.PingPrompt`, fixed | `--max-turns 1 --strict-mcp-config` (no MCP server loads at all) | none | | `Ping` | `PrimePrompts.PingPrompt`, fixed | `--max-turns 1 --strict-mcp-config` (no MCP server loads at all) | none |
| `FillMyDay` | `DailyPrepPrompt.BuildPrompt` (+ the schedule's prompt as an *addition*) | the two MyDay MCP tools | `daily-prep.log`, truncated per run, + `PrepStarted`/`PrepLine`/`PrepFinished` | | `FillMyDay` | `DailyPrepPrompt.BuildPrompt` (+ the schedule's prompt as an *addition*) | the two MyDay MCP tools | `daily-prep.log`, truncated per run, + `PrepStarted`/`PrepLine`/`PrepFinished` |
| `ImportTickets` | `TicketImportPrompt.BuildPrompt`, then the FillMyDay prompt (override appended to both) | step 1: `get_ticket_import_candidates` + `import_ticket`, max-turns 60; step 2: as FillMyDay | one `daily-prep.log` for both steps, one `PrepStarted`/`PrepFinished` |
| `Custom` | the schedule's prompt *verbatim* | `--allowedTools mcp__claudedo` — no Read/Write/Edit/Bash | none | | `Custom` | the schedule's prompt *verbatim* | `--allowedTools mcp__claudedo` — no Read/Write/Edit/Bash | none |
Ping and Custom deliberately write **no** log and raise **no** `Prep*` events: the prep log Ping and Custom deliberately write **no** log and raise **no** `Prep*` events: the prep log
belongs to the MyDay selection and would otherwise be overwritten with ping noise. All three belongs to the MyDay selection and would otherwise be overwritten with ping noise. All four
kinds still broadcast `PrimeFired` and update `last_run_at`. kinds still broadcast `PrimeFired` and update `last_run_at`.
`WorkerHub.RunDailyPrepNow` (the "Plan day" button) pins its synthetic schedule to `FillMyDay` — `WorkerHub.RunDailyPrepNow` (the "Plan day" button) pins its synthetic schedule to `FillMyDay` —
the button means "fill my day" no matter how the schedules are configured. the button means "fill my day" no matter how the schedules are configured.
`WorkerHub.RunTicketImportNow` (the "Tickets + plan day" button) does the same for `ImportTickets`.
A failed import step never blocks the FillMyDay step; without a configured ticket system the kind
runs plain FillMyDay.
`WorkerHub.UpsertPrimeSchedule` runs `PrimeScheduleValidation.Validate` first, so a `Custom` `WorkerHub.UpsertPrimeSchedule` runs `PrimeScheduleValidation.Validate` first, so a `Custom`
schedule can never be saved without a prompt. schedule can never be saved without a prompt.
@@ -287,6 +291,12 @@ distinguish the two singletons). The token never touches `worker.config.json` or
2026-09-01), so the original filter imported nothing, ever. `personInChargeName` is the wrong 2026-09-01), so the original filter imported nothing, ever. `personInChargeName` is the wrong
source too: it holds the login on some rows and the display name ("Mika Kuns") on others, while source too: it holds the login on some rows and the display name ("Mika Kuns") on others, while
`ticketAssignees[].assigneeName` matches `/pat/me`'s `userName` format exactly. `ticketAssignees[].assigneeName` matches `/pat/me`'s `userName` format exactly.
- **Cross-project import** (Prime `ImportTickets`, MCP `get_ticket_import_candidates` / `import_ticket`) —
`TicketImportService.GetCandidatesAsync`/`ImportItemAsync` sweep sidebar → every project board. Tickets
match on `ticketAssignees`, tasks (`tasks[]`) on `ticketTaskAssignees`; board tasks with `hasChildren` are
skipped. Dedupe on `TicketRef` is across all lists (also in the per-list `ImportAsync`). `import_ticket`
without `list` uses `AppSettings.TicketFallbackListId` (written only via `SetTicketFallbackListIdAsync` /
hub `SetTicketFallbackList`, never by `UpdateAsync`); with no fallback set the item is skipped.
- **Write-back** (`TicketStatusSync`) hangs off `TaskStateService.NotifyAsync` — the one place every - **Write-back** (`TicketStatusSync`) hangs off `TaskStateService.NotifyAsync` — the one place every
status transition passes through. Mapping: `Running`/`WaitingForReview` → ticket status `2 status transition passes through. Mapping: `Running`/`WaitingForReview` → ticket status `2
InBearbeitung`, `Done` → `3 Fertig`, everything else writes nothing (`1 Offen` is never written InBearbeitung`, `Done` → `3 Fertig`, everything else writes nothing (`1 Offen` is never written
@@ -294,10 +304,11 @@ distinguish the two singletons). The token never touches `worker.config.json` or
`bandel:<id>`, is a no-op before any DB read. Per-process de-dupe (`taskId → last-written status`) `bandel:<id>`, 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 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. a warning and does not block the status transition or the queue.
- `BandelTicketClient` wraps the four REST calls (`GET /api/ticketsystem/pat/me`, `GET - `BandelTicketClient` wraps the five REST calls (`GET /api/ticketsystem/pat/me`, `GET
/api/Navigation/sidebar`, `GET /api/Board/project/{id}`, `PATCH /api/Ticket/{id}/status`); every /api/Navigation/sidebar`, `GET /api/Board/project/{id}`, `GET /api/ticket/{id}` — works for tasks too,
live-verified 2026-10-01 — `PATCH /api/Ticket/{id}/status`); every
response is unwrapped from `BandelEnvelope<T>` and a non-2xx/`success:false` throws response is unwrapped from `BandelEnvelope<T>` and a non-2xx/`success:false` throws
`TicketApiException` with a readable message. All four are **live-verified (2026-09-01)** against `TicketApiException` with a readable message. The first four are **live-verified (2026-09-01)** against
`http://192.168.45.161`. `http://192.168.45.161`.
⚠️ `PATCH /api/Ticket/{id}/status` needs **both** `statusId` and `statusName` — omitting the name ⚠️ `PATCH /api/Ticket/{id}/status` needs **both** `statusId` and `statusName` — omitting the name
is a 400 (`"The StatusName field is required."`) and no status is written. The name is *not* is a 400 (`"The StatusName field is required."`) and no status is written. The name is *not*