# Lumotia v0.1 release-completion implementation plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan. Each task below is dispatched to a fresh sonnet subagent with a self-contained brief, then verified before checking off. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Complete every code-side item in `docs/release/v0.1-checklist.md` and `docs/release/v0.1-ui-hardening.md`. Document everything that fundamentally requires the human (signing certs, real-hardware probes, smoke-test on platforms we don't have, tester recruitment) so the user can pick those up immediately after this session.

**Architecture:** Three layers preserved (Svelte UI → Tauri commands → Rust crates). New work lands as: SQLite migration v17 (`onboarding_events`, `lumotia_events`), one new Tauri commands module (`commands/onboarding.rs`), new Svelte components (`StatusPill.svelte`, `PostCaptureCard.svelte`, settings restructure), surgical edits to `FirstRunPage.svelte` / `DictationPage.svelte` / `SettingsPage.svelte` / `+layout.svelte`, and release-doc additions (CHANGELOG.md, release notes, privacy disclosure page, README updates).

**Tech Stack:** Tauri 2 + Svelte 5 runes + Rust workspace + SQLite via sqlx 0.8 + FTS5. No new dependencies introduced.

---

## Baseline verified before starting (2026-05-14 22:32)

- ✅ `cargo check --workspace --all-targets` clean
- ✅ `npm run check` — 4015 files / 0 errors / 0 warnings
- ✅ `npm run test` — 13/13 vitest passing
- ✅ `cargo fmt --check` clean
- ✅ `scripts/dogfood-rebrand-drill.sh` exists + executable
- Not yet run on baseline: `cargo test --workspace`, `cargo clippy -D warnings`, dogfood drill (will run after Tier 1+2)

---

## Recon findings — what's already done vs missing

### ✅ Already complete (verify + tick only)
- Trust + security boundary: MCP read-only (`crates/mcp/src/main.rs:18` uses `init_readonly`); MCP stdio-only (no TCP listener); `lumotia-cloud-providers` has zero `#[tauri::command]`; `npm audit signatures` runs in `run.sh:30`.
- LLM cleanup + tag extraction failure paths preserve raw transcript (`crates/ai-formatting/src/llm_client.rs:142-159`, `src-tauri/src/commands/llm.rs:422-439`).
- Design-system preview catalog: 20 HTML files under `src/design-system/preview/` (matches doc inventory).
- `prefers-reduced-motion`: respected in CompletionSparkline, ToastViewport, SettingsGroup, TasksPage, DictationPage, app.css.
- Tauri commands invoke handler: 95+ commands wired in `src-tauri/src/lib.rs:678+`.
- `KNOWN-ISSUES.md`: KI-01 → KI-06 + RB-08 entries present.
- `rust-toolchain.toml`: pinned to 1.94.1.
- `dogfood-rebrand-drill.sh`: exists + executable.
- `FirstRunPage.svelte` (348 lines): exists with skip option (lines 339-345) and probe/download flows.
- Versions synced (3-of-4): `src-tauri/Cargo.toml` = `package.json` = `tauri.conf.json` = `0.1.0`. Workspace `Cargo.toml` lacks an explicit version field — decide policy in Tier 2.

### 🔧 Partial (needs targeted fix)
- First-run gate (`src/routes/+layout.svelte:345-353`) — gates on model-presence, should also check `onboarding_events` record.
- Recording-as-sacred-state — `page.recording` boolean is tracked but nav doesn't simplify during recording.
- Error-state copy — error strings exist but leak raw `err.message` (e.g. `SettingsPage.svelte` audio/vocabulary/diagnostic errors); needs plain-language sweep.
- Keyboard shortcuts + focus ring — hotkey infrastructure exists, but `:focus-visible` coverage is minimal in `app.css`, textarea uses `focus:outline-none` (line 1058 of DictationPage), no Ctrl+K / Esc-closes-modal documented.
- Task-extraction LLM call (`crates/llm/src/lib.rs:525-554`) — returns Err on parse failure with no rule-based fallback. Verify whether a separate frontend regex extractor exists; if not, add backend fallback.
- `how-lumotia-is-built.md` exists at `docs/release/` but is not linked from `README.md`.
- README mentions install paths + first-run but does not brand v0.1 as the launch release or link the GitHub issues URL explicitly.

### ❌ Missing entirely (build from scratch)
- `StatusPill.svelte` Svelte component + `components-status-pills.html` preview entry + app-wide integration (Ready / Recording / Paused / Transcribing / Cleaning / Extracting tasks / Saved / Exported / Needs review / Failed safely).
- Post-capture card component — surfaces after every recording with raw transcript / cleaned / extracted tasks / MicroSteps / Save / Export / Start-first-MicroStep / Open-in-History.
- `onboarding_events` SQLite table (`completed_at`, `skipped`, `version`) + migration v17.
- `lumotia_events` SQLite table for activation log.
- `src-tauri/src/commands/onboarding.rs` Tauri commands module.
- Settings → Help section with manual-launch tutorial trigger.
- Pre-supplied test-recording prompt step in FirstRunPage.
- Settings 6-section sanity pass: current 7-group structure (Audio / Vocabulary / Transcription / AI & Processing / Tasks & Rituals / Output & Capture / Appearance & System) does not match v0.1 spec (Start Here / Transcription / Models / Tasks / Accessibility / Privacy / Advanced).
- `CHANGELOG.md` (does not exist at repo root).
- Release notes draft (does not exist).
- Privacy + AI-use disclosure page (does not exist).
- AppImage SHA-256 publication in `.github/workflows/build.yml`.
- Per-platform "first install warning you may see" doc.
- Diagnostic bundle command (logs + system info + redacted preferences, skip transcripts/audio).
- `Settings → Diagnostics → Activation log` UI surfacing the local activation events.
- LLM hang timeout wrap (per known-limitations soft edge — confirm if v0.1 must-fix or v0.2 deferral).
- npm dev deps exact-pin sweep (10 ^/~ ranges remaining in `devDependencies`).
- Workspace `Cargo.toml` version-field policy decision.

