Scrum4Me/docs/old/plans/M11-claude-questions.md
Janpeter Visser 2bef1a4c20
fix(ci): docs:check-links groen — exclude docs/old/ + archiveer stale plans (#193)
CI faalde sinds #191 (docs cleanup) op pre-existing broken links:
- docs/old/ bevat archief-docs met by-design stale paden
- docs/plans/PBI-79*, M9*, M11* hadden geprojecteerde paden naar
  ../backlog/index.md (verplaatst naar docs/old/backlog/) en naar
  app-bestanden die nooit met de juiste relatieve prefix waren geschreven
- docs/adr/0000* verwees naar docs-restructure-ai-lookup.md (verplaatst)
- docs/glossary.md verwees naar /docs/backlog/index.md (verplaatst)

Fixes:
- scripts/check-doc-links.mjs: skip docs/old/ recursief
- Move docs/plans/{PBI-79,M9,M11}*.md → docs/old/plans/ (allemaal merged PBIs;
  plans waren historisch)
- docs/adr/0000-record-architecture-decisions.md: update pad naar archief
- docs/glossary.md: verwijder dode "backlog index"-link

Verificatie: `npm run docs:check-links` → ✓ All doc links valid (105 files)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:33:47 +02:00

475 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: "M11 — Claude vraagt, gebruiker antwoordt"
status: active
audience: [maintainer, contributor]
language: nl
last_updated: 2026-05-03
applies_to: [M11]
---
# M11 — Claude vraagt, gebruiker antwoordt
Persistent vraag-antwoord-kanaal tussen Claude Code (via MCP) en de actieve Scrum4Me-gebruiker. Claude schrijft een vraag naar `claude_questions` als hij vastloopt op een keuze; een Postgres-trigger emit op het bestaande `scrum4me_changes`-kanaal; de app toont een notificatie-badge; iedereen met product-toegang kan antwoorden; Claude leest het antwoord (sync via polling of in latere sessie via `get_question_answer`) en gaat door.
Eerste concrete uitwerking van strategische **richting B** (verdiepen van de unieke AI-driven dev-flow).
Backlog-entries: zie [backlog.md § M11](../backlog/index.md#m11-claude-vraagt-gebruiker-antwoordt) (op te leveren in ST-1108).
**Beveiligingsuitgangspunten:**
- Atomic answer via `updateMany WHERE status='open'` — concurrent dubbele submit kan niet
- Demo-blok op `ask_user_question` (MCP) en `answerQuestion` (Server Action)
- Access-check via `productAccessFilter` in DB-query én SSE-filter; vraag-tekst en antwoord komen pas via een aparte authenticated query
- Cron-endpoint beveiligd via `Authorization: Bearer ${CRON_SECRET}`
- Logging: alleen `question_id`, nooit vraag/antwoord-tekst (kan gevoelige info bevatten)
**Gekozen kaders (uit overleg):**
- **Sync-model**: default async — `ask_user_question` retourneert direct met `question_id`; optionele `wait_seconds` (max 600) voor polling tot het antwoord er is
- **Answer-policy**: iedereen met product-toegang mag antwoorden; story-assignee krijgt visuele *"wacht op jou"*-emphase
- **Realtime**: hergebruik `scrum4me_changes`-kanaal (uitgebreid met `entity: 'question'`); aparte user-scoped SSE-route `/api/realtime/notifications` zodat solo-board-SSE product-scoped blijft
---
## ST-1101 — `ClaudeQuestion` schema + Postgres-trigger
**Bestanden**
- `prisma/schema.prisma` — model `ClaudeQuestion` + relations op `User`/`Story`/`Task`/`Product`
- `prisma/migrations/<ts>_add_claude_questions/migration.sql` — table-DDL + trigger
- `vendor/scrum4me`-submodule in `mcp` — schema-sync ná merge
**Stappen**
1. Schema-uitbreiding:
```prisma
model ClaudeQuestion {
id String @id @default(cuid())
story_id String
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
task_id String?
task Task? @relation(fields: [task_id], references: [id], onDelete: SetNull)
product_id String // gedenormaliseerd voor SSE-filter
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
asked_by String
asker User @relation("ClaudeQuestionAsker", fields: [asked_by], references: [id])
question String @db.Text
options Json? // string[] voor multi-choice; null voor free-text
status String // 'open' | 'answered' | 'cancelled' | 'expired'
answer String? @db.Text
answered_by String?
answerer User? @relation("ClaudeQuestionAnswerer", fields: [answered_by], references: [id])
answered_at DateTime?
created_at DateTime @default(now())
expires_at DateTime
@@index([story_id, status])
@@index([product_id, status])
@@index([status, expires_at])
@@map("claude_questions")
}
```
Plus op `User`: `asked_questions ClaudeQuestion[] @relation("ClaudeQuestionAsker")` en `answered_questions ClaudeQuestion[] @relation("ClaudeQuestionAnswerer")`.
2. Migratie-SQL voegt naast tabel + indexes ook trigger toe (mirror van `notify_pairing_change` uit M10 ST-1001):
```sql
CREATE OR REPLACE FUNCTION notify_question_change() RETURNS trigger AS $$
DECLARE
story_row record;
payload jsonb;
BEGIN
SELECT assignee_id INTO story_row FROM stories WHERE id = NEW.story_id;
payload := jsonb_build_object(
'op', CASE TG_OP WHEN 'INSERT' THEN 'I' ELSE 'U' END,
'entity', 'question',
'id', NEW.id,
'product_id', NEW.product_id,
'story_id', NEW.story_id,
'task_id', NEW.task_id,
'assignee_id', story_row.assignee_id,
'status', NEW.status
);
PERFORM pg_notify('scrum4me_changes', payload::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER claude_questions_notify
AFTER INSERT OR UPDATE ON claude_questions
FOR EACH ROW EXECUTE FUNCTION notify_question_change();
```
3. `npx prisma migrate dev --name add_claude_questions`
**Aandachtspunten**
- `entity: 'question'` is een nieuwe waarde naast bestaande `'task'`/`'story'`. Solo-route in M8 filter't via `payload.entity` — moet `'question'`-events expliciet wegfilteren (niet emitten naar solo-clients)
- `product_id` op de question is gedenormaliseerd uit `story.product_id` — voorkomt extra join in SSE-filter (zelfde keuze als `story.product_id` in M3)
- `vendor/scrum4me`-submodule sync vereist na merge (drift-check `trig_015FFUnxjz9WMuhhWNGBQKFD`)
**Verificatie**
- `npx prisma migrate dev` slaagt; `npx prisma validate` clean
- `psql $DIRECT_URL -c "LISTEN scrum4me_changes;"` toont payload met `entity: 'question'` bij INSERT
- Bestaande solo-flow nog steeds werkend (regressie-check)
---
## ST-1102 — MCP-tools (in `mcp`-repo)
**Bestanden**
- `mcp/src/tools/ask-user-question.ts` — nieuw
- `mcp/src/tools/get-question-answer.ts` — nieuw
- `mcp/src/tools/list-open-questions.ts` — nieuw
- `mcp/src/tools/cancel-question.ts` — nieuw
- `mcp/src/index.ts` — register de vier tools
- `mcp/scripts/smoke-test.ts` — uitbreiden met question-roundtrip
- `mcp/README.md` — tool-tabel uitbreiden
**Stappen**
1. **`ask_user_question`** (write-tool, sjabloon `create-todo.ts`):
- Input: `{ story_id, question, options?, task_id?, wait_seconds? }` — `wait_seconds` 0600 (Zod `.min(0).max(600)`)
- `requireWriteAccess` (demo-blok)
- Access-check: `userCanAccessProduct(story.product_id, auth.userId)`
- Insert met `expires_at = now() + 24h`, `status = 'open'`
- Als `wait_seconds === 0` (default): return `{ question_id, status: 'open' }`
- Als `wait_seconds > 0`: poll elke 2s tot `status !== 'open'` of timeout. Bij answered: return `{ question_id, status, answer, answered_by, answered_at }`. Bij timeout: return `{ question_id, status: 'pending' }` zodat Claude met `get_question_answer` later kan ophalen
- Polling-implementatie: `setInterval` met `Promise` en abort-signal voor schone cleanup
2. **`get_question_answer`** (read-tool):
- Input: `{ question_id }`
- Access-check via `userCanAccessProduct(question.product_id, auth.userId)`
- Output: full row (`status`, `answer`, `answered_by`, `answered_at`, `expires_at`)
3. **`list_open_questions`** (read-tool):
- Input: `{ story_id? }` (optionele filter)
- Output: array van eigen vragen (`asked_by === auth.userId`) met status open of answered, max 50, geordend op `created_at desc`
- Bedoeld voor Claude om bij begin van een sessie te zien of eerdere vragen inmiddels beantwoord zijn
4. **`cancel_question`** (write-tool):
- Input: `{ question_id }`
- Alleen de asker mag cancelen; `requireWriteAccess` voor demo-blok
- Atomic `updateMany WHERE id=… AND status='open' AND asked_by=…`
- Bedoeld voor wanneer Claude zelf de oplossing vindt en de vraag overbodig wordt
5. Smoke-test in `scripts/smoke-test.ts`: `ask_user_question` met `wait_seconds=5` + parallel `answerQuestion` (via REST of direct DB-write) → verifieer dat de tool het antwoord retourneert binnen het venster
**Aandachtspunten**
- `wait_seconds` polling moet aborten als de MCP-cliënt disconnect (signal-check) — anders blijft de Node-process hangen op een dood socket
- `options`-veld accepteert string-array; in zod als `z.array(z.string()).optional()`
- Als `wait_seconds` > 300 raakt 'm Vercel-deploy onmogelijk (Vercel-functies cap op 300s) — maar de MCP-server draait *lokaal* bij Claude Code, dus 600s mag
**Verificatie**
- MCP Inspector toont 4 nieuwe tools (totaal 13)
- Smoke-test groen: ask + answer roundtrip binnen 5s
- Demo-token op `ask_user_question` of `cancel_question` geeft `PERMISSION_DENIED`
- `tsc --noEmit` clean op `mcp`
---
## ST-1103 — Server Actions voor de browser-UI
**Bestanden**
- `actions/questions.ts` — nieuw
- `__tests__/actions/questions.test.ts` — nieuw
**Stappen**
1. **`answerQuestion(questionId, answer)`** (volgt `docs/patterns/server-action.md`):
- `getSession` + `requireUser`; demo-blok via `if (session.isDemo) return { ok: false, error: 'Niet beschikbaar in demo-modus' }`
- Zod-input: `{ questionId: cuid, answer: string.min(1).max(4000) }`
- Lookup question + access-check via `userCanAccessProduct(question.product_id, userId)`
- Atomic `updateMany WHERE id=… AND status='open' AND expires_at > now()` met `data: { status: 'answered', answer, answered_by: userId, answered_at: now }`
- Bij `count === 0`: disambigueer (al-answered → 'al beantwoord', expired → 'verlopen', geen access → 'geen toegang')
- `revalidatePath('/', 'layout')` zodat badge-count overal updatet
2. **`cancelQuestionByAnswerer(questionId)`** — *uitgesteld naar v2*. Voor v1 alleen Claude (asker) kan annuleren via MCP. Als de UI later een dismiss-functie krijgt, komt het hier.
3. **Tests** `__tests__/actions/questions.test.ts` (6 cases):
- happy answer → status='answered', `revalidatePath` aangeroepen
- demo-user → error + geen DB-write
- user zonder product-access → error
- already-answered → race-error (`updateMany count=0` met status='answered' fallback)
- expired → error
- empty answer → Zod-validatie
**Aandachtspunten**
- `revalidatePath('/', 'layout')` is correct (zelfde keuze als M9 `setActiveProductAction`) — badge zit in app-layout
- Geen `revalidatePath` op `/sprint` of `/solo` nodig — die zien de question niet
- Bij multi-tab: na answer in tab-1 verdwijnt het item in tab-2 via SSE-event, niet via revalidate. revalidate is voor de SSR-render-na-navigatie
**Verificatie**
- `npm test` 6/6 voor questions
- Handmatig: open vraag in browser, antwoord, badge-count zakt met 1
- Demo-test: log in als demo, klik antwoord → toast "Niet beschikbaar in demo-modus"
---
## ST-1104 — User-scoped SSE-route `/api/realtime/notifications`
**Bestanden**
- `app/api/realtime/notifications/route.ts` — nieuw
- `app/api/realtime/solo/route.ts` — uitbreiden om `entity: 'question'` te filteren (anders krijgt solo-client question-events ongewenst door)
- `__tests__/api/notifications-stream.test.ts` — nieuw (auth-cases)
**Stappen**
1. Route Handler — sjabloon uit `app/api/realtime/solo/route.ts`:
- `runtime: 'nodejs'`, `maxDuration: 300`, `dynamic: 'force-dynamic'`
- Auth via iron-session cookie; 401 zonder
- **User-scoped** (geen `?product_id=`-param). Bij connect: query `productAccessFilter(userId)` om alle accessible product-IDs te krijgen
- LISTEN op `scrum4me_changes`; filter:
- `payload.entity === 'question'` (anders skip)
- `payload.product_id IN accessibleProductIds`
- Initial-state-event direct na connect, **na LISTEN actief**: query `claude_questions` met `status='open'` voor deze user's accessible products. Stuur als `event: state\ndata: [{...question-summary...}]`. Voorkomt race tussen connect en LISTEN (zelfde fix als M10 ST-1004)
- Auto-close bij hard-close 240s; client herconnect
2. Solo-route bijwerken: in `shouldEmit` toevoegen `if (payload.entity === 'question') return false`
3. Tests (auth-paden, full-stream blijft handmatig):
- 401 zonder iron-session cookie
- Bij connect met sessie: list van accessible products correct gefilterd
- Question-event op een product zonder access → niet doorgegeven
**Aandachtspunten**
- Twee parallelle SSE-streams in browser (solo-route op product-pagina + notifications-route in app-layout) — netwerk-overhead aanvaardbaar; Vercel rekent per-actieve-functie ongeacht aantal streams
- Initial-state event content: een kleine summary (id, story_code, question, options?) per open vraag — voorkomt dat de bridge eerst een aparte fetch moet doen voor de initial badge-count
- Path expliciet maken in een client `useNotificationsRealtime`-hook (volgt `useSoloRealtime`-pattern)
**Verificatie**
- `curl -N --cookie session-jar /api/realtime/notifications` blijft openstaan, levert `event: state` direct
- INSERT op `claude_questions` voor een toegankelijk product → event binnen 1s
- INSERT voor een ontoegankelijk product → geen event
- Solo-route op `/api/realtime/solo?product_id=…` levert geen question-events meer
---
## ST-1105 — Notifications-UI (Bell + Sheet + Answer-modal)
**Bestanden**
- `components/shared/notifications-bell.tsx` — nieuw
- `components/notifications/notifications-sheet.tsx` — nieuw
- `components/notifications/answer-modal.tsx` — nieuw
- `components/notifications/notifications-bridge.tsx` — nieuw, hookt SSE-listener aan store
- `stores/notifications-store.ts` — nieuw
- `lib/realtime/use-notifications-realtime.ts` — nieuw
- `components/shared/nav-bar.tsx` — `<NotificationsBell />` toevoegen rechts (links van `<UserMenu>`)
- `app/(app)/layout.tsx` — `<NotificationsBridge />` mounten (analoog aan `<SoloRealtimeBridge />`)
**Stappen**
1. **`stores/notifications-store.ts`** — Zustand store; volgt `stores/solo-store.ts`-pattern:
- State: `{ questions: Question[], pendingAnswerIds: Set<string> }`
- Actions: `init(q[])`, `add(q)`, `update(q)`, `remove(id)`, `optimisticAnswer(id)`, `rollbackAnswer(id, q)`
- Selectors: `openCount(userId)`, `forYouCount(userId)` (waar story-assignee = userId)
2. **`lib/realtime/use-notifications-realtime.ts`** — analoog aan `useSoloRealtime`. EventSource opent op `/api/realtime/notifications`, dispatcht `state`/`message`-events naar store via `add`/`update`/`remove`. Reconnect met exponential backoff.
3. **`<NotificationsBridge />`** — Server Component die initial questions ophaalt en aan de store geeft via `init`-prop. Mount in `(app)/layout.tsx` zodat de bridge altijd actief is wanneer user is ingelogd.
4. **`<NotificationsBell />`** — Client Component:
- Lucide `Bell`-icon met badge: `openCount` (totaal) + accent-dot als `forYouCount > 0`
- Klik: `setOpen(true)` op de Sheet
- Geen badge als count === 0
5. **`<NotificationsSheet />`** — shadcn `Sheet` van rechts:
- Header: "Vragen van Claude (N)"
- Lijst gegroepeerd op product (analoog aan M5 todo-data-table-styling), elk item: story-code + truncated title, vraag-preview (line-clamp-2), assignee-emphase als forYou, "Beantwoord" knop opent answer-modal
- Lege staat: "Geen openstaande vragen. Lekker bezig!"
6. **`<AnswerModal />`** — shadcn `Dialog`:
- Story-context-link bovenaan (kleine kaart)
- Volledige vraag-tekst
- Als `options`: `<RadioGroup>` met opties; geen vrije tekst
- Anders: `<Textarea>` (max 4000 chars, char-counter)
- "Verstuur" + "Annuleer" knoppen; submit roept `answerQuestion`-action via `useTransition`
- Demo-modus: knop disabled met tooltip
7. NavBar-edit: `<NotificationsBell />` rechts naast de huidige avatar-trigger. Nieuwe gap-spacing in NavBar's right-section.
**Aandachtspunten**
- Bell-icon en avatar moeten visueel balanceren — hoogte/padding gelijktrekken
- MD3-tokens uit `docs/design/styling.md`: badge `bg-error text-error-foreground` voor critical-count, `bg-primary` voor neutraal. Geen willekeurige Tailwind-kleuren
- Optimistic-answer in store: voor het Server Action-resultaat zet item op pending; bij error rollback met sonner-error-toast
- Sheet-content blijft open zodat de user meerdere vragen achter elkaar kan beantwoorden (zelfde patroon als ST-358 openstaande-stories-sheet)
- ARIA: bell-icon heeft `aria-label="Notificaties — N open vragen"`, badge `role="status"`
**Verificatie**
- Bell verschijnt in NavBar links van avatar; badge count = open question count
- Klik opent Sheet; lijst rendert correct met assignee-emphase
- Submit schiet event door — in tweede tab van zelfde user verdwijnt item binnen 1-2s
- Demo-modus: Sheet rendert, Modal opent, "Verstuur" disabled
- E2E-flow: Claude `ask_user_question` → bell-badge wordt 1 → klik → modal → submit → badge wordt 0 → Claude's `get_question_answer` levert antwoord
---
## ST-1106 — Demo-policy + access-rules + tests
**Bestanden**
- `__tests__/actions/questions.test.ts` — uitbreiden met access-cases (al opgezet in ST-1103)
- `__tests__/api/notifications-stream.test.ts` — access-cases
- Documentatie-aanpassingen in `actions/questions.ts` en SSE-route met expliciete demo-blok-comment
**Stappen**
1. Verifieer dat `requireProductWriter` alle Server-Action-mutaties al dekt (zou moeten — uit M3.5)
2. Voeg expliciete demo-test toe: demo-user opent answer-modal → Verstuur disabled met tooltip
3. Voeg access-test toe: user-A heeft geen access tot product van user-B → user-A's notification-stream krijgt geen events voor user-B's questions
**Aandachtspunten**
- Story-assignee-emphase is **alleen visueel** — toegang is product-membership-breed. Dit is bewust: als de assignee niet beschikbaar is moet een andere member kunnen invallen
- Demo kan een vraag wel **lezen** (transparantie over hoe de feature werkt) — alleen niet beantwoorden
**Verificatie**
- 6+ tests groen (al gedekt in ST-1103/1104)
- Handmatige cross-product-test met 2 users + 2 producten
---
## ST-1107 — Auto-expire + Vercel cron-cleanup
**Bestanden**
- `app/api/cron/expire-questions/route.ts` — nieuw
- `vercel.ts` — `crons`-entry toevoegen
- `lib/env.ts` — `CRON_SECRET` toevoegen aan Zod-schema
- `.env.example` — `CRON_SECRET` documenteren
**Stappen**
1. **Cron-handler**:
```ts
export const runtime = 'nodejs'
export async function POST(request: Request) {
const auth = request.headers.get('authorization')
if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
const result = await prisma.claudeQuestion.updateMany({
where: { status: 'open', expires_at: { lt: new Date() } },
data: { status: 'expired' },
})
// Optioneel: ook M10 login_pairings cleanup hier (eerder geparkeerd)
return Response.json({ expired: result.count })
}
```
2. **`vercel.ts`**:
```ts
export const config: VercelConfig = {
// ... bestaande config
crons: [{ path: '/api/cron/expire-questions', schedule: '0 4 * * *' }],
}
```
3. Documenteer in `.env.example`: `CRON_SECRET=<openssl rand -base64 32>`
**Aandachtspunten**
- Vercel cron POST't standaard zonder body; auth is alleen via header
- Op Hobby-plan zijn crons beperkt — check budget. M11 cron is 4x/dag, prima
- Cron-trigger update't `claude_questions` → trigger fired → SSE-events → UI updates badge real-time. Geen extra plumbing nodig
**Verificatie**
- Handmatig: `curl -X POST -H "Authorization: Bearer ${CRON_SECRET}" /api/cron/expire-questions` met een vraag waar `expires_at < now` → response `{expired: 1}`, vraag verdwijnt uit notifications
- Onbevoegde call zonder secret → 401
- Vercel dashboard toont cron-config na deploy
---
## ST-1108 — Documentatie + acceptatietest
**Bestanden**
- `docs/api/rest-contract.md` — secties "SSE — Notifications" + "Cron — Expire questions"
- `docs/architecture.md` — sectie "Vraag-antwoord-kanaal" met sequence-diagram
- `docs/patterns/claude-question-channel.md` — herbruikbaar pattern-doc
- `docs/backlog/index.md` — M11-tabel-rij + M11-sectie
- `prisma/seed-data/parse-backlog.ts` — `M11: 'ACTIVE'`, `M10: 'COMPLETED'`, `M3.5: 'COMPLETED'`
- `CLAUDE.md` — pattern-doc verwijzing in Implementatiepatronen-tabel
**Stappen**
1. Backlog-tabel-rij + M11-sectie in `docs/backlog/index.md` (mirror M10-format met **Implementatieplan:** verwijzing naar dit doc)
2. `docs/architecture.md` § "Vraag-antwoord-kanaal":
- Mermaid sequence-diagram: Claude → MCP → DB → trigger → SSE → user → Server Action → DB → trigger → polling-tool
- Threat-model-tabel (replay, demo-block, access-leak, expiry, race)
- "Waarom hergebruik scrum4me_changes-kanaal" sub-sectie
3. `docs/patterns/claude-question-channel.md` — generiek pattern voor toekomstige bidirectionele async-communicatie tussen MCP-agents en interactieve users
4. Parser-flip: M11 wordt nieuwe ACTIVE-milestone, M10 → COMPLETED. (Zelfde patroon als bij M10-start: chore-commit met vlag-flip + re-seed.)
5. **Acceptatie-scenario's** (zes, deels door unit-tests gedekt):
1. **Sync happy path**: Claude `ask_user_question(wait_seconds=300)` → user antwoordt binnen 30s → MCP-tool retourneert het antwoord ✅
2. **Async happy path**: `ask_user_question(wait_seconds=0)` → tool returnt direct → user antwoordt later → Claude `get_question_answer` → ziet antwoord ✅
3. **Demo-block**: demo-user opent vraag → kan inhoud lezen → "Verstuur" disabled (UI + Server Action ✅)
4. **Access-isolation**: vraag op product zonder access → onzichtbaar in andere user's notifications-bell (SSE-filter ✅)
5. **Expiry**: vraag met `expires_at < now` → na cron-run niet meer in badge-count ✅
6. **Race**: concurrent answer-poging op al-beantwoorde vraag → schone foutmelding (atomic `updateMany count=0` ✅)
**Aandachtspunten**
- Acceptatie-scenario's 1-2 zijn handmatig (full Claude+browser cyclus); 3-6 worden in unit-tests vastgelegd
- Pattern-doc moet ook beschrijven wanneer NIET te gebruiken (bv. wanneer een gewone API-call met sessie volstaat)
**Verificatie**
- Alle docs gepubliceerd in repo
- Backlog-parser-self-test: `npx tsx prisma/seed-data/parse-backlog.ts` toont M11 met `priority=4 sprint=ACTIVE`
- 6/6 acceptatie-scenario's groen
- `npm run lint && npx tsc --noEmit && npm test && npm run build` clean
- `vendor/scrum4me`-submodule sync in mcp na merge
---
## Branch- en commit-strategie
Per [Branch & PR Strategy](../runbooks/branch-and-commit.md):
- **Eén branch op Scrum4Me**: `feat/M11-claude-questions` afgesplitst van `main` ná M10-merge
- **Aparte branch op mcp**: `feat/M11-question-tools`
- Commits chronologisch per stap met ST-code in titel:
```
chore(M11): swap demo-active sprint from M10 to M11
feat(ST-1101): add ClaudeQuestion model + notify_question_change trigger
feat(ST-1102): add 4 MCP question tools (in mcp)
feat(ST-1103): add answerQuestion server action
feat(ST-1104): add /api/realtime/notifications user-scoped SSE
feat(ST-1104): filter entity='question' from solo-realtime stream
feat(ST-1105): add Zustand notifications-store + realtime hook
feat(ST-1105): add NotificationsBridge in app layout
feat(ST-1105): add NotificationsBell + Sheet + AnswerModal
chore(ST-1107): add CRON_SECRET to env schema
feat(ST-1107): add /api/cron/expire-questions handler
feat(ST-1107): wire vercel.ts cron entry
docs(ST-1108): document notifications SSE + cron in api.md
docs(ST-1108): add vraag-antwoord-kanaal flow to architecture
docs(ST-1108): add claude-question-channel pattern doc
chore(ST-1108): backlog M11 + parser ACTIVE-flip
```
**Push + PR pas na handmatige acceptatie** van scenario 1 (sync happy path) + 3 (demo-block) op localhost.
**MCP-PR pas mergen ná Scrum4Me-PR** + submodule-sync — anders wijzen MCP-tools naar een schema-tabel die op main nog niet bestaat.
---
## Reseed-stap (eenmalig vóór ST-1101-implementatie)
Backlog-markdown moet eerst de M11-stories bevatten en de parser moet M11 als ACTIVE-milestone kennen voordat `mcp__scrum4me__get_claude_context` ze als next-story kan teruggeven. Workflow:
1. Doe ST-1108 backlog-edit + parser-flip eerst (commit `chore(M11): swap demo-active sprint from M10 to M11` + de backlog-uitbreiding)
2. `npm run seed` — re-seed met M11=ACTIVE
3. `mcp__scrum4me__get_claude_context` levert nu ST-1101 als next-story
4. Verder met ST-1101-implementatie
> **Let op:** seed wist user-data. Doe dit op een dev-DB.
---
## Buiten scope (volgende milestones)
- **AI-suggested antwoorden** Claude leest de codebase en stelt 3 mogelijke antwoorden voor; user kiest. Vereist tweede LLM-call per vraag.
- **Mobile-push notifications** bouwt op M10 paired-flow + service-worker. v3.
- **Question-templates** "ambiguous-naming"-vraag, "missing-test-case"-vraag etc. voor consistentie.
- **Threading** vervolgvraag op een antwoord. v1 is single-shot Q&A.
- **File-uploads als antwoord** bv. een screenshot.
- **Stats/dashboard** gemiddelde antwoord-tijd, meest-gestelde-vraagsoorten.
- **Dismiss-per-user** een member negeert een vraag voor zichzelf zonder 'm te beantwoorden.