--- title: "Tweede Claude Agent — Planning Agent" status: proposal audience: [maintainer, contributor] language: nl last_updated: 2026-05-03 applies_to: [] --- # Plan: Tweede Claude Agent — Planning Agent (PBI/Story → children) > **Eerder goedgekeurd plan in deze file:** *Scrum4Me v1.0 Release* (mobile shell + sprint-snapshots + release-discipline). Beschikbaar in chat-history; te verhuizen naar `docs/plans/v1-release.md` op een later moment. Dit nieuwe plan vervangt de plan-file inhoudelijk niet — het v1.0-werk blijft van kracht parallel hieraan. --- ## Context **Wat de gebruiker wil:** twee gespecialiseerde Claude-agents, elk met eigen context. | Agent | Doel | Context | Status | |---|---|---|---| | **Implementation Agent** | Codeert één task af → branch + PR | Code-repo (filesystem), `implementation_plan`, story, pbi, sprint, repo_url | ✅ Live (M13 / ST-1111) | | **Planning Agent** *(nieuw)* | Genereert children: PBI→stories of story→tasks (incl. `implementation_plan` per task) | Specs + architectuur + patterns (filesystem in Scrum4Me-checkout), parent-record, bestaande children | ❌ Te bouwen | **Wat al staat (hergebruikbaar):** - `ClaudeJob`-model met state-machine `QUEUED→CLAIMED→RUNNING→DONE/FAILED`, `CANCELLED`-pad, stale-cleanup >30min - `ClaudeWorker`-model voor presence-heartbeat - SSE-pijplijn op `/api/realtime/solo` met payload-routing op `user_id + product_id` - MCP-tools `wait_for_job`, `update_job_status`, `get_claude_context`, `create_pbi`, `create_story`, `create_task`, `update_task_plan`, `log_implementation` - Idempotency-pattern: max 1 actieve job per resource **Vastgelegde keuzes (uit AskUserQuestion-sessies):** 1. Agent kan **beide** niveaus: PBI→stories én story→tasks (één agent, twee modi via `target_type`) 2. Output: items **direct in DB** aanmaken via bestaande `create_*`-tools — geen review-stap in v1 3. Context: **lokaal draaien + filesystem-toegang** tot Scrum4Me-checkout (zoals impl-agent al doet) 4. Rol-scheiding: **`ClaudeJob.kind` enum** (`IMPLEMENTATION` | `PLANNING`) — één table, polymorf 5. Bestaande children: **aanvullen** — agent leest bestaande titels en voegt alleen ontbrekende toe 6. Live feedback: **stille SSE + status-pill** op de PBI/Story-card; geen aparte modal 7. MCP-shape: **bestaande `wait_for_job` uitbreiden** met `accept_kinds: string[]`-arg, default `['IMPLEMENTATION']` (backwards-compat) --- ## Approach (8 stappen) ### Stap 1 — Schema-uitbreiding **`prisma/schema.prisma`:** ```prisma enum ClaudeJobKind { IMPLEMENTATION PLANNING } enum PlanningTargetType { PBI STORY } enum ApiTokenKind { IMPLEMENTATION PLANNING // beide kinds simultaan = afzonderlijke tokens; eenvoudiger dan multi-kind-flag } model ClaudeJob { // ... bestaande velden ... kind ClaudeJobKind @default(IMPLEMENTATION) task_id String? // wordt nullable — planning-jobs hebben geen task task Task? @relation(fields: [task_id], references: [id], onDelete: Cascade) planning_target_type PlanningTargetType? planning_target_id String? @@index([kind, status]) @@index([planning_target_type, planning_target_id, status]) } model ApiToken { // ... bestaande velden ... kind ApiTokenKind @default(IMPLEMENTATION) } model ClaudeWorker { // ... bestaande velden ... // accepted_kinds wordt afgeleid uit token.kind (geen extra kolom nodig) } ``` **Constraint via DB-check (CHECK constraint of app-level):** een `ClaudeJob` heeft óf `task_id` (kind=IMPLEMENTATION) óf `planning_target_*` (kind=PLANNING). Nooit beide leeg, nooit beide gevuld. **Migratie:** alle bestaande rijen krijgen `kind=IMPLEMENTATION` + `apiToken.kind=IMPLEMENTATION` als default. Backwards-compatible. **Bestand:** `prisma/migrations/_planning_job_kind/migration.sql` ### Stap 2 — Status-mappers + Zod-schemas - `lib/claude-job-status.ts` — voeg `kind` toe aan API-shape (lowercase: `implementation` | `planning`) - `lib/schemas/claude-job.ts` (NEW of MODIFY) — discriminated union op `kind` - `lib/schemas/planning-target.ts` (NEW) — `{ type: 'PBI'|'STORY', id: string }` validator ### Stap 3 — Server actions **`actions/claude-jobs.ts`** uitbreiden: ```ts export async function enqueuePlanningJobAction(input: { productId: string target: { type: 'PBI' | 'STORY', id: string } }): Promise ``` Logica: 1. Auth-scope-check (`productAccessFilter`) — target moet binnen product zitten 2. Demo-block (`session.isDemo` → 403) 3. Idempotency: weiger als er al een `PLANNING`-job actief is voor dit `(target_type, target_id)` 4. Insert `ClaudeJob` met `kind=PLANNING`, `task_id=null`, `planning_target_*` ingevuld 5. `pg_notify('scrum4me_changes', { type: 'claude_job_enqueued', kind: 'planning', ... })` `cancelClaudeJobAction` (bestaand) blijft werken — accepteert nu ook PLANNING-jobs (zelfde state-machine). ### Stap 4 — SSE-routing **`app/api/realtime/solo/route.ts`:** - Bestaande `claude_job_*`-events krijgen `kind` in payload - Bij connect: `claude_jobs_initial`-event bevat ook actieve PLANNING-jobs van vandaag - Filter blijft `user_id + product_id` — geen extra topic nodig ### Stap 5 — UI: triggers + status-pills **Trigger in beide dialog-profielen** (geprofileerd in PR #45): | Locatie | Knop-label | Target | |---|---|---| | `components/backlog/story-dialog.tsx` (edit-mode) | `🤖 Genereer taken met Claude` | `{ type: 'STORY', id: story.id }` | | `components/backlog/pbi-dialog.tsx` (edit-mode) | `🤖 Genereer stories met Claude` | `{ type: 'PBI', id: pbi.id }` | Knop-gedrag: - `` rond knop (laag 3 demo-policy) - `disabled` als er al een PLANNING-job actief is voor deze target (live via SSE-store) - Tooltip bij disabled: "Plan-job al gestart — wachten op resultaat" - Klik → `enqueuePlanningJobAction` → toast "Plan gestart" → dialog blijft open zodat user resultaat ziet binnenkomen **Status-pill component (NEW):** `components/shared/planning-job-pill.tsx` — kleine badge die de status van een lopende PLANNING-job toont: - `QUEUED` — grijs, "In wachtrij" - `CLAIMED / RUNNING` — blauw met spinner, "Plan wordt gegenereerd…" - `DONE` — groen, fade-out na 5s - `FAILED` — rood, klikbaar voor error-detail - `CANCELLED` — niet getoond (verwijdert pill) Plaatsing: - Op `PbiList`-card naast PBI-titel (rechts) - Op `StoryPanel`-card naast story-titel (rechts) - In `PbiDialog` / `StoryDialog`-header (edit-mode) als large variant **Live updates:** bestaande `useClaudeJobsStore` (Zustand, populated uit SSE) — alleen `kind` toevoegen aan filter-helpers. ### Stap 6 — MCP-tools (`mcp` repo, aparte PR) **Wijziging 1 — bestaande tool uitbreiden:** ```ts // wait_for_job tool input schema { accept_kinds?: ('IMPLEMENTATION' | 'PLANNING')[] // default: ['IMPLEMENTATION'] wait_seconds?: number // bestaand } ``` Server-side: `WHERE kind = ANY($1) AND status = 'QUEUED'` in de `FOR UPDATE SKIP LOCKED`-query. Token-kind moet ook compatibel zijn (token.kind `IN` accept_kinds-overlap). Response-shape voegt `kind` toe; voor `PLANNING`-jobs vervangt `task` door `planning_target` met embedded record: ```ts { job_id: string kind: 'IMPLEMENTATION' | 'PLANNING' product: { id, name, repo_url, ... } // IMPL-only: task?: { ..., implementation_plan, story, pbi, sprint } // PLANNING-only: planning_target?: { type: 'PBI' | 'STORY' pbi?: { id, code, title, description, priority, status, existing_stories: [{ id, code, title, priority }] } story?: { id, code, title, description, acceptance_criteria, priority, status, pbi: {...}, existing_tasks: [{ id, title, priority, status }] } } } ``` **Wijziging 2 — nieuwe MCP-tool:** `get_planning_context(target_type, target_id)` — losstaande lookup voor agent die handmatig een planning wil starten zonder job-claim. Optioneel; `wait_for_job` retourneert dezelfde data al. **Geen nieuwe write-tools nodig:** bestaande `create_story` + `create_task` + `update_task_plan` werken al. **Schema-sync:** vendor/scrum4me submodule update na Scrum4Me-PR merge. ### Stap 7 — Agent-prompt + lokale Claude-command In de Scrum4Me-checkout (of in een gedeelde plek voor agent-prompts) twee Claude Code commands: **`/implement-next-story`** — bestaand, gebruikt `wait_for_job({ accept_kinds: ['IMPLEMENTATION'] })` **`/generate-plan`** — nieuw: Korte prompt-flow: 1. `wait_for_job({ accept_kinds: ['PLANNING'], wait_seconds: 600 })` — claim 2. Lees `planning_target` uit response (PBI of STORY) + `existing_*` 3. **Lees lokale docs uit Scrum4Me-checkout:** - `docs/specs/functional.md` (functioneel kader) - `docs/architecture.md` (technisch kader) - `docs/patterns/*.md` (relevante patterns op basis van target-titel/-beschrijving) - `docs/design/styling.md` als target UI-werk betreft 4. Bedenk children: - Voor `STORY`-target: 3-7 taken met titel, korte beschrijving, `implementation_plan` (verwijst naar relevante patterns + bestanden), priority - Voor `PBI`-target: 2-5 stories met titel, beschrijving in user-story-format, acceptance_criteria, priority 5. Filter ontbrekende items: skip wat overlapt met `existing_*` (titel-match) 6. Voor elk: `create_task` of `create_story` via MCP 7. `update_job_status({ status: 'DONE', summary: 'Aangemaakt: 4 taken / 0 overgeslagen (titel-overlap)' })` Bij failure: `update_job_status({ status: 'FAILED', error })` + toast voor user. **Mens-rolverdeling:** twee Claude Code-sessies tegelijk draaien (één met `/implement-next-story` running, één met `/generate-plan` running). Beide claimen alleen hun eigen kind via `accept_kinds`. Dezelfde gebruiker-token of twee aparte (afhankelijk van hoe je workers wilt scheiden). ### Stap 8 — Tests | Test | Locatie | |---|---| | `enqueuePlanningJobAction` — auth, demo, idempotency, scope | `__tests__/actions/claude-jobs-planning.test.ts` | | Schema-mapper voor `kind` + `planning_target_*` | `__tests__/lib/claude-job-status.test.ts` | | SSE-event format met `kind` | `__tests__/api/realtime-solo-planning.test.ts` | | Status-pill rendering per status | `__tests__/components/shared/planning-job-pill.test.tsx` | | Knop disabled-state in StoryDialog/PbiDialog bij actieve job | `__tests__/components/backlog/dialog-planning-button.test.tsx` | MCP-tools testen in `mcp` repo (aparte PR). --- ## Critical files ### Scrum4Me-repo | File | Action | Reden | |---|---|---| | `prisma/schema.prisma` | MODIFY | `ClaudeJobKind`, `PlanningTargetType`, `ApiTokenKind` enums + nullable `task_id` + `planning_target_*` velden | | `prisma/migrations/_planning_job_kind/` | NEW | Migratie + check-constraint | | `lib/claude-job-status.ts` | MODIFY | `kind` in API-shape | | `lib/schemas/claude-job.ts` | NEW/MODIFY | Discriminated union op `kind` | | `lib/schemas/planning-target.ts` | NEW | Target-validator | | `actions/claude-jobs.ts` | MODIFY | `enqueuePlanningJobAction` toevoegen, idempotency uitbreiden | | `app/api/realtime/solo/route.ts` | MODIFY | `kind` in payload, initial-state ook PLANNING-jobs | | `stores/claude-jobs-store.ts` (of vergelijkbaar) | MODIFY | `kind`-filter helpers | | `components/backlog/story-dialog.tsx` | MODIFY | "Genereer taken"-knop + status-pill in header | | `components/backlog/pbi-dialog.tsx` | MODIFY | "Genereer stories"-knop + status-pill in header | | `components/backlog/story-panel.tsx` | MODIFY | Status-pill op story-card | | `components/backlog/pbi-list.tsx` | MODIFY | Status-pill op pbi-card | | `components/shared/planning-job-pill.tsx` | NEW | Generic pill-component | | `docs/patterns/claude-agent-roles.md` | NEW | Pattern-doc: één table, kind-enum, accept_kinds-arg, lokale agent-prompts | | `docs/architecture.md` | MODIFY | Sectie "Claude Agents" uitbreiden — twee rollen, schema, queue, prompts | | `docs/specs/dialogs/pbi.md` | MODIFY | Sectie "Speciale gedragingen → Planning-trigger" toevoegen | | `docs/specs/dialogs/story.md` | MODIFY | Idem | | `docs/specs/dialogs/task.md` | MODIFY | Vermelden dat tasks ook door planning-agent kunnen ontstaan | | `__tests__/actions/claude-jobs-planning.test.ts` | NEW | | | `__tests__/lib/claude-job-status.test.ts` | MODIFY | `kind`-mapping testen | | `__tests__/api/realtime-solo-planning.test.ts` | NEW | | | `__tests__/components/shared/planning-job-pill.test.tsx` | NEW | | | `__tests__/components/backlog/dialog-planning-button.test.tsx` | NEW | | ### mcp repo (aparte PR, na Scrum4Me-merge) | File | Action | |---|---| | `src/tools/wait_for_job.ts` | MODIFY — `accept_kinds`-arg + polymorf response | | `src/tools/get_planning_context.ts` | NEW (optioneel, helper) | | `src/types/job.ts` | MODIFY — kind + planning_target | | `src/prompts/generate-plan.md` | NEW — Claude command-prompt | | `vendor/scrum4me/` (submodule) | UPDATE — na Scrum4Me-merge | --- ## Volgorde van uitvoering 1. **Schema + migratie + status-mapper** (Stap 1+2) — eigen commit, geen UI-impact 2. **Server action `enqueuePlanningJobAction` + tests** (Stap 3) — werkt headless via curl/test 3. **SSE-payload uitbreiden + claude-jobs-store** (Stap 4) — backend pipe klaar 4. **Status-pill component + tests** (Stap 5a) — losstaande primitive 5. **Trigger-knoppen in StoryDialog + PbiDialog** (Stap 5b) — UI-trigger werkt, agent nog niet 6. **Pause** — verifieer end-to-end met handmatig insert in `claude_jobs` (kind=PLANNING) of via mock-MCP-call 7. **MCP-PR in mcp repo** (Stap 6) — `wait_for_job` uitbreiden, types updaten 8. **Lokaal `/generate-plan`-command schrijven + testen** (Stap 7) — agent claimt, leest, schrijft 9. **End-to-end test** (Stap 8) — story → klik knop → agent rendert taken → SSE → live in TaskPanel 10. **Docs-PR** — pattern-doc `claude-agent-roles.md`, architecture-update, dialog-profielen aanvullen Branch-naming: `feat/M15-planning-agent` (Scrum4Me) + `feat/planning-agent` (mcp). Conform CLAUDE.md "branch-per-milestone": commits accumuleren lokaal, pushen pas na gebruikerstest. --- ## Verification 1. `npm run lint && npm test && npm run build` groen 2. **Schema-migratie:** bestaande `claude_jobs`-rijen krijgen `kind=IMPLEMENTATION`; check-constraint blokkeert ongeldige combinaties 3. **Idempotency:** twee keer klikken op "Genereer taken" → tweede klik geeft toast "Plan-job al gestart", knop disabled 4. **Demo-block:** demo-user ziet knop disabled met DemoTooltip; server action returnt 403 als je 'm toch aanroept 5. **SSE live:** trigger planning-job → status-pill verschijnt op story-card binnen 1s zonder refresh 6. **End-to-end:** lokale `/generate-plan` agent claimt job, leest target via `wait_for_job`, leest 3-4 docs uit Scrum4Me-checkout, maakt 3-5 taken via `create_task`, status DONE → taken zichtbaar in TaskPanel zonder refresh 7. **Cancel-flow:** gebruiker kan vanuit dialog een running PLANNING-job cancellen → status-pill verdwijnt, agent ziet job-status `CANCELLED` bij volgende `update_job_status` 8. **Cross-kind isolation:** een implementation-agent met `accept_kinds=['IMPLEMENTATION']` (default) ziet PLANNING-jobs niet; idem omgekeerd 9. **Aanvullen-policy:** trigger op story die al 2 taken heeft — agent voegt alleen ontbrekende toe (logged in summary: "Aangemaakt: 3 / Overgeslagen: 2 (titel-overlap)") 10. **Documentatie:** `docs/patterns/claude-agent-roles.md` beschrijft beide agent-rollen, hun job-flow, en hoe je een derde agent zou toevoegen --- ## Open punten (na approval expliciet maken) 1. **Token-strategie:** krijgt elke agent-rol een eigen ApiToken (cleaner, twee credentials), of bestaat er één multi-kind-token per user? *Voorstel: aparte tokens — één per kind. Gebruiker beheert ze in Settings.* 2. **Concurrency in 1 worker:** mag `accept_kinds: ['IMPLEMENTATION', 'PLANNING']` (één worker pakt allebei)? *Voorstel: ja, technisch toegestaan, maar in v1 raad je af want context-pollution. Documenteren als "kan, maar gebruik gescheiden processen".* 3. **Doc-selectie:** hoe bepaalt de agent welke `docs/patterns/*.md` relevant zijn? *Voorstel: lees `docs/patterns/`-index op + match op keywords uit target-titel/-beschrijving. Geen embeddings in v1.* 4. **Hoeveel children per run?** *Voorstel: hard cap 8 in de prompt (anders gaat 'ie speculeren). Gebruiker kan opnieuw klikken voor meer.* 5. **Editable plan-text:** wanneer agent `implementation_plan` invult op een nieuwe task, kan de gebruiker die later via TaskDialog editen — dat werkt al, geen extra werk. 6. **Failure-recovery:** wat als agent halverwege crasht? Stale-cleanup >30min werkt al; partial-children blijven aangemaakt. *Voorstel: accepteer partial — gebruiker kan opnieuw triggeren, aanvullen-policy filtert duplicaten.* --- ## Out of scope (v1 van deze feature) - ❌ Diff-review-flow (vervangen-modus uit eerdere AskUserQuestion) - ❌ Live streaming-output van agent (alleen status-events, geen tekst-stream) - ❌ Meerdere parallele PLANNING-jobs op dezelfde target (idempotency blokkeert) - ❌ Custom prompts per product (vaste prompt-template `/generate-plan`) - ❌ Embeddings / vector-search over docs (agent leest plain files) - ❌ Cross-user planning (agent werkt altijd binnen eigen product-scope) - ❌ Auto-trigger (bv. "elke nieuwe lege story krijgt automatisch een plan-job") — handmatige trigger blijft regel - ❌ Agent-tot-agent-orkestratie (planning-agent kan geen impl-agent triggeren) — gebruiker blijft in the loop - ❌ Planning op product-niveau (PBI's genereren) — pas zinvol als product-spec-input bestaat - ❌ Planning-agent UI in Solo Paneel (Solo blijft impl-only) — triggers zitten in backlog-dialogs --- ## Migratie-pad voor toekomstige derde agent (referentie) Als er ooit een derde agent komt (bv. **review-agent** die een PR review't): 1. `ClaudeJobKind` enum uitbreiden met `REVIEW` 2. `ApiTokenKind` enum uitbreiden met `REVIEW` 3. `enqueueReviewJobAction` aanmaken (kopieert pattern van planning) 4. `wait_for_job` accepteert nieuwe `kind` automatisch via `accept_kinds` 5. Pattern-doc `claude-agent-roles.md` uitbreiden met de derde rol Geen schema-revolutie nodig — `kind`-enum is het uitbreidingspunt.