### 👤 Human-required (cannot complete in this session — Tier 4 will document)
- Windows code-signing certificate procurement + wiring secrets into `.github/workflows/build.yml`.
- macOS notarisation with Apple Developer ID + secrets wiring.
- App Nap real-hardware verification on Apple Silicon (`RB-08`).
- Manual smoke-test matrix on 5 platforms (Linux Fedora, Linux Ubuntu LTS, macOS Apple Silicon, macOS Intel, Windows 11).
- 10-step tester acceptance flow personally on Linux.
- Recruiting 20 testers + private-beta activation metrics + public-launch metrics.
- Decision recorded for KI-02 (Linux idle inhibit) and KI-03 (Windows sleep prevention) — fix-if-tiny vs document.

---

## Tier 1 — Foundation (sequential, must complete before Tier 2 parallels)

### Task 1.1: SQLite migration v17 — onboarding_events + lumotia_events tables

**Files:**
- Modify: `crates/storage/src/migrations.rs` (add migration v17)
- Reference: `crates/storage/src/lib.rs` (storage entry point — confirm migration registration pattern)

**Brief for subagent:**
Add migration v17 to `crates/storage/src/migrations.rs`. Two tables:

```sql
-- onboarding_events: gates first-run, supplies time-to-first-capture metric
CREATE TABLE IF NOT EXISTS onboarding_events (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    event TEXT NOT NULL,                    -- 'started' | 'permissions_granted' | 'model_ready' | 'test_recording' | 'cleaned_transcript_seen' | 'completed' | 'skipped'
    completed_at INTEGER NOT NULL,          -- unix epoch seconds
    version TEXT NOT NULL,                  -- onboarding flow version, e.g. '0.1.0'
    skipped INTEGER NOT NULL DEFAULT 0,     -- 1 if step skipped, 0 if completed
    notes TEXT                              -- optional free-text; e.g. selected model name, error reason
);
CREATE INDEX IF NOT EXISTS idx_onboarding_events_event ON onboarding_events(event);

-- lumotia_events: opt-in local activation log (Settings → Diagnostics → Activation log surface)
CREATE TABLE IF NOT EXISTS lumotia_events (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    kind TEXT NOT NULL,                     -- 'first_capture' | 'first_export' | 'first_search' | 'first_task_extract' | 'capture_completed' | 'task_extracted'
    occurred_at INTEGER NOT NULL,           -- unix epoch seconds
    payload TEXT                            -- JSON blob, optional, NEVER includes transcript text
);
CREATE INDEX IF NOT EXISTS idx_lumotia_events_kind ON lumotia_events(kind);
CREATE INDEX IF NOT EXISTS idx_lumotia_events_occurred ON lumotia_events(occurred_at);
```

Match the existing migration registration pattern (look at v16 to see how migrations are added to the migration list). Add a unit test in the same file pattern as existing migration tests that opens an in-memory SQLite, runs migrations through v17, and confirms both tables exist.

Run `cargo test -p lumotia-storage` and report pass/fail. If fail, fix and re-run.

---

### Task 1.2: Onboarding Tauri commands module

**Files:**
- Create: `src-tauri/src/commands/onboarding.rs`
- Modify: `src-tauri/src/commands/mod.rs` (register new module)
- Modify: `src-tauri/src/lib.rs` (register commands in invoke handler)

**Brief for subagent:**
Create `src-tauri/src/commands/onboarding.rs` exposing these `#[tauri::command]` functions, each calling into `lumotia-storage`:

- `record_onboarding_event(event: String, version: String, skipped: bool, notes: Option<String>) -> Result<(), String>`
- `list_onboarding_events() -> Result<Vec<OnboardingEvent>, String>`
- `has_completed_onboarding() -> Result<bool, String>` — returns true if any row with event='completed' or event='skipped' exists
- `record_lumotia_event(kind: String, payload: Option<String>) -> Result<(), String>`
- `list_lumotia_events() -> Result<Vec<LumotiaEvent>, String>`
- `clear_lumotia_events() -> Result<(), String>` — for the user opt-out path

Define `OnboardingEvent` and `LumotiaEvent` as `serde::Serialize` structs in the same file. Use `chrono::Utc::now().timestamp()` for occurred_at. Keep functions thin — the storage helpers do the SQL work.

Add the corresponding helpers to `crates/storage/src/lib.rs` (or an `events.rs` module): `insert_onboarding_event`, `list_onboarding_events`, `has_completed_onboarding`, `insert_lumotia_event`, `list_lumotia_events`, `clear_lumotia_events`. Use the existing `Storage` struct's connection pool.

Add unit tests in the storage crate for each helper (in-memory SQLite, insert + retrieve round-trip).

Register the module in `src-tauri/src/commands/mod.rs` and add the six commands to the `tauri::generate_handler!` macro list in `src-tauri/src/lib.rs`.

Run `cargo build -p lumotia` and `cargo test -p lumotia-storage`. Both must pass.

---

### Task 1.3: StatusPill Svelte component + preview entry

**Files:**
- Create: `src/lib/components/StatusPill.svelte`
- Create: `src/design-system/preview/components-status-pills.html`

**Brief for subagent:**
Create a `StatusPill.svelte` component using Svelte 5 runes. Props (use `$props()`):

```ts
type Status =
  | 'ready'
  | 'recording'
  | 'paused'
  | 'transcribing'
  | 'cleaning'
  | 'extracting-tasks'
  | 'saved'
  | 'exported'
  | 'needs-review'
  | 'failed-safely';

let { status, label = undefined } = $props<{ status: Status; label?: string }>();
```

Render a pill (rounded-full span) with:
- Plain-text label (use the prop, fall back to a default map: `ready → "Ready"`, `recording → "Recording"`, `paused → "Paused"`, `transcribing → "Transcribing"`, `cleaning → "Cleaning"`, `extracting-tasks → "Extracting tasks"`, `saved → "Saved"`, `exported → "Exported"`, `needs-review → "Needs review"`, `failed-safely → "Failed safely"`).
- A small leading dot (color-coded but never the only signal — the literal label is always present).
- Color tokens from `src/design-system/preview/colors-semantic.html` — read that file to pick the right semantic colour per state. (`ready` → neutral, `recording` → accent/danger, `paused` → warning, `transcribing/cleaning/extracting-tasks` → progress, `saved/exported` → success, `needs-review/failed-safely` → warning/danger).
- `aria-live="polite"` so screen readers announce state changes.
- Honour `prefers-reduced-motion` — no animation on the dot if the media query matches.

