---
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).
