diff --git a/docs/INDEX.md b/docs/INDEX.md index 9222da8..43063d2 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -2,7 +2,7 @@ # Documentation Index -Auto-generated on 2026-05-13 from front-matter and headings. +Auto-generated on 2026-05-14 from front-matter and headings. ## Architecture Decision Records @@ -43,6 +43,7 @@ Auto-generated on 2026-05-13 from front-matter and headings. | [Plan: model + mode-selectie per ClaudeJob-kind](./plans/job-model-selection.md) | — | — | | [Verbeterplan load/render Product Backlog, Sprint en Solo](./plans/load-render-improvement-plan-2026-05-10.md) | draft | 2026-05-10 | | [M12 — Idea entity + Grill/Plan Claude jobs](./plans/M12-ideas.md) | planned | — | +| [Bootstrap-wizard voor nieuwe Product-repo](./plans/M8-bootstrap-wizard-upload.md) | — | — | | [Plan v3.5 — Bootstrap-wizard voor nieuwe Product-repo (Scrum4Me feature)](./plans/M8-bootstrap-wizard.md) | reviewed | — | | [PBI-80 — Demo-gebruiker mag eigen UI-voorkeuren wijzigen](./plans/PBI-80-demo-prefs.md) | — | — | | [Queue-loop verplaatsen van Claude naar runner](./plans/queue-loop-extraction.md) | — | — | diff --git a/docs/plans/M8-bootstrap-wizard-upload.md b/docs/plans/M8-bootstrap-wizard-upload.md new file mode 100644 index 0000000..4b0f736 --- /dev/null +++ b/docs/plans/M8-bootstrap-wizard-upload.md @@ -0,0 +1,607 @@ +--- +pbi: + title: "Bootstrap-wizard voor nieuwe Product-repo" + description: | + Bij het aanmaken van een nieuw Product wil de user direct een GitHub-repo + bootstrappen volgens canonical conventies (MD3-theme, ADR-systeem, + docs-structuur, tooling). De catalogus van aanvinkbare opties + uitvoer- + recepten leeft in de database (configureerbaar, audit-bar). Uitvoering + gebeurt server-side via een aparte `bootstrap-service` — een deterministic + runtime onder ClaudeJobKind `BOOTSTRAP_REPO`. UX: twee-staps (Product + eerst, wizard later) met Configure → Preview → Run. + Volledig technisch plan: docs/plans/M8-bootstrap-wizard.md (v3.5). + priority: 2 + +stories: + - title: "Sprint 1a — Deterministic-job contracten + drift-CI" + description: | + Leg de fundamentele contracten vast voordat schema/UI/service worden + gebouwd: discriminated-union JobConfig, docker-runner skip-filter, + transactionele status-sync helper, shared bootstrap-actions package + scaffold, en vendor-copy drift-detectie via CI hash-check. + acceptance_criteria: | + - ADR-0009 in docs/adr/ met status accepted + - JobConfig is een discriminated union; BOOTSTRAP_REPO → runtime:'deterministic' + - scrum4me-docker claimt geen BOOTSTRAP_REPO-jobs (skip-filter actief) + - packages/bootstrap-actions/ scaffold bestaat in Scrum4Me-repo + - notify-helper doet post-commit pg_notify (NOTIFY niet in transaction) + - check-bootstrap-actions-hash.sh faalt CI bij drift + priority: 1 + tasks: + - title: "Schrijf ADR-0009 voor bootstrap-wizard architectuur" + description: | + Nygard-template ADR die de architectuur-keuze vastlegt: aparte + bootstrap-service als sibling-directory, deterministic runtime, + PAT-secret-boundary, declarative recipes in DB. + implementation_plan: | + Bestanden: + - `docs/adr/0009-bootstrap-wizard.md` (nieuw) + - `docs/adr/README.md` (update) + - `docs/INDEX.md` (regenereer) + + Stappen: + 1. Maak `docs/adr/0009-bootstrap-wizard.md` op basis van `docs/adr/templates/nygard.md` + 2. Sectie Context: waarom deze feature; verwijs naar `docs/plans/M8-bootstrap-wizard.md` + 3. Sectie Decision: bootstrap-service als sibling; ClaudeJob queue hergebruikt; declarative actions + 4. Sectie Consequences: positive (consistent product-onboarding), negative (extra service om te beheren) + 5. Status: accepted + 6. Update `docs/adr/README.md` met nieuwe ADR-link + 7. Regenereer `docs/INDEX.md` via `npm run docs` + priority: 1 + verify_required: ALIGNED_OR_PARTIAL + + - title: "Implementeer JobConfig discriminated union" + description: | + Vervang het bestaande JobConfig-type door een discriminated union + met `runtime: 'claude' | 'deterministic'`. BOOTSTRAP_REPO returnt + `{ runtime: 'deterministic', executor: 'bootstrap-repo' }` zonder + model/thinking_budget/permission_mode. + implementation_plan: | + Bestanden: + - `lib/job-config.ts` + - `scrum4me-mcp/src/lib/job-config.ts` (gespiegeld) + - `__tests__/lib/job-config.test.ts` (nieuw) + + Stappen: + 1. Refactor `lib/job-config.ts` naar discriminated union (runtime-discriminator) + 2. KIND_DEFAULTS toevoegen: BOOTSTRAP_REPO → deterministic + 3. resolveJobConfig() returnt union; consumers krijgen exhaustive switch + 4. getJobConfigSnapshot() schrijft requested_* als null voor deterministic kinds + 5. Spiegel `scrum4me-mcp/src/lib/job-config.ts` identiek (geen drift) + 6. Tests: BOOTSTRAP_REPO → runtime='deterministic'; alle bestaande kinds → runtime='claude' + priority: 1 + verify_required: ALIGNED_OR_PARTIAL + + - title: "scrum4me-docker skip-filter voor BOOTSTRAP_REPO" + description: | + De docker-runner mag geen BOOTSTRAP_REPO-jobs claimen — die zijn + voor de aparte bootstrap-service. Voeg kind-filter toe aan + tryClaimJob. + implementation_plan: | + Bestanden: + - `scrum4me-docker/bin/run-one-job.ts` + - `scrum4me-docker/README.md` (note over filter) + + Stappen: + 1. Open `scrum4me-docker/bin/run-one-job.ts` + 2. In tryClaimJob SQL: voeg `AND kind <> 'BOOTSTRAP_REPO'` toe aan WHERE + 3. Test: enqueue BOOTSTRAP_REPO-job; verifieer dat docker-runner het overslaat + 4. Update `scrum4me-docker/README.md` met note over kind-filter + priority: 1 + verify_required: ALIGNED_OR_PARTIAL + + - title: "Scaffold packages/bootstrap-actions/ shared package" + description: | + Nieuw package binnen Scrum4Me-repo dat schema + handler-interfaces + bevat. Geen secrets; gedeeld tussen app (dry-run) en service (echte + run via vendor-copy). + implementation_plan: | + Bestanden: + - `packages/bootstrap-actions/package.json` (nieuw) + - `packages/bootstrap-actions/tsconfig.json` (nieuw) + - `packages/bootstrap-actions/src/types.ts` (nieuw) + - `packages/bootstrap-actions/src/schema.ts` (nieuw) + - `packages/bootstrap-actions/src/index.ts` (nieuw) + - `tsconfig.json` (path-alias toevoegen) + + Stappen: + 1. Maak directory `packages/bootstrap-actions/src/` + 2. `packages/bootstrap-actions/package.json` met name "@scrum4me/bootstrap-actions" version 0.1.0 + 3. `packages/bootstrap-actions/tsconfig.json` extending root config + 4. `packages/bootstrap-actions/src/types.ts`: ActionContext, DryRunReport, CatalogSnapshot, RecipeSnapshot interfaces + 5. `packages/bootstrap-actions/src/schema.ts`: skelet ActionSchema (lege discriminated union; uitgewerkt in story 2) + 6. `packages/bootstrap-actions/src/index.ts`: re-exports + 7. `tsconfig.json` path-alias `@scrum4me/bootstrap-actions` → `./packages/bootstrap-actions/src` + 8. Verifieer build: `npm run typecheck` slaagt + priority: 2 + verify_required: ALIGNED_OR_PARTIAL + + - title: "lib/bootstrap/notify.ts post-commit pg_notify helper" + description: | + Helper voor transactionele status-updates met NOTIFY ná commit + (niet IN transaction). Payload-contract: type='claude_job_status', + user_id verplicht, kind, status (lowercase via jobStatusToApi), + bootstrap_run_id. + implementation_plan: | + Bestanden: + - `lib/bootstrap/notify.ts` (nieuw) + - `__tests__/lib/bootstrap/notify.test.ts` (nieuw) + + Stappen: + 1. Maak `lib/bootstrap/notify.ts` + 2. Functie notifyClaudeJobStatus(jobId, userId, status, extra?) die pg_notify('scrum4me_changes', payload) + 3. status wordt door jobStatusToApi() naar lowercase + 4. Wrapper-functie withPostCommitNotify(tx, payload) die NOTIFY ná tx commit doet + 5. Unit-tests in `__tests__/lib/bootstrap/notify.test.ts`: NOTIFY niet aangeroepen bij rollback; wel bij commit; payload-shape klopt + priority: 1 + verify_required: ALIGNED_OR_PARTIAL + + - title: "Schema-hash drift CI-check script" + description: | + Voorkomt drift tussen Scrum4Me/packages/bootstrap-actions en de + vendor-copy in bootstrap-service. Hash-vergelijking faalt CI. + implementation_plan: | + Bestanden: + - `scripts/check-bootstrap-actions-hash.sh` (nieuw) + - `.github/workflows/ci.yml` (CI-job toevoegen) + - `docs/runbooks/bootstrap-wizard.md` (placeholder, uitgewerkt sprint 1d) + + Stappen: + 1. Maak `scripts/check-bootstrap-actions-hash.sh` + 2. Script berekent sha256 over `packages/bootstrap-actions/src/**` + 3. Schrijf hash naar `packages/bootstrap-actions/.schema-hash` bij build + 4. CI-job in `.github/workflows/ci.yml`: vergelijk geschreven hash met bron-hash; faal bij mismatch + 5. Documenteer in `docs/runbooks/bootstrap-wizard.md` + priority: 2 + verify_required: ALIGNED_OR_PARTIAL + + - title: "Sprint 1b — Schema + seed + path-safety" + description: | + Volledige Prisma-modellen voor catalog (Category/Option/Action), + BootstrapRun met side-effect checkpoints, Product/User uitbreidingen, + partial unique index voor "1 active run per product". Plus seed met + alle 6 core categorieën en Zod-validatie per action-kind. + acceptance_criteria: | + - npx prisma migrate dev slaagt + - npm run seed produceert 7 categorieën (6 core SINGLE + 1 addons MULTI) + - Partial unique index "bootstrap_runs_one_active_per_product" bestaat + - Action-Zod schema rejected path-traversal en absolute paden + - Jobs-board (job-card/jobs-column) toont BOOTSTRAP_REPO label + - npm run typecheck groen na enum-uitbreiding + priority: 1 + tasks: + - title: "Prisma-modellen + migration" + description: | + BootstrapCategory/Option/Action/Run + enums (BootstrapSelectionType, + BootstrapActionKind, BootstrapRunStatus, RiskLevel, RoleRequired, + SideEffect) + Product/User uitbreidingen. Snake-case via @@map. + implementation_plan: | + Bestanden: + - `prisma/schema.prisma` + - `prisma/migrations/_bootstrap_wizard/migration.sql` (Prisma genereert + manual append) + - `scrum4me-mcp/prisma/schema.prisma` (gesynced via `sync-schema.sh`) + + Stappen: + 1. Open `prisma/schema.prisma` + 2. Voeg modellen toe: BootstrapCategory, BootstrapOption, BootstrapAction, BootstrapRun met @@map(snake_case) + 3. Enums: BootstrapSelectionType (SINGLE|MULTI), BootstrapActionKind, BootstrapRunStatus (incl. FAILED_NEEDS_CLEANUP), RiskLevel, RoleRequired, SideEffect + 4. Product: voeg repo_owner, repo_slug, template_version, last_bootstrap_run_id velden + @@unique([repo_owner, repo_slug]) + relaties met disjoint names (ProductBootstrapRuns history + ProductLastBootstrapRun pointer) + 5. User: voeg github_pat_encrypted, github_username, github_pat_verified_at, github_pat_scopes (@default([])), github_pat_expires_at, github_orgs velden + 6. ClaudeJob: voeg claimed_by_worker_id en bootstrap_run relation. ClaudeJobKind enum: BOOTSTRAP_REPO erbij + 7. BootstrapRun met @unique claude_job_id, github_repo_created_at/id/full_name, push_completed_at, recipe_hash, catalog_version, action_schema_version, dry_run_report + 8. Indexes: bootstrap_runs (product_id, status), (user_id, created_at), (status, finished_at) + 9. `npx prisma migrate dev --name bootstrap_wizard` + 10. Append raw SQL aan migration: `CREATE UNIQUE INDEX bootstrap_runs_one_active_per_product ON bootstrap_runs (product_id) WHERE status IN ('PENDING','RUNNING')` + 11. Sync schema naar `scrum4me-mcp/prisma/schema.prisma` via `sync-schema.sh` + priority: 1 + verify_required: ALIGNED_OR_PARTIAL + + - title: "Action-handlers + Zod-schema in shared package" + description: | + Per BootstrapActionKind een handler-functie + Zod-validatie. + Path-safety regels (deny .git, absolute paths, traversal); run-level + caps (200 acties, 256KiB log). + implementation_plan: | + Bestanden: + - `packages/bootstrap-actions/src/schema.ts` (uitbreiden) + - `packages/bootstrap-actions/src/handlers/copy-file.ts` + - `packages/bootstrap-actions/src/handlers/write-file.ts` + - `packages/bootstrap-actions/src/handlers/append-to-file.ts` + - `packages/bootstrap-actions/src/handlers/replace-string.ts` + - `packages/bootstrap-actions/src/handlers/create-adr-stub.ts` + - `packages/bootstrap-actions/src/handlers/add-dependency.ts` + - `packages/bootstrap-actions/src/recipe-hash.ts` + - `packages/bootstrap-actions/src/catalog-hash.ts` + - `packages/bootstrap-actions/src/__tests__/*.test.ts` + + Stappen: + 1. `packages/bootstrap-actions/src/schema.ts`: discriminated union met SafeRelPath validator + 2. SafeRelPath: max 256, regex [A-Za-z0-9_./-], deny absolute/'..'/'.git' + 3. Handlers: COPY_FILE, WRITE_FILE, APPEND_TO_FILE, REPLACE_STRING, CREATE_ADR_STUB, ADD_DEPENDENCY (regex docs MVP-beperking: alleen exact/range semver) + 4. RUN_BASH_TEMPLATE met allowlist (commented out in MVP — opt-in via fase-2) + 5. `packages/bootstrap-actions/src/recipe-hash.ts`: canonicalize() + sha256 + 6. `packages/bootstrap-actions/src/catalog-hash.ts`: canonical JSON over categories+options+actions, sha256 + 7. Run-level caps in runner-helper: maxActions=200, maxOutputLog=256KiB + 8. Tests per handler: idempotent, path-safety negative cases, hash determinisme + priority: 1 + verify_required: ALIGNED_OR_PARTIAL + + - title: "Seed bootstrap catalog (6 core + addons)" + description: | + prisma/seed.ts uitbreiden met seedBootstrapCatalog() die alle + categorieën + opties + acties insert. Idempotent (upsert op slug). + implementation_plan: | + Bestanden: + - `prisma/seed.ts` + + Stappen: + 1. Open `prisma/seed.ts`; voeg seedBootstrapCatalog() toe + 2. Categorieën (SINGLE/required): deploy, auth, database, ui-components, state-management, testing + 3. Categorie (MULTI/optional): addons + 4. Per categorie 2-4 opties met is_default-flag + 5. Per optie de bijbehorende acties (COPY_FILE/CREATE_ADR_STUB/ADD_DEPENDENCY/WRITE_FILE) + 6. Elke verplichte categorie genereert 1 CREATE_ADR_STUB action met number 1-6 + 7. Run `npm run seed`; verifieer 7 categorieën via psql + priority: 1 + verify_required: ALIGNED_OR_PARTIAL + + - title: "Jobs-board BOOTSTRAP_REPO kind-uitbreidingen" + description: | + Alle Record en exhaustive switches updaten; + BOOTSTRAP_REPO krijgt label/kleur/SSE-filter-set. + implementation_plan: | + Bestanden: + - `components/jobs/job-card.tsx` + - `components/jobs/jobs-column.tsx` + - `lib/insights/agent-throughput.ts` + - `app/api/realtime/jobs/route.ts` + + Stappen: + 1. `components/jobs/job-card.tsx`: voeg label-mapping BOOTSTRAP_REPO → 'Bootstrap repo' + 2. `components/jobs/jobs-column.tsx`: voeg kolom-titel + filter + 3. `lib/insights/agent-throughput.ts`: BOOTSTRAP_REPO opnemen in kind-aggregatie (nullable cost ok) + 4. `app/api/realtime/jobs/route.ts`: voeg kind toe aan initial-payload + filter + 5. JobPayload-type uitbreiding: bootstrap_run_id?: string (additive extension) + 6. `npm run typecheck` — alle exhaustive switches groen + priority: 2 + verify_required: ALIGNED_OR_PARTIAL + + - title: "Sprint 1c — PAT-settings + Dry-run + Wizard config/preview" + description: | + User kan classic PAT plakken in settings; preview-action draait + non-mutating handlers in tmpdir + Octokit-preflight; wizard heeft + Configure-stap (radio/checkbox) en Preview-stap (DryRunReport). + acceptance_criteria: | + - GitHub PAT plakken in settings → "Test" toont username + scopes + - PAT staat encrypted in DB (niet in plaintext) + - Preview-stap toont gefilterde file-tree (cap 500), action-log, warnings + - Geen DB-row in bootstrap_runs tijdens preview + - Wizard accepteert geen submit zonder geslaagde preview + priority: 1 + tasks: + - title: "lib/crypto/pat.ts AES-256-GCM encryption" + description: | + Encrypt-only in app-laag (decrypt leeft in bootstrap-service). + Prefix 'v1:' voor toekomstige key-rotation. + implementation_plan: | + Bestanden: + - `lib/crypto/pat.ts` (nieuw) + - `lib/env.ts` (uitbreiden) + - `.env.example` (instructie toevoegen) + - `__tests__/lib/crypto/pat.test.ts` (nieuw) + + Stappen: + 1. Maak `lib/crypto/pat.ts` + 2. Functie encryptPat(plaintext, key) returnt 'v1:' + 3. AES-256-GCM via Node's crypto module; random IV per call + 4. Voeg BOOTSTRAP_ENCRYPTION_KEY (required, min 32) toe aan `lib/env.ts` Zod-schema + 5. Voeg BOOTSTRAP_TEMPLATE_REPO (default 'madhura68/nextjs-baseline') toe + 6. Tests in `__tests__/lib/crypto/pat.test.ts`: encrypt → decrypt round-trip; verschillende ciphertexts bij zelfde plaintext (IV); rejectie bij key < 32 + 7. Update `.env.example` met genereer-instructie (`openssl rand -base64 32`) + priority: 1 + verify_required: ALIGNED_OR_PARTIAL + + - title: "GitHubPatSettings UI + saveGitHubPatAction" + description: | + Settings-page sectie waar user PAT plakt. Test-knop doet Octokit-call + en valideert classic-PAT scope=repo. Toon scopes + verified_at. + implementation_plan: | + Bestanden: + - `app/(app)/settings/_components/github-pat-settings.tsx` (nieuw) + - `actions/bootstrap.ts` (nieuw) + - `__tests__/actions/bootstrap.test.ts` (nieuw) + + Stappen: + 1. Maak `app/(app)/settings/_components/github-pat-settings.tsx` + 2. Form met password-input (gemaskeerd) + Test-knop + Save-knop + 3. UI-copy: "Vereist een classic PAT met 'repo' scope — fine-grained tokens nog niet ondersteund" + 4. Server-action in `actions/bootstrap.ts` → saveGitHubPatAction(token): + - Demo-check (403) + - Octokit.users.getAuthenticated() → username + - Parse x-oauth-scopes header → array + - Reject als scope 'repo' ontbreekt + - encryptPat() → store github_pat_encrypted/username/verified_at/scopes + 5. UI toont na save: "✓ · scopes: repo" + 6. Tests in `__tests__/actions/bootstrap.test.ts`: scope-rejection; encryption-roundtrip; demo-block + priority: 1 + verify_required: ALIGNED_OR_PARTIAL + + - title: "previewBootstrapAction + dry-run executor" + description: | + Server-action die recipe resolved, alle non-mutating handlers in + tmpdir draait, Octokit preflight doet (collision + best-effort + owner-discovery), DryRunReport retourneert. Geen DB-writes. + implementation_plan: | + Bestanden: + - `actions/bootstrap.ts` (previewBootstrapAction toevoegen) + - `lib/bootstrap/recipe.ts` (nieuw) + - `lib/bootstrap/dry-run.ts` (nieuw) + - `__tests__/lib/bootstrap/dry-run.test.ts` (nieuw) + + Stappen: + 1. Voeg previewBootstrapAction(productId, selections, repoOwner, repoSlug) toe aan `actions/bootstrap.ts` + 2. Auth + demo-check + Zod-validate selections + GitHub-name regex + 3. Resolve recipe via `lib/bootstrap/recipe.ts`: selections → BootstrapAction[] (geordend op execution_order) + 4. Compute recipe_hash + catalog_version + 5. Maak `lib/bootstrap/dry-run.ts`: clone template (geen cache MVP), iterate handlers met supports_dry_run=true + 6. Filter file-tree: deny .git/node_modules/.next/dist/build/out/coverage/*.log/.env*/.DS_Store; cap 500 entries met truncated-flag + 7. Octokit preflight: `octokit.repos.get({ owner, repo })` voor collision; `octokit.orgs.get()` voor best-effort owner-status + 8. Return DryRunReport { fileTree, truncated, actionLog, warnings, canProceed, collisions } + 9. Tests in `__tests__/lib/bootstrap/dry-run.test.ts`: collision-detect; path-safety enforcement; report-shape + priority: 1 + verify_required: ALIGNED_OR_PARTIAL + + - title: "BootstrapWizardDialog: Configure + Preview steps" + description: | + Multi-step wizard dialog vanuit product-detail-pagina. Step 1 + radios/checkboxes; Step 2 toont DryRunReport; Step 3 placeholder + voor status (sprint 1d). + implementation_plan: | + Bestanden: + - `app/(app)/products/[id]/_components/bootstrap-wizard-dialog.tsx` (nieuw) + - `app/(app)/products/[id]/_components/repo-owner-picker.tsx` (nieuw) + - `app/(app)/products/[id]/_components/bootstrap-preview-panel.tsx` (nieuw) + - `app/(app)/products/[id]/page.tsx` (Bootstrap-knop toevoegen) + + Stappen: + 1. Maak `app/(app)/products/[id]/_components/bootstrap-wizard-dialog.tsx` + 2. Volg `docs/patterns/dialog.md` Entity Dialog conventie (base-ui render-prop) + 3. Step Configure: render 6 radio-groups + 1 checkbox-array (Add-ons) op basis van catalog-query + 4. `repo-owner-picker.tsx`: user + orgs als opties met hint-badges (zichtbaar/onbekend/policy-blokkeert); GEEN automatisch verbergen + 5. repo_slug input met GitHub-naam-regex + 6. Step Preview: call previewBootstrapAction; toon `bootstrap-preview-panel.tsx` met file-tree, action-log, warnings, collision-banner + 7. Voorkom Run-knop tot canProceed=true + 8. `app/(app)/products/[id]/page.tsx`: Bootstrap-knop (verborgen voor demo-users) + 9. Tests: wizard-state-machine; preview-roundtrip + priority: 2 + verify_required: ALIGNED_OR_PARTIAL + + - title: "Sprint 1d — bootstrap-service + transactionele sync + E2E" + description: | + Sibling-repo bootstrap-service/ als nieuw Node-proces dat + BOOTSTRAP_REPO-jobs claimt, recipe uitvoert via isomorphic-git + (template-clone + commit + push), Octokit createRepo, met side-effect + checkpoints en transactionele status-sync. Plus stale-recovery cron + en realtime SSE-status panel. + acceptance_criteria: | + - bootstrap-service claimt BOOTSTRAP_REPO-jobs binnen 2s na NOTIFY + - E2E: nieuw product → wizard → preview → Run → SUCCEEDED in <60s + - GitHub repo bestaat met .scrum4me/bootstrap.json metadata en 6 ADR-stubs + - claude_jobs.status=DONE, bootstrap_runs.status=SUCCEEDED, product.repo_url gevuld + - Invalid PAT → FAILED zonder orphan repo + - Twee gelijktijdige submits: één gaat door, ander krijgt unique violation + - Stale-recovery cron markeert verlopen leases correct (FAILED vs FAILED_NEEDS_CLEANUP) + - CI-job faalt bij hash-drift van vendor-copy + priority: 1 + tasks: + - title: "Setup bootstrap-service sibling-repo skeleton" + description: | + Nieuwe directory ~/Development/bootstrap-service/ met package.json, + tsconfig, Dockerfile (multi-arch arm64-primary), sync-schema.sh, + sync-bootstrap-actions.sh. + implementation_plan: | + Bestanden (in sibling-repo `~/Development/bootstrap-service/`): + - `package.json` + - `tsconfig.json` + - `env.ts` + - `prisma/schema.prisma` (gesynced) + - `sync-schema.sh` + - `sync-bootstrap-actions.sh` + - `Dockerfile` + - `docker-compose.yml` + - `README.md` + + Plus in Scrum4Me-repo: + - `docs/manual/06-bootstrap-service.md` (nieuw) + + Stappen: + 1. `mkdir ~/Development/bootstrap-service` + 2. `package.json`: deps prisma, @prisma/client, isomorphic-git, @octokit/rest, zod + 3. `tsconfig.json` met strict mode + 4. `env.ts`: Zod-schema voor DATABASE_URL, DIRECT_URL, BOOTSTRAP_ENCRYPTION_KEY, BOOTSTRAP_TEMPLATE_REPO + 5. `prisma/schema.prisma` symlinked of synced via `sync-schema.sh` + 6. `sync-bootstrap-actions.sh` kopieert `packages/bootstrap-actions/` vanuit Scrum4Me met hash-write + 7. `Dockerfile`: FROM --platform=$BUILDPLATFORM node:24-alpine, multi-arch (arm64 + amd64) + 8. `docker-compose.yml`: arm64 default voor Mac dev + 9. `README.md`: setup-instructies + env-template + 10. Voeg `docs/manual/06-bootstrap-service.md` toe in Scrum4Me + priority: 1 + verify_required: ALIGNED_OR_PARTIAL + + - title: "Claim-loop + LISTEN + lease-renewal" + description: | + Daemon-loop in bin/run.ts: LISTEN op scrum4me_changes filter + claude_job_enqueued/BOOTSTRAP_REPO; SKIP-LOCKED claim; + claimed_by_worker_id (hostname-pid-startTs); lease-renewal elke 30s. + implementation_plan: | + Bestanden (sibling-repo `~/Development/bootstrap-service/`): + - `bin/run.ts` (nieuw) + - `src/claim.ts` (nieuw) + - `src/__tests__/claim.test.ts` (nieuw) + + Stappen: + 1. `bin/run.ts`: daemon-loop + 2. WORKER_ID = `${hostname}-${pid}-${startTs}` als string + 3. `src/claim.ts` tryClaimBootstrapJob: UPDATE claude_jobs SET status='CLAIMED', lease_until=NOW()+60s, claimed_at, claimed_by_worker_id WHERE id=(SELECT id FROM claude_jobs WHERE status='QUEUED' AND kind='BOOTSTRAP_REPO' ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 1) RETURNING id + 4. Lease-renewal setInterval(30s) UPDATE lease_until=NOW()+60s WHERE id=? AND claimed_by_worker_id=? (only-mine guard) + 5. LISTEN scrum4me_changes; bij claude_job_enqueued met kind=BOOTSTRAP_REPO → trigger claim-poll + 6. Fallback poll-interval 30s + 7. Tests in `src/__tests__/claim.test.ts`: SKIP-LOCKED safety bij parallel claim; lease-renewal-guard + priority: 1 + verify_required: ALIGNED_OR_PARTIAL + + - title: "Execute-flow: clone + recipe + Octokit + push + checkpoints" + description: | + Volledige bootstrap-uitvoer: isomorphic-git clone (tag-pinned), + recipe-iteratie via shared handlers, placeholder-replacement, + Octokit repo-create, isomorphic-git push (PAT via onAuth-callback), + .scrum4me/bootstrap.json metadata, side-effect checkpoints op DB. + implementation_plan: | + Bestanden (sibling-repo `~/Development/bootstrap-service/`): + - `src/runner.ts` (nieuw) + - `src/github.ts` (nieuw — Octokit wrapper) + - `src/template-clone.ts` (nieuw — isomorphic-git) + - `src/__tests__/runner.test.ts` (nieuw) + + Stappen: + 1. `src/runner.ts`: executeRecipe(run, pat) + 2. mkdtemp(); `src/template-clone.ts` isomorphic-git clone met depth=1 en ref=template_version + 3. Capture template_source_sha via resolveRef HEAD + 4. fs.rm tmpdir/.git; git.init met defaultBranch='main' + 5. Iterate run.recipe_snapshot.actions (sorted by execution_order); ActionSchema.parse runtime + 6. Dispatch per kind → handler uit `@scrum4me/bootstrap-actions` (vendor-copy) + 7. replacePlaceholders(tmpdir) voor __PRODUCT_NAME__/__PRODUCT_SLUG__/__GITHUB_OWNER__ + 8. writeFile `.scrum4me/bootstrap.json` met metadata (template/recipe_hash/catalog_version/etc.) + 9. git.add + git.commit + 10. `src/github.ts` Octokit createForAuthenticatedUser/createInOrg → checkpoint write github_repo_created_at/id/full_name + 11. git.addRemote + git.push met onAuth-callback { username: 'x-access-token', password: pat } → checkpoint write push_completed_at + 12. Cleanup tmpdir in finally; zeroize pat + 13. Tests in `src/__tests__/runner.test.ts`: dry-run-handlers identiek aan service-handlers (geen drift); push-via-onAuth zonder URL-leak + priority: 1 + verify_required: ALIGNED_OR_PARTIAL + + - title: "Transactionele status-sync (running/success/failed)" + description: | + syncRunning/syncSuccess/syncFailed in één prisma.$transaction met + count-checks. Lease_until + claimed_by_worker_id terminal op null. + NOTIFY na commit. FAILED vs FAILED_NEEDS_CLEANUP afhankelijk van + github_repo_full_name. + implementation_plan: | + Bestanden (sibling-repo `~/Development/bootstrap-service/`): + - `src/status-sync.ts` (nieuw) + - `src/__tests__/status-sync.test.ts` (nieuw) + + Stappen: + 1. `src/status-sync.ts` + 2. syncRunning(runId, jobId, userId): één now=new Date(); transaction: bootstrap_runs.started_at = now WHERE status='PENDING'; claude_jobs.started_at = now WHERE status='CLAIMED'; count-check beide; rollback bij mismatch + 3. syncSuccess: transaction met updateMany WHERE status='RUNNING' op zowel run als job; lease_until=null, claimed_by_worker_id=null; product.repo_url + template_version + last_bootstrap_run_id + 4. syncFailed: zelfde pattern; terminal-status = run.github_repo_full_name ? 'FAILED_NEEDS_CLEANUP' : 'FAILED'; bij created repo zonder push: compensating octokit.repos.delete in catch-pad + 5. NOTIFY pas na commit; status via jobStatusToApi(...) lowercase + 6. Tests in `src/__tests__/status-sync.test.ts`: cancel-tijdens-success blijft CANCELLED; lease-cleanup; status-mapping + priority: 1 + verify_required: ALIGNED_OR_PARTIAL + + - title: "Stale-recovery cron + service-startup recovery" + description: | + Verlopen BOOTSTRAP_REPO-leases (lease_until < NOW) splitsen tussen + FAILED en FAILED_NEEDS_CLEANUP op basis van github_repo presence. + Cron-route in app + globale startup-sweep in service. + implementation_plan: | + Bestanden: + - `app/api/cron/bootstrap-stale-recovery/route.ts` (nieuw, in Scrum4Me) + - `vercel.json` (cron-schedule toevoegen) + - `~/Development/bootstrap-service/src/stale-recovery.ts` (nieuw) + - `__tests__/api/cron/bootstrap-stale-recovery.test.ts` (nieuw) + + Stappen: + 1. Maak `app/api/cron/bootstrap-stale-recovery/route.ts` met Bearer-CRON_SECRET guard + 2. SQL stap 1: `UPDATE claude_jobs SET status='FAILED', error='lease expired', lease_until=null, claimed_by_worker_id=null WHERE status IN ('CLAIMED','RUNNING') AND kind='BOOTSTRAP_REPO' AND lease_until < NOW()` + 3. SQL stap 2a: `UPDATE bootstrap_runs → FAILED_NEEDS_CLEANUP WHERE github_repo_full_name IS NOT NULL OR github_repo_created_at IS NOT NULL` + 4. SQL stap 2b: `UPDATE bootstrap_runs → FAILED WHERE github_repo_full_name IS NULL AND github_repo_created_at IS NULL` + 5. Voeg cron-schedule toe in `vercel.json` (elke 5 min) + 6. `~/Development/bootstrap-service/src/stale-recovery.ts`: zelfde SQL bij service-startup (globale recovery — NIET filteren op claimed_by_worker_id) + 7. Tests in `__tests__/api/cron/bootstrap-stale-recovery.test.ts`: split-strategie; kind-filter respecteert bestaande Claude-jobs ongemoeid + priority: 1 + verify_required: ALIGNED_OR_PARTIAL + + - title: "Service-startup logging + drift CI-verificatie" + description: | + Bij service-startup: log action_schema_version, schema-hash van + geladen bootstrap-actions package, en catalog-version. CI faalt + release bij hash-mismatch met Scrum4Me-bron. + implementation_plan: | + Bestanden (sibling-repo `~/Development/bootstrap-service/`): + - `src/telemetry.ts` (nieuw) + - `.github/workflows/release.yml` (nieuw — drift-check) + + Stappen: + 1. `src/telemetry.ts`: bootSummary() print actionSchemaVersion, schemaHash (sha256 over geladen package src), catalogVersion (huidige DB) + 2. Print bij service-startup vóór claim-loop + 3. Telemetry-log gebruikt token-scrubbing helper (geen PAT/secrets in logs) + 4. CI `.github/workflows/release.yml`: run `scripts/check-bootstrap-actions-hash.sh` tegen Scrum4Me-bron-hash (vergelijk via env var of release-tag) + 5. Release-pipeline faalt bij drift + priority: 2 + verify_required: ALIGNED_OR_PARTIAL + + - title: "BootstrapStatusPanel realtime SSE" + description: | + Tijdens RUNNING-fase toont wizard-step 3 realtime status via SSE + op /api/realtime/jobs filtered op bootstrap_run_id. + implementation_plan: | + Bestanden: + - `app/(app)/products/[id]/_components/bootstrap-status-panel.tsx` (nieuw) + + Stappen: + 1. Component `app/(app)/products/[id]/_components/bootstrap-status-panel.tsx` + 2. Subscribe op SSE-stream `/api/realtime/jobs` (bestaand) + 3. Filter payloads op type='claude_job_status' + bootstrap_run_id=runId + 4. Render status-badge (queued/running/done/failed) + progress-hints + 5. Bij DONE: toon repo_url met "Open op GitHub"-link + 6. Bij FAILED/FAILED_NEEDS_CLEANUP: toon error + retry-knop placeholder (fase-2) + 7. Tests: SSE-event-mapping; UI-state-machine + priority: 2 + verify_required: ALIGNED_OR_PARTIAL + + - title: "E2E happy-path verificatie" + description: | + End-to-end test: maak product, run wizard (alle 6 core), preview, + submit, wacht op SUCCEEDED, verifieer GitHub-repo en DB-state. + implementation_plan: | + Bestanden: + - `__tests__/e2e/bootstrap-happy-path.test.ts` (nieuw) + + Stappen: + 1. Maak product 'e2e-bootstrap-test' via UI of seed + 2. Settings: PAT met repo-scope geconfigureerd voor test-user + 3. Wizard: deploy=self-hosted, auth=iron-session, db=postgres-prisma, ui=shadcn-baseui, state=zustand, testing=vitest-jsdom; repo_owner=test-org + 4. Preview-step → groen → Run + 5. Verifieer binnen 60s: bootstrap_runs.status=SUCCEEDED; claude_jobs.status=DONE; product.repo_url niet null; product.template_version='v1.0.0' + 6. Verifieer GitHub: repo bestaat private; `docs/adr/` bevat 0000+0001..0006; `.scrum4me/bootstrap.json` bevat recipe_hash/catalog_version/selected_options + 7. SQL-query met JOIN: `SELECT br.status, br.repo_url, cj.lease_until > NOW() AS lease_active FROM bootstrap_runs br JOIN claude_jobs cj ON cj.id=br.claude_job_id ORDER BY br.started_at DESC NULLS LAST LIMIT 1` + 8. Failure-pad: invalid PAT → FAILED + geen orphan repo + 9. Demo-pad: login als demo → Bootstrap-knop verborgen; direct API → 403 + priority: 1 + verify_required: ALIGNED_OR_PARTIAL + verify_only: false +--- + +# M8 Bootstrap-wizard — Upload variant + +Dit is de upload-variant van het volledige technische plan +`docs/plans/M8-bootstrap-wizard.md` (v3.5). De YAML-frontmatter hierboven +is bedoeld voor de "Upload plan"-functie in Scrum4Me die idea-status naar +`PLAN_READY` brengt en daarna via `materializeIdeaPlanAction` een PBI met +4 Stories en bijbehorende Tasks aanmaakt. + +## Mapping naar het volledige plan + +| Story | Sprint | Volledige plan-sectie | +|---|---|---| +| Story 1 | Sprint 1a — Contracten | "Fasering" Sprint 1a + "Deterministic-job contract" + "Vendor-copy CI-check" | +| Story 2 | Sprint 1b — Schema + seed + safety | "Domein-model (Prisma)" + "Action-schema + path-safety" + "Seed catalog" | +| Story 3 | Sprint 1c — PAT + Dry-run + Wizard | "PAT-secret-boundary" + "Dry-run als feature" + "Wizard-componenten" | +| Story 4 | Sprint 1d — bootstrap-service + E2E | "Executor: bootstrap-service" + "Status-sync" + "Stale-recovery" + "Verificatie" | + +Voor uitgebreide review-historie (5 reviews), architectuur-besluiten, +overwogen alternatieven, secret-boundary-onderbouwing, en open punten: +zie het volledige plan-document.