Create `src/design-system/preview/components-status-pills.html` mirroring the structure of the existing `components-toasts.html` and `components-buttons.html` files in the same directory: render every status with its label so the catalogued surface is visible.

Do NOT integrate into the app yet — that happens in Tier 2 task 2.5.

Smoke-test by running `npm run check` — must stay 0 errors / 0 warnings.

---

## Tier 2 — Independent UI + onboarding + LLM work (parallelisable after Tier 1)

### Task 2.1: First-run gate fix + tutorial relaunch from Settings → Help

**Files:**
- Modify: `src/routes/+layout.svelte` (gate logic around lines 345-353)
- Modify: `src/lib/pages/SettingsPage.svelte` (add Help section with "Replay first-run tutorial" button)
- Modify: `src/lib/pages/FirstRunPage.svelte` (call `record_onboarding_event` on each completion)

**Brief for subagent:**
Two changes:

1. **First-run gate** in `src/routes/+layout.svelte`: after the existing model-presence check around lines 345-353, ALSO call `invoke<boolean>('has_completed_onboarding')`. If `true`, do NOT route to first-run regardless of model state — that's the migration-aware bypass. If `false` AND no models, route to first-run as today.

2. **FirstRunPage** (`src/lib/pages/FirstRunPage.svelte`): after each successful step in the existing flow (probe success, model download success, ready-screen reached, skip), call `invoke('record_onboarding_event', { event, version: '0.1.0', skipped, notes })` with the appropriate event name. Use the event vocabulary from migration v17 (`started`, `permissions_granted`, `model_ready`, `test_recording`, `cleaned_transcript_seen`, `completed`, `skipped`). On the final success path call `record_onboarding_event` with `event='completed'`. On the skip path call with `event='skipped'`.

3. **SettingsPage Help section**: add a new top-level section titled "Help" near the bottom of the settings list. One button: "Replay first-run tutorial". On click, set a session flag and navigate to first-run (`page.current = 'first-run'`). The flag tells FirstRunPage to skip the migration-bypass.

Do NOT do the full settings 6-section regroup here — that's Task 2.4. Just add the Help section as-is.

Run `npm run check` — must stay 0 errors / 0 warnings. Run `cargo build -p lumotia` to make sure the Rust side still compiles.

---

### Task 2.2: Pre-supplied test-recording prompt + failure recovery in FirstRunPage

**Files:**
- Modify: `src/lib/pages/FirstRunPage.svelte`

**Brief for subagent:**
Add two improvements to FirstRunPage:

1. **Pre-supplied test-recording prompt step** — between the existing "model ready" state and the "rituals" preference prompts, add a "Try a test recording" step. Show the user a short, neutral pre-supplied sentence to read aloud, e.g.:

   > "Try saying: 'Today is a good day to test my microphone and see how the transcription looks.'"

   Render a record button (reuse the existing toggleRecording wiring from DictationPage if extractable, otherwise inline a minimal recorder). After a successful test recording, call `record_onboarding_event` with `event='test_recording'`, then proceed to a "Here's your cleaned transcript" preview step (event='cleaned_transcript_seen'). User clicks "Looks good" to continue.

2. **Failure recovery** — every existing error path (probe failure, download failure, test-recording failure) must show: error description in plain words, a **Retry** button that re-runs the same step, and a **Skip this step** secondary button that records the event with `skipped: true` and proceeds. No dead-end "something went wrong" with no buttons.

Run `npm run check` and `npm run test`. Both must stay green.

---

### Task 2.3: Post-capture card component + integration into capture flow

**Files:**
- Create: `src/lib/components/PostCaptureCard.svelte`
- Modify: `src/lib/pages/DictationPage.svelte` (render the card after recording stops)

**Brief for subagent:**
Create `PostCaptureCard.svelte` (Svelte 5 runes). Props:

```ts
type Props = {
  transcriptId: string;
  rawTranscript: string;
  cleanedTranscript: string;
  cleanedSource: 'llm' | 'rule-based';   // labels which path produced the cleaned version
  extractedTasks: string[];
  microSteps?: string[];                  // surfaced when a task is selected
  onSelectTask?: (idx: number) => void;
  onExport?: () => void;
  onStartFirstMicroStep?: () => void;
  onOpenInHistory?: () => void;
};
```

Render as a card (reuse classes from `src/design-system/preview/components-cards.html`):
- Heading: "Captured — saved" with a `<StatusPill status="saved" />` to the right.
- Cleaned transcript prominent; raw transcript collapsible (`<details><summary>Show raw transcript</summary>`).
- A label "(cleaned by local model)" or "(cleaned by rules)" depending on `cleanedSource`.
- Extracted tasks as a list; clicking one calls `onSelectTask` and reveals MicroSteps.
- Action buttons: Save (already happened — show as confirmation `<StatusPill status="saved" />`), Export, Start first MicroStep (only enabled if microSteps present), Open in History.
- DO NOT include: suggested title, suggested folder/project/area/person/topic, possible-links, accept/edit/park/archive, confidence scores. Those are v0.2 (per `docs/release/v0.1-ui-hardening.md` "post-capture card display-only" boundary).

Integrate into `DictationPage.svelte`: after a recording stops and cleanup completes, show the card below or in place of the textarea. Wire the action props to existing functions where they exist (export → existing export flow, openInHistory → router push to `/history?id=...`).

Also: on first successful capture, fire `invoke('record_lumotia_event', { kind: 'first_capture' })` — but only if no `first_capture` event already exists. (Cheap check: read activation events on app start, cache.)

Run `npm run check` and `npm run test`. Both must stay green.

---

### Task 2.4: Settings 6-section sanity pass + Help section integration

**Files:**
- Modify: `src/lib/pages/SettingsPage.svelte`

**Brief for subagent:**
Restructure SettingsPage.svelte's section grouping to match the v0.1 spec from `docs/release/v0.1-ui-hardening.md` section 5 — preserve every existing setting, just regroup. Final order (top to bottom):

