Scrum4Me/docs/plans/M11-claude-questions.md
Janpeter Visser 7e45bbdbc0
docs: AI-optimized docs restructure (Phases 1–8) (#61)
* docs(dialog-pattern): add generic entity-dialog spec

Introduceert docs/patterns/dialog.md als bron-of-truth voor elke
create/edit/detail-dialog in Scrum4Me, ongeacht het achterliggende
dataobject. Bevat 14 secties: uitgangspunten, stack, component-
architectuur, layout, validatie, drielaagse demo-policy, submission,
dialog-gedrag, theming, footer, triggers/URL-state, per-entiteit
profile-template, out-of-scope, en een verificatie-checklist.

Registreert het patroon in CLAUDE.md "Implementatiepatronen"-tabel
zodat Claude (en mensen) de spec verplicht raadplegen voor elke
nieuwe dialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(dialog-pattern): convert task spec + add pbi/story entity-profiles

Reduceert docs/scrum4me-task-dialog.md van 507 naar ~140 regels: alle
gedeelde regels verhuisd naar docs/patterns/dialog.md, dit document
bevat nu alleen Task-specifieke velden, URL-pattern, status-veld,
server actions, triggers en bewuste out-of-scope-keuzes.

Voegt twee nieuwe entity-profielen toe voor bestaande dialogen:
- docs/scrum4me-pbi-dialog.md (PbiDialog: state-based, code+title-rij,
  PbiStatusSelect, geen delete in v1)
- docs/scrum4me-story-dialog.md (StoryDialog: state-based, header met
  status/priority badges, inline activity-log, demo-readonly-fallback,
  inline-delete-confirm i.p.v. AlertDialog)

Beide profielen documenteren expliciet de "Bekende gaps t.o.v.
generieke spec" zodat opvolgende PR's de afwijkingen kunnen
rechtzetten of bewust kunnen accorderen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Added pdevelopment docs

* docs(plans): add docs-restructure plan for AI-optimized lookup

Audit of existing 39 doc files (~10.700 lines) and a phased restructure
proposal aimed at minimising the tokens an AI agent has to read to find
the right reference. Captures resolved decisions on language (English),
ADR template (Nygard default with MADR escape-hatch), index generator
(node script), and folder taxonomy. Proposal status — fase 1 to follow.

* docs(adr): add ADR scaffolding (templates, README, meta-ADR)

Set up docs/adr/ as the canonical home for architecture decisions:

- templates/nygard.md — default four-section format (Status, Context,
  Decision, Consequences) for one-way-door decisions.
- templates/madr.md — MADR v4 with YAML front-matter and explicit
  Considered Options for decisions where rejected alternatives matter.
- README.md — naming convention (NNNN-kebab-case), template-selection
  guidance (Nygard default; MADR for auth, queue mechanics, agent
  integration), status lifecycle, and ADR roster.
- 0000-record-architecture-decisions.md — meta-ADR establishing the
  practice itself, in Nygard format.

Backfilling existing implicit decisions (base-ui-over-radix, float
sort_order, demo-user three-layer policy, etc.) is fase 6 of the
docs-restructure plan.

* feat(docs): add docs index generator + initial INDEX.md

scripts/generate-docs-index.mjs walks docs/**/*.md, parses YAML
front-matter (or first H1 fallback) and a Nygard-style ## Status
section, then writes docs/INDEX.md with grouped tables for ADRs,
Specs, Plans (with archive subsection), Patterns, and Other.

Pure Node 20 (no external deps); idempotent — running it twice
produces byte-identical output. Excludes adr/templates/, the ADR
README, INDEX.md itself, and any *_*.md sidecar file.

Wire-up:
- package.json: docs:index → node scripts/generate-docs-index.mjs

Initial run indexed 35 docs across the existing structure; the
generated INDEX.md is committed so the table is reviewable in the
PR before hooking generation into a pre-commit step.

* chore: ignore Obsidian vault and personal sidecar files

Add .obsidian/ (Obsidian vault config) and _*.md (personal sidecar
notes) to .gitignore so the docs/ tree can serve as canonical source
of truth while still being usable as an Obsidian vault for personal
authoring. The docs index generator already excludes the same _*.md
pattern from INDEX.md.

* docs(plans): add PBI bulk-create spec for docs-restructure

Machine-parseable spec for an executor that calls the scrum4me MCP
(create_pbi → create_story → create_task) to seed the docs-restructure
work into the DB.

- Section 1 (Context) is the PBI description; serves as task-context
  via mcp__scrum4me__get_claude_context.
- Section 2 lists the 6 resolved decisions (English, MD3+styling
  merged, solo-paneel merged, .Plans archived, Nygard ADR default,
  node index script).
- Section 3 records what already shipped on this branch so the
  executor doesn't duplicate the ADR scaffolding or index generator.
- Section 4 carries the structured YAML graph: 1 PBI, 8 stories
  (one per phase), 39 tasks. product_id is REPLACE_ME — fill before
  running.
- YAML validated with PyYAML; field schema sanity-checked.

* docs(junk-cleanup): remove stub patterns/test.md

* docs(junk-cleanup): archive .Plans/ to docs/plans/archive/

* docs(front-matter): add YAML front-matter to docs/ root

* docs(front-matter): add YAML front-matter to patterns/

* docs(front-matter): add YAML front-matter to plans + agent files

* docs(index): regenerate INDEX.md after front-matter pass

* docs(naming): drop scrum4me- prefix from doc filenames

* docs(naming): lowercase API.md and MD3 filenames

* docs(naming): rename plan file to kebab-case ASCII

* docs(naming): rename middleware.md to proxy.md (next 16)

* docs(naming): polish CLAUDE.md doc-index after renames

* docs(taxonomy): scaffold topical folders under docs/

* docs(taxonomy): move spec files into docs/specs/

* docs(taxonomy): move design/api/qa/backlog/assets into folders

* docs(taxonomy): move agent-instruction-audit into decisions/

* docs(split): break architecture.md into 6 topical files

* docs(split): merge solo-paneel-spec into specs/functional.md

* docs(split): merge md3-color-scheme into design/styling

* docs(trim): extract branch/commit rules into runbook

* docs(trim): extract MCP integration into runbook

* docs(adr): add 0001-base-ui-over-radix

* docs(adr): add 0002-float-sort-order

* docs(adr): add 0003-one-branch-per-milestone

* docs(adr): add 0004-status-enum-mapping

* docs(adr): add 0005-iron-session-over-nextauth

* docs(adr): add 0006-demo-user-three-layer-policy

* docs(adr): add 0007-claude-question-channel-design

* docs(adr): add 0008-agent-instructions-in-claude-md + update README index

* docs(index): regenerate after ADR 0001-0008

* docs(glossary): add docs/glossary.md

* chore(docs): regenerate INDEX.md in pre-commit hook

* docs(readme): link INDEX + glossary + agent instructions

* feat(docs): add doc-link checker script

* chore(docs): wire docs:check-links and docs npm scripts

* ci(docs): block merge on broken doc links

* docs(links): fix broken cross-references after restructure

---------

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

24 KiB
Raw Blame History

title status audience language last_updated applies_to
M11 — Claude vraagt, gebruiker antwoordt active
maintainer
contributor
nl 2026-05-03
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 (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:

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

    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.tscrons-entry toevoegen
  • lib/env.tsCRON_SECRET toevoegen aan Zod-schema
  • .env.exampleCRON_SECRET documenteren

Stappen

  1. Cron-handler:

    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:

    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.tsM11: '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:

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