15 KiB
| phase | verified | status | score | human_verification | |||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 30-hardware-detection-mode-selection | 2026-04-02T23:58:20Z | human_needed | 11/11 must-haves verified |
|
Phase 30: Hardware Detection + Mode Selection — Verification Report
Phase Goal: Users see accurate hardware information during onboarding, get a model recommendation matched to their machine, and choose a mode that correctly gates all downstream features — with the probe working before board auth exists Verified: 2026-04-02T23:58:20Z Status: human_needed Re-verification: No — initial verification
Goal Achievement
Observable Truths
| # | Truth | Status | Evidence |
|---|---|---|---|
| 1 | GET /api/system/providers returns 200 with hardware info without any auth token | ✓ VERIFIED | hardwareRoutes() mounted with app.use("/api", hardwareRoutes()) at line 132 of app.ts, BEFORE const api = Router() at line 135 and api.use(boardMutationGuard()) at line 136 |
| 2 | Apple Silicon is detected via CPU brand string and returns unifiedMemory: true with hardwareTier: apple_silicon | ✓ VERIFIED | hardware.ts line 45: platform === "darwin" && cpuModel?.startsWith("Apple") — returns unifiedMemory: true, hardwareTier: "apple_silicon". Test confirms: returns apple_silicon tier when platform is darwin and CPU starts with Apple |
| 3 | GPU detection via systeminformation has a 3-second timeout; failure degrades to cpu_only tier | ✓ VERIFIED | hardware.ts lines 68-71: Promise.race([si.graphics(), new Promise<never>((_resolve, reject) => { setTimeout(() => reject(...), 3000) })]). Catch block sets hardwareTier = "cpu_only". Test confirms timeout scenario passes |
| 4 | nexusSettingsService persists mode to data/nexus-settings.json and reads it back | ✓ VERIFIED | nexus-settings.ts: set() writes to resolvePaperclipInstanceRoot()/data/nexus-settings.json, get() reads it back with Zod validation and graceful default of { mode: "both" }. Tests confirm CRUD round-trip |
| 5 | PATCH /api/nexus/settings requires board auth and persists the mode value | ✓ VERIFIED | nexus-settings route lines 23-35: assertBoard(req) before nexusSettingsService().set(req.body). Route mounted on api router (inside boardMutationGuard) via api.use(nexusSettingsRoutes()) at app.ts line 163 |
| 6 | Model catalog contains tier field on every variant and includes qwen3:8b family | ✓ VERIFIED | node -e check: Total variants: 12, Missing tier: 0, Has qwen3: true — all 12 variants have tier arrays, qwen3 family exists |
| 7 | getRecommendedModel filters by hardware tier when tier data is present | ✓ VERIFIED | ollama.ts line 186: if (hardwareTier && entry.tier && !entry.tier.includes(hardwareTier)) continue;. Signature accepts optional `hardwareTier?: "gpu" |
| 8 | User sees hardware detection results (GPU/RAM/unified memory) during onboarding within 5 seconds | ✓ VERIFIED | HardwareSummaryStep.tsx renders tier-appropriate stat rows using hardwareInfo from useHardwareInfo() hook. Server-side 3s GPU timeout + React Query retry:1 ensures result within 5s |
| 9 | User can select mode: Personal AI Assistant, Project Builder, or Both (default pre-selected) | ✓ VERIFIED | ModeSelector.tsx renders three cards with correct labels. NexusOnboardingWizard.tsx: useState<NexusMode>("both") — default "both" as required |
| 10 | Selected mode is persisted via PATCH /api/nexus/settings on wizard completion | ✓ VERIFIED | NexusOnboardingWizard.tsx lines 180-184: await updateNexusSettings({ mode: selectedMode }) called after company creation, wrapped in try/catch |
| 11 | Wizard has 3 steps: hardware detection, mode selection, root directory (existing) | ✓ VERIFIED | NexusOnboardingWizard.tsx: useState(1) for step, Step {step} of 3 indicator, conditional rendering for steps 1/2/3 with Back navigation on steps 2 and 3 |
Score: 11/11 truths verified
Required Artifacts
| Artifact | Expected | Status | Details |
|---|---|---|---|
server/src/services/hardware.ts |
hardwareService with detect() returning HardwareInfo | ✓ VERIFIED | Exports hardwareService, HardwareInfo, HardwareTier, _resetHardwareCache. 105 lines, fully implemented |
server/src/services/nexus-settings.ts |
File-backed nexus settings persistence | ✓ VERIFIED | Exports nexusSettingsService, NexusMode, NEXUS_MODES. Zod-validated, file-backed. 47 lines |
server/src/routes/hardware.ts |
Unauthenticated GET /api/system/providers | ✓ VERIFIED | Exports hardwareRoutes. Two routes: /system/providers and /system/providers/recommendation. Unauthenticated comment present |
server/src/routes/nexus-settings.ts |
Board-auth-gated GET/PATCH /api/nexus/settings | ✓ VERIFIED | Exports nexusSettingsRoutes. Both routes call assertBoard(req) before service calls |
server/src/data/ollama-model-catalog.json |
Extended model catalog with tier arrays and qwen3 family | ✓ VERIFIED | 12 variants, 0 missing tier, qwen3 family present with qwen3:8b |
server/src/__tests__/30-hardware-detection.test.ts |
Unit tests for hardware service, settings service, routes, and catalog | ✓ VERIFIED | 13 tests across 4 describe blocks; all pass |
ui/src/api/hardware.ts |
Typed fetch wrappers for hardware probe and nexus settings | ✓ VERIFIED | Exports fetchHardwareInfo, fetchNexusSettings, updateNexusSettings + all types. 34 lines |
ui/src/hooks/useHardwareInfo.ts |
useQuery wrapper for hardware data | ✓ VERIFIED | Exports useHardwareInfo. Uses queryKeys.hardware.info, 5-min staleTime, retry:1 |
ui/src/components/onboarding/ModeSelector.tsx |
Three-card mode selector with selected state styling | ✓ VERIFIED | Three cards, border-primary bg-primary/5 selected state, correct labels |
ui/src/components/onboarding/HardwareSummaryStep.tsx |
Hardware info display with skeleton loading, tier-appropriate labels, privacy frame | ✓ VERIFIED | All three tier paths implemented, privacy frame for non-cpu_only, Skeleton loading, error state |
ui/src/components/NexusOnboardingWizard.tsx |
Multi-step wizard: hardware -> mode -> root directory | ✓ VERIFIED | 3-step flow, step indicator, Back navigation, mode persistence on submit, export function OnboardingWizard() preserved |
Key Link Verification
| From | To | Via | Status | Details |
|---|---|---|---|---|
server/src/routes/hardware.ts |
server/src/services/hardware.ts |
hardwareService().detect() |
✓ WIRED | Line 12: const info = await hardwareService().detect() |
server/src/app.ts |
server/src/routes/hardware.ts |
app.use("/api", hardwareRoutes()) |
✓ WIRED | Line 132: app.use("/api", hardwareRoutes()) — before const api = Router() at line 135 |
server/src/services/ollama.ts |
server/src/data/ollama-model-catalog.json |
loadCatalog() |
✓ WIRED | loadCatalog() exists in ollama.ts (line 79), reads catalog JSON by resolved __dirname path |
ui/src/hooks/useHardwareInfo.ts |
/api/system/providers |
fetch in useQuery | ✓ WIRED | Hook calls fetchHardwareInfo() from api/hardware.ts which calls api.get("/system/providers") |
ui/src/components/onboarding/HardwareSummaryStep.tsx |
ui/src/hooks/useHardwareInfo.ts |
useHardwareInfo hook | ✓ WIRED | Props flow: hardwareInfo, isLoading, isError from useHardwareInfo() in wizard, passed down to HardwareSummaryStep |
ui/src/components/NexusOnboardingWizard.tsx |
ui/src/api/hardware.ts |
updateNexusSettings on wizard complete | ✓ WIRED | Lines 180-184: await updateNexusSettings({ mode: selectedMode }) |
ui/src/components/NexusOnboardingWizard.tsx |
ui/src/components/onboarding/ModeSelector.tsx |
React component composition | ✓ WIRED | Line 250: <ModeSelector value={selectedMode} onChange={setSelectedMode} /> |
Data-Flow Trace (Level 4)
| Artifact | Data Variable | Source | Produces Real Data | Status |
|---|---|---|---|---|
HardwareSummaryStep.tsx |
hardwareInfo |
useHardwareInfo() → fetchHardwareInfo() → GET /api/system/providers → hardwareService().detect() → os.totalmem(), os.freemem(), si.graphics() |
Yes — live OS calls | ✓ FLOWING |
ModeSelector.tsx |
value: NexusMode |
useState<NexusMode>("both") in wizard, updated by onChange |
Yes — interactive state | ✓ FLOWING |
NexusOnboardingWizard.tsx (persist) |
selectedMode |
useState<NexusMode>("both") → updateNexusSettings({ mode: selectedMode }) → PATCH /api/nexus/settings → nexusSettingsService().set() → writeFileSync |
Yes — writes to data/nexus-settings.json |
✓ FLOWING |
Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|---|---|---|---|
| All 13 hardware tests pass | npx vitest run src/__tests__/30-hardware-detection.test.ts |
13/13 passed (308ms) | ✓ PASS |
| Model catalog: all variants have tier | node -e "..." catalog check |
Total: 12, Missing tier: 0, Has qwen3: true | ✓ PASS |
| Hardware routes mounted before boardMutationGuard | grep lines 131-136 of app.ts | app.use("/api", hardwareRoutes()) at line 132, const api = Router() at line 135 |
✓ PASS |
| TypeScript compilation (UI) | pnpm --filter ui exec tsc --noEmit |
1 pre-existing error in AgentConfigForm.tsx (detectModel — introduced by prior Hermes commit 1583a2d6, not by phase 30; phase 30 files have 0 TS errors) |
⚠️ PRE-EXISTING |
Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|---|---|---|---|---|
| ONBD-01 | 30-01, 30-02 | User can select mode (Personal AI Assistant / Project Builder / Both) during onboarding | ✓ SATISFIED | ModeSelector.tsx three-card UI; NexusOnboardingWizard.tsx persists via updateNexusSettings |
| ONBD-02 | 30-01 | System auto-detects GPU, RAM, and Apple Silicon unified memory within 5 seconds | ✓ SATISFIED | hardwareService().detect() with 3s GPU timeout; HardwareSummaryStep displays results; server + React Query retry:1 ensures < 5s total |
| ONBD-03 | 30-01 | System recommends best local model from pre-built JSON database based on detected hardware | ✓ SATISFIED | Catalog extended with tier arrays; getRecommendedModel(models, ram, hardwareTier) filters by tier; GET /api/system/providers/recommendation endpoint returns tier-filtered catalog |
| ONBD-07 | 30-02 | Local AI framed as privacy premium ("runs entirely on your machine, no accounts, works offline") | ✓ SATISFIED | HardwareSummaryStep.tsx lines 85-92: "Local AI (recommended for privacy)" + "Runs entirely on your machine. No accounts. No tracking. Works offline." shown when hardwareTier !== "cpu_only" |
All 4 requirement IDs from PLAN frontmatter are accounted for. No orphaned requirements found for this phase.
Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|---|---|---|---|---|
ui/src/components/AgentConfigForm.tsx |
351 | agentsApi.detectModel — property does not exist on agentsApi type (TS2339) |
⚠️ Warning | Pre-existing error from commit 1583a2d6 (Hermes adapter upgrade); not introduced by phase 30; does not affect hardware/mode-selection functionality |
No stub patterns, empty returns, placeholder comments, or disconnected data paths found in any phase 30 artifacts.
Human Verification Required
1. Visual Walkthrough of 3-Step Onboarding Wizard
Test: Start dev server (pnpm dev in /opt/nexus), open browser to http://localhost:3100. Trigger the onboarding wizard. Walk through all 3 steps.
Expected:
- Step 1: Shows "Detecting your hardware..." heading with skeleton rows while probe is in-flight, transitions to "Your hardware" with real stats. Continue button always enabled.
- Step 2: "Choose your mode" heading. Three vertically-stacked cards: "Personal AI Assistant", "Project Builder", "Both (recommended)". "Both" pre-selected with blue border (
border-primary bg-primary/5). Clicking other cards moves selection. Step indicator shows "Step 2 of 3". Back button present. - Step 3: Existing root directory form preserved. "Welcome to [appName]" heading. Back button works (returns to step 2). Get Started submits and closes wizard. Why human: Visual rendering, interaction state, and card selection styling cannot be verified programmatically.
2. Privacy Frame for GPU/Apple Silicon Tiers
Test: Run on a machine with a GPU (>= 4GB VRAM) or Apple Silicon. Complete step 1 of onboarding wizard.
Expected: Below the hardware stats, "Local AI (recommended for privacy)" heading appears with "Runs entirely on your machine. No accounts. No tracking. Works offline." body text. This text should NOT appear on CPU-only machines.
Why human: Hardware tier depends on the runtime machine. The test environment (Linux, no discrete GPU) only exercises the cpu_only path.
3. Mode Persistence to Disk
Test: Complete the full onboarding wizard with "Personal AI Assistant" selected. After wizard closes, check ~/.paperclip/<instance>/data/nexus-settings.json (or wherever resolvePaperclipInstanceRoot() resolves).
Expected: File contains { "mode": "personal_ai" }.
Why human: Requires a full running server instance and end-to-end wizard submission.
Gaps Summary
No automated gaps found. All 11 must-have truths are verified. All 11 artifacts are substantive and wired. All 4 key links are confirmed. All 4 requirement IDs (ONBD-01, ONBD-02, ONBD-03, ONBD-07) are satisfied.
The sole TS error (AgentConfigForm.tsx detectModel) is pre-existing and introduced by commit 1583a2d6 prior to phase 30. Phase 30 did not touch AgentConfigForm.tsx (confirmed by git log -- ui/src/components/AgentConfigForm.tsx showing no phase-30 commits).
3 items are routed to human verification: the visual wizard flow, privacy frame display on capable hardware, and mode persistence end-to-end.
Verified: 2026-04-02T23:58:20Z Verifier: Claude (gsd-verifier)