1. **Start Here** — model picker, microphone, language
2. **Transcription** — engine choice, cleanup level, custom vocabulary preview
3. **Models** — download, switch, disk-space readout
4. **Tasks** — energy-aware sequencing toggles, WIP limit
5. **Accessibility** — `prefers-reduced-motion`, contrast, typography size, screen-reader hints
6. **Privacy** — local-only badge, AI-use disclosure link (link to `/docs/privacy.md` once Task 3.3 lands; for now use `/docs/release/how-lumotia-is-built.md`), local activation log toggle, data-dir location
7. **Advanced** — everything else, hidden under a click (collapsed by default)
8. **Help** (added in Task 2.1, keep at bottom) — replay first-run tutorial, link to known-limitations doc, link to GitHub issues

Existing settings to relocate (current → new section):
- Audio settings → split: device picker → Start Here, advanced gain/sample-rate → Advanced
- Vocabulary → Transcription
- AI & Processing → split: cleanup-level toggle → Transcription, model-warmup toggles → Models
- Tasks & Rituals → split: WIP/sequencing → Tasks, ritual settings → Advanced
- Output & Capture → Advanced (paste matrix, hotkey)
- Appearance & System → split: contrast/motion/typography → Accessibility, theme/window opacity → Advanced

The full progressive-disclosure regroup with search box is **deferred to v0.2** per the boundary doc — this pass is "the basics are findable", NOT "every setting is grouped beautifully". Do not invent new settings or remove existing ones. Just move them.

Add a one-line `<p>` under "Privacy": "Local activation log: this is stored on your device only and never sent anywhere. You can clear it any time." With a "Clear activation log" button wired to `invoke('clear_lumotia_events')`.

Run `npm run check` — must stay 0 errors / 0 warnings.

---

### Task 2.5: StatusPill app-wide integration

**Files:**
- Modify: `src/lib/pages/DictationPage.svelte` (replace status string + colour with StatusPill)
- Modify: any sidebar status chip component (recon noted `LlmStatusChip` — find it and integrate, or replace with StatusPill)
- Modify: `src/lib/components/PostCaptureCard.svelte` (already wired in Task 2.3 but verify)
- Modify: any other surface that surfaces async state strings — sweep with `rg "Recording\.\.\.|Cleaning|Transcribing|Saved|Exported"` and replace where appropriate

**Brief for subagent:**
Sweep the frontend for every place async/transcription state is surfaced as a hand-rolled label or coloured dot. Replace each with `<StatusPill status="..." />`. The Recon turned up these surfaces explicitly — start there:

- `src/lib/pages/DictationPage.svelte` lines 84-311 — `page.status` string + `page.statusColor` hex pairs
- Any `LlmStatusChip.svelte` or `StatusChip.svelte` in `src/lib/components/`
- `src/lib/pages/HistoryPage.svelte` if it surfaces per-row state (check)
- Toast text where async state is being narrated to the user

Map the old status strings to the new vocabulary:
- "Ready" → `ready`
- "Recording..." or "Recording" → `recording`
- "Loading model..." or "Transcribing..." → `transcribing`
- "Cleaning up..." → `cleaning`
- "Extracting tasks..." → `extracting-tasks`
- "Saved" → `saved`
- "Exported" → `exported`
- "Error" + raw err.message → `failed-safely` and the err.message goes into a sibling explainer line (not into the pill)
- "Paused" → `paused`

Preserve any custom user-visible label by passing `label={...}` if the existing copy is more specific than the default.

Run `npm run check`. Must stay 0 errors / 0 warnings. Do a manual visual check by running `npm run dev:frontend` if helpful.

---

### Task 2.6: Recording-as-sacred-state — nav simplification during active recording

**Files:**
- Modify: the layout file that renders the secondary nav (likely `src/routes/+layout.svelte` or `src/lib/components/Sidebar.svelte` — find via `rg "History" src/`)
- Modify: `src/lib/stores/page.svelte.ts` (expose `recording` state if not already exposed for layout consumption)

**Brief for subagent:**
When `page.recording === true`, the sidebar/secondary nav must visibly de-emphasise (greyed, lower opacity, non-interactive — but NOT removed from DOM, so screen-reader users can still navigate).

Specifically during recording:
- Settings / History / Tasks nav items: `opacity-30 pointer-events-none aria-disabled="true"`
- Only-visible primary action: the recording timer and Pause / Stop / Cancel buttons
- Cancel must show a confirm prompt before discarding

Add a small fade transition (200ms) wrapped in `prefers-reduced-motion` so it instantly snaps for users who opt out.

DO NOT remove DOM nodes — accessibility intent is "de-emphasise visually" not "hide from assistive tech". Use `aria-disabled` and `tabindex="-1"` to remove from tab order while recording.

Run `npm run check`. Must stay clean.

---

### Task 2.7: Home capture clarity — big record button + status pill + last-capture preview

**Files:**
- Modify: `src/lib/pages/DictationPage.svelte`

**Brief for subagent:**
Apply the v0.1-ui-hardening.md section 1 "Home capture clarity" mantra to DictationPage.svelte. Within the existing structure, ensure:

1. **Big record button** is the visually dominant element on initial render — minimum 80px diameter, centred or top-of-content, primary CTA colour. Visible within 1 second of landing on Home (no hover, no scroll required).

2. **Status pill** rendered prominently near the record button using `<StatusPill status="..." />` (already wired in Task 2.5).

3. **Profile + model summary** — single read-only line near the top: `<small>Profile: {{name}} · Model: {{model}}</small>` so the user can see what they're capturing as.

4. **Last-capture preview** — collapsed card showing: the last cleaned transcript (first 80 chars + ellipsis), timestamp, click expands or jumps to History. Hidden if no captures exist.

5. No more than 3 visible secondary CTAs at any time. If there are more than 3, fold the rest behind a "More" disclosure.

Don't redesign the page — additive hardening only. Existing template-selector / live-warning / save confirmation stay. Just hoist the primary action and add the preview.

Run `npm run check` and `npm run test`. Both must stay green.

---

### Task 2.8: Error-state copy sweep

**Files:**
- Sweep the frontend with `rg -n "Error|err\.message|error\.message|Could not|Failed" src/` and address every visible-to-user surface

