---
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/<ts>_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<ClaudeJobKind, ...> 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:<base64-ciphertext>'
          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: "✓ <username> · 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.
