Skip to content

Sprint 5 Design Pack — GWP-176

GWP-176 is the desktop GUI sibling to the device-side update path defined in ADR-0011 (Pi Zero 2 W system image update mechanism) and the multi-surface umbrella ADR-0020 (firmware update model). Both ADRs already nominate Tauri 2.x as the framework, sketch the UI state machine, and specify the helper-binary elevation pattern. This pack does not re-decide those points — it consolidates them into a single design surface, fills in screen-level wireframes, names the scaffold layout, and surfaces the open questions that need Josh-level decisions before any code lands.

Stale-ticket disambiguation. The original brief on this task referenced “kn86flash (the underlying CLI flasher tool, GWP-111).” That framing is obsolete in two ways: (a) GWP-111 was retired by ADR-0020 along with the rest of the pre-Pi firmware-cluster cohort (GWP-107, 108, 110, 111, 112, 113, 117 — see ADR-0020 L23 and L163), and (b) the Pi-OS-on-SD topology never had a standalone kn86flash CLI; the CLI surface is kn86-flasher-helper, the elevated raw-write helper invoked by the Tauri UI per ADR-0011 §“Desktop flasher architecture” L171–172. There is no second-class CLI flasher to “wrap” — the Tauri UI and its helper binary are the flasher. The header-format crate tools/kn86fw/ (GWP-144) is a separate concern (image builder and header parser, not a writer).

What’s already decided (do not re-litigate in this PR):

  • Framework: Tauri 2.x. Per ADR-0011 L165 and the update-system research brief §“Framework recommendation with rationale” (docs/device/os/update-system.md L42–58). Rationale: Rust backend owns disk I/O directly without IPC hops, ~10 MB bundle vs Electron’s ~100 MB, native webview matches the Kinoshita boutique aesthetic, and the Rust workspace lets us share the kn86-fw-format parser with tools/kn86fw/ and any future headless kn86-cli.
  • Two-binary architecture. kn86-flasher (Tauri UI, unprivileged) + kn86-flasher-helper (elevated CLI doing raw block-device writes), invoked via elevated-command crate cross-platform. Per ADR-0011 L169–177.
  • State machine. IDLE → DEVICE_DETECTED → CONFIRM → FLASHING → VERIFY → COMPLETE/ERROR. Per ADR-0011 L429.
  • Device detection. USB VID/PID match + MSC volume label KN86_UPDATE for positive confirmation. Per ADR-0011 L181–185.
  • Image format consumed. .kn86fw (Phase 0 today; .kn86release composite per ADR-0020 §“One composite release artifact” L52 once that lands). Header parsing reuses tools/kn86fw/ lib.rs per ADR-0011 L210.
  • Crate location. ADR-0011 L210, L448 says the flasher lives at tools/flasher/ (“sibling to tools/kn86fw/”). Director’s note: this design pack proposes tools/kn86flash-gui/ instead, both because the user brief named it that way and because tools/flasher/ is generic enough to confuse with the GUI’s underlying helper. Open question #1 below — confirm with Josh which name wins. Default if unconfirmed: follow ADR-0011 (tools/flasher/).

What this pack adds on top of the ADRs:

  1. Per-screen ASCII wireframes for the five UI states + error variants.
  2. Explicit user-story decomposition (initial bring-up, dev iteration, end-user re-flash, recovery).
  3. Backend-interface contract between the Tauri UI and kn86-flasher-helper (process spawn, stdout protocol, stderr error tokens) — closes ADR-0011’s open question #1 (“flasher invokes helper how?”) at the design-pack level.
  4. SD device enumeration strategy per OS, with the privilege escalation per OS spelled out concretely.
  5. Image-source UX (local file picker vs. GitHub releases pull).
  6. Verification UX boundary — what’s in scope for v1 vs. deferred.
  7. Cross-platform packaging strategy and how it relates to GWP-125 (the macOS DMG task) and the existing release CI.
  8. Scaffold directory proposal with file-by-file inventory.

Four distinct user personas / scenarios drive the GUI’s UX requirements. Each gets one row of the matrix below; each row is referenced by acceptance criterion in the §Acceptance section.