**Brief for subagent:**
Sweep every error surface in `src/` and rewrite to match this contract (per `docs/release/v0.1-ui-hardening.md` section 6):

1. **Preserve raw transcript** — any error path triggered during/after a recording must NOT clear the textarea or post-capture card data. If the failing function returns an error, surface the error but leave the captured data intact.

2. **Plain words** — replace raw `err.message` exposure with a human sentence + the raw cause folded into a `<details>` for the curious. Example transformation:

   - Before: `error = 'Could not enumerate audio devices: ' + err.message`
   - After: `error = 'We couldn't list your audio devices. Plug a microphone in or grant Lumotia microphone permission, then try again.'` with `<details><summary>Technical details</summary>{err.message}</details>` underneath.

3. **Tell the user what to do next** — every error surface ends with one concrete next action: "Try again", "Continue without LLM", "Open Settings → Privacy", "See known limitations".

4. **No stack traces in the user-facing surface** — stack traces go to the existing crash dump / log files only.

Use `<StatusPill status="failed-safely" />` next to the explainer when the data was preserved through the failure (LLM cleanup error, task-extract error, tag-extract error). Use `<StatusPill status="needs-review" />` when the user has to act.

Files known to need rewrites (from recon):
- `src/lib/pages/DictationPage.svelte` — recording / transcription error surfaces
- `src/lib/pages/SettingsPage.svelte` — `audioDevicesError`, `vocabularyError`, `diagnosticReportError`, `ttsVoicesError`
- Toast components — review every toast call site

Run `npm run check` and `npm run test`. Both must stay green. Run `rg "err\.message" src/` after the sweep — every remaining match should be inside a `<details>` block, not in primary copy.

---

### Task 2.9: Keyboard flow + focus ring restoration + 10-step keyboard test

**Files:**
- Modify: `src/app.css` (broaden `:focus-visible` coverage)
- Modify: `src/lib/pages/DictationPage.svelte` (remove `focus:outline-none` on textarea, line ~1058)
- Modify: layout/keybinding registration — find existing hotkey wiring, add Ctrl+K (search) and Esc (close modal) bindings
- Modify: any modal components — confirm Esc closes them

**Brief for subagent:**
Per v0.1-ui-hardening.md section 7 the entire 10-step tester acceptance flow must be completable via keyboard alone.

Concrete changes:

1. **Focus ring** — in `src/app.css`, add a global `:focus-visible` rule that's visible on every interactive element at default zoom. Use the existing accent token. Do NOT use `:focus` (always-on) — `:focus-visible` only fires for keyboard focus, which is what we want. Remove any `focus:outline-none` Tailwind utility on visible interactive elements (textarea on DictationPage line ~1058 is the known violator — sweep for others).

2. **Ctrl+K opens search** — wire a global keyboard listener in the root layout. On Ctrl+K (or ⌘+K on macOS), navigate to History and focus the search input. If already on History, focus the search input.

3. **Escape closes modal** — every dialog/modal component must respond to Escape with close. Check existing modals (template menu, ritual config, etc.) and add the listener if missing.

4. **Open Settings from anywhere** — Ctrl+, (or ⌘+,) navigates to Settings.

5. **Recording start/stop** — already wired via `lumotia:toggle-recording` event. Verify the keyboard binding is configurable in Settings → Advanced (or Help) and labelled there.

6. **Arrow keys through MicroSteps** — in PostCaptureCard.svelte, when MicroSteps are visible, ↑/↓ move focus between them, Enter starts the timer on the focused MicroStep.

7. **No `:hover`-only controls** — sweep `rg "group-hover|hover:" src/` and verify every hover-only affordance has a keyboard equivalent (focus-visible variant or always-visible).

After implementing, manually walk the 10-step flow with keyboard only in `npm run dev:frontend` if helpful; otherwise `npm run check` clean is the gate.

---

### Task 2.10: Task-extraction LLM rule-based fallback

**Files:**
- Modify: `crates/llm/src/lib.rs` (around `extract_tasks_with_feedback`, lines 525-554)
- Modify: `crates/llm/src/lib.rs` (add a `rule_based_extract_tasks` function or move existing if present)
- Modify: `src-tauri/src/commands/tasks.rs` (line 350-375 — fall back to rule-based on LLM error)

**Brief for subagent:**
Per `docs/release/v0.1-known-limitations.md` ("AI cleanup + extraction failure modes" — Task extraction throws → rule-based regex+verb-list extractor takes over), the contract is that task extraction NEVER returns no tasks just because the LLM failed.

First, recon: search the codebase for an existing rule-based task extractor. `rg -n "extract_tasks|rule_based|verb_list|imperative" crates/ src-tauri/`. If one exists, wire it in. If not, build a minimal one in `crates/llm/src/lib.rs`:

```rust
pub fn rule_based_extract_tasks(transcript: &str) -> Vec<String> {
    // Split on sentence boundaries (. ? ! and newlines).
    // Keep sentences that start with an imperative verb (need to / I need to / let me / I should /
    // remember to / don't forget to / make sure / call / send / write / fix / update / review).
    // Cap at 10 to avoid wall-of-text.
    // Trim, dedupe.
}
```

Then in `extract_tasks_with_feedback` (or wherever the cmd-side calls happen, around `commands/tasks.rs:350-375`): on `Err` from the LLM call, log a warning, call `rule_based_extract_tasks(&transcript)`, return those. Surface to the frontend with a flag so the UI can label them "(rule-based)" — extend the existing return type with an `extracted_via: 'llm' | 'rule-based'` field if helpful, or use a sentinel prefix in the strings.

Add unit tests: a transcript containing "I need to send Sarah the report tomorrow. Don't forget the slide deck." should produce 2 rule-based tasks.

Run `cargo test -p lumotia-llm` and `cargo test --workspace`. Both must pass.

---

### Task 2.11: LLM hang timeout wrap (decision: v0.1 vs v0.2)

**Decision needed:** The `v0.1-known-limitations.md` documents the LLM-hang case as a v0.2 hygiene track item ("the only soft edge"). Two options:

