Actionable roadmap to unlock Boris Cherny Step 2: several agents working
concurrently, each self-verifying and landing its own GitHub PR.
Evidence from live code · 2026-08-05. Deploy stays on Cloudflare Pages
(build.sh); GitHub Actions is verification-only.
findTEMP-webapp is Assisted-capable today (strict TS, ESLint max-warnings 0,
committed GraphQL schema). Parallel is blocked by zero tests, no CI, tracked
secrets, no agent docs, and merge hotspots
(jobs/new 2154 LOC · period_jobs/new 1732 LOC · single
th.json). This page is the backlog to clear those blockers —
including a modern Vitest 4 / Browser Mode / Playwright stack gated on a
Node 22 + Vite 7 upgrade.
yarn verify (typecheck + lint + unit + integration) green on trunk in under ~3 minutes.git worktree checkouts with no port, env, or node_modules collision.build.sh parity, codegen drift, semantic PR title); acceptance is a separate slower job.any / @ts-ignore cannot be introduced — typecheck is trustworthy.SENTRY_AUTH_TOKEN lives in Cloudflare/GitHub only.release has zero release-only commits.S / M / L (1 / 2 / 3) are relative sizing hints for splitting and sequencing. They are not commitments and must not be summed into a delivery date. The team is ~3 engineers with a ~60 pt/month combined ceiling that is mostly product work. At a realistic 20–30% platform allocation (~12–18 pts/month), treat the tiers below as capacity guidance.
Roughly 60–70% of this backlog (unit tests for pure functions, AGENTS.md drafts, .env.example, doc fixes, mechanical extractions, most acceptance authoring once the harness exists) can be done by agents at Step 1. Human-only rows are flagged No (secrets rotation, branch protection, release ritual, Sentry major).
| Finding | Evidence | Why it matters for L2 |
|---|---|---|
| Sentry auth token committed under wrong name | .env* has VITE_SENTRY_AUTH_TOKEN; vite.config.ts:42-53 never passes authToken (plugin reads SENTRY_AUTH_TOKEN) | Secret exposed and sourcemap upload likely silently failing (errorHandler only warns). |
RUN_ENV missing VITE_ prefix |
config.ts:1 · App.tsx:236 · no envPrefix in vite.config | import.meta.env.RUN_ENV almost certainly undefined → Sentry enabled always true. |
| Holiday lists drive pricing and are stale | config.ts:13 HOLIDAY_LIST ends 2024-12-31; SPECIAL ends 2025-04-15 | dateClassifier feeds job wage tiers with no tests. |
isMatchOrDoneHiring always true |
libs.ts:235 — status === 'match' || 'done' | Hiring filters silently wrong; perfect first unit-test regression. |
| 111 remote branches; release diverges from master | master...release = 2/4; Merge branch master into release | Blocks real trunk-based flow; promotion must become FF push. |
| Discord PR workflow written but never landed | origin/feat/add-discord-pr-notification-workflow | First CI artifact already exists — land it in Wave 0/1. |
| Modern Vitest blocked by Vite 4 + Node 18 | vite@4.3.9 · .nvmrc 18.20.2 | Vitest 4 needs Vite ≥6 + Node ≥20; Browser Mode needs Vitest 4. |
Only 18 data-testids; App.tsx side effects on import |
App.tsx:36-91 · scattered testids in job create | Acceptance needs flag to skip SW; testids scoped per flow task. |
Verified: Vitest 3 supports Vite 5/6 but not Vite 4. Vitest 4 requires Vite ≥6 and Node ≥20.
Browser Mode stabilized in Vitest 4; vitest-browser-react requires Vitest ≥4.
This repo is Vite 4.3.9 on Node 18.20.2 — so the modern stack is gated behind an upgrade.
Sequenced in three steps so testing is never blocked behind one giant PR.
| Step | Tier | Change | Notes |
|---|---|---|---|
| 1 | 1 | Node 18→22 LTS · Corepack packageManager · Vite 4→7 · plugin-react 5 · vite-plugin-pwa 1.x · Vitest 4 unit-only |
Ends Yarn Berry/Classic hybrid. Confirm CF Pages Node. PWA install+update on preview is acceptance. |
| 2 | 2 | Browser Mode + vitest-browser-react · ESLint 8→9 flat · drop airbnb-typescript → typescript-eslint v8 strictTypeChecked · Prettier standalone · Sentry 7→10 (human) | Pairs lint migration with type-strictness. Sentry init API changed in App.tsx:36-62. |
| 3 | 3 | Optional: React 19 · MUI 5→7 · Apollo 4 · react-router 7 · codegen 5 | Not required for Level 2. Defer. |
If Vite 7 stalls (especially vite-plugin-pwa injectManifest + src/sw.ts), pin Vitest 1.x on Vite 4 to unblock unit tests immediately, then revisit. Testing must not wait on the upgrade forever.
Three tiers with hard scope rules. Commands: yarn test:unit ·
yarn test:integration · yarn test:acceptance ·
yarn verify = typecheck + lint + unit + integration (fast pair).
Acceptance is a separate slower CI job.
Libs: Vitest 4, environment: 'node'; happy-dom only when a component cannot be broken down.
No network, no providers, frozen clock.
Targets: 14 pure functions in libs.ts, dateClassifier + holiday refresh,
OT wage math trapped in adjust_wage_dialog.tsx:167-170, scheduling constants,
Yup schemas (Thai ID 13, Thai char, phone), token_manager with fake clock,
notificationDataToRouteMap, errorsPairI18n.
Libs: Vitest 4 Browser Mode (Playwright provider) + vitest-browser-react locators + MSW 2.
Why Browser Mode not jsdom: app is dialog/drawer/date-picker heavy (MUI portals); jsdom lies on visibility, focus, pointer.
Why MSW not MockedProvider: gql is inlined in ~40 files; document+variables matching breaks on refactors; MSW matches by operation name.
Targets: renderWithProviders (8 providers in App.tsx:243-264), 7-guard chain,
jobs/new pricing+credit+confirm+idempotency, period_jobs/new, job_hirings_card_2 cache writes,
profile/restaurant forms + upload, credit checkout.
Libs: Playwright @playwright/test + webServer: vite preview.
Mock backend: graphql-yoga + @graphql-tools/mock seeded from committed
schema.graphql (2795 lines) + REST for /users/tokens/, upload, fast-track.
.env.acceptance points API URLs at the mock.
Repo-specific: seed auth with unsigned JWT in localStorage.findtempWeb_accessToken
(jwt-decode does not verify signatures) + per-role storageState;
disable SW registration under a test flag (App.tsx:85-91) or PWA cache leaks across tests.
Flow order: fast-track (no auth) → sign-in through guards → create job → hiring lifecycle → onboarding → credit.
data-testid backfill is scoped inside each flow task (only 18 exist today).
MSW's browser integration installs its own service worker at scope / and collides with this app's PWA worker.
MSW is safe in Browser Mode integration tests (components render directly; App.tsx registration never runs).
Acceptance uses the separate yoga mock server instead.
Explicitly not doing: snapshot tests (5× dayjs() in render), testing MUI internals, Jest, Cypress, Enzyme.
Stay inside what GitHub enforces natively — no custom git guards. Agents get fewer ways to get git wrong.
| Setting | Value | Why |
|---|---|---|
| Allowed merge method | Squash only (disable merge commit + rebase merge) | Linear trunk; one commit per PR |
| Default squash message | Pull request title and description | Quality control lives on the PR title |
| Require linear history | On | Correct with squash (every merge is one commit) |
| Required checks | Strict / up-to-date + reviews · no force-push | Parallel agents cannot land on stale bases |
| Auto-delete head branch | On + suggest updating PRs | Cuts branch sprawl |
| PR title format | Conventional: feat(scope): subject |
Becomes squash subject — formalizes existing (#257) style |
release promotion |
git push origin master:release (fast-forward) |
Zero release-only commits; version bumps move to master |
| Branch age | p95 < 3 days | Load-bearing with squash (bisect/partial revert are coarser) |
Intermediate commits are flattened, so agents can use GitHub's "Update branch" button freely without polluting trunk. Messy agent commit history stops mattering. No commitlint on individual commits — only semantic PR titles.
Squash makes git bisect coarser and partial reverts harder on large PRs.
Mitigation: 3-day branch cap + per-task size fences so one squashed commit stays a sensible unit.
The definition of a task good enough to hand to an AI — the 10-field spine, the four task types (story bug tech ops), the extra fields each type requires, and the role responsible for filling in every field — now lives on its own page.
It was split out because it is process work, not engineering work: it needs no code change, no CI job and no toolchain upgrade, so it can be adopted this sprint independently of everything else on this roadmap.
Includes the seven owner roles (PO, Design, Lead, Dev, Grooming, Ops owner, Reporter), the intake flow and its responsibilities, sizing and stop-and-split triggers, a smell list, worked before/after examples for each type, and copy-paste tracker templates.
Ten categories in dependency order. Size = S/M/L indicative. Tier = 1/2/3. Agent? = executable at Step 1 today. Checkboxes persist in this browser (localStorage). Click a task ID/title to fetch its agent-ready markdown from /webapp-tasks/.
Shared plumbing + unit + integration + acceptance. Unit first; acceptance after harness.
| Done | ID | Task | Size | Tier | Wave | Agent? | Evidence | Acceptance |
|---|---|---|---|---|---|---|---|---|
| S | 1 | 0 | Yes | package.json | yarn typecheck, yarn format:check, yarn verify exist; verify = typecheck+lint+test:unit | |||
| M | 1 | 0 | Partial | place_autocomplete_input.tsx:38 · gtm.ts:1 | yarn tsc exits 0 on a clean install | |||
| M | 1 | 0 | Yes | vite.config.ts · new vitest.config.ts | yarn test:unit runs; env stubs cover VITE_* + RUN_ENV | |||
| M | 2 | 1 | Yes | src/graphql/graphql.ts | factories for Job/JobHiring/Restaurant/User; no any | |||
| S | 1 | 1 | Yes | new src/test/clock.ts | documented helper; used by date/token tests | |||
| S | 2 | 1 | Yes | new coverage-floor.json | CI fails if coverage drops below floor | |||
| L | 1 | 0 | Partial | src/App.tsx:36-91 | Sentry/Firebase/GTM/SW behind bootstrap(); providers importable without side effects |
| Done | ID | Task | Size | Tier | Wave | Agent? | Evidence | Acceptance |
|---|---|---|---|---|---|---|---|---|
| M | 1 | 1 | Yes | libs.ts:127 · config.ts:13 | tests cover weekday/weekend/holiday/special; 2026 holidays present | |||
| S | 1 | 1 | Yes | libs.ts:235 | status === 'match' || status === 'done'; failing test first | |||
| S | 1 | 1 | Yes | libs.ts:20-59 | edge cases: midnight, zero duration, locale th | |||
| S | 1 | 1 | Yes | libs.ts:154-198 | all known push types map to expected routes | |||
| S | 1 | 1 | Yes | libs.ts | table-driven cases for empty/null/mixed shapes | |||
| S | 1 | 1 | Yes | libs.ts:233-250 | composition tests; calJobsPositionCountMap | |||
| M | 1 | 1 | Yes | adjust_wage_dialog.tsx:167-170 | pure calcOvertimeWage(regular, ot, wage); 1.5× OT covered | |||
| M | 2 | 1 | Yes | adjust_wage_dialog.tsx:132-138 | urgent vs normal × day-type tiers | |||
| S | 2 | 1 | Yes | fast_track/page.tsx:20-22 · period_jobs/new:98-100 | min hours/days/max length enforced in pure validators | |||
| M | 2 | 1 | Yes | profile/edit.tsx · register.tsx · fast_track | valid/invalid fixtures; flag US-centric phone as known issue | |||
| M | 2 | 1 | Partial | token_manager.ts | expiry, refresh race, missing token; localStorage mocked | |||
| S | 1 | 1 | Yes | idempotency.ts · stash.ts | prefix+uuid format; stash round-trip with mock storage | |||
| S | 2 | 1 | Yes | i18n/i18n.ts:70-99 | known error codes map; unknown falls back safely |
| Done | ID | Task | Size | Tier | Wave | Agent? | Evidence | Acceptance |
|---|---|---|---|---|---|---|---|---|
| L | 2 | 1 | Partial | new vitest.integration.config.ts | yarn test:integration boots Chromium; one smoke render | |||
| M | 2 | 1 | Yes | App.tsx:243-264 | helper exports; docs list required wrappers | |||
| L | 2 | 1 | Partial | src/routes/* | login/PDPA/force-password/info-complete/restaurant paths covered | |||
| L | 2 | 2 | Partial | pages/jobs/new/page.tsx | MSW createJob; idempotency header asserted | |||
| L | 2 | 2 | Partial | pages/jobs/period_jobs/new/page.tsx | batch pricing + createJobs mutation | |||
| L | 2 | 2 | Partial | job_hirings_card_2.tsx | start/get-off/review/ban; fragment writes asserted | |||
| L | 2 | 2 | Partial | profile/edit.tsx · restaurants/* | Yup fail paths + upload REST mocked | |||
| M | 2 | 2 | Partial | pages/credit/* | QR + card pending flows with MSW |
| Done | ID | Task | Size | Tier | Wave | Agent? | Evidence | Acceptance |
|---|---|---|---|---|---|---|---|---|
| L | 2 | 2 | Partial | new playwright.config.ts | yarn test:acceptance boots preview on free port | |||
| L | 2 | 2 | Partial | schema.graphql · new test/mock-server | GraphQL + /users/tokens/ + upload + fast-track | |||
| M | 2 | 2 | Yes | token_manager keys | addInitScript writes findtempWeb_accessToken; per-role states | |||
| S | 2 | 2 | Yes | App.tsx:85-91 | no SW register when flag set; documented | |||
| L | 2 | 2 | Partial | pages/fast_track/* | end-to-end submit against mock; data-testid added as needed | |||
| L | 2 | 2 | Partial | auth/sign_in · routes | lands on jobs-page; guard redirects covered | |||
| L | 2 | 2 | Partial | jobs/new | happy path create; preview-equivalent | |||
| L | 3 | 3 | Partial | job_hirings | start → get-off → review | |||
| L | 3 | 3 | Partial | routes guards | first-login path completes | |||
| L | 3 | 3 | Partial | pages/credit | QR or card path against mock |
Node 22 · Vite 7 · Corepack · Vitest 4. Step 3 (React 19 etc.) is optional and not required for Level 2.
| Done | ID | Task | Size | Tier | Wave | Agent? | Evidence | Acceptance |
|---|---|---|---|---|---|---|---|---|
| M | 1 | 0 | Partial | .nvmrc · package.json | engines.node set; yarn install + lint on Node 22 | |||
| S | 1 | 0 | No | CF Pages settings / env | preview deploy builds on Node 22 | |||
| M | 1 | 0 | Yes | .yarnrc.yml · yarn.lock · .yarn/ | single Yarn path; install-state consistent | |||
| L | 1 | 0 | Partial | vite.config.ts · sw.ts | yarn build:staging green; PWA install+update on preview | |||
| M | 1 | 0 | Yes | vitest.config.ts | yarn test:unit green after B04 | |||
| S | 1 | 0 | Yes | docs note | documented contingency; unblocks A* unit | |||
| M | 2 | 1 | Yes | @vitest/browser · vitest-browser-react | yarn test:integration starts | |||
| L | 2 | 1 | Partial | .eslintrc.cjs → eslint.config.js | lint green; prettier --check separate; no eslint-plugin-prettier | |||
| L | 2 | 2 | No | App.tsx:36-62 | init API migrated; sourcemaps still upload | |||
| L | 3 | 3 | No | package.json | NOT required for L2; tracked as Level 3 on-ramp |
any is how an agent silently defeats typecheck — ratchet, don't big-bang.
| Done | ID | Task | Size | Tier | Wave | Agent? | Evidence | Acceptance |
|---|---|---|---|---|---|---|---|---|
| S | 1 | 0 | Yes | src/** | committed counts JSON with current totals | |||
| S | 1 | 1 | Yes | new scripts/type-ratchet.ts | CI fails on increase; decrease allowed | |||
| M | 1 | 1 | Partial | eslint config | no new any; existing tracked by ratchet | |||
| S | 1 | 1 | Yes | eslint config | @ts-ignore banned; descriptions required | |||
| M | 1 | 1 | Partial | tsconfig.json | tsc green with flag on | |||
| S | 2 | 1 | Yes | eslint config | ! assertions banned or ratchet-tracked | |||
| M | 1 | 0 | Yes | src/vite-env.d.ts · config.ts:1 | RUN_ENV/VITE_* typed; fix RUN_ENV prefix bug | |||
| S | 2 | 2 | Yes | eslint / AGENTS.md | documented; lint or review checklist | |||
| L | 3 | 3 | Partial | typescript-eslint strictTypeChecked | land dir-by-dir; gtm untyped any caught | |||
| L | 3 | 3 | Partial | tsconfig | would catch HOLIDAY_LIST lookups; gradual |
Verification-only GitHub Actions. Deploy stays on Cloudflare Pages via build.sh.
| Done | ID | Task | Size | Tier | Wave | Agent? | Evidence | Acceptance |
|---|---|---|---|---|---|---|---|---|
| M | 1 | 0 | Yes | new .github/workflows/pr.yml | PR runs typecheck+lint+unit on push | |||
| M | 2 | 2 | Yes | pr.yml or acceptance.yml | separate job; browsers cached | |||
| M | 1 | 1 | Yes | build.sh | CI runs staging-mode build; no secret required | |||
| S | 1 | 1 | Yes | src/graphql/* | fails if generated files dirty | |||
| S | 1 | 1 | Yes | C02 script | wired as required check | |||
| S | 1 | 1 | Yes | PR title | feat|fix|chore|docs(scope): subject | |||
| S | 1 | 1 | Yes | origin/feat/add-discord-pr-notification-workflow | workflow on master; Discord fires | |||
| S | 1 | 1 | Yes | .github/PULL_REQUEST_TEMPLATE.md | required sections present | |||
| S | 2 | 2 | No | new CODEOWNERS | graphql/i18n/routes/App require review | |||
| S | 2 | 2 | Partial | PR comment bot or CF integration | preview link visible | |||
| M | 1 | 1 | No | GitHub settings | human; docs the required check names | |||
| S | 1 | 1 | No | GitHub settings | only squash enabled; linear history on | |||
| S | 1 | 0 | Yes | pr.yml | stale runs cancelled |
Docs an agent can trust. Fix README drift first.
| Done | ID | Task | Size | Tier | Wave | Agent? | Evidence | Acceptance |
|---|---|---|---|---|---|---|---|---|
| M | 1 | 0 | Yes | new AGENTS.md | agent can bootstrap without human questions | |||
| M | 1 | 1 | Yes | src/*/AGENTS.md | 4 files; point to shared hotspots | |||
| S | 1 | 0 | Yes | README.md | gql from src/graphql/gql.ts; types from graphql.ts | |||
| S | 1 | 1 | Yes | src/routes/AGENTS.md or docs | table of guards + redirect targets | |||
| S | 1 | 1 | Yes | AGENTS.md | rule + examples from existing 18 ids | |||
| S | 1 | 1 | Yes | AGENTS.md | unit vs integration vs acceptance guidance | |||
| S | 1 | 1 | Yes | new CONTRIBUTING.md | links; 3-day branch age; squash |
VITE_* are client-public by design. Urgent: VITE_SENTRY_AUTH_TOKEN. Do not rewrite git history.
| Done | ID | Task | Size | Tier | Wave | Agent? | Evidence | Acceptance |
|---|---|---|---|---|---|---|---|---|
| S | 1 | 0 | Yes | build output | report yes/no with evidence | |||
| S | 1 | 0 | No | Sentry org | old token revoked; new in CF/GitHub only | |||
| M | 1 | 0 | Partial | vite.config.ts:42-53 | authToken passed; upload works without VITE_ prefix | |||
| M | 1 | 0 | Partial | .env · .env.staging · .env.production | git rm --cached; .gitignore; CF vars set | |||
| S | 1 | 0 | Yes | new files | keys only; no secrets; documented | |||
| S | 2 | 1 | No | GCP console | HTTP referrer allowlist | |||
| S | 1 | 0 | No | GitHub settings | enabled on repo |
Three worktrees, no collisions.
| Done | ID | Task | Size | Tier | Wave | Agent? | Evidence | Acceptance |
|---|---|---|---|---|---|---|---|---|
| S | 1 | 1 | Yes | AGENTS.md / docs/worktrees.md | copy-paste recipe; no collisions | |||
| S | 1 | 1 | Yes | docs | PORT/env convention documented | |||
| S | 1 | 1 | Yes | packageManager | identical Yarn across trees | |||
| S | 1 | 1 | Yes | docs | YARN_CACHE_FOLDER or similar | |||
| S | 2 | 2 | Yes | docs | PLAYWRIGHT_BROWSERS_PATH | |||
| M | 1 | 1 | Partial | manual/script | documented run; no port/env clash |
Cheap wins before god-file splits. Unit extractions in A already help.
| Done | ID | Task | Size | Tier | Wave | Agent? | Evidence | Acceptance |
|---|---|---|---|---|---|---|---|---|
| S | 1 | 1 | Yes | .gitattributes | gql.ts/graphql.ts marked | |||
| S | 1 | 1 | Yes | docs | rerere enabled locally; noted in AGENTS | |||
| S | 1 | 1 | Yes | job_hirings_card.tsx | removed; only Card2 remains | |||
| M | 2 | 2 | Yes | jobs/new · period_jobs/new | schemas importable; unit-tested | |||
| M | 2 | 2 | Yes | jobs/new · period_jobs/new | ops in .ts modules; codegen still works | |||
| S | 2 | 2 | Yes | eslint | threshold documented; god files grandfathered | |||
| L | 2 | 2 | Partial | i18n/th.json | fewer merge conflicts; load still works | |||
| L | 2 | 2 | Partial | root_router.tsx | lazy routes; smaller conflict surface | |||
| M | 2 | 2 | Yes | components/index.ts | AGENTS: no new barrel exports; migrate gradually | |||
| L | 3 | 3 | Partial | jobs/new/page.tsx 2154 LOC | ≤400 LOC files; behavior preserved | |||
| L | 3 | 3 | Partial | period_jobs/new 1732 LOC | shared pricing/validation; duplication cut | |||
| L | 3 | 3 | Partial | job_hirings_card_2.tsx 875 LOC | actions extracted; tests from A26 | |||
| S | 1 | 1 | Yes | AGENTS.md | never hand-merge generated files | |||
| S | 1 | 1 | Yes | docs/hotspots.md | referenced by task scope fences |
Native GitHub enforcement. No custom merge-commit guards.
| Done | ID | Task | Size | Tier | Wave | Agent? | Evidence | Acceptance |
|---|---|---|---|---|---|---|---|---|
| S | 1 | 1 | No | GitHub settings | only squash allowed | |||
| S | 1 | 1 | No | GitHub settings | configured | |||
| M | 1 | 1 | No | GitHub settings | matches D11/D12; documented | |||
| S | 1 | 1 | No | GitHub settings | enabled | |||
| S | 1 | 1 | Yes | D06 | title format enforced | |||
| S | 1 | 1 | Yes | CONTRIBUTING.md | p95 target stated | |||
| M | 2 | 2 | Yes | new workflow | bot comments; no auto-close | |||
| M | 1 | 1 | No | release ritual | zero release-only commits; version bumps on master | |||
| L | 1 | 1 | No | in-flight remotes | plan per branch; land or archive | |||
| M | 1 | 1 | Partial | origin/* | script + human confirm; auto-delete on | |||
| M | 2 | 2 | Yes | new src/flags.ts | MODE gating pattern documented | |||
| S | 2 | 2 | Yes | AGENTS.md | how to stack; when to stop |
Definition of a task good enough to hand to an AI — operationalized.
| Done | ID | Task | Size | Tier | Wave | Agent? | Evidence | Acceptance |
|---|---|---|---|---|---|---|---|---|
| M | 1 | 0 | Yes | new docs/agent_tasks.md | 10-field rubric committed | |||
| M | 1 | 0 | Partial | task tracker | template live; required fields | |||
| S | 1 | 1 | No | tracker + calendar | label exists; 30-min weekly slot | |||
| S | 1 | 1 | Yes | D08 | enforced by template | |||
| M | 2 | 2 | Yes | docs/design-tokens.md | colors/spacing map to theme | |||
| S | 1 | 1 | Yes | new .mcp.json | Asana/Linear + Figma/Miro listed | |||
| M | 2 | 2 | Partial | docs/staging-matrix.md | covers 7-guard preconditions | |||
| M | 1 | 1 | Yes | docs/examples/ | isMatchOrDoneHiring + holidays + one UI | |||
| S | 1 | 1 | Yes | AGENTS.md | checklist agents must follow | |||
| M | 3 | 3 | Partial | docs/metrics.md | definitions + how to measure | |||
| S | 1 | 0 | Yes | docs/agent_tasks.md | two rewrites published |
| Deploy CI / replacing Cloudflare Pages | build.sh stays the deploy path |
| Rewriting git history to scrub old .env commits | Rotate tokens instead |
| React 19 / MUI 7 / Apollo 4 / RR7 | Optional Tier 3; not required for L2 |
| Jest / Cypress / Enzyme / snapshot testing | Modern stack only |
| Org seat counts, training, prompt libraries | Harness readiness only |
| Risk | Mitigation |
|---|---|
yarn tsc already red — typecheck gate can block Wave 0 |
A02 clears debt before making the gate required |
| Vite 4→7 + vite-plugin-pwa is the riskiest Tier 1 item | PWA install/update on preview as AC; Vitest 1.x fallback (B06) |
| .nvmrc bump changes Cloudflare Pages Node too | Preview-deploy check (B02) before merging to master |
| Type strictness big-bang is unmergeable on 28k LOC | Ratchet + per-directory for no-unsafe / noUncheckedIndexedAccess |
| airbnb-typescript → flat ESLint changes lint output | Pair with ESLint 9 move so churn happens once |
| Squash coarsens bisect / partial revert | 3-day branch cap + size fences are load-bearing |
| release FF promotion changes ritual | Human-only; release-day rehearsal |
| Sentry token rotation needs org access | Sequence early; coordination-dependent |
Start with Tier 1 rows in Waves 0–1. Filter the backlog, tick Done as you go (saved in this browser), and use Copy checklist report or per-category Copy as Markdown for Asana/Linear. Keep product work flowing — this roadmap is sized for a minority of engineering capacity. Related: findTEMP-webapp audit · org overview.