Files
Lumotia/docs/superpowers/plans/2026-05-14-v0.1-release-completion.md
Jake 3770815fbf
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
agent: lumotia — v0.1 release-completion run
Closes the code-side v0.1 ship gate. All quality gates green:
cargo fmt/clippy/test (~327 tests), npm check (0/0), vitest 13/13,
scripts/dogfood-rebrand-drill.sh 8/8.

Phase F — first-run onboarding promoted to v0.1
- FirstRunPage with skip-to-main + failure recovery + event recording
- Six onboarding commands (record/list/has-completed + lumotia_events)
- Storage migration v17 (onboarding_events + lumotia_events tables)

UI hardening (in-scope items from v0.1-ui-hardening.md)
- StatusPill + PostCaptureCard components, 21st preview entry
- Sidebar recording-as-sacred-state (opacity + aria-disabled, reduced-motion)
- Settings 6-section regroup + Help section + Activation log + Privacy toggle
- Error-state copy sweep (DictationPage + SettingsPage, plain-language)
- Global :focus-visible rule, textarea outlines restored
- Ctrl+K / Ctrl+, / Escape bindings in +layout

LLM resilience
- rule_based_extract_tasks (regex-free imperative-verb extractor) +
  extract_tasks_with_fallback wrapper — task extraction never returns zero
- tokio::time::timeout(120s) wraps cleanup/tags/tasks commands

Release artefacts
- LICENSE (canonical AGPL-3.0), CHANGELOG (Keep-a-Changelog format)
- v0.1-release-notes, privacy-and-ai-use, install-warnings,
  tester-onboarding-kit, tester-acceptance-runbook, code-signing-setup,
  apple-silicon-rb08-runbook, virtual-audio-setup, v0.1-contrast-audit
- Workspace versioning + AGPL spdx; npm exact-pin (10 ranges removed)
- AppImage SHA-256 sidecar in build.yml
- README v0.1 section + Reporting-issues; canonical repo slug

Closure pass — items moved from human-required to code-complete
- KI-02 Linux idle inhibit: zbus 5 → org.freedesktop.login1.Manager.Inhibit
- KI-03 Windows sleep prevention: SetThreadExecutionState(ES_CONTINUOUS|...)
- acquire/release_idle_inhibit Tauri commands, wired in DictationPage
- Diagnostic-bundle frontend wire-up (Settings → Help button)
- WCAG-AA contrast fix via .btn-filled-text utility (no token changes)
- 8 destructive-action sites wrapped in plain-language confirm() guards
- KNOWN-ISSUES.md + v0.1-known-limitations.md updated (KI-02/03 fixed)

Scripts
- pre-tag-verify.sh, tag-day.sh, smoke-linux + driver
- parse-diagnostic-bundle.sh, parse-activation-log.py

Per-item audit trail: docs/release/v0.1-completion-status.md
Remaining: W-01…W-08 (signing certs, hardware probes, smoke matrix,
tester recruitment) — see docs/release/v0.1-known-limitations.md.
2026-05-15 06:59:08 +01:00

48 KiB

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:

-- 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()):

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:

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. Accessibilityprefers-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.svelteaudioDevicesError, 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:

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).


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:

    [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:

    [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.