StoryUserWhenTriggerExpected outcomeAcceptance criterion
S1: Initial bring-upJosh / hardware bring-up engineerFirst time a freshly assembled prototype bootsJust-printed PCB, blank SD card from manufacturingInsert SD, run flasher, write a known-good .kn86fw, eject. Device boots from p2/p4 (slot A) on first power-up.AC1, AC2, AC8
S2: Dev iterationC engineer working on nOSh / cart engineerSeveral times per day during nOSh-runtime feature developmentJust built a new .kn86fw locally via kn86fw build. Want to push it to the dev rig without unplugging anything physical.Hold SYS+LINK on dev rig → boots into updater mode → MSC volume mounts on dev laptop → flasher auto-detects, one click, done. <60 s round-trip.AC1, AC3, AC4, AC9
S3: End-user re-flashEventual customer (post-Q4 2027 ship)When KN-86 issues a firmware updateReceives a .kn86release from the website (or auto-update notification — Phase 3 deferred)Download .kn86release, open flasher (or click a kn86flash:// URL), follow the on-screen instructions (“hold SYS+LINK, plug in cable”), one button. Visual feedback confirms completion. Tryboot rollback handles any failed boot.AC1, AC4, AC5, AC10
S4: Recovery / re-imageEnd user OR engineer when both A/B slots are corruptRare — only after a catastrophic failure or factory-reset requestEither (a) device boots into updater mode but neither slot is committable, or (b) user uses a separate SD card writer to re-image from scratch using a “full SD image” variantThe Tauri flasher offers a “Recovery” mode (advanced) that writes both slots A+B sequentially from a full .kn86recovery image, NOT the differential .kn86fw. Or the user pulls the SD and uses the dd-equivalent path via the tools/sd-provision/ pipeline directly.AC6 (deferred path call-out)

The four stories are intentionally ranked by frequency: dev iteration (S2) is the highest-volume use case and dominates the UX budget; end-user re-flash (S3) is the highest-stakes use case and dominates the polish budget; recovery (S4) is the lowest-frequency but highest-blast-radius and is explicitly deferred to a v2 scope.

The flow follows the state machine from ADR-0011 L429: IDLE → DEVICE_DETECTED → CONFIRM → FLASHING → VERIFY → COMPLETE / ERROR. Below is the per-screen wireframe — each is a single viewport (~720×480 logical, the Tauri default) rendered as ASCII to keep design diff-able and to set expectations for the visual designer (Kinoshita AMBER #E6A020 on black #000000 per CLAUDE.md hardware spec, as amended 2026-06-13 per ADR-0036; same default palette as the device, intentionally on-brand — WHITE / GREEN alternates not surfaced in the flasher UI).

┌──────────────────────────────────────────────────────────────────────────┐
│ KINOSHITA ◇ KN-86 DECKLINE FLASHER v0.1.0 │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ [ KINOSHITA WORDMARK / LOGO ] │
│ │
│ Connect a KN-86 in updater mode to get started. │
│ │
│ ┌─ HOW TO ENTER UPDATER MODE ─────────────────┐ │
│ │ 1. Power off the deck. │ │
│ │ 2. Hold SYS + LINK on the keypad. │ │
│ │ 3. Connect USB-C cable while still held. │ │
│ │ 4. Release after the amber bezel pulses. │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ◆ WAITING FOR DEVICE ◆ (animated) │
│ │
│ [ Choose firmware file… ] [ Pull latest from kinoshita.dev ] │
│ │
├──────────────────────────────────────────────────────────────────────────┤
│ STATUS: idle · no device · no firmware loaded [ ? Help ] │
└──────────────────────────────────────────────────────────────────────────┘

Notes: image source can be picked before the device is connected (so the user can preload the file while the device boots into updater mode); preload is reflected in the status bar.

Screen 1 — DEVICE_DETECTED (firmware not yet loaded)

Section titled “Screen 1 — DEVICE_DETECTED (firmware not yet loaded)”
┌──────────────────────────────────────────────────────────────────────────┐
│ KINOSHITA ◇ KN-86 DECKLINE FLASHER v0.1.0 │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ✓ KN-86 DETECTED │
│ Serial: KN86-PROTO-0007 │
│ Mount: /Volumes/KN86_UPDATE (macOS) │
│ Slot: B (active is A — writing to inactive slot) │
│ Battery: 74% ⚡ charging via USB │
│ │
│ FIRMWARE TO WRITE │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ No firmware selected. │ │
│ │ │ │
│ │ [ Choose .kn86fw or .kn86release file… ] │ │
│ │ [ Pull latest stable from kinoshita.dev ] │ │
│ │ [ Pull latest nightly (advanced) ] │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
├──────────────────────────────────────────────────────────────────────────┤
│ STATUS: device ready · firmware required [ ? Help ] │
└──────────────────────────────────────────────────────────────────────────┘

Screen 2 — CONFIRM (device + firmware both loaded; gate before write)

Section titled “Screen 2 — CONFIRM (device + firmware both loaded; gate before write)”
┌──────────────────────────────────────────────────────────────────────────┐
│ KINOSHITA ◇ KN-86 DECKLINE FLASHER v0.1.0 │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ READY TO WRITE │
│ │
│ ┌─ DEVICE ─────────────────────┐ ┌─ FIRMWARE ────────────────────┐ │
│ │ KN86-PROTO-0007 │ │ kn86-v0.2.1-stable.kn86fw │ │
│ │ Slot B (inactive) │ │ Built: 2026-05-01 │ │
│ │ Battery: 74% ⚡ │ │ nOSh: 0.2.1 │ │
│ │ │ │ SHA-256: 6cca8e…d749 ✓ │ │
│ └──────────────────────────────┘ │ Size: 187 MB │ │
│ └───────────────────────────────┘ │
│ │
│ ⚠ This will overwrite slot B. Slot A (current) remains intact. │
│ If the new firmware fails to boot, the deck reverts to slot A │
│ automatically (tryboot rollback). │
│ │
│ ⚠ Integrity-checked, not signed. Verify firmware source. │
│ │
│ [ Cancel ] [ Begin flash → ] (primary) │
│ │
├──────────────────────────────────────────────────────────────────────────┤
│ STATUS: ready · awaiting confirmation [ ? Help ] │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ KINOSHITA ◇ KN-86 DECKLINE FLASHER v0.1.0 │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ WRITING FIRMWARE TO SLOT B │
│ │
│ kn86-v0.2.1-stable.kn86fw → /dev/disk5 (slot B: bootfs+rootfs) │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Writing bootfs (p3) ████████████████████░░░░░░░░░░░ 68% │ │
│ │ Writing rootfs (p5) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0% │ │
│ │ fsync ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │
│ │ Verify ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ Throughput: 4.2 MB/s Elapsed: 00:14 ETA: 00:32 │
│ │
│ ⛔ DO NOT UNPLUG OR POWER OFF │
│ │
├──────────────────────────────────────────────────────────────────────────┤
│ STATUS: flashing 68% · do not disconnect [ ? Help ] │
└──────────────────────────────────────────────────────────────────────────┘

Screen 4 — VERIFY (read-back integrity check)

Section titled “Screen 4 — VERIFY (read-back integrity check)”
┌──────────────────────────────────────────────────────────────────────────┐
│ KINOSHITA ◇ KN-86 DECKLINE FLASHER v0.1.0 │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ VERIFYING SLOT B │
│ │
│ Recomputing SHA-256 across written payload… │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Verify ████████████████████░░░░░░░░░░░ 72% │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ Expected: 6cca8ed3b70a3298a74910fdfbb700a8120ed07dac959962fec0c66c… │
│ Computed: (in progress) │
│ │
│ ⛔ DO NOT UNPLUG OR POWER OFF │
│ │
├──────────────────────────────────────────────────────────────────────────┤
│ STATUS: verifying 72% · do not disconnect [ ? Help ] │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ KINOSHITA ◇ KN-86 DECKLINE FLASHER v0.1.0 │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ✓ │
│ │
│ FLASH COMPLETE │
│ │
│ Slot B written and verified. Tryboot armed for next boot. │
│ │
│ ┌─ NEXT STEPS ──────────────────────────────────────────────────────┐ │
│ │ 1. Disconnect the USB cable. │ │
│ │ 2. Power-cycle the deck. │ │
│ │ 3. The deck will boot from slot B. If it fails to reach the │ │
│ │ desktop within 60 s, it auto-reverts to slot A on the next │ │
│ │ power cycle. │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ Wrote 187 MB in 00:42 · verified in 00:18 · total 01:00 │
│ │
│ [ Flash another device ] [ Quit ] │
│ │
├──────────────────────────────────────────────────────────────────────────┤
│ STATUS: complete · safe to disconnect [ ? Help ] │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ KINOSHITA ◇ KN-86 DECKLINE FLASHER v0.1.0 │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ✗ │
│ │
│ FLASH FAILED │
│ │
│ ERROR: SHA256_MISMATCH │
│ Verify pass found a checksum mismatch on slot B (p5/rootfs). │
│ │
│ What this means: │
│ The written data does not match the source firmware. This is │
│ usually caused by the cable being disconnected mid-write, a │
│ failing SD card, or USB power instability. │
│ │
│ What to do: │
│ • Slot A is unchanged — the deck still boots normally. │
│ • Try a different USB-C cable. │
│ • Try a different USB port (data-capable, not a hub). │
│ • If the problem persists, the SD card may need replacement. │
│ │
│ [ Copy diagnostics ] [ Try again ] [ Quit ] │
│ │
├──────────────────────────────────────────────────────────────────────────┤
│ STATUS: error · slot A unchanged · safe to disconnect [ ? Help ] │
└──────────────────────────────────────────────────────────────────────────┘

Each error variant follows this skeleton with a different ERROR token + body. Tokens map 1:1 to the helper-binary’s stderr emissions (see §Backend interface contract below): MAGIC_MISMATCH, TRUNCATED_HEADER, SHA256_MISMATCH, UNSUPPORTED_VERSION, DEVICE_DISAPPEARED, WRITE_IO_ERROR, ELEVATION_DENIED, WRONG_VOLUME_LABEL.

This is restated only to record it in one place; the underlying decision is owned by ADR-0011 §“Framework recommendation with rationale” and docs/device/os/update-system.md L42–58. The short form:

OptionWhy it loses
Tauri 2.xWinner. Rust backend owns disk I/O + elevated-command + native webview, ~10 MB bundle, shares a workspace crate with tools/kn86fw/.
Electron~100 MB bundle, requires Node IPC dance to call Rust, no advantage over Tauri for our use case.
Native Qt (rpi-imager fork)Apache-2.0 base is permissive and proven, but locks us into Qt/QML and the generic “write any disk” framing — we want a narrow Kinoshita-branded experience. Useful as a reference for buffer sizes, sync semantics, and the drivelist/mountutils MIT-licensed standalone libs (which we can pull into Tauri).
Rust + eguiNative-rendered, single-binary, no webview tax. Real candidate — but the team’s frontend skillset leans web, and the visual design budget for a boutique product favors HTML/CSS over immediate-mode GUI. Reconsider only if Tauri’s webview adds friction we didn’t anticipate (e.g., macOS WKWebView quirks blocking USB device enumeration UI).
Plain CLI (kn86-flasher-helper alone)Already exists in the architecture. Insufficient for end-user S3 path — non-technical users can’t be expected to enumerate /dev/disk* and pick the right one. CLI is the foundation; the GUI is the polish layer on top.

Backend interface contract — UI ↔ kn86-flasher-helper

Section titled “Backend interface contract — UI ↔ kn86-flasher-helper”

ADR-0011 leaves “exactly how the UI invokes the helper” as an open implementation question. Resolving it here keeps the engineering brief tight when implementation gates open.

Decision: spawn-and-pipe, not linked crate.

The Tauri UI (running unprivileged) spawns kn86-flasher-helper as a subprocess via elevated-command::Command. They communicate over stdin/stdout/stderr with a simple line-oriented protocol (newline-delimited JSON for structured events; plain text for fatal-error tokens). No FFI, no shared library, no ABI lock-in.

Rationale:

  • Process boundary = privilege boundary. The helper runs elevated (root on macOS/Linux, Administrator on Windows); the UI does not. A linked-crate model would pull privileged code into the UI’s address space, defeating the helper’s whole reason for existing.
  • Crash isolation. A bug in the helper’s raw-write path can’t crash the UI (and vice versa). The UI can detect helper exit and surface a clean error.
  • Trivially testable. kn86-flasher-helper is testable in isolation via shell scripts; mock-helpers for UI integration tests are tiny. A linked-crate model would force in-process mocking and elevation simulation.
  • Aligns with tools/kn86fw/. That crate is already a CLI with both lib.rs (consumed in-process) and main.rs (consumed via spawn). The flasher repeats the pattern: shared workspace crate kn86-fw-format for in-process header parsing, helper binary for the privileged write path.

Protocol sketch:

helper-stdin (UI → helper):
command JSON, e.g.:
{"op":"flash","fw_path":"/path/to/x.kn86fw","device":"/dev/disk5","slot":"B"}
{"op":"verify","fw_path":"...","device":"/dev/disk5"}
{"op":"abort"}
helper-stdout (helper → UI):
newline-delimited JSON events:
{"event":"phase","phase":"writing_bootfs","total":67108864}
{"event":"progress","bytes_done":40000000,"bytes_total":67108864}
{"event":"phase","phase":"verify"}
{"event":"complete","slot":"B","sha256":"6cca8e..."}
helper-stderr (helper → UI):
fatal error tokens, one per line:
ERROR MAGIC_MISMATCH "header magic 'XXXXXXXX' does not match KN86FWv1"
ERROR SHA256_MISMATCH "expected 6cca8e...d749 got deadbe...beef"
ERROR ELEVATION_DENIED "user dismissed the elevation prompt"
helper exit code:
0 = success, non-zero = error (token already on stderr)

The UI keeps the helper alive for the duration of a single flash transaction; the helper exits cleanly on op:abort or after a complete event. Long-lived helper would buy nothing and complicate elevation.

SD device enumeration + privilege escalation per OS

Section titled “SD device enumeration + privilege escalation per OS”

The flasher needs to (a) find candidate USB block devices, (b) filter to ones that look like a KN-86 in updater mode, and (c) elevate when it’s time to write. Each OS has a distinct path.

OSEnumerationFilterElevation
Linuxudev via the udev Rust crate (or block-utils + /sys/block walk as a fallback). Subscribe to add/remove events for live device-list updates.Match USB VID/PID pid.codes-allocated or Pi-Foundation range (open question #2 below) AND mount label KN86_UPDATE.pkexec (polkit) wrapping the helper. Ship a polkit policy file /usr/share/polkit-1/actions/dev.kinoshita.flasher.policy in the AppImage. The elevated-command crate handles the wrapping; we own the .policy authoring.
macOSIOKit device-arrival notifications via the core-foundation + io-kit-sys crates, OR diskutil list -plist polled at ~1 Hz as a simpler fallback for v1.Same VID/PID + volume label match. macOS auto-mounts MSC volumes under /Volumes/KN86_UPDATE.Authorization Services via Apple’s SMJobBless (deprecated but still works) OR a one-shot osascript -e 'do shell script ... with administrator privileges' for v1 simplicity. The elevated-command crate currently uses osascript on macOS; that’s acceptable for v1.
WindowsSetupDiGetClassDevs from the Win32 API for USB enumeration, OR poll wmic logicaldisk (or the modern PowerShell Get-Volume) for v1 simplicity.Same VID/PID + drive volume label match. Windows assigns a drive letter (e.g., E:); we read \\.\PhysicalDriveN for raw write, not the letter.UAC via runas verb on ShellExecute. The elevated-command crate handles this; the manifest must declare requireAdministrator for the helper, not the UI.

v1 enumeration scope: poll-based across all three OSes (1 Hz diskutil / lsblk / wmic poll) is sufficient for the “user explicitly enters updater mode and waits a few seconds” UX. Event-based hot-plug detection (udev / IOKit / SetupDi) is a v2 polish — moves “Waiting for device…” → “Detected” latency from ~1 s to instant. Not worth the platform-specific crate burden in v1.

Drive-name display: show the OS-native path (/dev/disk5 on macOS, /dev/sdc on Linux, \\.\PhysicalDrive3 on Windows) in the UI alongside the friendlier “Slot B (inactive)” label. Power users want the device path; non-technical users ignore it.

Two image-source modes, both available from Screen 0 / Screen 1:

  1. Local file picker (default for S2 dev iteration). OS-native open dialog filtered to .kn86fw and .kn86release extensions. Recently-used files surfaced as a “recents” list in a dropdown.
  2. GitHub releases pull (default for S3 end-user). UI hits https://api.github.com/repos/jschairb/kn86-deckline/releases/latest (or whatever the canonical release surface ends up being — open question #3), shows the most recent stable + most recent nightly, downloads the chosen .kn86release to ~/.cache/kn86-flasher/ (XDG basedir / Library/Caches/ / %LOCALAPPDATA%), verifies its SHA-256 against the GitHub-attached signature file (or just the asset’s own SHA-256 if we don’t sign in v1), then proceeds as if the user had picked it locally.

Both modes converge on the same Screen 2 (CONFIRM) flow — the difference is opaque to everything downstream of file selection. The “Pull from kinoshita.dev” button on Screen 0 is a third entry point that’s just a redirect to the GitHub release pull (with a friendlier label); we don’t need a separate kinoshita.dev artifact host in v1.

Caching: downloaded .kn86release files stay in the cache dir keyed by SHA-256 so re-flashing the same version is instant the second time.

ADR-0011 §“Update flow” L114 already specifies: after write, the helper recomputes SHA-256 of the written payload and compares to the header’s stored digest. This pack scopes the UX:

v1 in-scope:

  • Post-write SHA-256 verification pass (Screen 4), surfaced as a separate progress phase in the UI. Failure → ERROR screen with token SHA256_MISMATCH.
  • Header validation before write (magic, version, reserved-region zeros, file truncation) using the kn86-fw-format workspace crate. Failures shown on Screen 2 (CONFIRM) before the user can click “Begin flash.”

v1 explicitly deferred (separate task):

  • Boot-once verification. ADR-0011 mentions a 60-second boot-success sentinel that nOSh writes after a healthy boot; the tryboot_a_b flag is then cleared by either nOSh itself or by the flasher on next reconnect (ADR-0011 open question #2 L386). Wiring the flasher to clear tryboot_a_b on next reconnect is its own task — it requires a stable side-channel (probably g_serial) for the device to report “I committed” to the flasher. Defer to a sibling GWP-176-B (file later), or roll into the nOSh-owned commit path entirely.

The v1 verification message (“Slot B written and verified. Tryboot armed for next boot.”) is honest about this boundary — we verify the write, the device verifies the boot, and the user’s next-power-cycle determines commit. Don’t promise more.

Tauri 2.x produces native installers per platform via tauri-bundle. The packaging matrix:

OSFormatToolingCrosslink
LinuxAppImage (primary), .deb + .rpm (secondary, lower-priority)tauri-bundle + appimage-builder for the AppImage; bundles polkit policy + helper binaryExisting release CI (see docs/device/os/release-setup.md) extends to a parallel flasher-release job — distinct from the .kn86fw system-image release pipeline.
macOS.dmg containing notarized .app bundletauri-bundle + Apple notarization in CI. Universal binary (Intel + Apple Silicon).Coordinate with GWP-125 — that task is the macOS DMG packaging path for the kn86-emulator. The flasher reuses the same notarization cert + CI patterns; they’re independent artifacts but share infrastructure.
Windows.msi (primary), .exe (NSIS, secondary)tauri-bundle + WiX for MSI. Code-signed with an EV cert (long-term — for v1 prototype, a self-signed dev cert is fine; ship the SmartScreen warning until we get a proper cert).New CI job; nothing to crosslink yet.

Code signing strategy is a longer conversation tied to the broader release infrastructure — for v1 prototype distribution to a small alpha cohort, unsigned (with documented “right-click → Open” workaround on macOS, “More info → Run anyway” on Windows) is acceptable. Signing graduates to gating when we widen distribution. Cross-link this concern to ADR-0011 open question §“Signing and rollback keys” and the eventual release-infra ADR.

Helper bundling: kn86-flasher-helper is co-bundled with the UI in every package. Both binaries live in the same install dir; the UI invokes the helper by relative path. Updating one updates the other (no version-skew risk).

Scaffold proposal — tools/kn86flash-gui/ (or tools/flasher/)

Section titled “Scaffold proposal — tools/kn86flash-gui/ (or tools/flasher/)”

Directory layout when implementation gate opens. No files created in this PR — this is the proposal.

tools/kn86flash-gui/ # see open question #1 re: name
├── README.md # what it is, build, test, package
├── Cargo.toml # workspace member; depends on kn86-fw-format
├── src-tauri/
│ ├── Cargo.toml
│ ├── tauri.conf.json # window config, allowlist, bundle metadata
│ ├── build.rs
│ ├── icons/
│ │ ├── icon.png # 1024×1024 source
│ │ ├── icon.icns # macOS
│ │ ├── icon.ico # Windows
│ │ └── 512x512.png, 256x256.png # Linux
│ └── src/
│ ├── main.rs # Tauri app entry, command registration
│ ├── commands.rs # invoke handlers exposed to webview
│ ├── device.rs # SD device enumeration (cfg-gated per OS)
│ ├── helper.rs # spawn + protocol handling for kn86-flasher-helper
│ ├── source.rs # local file / GitHub release fetch
│ └── error.rs # error token taxonomy
├── src/ # webview frontend (Vite + vanilla TS or Svelte — open Q #4)
│ ├── index.html
│ ├── main.ts # state machine + Tauri invoke calls
│ ├── styles.css # Kinoshita amber/black palette
│ └── screens/
│ ├── idle.ts
│ ├── device_detected.ts
│ ├── confirm.ts
│ ├── flashing.ts
│ ├── verify.ts
│ ├── complete.ts
│ └── error.ts
├── package.json # frontend tooling deps
├── vite.config.ts
└── tsconfig.json
tools/kn86-flasher-helper/ # new sibling crate
├── README.md
├── Cargo.toml # workspace member; depends on kn86-fw-format
└── src/
├── main.rs # CLI entry, JSON protocol parser
├── write.rs # raw block-device write loop with O_DIRECT
├── verify.rs # post-write SHA-256 pass
└── allowlist.rs # hard-coded allowed device patterns
tools/kn86-fw-format/ # extracted from tools/kn86fw/src/header.rs
├── Cargo.toml # new workspace member, leaf dep of kn86fw + flasher
└── src/
└── lib.rs # Header parsing + SHA-256 verify, no IO
tools/Cargo.toml # workspace; add kn86flash-gui, kn86-flasher-helper, kn86-fw-format

Workspace impact: the existing tools/Cargo.toml workspace gains three members. tools/kn86fw/ gets refactored to depend on kn86-fw-format instead of inlining the header (a small follow-on PR — out of scope for the design pack but flagged as a precondition).

Acceptance criteria expanded (≥4 testable items with file paths)

Section titled “Acceptance criteria expanded (≥4 testable items with file paths)”

These criteria are for the design pack itself (this PR). Implementation acceptance criteria will be authored on the implementation task(s) when the gate opens.

  1. docs/plans/sprints/2026-05-01-gwp-176-tauri-flasher-design.md exists and matches the format of the other Sprint 4 design packs (story narrative, user stories, acceptance criteria, edge cases, engineering hand-off, open questions). Current PR delivers this.
  2. Wireframes cover all six screens (IDLE, DEVICE_DETECTED, CONFIRM, FLASHING, VERIFY, COMPLETE, plus ERROR variant). Each is a single ASCII viewport. Verified against §UI flow above.
  3. Backend protocol contract is concrete enough for an engineer to implement against — JSON event shapes, error token list, exit-code semantics. Verified against §Backend interface contract above.
  4. Per-OS enumeration + elevation paths are named with specific crates / system commands per OS, not abstract “use the OS’s native API.” Verified against §SD device enumeration table.
  5. Stale-ticket disambiguation is recorded — design pack notes that kn86flash/GWP-111 is retired and the actual CLI is kn86-flasher-helper. Verified against §Story narrative.
  6. Scaffold proposal lists every directory + file the eventual implementation PR will land. No files created in this PR; only the proposal exists. Verified against §Scaffold proposal.
  7. Cross-references to ADR-0011 and ADR-0020 are explicit with section/line numbers where load-bearing. Verified throughout.
  8. Open questions list is decidable — each item has a default answer the implementer would use absent Josh’s call. Verified against §Open questions.
  1. User picks a .kn86fw for a different KN-86 hardware revision. The header carries min_bootloader_version (per tools/kn86fw/format/kn86fw.h); the helper rejects with UNSUPPORTED_VERSION. The UI shows a clean error explaining “this firmware needs bootloader v3, your device has v2 — update the bootloader first or pick an older firmware.” Bootloader version comes from a side-channel (g_serial query) or — for v1 simplicity — is just not enforced and the device’s own boot-time check fails into tryboot rollback. v1 default: rely on tryboot rollback; surface min-bootloader-version mismatch as a CONFIRM-screen warning, not a block.
  2. User unplugs the cable mid-write. Helper detects via failed write() → exits non-zero with DEVICE_DISAPPEARED. UI surfaces ERROR screen with “slot B is now in an indeterminate state; slot A is unaffected; reconnect and re-flash.” This is the most common failure mode in S2 (dev iteration) and the UX needs to make it un-scary, not catastrophic — the A/B model means partial writes never brick.
  3. MSC volume mounts but doesn’t have the KN86_UPDATE label (e.g., user has a generic USB drive plugged in with a coincidental VID/PID match — extremely unlikely with a pid.codes-allocated PID, but defensive coding). Helper refuses to write with WRONG_VOLUME_LABEL. UI explains “this device looks like a KN-86 but the volume label doesn’t match — refusing to write to avoid corrupting your other USB drive.”
  4. Two KN-86 devices connected simultaneously (Josh has multiple prototypes on the bench during S2). UI lists both, user picks one. Helper writes to the picked one only; the other is unaffected. Multi-device flash (write same firmware to both serially) is a v2 nice-to-have, not v1.
  5. Helper binary is missing from the install dir (corrupted install, or AV quarantined it on Windows). UI detects via spawn failure → “kn86-flasher-helper not found at expected path; reinstall the flasher.” Don’t try to be clever about helper relocation in v1.
  6. GitHub releases API rate-limited / network down. UI falls back to the local file picker with a “couldn’t reach kinoshita.dev — pick a file you’ve already downloaded?” prompt. Don’t hard-fail the IDLE screen on network errors.
  7. User cancels the elevation prompt (UAC dismissed, polkit denied, osascript admin sheet cancelled). Helper exits with ELEVATION_DENIED; UI returns to CONFIRM screen with a polite “elevation required to write — try again?” inline message. Don’t crash, don’t moon-landing-explain.

Engineering hand-off notes (when gate opens)

Section titled “Engineering hand-off notes (when gate opens)”

Owner when implementation lands: ideally a single Rust + frontend engineer working across both tools/kn86flash-gui/src-tauri/ and tools/kn86flash-gui/src/. Estimate from ADR-0011 L429–433: ~4–5 days for the Tauri UI + Rust backend + ~2–3 days for the helper = ~7 days total, dispatched as one feature with two PRs:

  • PR-A: kn86-fw-format workspace crate extraction (precondition). Refactor tools/kn86fw/src/header.rs to live in tools/kn86-fw-format/ and re-export. ~1 day. No behavior change.
  • PR-B: tools/kn86flash-gui/ + tools/kn86-flasher-helper/ (the work). Tauri scaffold + helper binary + cross-platform CI integration. ~6–7 days.

Files NOT touched in either PR:

  • kn86-emulator/ — flasher is desktop tooling, not emulator code.
  • tools/sd-provision/ — that’s the upstream image producer; flasher is the downstream image writer. They share the .kn86fw format and nothing else.
  • /home/shared/ partition (p6) — flasher policy is “never open p6, never mount p6, never reference any device node other than the inactive slot’s bootfs + rootfs.” Hard-coded in kn86-flasher-helper/src/allowlist.rs. ADR-0011 L331.

Dispatch shape: single Rust engineer, one feature branch, two PRs (extraction first, then app). Frontend skill required (TypeScript or Svelte, Tauri webview integration). CI work to add MSI/DMG/AppImage build matrix is bounded — Tauri’s bundler does most of it, but the macOS notarization step will need a Josh-supplied Apple Developer cert.

Watch for: scope creep into Phase 3 features (auto-update server check, signing, OTA WiFi path) that ADR-0011 explicitly defers. Helper binary’s allow-list is the load-bearing safety mechanism; reviewer should confirm it can’t be bypassed by any UI invocation.

  1. Crate name: tools/kn86flash-gui/ (per the user brief on this task) or tools/flasher/ (per ADR-0011 L210, L448)? Recommendation: tools/flasher/ — matches the ADR which is the durable spec, and the parallel tools/kn86fw/ (image builder) + tools/flasher/ (image writer) is a clean naming pair. The brief’s kn86flash-gui was likely written before ADR-0011’s crate-name decision was inked. Default if not answered: tools/flasher/.
  2. VID/PID allocation. ADR-0011 L322 names this as an open question — request a free PID from pid.codes (community-managed range, free for hobby use), use the Pi Foundation’s allocated range (requires their permission), or pick something that won’t collide with existing USB devices. Recommendation: pid.codes — fast, free, well-understood. Default if not answered: file a pid.codes request as a Phase 2 prereq task.
  3. Update server URL for “Pull from kinoshita.dev.” Recommendation: GitHub Releases API on jschairb/kn86-deckline for v1 (no separate hosting infrastructure needed), with the URL displayed in the UI as kinoshita.dev/firmware (purely cosmetic — actual fetch hits GitHub). Default if not answered: GitHub Releases on this repo.
  4. Frontend framework inside Tauri. Vanilla TypeScript + a tiny state machine library, vs. Svelte (lightweight, reactive, compiles to vanilla), vs. React (heavy for this use case). Recommendation: Svelte 4 — minimal runtime, reactive, the seven screens are static enough that React’s component-tree complexity is unjustified. Default if not answered: vanilla TypeScript (lower learning curve, no framework lock-in, easier review for the C-engineer-heavy team).
  5. Recovery mode (S4) — in v1 or deferred? Recommendation: defer to v2 (file as GWP-176-C). v1 covers S1–S3, which is 99% of expected usage; recovery is rare and well-served by pulling the SD and re-imaging via tools/sd-provision/. Default if not answered: defer.
  6. .kn86release composite (per ADR-0020) — does flasher v1 consume .kn86fw only, or .kn86release from day one? Recommendation: support both in v1 — .kn86release is just a tarball wrapping a .kn86fw, the extraction is trivial. Designing for .kn86release from day one avoids a v1.1 rework. Default if not answered: support both.
  7. Boot-once commit (clearing tryboot_a_b from the flasher) — in v1 or deferred? Recommendation: defer to a sibling task (GWP-176-B). v1 ships verified-write; commit is its own concern that needs the g_serial side channel design (ADR-0011 open question #2). Default if not answered: defer.
  8. Implementation gate. Per parent GWP-163, no sub-task goes In Progress while GWP-152 is open. This pack ships under that gate (design only). Confirm: the implementation tasks above (PR-A crate extraction, PR-B app scaffold) wait for GWP-152 close before being filed / dispatched, OR file them now in Planning so they’re queued for the v0.1-ship-day sprint kickoff. Recommendation: file in Planning now, dispatch when gate opens.