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) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-09 22:54:14 +02:00
parent d292e445d9
commit 0d126695db
6 changed files with 1990 additions and 0 deletions

View file

@ -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 |

View file

@ -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 PRs
- 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
- 816 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 PRs
- 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

View file

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

View file

@ -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<string, BacklogPbi>
storiesById: Record<string, BacklogStory>
tasksById: Record<string, BacklogTask | TaskDetail>
}
relations: {
pbiIds: string[]
storyIdsByPbi: Record<string, string[]>
taskIdsByStory: Record<string, string[]>
}
loading: {
loadedProductId: string | null
loadingProductId: string | null
loadedPbiIds: Record<string, true>
loadedStoryIds: Record<string, true>
loadedTaskIds: Record<string, true>
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<void>
ensurePbiLoaded(pbiId: string, requestId?: string): Promise<void>
ensureStoryLoaded(storyId: string, requestId?: string): Promise<void>
ensureTaskLoaded(taskId: string, requestId?: string): Promise<void>
applyRealtimeEvent(event: ProductRealtimeEvent): void
resyncActiveScopes(reason: ResyncReason): Promise<void>
resyncLoadedScopes(reason: ResyncReason): Promise<void>
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=<id>` 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<void>[] = []
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<string, unknown>): 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.

View file

@ -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: <https://ubuntu.com/server/docs/reference/installation/system-requirements/>
- Ubuntu Server install docs: <https://ubuntu.com/server/docs/how-to/installation/>
- Intel Linux graphics guidance: <https://www.intel.com/content/www/us/en/support/articles/000005520/graphics.html>
- Docker Engine on Ubuntu: <https://docs.docker.com/installation/ubuntulinux/>
- Next.js self-hosting: <https://nextjs.org/docs/app/guides/self-hosting>

View file

@ -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<br/>branch per story"]
S -->|SPRINT| J2["TASK_IMPLEMENTATION jobs<br/>branch per sprint"]
S -->|SPRINT_BATCH| J3["1 SPRINT_IMPLEMENTATION job<br/>hele sprint"]
Q --> J4["IDEA_GRILL / IDEA_MAKE_PLAN / PLAN_CHAT<br/>geen git PR-flow"]
J1 --> R["scrum4me-docker runner<br/>quota, claim, payload, claude -p"]
J2 --> R
J3 --> R
J4 --> R
R --> C["Claude op VM<br/>wijzigt code, commit lokaal, logt, verifieert"]
C --> M["scrum4me-mcp update_job_status<br/>verify gate, push, PR, statuspropagatie, cleanup"]
M --> P{"PR-strategie"}
P -->|STORY| PS["PR per story<br/>auto-merge na groene checks"]
P -->|SPRINT| PP["draft PR per sprint<br/>ready bij sprint DONE"]
P -->|SPRINT_BATCH| PB["draft PR per sprint<br/>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-<story-id>` | PR per story | Auto-merge na groene checks | Kleine, onafhankelijke stories met betrouwbare CI |
| `SPRINT` | Een job per taak | `feat/sprint-<sprint-run-id>` | Een draft PR per sprint | Ready bij sprint DONE, menselijke merge | Veilige default voor productwerk |
| `SPRINT_BATCH` | Een job voor hele sprint | `feat/sprint-<sprint-run-id>` | 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: <https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches>
- GitHub Docs — Required status checks: <https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches#require-status-checks-before-merging>
- GitHub Docs — Auto-merge: <https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-auto-merge>
- GitHub CLI — `gh pr merge`: <https://cli.github.com/manual/gh_pr_merge>
- GitHub Docs — Merge queue: <https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue>
- Trunk Based Development — Short-lived branches: <https://trunkbaseddevelopment.com/short-lived-feature-branches/>
- DORA — Trunk-based development capability: <https://dora.dev/capabilities/trunk-based-development/>
- Temporal Docs — Durable workflows: <https://docs.temporal.io/>
- XState Docs — State machines: <https://stately.ai/docs/state-machines>
- Anthropic Docs — Claude Code headless mode: <https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-headless>
- Anthropic Docs — Secure deployment guidance: <https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-secure-deployment>