- **Option A (v0.1 fix):** Wrap the LLM cleanup / extraction calls in a `tokio::time::timeout(Duration::from_secs(120), ...)`. On timeout, propagate to the rule-based fallback. Status pill shows `failed-safely`.
- **Option B (v0.2 deferral):** Leave the soft edge as documented. No code change in this task.

**Default choice for this plan:** Option A. The 120s timeout is a one-line change per call site and closes a real user-trust issue with no architectural risk. If the user prefers Option B, this task becomes a no-op + a tick on the known-limitations entry.

**Files (if Option A):**
- Modify: `src-tauri/src/commands/llm.rs` (cleanup_text command + extract_tasks command + extract_content_tags command)

**Brief for subagent (Option A):**
For each of the three LLM Tauri commands (cleanup_text, extract_tasks_cmd, extract_content_tags_cmd) wrap the LLM call in `tokio::time::timeout(Duration::from_secs(120), ...)`. On timeout, return `Err("LLM call timed out — using fallback or preserved input.")`. The frontend already receives this and surfaces via Task 2.8 error-copy contract.

For `extract_tasks_cmd`, after the timeout fires, call `rule_based_extract_tasks` from Task 2.10 and return those tasks instead of an error.

Run `cargo build -p lumotia` and `cargo test --workspace`.

---

## Tier 3 — Release artefacts + docs (parallelisable, can start anytime)

### Task 3.1: CHANGELOG.md seeded with Phase 1-8 outcomes

**Files:**
- Create: `CHANGELOG.md` (repo root)

**Brief for subagent:**
Create `CHANGELOG.md` at repo root following Keep a Changelog format. Seed it with the v0.1.0 entry written in end-user voice (NOT commit-log style). Read recent commits via `git log --oneline -50` and `docs/release/how-lumotia-is-built.md` to harvest material, then write 5-10 bullet points under sections:

```
# Changelog

All notable user-facing changes to Lumotia are documented here.
Format: Keep a Changelog. Versioning: SemVer.

## [Unreleased]

## [0.1.0] - 2026-MM-DD

### Added
- Local-first dictation with Whisper or Parakeet on your device
- Automatic transcript cleanup (rule-based + optional local LLM)
- Task extraction with rule-based fallback when the LLM is unavailable
- MicroSteps + 5-minute focus timer
- History with full-text search
- Markdown export with frontmatter (single + bulk + collision-suffixing)
- Read-only MCP server (lumotia-mcp) for connecting external agents to your transcript history
- First-run onboarding flow with optional skip
- ...

### Privacy
- All transcription, cleanup, and task extraction runs on your device.
- No telemetry. Optional local-only activation log lives in the Settings → Diagnostics surface and never leaves the machine.

### Known limitations
- See docs/release/v0.1-known-limitations.md for the honest list.
```

Keep it under one screen per section. End-user voice means: "task extraction" not "extract_content_tags GBNF grammar wiring". Date placeholder `2026-MM-DD` — fill in on tag day.

No code change to verify. After writing, view it via `cat CHANGELOG.md` and confirm structure.

---

### Task 3.2: Release notes draft (one page max, plain language)

**Files:**
- Create: `docs/release/v0.1-release-notes.md`

**Brief for subagent:**
Write a one-page (≤ 600 word) release notes draft for the public download page. Cover:

- **What Lumotia is** — one paragraph, no jargon. "Local-first dictation that turns your voice notes into clean transcripts and actionable tasks. Everything runs on your device."
- **What's in v0.1** — 5-7 bullet points in user-benefit voice ("Talk into the app. Get a clean transcript. See your to-dos. Search your history.")
- **Privacy + AI use** — one paragraph linking to `how-lumotia-is-built.md` and `v0.1-known-limitations.md`. "We use AI tools to write Lumotia. We disclose how. We test what matters. Read the trust page."
- **First-install warnings** — one short section: macOS Gatekeeper, Windows SmartScreen, Linux AppImage SHA-256 instructions. Two sentences each. (Cross-link: see `docs/release/install-warnings.md` from Task 3.5.)
- **Supported platforms** — table from the checklist. Linux primary; macOS Apple Silicon + Windows 11 best-effort; macOS Intel only-if-smoke-tested.
- **Known limitations link** — single line: "See `docs/release/v0.1-known-limitations.md` for the honest list."
- **Reporting issues** — GitHub URL placeholder.

Match the tone of `docs/release/how-lumotia-is-built.md`. No emoji. No marketing fluff. Calm, specific.

---

### Task 3.3: Privacy + AI-use disclosure page

**Files:**
- Create: `docs/release/privacy-and-ai-use.md`

**Brief for subagent:**
Create `docs/release/privacy-and-ai-use.md`. Sections:

1. **What stays local** — explicit list (audio, transcripts, tasks, MicroSteps, history, models, ontology, activation log).
2. **What optionally reaches the network** — explicit list of every outbound network call the app can make (model downloads from HuggingFace, version-update check if wired, npm audit signatures during dev). For each: when it happens, what data is sent, what consent gate exists.
3. **What NEVER leaves the machine** — explicit list (audio, transcripts, tasks, voice).
4. **AI use disclosure** — local LLM is used for cleanup + task extraction; runs on device; downloaded once. The implementation itself is AI-assisted (per `how-lumotia-is-built.md`). Link to that doc.
5. **MCP server caveat** — the read-only MCP surface gives any wired client read access to all transcripts and tasks. Treat with the same care as a folder of personal notes. (Mirror the wording from `v0.1-known-limitations.md`.)
6. **Activation log** — opt-in, local-only, never sent. Settings → Diagnostics → Clear button.
7. **Crash dumps + logs** — where they live, what they contain, whether they're auto-deleted.
8. **Your data, your machine** — one closing paragraph. AGPL-3.0-or-later licence, public repo, audit it yourself.

Audience: end users, not engineers. Tone: calm, specific, no fluff. Length: ≤ 600 words.

Link from: `SettingsPage.svelte` Privacy section (already wired to point here in Task 2.4), and `README.md` (added in Task 3.4).

---

### Task 3.4: README updates — link how-built, GitHub issues, v0.1 framing

**Files:**
- Modify: `README.md`

**Brief for subagent:**
Targeted README updates:

