Merge pull request #91 from madhura68/feat/m12-ideas
M12 — Idea entity + Grill/Plan jobs
This commit is contained in:
commit
2893573004
51 changed files with 5623 additions and 141 deletions
|
|
@ -25,6 +25,7 @@ Auto-generated on 2026-05-04 from front-matter and headings.
|
|||
|---|---|---|
|
||||
| [AnswerModal Profiel](./specs/dialogs/answer-modal.md) | active | 2026-05-04 |
|
||||
| [BatchEnqueueBlockerDialog Profiel](./specs/dialogs/batch-enqueue-blocker.md) | active | 2026-05-04 |
|
||||
| [IdeaDialog Profiel](./specs/dialogs/idea.md) | active | 2026-05-04 |
|
||||
| [PbiDialog Profiel](./specs/dialogs/pbi.md) | active | 2026-05-04 |
|
||||
| [ProductDialog Profiel](./specs/dialogs/product.md) | active | 2026-05-04 |
|
||||
| [Sprint Dialogs Profiel](./specs/dialogs/sprint.md) | active | 2026-05-04 |
|
||||
|
|
@ -43,6 +44,7 @@ Auto-generated on 2026-05-04 from front-matter and headings.
|
|||
| [Landing v2 — lokaal & veilig + architectuurdiagram](./plans/landing-local-first.md) | active | 2026-05-03 |
|
||||
| [M10 — Password-loze inlog via QR-pairing](./plans/M10-qr-pairing-login.md) | active | 2026-05-03 |
|
||||
| [M11 — Claude vraagt, gebruiker antwoordt](./plans/M11-claude-questions.md) | active | 2026-05-03 |
|
||||
| [M12 — Idea entity + Grill/Plan Claude jobs](./plans/M12-ideas.md) | planned | — |
|
||||
| [M9 — Actief Product Backlog](./plans/M9-active-product-backlog.md) | active | 2026-05-03 |
|
||||
| [PBI-11 — Mobile-shell met landscape-lock (settings + backlog + solo)](./plans/PBI-11-mobile-shell.md) | — | — |
|
||||
| [ST-1109 — PBI krijgt een status (Ready / Blocked / Done)](./plans/ST-1109-pbi-status.md) | active | 2026-05-03 |
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
|
|||
| M9: Actief Product Backlog | Persistente actieve PB-keuze, gesplitste navigatie, disabled-states | ST-901 – ST-907 |
|
||||
| M10: Password-loze inlog via QR-pairing | Mobiel als bevestigingskanaal voor desktop-login zonder wachtwoord | ST-1001 – ST-1008 |
|
||||
| M11: Claude vraagt, gebruiker antwoordt | Persistent vraag-antwoord-kanaal tussen Claude (MCP) en de actieve gebruiker | ST-1101 – ST-1108 |
|
||||
| M12: Ideeën & Grill/Plan jobs | Idee-entity tussen Todo en PBI; interactief grillen + deterministisch materialiseren | ST-1192 – ST-1201 |
|
||||
---
|
||||
|
||||
## Backlog
|
||||
|
|
@ -755,6 +756,49 @@ Persistent vraag-antwoord-kanaal tussen Claude Code (via MCP) en de actieve Scru
|
|||
|
||||
---
|
||||
|
||||
### M12: Ideeën & Grill/Plan jobs
|
||||
|
||||
**Implementatieplan:** [docs/plans/M12-ideas.md](../plans/M12-ideas.md)
|
||||
**Dialog-profiel:** [docs/specs/dialogs/idea.md](../specs/dialogs/idea.md)
|
||||
|
||||
Idee is een nieuw concept tussen Todo en PBI. Strikt user_id-only (privé), met
|
||||
twee Claude-jobs: **Grill Me** (interactief vragen-stellen via MCP) en **Make
|
||||
Plan** (single-pass yaml-frontmatter genereren). De **Materialiseer**-knop
|
||||
parseert het plan deterministisch en creëert PBI + stories + taken.
|
||||
|
||||
- [x] **ST-1192** — DB-schema & migratie voor Idea (T-491, T-492, T-489)
|
||||
- Idea-model + IdeaLog-model + 3 enums; ClaudeJob.task_id nullable + idea_id +
|
||||
kind; ClaudeQuestion.story_id nullable + idea_id; check-constraints +
|
||||
pg_notify-trigger update
|
||||
- [x] **ST-1193** — Lib + schemas + embedded prompts (T-493, T-494, T-495)
|
||||
- zod-schemas, status-mapper + transition-guard, atomic code-generator,
|
||||
yaml-frontmatter parser, embedded grill+make-plan prompts
|
||||
- [x] **ST-1194** — Server actions + Todo→Idea promotie (T-496..T-499)
|
||||
- CRUD, md-edit, job-triggers, materialize, relink, promoteTodoToIdeaAction
|
||||
- [x] **ST-1195** — REST API + proxy demo-laag (T-500, T-501)
|
||||
- /api/ideas + /api/ideas/[id]; demo-403 via proxy.ts catch-all
|
||||
- [x] **ST-1196** — Realtime SSE + idea-store (T-502, T-503)
|
||||
- SSE-routing voor idea-events; Zustand idea-store; extension van bestaande
|
||||
notifications-realtime hook
|
||||
- [ ] **ST-1197** — MCP-server tools (extern: madhura68/scrum4me-mcp)
|
||||
- get_idea_context, update_idea_grill_md, update_idea_plan_md, log_idea_decision;
|
||||
uitbreiding ask_user_question/wait_for_job/update_job_status; Docker rebuild
|
||||
- [x] **ST-1198** — UI lijstpagina + row-actions (T-507, T-508, T-509)
|
||||
- /ideas pagina, IdeaList tabel met filters, IdeaRowActions met
|
||||
disabled-rules per status, idea-status-badge helper
|
||||
- [x] **ST-1199** — UI detail + dialog + tabs (T-510..T-513)
|
||||
- /ideas/[id] met 4 tabs (Idee/Grill/Plan/Timeline); md-editor met
|
||||
yaml-validate; timeline met UNION view; pbi-link-card; dialog-profiel doc
|
||||
- [x] **ST-1200** — Promote-from-Todo + sidebar (T-514, T-515)
|
||||
- "→ Idee" knop in TodoCard, PromoteIdeaDialog, "Ideeën" nav-entry
|
||||
- [ ] **ST-1201** — End-to-end smoke + docs-update (T-516, T-517)
|
||||
- Volledige flow doorlopen volgens M12-ideas.md verificatie-script;
|
||||
docs/runbooks/mcp-integration.md uitbreiden voor IDEA_*-job-kinds
|
||||
- Done when: docs/INDEX.md opnieuw gegenereerd, alle stories ✓, MCP-server
|
||||
PR met passende versie-bump gedeployed
|
||||
|
||||
---
|
||||
|
||||
## v2 Backlog (na MVP)
|
||||
|
||||
- [ ] Uitnodigingsflow voor teams — e-mailuitnodiging of link-gebaseerd; nu kunnen alleen admins met toegang tot het systeem Developers toevoegen via gebruikersnaam
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 552 KiB After Width: | Height: | Size: 667 KiB |
299
docs/plans/M12-ideas.md
Normal file
299
docs/plans/M12-ideas.md
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
---
|
||||
title: "M12 — Idea entity + Grill/Plan Claude jobs"
|
||||
status: planned
|
||||
audience: implementation
|
||||
language: nl
|
||||
---
|
||||
|
||||
# M12 — Idea entity + Grill/Plan Claude jobs
|
||||
|
||||
## Context
|
||||
|
||||
Scrum4Me ondersteunt `Todo` als lichtgewicht voorstel-laag, en kan dat handmatig promoveren naar PBI/Story. Dat slaat het *denkproces* niet vast: waarom werd iets een PBI, welke alternatieven zijn afgewogen, welke randvoorwaarden waren er.
|
||||
|
||||
Doel: een nieuw concept **Idee** dat:
|
||||
- werkt als een Todo (top-level lijst, privé per gebruiker), met een **Grill Me**- en **Make Plan**-knop;
|
||||
- via de bestaande Claude-job/worker-infrastructuur een gestructureerd plan oplevert;
|
||||
- het hele planningsproces vastlegt (Q&A, beslissingen, grill-md, plan-md, link naar PBI);
|
||||
- na goedkeuring deterministisch materialiseert tot PBI + stories + taken (incl. `implementation_plan`).
|
||||
|
||||
## Vastgelegde keuzes (uit grill-sessie)
|
||||
|
||||
1. **UI-plek**: top-level `/ideas`, naast `/todos`.
|
||||
2. **Auth-scope**: strikt `user_id`-only (privé, ook ná `PLANNED`). Geen `productAccessFilter` op idea-acties; geen `pbi.idea_id`-veld nodig.
|
||||
3. **Product-binding**: een idee mag bestaan zonder product, maar **Grill Me** én **Make Plan** vereisen een product met `repo_url` (de worker leest sources/docs uit de repo). `claude_jobs.product_id` blijft NOT NULL.
|
||||
4. **Executie-model**: bestaand worker-model. `ClaudeJob{kind:IDEA_*}` QUEUED → lokale Claude-CLI claimt via `wait_for_job`. Knoppen zijn **disabled als `connectedWorkers === 0`** (exact zoals `solo/task-detail-dialog.tsx`).
|
||||
5. **Skill-afhankelijkheid**: **embedded prompts** in `lib/idea-prompts/{grill,make-plan}.md`; meegestuurd in payload. Geen externe `anthropic-skills:grill-me`-plugin-vereiste op de worker.
|
||||
6. **Make-Plan flow**: preview-en-bevestigen. Job produceert `Idea.plan_md`, status → `PLAN_READY`. Aparte knop **"Materialiseer plan"** parseert md → entiteiten in één Prisma-transactie, status → `PLANNED`.
|
||||
7. **Plan-md formaat**: YAML-frontmatter (structuur) + markdown-body (overwegingen, alternatieven, vrije reasoning).
|
||||
8. **Make-Plan-job**: single-pass (geen `ask_user_question`). Twijfels → terug naar grill (append-context).
|
||||
9. **Backward transitions**:
|
||||
- Re-grill vanuit `GRILLED`/`PLAN_READY`: nieuwe `IDEA_GRILL`-job met **append-context** (oude `grill_md` als input); oude versie naar `IdeaLog{type:GRILL_RESULT}` als history.
|
||||
- Re-plan vanuit `PLAN_READY`: idem voor `plan_md`.
|
||||
- PBI-verwijdering vanuit `PLANNED`: **expliciete user-actie "Re-link plan"** (geen DB-trigger). Zet `pbi_id=null`, status `PLAN_READY`.
|
||||
- Failed grill/plan: dedicated states **`GRILL_FAILED` / `PLAN_FAILED`** (zichtbaar voor user), niet stilzwijgend resetten.
|
||||
10. **Logging-model**: `IdeaLog` smal (`DECISION | NOTE | GRILL_RESULT | PLAN_RESULT | STATUS_CHANGE | JOB_EVENT`). Q&A blijft uitsluitend in `claude_questions`. Timeline-tab in UI doet `UNION ALL` over beide bronnen.
|
||||
11. **Opslag md-bestanden**: alleen DB (`Idea.grill_md`, `Idea.plan_md`). Geen auto-commit naar repo (zou strict-private auth-keuze ondergraven). UI biedt **"Download .md"**.
|
||||
12. **Editability**: beide md's bewerkbaar door user in hun ready-states (`GRILLED` voor grill_md, `PLAN_READY` voor plan_md). Bij `PLANNED`: read-only. Yaml-frontmatter wordt zod-gevalideerd-on-save voor `plan_md`.
|
||||
13. **Promotie vanuit Todo**: nieuwe `promoteTodoToIdeaAction` (Todo → DRAFT-Idea + Todo wordt `archived=true`). Bestaande Todo→PBI/Story-acties blijven onaangetast.
|
||||
14. **Demo-policy** (3-laag, zoals Todo): create/edit/archive **mag**; Grill / Make Plan / Materialiseer / promote-from-Todo zijn **geblokkeerd** (proxy.ts 403 + `session.isDemo`-guard + `<DemoTooltip>`).
|
||||
15. **Idea-code**: `Idea.code = "IDEA-{nnn}"`, `@@unique([user_id, code])`, counter op `User.idea_code_counter`.
|
||||
16. **Realtime-store**: nieuwe `stores/idea-store.ts`. `connectedWorkers` direct selecten via `useSoloStore(s => s.connectedWorkers)` (lift naar shared store is opvolg-refactor).
|
||||
17. **Sidebar**: nieuwe entry **Ideeën** (`Lightbulb`-icon) direct boven Todo's.
|
||||
18. **Q&A-expiry**: 24h aanhouden (consistent met bestaand). Verlopen → re-grill (append-context).
|
||||
|
||||
## State machine
|
||||
|
||||
```
|
||||
┌──── re-grill ────┐
|
||||
▼ │
|
||||
DRAFT ──Grill Me──▶ GRILLING ─done──▶ GRILLED ─Make Plan─▶ PLANNING ─done──▶ PLAN_READY ─Materialiseer─▶ PLANNED
|
||||
│ fail │ fail │ ▲ │
|
||||
▼ ▼ │ │ │
|
||||
GRILL_FAILED PLAN_FAILED └──┘ re-plan │
|
||||
│ │
|
||||
└────── retry/edit ──────────────────────────────────────── PBI verwijderd ──────────┘
|
||||
+ "Re-link plan"
|
||||
```
|
||||
|
||||
`archived: boolean` is orthogonaal en kan vanuit elke status.
|
||||
|
||||
## Datamodel
|
||||
|
||||
### Nieuwe enums
|
||||
```prisma
|
||||
enum IdeaStatus {
|
||||
DRAFT
|
||||
GRILLING
|
||||
GRILL_FAILED
|
||||
GRILLED
|
||||
PLANNING
|
||||
PLAN_FAILED
|
||||
PLAN_READY
|
||||
PLANNED
|
||||
}
|
||||
|
||||
enum ClaudeJobKind {
|
||||
TASK_IMPLEMENTATION
|
||||
IDEA_GRILL
|
||||
IDEA_MAKE_PLAN
|
||||
}
|
||||
|
||||
enum IdeaLogType {
|
||||
DECISION
|
||||
NOTE
|
||||
GRILL_RESULT
|
||||
PLAN_RESULT
|
||||
STATUS_CHANGE
|
||||
JOB_EVENT
|
||||
}
|
||||
```
|
||||
|
||||
### `User`
|
||||
- Veld toevoegen: `idea_code_counter Int @default(0)`.
|
||||
|
||||
### Nieuwe tabel `ideas`
|
||||
```prisma
|
||||
model Idea {
|
||||
id String @id @default(cuid())
|
||||
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
||||
user_id String
|
||||
product Product? @relation(fields: [product_id], references: [id], onDelete: SetNull)
|
||||
product_id String?
|
||||
code String @db.VarChar(30)
|
||||
title String
|
||||
description String? @db.VarChar(4000)
|
||||
grill_md String? @db.Text
|
||||
plan_md String? @db.Text
|
||||
pbi Pbi? @relation(fields: [pbi_id], references: [id], onDelete: SetNull)
|
||||
pbi_id String? @unique
|
||||
status IdeaStatus @default(DRAFT)
|
||||
archived Boolean @default(false)
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
questions ClaudeQuestion[]
|
||||
jobs ClaudeJob[]
|
||||
logs IdeaLog[]
|
||||
|
||||
@@unique([user_id, code])
|
||||
@@index([user_id, archived, status])
|
||||
@@index([user_id, product_id])
|
||||
@@map("ideas")
|
||||
}
|
||||
```
|
||||
|
||||
### Nieuwe tabel `idea_logs`
|
||||
```prisma
|
||||
model IdeaLog {
|
||||
id String @id @default(cuid())
|
||||
idea Idea @relation(fields: [idea_id], references: [id], onDelete: Cascade)
|
||||
idea_id String
|
||||
type IdeaLogType
|
||||
content String @db.Text
|
||||
metadata Json?
|
||||
created_at DateTime @default(now())
|
||||
|
||||
@@index([idea_id, created_at])
|
||||
@@map("idea_logs")
|
||||
}
|
||||
```
|
||||
|
||||
### Aanpassingen `claude_jobs`
|
||||
- `task_id` → **nullable**.
|
||||
- `idea_id String?` toegevoegd, FK → `Idea`, `onDelete: Cascade`.
|
||||
- `kind ClaudeJobKind @default(TASK_IMPLEMENTATION)` toegevoegd.
|
||||
- `product_id` blijft NOT NULL.
|
||||
- Raw-SQL check-constraint: `(task_id IS NOT NULL) <> (idea_id IS NOT NULL)`.
|
||||
- Index: `@@index([idea_id, status])`.
|
||||
|
||||
### Aanpassingen `claude_questions`
|
||||
- `story_id` → **nullable**.
|
||||
- `idea_id String?` toegevoegd, FK → `Idea`, `onDelete: Cascade`.
|
||||
- Raw-SQL check-constraint: `(story_id IS NOT NULL) <> (idea_id IS NOT NULL)`.
|
||||
- Index: `@@index([idea_id, status])`.
|
||||
- pg_notify-trigger payload uitbreiden met `idea_id` (nullable). SSE-filter laat idea-payloads alleen door naar `idea.user_id === session.user_id`.
|
||||
|
||||
## Server-laag
|
||||
|
||||
### Schemas + helpers
|
||||
- `lib/schemas/idea.ts` — `ideaCreateSchema`, `ideaUpdateSchema`, `ideaPlanMdFrontmatterSchema`.
|
||||
- `lib/idea-status.ts` — DB-enum ↔ API-string mapping.
|
||||
- `lib/idea-plan-parser.ts` — synchroon: `parsePlanMd(md): ParsedPlan | ZodError`. Gebruikt `yaml`-package + zod.
|
||||
- `lib/idea-code.ts` — atomair `nextIdeaCode(userId)` via Prisma-transactie.
|
||||
|
||||
### Embedded prompts
|
||||
- `lib/idea-prompts/grill.md` — eigen scrum4me-versie. Instrueert: gebruik `ask_user_question` MCP, schrijf via `update_idea_grill_md` aan eind.
|
||||
- `lib/idea-prompts/make-plan.md` — strict yaml-frontmatter-format. Instrueert: lees `grill_md`, gebruik repo-files, **geen vragen**, eindig met `update_idea_plan_md`.
|
||||
|
||||
### Server actions — `actions/ideas.ts`
|
||||
Volg `docs/patterns/server-action.md`: auth → demo-check → zod → user-id-scope-check → write → `revalidatePath`.
|
||||
|
||||
- `createIdeaAction(input)` — `nextIdeaCode(userId)`, status `DRAFT`.
|
||||
- `updateIdeaAction(id, input)` — alleen `DRAFT|GRILL_FAILED|GRILLED|PLAN_FAILED|PLAN_READY`.
|
||||
- `archiveIdeaAction(id)` / `unarchiveIdeaAction(id)`.
|
||||
- `deleteIdeaAction(id)` — geweigerd als `pbi_id` gevuld.
|
||||
- `updateGrillMdAction(id, md)` — alleen in `GRILLED|PLAN_READY`. Logt `IdeaLog{NOTE}`.
|
||||
- `updatePlanMdAction(id, md)` — alleen in `PLAN_READY`. Eerst `parsePlanMd(md)`; bij parse-fail → 422 met line-info.
|
||||
- `startGrillJobAction(id)` — vereist product met `repo_url`, `connectedWorkers > 0`. `ClaudeJob{kind:IDEA_GRILL, idea_id, product_id, QUEUED}`. Status → `GRILLING`. Demo: 403.
|
||||
- `startMakePlanJobAction(id)` — vereist `GRILLED|PLAN_FAILED|PLAN_READY` voor re-plan, product met repo, worker. Status → `PLANNING`. Demo: 403.
|
||||
- `cancelIdeaJobAction(id)` — actieve job CANCELLED, idea-status terug naar vorige.
|
||||
- `materializeIdeaPlanAction(id)` — `PLAN_READY` → `parsePlanMd` → Prisma-`$transaction`:
|
||||
1. Counters incrementeren (PBI/Story/Task).
|
||||
2. INSERT PBI + N stories + M tasks (incl. `implementation_plan`).
|
||||
3. UPDATE idea: `pbi_id`, `status:PLANNED`.
|
||||
4. INSERT `IdeaLog{type:PLAN_RESULT, metadata}`.
|
||||
Rollback bij ANY fail. Demo: 403.
|
||||
- `relinkIdeaPlanAction(id)` — alleen als `status===PLANNED && pbi_id===null`. Status → `PLAN_READY`.
|
||||
- `downloadIdeaMdAction(id, kind: 'grill'|'plan')` — server returnt md.
|
||||
|
||||
### Promote van Todo → Idea
|
||||
- `actions/todos.ts`: nieuwe `promoteTodoToIdeaAction(todoId)` — auth + demo + scope. Maakt Idea (DRAFT) met title/description; zet Todo `archived=true`. Demo: 403.
|
||||
|
||||
### REST-routes
|
||||
- `app/api/ideas/route.ts` (GET, POST) en `app/api/ideas/[id]/route.ts` (GET, PATCH).
|
||||
|
||||
### proxy.ts (demo-laag)
|
||||
- 403 op `POST/PATCH/DELETE /api/ideas*` voor demo-token.
|
||||
- 403 op grill/make-plan/materialize-endpoints.
|
||||
|
||||
## MCP-laag (`scrum4me-mcp`-repo)
|
||||
|
||||
### Nieuwe tools
|
||||
- `get_idea_context(idea_id)` — `{idea, product, repo_url, grill_md_so_far, open_questions, prompt_text}`.
|
||||
- `update_idea_grill_md(idea_id, markdown)` — schrijft veld; status → `GRILLED`; logt `IdeaLog{GRILL_RESULT}`.
|
||||
- `update_idea_plan_md(idea_id, markdown)` — schrijft veld; parser draait server-side; status → `PLAN_READY` of `PLAN_FAILED`.
|
||||
- `log_idea_decision(idea_id, type, content, metadata?)` — types: `DECISION | NOTE`.
|
||||
|
||||
### Uitbreiding bestaande tools
|
||||
- `ask_user_question`: contract uitbreiden — exact één van `story_id` of `idea_id`.
|
||||
- `wait_for_job`: response uitbreiden:
|
||||
- `kind: 'TASK_IMPLEMENTATION' | 'IDEA_GRILL' | 'IDEA_MAKE_PLAN'`
|
||||
- bij `IDEA_*`: `idea`, `product`, `repo_url`, `prompt_text`.
|
||||
- `update_job_status`: bij `failed` voor IDEA_*-jobs zet idea-status `GRILL_FAILED` / `PLAN_FAILED`.
|
||||
|
||||
### Schema-drift
|
||||
- `docs/runbooks/mcp-integration.md:62`: schema-drift-watchdog moet groen zijn vóór merge. MCP-server-PR parallel.
|
||||
|
||||
## Realtime-laag
|
||||
|
||||
- `app/api/realtime/notifications/route.ts` — idea-questions alleen aan `idea.user_id === session.user_id`.
|
||||
- `app/api/realtime/solo/route.ts` — `JobPayload` uitbreiden met `kind` en `idea_id`. Idea-jobs op `user_id`.
|
||||
- `stores/idea-store.ts` (nieuw). `connectedWorkers` direct uit `useSoloStore`.
|
||||
|
||||
## UI-laag
|
||||
|
||||
### Routing
|
||||
- `app/(app)/ideas/page.tsx` — top-level lijst.
|
||||
- `app/(app)/ideas/[id]/page.tsx` — detailpagina met tabs **Idee** · **Grill** · **Plan** · **Timeline**.
|
||||
- Sidebar-entry: `Lightbulb`, label "Ideeën", boven Todo's.
|
||||
|
||||
### Componenten — `components/ideas/`
|
||||
- `idea-list.tsx` — TanStack Table; kolommen code/title/product/status/archived. Bulk-archive.
|
||||
- `idea-row-actions.tsx` — Grill Me / Make Plan / Materialiseer / Edit / Archive met disabled-rules.
|
||||
- `idea-dialog.tsx` + `components/dialogs/idea-dialog.tsx` (wrapper) volgens dialog-pattern.
|
||||
- `idea-md-editor.tsx` — markdown editor met yaml-validate voor plan_md.
|
||||
- `idea-timeline.tsx` — UNION-view IdeaLog + claude_questions.
|
||||
- `idea-pbi-link-card.tsx` — incl. "Re-link plan"-banner.
|
||||
- `download-md-button.tsx`.
|
||||
|
||||
### Promote-from-Todo UI
|
||||
- `components/todos/todo-list.tsx`: extra menu-item "Promote naar Idee".
|
||||
|
||||
### Profiel-doc
|
||||
- `docs/specs/dialogs/idea.md` — verplicht volgens dialog-pattern.
|
||||
|
||||
## Te raken / aan te maken bestanden
|
||||
|
||||
| Laag | Bestand |
|
||||
|---|---|
|
||||
| Schema | `prisma/schema.prisma` |
|
||||
| Migratie | `prisma/migrations/<ts>_add_ideas/migration.sql` |
|
||||
| Schemas | `lib/schemas/idea.ts`, `lib/idea-status.ts`, `lib/idea-plan-parser.ts`, `lib/idea-code.ts` |
|
||||
| Prompts | `lib/idea-prompts/grill.md`, `lib/idea-prompts/make-plan.md` |
|
||||
| Actions | `actions/ideas.ts`, uitbreiding `actions/todos.ts` |
|
||||
| API | `app/api/ideas/route.ts`, `app/api/ideas/[id]/route.ts`, `proxy.ts` |
|
||||
| Realtime | `app/api/realtime/notifications/route.ts`, `app/api/realtime/solo/route.ts` |
|
||||
| Pages | `app/(app)/ideas/page.tsx`, `app/(app)/ideas/[id]/page.tsx` |
|
||||
| UI | `components/ideas/*.tsx`, `components/dialogs/idea-dialog.tsx`, sidebar-update |
|
||||
| Store | `stores/idea-store.ts` |
|
||||
| Docs | `docs/specs/dialogs/idea.md`, `docs/runbooks/mcp-integration.md`, `docs/backlog/index.md` |
|
||||
| MCP-server | `madhura68/scrum4me-mcp` (parallel-PR) |
|
||||
|
||||
## Implementatievolgorde
|
||||
|
||||
1. **DB & migratie**
|
||||
2. **Lib + schemas + prompts**
|
||||
3. **Server actions + Todo-promote**
|
||||
4. **REST + proxy demo-laag**
|
||||
5. **Realtime SSE + idea-store**
|
||||
6. **MCP-server tools (extern repo, parallel)**
|
||||
7. **UI lijst + row-actions**
|
||||
8. **UI detail + dialog + tabs**
|
||||
9. **UI promote-from-Todo + sidebar-entry**
|
||||
10. **End-to-end smoke + docs**
|
||||
|
||||
## Verificatie
|
||||
|
||||
```bash
|
||||
npm run lint && npm test && npm run build
|
||||
```
|
||||
|
||||
End-to-end:
|
||||
1. `npm run dev` + lokale Claude-CLI met `wait_for_job`-loop.
|
||||
2. Maak idee, koppel aan product. Status `DRAFT`.
|
||||
3. Grill Me → vragen via answer-modal → `update_idea_grill_md` → `GRILLED`.
|
||||
4. Edit grill_md handmatig → `IdeaLog{NOTE}`.
|
||||
5. Make Plan → `update_idea_plan_md` → `PLAN_READY`.
|
||||
6. Yaml-fout in plan_md → save geblokkeerd.
|
||||
7. Materialiseer → PBI + stories + taken in transactie. `idea.pbi_id` gezet, `PLANNED`.
|
||||
8. PBI verwijderen → "Re-link plan"-banner → `PLAN_READY`.
|
||||
9. Demo-test: knoppen geblokkeerd via DemoTooltip + 403.
|
||||
10. Failure-test: kill worker → `GRILL_FAILED`/`PLAN_FAILED`.
|
||||
11. Promote-test: Todo → "Promote naar Idee" → DRAFT-Idea, Todo archived.
|
||||
|
||||
## Open punten (niet-blokkerend)
|
||||
|
||||
- Concrete copy-finetuning van prompt-md's tijdens implementatie.
|
||||
- Lift `connectedWorkers` naar gedeelde `worker-presence-store` (opvolg-refactor).
|
||||
- Optionele "Commit plan_md naar repo"-knop (buiten v1).
|
||||
|
|
@ -35,15 +35,35 @@ Scrum4Me heeft een eigen MCP-server in repo [`madhura68/scrum4me-mcp`](https://g
|
|||
- `mcp__scrum4me__cancel_question` — asker-only annulering van een eigen open vraag
|
||||
|
||||
**Job queue — agent worker mode (M13):**
|
||||
- `mcp__scrum4me__wait_for_job` — blokkeert ≤600s, claimt atomisch een QUEUED-job via FOR UPDATE SKIP LOCKED; retourneert volledige task-context (implementation_plan, story, pbi, sprint, repo_url). Zet stale CLAIMED-jobs (>30min) eerst terug naar QUEUED. Wanneer de full block-time verstrijkt zonder claim is de queue leeg.
|
||||
- `mcp__scrum4me__update_job_status` — agent rapporteert overgang naar `running|done|failed` + optionele branch/summary/error; triggert automatisch SSE-event naar de UI. Auth: Bearer-token moet matchen `claimed_by_token_id`.
|
||||
- `mcp__scrum4me__wait_for_job` — blokkeert ≤600s, claimt atomisch een QUEUED-job via FOR UPDATE SKIP LOCKED. **Sinds M12** retourneert de payload een `kind`-discriminator:
|
||||
- `kind: 'TASK_IMPLEMENTATION'` (default) — payload met `implementation_plan`, `story`, `pbi`, `sprint`, `repo_url`
|
||||
- `kind: 'IDEA_GRILL'` of `'IDEA_MAKE_PLAN'` — payload met `idea`, `product`, `repo_url`, en `prompt_text` (de embedded prompt uit `lib/idea-prompts/`)
|
||||
Stale CLAIMED-jobs (>30min) worden eerst terug naar QUEUED gezet. Lege queue na block-time = klaar.
|
||||
- `mcp__scrum4me__update_job_status` — agent rapporteert `running|done|failed` + optionele branch/summary/error; triggert automatisch SSE-event. Bij `failed` voor `IDEA_GRILL`/`IDEA_MAKE_PLAN` wordt de idea-status automatisch op `GRILL_FAILED` resp. `PLAN_FAILED` gezet. Auth: Bearer-token moet matchen `claimed_by_token_id`.
|
||||
|
||||
**Idea-jobs (M12) — agent gedrag per kind:**
|
||||
|
||||
| Kind | Werkwijze | Eind-call |
|
||||
|---|---|---|
|
||||
| `IDEA_GRILL` | Lees `prompt_text` (embedded grill-prompt) + `idea.grill_md` als startpunt; itereer met `ask_user_question(idea_id=...)`/`get_question_answer`; log onderweg `log_idea_decision`; eindig met `update_idea_grill_md(markdown)` | `update_job_status('done')` |
|
||||
| `IDEA_MAKE_PLAN` | Lees `prompt_text` (embedded make-plan-prompt) + `idea.grill_md` + repo-context. **Stel GEEN vragen** — single-pass output. Bouw plan in strict yaml-frontmatter format en eindig met `update_idea_plan_md(markdown)`. Server-side parser kan parse-fail → `PLAN_FAILED` | `update_job_status('done')` |
|
||||
|
||||
**MCP-tools — Idea-laag (M12):**
|
||||
- `mcp__scrum4me__get_idea_context(idea_id)` — `{ idea, product, repo_url, grill_md_so_far, open_questions, prompt_text }`
|
||||
- `mcp__scrum4me__update_idea_grill_md(idea_id, markdown)` — schrijft veld; status → `GRILLED`; logt `IdeaLog{GRILL_RESULT}`
|
||||
- `mcp__scrum4me__update_idea_plan_md(idea_id, markdown)` — server-side `parsePlanMd`; ok → `PLAN_READY` + `IdeaLog{PLAN_RESULT}`; parse-fail → `PLAN_FAILED` + `IdeaLog{JOB_EVENT, errors}`
|
||||
- `mcp__scrum4me__log_idea_decision(idea_id, type, content, metadata?)` — `type ∈ {DECISION, NOTE}`
|
||||
- `mcp__scrum4me__ask_user_question` — geüpgrade contract: exact één van `story_id` óf `idea_id` (xor); idea-vragen zijn user-private (geen productAccessFilter).
|
||||
|
||||
## Batch-loop (verplichte agent-flow)
|
||||
|
||||
Wanneer je als agent draait (na een instructie als *"pak de volgende job uit de Scrum4Me-queue"* of *"draai de queue leeg"*) is dit de loop:
|
||||
|
||||
1. `wait_for_job` aanroepen.
|
||||
2. Job uitvoeren volgens het meegegeven `implementation_plan`.
|
||||
2. Switch op `kind`:
|
||||
- `TASK_IMPLEMENTATION`: voer uit volgens het meegegeven `implementation_plan` (zoals altijd — branch, code, commit, push, verify_task_against_plan).
|
||||
- `IDEA_GRILL`: laad `prompt_text` als gids; gebruik `ask_user_question` / `get_question_answer` voor de Q&A-loop; eindig met `update_idea_grill_md`.
|
||||
- `IDEA_MAKE_PLAN`: laad `prompt_text` + `idea.grill_md`; **stel geen vragen**; produceer strict yaml-frontmatter; eindig met `update_idea_plan_md`.
|
||||
3. `update_job_status('done'|'failed')` aanroepen.
|
||||
4. **Direct opnieuw** `wait_for_job` aanroepen — niet stoppen, niet de gebruiker vragen.
|
||||
5. Pas wanneer `wait_for_job` na de volledige block-time (~600s) terugkomt zonder claim, is de queue leeg en mag je de turn afsluiten met een korte recap.
|
||||
|
|
|
|||
167
docs/specs/dialogs/idea.md
Normal file
167
docs/specs/dialogs/idea.md
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
---
|
||||
title: "IdeaDialog Profiel"
|
||||
status: active
|
||||
audience: [ai-agent, contributor]
|
||||
language: nl
|
||||
last_updated: 2026-05-04
|
||||
---
|
||||
|
||||
# IdeaDialog / IdeaDetailLayout Profiel
|
||||
|
||||
> Volgt **`docs/patterns/dialog.md`** (de generieke spec voor élke entity-dialog in Scrum4Me).
|
||||
> Dit document beschrijft alleen de Idea-specifieke afwijkingen en keuzes — alle gedeelde regels (layout, motion, demo-policy, foutcodes, validatie, theming) staan in de generieke spec en worden hier niet herhaald.
|
||||
|
||||
> **Belangrijk:** als een regel in dit profiel botst met de generieke spec, wint de generieke spec. Documenteer hier de afwijking + reden, of pas de generieke spec aan.
|
||||
|
||||
---
|
||||
|
||||
## Velden
|
||||
|
||||
| Veld | Type | Mode | Validatie | Bron-zod |
|
||||
|---|---|---|---|---|
|
||||
| `title` | `string` (required) | beide | trim, 1-200 chars | `ideaCreateSchema.title` |
|
||||
| `description` | `string \| null` | beide | optional, max 4000 chars, plain textarea | `ideaCreateSchema.description` |
|
||||
| `product_id` | `string \| null` | beide | optional cuid; **vereist voordat Grill/Make Plan kan starten** (M12 grill-keuze 3) | `ideaCreateSchema.product_id` |
|
||||
| `code` | `string` (auto) | read-only | `IDEA-NNN`, server-generated via `nextIdeaCode(userId)` op `User.idea_code_counter` | n.v.t. |
|
||||
| `status` | `IdeaStatus` enum | read-only | door server gezet via state-machine | `lib/idea-status.ts canTransition` |
|
||||
| `grill_md` | `string \| null` | edit-tab | bewerkbaar in `GRILLED \| PLAN_READY` | n.v.t. |
|
||||
| `plan_md` | `string \| null` | edit-tab | bewerkbaar in `PLAN_READY` + yaml-frontmatter must parse | `ideaPlanMdFrontmatterSchema` |
|
||||
| `archived` | `boolean` | read-only | via archive-actie | n.v.t. |
|
||||
| `pbi_id` | `string \| null` | read-only | gezet door `materializeIdeaPlanAction`, `SetNull` als PBI verwijderd | n.v.t. |
|
||||
|
||||
---
|
||||
|
||||
## URL- of state-pattern
|
||||
|
||||
**Afwijking van generieke spec:** Idee gebruikt een **dedicated route** `/ideas/[id]` ipv een modal-dialog. Reden: het detail-scherm is rijker dan een modal kan dragen (4 tabs incl. md-editor + timeline) en de planningsgeschiedenis is een leesbaar artifact dat verdiend om bookmarkable te zijn.
|
||||
|
||||
- **Lijst-create**: state-based inline form bovenaan `/ideas` lijst (`IdeaList.showCreate`).
|
||||
- **Detail / edit**: route `/ideas/[id]` met tab-switcher via query-param (`?tab=idee|grill|plan|timeline`).
|
||||
- **Geen modal**: dus geen `Cmd/Ctrl+Enter`-submit op de detail-form (alleen op md-editor); `Esc` doet niets in het detail-scherm.
|
||||
|
||||
---
|
||||
|
||||
## Tabs (alleen op detail-route)
|
||||
|
||||
| Tab | Content | Editable in |
|
||||
|---|---|---|
|
||||
| `idee` | inline form (title, description, product_id) | `DRAFT \| GRILL_FAILED \| GRILLED \| PLAN_FAILED \| PLAN_READY` |
|
||||
| `grill` | `grill_md` markdown render + Bewerk-knop | `GRILLED \| PLAN_READY` |
|
||||
| `plan` | `plan_md` markdown render + Bewerk-knop | `PLAN_READY` |
|
||||
| `timeline` | UNION van `IdeaLog` + `ClaudeQuestion` chronologisch | n.v.t. (read-only) |
|
||||
|
||||
`isIdeaEditable`, `isGrillMdEditable` en `isPlanMdEditable` helpers in `lib/idea-status.ts` bepalen de exacte regels.
|
||||
|
||||
---
|
||||
|
||||
## Status-machine
|
||||
|
||||
```
|
||||
DRAFT ──Grill──▶ GRILLING ─done──▶ GRILLED ──Make Plan──▶ PLANNING ─done──▶ PLAN_READY ──Materialiseer──▶ PLANNED
|
||||
│ fail │ fail ▲ │
|
||||
▼ ▼ │ │
|
||||
GRILL_FAILED PLAN_FAILED └─── re-grill / re-plan (append-context) │
|
||||
│
|
||||
PLANNED ◀── (PBI verwijderd: pbi_id=null, status blijft PLANNED tot Re-link) ────────────────────────────────┘
|
||||
```
|
||||
|
||||
| Van | Naar | Trigger | Server-action |
|
||||
|---|---|---|---|
|
||||
| `DRAFT` | `GRILLING` | "Grill" knop | `startGrillJobAction` |
|
||||
| `GRILLING` | `GRILLED` | worker → `update_idea_grill_md` | (MCP) |
|
||||
| `GRILLING` | `GRILL_FAILED` | worker → `update_job_status('failed')` | (MCP) |
|
||||
| `GRILLED` / `PLAN_FAILED` / `PLAN_READY` | `GRILLING` | "Grill" knop (re-grill) | `startGrillJobAction` |
|
||||
| `GRILLED` / `PLAN_FAILED` / `PLAN_READY` | `PLANNING` | "Plan" knop | `startMakePlanJobAction` |
|
||||
| `PLANNING` | `PLAN_READY` | worker → `update_idea_plan_md` (parser ok) | (MCP) |
|
||||
| `PLANNING` | `PLAN_FAILED` | worker → `update_job_status('failed')` of parse-fail | (MCP) |
|
||||
| `PLAN_READY` | `PLANNED` | "Maak PBI" knop | `materializeIdeaPlanAction` |
|
||||
| `PLANNED` (pbi_id=null) | `PLAN_READY` | "Plan opnieuw beschikbaar maken" knop | `relinkIdeaPlanAction` |
|
||||
| any | `*` archived | Archive-knop | `archiveIdeaAction` |
|
||||
|
||||
---
|
||||
|
||||
## Server actions
|
||||
|
||||
`actions/ideas.ts`:
|
||||
|
||||
| Actie | Precondition | Effect |
|
||||
|---|---|---|
|
||||
| `createIdeaAction(input)` | auth + niet-demo | nieuwe DRAFT-idea + auto-code |
|
||||
| `updateIdeaAction(id, input)` | `isIdeaEditable(status)` | update title/description/product_id |
|
||||
| `archiveIdeaAction(id)` / `unarchiveIdeaAction(id)` | scoped on user_id | flip `archived` |
|
||||
| `deleteIdeaAction(id)` | `pbi_id === null` | hard delete (cascades naar IdeaLog) |
|
||||
| `updateGrillMdAction(id, md)` | `isGrillMdEditable(status)` | update + IdeaLog{NOTE} |
|
||||
| `updatePlanMdAction(id, md)` | `isPlanMdEditable(status)` + `parsePlanMd.ok` | update + IdeaLog{NOTE} |
|
||||
| `startGrillJobAction(id)` | product+repo + worker actief + status in `GRILL_TRIGGERABLE_FROM` | enqueue ClaudeJob{kind:IDEA_GRILL} |
|
||||
| `startMakePlanJobAction(id)` | idem + status in `MAKE_PLAN_TRIGGERABLE_FROM` | enqueue ClaudeJob{kind:IDEA_MAKE_PLAN} |
|
||||
| `cancelIdeaJobAction(id)` | actieve job aanwezig | job→CANCELLED + status revert |
|
||||
| `materializeIdeaPlanAction(id)` | `status===PLAN_READY` + `plan_md` parseable | atomic create PBI + stories + tasks; idea→PLANNED |
|
||||
| `relinkIdeaPlanAction(id)` | `status===PLANNED && pbi_id===null` | status→PLAN_READY |
|
||||
| `downloadIdeaMdAction(id, kind)` | scope (demo OK, read-only) | return md-string |
|
||||
| `promoteTodoToIdeaAction(todoId)` (in `actions/todos.ts`) | todo niet archived + niet-demo | DRAFT-idea + Todo→archived |
|
||||
|
||||
Foutcodes: 400 = JSON parse, 401 = auth, 403 = demo, 404 = scope/not-found, 409 = idempotency/race, 422 = validatie/status-mismatch, 429 = rate-limit.
|
||||
|
||||
---
|
||||
|
||||
## Demo-policy (3-laag)
|
||||
|
||||
| Laag | Wat | Waar |
|
||||
|---|---|---|
|
||||
| 1 | `proxy.ts` blokt `POST/PATCH/DELETE /api/ideas*` | `proxy.ts` catch-all rule |
|
||||
| 2 | `session.isDemo` guard in elke muteer-actie | `actions/ideas.ts` |
|
||||
| 3 | `<DemoTooltip show={isDemo}>` rondom muteer-knoppen | `idea-row-actions.tsx`, `idea-list.tsx`, `idea-detail-layout.tsx`, `download-md-button.tsx` (NIET — read-only mag) |
|
||||
|
||||
Demo-user MAG: lijst zien, idee zien, navigeren tussen tabs, downloaden van md.
|
||||
Demo-user MAG NIET: aanmaken, bewerken, archiveren, Grill, Plan, Materialiseer, Re-link, Promote-from-Todo.
|
||||
|
||||
---
|
||||
|
||||
## Special behaviors
|
||||
|
||||
### IdeaMdEditor
|
||||
|
||||
- **Cmd/Ctrl+S** triggert save (alleen in editor, niet in detail-form).
|
||||
- **localStorage draft** per `(idea_id, kind)`: lazy read-on-mount via `useState(() => readSeed(...))` om setState-in-effect te vermijden. Drift met server → toast info bij restore.
|
||||
- **Live yaml-validate** voor plan-kind: `useMemo(() => parsePlanMd(value))` → derived state, geen useEffect.
|
||||
- **Submit-errors** los van validation-errors in state — server-side details overschrijven client-side validate als die er zijn.
|
||||
|
||||
### IdeaPbiLinkCard
|
||||
|
||||
- Drie states: PLANNED+pbi (groene link), PLANNED+pbi-null (oranje banner met Re-link knop), niet-PLANNED (return null).
|
||||
|
||||
### Status badges
|
||||
|
||||
- Status-tokens via `lib/idea-status-colors.ts` → `getIdeaStatusBadge(status)` → `{ label, classes, pulse? }`.
|
||||
- `GRILLING` en `PLANNING` → `animate-pulse` om "actief" te signaleren.
|
||||
|
||||
### Connected workers
|
||||
|
||||
- `IdeaRowActions` leest `useSoloStore(s => s.connectedWorkers)` (M12 grill-keuze 16 — geen lift naar gedeelde store voor v1).
|
||||
- Zonder worker: Grill / Make Plan disabled met tooltip "Geen Claude-worker actief". Materialiseer is server-side synchroon en heeft géén worker nodig.
|
||||
|
||||
---
|
||||
|
||||
## Realtime
|
||||
|
||||
SSE-stream `/api/realtime/notifications` levert idea-events (M12 T-502). Routing in `lib/realtime/use-notifications-realtime.ts`:
|
||||
|
||||
- `claude_job_*` payloads met `kind=IDEA_*` → `useIdeaStore.handleIdeaJobEvent`
|
||||
- `entity:'question'` payloads met `idea_id` set → `useIdeaStore.handleIdeaQuestionEvent`
|
||||
- Story-questions blijven in `useNotificationsStore`
|
||||
|
||||
`useIdeaStore` houdt optimistic state: `jobByIdea`, `ideaStatuses`, `openQuestionsByIdea`. Voor de detail-pagina is de server-state na `router.refresh()` source-of-truth — de store is een UI-cache.
|
||||
|
||||
---
|
||||
|
||||
## Test-fixtures
|
||||
|
||||
- `__tests__/actions/ideas-crud.test.ts` (39 cases) — alle CRUD + job-trigger + materialize + relink paden
|
||||
- `__tests__/api/ideas.test.ts` (13 cases) — REST-laag
|
||||
- `__tests__/stores/idea-store.test.ts` (7 cases) — Zustand event-handling
|
||||
- `__tests__/lib/idea-status.test.ts` (15+ cases) — status mappers + transition guards
|
||||
- `__tests__/lib/idea-schemas.test.ts` (16+ cases) — zod-validatie
|
||||
- `__tests__/lib/idea-plan-parser.test.ts` (6 cases) — yaml-frontmatter
|
||||
- `__tests__/proxy/demo-guard.test.ts` (9 cases) — incl. 3 idea-cases
|
||||
|
||||
Geen Playwright/MSW E2E voor v1 — handmatig E2E-script staat in `docs/plans/M12-ideas.md` "Verificatie".
|
||||
Loading…
Add table
Add a link
Reference in a new issue