Files
workspace/docs/PROMPT-onboarding.md
Roberto Musso 8ce3ade8ce feat(onboarding): implement first-run user onboarding wizard for profile setup
- Added a new onboarding wizard that runs on the first app launch post-login.
- Collects user personalization data (job role, industry, primary use case, tone preference, language) and stores it in encrypted core memory.
- Auto-detects and saves formatting preferences (timezone, time format, date format) in local electron-store.
- Normalizes user free-text inputs via a backend LLM call before persisting.
- Introduced new backend routes for memory updates and normalization.
- Updated frontend components to support the onboarding flow with a chat-bubble aesthetic.
- Added settings section for profile editing and re-running the onboarding process.
- Ensured that the onboarding process is skippable and editable in the settings.
- Implemented verification steps to ensure proper functionality and data handling.

chore: update submodules for waitlist and website
2026-04-12 00:36:11 +02:00

714 lines
24 KiB
Markdown
Raw 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.

# RALPH LOOP PROMPT — First-Run Onboarding Wizard
> **How to run:**
> ```
> /ralph-loop "Implement the onboarding wizard exactly as specified in docs/PROMPT-onboarding.md. Output <promise>ONBOARDING COMPLETE</promise> when all phases pass lint." --max-iterations 25 --completion-promise "ONBOARDING COMPLETE"
> ```
---
## INSTRUCTIONS FOR CLAUDE
You are implementing a first-run onboarding wizard for the adiuvAI Electron app. This is a **multi-file, multi-iteration** task. On each iteration:
1. **Read this file** in full.
2. **Inspect which tasks are already done** by checking if the target files exist and contain the expected code.
3. **Pick the next incomplete task** (always in phase order: Phase 1 → 2 → 3 → 4).
4. **Implement it**, then **run the relevant lint command** before exiting.
5. When ALL phases are complete AND both lint commands pass, output `<promise>ONBOARDING COMPLETE</promise>`.
**DO NOT** skip phases. **DO NOT** implement out of order — backend must exist before the FE can call it.
**LINT COMMANDS** (run after each phase):
- Backend: `cd api && ruff check . --fix`
- Frontend: `cd adiuvAI && npx eslint . --fix`
---
## WHAT THIS FEATURE DOES
After login, new users see a chat-styled wizard that collects 5 fields:
- `job_role`, `industry`, `primary_use_case`, `tone_preference`, `language`
These are stored encrypted in `MemoryCore` (backend) so the AI agents personalize responses. Three formatting prefs (`timezone`, `date_format`, `time_format`) are auto-detected from the OS and stored in electron-store (FE only) — the LLM never sees them. The FE formats all timestamp columns in tool-result rows before sending them back to the backend.
**Storage split:**
| Field | Where | Why |
|-------|-------|-----|
| job_role, industry, primary_use_case, tone_preference, language | `MemoryCore` (backend, encrypted) | LLM needs these for personalization |
| timezone, date_format, time_format | electron-store (FE) | FE formatter only — LLM must never see raw timestamps |
**Key architectural fact:** `memory_middleware.py` `enrich_context()` already injects `core_memory` into every orchestrator call. Writing to `MemoryCore` is sufficient — no system-prompt changes needed.
---
## PHASE 1 — Backend (api/)
### TASK 1.1: Alembic migration — `onboarding_completed_at` column
**File:** `api/alembic/versions/XXX_add_onboarding_completed_at.py` (new)
Create a new Alembic migration that adds:
```sql
ALTER TABLE users ADD COLUMN onboarding_completed_at TIMESTAMPTZ NULL;
```
Use the existing migrations in `api/alembic/versions/` as a pattern reference. The revision ID should be sequential (check the latest existing migration number and increment).
**Done signal:** File exists in `api/alembic/versions/` with the column add.
---
### TASK 1.2: Add column to User model
**File:** `api/app/models.py`
Find the `User` class (around line 63-94). Add:
```python
onboarding_completed_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True, default=None
)
```
Import `DateTime` from sqlalchemy if not already imported.
**Done signal:** `User` model has `onboarding_completed_at` field.
---
### TASK 1.3: Extend UserProfile schema
**File:** `api/app/schemas.py`
Find `UserProfile` (around line 27-33). Add two fields:
```python
onboarding_completed_at: int | None = None # epoch ms, null = not onboarded
memory: dict[str, str] = Field(default_factory=dict) # decrypted core memory k/v
```
**Done signal:** `UserProfile` has both new fields.
---
### TASK 1.4: Extend `get_current_user` to return memory + onboarding flag
**File:** `api/app/api/middleware/auth.py`
In `get_current_user()`, after fetching the user row and resolving the tier:
1. Read `user.onboarding_completed_at` — convert to epoch ms (int) or None.
2. Use `MemoryMiddleware(db).enrich_context(user.id)` to load decrypted core memory. Extract the `core` dict → `{label: value}` pairs.
3. Return `UserProfile(..., onboarding_completed_at=..., memory=...)`.
This requires `get_current_user` to also receive the `db: AsyncSession` dependency. Check if it already does — if not, add `Depends(get_session)`.
**Done signal:** `GET /api/v1/auth/me` returns `onboarding_completed_at` and `memory` fields.
---
### TASK 1.5: New route — `PUT /auth/me/memory`
**File:** `api/app/api/routes/auth.py`
Add a new route (do NOT modify `_UpdateProfileRequest`):
```python
class _UpdateMemoryRequest(BaseModel):
memory: dict[str, str] = Field(default_factory=dict)
mark_onboarded: bool = False
@router.put("/me/memory", response_model=UserProfile)
async def update_memory(
body: _UpdateMemoryRequest,
current_user: UserProfile = Depends(get_current_user),
db: AsyncSession = Depends(get_session),
) -> UserProfile:
mw = MemoryMiddleware(db)
for key, value in body.memory.items():
await mw.update_core(current_user.id, key, value)
if body.mark_onboarded:
result = await db.execute(select(User).where(User.id == current_user.id))
user = result.scalar_one()
user.onboarding_completed_at = datetime.now(timezone.utc)
await db.commit()
# Re-fetch profile and return
return await get_current_user(...) # use same logic as GET /me
```
Also add a companion route to reset onboarding (for "Re-run onboarding" in Settings):
```python
@router.post("/me/onboarding/reset")
async def reset_onboarding(
current_user: UserProfile = Depends(get_current_user),
db: AsyncSession = Depends(get_session),
):
result = await db.execute(select(User).where(User.id == current_user.id))
user = result.scalar_one()
user.onboarding_completed_at = None
await db.commit()
return {"status": "reset"}
```
**Done signal:** Both routes exist and are syntactically correct.
---
### TASK 1.6: New route — `POST /auth/onboarding/normalize`
**File:** `api/app/api/routes/auth.py`
```python
class _NormalizeRequest(BaseModel):
inputs: dict[str, str] # {"job_role": "i build websites"}
class _NormalizeResponse(BaseModel):
normalized: dict[str, str]
@router.post("/onboarding/normalize", response_model=_NormalizeResponse)
async def normalize_onboarding(
body: _NormalizeRequest,
current_user: UserProfile = Depends(get_current_user),
) -> _NormalizeResponse:
"""One-shot LLM normalization for free-text onboarding answers."""
if not body.inputs:
return _NormalizeResponse(normalized={})
try:
llm = get_llm("gpt-4o-mini", temperature=0)
prompt = (
"You normalize user onboarding answers into clean, ≤3-word canonical labels.\n"
"Return a JSON object with the same keys and normalized values.\n"
"Examples: 'i build websites' → 'Web Developer', 'tech-ish stuff' → 'Technology'\n"
f"Input: {json.dumps(body.inputs)}"
)
response = await llm.ainvoke(
[{"role": "system", "content": "You normalize user inputs. Return JSON only."},
{"role": "user", "content": prompt}],
)
normalized = json.loads(response.content)
return _NormalizeResponse(normalized=normalized)
except Exception:
# LLM failure must never block onboarding — return inputs unchanged
return _NormalizeResponse(normalized=body.inputs)
```
Use `get_llm` from `app.core.llm`. Use `json` stdlib. The `try/except` is critical — flaky LLM must never block the wizard.
**Done signal:** Route exists, has the safety try/except, returns inputs on failure.
---
### TASK 1.7: Backend lint check
Run: `cd api && ruff check . --fix`
Fix any issues before proceeding to Phase 2.
**Done signal:** `ruff check .` exits 0.
---
## PHASE 2 — Electron Main Process (adiuvAI/src/main/)
### TASK 2.1: Extend `UserProfileSchema`
**File:** `adiuvAI/src/shared/api-types.ts`
Find `UserProfileSchema` (Zod schema). Add:
```ts
onboardingCompletedAt: z.number().int().nullable().optional(),
memory: z.record(z.string(), z.string()).default({}),
```
**Done signal:** Schema has both fields.
---
### TASK 2.2: Add formatPrefs to electron-store
**File:** `adiuvAI/src/main/store.ts`
Extend the `AppSettings` interface:
```ts
formatPrefs: {
timezone: string;
dateFormat: string; // 'dd/MM/yyyy' | 'MM/dd/yyyy' | 'yyyy-MM-dd'
timeFormat: '12h' | '24h';
} | null;
```
Default to `null` in the store defaults.
Add helpers:
```ts
export function getFormatPrefs(): FormatPrefs | null {
return getStore().get('formatPrefs', null);
}
export function setFormatPrefs(prefs: FormatPrefs): void {
getStore().set('formatPrefs', prefs);
}
```
Export `FormatPrefs` as a type.
**Done signal:** `getFormatPrefs()` and `setFormatPrefs()` exported from store.ts.
---
### TASK 2.3: Create locale-defaults helper
**File:** `adiuvAI/src/main/auth/locale-defaults.ts` (new)
```ts
import { app } from 'electron';
export interface FormatPrefs {
timezone: string;
dateFormat: string;
timeFormat: '12h' | '24h';
}
export function detectFormatPrefs(): FormatPrefs {
const locale = app.getLocale();
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const hour12 = Intl.DateTimeFormat(locale, { hour: 'numeric' }).resolvedOptions().hour12;
const timeFormat = hour12 ? '12h' : '24h';
const dateFormat = inferDateFormat(locale);
return { timezone, timeFormat, dateFormat };
}
export function detectLanguage(): string {
return app.getLocale(); // e.g. 'it-IT', 'en-US'
}
function inferDateFormat(locale: string): string {
// MDY locales
const mdyLocales = ['en-US', 'en-PH', 'en-BZ'];
if (mdyLocales.some(l => locale.startsWith(l))) return 'MM/dd/yyyy';
// YMD locales (CJK, ISO-oriented)
const ymdPrefixes = ['ja', 'zh', 'ko', 'hu', 'lt', 'sv', 'fi'];
if (ymdPrefixes.some(p => locale.startsWith(p))) return 'yyyy-MM-dd';
// Default: DMY (most of the world)
return 'dd/MM/yyyy';
}
```
**Done signal:** File exists with `detectFormatPrefs()` and `detectLanguage()` exported.
---
### TASK 2.4: Create format-row helper
**File:** `adiuvAI/src/main/api/format-row.ts` (new)
```ts
import type { FormatPrefs } from '../auth/locale-defaults';
const TIMESTAMP_COLUMNS = new Set([
'createdAt', 'updatedAt', 'dueDate', 'date', 'endDate',
'lastRunAt', 'startedAt', 'completedAt',
]);
export function formatRows<T extends Record<string, unknown>>(
rows: T[],
prefs: FormatPrefs,
): T[] {
return rows.map(row => formatRow(row, prefs));
}
export function formatRow<T extends Record<string, unknown>>(
row: T,
prefs: FormatPrefs,
): T {
const result = { ...row };
for (const col of TIMESTAMP_COLUMNS) {
if (col in result && typeof result[col] === 'number') {
(result as Record<string, unknown>)[col] = formatTimestamp(
result[col] as number,
prefs,
);
}
}
return result;
}
function formatTimestamp(epochMs: number, prefs: FormatPrefs): string {
const date = new Date(epochMs);
const hour12 = prefs.timeFormat === '12h';
const opts: Intl.DateTimeFormatOptions = {
timeZone: prefs.timezone,
hour12,
hour: '2-digit',
minute: '2-digit',
};
const timePart = date.toLocaleTimeString('en-US', opts);
const day = date.toLocaleDateString('en-CA', { timeZone: prefs.timezone, day: '2-digit' }).slice(-2);
const month = date.toLocaleDateString('en-CA', { timeZone: prefs.timezone, month: '2-digit' }).slice(-2);
const year = date.toLocaleDateString('en-CA', { timeZone: prefs.timezone, year: 'numeric' }).slice(0, 4);
let datePart: string;
switch (prefs.dateFormat) {
case 'MM/dd/yyyy': datePart = `${month}/${day}/${year}`; break;
case 'yyyy-MM-dd': datePart = `${year}-${month}-${day}`; break;
default: datePart = `${day}/${month}/${year}`; break;
}
return `${datePart} ${timePart}`;
}
```
**Done signal:** File exists with `formatRow` and `formatRows` exported.
---
### TASK 2.5: Wire format-row into drizzle-executor
**File:** `adiuvAI/src/main/api/drizzle-executor.ts`
Import `formatRows`, `formatRow` from `./format-row` and `getFormatPrefs` from `../store` and `detectFormatPrefs` from `../auth/locale-defaults`.
Find every place that returns `{ rows }` or `{ row }` results. Wrap them:
- `rows``formatRows(rows, getFormatPrefs() ?? detectFormatPrefs())`
- `row``formatRow(row, getFormatPrefs() ?? detectFormatPrefs())`
The `?? detectFormatPrefs()` fallback handles the edge case where executor runs before first auth.status seed.
**Important:** Only format on `handleList`, `handleGet`, `handleInsert`, `handleUpdate` return paths — NOT on delete. Do not mutate the original rows — `formatRow` returns a new object.
**Done signal:** All select/get/insert/update returns pass through formatRow(s).
---
### TASK 2.6: Add auth methods to auth-manager
**File:** `adiuvAI/src/main/auth/auth-manager.ts`
Add two methods to the `AuthManager` class:
```ts
async updateMemory(
memory: Record<string, string>,
markOnboarded = false,
): Promise<UserProfile> {
return this.put('/api/v1/auth/me/memory', {
memory,
mark_onboarded: markOnboarded,
});
}
async normalizeOnboarding(
inputs: Record<string, string>,
): Promise<Record<string, string>> {
const res = await this.post('/api/v1/auth/onboarding/normalize', { inputs });
return res.normalized;
}
async resetOnboarding(): Promise<void> {
await this.post('/api/v1/auth/me/onboarding/reset', {});
}
```
Use the existing `this.put()` / `this.post()` helpers (they handle auth headers and camelCase/snakeCase conversion).
**Done signal:** All three methods exist on AuthManager.
---
### TASK 2.7: Extend tRPC authRouter
**File:** `adiuvAI/src/main/router/index.ts`
In the `authRouter`, add these procedures:
```ts
updateMemory: t.procedure
.input(z.object({
memory: z.record(z.string(), z.string()),
markOnboarded: z.boolean().optional().default(false),
}))
.mutation(async ({ input }) => {
return authManager.updateMemory(input.memory, input.markOnboarded);
}),
normalizeOnboarding: t.procedure
.input(z.object({
inputs: z.record(z.string(), z.string()),
}))
.mutation(async ({ input }) => {
return authManager.normalizeOnboarding(input.inputs);
}),
resetOnboarding: t.procedure
.mutation(async () => {
return authManager.resetOnboarding();
}),
```
Also, in the existing `auth.status` procedure, add **silent auto-seeding** logic:
- After fetching the profile, if `getFormatPrefs()` returns null → call `setFormatPrefs(detectFormatPrefs())`.
- If `profile.memory.language` is missing/empty → call `authManager.updateMemory({ language: detectLanguage() })` silently (fire-and-forget, don't block the return).
**Done signal:** Three new procedures exist. Status procedure auto-seeds format prefs and language.
---
### TASK 2.8: Add settings routes for formatPrefs
**File:** `adiuvAI/src/main/router/index.ts`
In `settingsRouter` (or create one if it doesn't exist), add:
```ts
getFormatPrefs: t.procedure.query(() => {
return getFormatPrefs();
}),
setFormatPrefs: t.procedure
.input(z.object({
timezone: z.string(),
dateFormat: z.string(),
timeFormat: z.enum(['12h', '24h']),
}))
.mutation(({ input }) => {
setFormatPrefs(input);
return input;
}),
```
**Done signal:** Both procedures exist.
---
### TASK 2.9: Frontend lint check
Run: `cd adiuvAI && npx eslint . --fix`
Fix any TypeScript/ESLint issues before proceeding to Phase 3.
**Done signal:** ESLint exits 0.
---
## PHASE 3 — Electron Renderer (adiuvAI/src/renderer/)
### TASK 3.1: Create onboarding chip options
**File:** `adiuvAI/src/renderer/components/onboarding/onboardingOptions.ts` (new)
```ts
export const JOB_ROLES = ['Developer', 'Designer', 'Consultant', 'Founder', 'Project Manager'] as const;
export const INDUSTRIES = ['Tech', 'Design', 'Consulting', 'Legal', 'Marketing', 'Education'] as const;
export const USE_CASES = ['Solo freelancer', 'Client manager', 'Team lead', 'Personal productivity'] as const;
export const TONES = ['Casual', 'Formal', 'Concise', 'Detailed'] as const;
```
**Done signal:** File exists with all four arrays exported.
---
### TASK 3.2: Create OnboardingFlow component
**File:** `adiuvAI/src/renderer/components/onboarding/OnboardingFlow.tsx` (new)
This is the most complex file. Key requirements:
**State machine:**
```ts
type Step = 'welcome' | 'jobRole' | 'industry' | 'useCase' | 'tone' | 'language' | 'reviewing' | 'done';
```
**Props:**
```ts
interface OnboardingFlowProps {
profile: UserProfile;
}
```
**Visual style — must match AIChatPanel:**
- Chat bubble layout: AI messages in `rounded-2xl` bubbles with a Sparkles icon (from lucide-react).
- Use glassmorphism: `bg-white/5 backdrop-blur-md border border-white/10`.
- Spring transitions (framer-motion) for each step entering.
- Use shadcn components: `Button`, `Input`, `Card`.
**Each wizard step shows:**
1. An "AI bubble" with the question text.
2. 36 preset chip buttons (from onboardingOptions.ts).
3. An optional "Type your own" text input (for `job_role` and `industry` only).
4. A "Skip" link at the bottom.
5. Previous answers appear above as "user bubbles" (right-aligned).
**Step details:**
| Step | Question | Chips | Free text? |
|------|----------|-------|-----------|
| welcome | "Hi {name}! I'm your AI assistant. Let me learn a few things about you so I can help better." | Just a "Let's go" button | No |
| jobRole | "What's your role?" | JOB_ROLES | Yes |
| industry | "What industry do you work in?" | INDUSTRIES | Yes |
| useCase | "How will you mainly use adiuvAI?" | USE_CASES | No |
| tone | "How should I talk to you?" | TONES | No |
| language | "I'll respond in {detected}. Want to change it?" | Show detected language pre-selected; allow typing a different one | Yes |
| reviewing | Review screen (see below) | — | — |
| done | Redirect (never renders) | — | — |
**Reviewing step logic:**
1. Partition answers: chip selections (already clean) vs free-text answers (need normalization).
2. If free-text map is non-empty → call `trpc.auth.normalizeOnboarding.useMutation`. Show "Tidying up…" spinner on those fields only.
3. Show a Card titled "Here's what I'll save" with all 5 fields as rows.
4. Each row has an Edit pencil icon → converts to inline input → Enter saves → back to read-only.
5. If LLM changed a value, show grey hint: `auto-tidied from "original text"`.
6. Primary button: "Looks good — save" → calls `trpc.auth.updateMemory.useMutation({ memory: finalMap, markOnboarded: true })``utils.auth.status.invalidate()`.
7. Secondary link: "Back to wizard" → resets to `jobRole` step with values pre-filled.
8. **Failure modes:**
- Normalization fails → show raw values + banner "Couldn't auto-tidy — review and save". Save still works.
- Save fails → toast error, stay on review screen.
**Skip behavior:** Clicking Skip on any step → calls `updateMemory({}, markOnboarded: true)` with empty map → wizard closes. Language was already auto-seeded by `auth.status`.
**Done signal:** Component file exists, renders a multi-step wizard, handles reviewing + save.
---
### TASK 3.3: Gate OnboardingFlow in AppShell
**File:** `adiuvAI/src/renderer/components/layout/AppShell.tsx`
After the `authenticated === false``LoginForm` branch, add:
```tsx
if (
authStatusQuery.data?.profile &&
authStatusQuery.data.profile.onboardingCompletedAt == null
) {
return <OnboardingFlow profile={authStatusQuery.data.profile} />;
}
```
Import `OnboardingFlow` from `../onboarding/OnboardingFlow`.
**Done signal:** AppShell conditionally renders OnboardingFlow when onboardingCompletedAt is null.
---
### TASK 3.4: Add 'profile' to settings sections
**File:** `adiuvAI/src/renderer/components/settings/types.ts`
Add `'profile'` to the `SectionId` type and `{ id: 'profile', label: 'Profile' }` to `SECTIONS` array — insert it before `'account'`.
**Done signal:** SECTIONS includes 'profile' as the first or second entry.
---
### TASK 3.5: Create ProfileSection component
**File:** `adiuvAI/src/renderer/components/settings/ProfileSection.tsx` (new)
Plain form (no chat aesthetic — this is Settings, not the wizard). Two cards:
**Card 1 — "About you"** (writes to MemoryCore via `auth.updateMemory`):
- Fields: job_role, industry, primary_use_case (select from USE_CASES), tone_preference (select from TONES), language (text input).
- Pre-populate from `authStatusQuery.data.profile.memory`.
- Save button → `trpc.auth.updateMemory.useMutation`.
- "Re-run onboarding" button → `trpc.auth.resetOnboarding.useMutation``utils.auth.status.invalidate()` (triggers wizard via AppShell gate).
**Card 2 — "Display preferences"** (writes to electron-store via `trpc.settings.setFormatPrefs`):
- Timezone: searchable select, populated from `Intl.supportedValuesOf('timeZone')`.
- Date format: select with options: `dd/MM/yyyy`, `MM/dd/yyyy`, `yyyy-MM-dd`.
- Time format: radio group — 12h / 24h.
- Pre-populate from `trpc.settings.getFormatPrefs` query.
- Save button → `trpc.settings.setFormatPrefs.useMutation`.
Use shadcn `Card`, `Input`, `Select`, `Button`, `Label`, `RadioGroup` components.
**Done signal:** Component exists with both cards, save functionality wired.
---
### TASK 3.6: Wire ProfileSection into settings route
**File:** `adiuvAI/src/renderer/routes/settings.tsx`
Add the import and the conditional render:
```tsx
{section === 'profile' && <ProfileSection />}
```
**Done signal:** ProfileSection renders when section=profile.
---
### TASK 3.7: Final lint check
Run both:
```bash
cd api && ruff check . --fix
cd adiuvAI && npx eslint . --fix
```
**Done signal:** Both exit 0.
---
## PHASE 4 — Completion
### TASK 4.1: Verify all files exist
Check that these files exist:
- [ ] `api/alembic/versions/*_add_onboarding_completed_at.py`
- [ ] `adiuvAI/src/main/auth/locale-defaults.ts`
- [ ] `adiuvAI/src/main/api/format-row.ts`
- [ ] `adiuvAI/src/renderer/components/onboarding/onboardingOptions.ts`
- [ ] `adiuvAI/src/renderer/components/onboarding/OnboardingFlow.tsx`
- [ ] `adiuvAI/src/renderer/components/settings/ProfileSection.tsx`
Check that these files were modified:
- [ ] `api/app/models.py` (has `onboarding_completed_at`)
- [ ] `api/app/schemas.py` (UserProfile has `memory` + `onboarding_completed_at`)
- [ ] `api/app/api/middleware/auth.py` (get_current_user returns memory)
- [ ] `api/app/api/routes/auth.py` (3 new routes)
- [ ] `adiuvAI/src/shared/api-types.ts` (UserProfileSchema extended)
- [ ] `adiuvAI/src/main/store.ts` (formatPrefs + helpers)
- [ ] `adiuvAI/src/main/auth/auth-manager.ts` (3 new methods)
- [ ] `adiuvAI/src/main/router/index.ts` (5 new procedures + auto-seed in status)
- [ ] `adiuvAI/src/main/api/drizzle-executor.ts` (formatRow wiring)
- [ ] `adiuvAI/src/renderer/components/layout/AppShell.tsx` (onboarding gate)
- [ ] `adiuvAI/src/renderer/components/settings/types.ts` (profile section)
- [ ] `adiuvAI/src/renderer/routes/settings.tsx` (ProfileSection render)
### TASK 4.2: Output completion promise
If everything above is done and lint passes:
```
<promise>ONBOARDING COMPLETE</promise>
```
---
## REFERENCE — Existing patterns to reuse
**DO NOT reinvent these. Copy their patterns:**
| Pattern | Source file | Reuse for |
|---------|-----------|-----------|
| Chat bubble + Sparkles + glass | `src/renderer/components/ai/AIChatPanel.tsx` | OnboardingFlow bubbles |
| Stepper state machine | `InlineAgentCreationStepper` in renderer | Wizard step transitions |
| MemoryMiddleware.update_core | `api/app/core/memory_middleware.py:137-173` | PUT /me/memory route |
| get_llm() | `api/app/core/llm.py` | Normalize route |
| electron-store helpers | `src/main/store.ts` (getDeviceId pattern) | getFormatPrefs/setFormatPrefs |
| tRPC procedure pattern | `src/main/router/index.ts` (auth.status) | New procedures |
| shadcn form components | Existing settings sections | ProfileSection |
| toCamelCase / toSnakeCase | `auth-manager.ts` proxy helpers | Automatic key conversion |
---
## DO NOT
- Add features not described here (no avatar upload, no i18n framework, no animation library beyond framer-motion if already installed).
- Modify the orchestrator or system prompts — MemoryCore injection is already handled.
- Add foreign key constraints to the migration.
- Store formatting prefs in MemoryCore.
- Let the LLM normalization route throw on failure — it MUST return inputs unchanged.
- Skip the reviewing step in the wizard.
- Run both lint checks and fix issues before claiming completion.