1. Replace the "Pre-alpha" status line with a short "v0.1 release" section that points to:
   - `docs/release/v0.1-release-notes.md`
   - `docs/release/v0.1-known-limitations.md`
   - `docs/release/how-lumotia-is-built.md`
   - `docs/release/privacy-and-ai-use.md`
2. Add a "Reporting issues" section near the bottom: GitHub issues URL `https://github.com/jakeadriansames/lumotia/issues`.
3. Confirm install paths per platform are still correct (AppImage / .deb / .dmg / .msi / .exe).
4. Confirm first-run expectations are accurate (model download, microphone permission prompt).
5. Do NOT touch the project pitch / design principles sections — they're locked.

After editing, run `npm run check` (no Svelte impact, but checks markdown links if configured). Read the final file and confirm all four release-doc links resolve to actual files.

---

### Task 3.5: Per-platform first-install warning doc

**Files:**
- Create: `docs/release/install-warnings.md`

**Brief for subagent:**
One-page doc enumerating what users will see on first install per platform and how to proceed safely. Sections:

- **macOS** — Gatekeeper warning ("App can't be opened because it is from an unidentified developer"). Workaround: System Settings → Privacy & Security → "Open Anyway". This warning will disappear once we ship a notarised build with an Apple Developer ID. Tracked: see `KNOWN-ISSUES.md` (RB-08-related).
- **Windows** — SmartScreen "Windows protected your PC" warning. Workaround: click "More info" → "Run anyway". This warning will disappear once we ship an EV-signed installer.
- **Linux AppImage** — verify the SHA-256 checksum published next to the AppImage on the release page. Show the verification one-liner: `sha256sum lumotia-0.1.0-linux-x86_64.AppImage` then compare to the published value. Optional: import the GPG key once published.

Tone: matter-of-fact, "this is what to do, don't panic, it's expected for a new app". Length: ≤ 400 words.

Link from `v0.1-release-notes.md` (Task 3.2) and `README.md` (Task 3.4).

---

### Task 3.6: AppImage SHA-256 publication wired into build.yml

**Files:**
- Modify: `.github/workflows/build.yml`

**Brief for subagent:**
After the AppImage build step in `.github/workflows/build.yml`, add a step that:

1. Computes `sha256sum lumotia-*.AppImage > lumotia-*.AppImage.sha256`
2. Uploads the `.sha256` file alongside the AppImage as a release artefact

Use the existing `actions/upload-artifact@v4` pattern in the workflow. Do not modify the macOS or Windows steps — they have signing pending (Task 4 — human-required documented).

Reference: GitHub Actions native `runner.os == 'Linux'` conditional. Use `sha256sum` (Linux) — there's no need for a cross-platform variant because this only runs on the Linux job.

After editing, validate the YAML parses by running `yq eval . .github/workflows/build.yml > /dev/null` or just inspect manually.

---

### Task 3.7: Workspace Cargo.toml version policy + npm dev deps exact-pin sweep

**Files:**
- Modify: `Cargo.toml` (workspace root)
- Modify: `package.json`

**Brief for subagent:**
Two parts:

1. **Workspace version policy:** Add `[workspace.package]` section to root `Cargo.toml`:

   ```toml
   [workspace.package]
   version = "0.1.0"
   edition = "2021"
   license = "AGPL-3.0-or-later"
   repository = "https://github.com/jakeadriansames/lumotia"
   ```

   Then for each crate's `Cargo.toml` (under `crates/*` and `src-tauri/`), change the `[package]` section to inherit:

   ```toml
   [package]
   name = "lumotia-foo"
   version.workspace = true
   edition.workspace = true
   license.workspace = true
   repository.workspace = true
   description = "..."  # keep crate-specific descriptions
   ```

   This keeps the version source-of-truth in one place. Run `cargo check --workspace --all-targets` after — must stay green.

