Files
waggle-os/docs/ux-refactor/2D-BUILD-PLAN.md
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

336 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 2D — Onboarding Chain · Build Plan
> Scope: the day-0 onboarding/first-run chain (PRD §12.12 / DoD #6). Execution model is LOCKED:
> **in-place incremental refactor** of `apps/web` + targeted sidecar extension. PRD acceptance criteria
> win over mockups (PRD §24). Every claim below is grounded in live source under
> `D:/Projects/waggle-os-ux-refactor` (paths cited inline).
>
> **Critical reuse note:** Harvest preview-classification (C33 unreviewed + B2 confidence + B6 kind) was
> already built in **Phase 2B.3** (`harvest-classify.ts`, `harvest.ts` preview/commit, `harvest-kind-map.ts`)
> and the Memory Center "Needs review" review surface was built in **Phase 2B-FE.2** (`MemoryCenterTab.tsx`).
> 2D **reuses** these; it does NOT rebuild classification, confidence, or the review queue.
---
## 1. CURRENT vs TARGET
### Current OnboardingWizard (8 steps, `apps/web/src/components/os/overlays/OnboardingWizard.tsx`)
`STEP_NAMES` (`OnboardingWizard.tsx:35`):
`welcome → why-waggle → tier → memory-import → template → persona → api-key → ready` (steps 07).
| Step | Component | Wired vs hardcoded |
|---|---|---|
| 0 welcome | `WelcomeStep` | Hardcoded brand copy; 3s auto-advance (`:136-141`). |
| 1 why-waggle | `WhyWaggleStep` | Hardcoded `VALUE_PROPS` (`constants.ts:111`); "Skip and set me up" escape hatch (`:271-304`). |
| 2 tier | `TierStep` | `selectedTier``onUpdate({tier})` (UI density, not billing). |
| 3 memory-import | `ImportStep` | **Wired**: `harvestPreview`/`harvestCommit`/`scanClaudeCode` (`:156-202`). Commits inline then `goToStep(4)`. |
| 4 template | `TemplateStep` | Hardcoded 15 `TEMPLATES` (`constants.ts:10`), `TEMPLATE_PERSONA` map. |
| 5 persona | `PersonaStep` | `ALL_ONBOARDING_PERSONAS` (`constants.ts:46`) + custom-persona create. |
| 6 api-key | `ModelTierStep` | Model/key; calls `handleFinish``adapter.createWorkspace` (`:306-354`). |
| 7 ready | `ReadyStep` | Auto-finish after 2s. |
State backbone: `useOnboarding.ts` `OnboardingState` (`:10-19`) = `{ completed, step, tier?, workspaceId?,
apiKeySet?, templateId?, personaId?, tooltipsDismissed? }`, localStorage key `waggle:onboarding`. Resume is
half-built: `state.step` persists; returning-user auto-complete via Tauri flag + `getWorkspaces().length>0`
(`:102-176`). `?forceWizard=true` (DEV) forces step 0.
**There is NO profile/"Who Are You" step, NO Tool Discovery step.** The current IA (tier/api-key/model) is the
OLD onboarding, not the PRD §12.12 flow.
### Target — PRD §12.12 5-step chain (S12S17)
PRD order: **First Launch (S12) → Who Are You (S13) → Tool Discovery (S14) → Memory Import (S15) →
Memory Review (S16) → Workspace Creation (S17) → Home Cockpit**. Per the ratified C33 decision, Memory
**Review (S16) is NOT a blocking onboarding step** — import commits immediately as `status:'unreviewed'`,
and review happens later in the Memory Center "Needs review" filter / J08 queue. So the in-wizard chain is
effectively **5 interactive steps**: First Launch → Who Are You → Tool Discovery → Memory Import →
Workspace Creation, with a Ready/handoff terminal.
Target step mapping (keep the existing wizard shell + AnimatePresence switch; re-key `STEP_NAMES`):
```
0 first-launch (S12) rework WelcomeStep
1 who-are-you (S13) NEW WhoAreYouStep ← B8 profile + identity seed
2 tool-discovery (S14) NEW ToolDiscoveryStep (DEFERRABLE — see §6 note)
3 memory-import (S15) rework ImportStep — commit-as-unreviewed (C33)
4 workspace-create (S17) route through CreateWorkspaceDialog step bodies (or inline)
5 ready keep ReadyStep
```
> S16 Memory Review is delivered by the **already-shipped** Memory Center "Needs review" filter
> (`MemoryCenterTab.tsx:25-31`), NOT a wizard step. This is the C33 ratification (below).
### Explicit gate ratifications (verbatim, from `deltas/open-questions.md`)
- **B8** (`open-questions.md:28, :350`): *"Onboarding writes profile AND seeds the `identity` table."* /
*"Write profile + seed identity in the same onboarding commit. Rationale: prevents the two-store drift…
makes the Phase-1 Home greeting correct."* **[BLOCKS P1 greeting]**
- **C33** — RESOLVED to the middle path (`open-questions.md:45-53`): *"Commit-as-unreviewed, non-blocking
review: onboarding Import commits immediately (memory feels alive on first run), but frames land with
`status:'unreviewed'` + the B2 confidence score; Review is a **non-blocking** curation surface (Memory
Center "needs review" filter + the standing J08 queue), NOT a blocking onboarding step… Supersedes the
original §C C33 'blocking split' recommendation."*
- **B2** (`:39`): *"cheap heuristic at preview (source-trust × adapter-type × dedup), persisted in `metadata`
LLM scoring reserved for the standing J08 queue."*
- **B6** (`:41`): *"PRD §15.2 canonical in `@waggle/shared`; pure harvest + display-category mapping helpers.
Drop FE `event`/`insight` drift."*
- **C28** (`:420`): language selector → *"static disabled `English (US)` chip (no i18n exists…)."*
- **C29** (`:422`): *"drop the auto-advance"* on the first-launch privacy screen.
- **C30** (`:424`): *"keep Work type as a distinct personalization signal; don't re-ask what the template
already implies."*
- **C31** (`:426`): Tool Discovery id-space → *"namespaced scheme (`connector:gmail` / `tool:cursor`)."*
- **C32** (`:428`): *"pre-select from `GET /api/tools/detect` with a visible 'detected' badge."*
- **C34** (`:433`): Hermes/Codex/Cursor tiles → *"render as `upload`/'Other' file pickers or 'coming soon' v1."*
- **C35** (`:435`): Workspace Creation → *"record connector/MCP ids as workspace *intent* on the config; do
NOT run install/OAuth at create time."*
- **C6** (`:373`): `WorkspaceType` = *"`project | client | research | personal` (+`team`/`organization`
reserved); `type` coexists with the free-string `group`."*
---
## 2. B8 — Identity seed ("capture who-you-are + greet by name")
### What already exists
- **Profile store** (`profile.ts`): `UserProfile` (`:41-94`) carries `name/role/company/industry/bio` +
writingStyle/brand/interests/language/timezone. `GET /api/profile` (`:162`) + `PUT /api/profile` (`:167`,
partial-merge allow-list `:172-196`). On save it ALSO mirrors a `User identity: …` P/I frame to personal
memory (`:201-224`). Stored in `profile.json` (file, no SQLite). Adapter: `getProfile`/`updateProfile`
(`adapter.ts:1357-1365`, typed `any`).
- **Identity table** (`identity.ts` route + `hive-mind-core/src/mind/identity.ts` `IdentityLayer`):
single-row per-mind `identity` table (`name/role/department/personality/capabilities/system_prompt`).
`GET /api/identity` (`:62`) + `POST /api/identity` upsert (`:104-156`). `IdentityLayer` exposes
`create/get/exists/update/toContext` (`identity.ts:18-73`). This is what backs the **Home greeting name**
via `adapter.getIdentity()` (`adapter.ts:742`).
- **UserProfileApp** (`apps/web/.../apps/UserProfileApp.tsx`, "My Profile") already renders a "Who Are You?"
Identity tab capturing Name/Role/Company/Industry/Bio with `INDUSTRIES` + chip-toggle patterns —
the **reuse target for the form fields**.
### The GAP to close (B8)
1. **No `setIdentity` adapter method.** `adapter.getIdentity()` exists (`:742`) but there is **no**
`adapter.setIdentity(...)` calling `POST /api/identity`. → ADD it.
2. **Onboarding never writes profile or identity.** The wizard has no profile step; `OnboardingState` has no
profile fields. → NEW `WhoAreYouStep` posts to `PUT /api/profile` AND `POST /api/identity` in one save.
3. **3 new profile fields** (`workType?`, `teamSize?`, `goals?: string[]`) are net-new on `UserProfile`
(S13 §5). `PUT /api/profile` allow-list (`:172-196`) must be EXTENDED to merge them; `DEFAULT_PROFILE`
(`:96-135`) extended; FE `UserProfile`/types updated.
4. **Identity-seed payload:** map profile → identity fields: `name→name`, `role→role`,
`industry→department` (or leave department blank), optionally `bio→personality`. `system_prompt` left
empty (the agent path already injects the profile frame). No schema change — `IdentityLayer.create/update`
already upserts.
Backend substrate: `profile.json` + the existing identity table. **No `.mind` migration.**
---
## 3. C33 — Import → review (the EXACT existing contract to reuse)
### Existing harvest preview/commit contract (Phase 2B.3 — DO NOT rebuild)
**`POST /api/harvest/preview`** (`harvest.ts:227-258`). Request `{ data, source }`. Response:
```ts
{
source: ImportSourceType,
itemCount: number, // true total
types: Record<string, number>, // count by ImportItemType
preview: ClassifiedItem[], // back-compat: first 10
items: ClassifiedItem[], // NEW: full list (capped at PREVIEW_ITEM_CAP=5000)
itemsTruncated: boolean,
}
// ClassifiedItem = { id, title, type, source, kind: MemoryKind, confidence: number } (harvest.ts:239-246)
```
`kind` from `importItemTypeToMemoryKind` (`harvest-classify.ts:32`); `confidence` (0100) from
`harvestConfidence` (`harvest-classify.ts:66`, B2 heuristic = source-trust × item-type, no LLM).
**`POST /api/harvest/commit`** (`harvest.ts:265-582`). Request:
```ts
{ data?, source?, resumeFromRun?, selectedIds?: Array<string|number> } // harvest.ts:266-275
```
- `selectedIds` present + non-empty → filters items to that subset (`:344-347`); absent → commits ALL
(the C33 onboarding default).
- Each committed frame is stamped (`:449-456`), guarded on empty metadata, with:
```ts
{ kind: importItemTypeToMemoryKind(item.type), confidence: harvestConfidence(item),
status: 'unreviewed', sourceId: item.id }
```
→ exactly the C33 "commit-as-unreviewed" behavior. Response carries `saved`, `cognifySkippedReason`,
`wikiSkippedReason` (`no_real_embedder`), `runId`, etc.
**The review surface already exists.** `MemoryCenterTab.tsx` (Phase 2B-FE.2) has a **"Needs review"
status filter** (`:25-31`, `value:'unreviewed'`) + `MemoryCard` renders an "Needs review" badge
(`MemoryCard.tsx:18`). So S16's review is satisfied by Memory Center — **2D does not build a Review step.**
### The GAP the wizard must close
1. **Adapter `harvestCommit` cannot pass `selectedIds`.** Current signature
`harvestCommit(data, source)` (`adapter.ts:1898-1901`) — the body omits `selectedIds`. → WIDEN to
`harvestCommit(data, source, opts?: { selectedIds?, resumeFromRun? })`. (Backend already accepts it.)
2. **`harvestPreview`/`harvestCommit` typed `any`** (`adapter.ts:1893,1898`) — tighten to the shared
`HarvestPreview`/`HarvestCommitResult` shapes (repo bans `any`; CLAUDE.md §10 — `no-explicit-any` is error).
3. **`ImportStep` reads `result.preview` (first 10), not the classified `items[]`** with kind/confidence
(`OnboardingWizard.tsx:164` sets `importPreview` from `result.preview`). → rework to consume `items[]`
and render kind chips + confidence so the user sees what is being imported (matches S15 §4). Selection is
OPTIONAL per C33 (default = import all as unreviewed); if a "deselect" affordance is added it posts
`selectedIds`. **Minimum viable 2D = keep commit-all; just surface kind/confidence + the
"you can review later in Memory Center" pointer.**
4. **`ImportStepProps.importPreview` is `readonly unknown[]`** (`types.ts:39-40`) — widen to the classified
item type.
No backend change is required for C33 — the contract shipped in 2B.3. 2D is **FE wiring only** here.
---
## 4. S17 — Workspace creation: exists vs 2D adds
### Exists
- **Backend `POST /api/workspaces`** (`workspaces.ts:162-230`): accepts `name/group/icon/model/personaId/
templateId/storageType/storagePath/teamId/...`; enforces tier workspace-limit (403, `:194-203`);
auto-creates storage structure + starter skills + template starter-memory. `WorkspaceManager.create`
writes `workspace.json` (no SQLite).
- **Adapter `createWorkspace`** (`adapter.ts:253-260`): bridges `persona`→`personaId`.
- **Onboarding already creates a workspace** via `handleFinish`/`handleSkipSetup`
(`OnboardingWizard.tsx:278-326`) — bespoke `adapter.createWorkspace` calls, NOT the dialog.
- **`CreateWorkspaceDialog.tsx`** (1129 LOC): the full standalone modal (template/persona/storage/folder
picker). The richer S17 wizard (type picker, suggested capabilities, summary card) is a **separate S17
card** — most of that is the standalone-modal rework, not the onboarding embed.
### What 2D adds (minimal, onboarding-scoped)
2D needs a workspace-create **step in the chain**, not the full S17 modal rework. Minimal:
- Reuse the existing `handleFinish` create path (name + template + persona) OR render a trimmed
type-picker + name. Per **C6** add `type?: WorkspaceType` (`project|client|research|personal`) coexisting
with `group`; per **C35** record any selected connector/MCP ids as **intent** (config fields), not installs.
- Backend: EXTEND `POST /api/workspaces` Body + `WorkspaceManager.create` with the additive optional V2
fields (`description?, type?, status?, agentIds?, connectorIds?, mcpIds?`) — **JSON file, no migration**.
(This field extension is shared with the Phase-0 `WorkspaceConfig` V2 work and the standalone S17 card.)
- Adapter `createWorkspace` body type widened to carry `description/type` (+ capability-id arrays if used).
> The full stepped S17 wizard (Suggested Skills/Agents/Connectors/MCPs panels + summary card) is the
> **standalone S17 card's** scope. 2D's workspace-create is the onboarding-embedded variant; keep it thin
> and route it through the SAME create call so onboarding and the modal converge later (S17 §4 parity note).
---
## 5. WHAT ALREADY EXISTS (do-not-rebuild) vs NET-NEW
### Do-not-rebuild (verified present)
| Capability | Where | Status |
|---|---|---|
| Harvest preview w/ per-item `kind`+`confidence` | `harvest.ts:227-258` + `harvest-classify.ts` | DONE (2B.3) |
| Harvest commit w/ `selectedIds` + `status:'unreviewed'` stamp | `harvest.ts:265-456` | DONE (2B.3) |
| B2 heuristic confidence | `harvest-classify.ts:66` | DONE |
| B6 `ImportItemType→MemoryKind` (server) | `harvest-classify.ts:32` | DONE |
| B6 FE display-category map | `apps/web/src/lib/harvest-kind-map.ts` | DONE (2B.3) |
| Memory Center "Needs review" filter (the S16 review surface) | `MemoryCenterTab.tsx:25-31`, `MemoryCard.tsx:18` | DONE (2B-FE.2) |
| Profile store + `GET/PUT /api/profile` + identity-frame mirror | `profile.ts` | EXISTS |
| Identity table + `GET/POST /api/identity` + `IdentityLayer` | `identity.ts`, `mind/identity.ts` | EXISTS |
| `adapter.getProfile/updateProfile/getIdentity` | `adapter.ts:1357,1362,742` | EXISTS |
| Profile form fields/INDUSTRIES/chip-toggle to copy | `UserProfileApp.tsx` | EXISTS |
| Claude-Code auto-detect + harvest | `harvest.ts:831`, wizard `:182-202` | EXISTS |
| Connector catalog `GET /api/connectors`; tool-detect `GET /api/tools/detect` (S14) | `connectors.ts`, `tools.ts` | EXISTS |
| `POST /api/workspaces` + tier-limit + starter skills | `workspaces.ts:162` | EXISTS |
| Wizard shell (progress/dots/Back/Skip/AnimatePresence) | `OnboardingWizard.tsx:399-575` | EXISTS — keep |
| Resume backbone (`state.step` persist) | `useOnboarding.ts` | EXISTS |
### Net-new in 2D
- FE: `WhoAreYouStep.tsx` (S13); `ToolDiscoveryStep.tsx` + `lib/tool-recommendations.ts` (S14, deferrable);
reworked `WelcomeStep`/`FirstLaunchStep` (S12); reworked `ImportStep` (S15 surface kind/confidence);
workspace-create step (S17 thin); `lib/onboarding-profile.ts` (option constants + `buildProfilePreview`).
- Adapter: **`setIdentity(...)`** (new); `harvestCommit` 3rd `opts` arg; tighten `harvestPreview/harvestCommit`
types; widen `createWorkspace` body type.
- Backend: EXTEND `PUT /api/profile` allow-list (+`workType/teamSize/goals`) + `DEFAULT_PROFILE` + `UserProfile`;
EXTEND `POST /api/workspaces` + `WorkspaceManager.create` V2 fields. **No new route, no `.mind` migration.**
- Types: `OnboardingState` += `profileSeeded?`, `toolsUsed?: string[]` (localStorage, additive); shared
`WorkspaceType` (C6) + `HarvestPreview`/`HarvestCommitResult`; FE `UserProfile` (+3 fields).
- Wizard: re-key `STEP_NAMES`, the step switch (`:498-572`), progress math (`progressPct :396`, dots `:472`,
`aria-valuemax :426`), Back-range (`:444`), and the `goToStep(N)` auto-advances.
---
## 6. Minimal sub-phase build plan (each independently tsc + test verifiable)
> Verify gate per sub-phase (CLAUDE.md §2): `tsc` on touched packages
> (`shared → hive-mind-core → server`, then `npm run build` for apps/web), `npm run test -- --run`, `npm run lint`.
> Build order: shared → hive-mind-core → core → agent → server.
### 2D.1 — Backend + shared + adapter (no UI)
Ordered, each file with the exact change + contract:
1. `packages/shared/src/types.ts` — add `WorkspaceType = 'project'|'client'|'research'|'personal'` (C6;
`team`/`organization` reserved). (`MemoryKind` already canonical here from B6/2B.3 — do NOT redefine.)
2. `packages/server/src/local/routes/profile.ts` —
- `UserProfile` (`:41-94`): add `workType?: string; teamSize?: string; goals?: string[]`.
- `DEFAULT_PROFILE` (`:96-135`): add the three defaults (`''`, `''`, `[]`).
- `PUT` allow-list (`:172-196`): merge the three (`if (updates.workType != null) …`).
- Identity-frame mirror string (`:205-210`): optionally append role/industry (pure edit).
3. `packages/server/src/local/routes/workspaces.ts` — extend the `POST` Body (`:163-181`) + the
`WorkspaceManager.create(...)` call (`:223-230`) with additive optional `description?, type?, status?,
agentIds?, connectorIds?, mcpIds?` (JSON, no migration). (Coordinate with Phase-0 V2 field work.)
4. `packages/hive-mind-core/src/workspace-manager.ts` — add the same optional fields to `WorkspaceConfig` +
`CreateWorkspaceOptions`; persist in `create()`/`update()` (stamp `updatedAt`). **No DB migration.**
5. `apps/web/src/lib/adapter.ts` —
- NEW `async setIdentity(body): Promise<IdentityResponse>` → `POST /api/identity` (mirror `getIdentity`
`:742`).
- `harvestCommit` (`:1898`): add `opts?: { selectedIds?: Array<string|number>; resumeFromRun?: number }`
and include in the POST body.
- Tighten `harvestPreview`/`harvestCommit` return types to shared `HarvestPreview`/`HarvestCommitResult`
(drop `any`); `getProfile`/`updateProfile` to `UserProfile` (drop `any`).
- `createWorkspace` body type (`:253`): add `description?, type?: WorkspaceType, connectorIds?, mcpIds?`.
**Verify 2D.1:** tsc shared + hive-mind-core + server (+ apps/web build for adapter); add/extend route tests
(profile merge of 3 fields; identity upsert; commit `selectedIds` filter — the last already covered by 2B.3
tests, just confirm green).
### 2D.2 — Frontend (the chain)
1. `apps/web/src/lib/onboarding-profile.ts` (NEW, pure + `.test.ts`) — `WORK_TYPES`, `TEAM_SIZES`, `GOALS`
option constants + `buildProfilePreview(partial)` (keeps the step thin).
2. `apps/web/src/lib/types.ts` — promote shared `UserProfile` (+ `workType/teamSize/goals`); import
`WorkspaceType`, `HarvestPreview`/`HarvestCommitResult` from `@waggle/shared`.
3. `apps/web/src/hooks/useOnboarding.ts` — `OnboardingState` (`:10-19`): add additive optional
`profileSeeded?: boolean`, `toolsUsed?: string[]` (localStorage only).
4. `apps/web/.../onboarding/WelcomeStep.tsx` (rework → S12): align copy; add static `English (US)` chip
(C28) + privacy note; consume `useOfflineStatus`. `OnboardingWizard.tsx:136-141`: **drop the 3s
auto-advance** (C29).
5. `apps/web/.../onboarding/WhoAreYouStep.tsx` (NEW → S13/B8): Name/Role/Industry (reuse `UserProfileApp`
`INDUSTRIES`/chips) + WorkType/TeamSize/Goals + live preview. On Continue: `adapter.updateProfile(...)`
**AND** `adapter.setIdentity({name, role, department: industry})`. Props `{ profile, onChange,
onContinue, onBack, saving }`. Add `WhoAreYouStepProps` to `onboarding/types.ts`.
6. `apps/web/.../onboarding/ToolDiscoveryStep.tsx` (NEW → S14): grouped grid from `getConnectors` +
`detectTools` (pre-check detected, C32); namespaced ids `connector:*`/`tool:*` (C31); writes
`toolsUsed[]`. **DEFERRABLE** — if descoping 2D, omit this step and renumber; S14 has zero backend and
can land in a 2D follow-up. (Flag in the chain as optional.)
7. `apps/web/.../onboarding/ImportStep.tsx` (rework → S15): consume `result.items[]` (kind+confidence) not
`preview[]`; render kind chips + `ConfidenceBadge` (reuse `components/ui/confidence-badge`); add the
"review anytime in Memory → Needs review" pointer (C33). Hermes/Codex/Cursor = upload/"Other"/coming-soon
(C34). Keep commit-all default; commit still lands `unreviewed` server-side. Widen `ImportStepProps`.
8. `apps/web/.../onboarding/` workspace-create step (S17 thin): trimmed type-picker (C6) + name → existing
`handleFinish` create path; record connector/MCP intent ids (C35). (Full stepped S17 = separate card.)
9. `apps/web/.../overlays/OnboardingWizard.tsx` — re-key `STEP_NAMES` (`:35`) to
`first-launch/who-are-you/tool-discovery/memory-import/workspace-create/ready`; rewrite the step switch
(`:498-572`); fix progress math (`:396`, dots `:472`, `aria-valuemax :426`, Back-range `:444`); add
`handleProfileSave`; point import commit at the new step order; **remove the dead tier/api-key/model
steps from the chain** (or keep behind a flag — surgical, prefer removal of the now-orphaned wiring).
**Verify 2D.2:** `npm run build` (apps/web tsc), `npm run test -- --run` (new `onboarding-profile.test.ts`
+ any wizard tests), `npm run lint`. Manual: `?forceWizard=true` walks First Launch → Who Are You (writes
profile+identity → Home greets by name) → [Tool Discovery] → Import (commits unreviewed; appears in Memory
"Needs review") → Workspace Create → Home.
### Largest single risk
**The `OnboardingWizard.tsx` step-index rewire.** It re-keys `STEP_NAMES`, the AnimatePresence step switch,
progress %, the dot array, `aria-valuemax`, the Back-button range, AND every `goToStep(N)` literal
(`:174, :199, :300, :350` etc.) — plus removal of the orphaned tier/api-key/model handlers and their
step-local state (`selectedTier/apiKey/keyValid/...`, `:47-60`). An off-by-one or a stale `goToStep(4)`
strands the user mid-flow or skips identity-seed/commit. Mitigation: change the step set in ONE pass, drive
all navigation off `STEP_NAMES.indexOf(name)` (not magic numbers), keep an E2E that asserts the full chain
reaches Home, and verify resume (`state.step`) lands on the correct re-keyed step. This is exactly the churn
S13/S14/S15/S16 §7 all warn must be "rewired once, not per-card."