feat(PBI-67): IDEA_REVIEW_PLAN — iterative multi-model plan review (#199)
* feat(ideas): upload-plan knop — short-circuit van Make-Plan AI-flow Voegt een 'Upload plan' knop toe in idea-row-actions (verschijnt in zowel list als idea-detail). Klik → file picker → kies .md → server-side parse + opslaan; idea-status springt naar PLAN_READY. Vandaaruit de bestaande 'Maak PBI' knop voor materialize. Server (uploadPlanMdAction): - Toegestaan vanuit DRAFT, GRILLED, PLAN_FAILED, PLAN_READY - DRAFT → skip-grill: status gaat direct naar PLAN_READY - PLAN_READY overschrijft het bestaande plan (consistent met updatePlanMdAction, geen confirmation) - Geblokkeerd in GRILLING/PLANNING (job loopt), PLANNED (al gematerialiseerd) - Parse-failure → 422 + details (NIET opslaan, zodat een onparseerbaar plan nooit in de DB belandt) - Empty / >100k chars → 422 - Schrijft IdeaLog NOTE met from_status + length - Rate-limit + demo-guard + ownership-check via loadOwnedIdea (zelfde patroon als updatePlanMdAction) UI (idea-row-actions.tsx): - Hidden <input type=file accept=".md,.markdown,text/markdown,text/plain"> - FileReader → text → action - Toast bij success + router.refresh() - Blocked-tooltip in andere statussen Tests: 10 nieuwe in __tests__/actions/ideas-crud.test.ts dekkend voor: happy paths (DRAFT/GRILLED/PLAN_READY-overwrite/PLAN_FAILED), blocks (PLANNED/GRILLING), validation (empty/oversized/parse-fail), 404. Full suite groen: 849/849. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add reviews for Bootstrap-wizard plans v3.2 to v3.4 - Review v3.2: Addressed executor model, fire-and-forget issues, and PAT handling. - Review v3.3: Improved transaction handling, stale recovery, and ID generation. - Review v3.4: Finalized GitHub permissions, catalog versioning, and E2E verification queries. - Updated recommendations for each version to enhance implementation readiness. * docs(plans): M8 bootstrap-wizard upload-variant v1.4 — backtick-paden Upload-variant van het volledige technische plan (docs/plans/M8-bootstrap-wizard.md), bedoeld voor de "Upload plan"-functie. Genereert 1 PBI + 4 Stories + 22 Tasks via materializeIdeaPlanAction. v1.4-aanpassingen tov eerdere generatie-iteratie: - Alle bestandspaden in implementation_plan in backticks (path-extractor matchen) - Expliciete "Bestanden:" blok per task vóór de stappen - Alle tasks op verify_required: ALIGNED_OR_PARTIAL (was deels ALIGNED — te strict voor ADR-stubs en multi-file edits) Fixt forward-only: T-963 cancelled_by_self door DIVERGENT verifier-verdict. Re-upload van dit bestand produceert tasks die door verify_task_against_plan als ALIGNED of PARTIAL geclassificeerd kunnen worden. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * PBI-67: Add review-plan support to Idea model and job config - Add plan_review_log and reviewed_at fields to Idea model - Add REVIEWING_PLAN, PLAN_REVIEW_FAILED, PLAN_REVIEWED to IdeaStatus enum - Add IDEA_REVIEW_PLAN to ClaudeJobKind enum - Add IDEA_REVIEW_PLAN config to job-config.ts with model=opus, thinking_budget=6000 - Create migration record for schema changes (applied via db push) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * PBI-67 Phase 2: Add update-idea-plan-reviewed MCP tool - Create src/tools/update-idea-plan-reviewed.ts: saves review-log and transitions idea status to PLAN_REVIEWED - Add PLAN_REVIEW_RESULT to IdeaLogType enum (both repos) - Register tool in src/index.ts - Update Prisma schemas (both repos): add plan_review_log and reviewed_at fields to Idea model - Add REVIEWING_PLAN, PLAN_REVIEW_FAILED, PLAN_REVIEWED to IdeaStatus enum (MCP schema) - Add IDEA_REVIEW_PLAN to ClaudeJobKind enum (MCP schema) - Tool includes transaction safety and convergence metrics logging Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * feat(PBI-67): IDEA_REVIEW_PLAN Phases 3-6 — server actions, UI components, prompt & tests - Phase 3: startReviewPlanJobAction, cancelIdeaJobAction, status transitions (REVIEWING_PLAN / PLAN_REVIEWED / PLAN_REVIEW_FAILED), status colors, job-card/jobs-column filters, idea-list status tabs - Phase 4: review-plan-job.md prompt (multi-model orchestration with codex injection + active plan revision via update_idea_plan_md after each round), runbook, 13 unit tests - Phase 5: ReviewLogViewer component (rounds, convergence, approval, issues), idea-detail integration, proper ReviewLog TypeScript types exported from component - Phase 6.1: wait-for-job discriminator wired (IDEA_REVIEW_PLAN), plan-revision step made mandatory in prompt (was previously optional/missing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b8e22539f6
commit
d84cdf664f
28 changed files with 4387 additions and 30 deletions
607
docs/plans/M8-bootstrap-wizard-upload.md
Normal file
607
docs/plans/M8-bootstrap-wizard-upload.md
Normal file
|
|
@ -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/<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.
|
||||
1170
docs/plans/M8-bootstrap-wizard.md
Normal file
1170
docs/plans/M8-bootstrap-wizard.md
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue