From 0d126695dbfec9bb9ade3ab1e65fdddb4f20654a Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Sat, 9 May 2026 22:54:14 +0200 Subject: [PATCH] docs: add plans and recommendations - docs/plans/Local github setup.md - docs/plans/lees-de-readme-md-validated-book.md - docs/plans/zustand-store-rearchitecture.md - docs/recommendations/beelink-ubuntu-scrum4me-server-caveman-plan.md - docs/recommendations/claude-vm-job-flow-git-strategy.md - docs/INDEX.md updated Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/INDEX.md | 5 + docs/plans/Local github setup.md | 371 +++++++++ .../plans/lees-de-readme-md-validated-book.md | 197 +++++ docs/plans/zustand-store-rearchitecture.md | 746 ++++++++++++++++++ ...ink-ubuntu-scrum4me-server-caveman-plan.md | 458 +++++++++++ .../claude-vm-job-flow-git-strategy.md | 213 +++++ 6 files changed, 1990 insertions(+) create mode 100644 docs/plans/Local github setup.md create mode 100644 docs/plans/lees-de-readme-md-validated-book.md create mode 100644 docs/plans/zustand-store-rearchitecture.md create mode 100644 docs/recommendations/beelink-ubuntu-scrum4me-server-caveman-plan.md create mode 100644 docs/recommendations/claude-vm-job-flow-git-strategy.md diff --git a/docs/INDEX.md b/docs/INDEX.md index 99d880b..8d183a4 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -45,6 +45,8 @@ Auto-generated on 2026-05-09 from front-matter and headings. | [Plan: model + mode-selectie per ClaudeJob-kind](./plans/job-model-selection.md) | — | — | | [Landing v2 — lokaal & veilig + architectuurdiagram](./plans/landing-local-first.md) | active | 2026-05-03 | | [Landing v3 — van idee tot pull request](./plans/landing-v3-idea-flow.md) | active | 2026-05-04 | +| [Scrum4Me-Research — Zustand rearchitecture (reset + execute)](./plans/lees-de-readme-md-validated-book.md) | — | — | +| [Advies — Zelf een Git-platform hosten naast of in plaats van GitHub](./plans/Local github setup.md) | — | — | | [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 | — | @@ -59,6 +61,7 @@ Auto-generated on 2026-05-09 from front-matter and headings. | [Plan: wekelijkse sync van `model_prices` (PBI-66 / ST-1296)](./plans/sync-model-prices.md) | — | — | | [Tweede Claude Agent — Planning Agent](./plans/tweede-claude-agent-planning.md) | proposal | 2026-05-03 | | [Scrum4Me — v1.0 readiness](./plans/v1-readiness.md) | active | 2026-05-04 | +| [Zustand store rearchitecture - active context, realtime en resync](./plans/zustand-store-rearchitecture.md) | ready-to-execute | 2026-05-09 | ### Archive @@ -125,6 +128,8 @@ Auto-generated on 2026-05-09 from front-matter and headings. | [DevPlanner — Product Backlog](./product-backlog.md) | `product-backlog.md` | active | 2026-05-03 | | [Scrum4Me — API Test Plan](./qa/api-test-plan.md) | `qa/api-test-plan.md` | active | 2026-05-03 | | [Realtime smoke-checklist — PBI / Story / Task](./realtime-smoke.md) | `realtime-smoke.md` | active | 2026-05-03 | +| [Caveman plan — Beelink naar Ubuntu Scrum4Me server](./recommendations/beelink-ubuntu-scrum4me-server-caveman-plan.md) | `recommendations/beelink-ubuntu-scrum4me-server-caveman-plan.md` | draft | 2026-05-09 | +| [Aanbeveling — Claude VM jobflow en gitstrategie](./recommendations/claude-vm-job-flow-git-strategy.md) | `recommendations/claude-vm-job-flow-git-strategy.md` | draft | 2026-05-09 | | [Agent-flow: open issues & decision log](./runbooks/agent-flow-pitfalls.md) | `runbooks/agent-flow-pitfalls.md` | active | 2026-05-03 | | [Auto-PR flow: van story-DONE naar gemergde PR](./runbooks/auto-pr-flow.md) | `runbooks/auto-pr-flow.md` | active | 2026-05-06 | | [Branch, PR & Commit Strategy](./runbooks/branch-and-commit.md) | `runbooks/branch-and-commit.md` | active | 2026-05-03 | diff --git a/docs/plans/Local github setup.md b/docs/plans/Local github setup.md new file mode 100644 index 0000000..07d160a --- /dev/null +++ b/docs/plans/Local github setup.md @@ -0,0 +1,371 @@ +# Advies — Zelf een Git-platform hosten naast of in plaats van GitHub + +## Situatie + +Je wilt onderzoeken of je lokaal of op een eigen server een Git repository-platform kunt draaien vergelijkbaar met GitHub. + +In jouw situatie spelen mee: + +- Next.js/Vercel apps +- AI-workers / automation +- batch processing +- deploy pipelines +- private code +- mogelijk draaien op NAS of VPS +- integratie met Claude Code / Codex / agents + +Het antwoord is: ja, dit kan uitstekend. + +--- + +# Architectuur-opties + +## Optie 1 — Alleen een centrale Git remote + +De lichtste oplossing. + +Je draait alleen een zogenaamde "bare repo" op een Linux server. + +### Voordelen + +- extreem simpel +- weinig resources +- volledige controle +- SSH push/pull + +### Nadelen + +- geen webinterface +- geen PR’s +- geen issues +- geen gebruikersbeheer +- geen CI/CD UI + +### Setup + +Server: + +```bash +mkdir -p /srv/git/myapp.git +cd /srv/git/myapp.git +git init --bare +``` + +Client: + +```bash +git remote add origin ssh://user@server:/srv/git/myapp.git +git push -u origin main +``` + +--- + +# Optie 2 — Self-hosted GitHub alternatief + +Dit is meestal de beste keuze. + +Software opties: + +| Software | Omschrijving | +|---|---| +| Gitea | Lichtgewicht GitHub alternatief | +| Forgejo | Community fork van Gitea | +| GitLab | Zeer compleet maar zwaar | +| OneDev | Moderne alles-in-één oplossing | + +--- + +# Aanbevolen keuze: Gitea + +## Waarom + +Voor jouw situatie is Gitea waarschijnlijk de beste balans tussen: + +- eenvoud +- performance +- features +- beheerlast + +Je krijgt: + +- Git hosting +- web UI +- pull requests +- issues +- SSH support +- webhooks +- CI integratie +- Docker support +- private repos +- multi-user support + +--- + +# Aanbevolen architectuur voor jouw setup + +## Huidige richting + +```text +MacBook + ↓ +GitHub + ↓ +Vercel deploy +``` + +## Uitgebreide AI workflow + +```text +MacBook + ↓ +Gitea / GitHub + ↓ webhook +AI Worker Server + ↓ +Repo clone + ↓ +Code generatie + ↓ +Commit + push + ↓ +PR creation + ↓ +Merge + ↓ +Vercel deploy +``` + +--- + +# Beste strategie voor jouw situatie + +## Advies: hybride model + +Gebruik: + +| Component | Platform | +|---|---| +| publieke repos | GitHub | +| deploys | Vercel | +| AI worker orchestration | eigen server | +| interne experimenten | Gitea | +| automation | self-hosted | + +Waarom: + +- GitHub ecosystem blijft beschikbaar +- recruiters herkennen GitHub +- Copilot integratie blijft optimaal +- minder beheer +- sneller stabiel + +--- + +# Wanneer volledig self-hosted interessant wordt + +Volledig self-hosted wordt interessant als: + +- privacy belangrijk is +- AI agents autonoom moeten kunnen werken +- je volledige controle wilt +- je GitHub limieten wilt vermijden +- je meerdere workers wilt draaien + +Dan bouw je: + +```text +Gitea + + Postgres + + Docker Registry + + CI Runners + + Reverse Proxy + + Backups + + Monitoring +``` + +--- + +# Aanbevolen infrastructuur + +## Lichtgewicht setup + +### Hardware + +- Synology NAS of mini-PC +- 8–16 GB RAM +- SSD opslag + +### Software stack + +| Component | Advies | +|---|---| +| OS | Ubuntu Server | +| Containers | Docker Compose | +| Git platform | Gitea | +| Reverse proxy | Traefik | +| Database | Postgres | +| SSL | Let's Encrypt | +| Deploys | Vercel | + +--- + +# Docker Compose voorbeeld + +```yaml +services: + gitea: + image: gitea/gitea:latest + container_name: gitea + + ports: + - "3000:3000" + - "222:22" + + volumes: + - ./gitea:/data + + restart: always +``` + +Starten: + +```bash +docker compose up -d +``` + +Daarna bereikbaar via: + +```text +http://server-ip:3000 +``` + +--- + +# Belangrijke aandachtspunten + +## Backups + +Bij self-hosting moet je zelf regelen: + +- database backups +- repo backups +- disaster recovery + +--- + +## Security + +Je bent zelf verantwoordelijk voor: + +- updates +- SSH security +- firewall +- SSL certificaten +- gebruikersbeheer + +--- + +## CI/CD + +GitHub Actions vervang je mogelijk door: + +- Gitea Actions +- Drone CI +- Woodpecker CI +- self-hosted runners + +--- + +# Integratie met jouw AI-worker ideeën + +Dit sluit zeer goed aan op jouw eerdere ideeën: + +- Neon database events +- worker servers +- auto-generated PR’s +- selective deploys +- batch execution + +Je kunt bijvoorbeeld: + +1. story wordt aangemaakt +2. worker krijgt event via SSE/webhook +3. repo wordt gecloned +4. AI implementeert wijziging +5. commit + push +6. PR automatisch aangemaakt +7. review pipeline start +8. merge → deploy + +Dit wordt veel eenvoudiger wanneer je volledige controle hebt over de Git infrastructuur. + +--- + +# Concrete roadmap + +## Fase 1 — huidige setup stabiliseren + +Hou: + +- GitHub +- Vercel +- Neon + +Voeg toe: + +- AI worker server +- webhooks +- automation pipeline + +--- + +## Fase 2 — interne Git infrastructuur + +Installeer: + +- Gitea +- Docker +- Postgres + +Gebruik dit voor: + +- experimenten +- AI-generated branches +- interne repos +- automation testing + +--- + +## Fase 3 — geavanceerde automation + +Later toevoegen: + +- self-hosted runners +- preview environments +- deploy approvals +- selective deployments +- agent orchestration + +--- + +# Eindadvies + +Voor jouw situatie: + +## Niet meteen GitHub vervangen + +Dat levert nu vooral extra beheerlast op. + +## Wel nu al beginnen met: + +- eigen AI worker server +- webhook automation +- lokale Git orchestration +- Gitea testomgeving + +Dat sluit perfect aan op: + +- Scrum4Me +- AI-assisted development +- batch story execution +- autonome pipelines diff --git a/docs/plans/lees-de-readme-md-validated-book.md b/docs/plans/lees-de-readme-md-validated-book.md new file mode 100644 index 0000000..c68f1eb --- /dev/null +++ b/docs/plans/lees-de-readme-md-validated-book.md @@ -0,0 +1,197 @@ +# Scrum4Me-Research — Zustand rearchitecture (reset + execute) + +## Context + +Het bestaande [docs/plans/zustand-store-rearchitecture.md](docs/plans/zustand-store-rearchitecture.md) beschrijft een doel-architectuur (`product-workspace-store` met genormaliseerde entities, race-safe loaders, resync-laag, optimistic mutations). De research-repo is dé plek om dat eerst te testen voordat het in `Scrum4Me/` belandt. + +Probleem nu: de research-repo wijkt af van het hoofdproject. Mijn custom `data-store.ts` lijkt qua vorm op de doel-architectuur, maar springt over de baseline heen. We willen aantonen dat de migratie *vanaf* de huidige Scrum4Me-patronen werkt, niet vanaf een verzonnen tussenvorm. + +Dus: eerst de research-repo terugbrengen naar dezelfde stores/hooks/routes als Scrum4Me nu heeft, dan de rearchitecture daarop uitvoeren. + +## Bron-documenten + +- **Doel-architectuur**: [docs/plans/zustand-store-rearchitecture.md](docs/plans/zustand-store-rearchitecture.md) (in research-repo). Dit plan voert dat document uit; herhaalt het niet. +- **Conventies**: [CLAUDE.md](../Scrum4Me/CLAUDE.md) hoofdproject. Taal NL, MD3 tokens, `@base-ui/react` render-prop, `*-server.ts`, enum UPPER_SNAKE↔lowercase via `lib/task-status.ts`. + +## Drie-faseplan + +### Fase A — Reset naar Scrum4Me-patronen + +Doel: onze research-pagina werkt op exact dezelfde store/hook/route-vorm als het hoofdproject, met identiek gedrag. + +**Verwijderen** (research-repo): +- [stores/data-store.ts](stores/data-store.ts) — mijn megastore +- [hooks/use-event-stream.ts](hooks/use-event-stream.ts) — vervangen door `use-backlog-realtime.ts` +- [hooks/use-browser-presence.ts](hooks/use-browser-presence.ts) — niet in main, drop voor reset +- [app/api/realtime/events/route.ts](app/api/realtime/events/route.ts) — vervangen door `app/api/realtime/backlog/route.ts` +- Mijn custom `loadX/resyncAll`-paden in selectie-componenten + +**Kopiëren uit `/Users/janpetervisser/Development/Scrum4Me/`** (1-op-1 of stripped van auth): +| Bron | Doel | +|---|---| +| `stores/backlog-store.ts` | `stores/backlog-store.ts` (`pbis`, `storiesByPbi`, `tasksByStory`; `setInitialData`, `applyChange`) | +| `stores/planner-store.ts` | `stores/planner-store.ts` (DnD-order; voor research nog niet gebruikt maar we zetten 'm klaar) | +| `stores/selection-store.ts` | overschrijf bestaand (state: `selectedPbiId`, `selectedStoryId`, geen taskId/productId in main; add `selectedTaskId` + `productId` als research-uitbreiding) | +| `stores/product-store.ts` | `stores/product-store.ts` (`currentProduct`) | +| `stores/products-store.ts` | `stores/products-store.ts` (lijst, voor pulldown) | +| `lib/realtime/use-backlog-realtime.ts` | `lib/realtime/use-backlog-realtime.ts` (SSE-client → `applyChange` op backlog-store) | +| `lib/task-status.ts` | `lib/task-status.ts` (enum-converters) | +| `app/api/realtime/backlog/route.ts` | `app/api/realtime/backlog/route.ts` (SSE+LISTEN, **research-only: strip auth/session/getAccessibleProduct** — vraagt enkel `product_id` querystring) | + +> ⚠️ **Auth-strip is research-only.** Het hoofdproject MOET sessie + `getAccessibleProduct()`-check op SSE en read-routes behouden. Bij backport vanaf de research-repo nooit de geknipte route 1-op-1 overnemen. Dit geldt voor zowel `app/api/realtime/backlog/route.ts` als alle read-routes onder `app/api/products/...`, `/pbis/...`, `/stories/...`, `/tasks/...`. + +**API-routes (read)** — bestaande paden behouden, alleen `force-dynamic` blijft: +- `GET /api/products` (list voor pulldown) +- `GET /api/products/[id]/pbis` (open: READY+BLOCKED) +- `GET /api/pbis/[id]/stories` +- `GET /api/stories/[id]/tasks` +- `GET /api/tasks/[id]` + +**Componenten herschrijven**: +- [components/product-select.tsx](components/product-select.tsx) → leest `useProductsStore`, schrijft naar `useProductStore.setCurrentProduct` +- [components/pbi-select.tsx](components/pbi-select.tsx) → leest `useBacklogStore` (filter op currentProduct), `useSelectionStore.selectPbi`. Triggert fetch op product-mount via een `useBacklogLoader`-helper die initial data binnenhaalt. +- [components/story-select.tsx](components/story-select.tsx) → idem voor stories +- [components/tasks-table.tsx](components/tasks-table.tsx) → leest `tasksByStory[selectedStoryId]`. **Max 10 rijen, scrollbaar** (al ingebouwd, behouden) +- [components/task-detail-card.tsx](components/task-detail-card.tsx) → fetcht task detail apart (geen full-fat backlog veld; matcht main's `tasks/[id]` route) +- [components/event-stream-panel.tsx](components/event-stream-panel.tsx) → blijft bestaan voor research-doel (event-tap), maar luistert nu mee op dezelfde EventSource via `use-backlog-realtime` (of een tweede readonly listener); selecteerbare events met JSON-detail rechts blijven. Twee checkboxes (Postgres / Browser). Truncate met ellipsis in de lijst. + +**Werkwijzen (verifiëren tijdens reset)**: +- Comments en UI-tekst NL +- Geen `bg-blue-500` etc; enkel MD3 tokens (`bg-primary`, `bg-card`, `bg-status-done`, ...) +- shadcn-componenten al `base-nova` style +- Server-only files krijgen `*-server.ts` suffix waar van toepassing (in deze fase niet nodig — alle DB-toegang loopt via `lib/prisma.ts` in route handlers) +- TaskStatus-mapping via `lib/task-status.ts` als de UI lowercase wil + +**Acceptatie Fase A**: +- `npx tsc --noEmit` schoon +- Pagina rendert, cascading werkt, tabel toont taken, detail-card vult, events stromen door (preview-verificatie) +- Stores matchen het hoofdproject qua vorm (vergelijking via `diff` uitvoerbaar voor backlog-store etc.) + +### Fase B — Rearchitecture uitvoeren + +Volgt de 15 stappen uit [docs/plans/zustand-store-rearchitecture.md](docs/plans/zustand-store-rearchitecture.md) §Implementatiepad. Concreet voor de research-repo: + +1. **Map** `stores/product-workspace/` aanmaken (factory + provider + selectors). +2. **`activeProduct`** wordt nu nog gespiegeld vanuit `useProductStore`; voor de research-pagina geen layout/server-side bepaling — we lezen het uit de pulldown-state. +3. **Selection migreren** — `selection-store` → `context.{activePbiId, activeStoryId, activeTaskId}` + `productId`. Setters cascaden de reset naar children (zoals doc beschrijft). +4. **Backlog naar entities + relations** — `pbisById`, `storiesById`, `tasksById`, `pbiIds`, `storyIdsByPbi`, `taskIdsByStory`. Selectors: + - `selectVisiblePbis(productId)` + - `selectStoriesForActivePbi(state)` + - `selectTasksForActiveStory(state)` + - `selectActivePbi/Story/Task(state)` +5. **Planner-state** in dezelfde workspace-store landen (`relations` slice); voor research: niet actief gebruikt, wel structureel meekoppen. +6. **Race-safe loaders** — `ensureProductLoaded`, `ensurePbiLoaded`, `ensureStoryLoaded`, `ensureTaskLoaded` met `requestId`-guard. Implementatie: + ```ts + setActivePbi(pbiId) { + const requestId = crypto.randomUUID() + set({ context: { ..., activePbiId: pbiId, ... }, loading: { ..., activeRequestId: requestId } }) + void get().ensurePbiLoaded(pbiId, requestId) + } + // in ensure: if (get().loading.activeRequestId !== requestId) return + ``` +7. **localStorage = restore hints** — `lastActivePbiIdByProduct`, `lastActiveStoryIdByProduct`, `lastActiveTaskIdByProduct`. Niet de waarheid, alleen hint die getoetst wordt aan toegankelijkheid. +8. **`use-backlog-realtime` dispatcht naar `applyRealtimeEvent`** — store interpreteert pbi/story/task I|U|D events, doet upsert + parent-id move + sort. +9. **Hidden tab beleid** — `EventSource` openhouden bij `hidden`. Op `visible` → `resyncActiveScopes('visible')`. +10. **Reconnect resync** — bij `ready` na disconnect of na exponential backoff: `resyncActiveScopes('reconnect')`. +11. **Unknown-event fallback** — onbekend event met `payload.product_id === activeProductId` → `resyncActiveScopes('unknown-event')`. Dit is wat het "veel events maar geen update" issue oplost. +12. **`force-dynamic` + `cache: 'no-store'`** — al gedaan in mijn fixes; behouden bij reset en versterken. +13. **Componenten naar selectors** — backlog-componenten lezen via `selectStoriesForActivePbi` etc., niet via raw store-velden. +14. **Tests** (Vitest, conform main): + - hydrate snapshot + - active selectie cascade + - race-safe ensure (laat trage promise van oude selectie geen nieuwe data overschrijven) + - SSE I|U|D voor pbi/story/task + - parent-move (story verandert van pbi) + - hidden→visible resync + - reconnect resync + - unknown-event resync + - delete-cleanup van actieve selectie + - localStorage restore-hint validatie tegen toegankelijkheid +15. **Sprint-workspace** — buiten scope; flag voor latere herhaling. + +**Optimistic mutations** (§Optimistic in doc): voor research geen DnD, dus alleen het patroon dropunten en niet bouwen. Wel: `applyOptimisticMutation`-action wel klaarzetten in de store-API zodat het patroon zichtbaar is. + +### Fase C — Werkwijzen verweven en doortrekken + +**Tijdens Fase A én B respecteren**: +1. **Plan mode workflow** — eerst Plan, ExitPlanMode, dan code. Bij grote wendingen opnieuw plannen. +2. **TodoWrite** voor multi-step werk; markeer immediate completion. +3. **Verify via preview** voor elke observable verandering (de hook reminder doet dit al). +4. **`tsc --noEmit`** voor afronden van een stap. +5. **Comments/Dutch** consistent. WHY-comments over de invariant; geen WHAT-comments. +6. **MD3 tokens** alleen. +7. **Geen secrets in chat** — `.env.local` blijft lokaal. +8. **Niet schrijven naar shared DB** zonder expliciete user-toestemming (geen `pg_notify` op shared channel). +9. **Source of truth = DB**. Zustand is projectie. localStorage = hint. +10. **Vóór elke fase**: kort statusrapport in de chat met wat er aankomt en waarom. + +**Doortrekken naar hoofdproject** (out-of-scope deze run, maar geflagd): +- Na bewezen werking in research-repo: backport `product-workspace-store` + selectors + realtime-apply + resync-laag naar `Scrum4Me/stores/product-workspace/`. +- **Niet backporten**: de auth-stripped routes uit research. Main behoudt iron-session, `getAccessibleProduct()`, en alle product-access/sprint/personal filters in z'n SSE- en read-routes. +- **Wel backporten**: store-shape, selectors, race-safe `ensure*Loaded`, hidden-tab beleid, `resyncActiveScopes`, unknown-event fallback, restore-hint patroon, `force-dynamic` + `cache: 'no-store'`. +- Migratie main-project zal langer duren (DnD, sprint, jobs, tests). Apart plan. + +## Bestandsmutaties (overzicht) + +### Verwijderen na Fase A +- [stores/data-store.ts](stores/data-store.ts) +- [hooks/use-event-stream.ts](hooks/use-event-stream.ts) +- [hooks/use-browser-presence.ts](hooks/use-browser-presence.ts) — komt deels terug in Fase B als helper voor visibility/online resync trigger +- [app/api/realtime/events/route.ts](app/api/realtime/events/route.ts) + +### Toevoegen Fase A (uit Scrum4Me) +- `stores/backlog-store.ts` +- `stores/planner-store.ts` +- `stores/selection-store.ts` (overschrijf) +- `stores/product-store.ts` +- `stores/products-store.ts` +- `lib/realtime/use-backlog-realtime.ts` +- `lib/task-status.ts` +- `app/api/realtime/backlog/route.ts` (zonder auth) + +### Toevoegen Fase B (nieuw, conform doc) +- `stores/product-workspace/store.ts` (zustand factory) +- `stores/product-workspace/selectors.ts` +- `stores/product-workspace/types.ts` +- `stores/product-workspace/restore.ts` (localStorage hints) +- `stores/product-workspace/realtime-apply.ts` (SSE event → store) +- `stores/product-workspace/resync.ts` (`resyncActiveScopes`, `resyncLoadedScopes`) +- `tests/product-workspace/*.test.ts` (Vitest, install vitest als devDep) + +### Te aanpassen in Fase B +- Alle `components/*.tsx` (nu shadcn select/table/card panels) → consumeren via selectors uit workspace-store +- `lib/realtime/use-backlog-realtime.ts` → dispatcht `applyRealtimeEvent` naar workspace-store i.p.v. `applyChange` naar backlog-store +- `event-stream-panel.tsx` → blijft bestaan (research-tap), maar leest events ook uit workspace-store of via een dunne `event-log-store` ernaast (in bounded-context-stijl: aparte log-store voor pure observatie hoort er niet thuis in de workspace-store) + +## Verificatie + +### Na Fase A (baseline) +1. `npm run dev` op port 3001 +2. Pagina laadt, cascading werkt: product → PBI → story → tasks +3. Detail-card vult bij klik op task +4. Event-paneel toont realtime events (truncate + JSON-detail) +5. `npx tsc --noEmit` schoon +6. Vergelijk: `diff Scrum4Me/stores/backlog-store.ts Scrum4Me-Research/stores/backlog-store.ts` → identiek (modulo lokale interface-uitbreidingen waar gedocumenteerd) + +### Na Fase B (target) +Alle acceptatiecriteria uit [docs/plans/zustand-store-rearchitecture.md §Acceptatiecriteria](docs/plans/zustand-store-rearchitecture.md): +- Eén waarheid per entity in de store ✓ +- Selectors als enige UI-leesweg ✓ +- SSE patcht zonder full-page refresh ✓ +- Hidden→visible herstelt missers binnen één resync-cyclus ✓ +- Reconnect resync werkt zonder NOTIFY-replay ✓ +- Directe task-edits zonder `entity:'task'` NOTIFY worden via unknown-event fallback zichtbaar ✓ +- LocalStorage = hint, geen forced state ✓ +- `force-dynamic` + `cache: 'no-store'` overal ✓ + +### Manuele preview-verificatie (na elke fase) +- TODO via TodoWrite tijdens uitvoer; preview-screenshot na grote stappen +- Tab-switch test: open page, switch tab, doe een wijziging via een ander mechanisme (psql na user-akkoord, of UI in main-project), keer terug → verwacht: zonder warnings + data gerefresht + +## Open vragen / risico's + +1. **Reset-import** uit hoofdproject: voor de research-repo strippen we auth/session-deps uit de gekopieerde routes (research-repo heeft geen auth-laag, draait lokaal). Belangrijk: **dit is een research-repo-keuze; main behoudt de volledige auth-filters**. Zie de waarschuwing onder "API-routes (read)" hierboven. +2. **`use-backlog-realtime` heeft mogelijk auth-headers/session-checks**: bevestigen tijdens copy. Indien zo: research-versie gebruikt geen auth, route is publiek-bereikbaar binnen lokale dev. Geldt alleen lokaal — geen wijziging aan main. +3. **Tests-deps** (vitest, @testing-library/react) toevoegen tijdens Fase B. Of pas in Fase B step 14 vanwege scope. +4. **Event-paneel toekomst**: blijft het in research-repo of stoten we het af zodra de workspace-store af is? Voorstel: behouden als observatie-tool, maar er aparte `event-log-store` (kleine UI store) voor maken zodat het niet meelift in de workspace-store. +5. **README.md** update na Fase B (optioneel) — kort beschrijven dat dit nu het canonical migratie-pad demonstreert. diff --git a/docs/plans/zustand-store-rearchitecture.md b/docs/plans/zustand-store-rearchitecture.md new file mode 100644 index 0000000..957a77c --- /dev/null +++ b/docs/plans/zustand-store-rearchitecture.md @@ -0,0 +1,746 @@ +--- +title: "Zustand store rearchitecture - active context, realtime en resync" +status: ready-to-execute +audience: [maintainer, contributor, ai-agent] +language: nl +last_updated: 2026-05-09 +revision: 3 +--- + +# Zustand store rearchitecture + +Doel: de client-state van Scrum4Me voorspelbaar houden terwijl de app groeit. +De database blijft de bron van waarheid. Zustand wordt de live client-projectie +voor de actieve workflow: snel genoeg voor optimistic UI, robuust genoeg tegen +gemiste SSE-events, hidden tabs en onbekende notify-vormen. + +## Kernkeuze + +Geen app-brede megastore en ook geen pure splits per pagina. Stores per bounded +context: + +| Store | Scope | Verantwoordelijkheid | +|---|---|---| +| `product-workspace-store` | actief product | active context, product backlog data, selectie, DnD-order, SSE, resync | +| `sprint-workspace-store` | actieve sprint | sprint stories, sprint tasks, assignment, sprint-DnD, SSE, resync | +| `solo-store` | actieve user + product | uitvoerbare taken, worker/job status, solo-kanban realtime | +| `notifications-store` | user | vragen, alerts, notification badge | +| `idea-store` | idee/product | grill/make-plan jobstate, open vragen, idea-status | +| `jobs-store` | jobs pagina | actieve/afgeronde jobs en jobs-page selectie | +| kleine UI stores | app/client | debug mode, lichte UI-voorkeuren | + +De huidige `backlog-store`, `planner-store`, `selection-store` en +`product-store` worden samengevoegd tot `product-workspace-store`. Ze +beschrijven dezelfde workflow en verdelen nu PBI/story/task waarheid, +order-state en selectie over meerdere stores. + +## Source of truth + +| Data | Waarheid | Zustand rol | Persistentie | +|---|---|---|---| +| `activeProductId` | `users.active_product_id` in DB | client mirror voor navigatie en actieve stream | DB | +| `activePbiId` | runtime selectie of URL | active context | optioneel localStorage restore hint | +| `activeStoryId` | runtime selectie of URL | active context | optioneel localStorage restore hint | +| `activeTaskId` | runtime selectie of URL dialog param | active context + task detail | optioneel localStorage restore hint | +| PBI/story/task data | DB | genormaliseerde live client-projectie | geen localStorage | +| DnD-order | DB `sort_order`, tijdelijk optimistic in store | relatie-arrays met rollback | DB na server action | +| filters/sort UI | client preference | component/store UI state | localStorage mag | + +LocalStorage is dus geen waarheid voor actieve entiteiten. Het mag alleen helpen +om een vorige selectie te herstellen nadat de server de actieve product-context +heeft bepaald — én alleen als de hint-id na laden nog bestaat in de store. + +## Product workspace store + +Vorm: + +```ts +type ProductWorkspaceStore = { + context: { + activeProduct: { id: string; name: string } | null + activePbiId: string | null + activeStoryId: string | null + activeTaskId: string | null + } + + entities: { + pbisById: Record + storiesById: Record + tasksById: Record + } + + relations: { + pbiIds: string[] + storyIdsByPbi: Record + taskIdsByStory: Record + } + + loading: { + loadedProductId: string | null + loadingProductId: string | null + loadedPbiIds: Record + loadedStoryIds: Record + loadedTaskIds: Record + activeRequestId: string | null + } + + sync: { + realtimeStatus: 'connecting' | 'open' | 'disconnected' + lastEventAt: number | null + lastResyncAt: number | null + resyncReason: ResyncReason | null + } + + hydrateSnapshot(snapshot: ProductBacklogSnapshot): void + setActiveProduct(product: { id: string; name: string } | null): void + setActivePbi(pbiId: string | null): void + setActiveStory(storyId: string | null): void + setActiveTask(taskId: string | null): void + + ensureProductLoaded(productId: string, requestId?: string): Promise + ensurePbiLoaded(pbiId: string, requestId?: string): Promise + ensureStoryLoaded(storyId: string, requestId?: string): Promise + ensureTaskLoaded(taskId: string, requestId?: string): Promise + + applyRealtimeEvent(event: ProductRealtimeEvent): void + resyncActiveScopes(reason: ResyncReason): Promise + resyncLoadedScopes(reason: ResyncReason): Promise + + applyOptimisticMutation(mutation: OptimisticMutation): string + rollbackMutation(mutationId: string): void + settleMutation(mutationId: string): void +} +``` + +State blijft vlak en genormaliseerd. Componenten lezen via selectors: + +```ts +selectVisiblePbis(state) +selectStoriesForActivePbi(state) +selectTasksForActiveStory(state) +selectActivePbi(state) +selectActiveStory(state) +selectActiveTask(state) +``` + +Een task zit in `tasksById` als `BacklogTask` (lite) zolang alleen de lijst +geladen is, en wordt verrijkt naar `TaskDetail` zodra `ensureTaskLoaded` is +gedraaid. Componenten gebruiken een `isDetail()`-typeguard voor de extra +velden. + +## Active context flow + +### Product wisselen + +```txt +setActiveProduct(product) +-> nieuw requestId, zet activeRequestId +-> zet activeProduct, reset activePbiId/activeStoryId/activeTaskId +-> reset entities + relations als product wisselt +-> SSE-stream wisselt mee met product.id +-> ;(async) await ensureProductLoaded(product.id, requestId) +-> nadat ensure resolved + activeRequestId nog == requestId: + probeer restore hint (activePbiId) — alleen als hint-id in entities zit +``` + +`activeProduct` komt server-side uit de layout via `users.active_product_id`. +De client-store spiegelt dit zodat client componenten niet overal props hoeven +te ontvangen. + +### PBI selecteren + +```txt +setActivePbi(pbiId) +-> nieuw requestId +-> zet activePbiId, reset activeStoryId en activeTaskId +-> schrijf lastActivePbiIdByProduct[productId] als restore hint +-> ;(async) await ensurePbiLoaded(pbiId, requestId) +-> nadat ensure resolved + activeRequestId nog == requestId: + probeer restore hint (activeStoryId) +``` + +### Story selecteren + +```txt +setActiveStory(storyId) +-> nieuw requestId +-> zet activeStoryId, reset activeTaskId +-> ensureStoryLoaded(storyId, requestId) +-> schrijf lastActiveStoryIdByProduct[productId] als restore hint +``` + +### Task selecteren + +```txt +setActiveTask(taskId) +-> zet activeTaskId +-> ensureTaskLoaded(taskId) +-> schrijf lastActiveTaskIdByProduct[productId] als restore hint +``` + +### Race-safe loaders + +Setters mogen loaders starten, maar loaders moeten race-safe zijn. + +```ts +setActivePbi(pbiId) { + const requestId = crypto.randomUUID() + + set((s) => { + s.context.activePbiId = pbiId + s.context.activeStoryId = null + s.context.activeTaskId = null + s.loading.activeRequestId = requestId + }) + + void get().ensurePbiLoaded(pbiId, requestId) +} +``` + +Bij terugkomst: + +```ts +if (get().loading.activeRequestId !== requestId) return +``` + +Een trage fetch van een oude selectie mag nooit de nieuwste selectie of data +overschrijven. + +> **Niet `state.ensureXxx(...)` aanroepen vanuit een gecaptured snapshot.** +> Method-references zijn niet noodzakelijk identiek over verschillende +> immer-state-versies. Roep acties intern aan via `get().ensureXxx(...)` +> direct vóór gebruik. Zie §Implementation-gotchas G4. + +## Hydration-strategie + +Er zijn twee patronen. Kies bewust per pagina. + +### Patroon A — Server snapshot (productpagina, sprintboard) + +Voor pagina's die een specifieke product-route hebben en SSR/RSC kunnen +benutten: + +```txt +1. Server layout leest `users.active_product_id`. +2. Server-page fetcht initial backlog snapshot voor dat product. +3. Client krijgt snapshot via prop / `BacklogHydrationWrapper` → `hydrateSnapshot()`. +4. Vervolgens: + - leest store optionele restore hints (activePbiId/Story/Task). + - Herstelt alleen als id nog bestaat in entities en toegankelijk is. + - Anders selectie leeg laten. +5. SSE-hook mount op activeProductId. +``` + +### Patroon B — Cascading client-load (productpicker zonder server-context) + +Voor pagina's zonder server-determined product (b.v. een dashboard met +product-pulldown). De `hydrateSnapshot` blijft beschikbaar als API maar wordt +niet gebruikt; loaders worden door de UI getriggerd via `setActiveProduct`, +`setActivePbi`, etc. + +```txt +1. UI biedt productpicker; geen server-side activeProduct. +2. Op mount: lees `lastActiveProductId` uit localStorage (als hint). +3. setActiveProduct(restoredProduct) → trigger ensureProductLoaded. +4. Na elke ensure*Loaded: pas vervolg-restore-hint toe (zie restore-flow). +5. SSE-hook mount op activeProductId. +``` + +### Restore-hint flow + +```txt +setActiveProduct(p): + ;(async () => { + await ensureProductLoaded(p.id, requestId) + if (loading.activeRequestId !== requestId) return + const hint = hints.perProduct[p.id]?.lastActivePbiId + if (hint && entities.pbisById[hint]) setActivePbi(hint) + })() + +setActivePbi(pbiId): + ;(async () => { + await ensurePbiLoaded(pbiId, requestId) + if (loading.activeRequestId !== requestId) return + const hint = hints.perProduct[productId]?.lastActiveStoryId + if (hint && entities.storiesById[hint]) setActiveStory(hint) + })() +``` + +> **Geen setTimeout(0) of microtask-trick.** De fetch is dan nog niet klaar, +> dus de validatie `entities.byId[hint]` faalt altijd. Chain dus altijd +> `await ensureXxxLoaded` en valideer in dezelfde requestId-cycle. + +Als een route een expliciete task in de URL heeft, wint de URL boven de +restore hint. Voorbeeld: `?editTask=` of een toekomstige deep link. + +## SSE integratie + +De SSE-hook beheert alleen transport: + +```txt +useProductWorkspaceRealtime(activeProductId) +-> opent /api/realtime/backlog?product_id=... +-> parsed events +-> dispatcht naar store.applyRealtimeEvent(event) +-> beheert reconnect/backoff/status (via store.setRealtimeStatus) +-> op 'ready' na (re)connect: void store.resyncActiveScopes('reconnect') +``` + +De store beheert de betekenis: + +```txt +PBI insert/update +-> upsert pbisById +-> voeg id toe aan pbiIds indien nodig +-> sorteer pbiIds op priority/sort_order + +PBI delete +-> verwijder pbi +-> verwijder child stories en tasks +-> clear actieve selectie als die onder deze PBI viel + +Story insert/update +-> upsert storiesById +-> verplaats id tussen storyIdsByPbi indien pbi_id wijzigt +-> sorteer alleen de betrokken parent-lijsten + +Story delete +-> verwijder story +-> verwijder child tasks +-> clear activeStoryId/activeTaskId indien nodig + +Task insert/update +-> upsert tasksById +-> verplaats id tussen taskIdsByStory indien story_id wijzigt +-> sorteer alleen de betrokken task-lijst + +Task delete +-> verwijder task +-> clear activeTaskId indien nodig +``` + +SSE-events zijn idempotent. Een event dat al optimistisch is toegepast, mag +geen dubbele insert of verkeerde rollback veroorzaken. + +## Reconciliation en resync + +SSE is snel, maar niet voldoende als enige correctheidsmechanisme: + +- browsers kunnen hidden tabs throttlen of freezen; +- Postgres NOTIFY heeft geen replay; +- de tab kan offline zijn; +- een event-router kan een relevant semantisch event niet herkennen; +- sommige wijzigingen vereisen refetch in plaats van een kleine patch. + +Daarom krijgt de store een expliciete resync-laag. + +```ts +type ResyncReason = + | 'visible' + | 'reconnect' + | 'manual' + | 'unknown-event' + | 'stale-scope' + | 'mutation-settled' +``` + +### Hidden tab beleid + +Sluit de SSE-stream niet actief zodra de tab hidden wordt. Laat `EventSource` +open zolang browser en netwerk dit toelaten. + +Bij overgang hidden -> visible: + +```txt +resyncActiveScopes('visible') +``` + +Bij reconnect of nieuw `ready` event na disconnect: + +```txt +resyncActiveScopes('reconnect') +``` + +> Het bestaande `use-backlog-realtime.ts` sluit de EventSource op `hidden`. +> Vervang dat gedrag in dezelfde PR als waarin `resyncActiveScopes('visible')` +> wordt toegevoegd; los gezien zou je oude gedrag kwijtraken zonder vangnet. + +### Active scopes + +Minimale resync: + +```txt +activeProductId -> refetch product/PBI snapshot +activePbiId -> refetch stories voor PBI +activeStoryId -> refetch tasks voor story +activeTaskId -> refetch task detail +``` + +Implementeer `resyncActiveScopes` zonder gecaptured snapshot: + +```ts +async resyncActiveScopes(reason) { + const ctx = get().context + const tasks: Promise[] = [] + if (ctx.activeProduct?.id) tasks.push(get().ensureProductLoaded(ctx.activeProduct.id)) + if (ctx.activePbiId) tasks.push(get().ensurePbiLoaded(ctx.activePbiId)) + if (ctx.activeStoryId) tasks.push(get().ensureStoryLoaded(ctx.activeStoryId)) + if (ctx.activeTaskId) tasks.push(get().ensureTaskLoaded(ctx.activeTaskId)) + set((s) => { s.sync.lastResyncAt = Date.now(); s.sync.resyncReason = reason }) + await Promise.allSettled(tasks) +} +``` + +Als de UX merkt dat eerder bezochte panels stale blijven, breid dit uit naar +`resyncLoadedScopes`, dat alle scopes in `loadedPbiIds`, `loadedStoryIds` en +`loadedTaskIds` parallel herlaadt. + +### Unknown relevant events + +Een SSE-route mag onbekende product-events niet stil negeren — maar ook niet +elk geluid blind als refetch-trigger interpreteren. + +```txt +known pbi/story/task event +-> applyRealtimeEvent(event) + +known semantic event, zoals story_log of claude_job_status +-> patch specifieke slice of markeer scope stale + +unknown event met product_id == activeProductId EN entity-shape +-> resyncActiveScopes('unknown-event') + +worker_*, claude_job_*, heartbeat, question-events +-> negeer voor de product-workspace, behoort op andere bounded contexts + (solo-store, notifications-store, jobs-store) +``` + +Concrete filter: + +```ts +function isUnknownEntityEvent(p: Record): boolean { + if (typeof p.entity !== 'string') return false + if (['pbi', 'story', 'task'].includes(p.entity)) return false + if ('type' in p) return false // job/worker hebben `type` + return true +} +``` + +## Fetch en cache regels + +Read-routes die store-data voeden: + +```ts +export const dynamic = 'force-dynamic' +``` + +Client fetches vanuit `ensure...Loaded` en `resync...`: + +```ts +fetch(url, { cache: 'no-store' }) +``` + +SSE-routes blijven ook `force-dynamic`. SSE-routes zelf veranderen niet door +deze rearchitecture: auth (`getSession()`) en `getAccessibleProduct()` blijven +leidend. De rearchitecture raakt alleen wat de client met de events doet. + +Waar nuttig stuurt de SSE-route na `LISTEN` een initial state event. Dat +voorkomt de race waarbij de status wijzigt tussen de eerste DB-read en het +moment dat LISTEN actief is. + +## Optimistic updates + +Voor DnD en status toggles: + +```txt +1. Maak mutationId. +2. Bewaar rollback snapshot van alleen de betrokken relatie/entity. +3. Patch store direct. +4. Start server action. +5. Success: settle mutation. +6. Error: rollback mutation. +7. SSE echo: idempotent toepassen of markeren als bevestigd. +``` + +Voor create/update/delete dialogs is optimisme optioneel. Standaard mag: + +```txt +server action -> resultaat in store verwerken -> SSE echo idempotent negeren +``` + +## Sprint workspace + +Na stabilisatie van product-workspace volgt dezelfde vorm voor sprint: + +```txt +sprint-workspace-store + activeSprintId + selectedStoryId + selectedTaskId + storiesById + tasksById + pbisById + storyIdsByPbi + sprintStoryIds + taskIdsByStory + loaded scopes + applyRealtimeEvent() + resyncActiveScopes() +``` + +Dit vervangt op termijn de combinatie van lokale state in de sprint board en +de huidige `sprint-store` order maps. Pak het pas op nadat product-workspace +in productie staat en de eerste paar weken stabiel draait. + +## Implementation-gotchas + +Deze pitfalls zijn subtiel genoeg om opnieuw te maken. Documenteer ze in code +via comments boven de fix. + +### G1. `s.byId[x] ?? []` triggert React-loop + +```ts +// FOUT: nieuwe array per render → "Maximum update depth exceeded" +const stories = useStore((s) => s.storiesByPbi[pbiId] ?? []) + +// GOED: stable empty const op module-level +const EMPTY: BacklogStory[] = [] +const stories = useStore((s) => s.storiesByPbi[pbiId] ?? EMPTY) +``` + +### G2. List-selectors materialiseren → `useShallow` verplicht + +`selectVisiblePbis(state)` en vrienden bouwen `ids.map(id => byId[id])` per +call. Zonder `useShallow` re-rendert het component op elke onafhankelijke +store-mutatie. + +```ts +import { useShallow } from 'zustand/react/shallow' +const list = useStore(useShallow(selectVisiblePbis)) +``` + +Single-value selectors (b.v. `selectActivePbi`) hebben dit niet nodig — die +retourneren een stable entity-reference. + +### G3. immer + `setState((s) => ({...}))` REPLACES de state + +Met `zustand/middleware/immer` interpreteert `produce` een return-waarde als +de nieuwe state. Dat lijkt op de pre-immer Zustand-API maar wist alle andere +slices én alle action-properties. + +```ts +// FOUT (in immer-middleware): vervangt hele state met { context: {...} } +useStore.setState((s) => ({ context: { ...s.context, activePbiId: 'x' } })) + +// GOED: mutation-style (immer recipe muteert draft) +useStore.setState((s) => { s.context.activePbiId = 'x' }) +``` + +### G4. Method-refs zijn niet stabiel over state-versies + +```ts +// FOUT: state-snapshot kan andere method-ref hebben dan de huidige +async resyncActiveScopes(reason) { + const state = get() + // ... + state.ensureProductLoaded(...) // niet betrouwbaar +} + +// GOED: per call fresh ophalen via get() +async resyncActiveScopes(reason) { + const ctx = get().context + // ... + get().ensureProductLoaded(...) +} +``` + +### G5. Tests die acties mocken via setState lekken naar volgende tests + +`useStore.setState({ resyncActiveScopes: vi.fn() })` blijft staan na de test. +`beforeEach` reset alleen data-velden. Snapshot originele acties op +module-load + restore in `beforeEach`: + +```ts +const originalActions = (() => { + const s = useStore.getState() + return { resyncActiveScopes: s.resyncActiveScopes, /* ... */ } +})() + +function resetStore() { + useStore.setState({ ...initialData, ...originalActions }) +} +``` + +### G6. localStorage in vitest 4 + jsdom 29 + +In deze combinatie is `localStorage.clear/getItem/setItem` niet aanwezig op +het globale localStorage-object. Bind in `tests/setup.ts` een eigen +MemoryStorage: + +```ts +class MemoryStorage implements Storage { /* ... */ } +const memory = new MemoryStorage() +Object.defineProperty(globalThis, 'localStorage', { value: memory, configurable: true }) +Object.defineProperty(window, 'localStorage', { value: memory, configurable: true }) +``` + +### G7. `fetch` in node-test omgeving accepteert geen relative URLs + +`/api/products/...` faalt met `Invalid URL`. Mock fetch in elke test die +indirect een `ensure*Loaded` aanroept, of stub de implementatie. + +### G8. `Response`-body wordt één keer geconsumeerd + +`vi.spyOn(fetch).mockResolvedValue(response)` levert dezelfde Response aan +elke fetch — eerste `.json()` werkt, daarna error. Gebruik +`mockImplementation()` voor een fresh body per call: + +```ts +vi.spyOn(globalThis, 'fetch').mockImplementation(() => + Promise.resolve(new Response(JSON.stringify([]), { status: 200 })), +) +``` + +## Testing setup (Vitest) + +Minimale config: + +```ts +// vitest.config.ts +import { defineConfig } from 'vitest/config' +import path from 'node:path' +export default defineConfig({ + test: { + environment: 'jsdom', + include: ['tests/**/*.test.ts', 'tests/**/*.test.tsx'], + setupFiles: ['tests/setup.ts'], + }, + resolve: { alias: { '@': path.resolve(__dirname, '.') } }, +}) +``` + +`tests/setup.ts` bevat MemoryStorage-binding (zie G6) en `vi.restoreAllMocks()` +in `beforeEach`. + +Verplichte test-cases per workspace-store: + +- `hydrateSnapshot` vult entities + relations. +- Selection cascade: `setActivePbi` reset story+task; `setActiveStory` reset + task. +- `setActiveProduct(null)` ruimt entities en relations op. +- `applyRealtimeEvent`: pbi/story/task `I|U|D` met sortering en parent-move. +- `applyRealtimeEvent`: event voor ander product wordt genegeerd. +- `applyRealtimeEvent`: unknown entity met matching product → resync trigger. +- Delete-cleanup van actieve selectie. +- `ensureProductLoaded` fetch + sortering. +- Race-safe `ensure*Loaded` met requestId-guard (oude in-flight mag niet + nieuwere selectie overschrijven). +- `ensureTaskLoaded` zet detail-flag. +- `resyncActiveScopes` triggert ensure-keten met juiste URLs en zet + `lastResyncAt` + `resyncReason`. +- localStorage restore-hints: `setActiveProduct` en `setActivePbi` schrijven + de juiste keys. +- Optimistic mutation: rollback herstelt vorige state; settle ruimt pending + op; SSE-echo wordt idempotent verwerkt. + +## Implementatiepad + +Migratie van het bestaande systeem (`backlog-store` + `planner-store` + +`selection-store` + `product-store`) naar `product-workspace-store`. Doe dit +in opeenvolgende PRs zodat elke stap in productie te verifiëren is. + +### Stap 1 — Skelet opzetten (nieuwe store, nog niet gebruikt) + +1. Maak `stores/product-workspace/` met: + - `types.ts` (entity types, snapshot, realtime event union, ResyncReason). + - `store.ts` (factory met immer-middleware, alle slices, alle acties). + - `selectors.ts` (pure functies, useShallow-vriendelijk). + - `restore.ts` (localStorage hints met validatie). +2. Voeg `applyRealtimeEvent`, `resyncActiveScopes`, `resyncLoadedScopes`, + `ensure*Loaded` toe — eerst zonder integraties; getest via Vitest. +3. Acceptatie: alle test-cases uit §Testing setup groen. Geen UI-impact nog. + +### Stap 2 — Hydration overstappen + +4. `BacklogHydrationWrapper` (of equivalent) roept `hydrateSnapshot` aan op de + nieuwe store i.p.v. `useBacklogStore.setInitialData`. +5. `lib/realtime/use-backlog-realtime.ts` dispatcht naar + `useProductWorkspaceStore.applyRealtimeEvent` i.p.v. `applyChange` op de + oude store. +6. Componenten lezen voorlopig nog van de oude stores; nieuwe store loopt + parallel mee als read-only schaduwkopie. Vergelijk in dev-tools dat de + inhoud klopt. + +### Stap 3 — Componenten omzetten + +7. Per panel/component: vervang `useBacklogStore`/`useSelectionStore`/ + `useProductStore` reads door selectors uit de workspace-store + `useShallow` + waar nodig. +8. Setters (`selectPbi`, `selectStory`, `setCurrentProduct`) vervangen door de + workspace-acties (`setActivePbi`, `setActiveStory`, `setActiveProduct`). +9. Handle G1 en G2 expliciet: stable empty refs, useShallow voor lijsten. + +### Stap 4 — Race-safe en restore-hints + +10. `ensure*Loaded` met `activeRequestId`-guard implementeren conform + §Race-safe loaders. +11. localStorage hints introduceren met `await ensure*Loaded`-chain (zie + §Restore-hint flow). +12. Verifieer in een staging-omgeving: cold reload met persisted hint + herstelt selectie zonder fouten. + +### Stap 5 — Hidden-tab + reconnect-resync + +13. `use-backlog-realtime` aanpassen: niet meer sluiten op `hidden`, blijven + luisteren. Op `ready` na reconnect: `resyncActiveScopes('reconnect')`. +14. Aparte `useWorkspaceResync()`-hook: trigger `resyncActiveScopes('visible')` + bij `visibilitychange` van hidden→visible, en bij `online`-event. +15. Doe deze twee in één PR — los gezien zou je oude gedrag kwijtraken zonder + vangnet. + +### Stap 6 — Unknown-event fallback + +16. `applyRealtimeEvent` handelt onbekende entity-events af volgens §Unknown + relevant events. Filter op `isUnknownEntityEvent(payload)`. +17. Verifieer dat job-/worker-/heartbeat-events GEEN refetch triggeren. + +### Stap 7 — Cache-headers + +18. Zet `cache: 'no-store'` op alle client fetches uit `ensure*Loaded` en + `resync...`. +19. Bevestig `force-dynamic` op alle read-routes die store-data leveren. + +### Stap 8 — Oude stores opruimen + +20. Verwijder `stores/backlog-store.ts`, `stores/planner-store.ts`, + `stores/selection-store.ts`, `stores/product-store.ts` zodra geen + component er nog op leest. Grep over de hele codebase om verrassingen + te voorkomen. +21. `stores/products-store.ts` blijft (lijst van producten ≠ active product). + +### Stap 9 — Sprint workspace + +22. Herhaal het patroon voor `sprint-workspace-store`. Eerste een paar weken + de product-workspace stabiel laten draaien. + +## Acceptatiecriteria + +- Een PBI/story/task bestaat als waarheid maar op één plek in de + client-store. +- Product backlog panels lezen via selectors uit dezelfde workspace-store. +- PBI/story/task SSE-events patchen de store zonder full page refresh. +- Hidden -> visible herstelt gemiste wijzigingen binnen één resync-cyclus. +- Reconnect herstelt gemiste wijzigingen zonder afhankelijkheid van NOTIFY + replay. +- Directe entity-edits zonder herkenbare delta worden via resync zichtbaar + (unknown-event filter staat aan, job/worker noise niet meegerekend). +- LocalStorage kan een vorige selectie herstellen, maar nooit ontoegankelijke + of verwijderde entiteiten forceren; valideer altijd na ensure-load. +- Optimistic DnD heeft rollback en wordt niet dubbel toegepast door SSE + echoes. +- Read-routes en client fetches leveren geen stale browser/Next cache data. +- Test-suite dekt §Testing setup checklist en draait groen in CI. +- Geen "Maximum update depth exceeded" of "result of getServerSnapshot + should be cached" warnings in de console (zie §G1 en §G2). +- Auth en `getAccessibleProduct()` blijven ongewijzigd op SSE/read-routes; + deze rearchitecture raakt alleen client-state, geen serverlaag-security. diff --git a/docs/recommendations/beelink-ubuntu-scrum4me-server-caveman-plan.md b/docs/recommendations/beelink-ubuntu-scrum4me-server-caveman-plan.md new file mode 100644 index 0000000..cb062c2 --- /dev/null +++ b/docs/recommendations/beelink-ubuntu-scrum4me-server-caveman-plan.md @@ -0,0 +1,458 @@ +--- +title: "Caveman plan — Beelink naar Ubuntu Scrum4Me server" +status: draft +audience: [maintainer, operator] +language: nl +last_updated: 2026-05-09 +--- + +# Caveman plan — Beelink naar Ubuntu Scrum4Me server + +## Doel + +Zet de Beelink mini-PC om naar een dual-boot machine waarop **Ubuntu Server 24.04 LTS** de standaard server-boot is. Windows blijft bestaan als fallback, maar Scrum4Me draait op Ubuntu. + +Doelopstelling: + +```text +Beelink mini-PC +├─ Windows fallback +└─ Ubuntu Server 24.04 LTS default + ├─ Docker Engine + ├─ Postgres + ├─ Scrum4Me webserver + ├─ worker-idea + ├─ worker-implementation + └─ worker-orchestrator +``` + +## Hardware + +Bekende specs: + +| Onderdeel | Waarde | +|---|---| +| Merk | Beelink | +| CPU | Intel Core i5-12450H | +| CPU boost | Tot 4,4 GHz | +| RAM | 32 GB DDR4 | +| Opslag | 1 TB | +| GPU | Intel integrated graphics | +| Vorm | Mini-PC | + +Conclusie: geschikt voor Scrum4Me als single-user/small-team server, mits implementation-concurrency op 1 blijft en resource limits strak staan. + +## Caveman Regels + +- Geen Ubuntu Desktop installeren. +- Geen GPU-driver installeren tenzij beeld echt kapot is. +- Geen Docker Desktop. +- Geen Postgres-poort naar internet. +- Geen Docker socket mounten in workercontainers. +- Geen repos, caches of worktrees op de Windows-partitie. +- Alles onder `/srv/scrum4me`. +- Eerst één worker werkend krijgen, daarna pas drie. +- Eerst via lokaal IP testen, daarna pas domein/TLS. +- Ubuntu wordt default boot; Windows is fallback. + +## Waarom Geen Drivergedoe Verwacht Wordt + +De CPU heeft Intel integrated graphics. Ubuntu Server heeft geen desktop nodig. Intel geeft aan dat de meeste Linux-distributies Intel graphics drivers al meeleveren. Voor deze machine verwacht je de kernel-driver `i915`. + +Na installatie alleen checken: + +```bash +lspci -k | grep -EA3 'VGA|3D|Display' +lsmod | grep i915 +``` + +Als `i915` zichtbaar is: klaar. Niet verder aan sleutelen. + +## Fase 0 — Voorbereiding In Windows + +1. Maak backup van belangrijke Windows-data. +2. Sla BitLocker recovery key op als BitLocker aan staat. +3. Zet Windows Fast Startup uit: + - Control Panel + - Power Options + - Choose what the power buttons do + - Turn off fast startup +4. Maak vrije ruimte: + - Open Disk Management. + - Shrink `C:`. + - Laat ongeveer `600 GB` unallocated voor Ubuntu. + +Aanbevolen diskverdeling: + +```text +Windows: 250-300 GB +Ubuntu /: 120 GB +/srv/scrum4me: rest van vrije ruimte +swapfile: 16 GB +EFI: bestaande EFI behouden +``` + +## Fase 1 — Ubuntu USB Maken + +1. Download Ubuntu Server 24.04 LTS amd64. +2. Maak USB-stick met Rufus of Balena Etcher. +3. Sluit ethernet aan op de Beelink. +4. Boot van USB. + +Veelvoorkomende Beelink toetsen: + +```text +Boot menu: F7 +BIOS: Del +``` + +BIOS-checks: + +```text +UEFI boot: aan +Secure Boot: mag aan blijven, maar uitzetten als install gedoe geeft +Power on after power loss: aan +Ubuntu later als eerste boot entry +``` + +## Fase 2 — Ubuntu Installeren + +Kies tijdens installatie: + +```text +Install Ubuntu Server +OpenSSH server: YES +Desktop: NO +Storage: Custom layout +``` + +Storage: + +```text +Bestaande EFI partition: + mount: /boot/efi + formatteren: NEE + +Nieuwe ext4 partition 120 GB: + mount: / + +Nieuwe ext4 partition rest: + mount: /srv/scrum4me +``` + +Niet kiezen: + +```text +Use entire disk +``` + +Dat zou Windows verwijderen. + +## Fase 3 — Eerste Boot + +Login op Ubuntu. + +```bash +sudo apt update +sudo apt upgrade -y +sudo reboot +``` + +Na reboot: + +```bash +sudo hostnamectl set-hostname scrum4me-server +ip a +``` + +Zet in je router een DHCP reservation voor het IP-adres. Dat is simpeler dan handmatige netwerkconfiguratie. + +## Fase 4 — Server Niet Laten Slapen + +```bash +sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target +``` + +In BIOS: + +```text +Power on after power loss: ON +Sleep: OFF als optie bestaat +Boot order: Ubuntu eerst +``` + +## Fase 5 — Basis Tools + +```bash +sudo apt install -y git curl ca-certificates gnupg htop iotop ufw fail2ban unzip jq +``` + +Firewall: + +```bash +sudo ufw allow OpenSSH +sudo ufw allow 80 +sudo ufw allow 443 +sudo ufw enable +sudo ufw status +``` + +Let op: Docker kan gepubliceerde containerpoorten buiten gewone `ufw`-verwachtingen om bereikbaar maken. Publiceer straks alleen reverse proxy poorten naar buiten. + +## Fase 6 — Docker Engine Installeren + +Gebruik Docker Engine native op Ubuntu. Geen Docker Desktop. + +```bash +sudo apt update +sudo apt install -y ca-certificates curl +sudo install -m 0755 -d /etc/apt/keyrings +sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc +``` + +```bash +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +``` + +```bash +sudo apt update +sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin +sudo usermod -aG docker $USER +sudo reboot +``` + +Na reboot: + +```bash +docker run hello-world +docker compose version +``` + +## Fase 7 — Scrum4Me Directories + +```bash +sudo mkdir -p /srv/scrum4me/{postgres,repos,worker-cache,worker-logs,worker-state,backups,compose,caddy} +sudo chown -R $USER:$USER /srv/scrum4me +``` + +Doelstructuur: + +```text +/srv/scrum4me/postgres database data +/srv/scrum4me/repos cloned GitHub repos / mirrors +/srv/scrum4me/worker-cache npm/git/cache +/srv/scrum4me/worker-logs worker logs +/srv/scrum4me/worker-state worker state +/srv/scrum4me/backups local backup staging +/srv/scrum4me/compose docker compose files +/srv/scrum4me/caddy reverse proxy config +``` + +## Fase 8 — Services + +Einddoel: + +```text +postgres +scrum4me-web +worker-idea +worker-implementation +worker-orchestrator +caddy +``` + +Aanbevolen resource limits voor 32 GB RAM: + +| Service | CPU limit | Memory limit | Opmerking | +|---|---:|---:|---| +| `postgres` | 2 CPU | 3-4 GB | Lokale DB | +| `scrum4me-web` | 2 CPU | 2-3 GB | Next.js runtime | +| `worker-idea` | 2 CPU | 4 GB | Grill, plan, chat | +| `worker-implementation` | 4-5 CPU | 10-12 GB | Zwaarste worker | +| `worker-orchestrator` | 2-3 CPU | 5-6 GB | PR review, CI triage, conflicts | +| `caddy` of `nginx` | 0.25 CPU | 256 MB | Reverse proxy | + +Laat 5-7 GB vrij voor Ubuntu, Docker overhead, filesystem cache en pieken. + +## Fase 9 — Tokens en Secrets + +Maak aparte tokens per rol: + +```text +SCRUM4ME_TOKEN_IDEA +SCRUM4ME_TOKEN_IMPLEMENTATION +SCRUM4ME_TOKEN_ORCHESTRATOR + +GH_TOKEN_IDEA +GH_TOKEN_IMPLEMENTATION +GH_TOKEN_ORCHESTRATOR + +CLAUDE_CODE_OAUTH_TOKEN_IDEA +CLAUDE_CODE_OAUTH_TOKEN_IMPLEMENTATION +CLAUDE_CODE_OAUTH_TOKEN_ORCHESTRATOR +``` + +Tokenbeleid: + +| Token | Rechten | +|---|---| +| Idea | Read-only waar mogelijk, markdown/status updates via Scrum4Me | +| Implementation | GitHub contents RW + pull requests RW | +| Orchestrator | PR RW, contents RW alleen voor conflict/repair | + +Bestandsrechten: + +```bash +chmod 600 /srv/scrum4me/compose/*.env +``` + +Geen secrets in git. + +## Fase 10 — Backups + +Minimum: + +```text +Elke nacht pg_dump +Elke nacht backup van /srv/scrum4me/compose +Offsite kopie naar cloud, NAS of externe disk +Backup restore maandelijks testen +``` + +Lokale backup alleen is onvoldoende. Als de SSD stuk gaat, is alles weg. + +## Fase 11 — Monitoring + +Simpel beginnen: + +```bash +docker ps +docker stats +htop +iotop +df -h +du -sh /srv/scrum4me/* +journalctl -u docker --no-pager -n 100 +``` + +Dagelijkse health checklist: + +```text +Docker containers up? +Disk < 80% vol? +Backups gelukt? +Workers online? +Postgres bereikbaar? +Webserver bereikbaar via HTTPS? +Geen runaway logs? +``` + +## Fase 12 — Uitrolvolgorde + +Niet alles tegelijk. + +1. Ubuntu werkt. +2. SSH werkt. +3. Docker werkt. +4. Caddy/nginx testpagina werkt. +5. Postgres container werkt. +6. Scrum4Me webserver werkt lokaal. +7. Scrum4Me webserver werkt via HTTPS. +8. Eén worker werkt: `worker-idea`. +9. Tweede worker werkt: `worker-implementation`. +10. Derde worker werkt: `worker-orchestrator`. +11. Role-aware queue claiming aanzetten. +12. Backups testen. + +## Eerste Smoke Test + +Na installatie: + +```bash +hostnamectl +free -h +df -h +lscpu +docker version +docker compose version +docker run hello-world +lspci -k | grep -EA3 'VGA|3D|Display' +lsmod | grep i915 +``` + +Verwacht: + +```text +Ubuntu 24.04 LTS +~32 GB RAM zichtbaar +Docker werkt +1 TB disk verdeeld zoals gepland +i915 zichtbaar voor Intel integrated graphics +``` + +## Foutscenario's + +### Geen beeld na Ubuntu install + +Eerst: + +```text +Gebruik HDMI-poort 1 +Gebruik andere kabel +Boot recovery mode +Probeer tijdelijk Secure Boot uit +``` + +Niet meteen drivers installeren. + +### Windows start direct, geen Ubuntu menu + +BIOS boot order aanpassen: + +```text +Ubuntu boven Windows Boot Manager +``` + +### Docker permission denied + +```bash +groups +``` + +Als `docker` ontbreekt: + +```bash +sudo usermod -aG docker $USER +sudo reboot +``` + +### Server wordt traag + +Check: + +```bash +docker stats +free -h +htop +iotop +``` + +Eerste maatregel: + +```text +implementation-worker alleen laten draaien +orchestrator zware builds verbieden +worker memory limits verlagen +``` + +## Bronnen + +- Ubuntu Server requirements: +- Ubuntu Server install docs: +- Intel Linux graphics guidance: +- Docker Engine on Ubuntu: +- Next.js self-hosting: diff --git a/docs/recommendations/claude-vm-job-flow-git-strategy.md b/docs/recommendations/claude-vm-job-flow-git-strategy.md new file mode 100644 index 0000000..a87994e --- /dev/null +++ b/docs/recommendations/claude-vm-job-flow-git-strategy.md @@ -0,0 +1,213 @@ +--- +title: "Aanbeveling — Claude VM jobflow en gitstrategie" +status: draft +audience: [product-owner, maintainer, ai-agent] +language: nl +last_updated: 2026-05-09 +--- + +# Aanbeveling — Claude VM jobflow en gitstrategie + +## Managementsamenvatting + +Scrum4Me heeft inmiddels een duidelijke jobarchitectuur: de app maakt jobs aan, de Docker-runner claimt jobs en start Claude op een VM, en `scrum4me-mcp` bewaakt de lifecycle, worktrees, branches, pushes en PR's. De huidige implementatie is daarmee sterker en veiliger dan een prompt-gestuurde agent die zelf jobs ophaalt, pusht of PR's maakt. + +De belangrijkste aanbeveling is om deze servergestuurde lijn expliciet leidend te maken: + +- Claude implementeert en commit lokaal, maar bepaalt niet de branch-, push- of PR-strategie. +- `scrum4me-mcp` blijft de enige partij die jobs claimt, worktrees koppelt, branches pusht, PR's maakt en auto-merge activeert. +- Productinstellingen bepalen bewust de PR-strategie: `SPRINT` als veilige default, `STORY` voor kleine auto-mergebare changes, `SPRINT_BATCH` alleen voor goed afgebakende single-repo sprints. +- De documentatie en prompts moeten worden bijgewerkt, omdat sommige oudere docs nog een handmatige "niet pushen tot user-test"-flow beschrijven terwijl de actuele VM-flow al automatisch pusht na een succesvolle job. + +## Huidige Flow + +```mermaid +flowchart TD + U["Gebruiker start sprint, idee of plan"] --> A["Scrum4Me app voert preflight uit"] + A --> Q["ClaudeJob wordt QUEUED"] + Q --> S{"Product.pr_strategy"} + + S -->|STORY| J1["TASK_IMPLEMENTATION jobs
branch per story"] + S -->|SPRINT| J2["TASK_IMPLEMENTATION jobs
branch per sprint"] + S -->|SPRINT_BATCH| J3["1 SPRINT_IMPLEMENTATION job
hele sprint"] + Q --> J4["IDEA_GRILL / IDEA_MAKE_PLAN / PLAN_CHAT
geen git PR-flow"] + + J1 --> R["scrum4me-docker runner
quota, claim, payload, claude -p"] + J2 --> R + J3 --> R + J4 --> R + + R --> C["Claude op VM
wijzigt code, commit lokaal, logt, verifieert"] + C --> M["scrum4me-mcp update_job_status
verify gate, push, PR, statuspropagatie, cleanup"] + + M --> P{"PR-strategie"} + P -->|STORY| PS["PR per story
auto-merge na groene checks"] + P -->|SPRINT| PP["draft PR per sprint
ready bij sprint DONE"] + P -->|SPRINT_BATCH| PB["draft PR per sprint
ready na batch DONE"] +``` + +## Rollen en Verantwoordelijkheden + +| Actor | Verantwoordelijkheid | Mag niet doen | +|---|---|---| +| Gebruiker / product owner | Productinstellingen kiezen, sprint starten, review/merge bij sprint-PR's | Impliciete gitregels in prompts laten zweven | +| Scrum4Me app | Preflight, jobcreatie, strategie snapshotten op SprintRun | Zelf VM-werk orkestreren | +| scrum4me-docker | Job claimen, Claude starten, lease vernieuwen bij batchjobs | Zelf branch/PR-beleid bepalen | +| Claude op VM | Implementeren, lokaal committen, logs schrijven, verificatie draaien | Pushen, PR's maken, jobs ophalen | +| scrum4me-mcp | Claimprotocol, worktrees, branchnaam, push, PR, auto-merge, cleanup | Beslissingen overlaten aan losse prompttekst | +| GitHub | Branch protection, status checks, auto-merge, merge queue | Onbeschermde main-merge toestaan | + +## Beslissingspunten + +| Moment | Beslissing | Eigenaar | Advies | +|---|---|---|---| +| Productconfiguratie | `pr_strategy` en `auto_pr` | Gebruiker / product owner | Maak dit expliciet zichtbaar als operationele keuze | +| Sprint-start | Welke jobs worden aangemaakt | Scrum4Me app | Blijf blokkeren op ontbrekende plannen, open vragen en cross-repo batchrisico | +| Jobclaim | Welke job mag draaien | scrum4me-mcp | Houd atomic claim met lease en stale reset centraal | +| Runtime | Model, thinking budget, permissions | Job-config resolver | Snapshot bij enqueue en log de gekozen configuratie | +| Implementatie | Welke codewijziging en commits | Claude | Commit lokaal per logische laag, geen push | +| Verify gate | `EMPTY`, `PARTIAL`, `DIVERGENT` acceptabel? | scrum4me-mcp | Maak gate-regels testbaar en documenteer per jobkind | +| Push | Branch pushen of no-changes | scrum4me-mcp | Push alleen na succesvolle terminale status en geldige verify gate | +| PR | Geen PR, draft PR, ready PR, auto-merge | scrum4me-mcp + GitHub | Gebruik branch protection en required checks als harde randvoorwaarde | +| Failure | Retry, fail, skip, pause, cascade | scrum4me-mcp | Houd foutafhandeling server-side, niet prompt-side | + +## Git- en PR-strategie + +| Strategie | Jobs | Branch | PR | Mergebeleid | Aanbevolen gebruik | +|---|---:|---|---|---|---| +| `STORY` | Een job per taak | `feat/story-` | PR per story | Auto-merge na groene checks | Kleine, onafhankelijke stories met betrouwbare CI | +| `SPRINT` | Een job per taak | `feat/sprint-` | Een draft PR per sprint | Ready bij sprint DONE, menselijke merge | Veilige default voor productwerk | +| `SPRINT_BATCH` | Een job voor hele sprint | `feat/sprint-` | Een draft PR per sprint | Ready na batch DONE, menselijke merge | Single-repo sprint met stabiele scope en contextvoordeel | +| Idee/plan jobs | Een job | Geen normale featurebranch | Geen PR | Alleen status/docs/logs | Ideevorming en planvorming | + +Belangrijk: in de huidige implementatie betekent `auto_pr=false` niet automatisch "niet pushen". MCP pusht nog steeds branches wanneer een job succesvol afrondt en er commits zijn. `auto_pr` bepaalt vooral of daarna automatisch een PR wordt gemaakt. + +## Aanbevolen Default + +Gebruik `SPRINT` als standaardstrategie voor Scrum4Me-productwerk. + +Redenen: + +- Er is maar één PR per sprint, dus review en deployment blijven overzichtelijk. +- Claude kan per taak draaien en falen zonder dat de hele sprintcontext in één lange sessie hoeft te blijven leven. +- De PR blijft draft totdat de sprint klaar is, wat goed past bij menselijke review. +- Het voorkomt dat veel kleine story-PR's automatisch deployments of reviewprocessen starten. + +Gebruik `STORY` alleen wanneer: + +- De repository sterke branch protection heeft. +- Required checks verplicht zijn. +- De story klein genoeg is om automatisch te mergen. +- Auto-merge gewenst is en deploymentkosten acceptabel zijn. + +Gebruik `SPRINT_BATCH` alleen wanneer: + +- Alle taken in dezelfde repository zitten. +- De sprintscope stabiel is. +- Er weinig kans is op tussentijdse gebruikersvragen. +- Contextbehoud belangrijker is dan kleine herstelbare stappen. + +## Concrete Acties + +### 1. Maak MCP de formele orchestrator + +Leg in de docs vast dat `scrum4me-mcp` de enige eigenaar is van: + +- jobclaim en lease; +- worktree-aanmaak; +- branchnamen; +- push; +- PR-creatie; +- auto-merge; +- statuspropagatie; +- cleanup. + +Claude mag alleen lokaal committen en via MCP-tools status/logs/verificatie doorgeven. + +### 2. Trek documentatie gelijk + +Werk minimaal deze stukken bij: + +- `CLAUDE.md`: onderscheid maken tussen handmatige lokale agentflow en VM-jobflow. +- `docs/runbooks/branch-and-commit.md`: verouderde "push pas na user-test"-regel beperken tot handmatige runs. +- `docs/runbooks/auto-pr-flow.md`: expliciet maken dat MCP na `done` pusht en daarna optioneel PR maakt. +- `scrum4me-docker/README.md`: beschrijven dat de runner één job per `claude -p` uitvoert. +- `scrum4me-mcp/README.md`: branchnamen actualiseren naar `feat/story-*` en `feat/sprint-*`. + +### 3. Fix prompt/tool-contracten + +Los deze inconsistenties op: + +- Task prompt: `update_job_status(skipped)` vereist een `error`, niet alleen een `summary`. +- Sprint prompt: `verify_required` wordt gebruikt als enum/gate, niet als boolean. +- Sprint prompt: verduidelijk wanneer een task binnen een batch echt `SKIPPED` mag worden. +- Docker docs: verwijder de oude instructie dat Claude zelf `wait_for_job` blijft aanroepen. + +### 4. Maak de state machine expliciet + +Documenteer en test de lifecycle als state machine: + +```text +QUEUED -> CLAIMED -> RUNNING -> DONE + |-> FAILED + |-> SKIPPED + |-> CANCELLED +``` + +Aanbevolen start: een pure TypeScript transition table met tests. XState is pas nodig als visualisatie, model-based testing of complexere parallelle staten belangrijk worden. + +### 5. Versterk GitHub-randvoorwaarden + +Voor repositories waar `STORY` auto-merge gebruikt: + +- Require status checks before merging. +- Require pull request reviews of CODEOWNERS voor risicovolle paden. +- Disable force pushes op beschermde branches. +- Gebruik `gh pr merge --auto --squash --match-head-commit` of equivalent met head-SHA guard. +- Overweeg merge queue zodra meerdere workers tegelijk PR's kunnen laten landen. + +### 6. Beperk VM-risico + +De VM-runner moet blijven werken met: + +- least-privilege tokens; +- expliciete allowed tools; +- worktrees per job; +- geen secrets in logs; +- geïsoleerde runtime; +- duidelijke retry- en stale-claimregels; +- optioneel netwerkbeleid per repository of jobtype. + +## Governance-Regel + +De centrale regel voor Scrum4Me zou moeten zijn: + +> Claude mag code veranderen en lokaal committen. Alleen Scrum4Me MCP mag bepalen wanneer werk klaar is, welke branch wordt gepusht, of er een PR komt, en of die PR automatisch mag mergen. + +Deze regel voorkomt dat prompts, docs en runtimegedrag uit elkaar gaan lopen. + +## Bronnen en Lokale Referenties + +Lokale referenties: + +- `actions/sprint-runs.ts` — sprint-start, preflight en jobcreatie. +- `components/products/pr-strategy-select.tsx` — productkeuze voor `STORY`, `SPRINT`, `SPRINT_BATCH`. +- `scrum4me-docker/bin/run-one-job.ts` — runner claimt één job en start Claude. +- `scrum4me-mcp/src/tools/wait-for-job.ts` — claimprotocol, worktree en branchresolutie. +- `scrum4me-mcp/src/tools/update-job-status.ts` — verify gate, push, PR, auto-merge, statuspropagatie. +- `scrum4me-mcp/src/git/push.ts` — branch push. +- `scrum4me-mcp/src/git/pr.ts` — GitHub PR-create, PR-ready en auto-merge. + +Externe bronnen: + +- GitHub Docs — Protected branches: +- GitHub Docs — Required status checks: +- GitHub Docs — Auto-merge: +- GitHub CLI — `gh pr merge`: +- GitHub Docs — Merge queue: +- Trunk Based Development — Short-lived branches: +- DORA — Trunk-based development capability: +- Temporal Docs — Durable workflows: +- XState Docs — State machines: +- Anthropic Docs — Claude Code headless mode: +- Anthropic Docs — Secure deployment guidance: