# 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 0–7). | 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 (S12–S17) 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, // 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` (0–100) 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 } // 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` → `POST /api/identity` (mirror `getIdentity` `:742`). - `harvestCommit` (`:1898`): add `opts?: { selectedIds?: Array; 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."