2. **npm dev deps exact-pin:** In `package.json`, find every `^X.Y.Z` and `~X.Y.Z` in `devDependencies`. For each, run `npm view <pkg> version` to get the latest X.Y.Z (or pin to the version that's currently installed per `package-lock.json` — safer). Replace `^X.Y.Z` with `X.Y.Z`. Do NOT change ranges in `dependencies` (runtime deps are usually OK to leave caret-ranged for security patches).

   Recon noted these 10 ranges in devDependencies — pin each:
   - `@sveltejs/adapter-static`, `@sveltejs/kit`, `@tauri-apps/cli`, plus 7 others — discover via `node -e "const p = require('./package.json'); for (const [k,v] of Object.entries(p.devDependencies||{})) if (/[\^~]/.test(v)) console.log(k, v)"`.

   After editing, run `npm ci --ignore-scripts` to confirm the lockfile resolves cleanly. Run `npm run check` and `npm run test` — both must stay green.

Report any version mismatch surprises (if a current devDep version is behind a published security patch, flag it).

---

### Task 3.8: Diagnostic bundle Tauri command

**Files:**
- Create: `src-tauri/src/commands/diagnostics.rs` (or extend if already exists per recon — module list shows `diagnostics` exists)
- Modify: `src-tauri/src/lib.rs` (register if new command)
- Modify: `src/lib/pages/SettingsPage.svelte` Diagnostics section (add "Generate diagnostic bundle" button)

**Brief for subagent:**
The existing `commands/diagnostics.rs` already does some work (recon noted `diagnosticReportError` in SettingsPage). Extend it (or add) `generate_diagnostic_bundle()` that produces a zip file at a user-chosen path containing:

- App version, Tauri version, Rust toolchain, OS + version
- Last 7 days of logs (whatever Tauri-app `tracing` is writing, capped at 5 MB)
- Recent crash dumps (cap at 3 most recent files)
- Redacted preferences — `preferences.json` with sensitive fields blanked (no API keys if any exist; mask user-set vocabulary entries)

**Critical:** must NEVER include audio files or transcript text. Sweep the bundle assembler with a deny-list of file globs (`**/*.wav`, `**/*.opus`, `**/transcripts/**`, `**/audio/**`).

Use `zip` crate (already in workspace if present, otherwise add). Pop a save dialog via `tauri-plugin-dialog`. After save, surface a toast: "Diagnostic bundle saved. You can attach this to a GitHub issue."

Wire into SettingsPage.svelte under the Help (or Diagnostics) section: a "Generate diagnostic bundle" button that calls the command.

Run `cargo build -p lumotia` and `cargo test --workspace`.

---

### Task 3.9: Activation log surface in Settings → Diagnostics

**Files:**
- Modify: `src/lib/pages/SettingsPage.svelte` (add Activation log subsection under Privacy or Help)

**Brief for subagent:**
Add a small Activation log subsection in SettingsPage.svelte. On render, call `invoke('list_lumotia_events')` and display:

- A short paragraph: "This log lives on your device only. We never send it anywhere. It records anonymous milestones (first capture, first export, first task extracted) so you can see your own usage shape."
- A table: kind | occurred_at (formatted) | payload (if any)
- A "Clear activation log" button → `invoke('clear_lumotia_events')` then refresh the table.
- A toggle: "Record activation events" — when off, frontend doesn't fire any new `record_lumotia_event` calls. Persist the toggle in `preferences.json` (use existing preferences plumbing).

Run `npm run check`. Must stay clean.

---

## Tier 4 — Documentation of human-required residual

### Task 4.1: Annotate the checklist with residual + human-required notes

**Files:**
- Modify: `docs/release/v0.1-checklist.md`
- Modify: `docs/release/v0.1-ui-hardening.md`

**Brief for subagent (or me directly at end):**
After Tier 1-3 land and Tier 5 gates pass, walk every unchecked box in the two release docs and either:

- Tick if completed (with a one-liner reference to the commit / file that satisfies it)
- Leave unchecked + add a `> 👤 HUMAN REQUIRED:` note explaining why this can't be automated and what specific action the user takes (e.g. "purchase EV signing certificate from DigiCert", "run smoke-test on Apple Silicon hardware", "recruit 5 testers").

Specific items expected to land in the human-required bucket:
- Windows code-signing certificate sourcing + signing wiring
- macOS notarisation with Apple Developer ID
- macOS Apple Silicon App Nap real-hardware verification (RB-08)
- The 5-platform smoke-test matrix
- The 10-step tester acceptance flow personally on Linux
- Private-beta tester recruitment + activation metrics measurement
- Public v0.1 launch metrics (20/15/10/5 strangers)
- Decision log for KI-02 (Linux idle inhibit) + KI-03 (Windows sleep prevention) — fix-if-tiny vs document

Do NOT silently tick a box that hasn't been verified. If a code-side item lands but a manual verification step is also required, leave the box unticked and add both a `Code: ✅` and a `Manual: 👤 pending` note.

---

## Tier 5 — Quality gates + dogfood drill (final pass)

### Task 5.1: Run all quality gates green

**Brief for subagent (or me directly):**
After Tier 1-3 lands, in order:

```
cargo fmt --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
npm run check
npm run test
scripts/dogfood-rebrand-drill.sh
```

Each must exit 0. Fix any failure root-cause; do not paper over with `#[allow(...)]` or `npm-check-ignore`. Any new clippy lints introduced by Tier 1-3 work get fixed at the call site.

Report the full output (or summary if green) of each command. The session is done when all six are green.

---

## Self-review of plan

**Spec coverage check** (matched to `v0.1-checklist.md` + `v0.1-ui-hardening.md`):
- Product surface: covered by recon (already shipped) + Task 4.1 (verify + tick).
- First-run onboarding: Task 1.1 (table) + Task 1.2 (commands) + Task 2.1 (gate) + Task 2.2 (test prompt + recovery) + Task 2.4 (Help section) — all 6 sub-items addressed.
- Release artefacts: Task 3.1 (CHANGELOG) + Task 3.2 (release notes) + Task 3.4 (README) + Task 3.5 (warnings doc) + Task 3.6 (AppImage SHA) + Task 3.7 (version sync + npm pin). Code-signing → Task 4.1 human-required.
- Documentation: Task 3.3 (privacy page) + Task 3.4 (README link) — known-limitations + how-built already exist.
- UI acceptance: Tasks 1.3 + 2.3 + 2.5 + 2.6 + 2.7 + 2.8 + 2.9 — all 13 checklist sub-items addressed.
- Quality gates: Task 5.1 — all 8 gates run.
- Trust + security: already verified by recon (Task 4.1 ticks).
- Release-blocker resolution: Task 4.1 documents RB-08 + KI-02 + KI-03 as human decisions.
- Supported platforms + smoke matrix: Task 4.1 documents as human-required.
- Activation metrics: Task 1.1 (table) + Task 3.9 (UI surface) + Task 4.1 documents the human measurement.

**Placeholder scan:** none. Every task has concrete files, brief, gate. The Apple-ID / Windows-cert items live in Tier 4 deliberately as documented residual.

**Type-consistency check:** `record_onboarding_event` / `list_onboarding_events` / `has_completed_onboarding` / `record_lumotia_event` / `list_lumotia_events` / `clear_lumotia_events` are used identically across Tasks 1.2, 2.1, 2.3, 2.4, 3.9. `StatusPill` props (`status: Status; label?: string`) are used identically across Tasks 1.3, 2.3, 2.5, 2.7, 2.8.

---

## Execution model

Subagent-Driven mode: each task is dispatched to a fresh sonnet subagent with a self-contained brief lifted from the task body above. The dispatcher (this main session) verifies the work between tasks via:

- Re-running the gate the task brief specified (e.g. `cargo test -p lumotia-storage` after Task 1.1)
- Spot-reading the changed files
- Confirming no regression in the global gates set

Tier 1 runs sequentially (Tasks 1.1 → 1.2 → 1.3) because Task 1.2 depends on 1.1's tables and Task 2.x depends on 1.1-1.3 foundations.

Tier 2 fans out: 2.1, 2.2, 2.3, 2.4, 2.6, 2.7, 2.8, 2.9, 2.10, 2.11 can run in parallel batches of 3-4 (avoid touching the same files concurrently — DictationPage.svelte is touched by 2.3, 2.5, 2.7, 2.8, 2.9 so those serialise).

Tier 3 fans out completely: 3.1-3.9 are independent of each other and of Tier 2.

Tier 4 + 5 are end-of-session.
