docs GitHub

ContributingDraft

Windows support: tiers and plan

Decision and what shipped from it: ADR 0037. No Windows hardware was used for this pass; recommendations that depend on Windows-only behavior are marked unverified.

Acceptance criteria for Tier A and Tier B, stated now rather than retrofitted later (cohesion's interaction pass, docs/design/interaction.md, binding for 0.1.1): a Windows device is not "the Mac app minus native bits." It streams (memory.ask {stream:true}, Render {kind} once a command's answer is slow), it updates live off events rather than polling (sight.stepped, context.changed, waiting.changed), and its motion, card/row states and gestures follow one vocabulary (docs/design/one-app/DIRECTION.md's smoothness bar and docs/design/system/components/*), the same as every other surface. Tier A ships this for free: the Deck and PWA on Windows are the same build as everywhere else, so whatever interaction.md specifies there is already there. Tier B ships it because vyred under WSL2 is the same server code as Linux; nothing about the interaction language is Mac-specific, and none of it is deferred to Tier C.

1. Inventory

vyred (core/deck/box/CLI) is already cross-platform: node.yml runs the full npm test suite on ubuntu-latest (Node 22/24), so core, deck, modules, box and most of the CLI pass with no macOS. Mac-only code is concentrated and gated:

  • Swift native: local/capsule/native (the Capsule app), local/hands-mac, local/screen-mac, local/sideview, local/voice/swift, core/vault/mac, core/presence/touchid, core/link/se, plus the iOS app (apps/ios). All local/*-mac modules, built via lumen-mac.yml.
  • process.platform === 'darwin' gates (~60 files): vault (keychain, Touch ID, login-keychain), clipboard (pbcopy/pbpaste), local/apps (installed-app discovery via osascript), CLI commands (box, capsule, up, vault, doctor, connect, relay), core/system, core/link/mac.js, core/sessions/spawn.js, core/daemon/peer.js, core/files/index.js. These mostly branch cleanly to a non-mac path today (stub/no-op), which is the pattern to extend.
  • osascript: harness rules/shell, files/drive (Finder), local/apps (app launch/discovery), keychain test doubles.
  • security(1)/keychain: core/vault/keys.js, core/vault/mac/keychain.swift, capsule-native.js, this is the whole native-vault backend on Mac.
  • launchd: core/daemon/main.js/peer.js (vyred as a LaunchAgent), local/capsule/native build/install, local/voice/build.sh, local/apps/installed.js. Linux equivalent today is systemd/Docker on the server; nothing here assumes launchd outside these files.
  • Already cross-platform: the server (Docker on Linux, the recommended home), Deck (web), CLI core, Expo phone app + PWA, harness/plugin (harness/), relay, Tailscale integration (core/names/tailscale.js shells out to the tailscale binary, not Mac-specific).

Conclusion: the kernel and server were already built to run headless on Linux. The Mac-only surface is the Capsule (native UI, hotkey, screen context, hands/voice) and the native vault backend, exactly the parts a Windows Capsule would also need to newly build, not retrofit.

2. Tiers (cheapest first)

Tier A, Windows as a device against a Linux server. Deck in a browser, the PWA, CLI, and Claude Code + the Vyre plugin (harness/) all run on Windows today with near-zero change, since they're pure Node/web, and inherit the interaction language above for free (same build, same components). Gaps: bin/vyre and scripts/postinstall.mjs need testing on Windows paths (no ~, backslash paths, no chmod); the CLI's darwin branches need an explicit "unsupported here, use the Deck" message instead of failing silently. No native code required. This is close to done already; verification is the remaining work.

Tier B, vyred running on Windows itself, for someone who wants their own PC as the server instead of Linux. Recommend WSL2, not native Windows Node: the server's Docker Compose (box/compose.yml), systemd-style daemon assumptions, and tailscale/osascript-shaped shell patterns all map onto WSL2's Linux userland with no code change, and Docker Desktop already targets WSL2 as its backend. Native Windows Node would need a second platform branch throughout core/daemon, core/vault, core/files, local/apps for every darwin gate, real, ongoing maintenance. WSL2 costs a heavier install (Docker Desktop or WSL) but reuses the whole Linux server image unchanged, including every server-side interaction feature above.

A concrete "Windows already feels alive" milestone, before any Tier C/D native work: suggest and sight are both built, server-side tools with no Mac dependency at all. The moment vyred runs under Tier B (WSL2), suggest.query (live command-bar completion) and sight.now/ sight.stepped (the computer-use status strip, over Glass or the Deck) work unmodified, on Windows, today's code, no port. Worth stating and testing as its own deliverable, not folded silently into "Tier B works": it's the first proof that a Windows server isn't a lesser one.

Tier C, a Windows "Capsule." Recommend Tauri over WinUI 3/.NET or Electron: Tauri gives a lightweight native shell (Rust core, a web view) matching the design intent behind retiring Electron on the Mac, with a real global-hotkey API (tauri-plugin-global-shortcut) and small footprint. Build its UI directly on docs/design/system/components/*, the same card, row, button and status vocabulary every other surface uses, not a reimplementation of Vyre's interface in a new toolkit from scratch: a Windows Capsule that invents its own component language would be exactly the "flatter, different product" this plan is trying to avoid, just arriving at 0.2 instead of 0.1. WinUI 3/.NET (C#) is the "most native" option and gets deepest UI Automation access for screen context, at the cost of a second UI codebase with no code-sharing with the rest of Vyre. Screen context and computer use are both anchored on Windows UI Automation (UIA), the Windows analogue of the accessibility tree local/hands-mac/screen-mac already use, so the module shape carries over even though the API doesn't. Tauri is now confirmed (the lead, 2026-09-28), not just recommended; the full build plan (hotkey, tray, toast, Windows Hello, signing/pinning, CI) is section 9. Windows Solo (a Windows PC running everything alone, no Tailscale, per ADR 0039) is section 8, ahead of Tier C in build order per the lead's current instruction even though it was originally scoped inside Tier B/C above.

Tier D, computer use, voice, credentials. Computer use: UIA for observe, SendInput for act, mirroring hands-mac's observe/find/act/commit/verify loop and its floor (no acting in password/sign-in surfaces) and stop-key contract. Voice: existing Deepgram/voice module is already network-based, not Mac-specific, porting is mostly the audio-capture layer. Credentials: Windows Credential Manager replaces Keychain (core/vault/keys.js's security(1) calls), Windows Hello replaces Touch ID (core/presence/touchid), both have first-party Node bindings or a small native helper, same shape as the current keychain-helper/touchid split.

3. Multiple Windows PCs per person

Same pattern as multiple Macs today: each device pairs into the person's tailnet and gets a device identity (ADR 0032, person-and-device); "which device is mine" / "answer on this PC" is already a federation concern (ADR 0021, box-reads-the-mac) generalized to N capsules, not Mac-specific, a Windows Capsule is just another federated peer. Vyre Drive (Taildrive) is a Tailscale feature, not ours; Tailscale ships a native Windows client and ADR 0014 (tailnet) already treats the tailscale binary as a black box via core/names/tailscale.js, so this should carry over unchanged. Unverified: Taildrive's Windows-side file-share UX specifically.

4. Modularity

Every Windows piece ships as its own local/*-win module (local/hands-win, local/screen-win, local/vault-win or a vault backend switch, local/capsule-win) behind the same registry/manifest contract local/hands-mac already uses (module.json with does.tools, watches.emits, no cross- feature imports, enforced by test/boundaries.test.js). No fork of core; the existing darwin/else branches in files like core/vault/keys.js become a real platform-backend switch (darwin -> mac module, win32 -> win module, else -> unsupported) rather than new special-casing per file.

5. Testing without Windows hardware

  • windows-latest GitHub Actions runners: add a windows job to node.yml (or a new node-windows.yml) running the same npm test, this alone would catch path/shell assumptions today, since nothing currently runs the suite on Windows.
  • A dedicated local/*-win module's Swift/native-equivalent build (Tauri/Rust or C#) needs its own workflow, same shape as lumen-mac.yml/ios.yml/android.yml, a lumen-win.yml building on windows-latest.
  • A Windows VM (Parallels/UTM on the Mac, or a cloud Windows PC) is the only way to hand-test UIA, Windows Hello and the hotkey, recommend this only once Tier C is actually being built, not for the Tier A/B assessment.

6. Sizes, order, 0.1.x vs later

Size0.1.x?
Tier A verification (Deck/PWA/CLI/plugin on Windows, path fixes, CI job)Syes
Tier B (WSL2 server path, docs + install script branch)S-Myes
Tier B's suggest/sight milestone (state + test it, no new code)Syes
Tier C (Tauri Capsule shell + hotkey, no screen/hands yet)M0.2
Tier D (UIA hands/screen, voice port, Credential Manager/Hello vault backend)L0.2+

Recommended order: A -> B -> C -> D. A and B give every current Windows-using client a working path with days, not weeks, of work and no new native surface. C/D is a real second native client to build and maintain long-term, worth it once Windows users want Capsule-parity (hotkey, screen context, computer use), not before.

Timeline correction, the lead, 2026-09-28: the local vyred socket turned out deeper than a LOW-severity ACL fix (section 7a): a squatting-resistant name, a native helper for the peer check, and a Windows presence method all need building first. Windows ships in 0.1.2 or later, not 0.1.1: get it right rather than fast. The "yes" column above still describes what Tier A/B are (no change to scope), just not the release they land in until 7a's open items close.

7. Risks and open questions

  • RESOLVED without hardware, per the lead: proven in CI, not left to a physical machine, and the real fix turned out to be a different Windows primitive, not an ACL. The first two rounds chased an ACL problem that didn't exist: config.socketPath()'s win32 branch put the socket under a per-user %LOCALAPPDATA%\Vyre\sockets folder, and ensureWindowsSocketDir set an explicit icacls grant on it (current user by SID + SYSTEM, inheritance stripped). Every windows-latest run still failed listen() with EACCES, and a diagnostic round proved why: a brand-new folder with zero icacls calls applied failed the identical way, and whoami /priv showed SeCreateSymbolicLinkPrivilege Disabled for the runner's token. A bound socket file on Windows is implemented as an NTFS reparse point, which needs that privilege to create; most Windows accounts, this runner's included, don't hold it, and a real person's account won't either unless Developer Mode is on. The fix: socketPath()'s win32 branch now returns a literal named pipe name (\\.\pipe\vyre-<hash>), never a filesystem path. A named pipe needs no privilege and no folder: Node gives it a current-user-only security descriptor by default (nothing here passes readableAll/writableAll), the same restriction chmod 0600 gives the POSIX socket. ensureWindowsSocketDir, currentUserPrincipal and scripts/win-socket-acl-check.mjs are gone; there is no folder or ACL left to set or check. .github/workflows/node.yml's windows-socket-acl job now proves the actual security property directly: starts a real vyred, waits for the pipe to answer, connects as the owner (scripts/win-connect-probe.mjs), then proves a second local user's connection attempt is refused. Not continue-on-error: if this job cannot be made to pass, Tier A ships in 0.1.1 marked "preview" with the gap written down, per the lead's call, not silently green.
  • Tier B WSL2 requires Docker Desktop (licensing cost at company scale) or bare WSL2 + Docker Engine, which does he want documented/supported?
  • Tier C native cost: Tauri vs WinUI 3 is a real fork in long-term maintenance burden (Rust+web vs C#), worth prototyping both before committing an ADR.
  • UIA parity with the macOS accessibility tree is unverified in depth: some apps (Electron, custom- drawn UI) expose weaker UIA trees than AX on Mac, which could make hands-win less reliable than hands-mac for those apps.
  • Windows Hello / Credential Manager: unverified whether a Node-native binding exists at the quality of the current Swift keychain.swift/touchid helpers, or whether a small C# helper process (mirroring the Mac helper-binary pattern) is needed.
  • Taildrive on Windows: unverified UX; needs a hands-on check once a Windows PC exists.
  • Claude Code and Tailscale Windows support: both ship official Windows clients (Claude Code via npm/native installer, Tailscale via its Windows app) at a level that should cover Tier A/B, but neither was hand-verified against this repo's specific assumptions in this pass.

7a. Named-pipe security design (reviewed; rules below are binding, per the lead)

Section 7's named-pipe fix (the socket now binds) solved the wrong-privilege problem, but raised three things a filesystem-path socket never had to answer that a pipe does. Reviewer answered all three; the lead confirmed the rules are binding. This is why section 6 now says Windows ships in 0.1.2 or later: get it right rather than fast.

  1. The security descriptor cannot be assumed - confirmed, and now a hard gate. Reviewer: the raw CreateNamedPipe default (a NULL security attributes struct) gives Everyone and Anonymous read, and only the creator, SYSTEM and Administrators full access - Node does not necessarily override this fully for a bare listen() call the way this plan assumed. .github/workflows/node.yml's "a second local user cannot connect" check is now a hard, non-negotiable gate: Windows does not ship until it passes for real (not continue-on-error, already true; the point is it must actually be green, not merely present). If it fails once run, an explicit DACL for the owner SID alone is set from the same native piece as point 3.
  2. Squatting - real, and worse than denial (reviewer): a squatter who owns the name receives the CLI's requests, including presence proof headers, and can play vyred to the person. Three parts, all required: a. Done (core/config/index.js's pipeToken). A random, per-home token folded into the pipe name (crypto.randomBytes(16), persisted at <root>/pipe-token, a sibling of config.json inside the home folder rather than a field inside it, since socketPath is called before config.load() in places and a dedicated file avoids coupling to the config-merge machinery). Its own protection is the home folder's normal per-profile protection (%USERPROFILE%\...), the same thing config.json itself already relies on; this doc treats that as sufficient rather than re-running an icacls check, the exact tool that solved the wrong problem in section 7. Tested: core/config/config.test.js (two new cases - the token is a 32-hex-char random value, not derivable from root; two different homes never share one). b. Done, verified empirically rather than assumed (.github/workflows/node.yml's windows-socket-acl job): whether libuv sets FILE_FLAG_FIRST_PIPE_INSTANCE was left an open question in this doc; rather than resolve it by reading libuv's source, the job now starts a second vyred against the exact same home while the first is still running and asserts it exits non-zero (via core/daemon/index.js's existing fs.existsSync+ping pre-listen check, which turns out to need no win32-specific code at all: a crashed vyred's pipe cannot linger the way a POSIX socket file can, since the OS destroys a named pipe the moment its last handle closes, so "exists and answers" already means "a live vyred is running," full stop) and that the first vyred is still the one actually listening afterward. No silent fallback to observe, because there is no code path that would produce one. c. Done, CI-green. core/daemon/native-win (vyre-pipe-verify, a small standalone Rust binary, windows crate) opens the pipe as a client, asks GetNamedPipeServerProcessId who owns the server end, then EqualSid between that process's token SID and the caller's own
    • exit 0 only on a proven match (Verdict::exit_code), every other case (a mismatch, or any Win32 call failing partway through) exits non-zero, so "couldn't tell" can never look like "safe to send." lib.rs's verdict logic is unit-tested on any platform (3 tests); the actual Win32 plumbing (main.rs) only ever runs for real on windows-latest (.github/workflows/windows-pipe-verify.yml), which proved it end to end on the first fully green run: passes against vyred's own pipe, refused against a pipe owned by a different local account (the same net user trick windows-socket-acl uses, since a same-user "other pipe" would legitimately verify as ok and prove nothing). Took three CI rounds to get the Win32 call shapes right (a missing crate feature, LocalFree in the wrong module, EqualSid returning Result<()> rather than a raw BOOL in this crate's binding, Ok meaning equal) - nothing about the security logic itself changed across those, only which Win32 API shape got it there. Not yet wired into the CLI's actual connection path; this is the tool, not the integration.
  3. The peer check: a standalone helper exe, not a native addon inside vyred, and never "unknown means allow." Reviewer's design: a small helper process owns the pipe (or sits in the accept path), calls GetNamedPipeClientProcessId per connection, and forwards to vyred over a private channel carrying the kernel-given pid - the Windows shape of peer.js's perl- over-inherited-fd trick, without coupling to libuv's internals from inside the same process (the N-API-addon option this doc floated earlier is dropped; a separate helper is cleaner and is what's binding now). Critically: "peer unknown" must never be treated as "allow." ancestry()'s POSIX fallback for an unreadable chain is fail-open-shaped (falls through to other checks); on Windows a missing peer pid has to fail closed for PERSON_ONLY, the same direction as the ancestry warning below, not the graceful degrade this doc proposed before review.
  4. Bigger warning (reviewer): process ancestry is much weaker evidence on Windows than on Mac/Linux, independent of whether the peer pid itself is trustworthy. PROC_THREAD_ATTRIBUTE_PARENT_PROCESS lets any process create a child claiming an arbitrary parent pid (explorer.exe, say) at the same privilege level - a model can forge its whole ancestry chain on Windows in a way it cannot on Mac/Linux. Consequence: PERSON_ONLY (core/presence) on Windows must rest on presence, not on ancestry. core/daemon/peer.js's ancestry()/insideClaudeSession() machinery stays exactly as useful as it is today for Mac/Linux; on win32 it is not the trust anchor, presence is (below).
  5. Windows presence method, the lead's final call (superseding an earlier draft that included a password prompt - dropped, since a password can be keylogged or phished; a model can defeat none of these three):
    1. Windows Hello, when the device has it.
    2. The UAC consent prompt ("Allow Vyre to verify it's you?"), which runs on the secure desktop by default - a model can raise the prompt but cannot click it, and no password ever touches Vyre's own UI. If the machine's UAC is set to "never notify" (so no secure-desktop prompt would appear), skip straight to step 3 rather than show a weaker, spoofable prompt.
    3. Approve on the paired phone (the existing device presence method, unchanged). Once per terminal session, same cadence as the Mac's presence-once. core/presence needs a new method (working name winhello, covering both the real Windows Hello case and the UAC-consent case, since both resolve to "the OS proved a human clicked something only a human can reach") alongside "capsule"/"touchid"; the approved record lives in the Windows equivalent of vyre-core (point 6), never in a file the person's own account can write - a model running as that same account must not be able to forge or extend its own presence window.
  6. ADR 0040's same-uid vault problem applies to Windows too, and the Mac fix does not cover it. On the Mac, vyre-core is a separate account so the vault process and the person's own (possibly model-driven) shell are not the same trust boundary. Windows needs its own version of that split - a service running under its own account (a Windows service account, not the signed-in person's) - not an assumption inherited for free from the Mac work. Scoping this (and point 5's winhello method, and point 3's helper exe) is one conversation with whoever owns core/presence and ADR 0040, not three separate asks landing on them piecemeal.

Status: 2a, 2b and 2c done and CI-green (socketPath includes the random token; a second vyred against the same home is proven to refuse rather than move; the client-side owner-SID check passes against vyred's own pipe and refuses a different local account's). Started directly with reviewer (the lead's call, e2e being restarted): winhello and the point-3 helper exe's design, proposed but not agreed yet. Still open: 1 (the hard second-user-refused gate now runs against the token'd name, still worth a deliberate look rather than treating "it's green" as proof of the DACL claim specifically), 3 (the peer-check helper, likely sharing core/daemon/native-win with 2c once its shape is confirmed), 4's winhello presence method, and 6's Windows vyre-core equivalent (waits on ADR 0040). 2c is built but not yet wired into the CLI's actual connection path - the tool exists and is proven, the integration is next.

8. Windows Solo build plan (the lead's 2026-09-28 "Vyre anywhere" call)

ADR 0039 (owner: anywhere, docs/design/anywhere.md) makes "solo / server / device" a config.machine choice with no OS baked into the model: core/modules/index.js's role→bucket mapping doesn't know or care what OS is running "solo" or "device"; only the local/* modules do. Windows Solo is that seam, filled in for win32, not a new role system:

  • One-command install. winget install vyre (or an .msi/.exe from the release, if winget publishing isn't ready for 0.1.x) runs the same scripts/postinstall.mjs path Tier A already proved on Windows, then offers "run this PC as Solo" the same way onboarding's "make this the server" path does on the Mac. No WSL2, no Docker: Solo is native Windows Node running vyred directly (Tier A/B's distinction, "native Windows Node vs. WSL2," is about being a server for other devices; a Solo box with nobody else attaching to it has no need for Docker Compose or the eight box-only modules at all, so it skips that cost entirely).
  • anywhere.md's server-setup flow, a Windows twin, mirroring the Mac section 1:1, same three steps, different OS primitive. Corrected by the lead, 2026-09-28: no Windows Service. A Service runs as SYSTEM or a dedicated service account, not as the signed-in person, which breaks both the one-user trust model and ensureWindowsSocketDir's ACL (granted to the person's own SID, section 7): a SYSTEM-run vyred would still work for that account, but every other assumption in this plan treats vyred as running as the person, not as the machine. Use a per-user Task Scheduler logon task instead, via the built-in schtasks (no node-windows or third-party service-wrapper dependency, no spike needed):
    1. Sets config.machine (asks Solo-vs-Server the same way onboarding does).
    2. schtasks /create /tn "Vyre" /tr "node core/daemon/main.js" /sc onlogon /rl limited runs vyred as the signed-in person the moment they log on, restarting a Solo box's vyred exactly the way the Mac's LaunchAgent RunAtLoad does, at the person's own privilege level (never elevated), which is what the socket ACL already assumes. Logs still go to core/config's existing ~/.vyre/logs/ (Windows equivalent of ~: %USERPROFILE%).
    3. For "this Windows PC is the server" (always-on), the same user-level task set to run whether or not the person is logged on (schtasks's /rl and /it options, or the "Run whether user is logged on or not" flag in the underlying task definition XML), plus a power setting: powercfg /change standby-timeout-ac 0 (or SetThreadExecutionState (ES_SYSTEM_REQUIRED | ES_CONTINUOUS) held by vyred itself while it's the server) blocking idle system sleep only, display sleep untouched, same boundary as the Mac's caffeinate -s. Still a per-user task, still running as the person, just no longer gated on an interactive logon.
    4. Task Scheduler's own restart-on-failure action (schtasks /create ... /ri <minutes> /du 9999:59 or the task definition's <RestartOnFailure>) stands in for KeepAlive, restarting a crashed vyred, not a clean vyre down.
    5. --undo runs schtasks /delete /tn "Vyre" /f; config.machine is the move flow's to change, same division as the Mac.
  • Deck in the browser is Tier A already: vyre up opens http://localhost:<port> (or the paired device flow once a second device exists); Solo needs nothing new here beyond what Tier A verification already covers.
  • Owner split, per the lead: windows builds the schtasks/keep-awake/install-script pieces above (this section); anywhere owns ADR 0039 and the role system itself; federation owns the move-to-server engine (unchanged by Windows: a Windows Solo box moving to a Linux/Mac server, or vice versa, goes through the same copy-then-flip flow, cross-platform by construction since it moves files, a re-indexed store and re-encrypted vault entries, not OS-specific state).
  • Open: none blocking; schtasks is a built-in, no spike needed. Still worth a capsule- win.yml-style CI proof (create the task, log off/on isn't drivable in CI, but the "runs whether logged on or not" flag and the restart-on-failure action can both be asserted via schtasks /query right after creation) before committing to the exact flag set in an ADR update, same "prove it in CI, not on hardware we don't have" discipline as the socket ACL work in section 7.

9. Tier C build plan (the Windows Capsule)

Per the lead's 2026-09-28 instruction: build a thin native shell, not a second UI codebase. The Mac Capsule's content model (rows, tools, events, tokens) is reused as-is; only the shell differs. app-design's spec is docs/design/system/components/capsule-windows.md (sha d044f0e1 on work/app-design): Mica on the panel (Acrylic only on the transient tray menu), Segoe UI Variable/Segoe UI ahead of Helvetica Neue in the font fallback, DWM rounded corners, a tray icon

  • native-feeling context menu, Windows Toast for Needs-you items when the panel is closed, "Windows Hello" copy (never "fingerprint"/"Touch ID"), high-contrast and transparency-off fallbacks to solid tokens. That spec is the source of truth for anatomy/states/copy; this section is only the build plan.

Shell: Tauri (Rust + WebView2), per windows-plan.md section 2's original recommendation, confirmed by the lead over WinUI 3/C#: it hosts Deck's existing web views (deck/views/{ask,find, now,needs}.js + chat) unmodified inside the panel, so the Capsule's content is the same build as every other surface, not a reimplementation. Fall back to a small C# + WebView2 app only if a concrete Tauri gap shows up (a specific WinRT API Tauri's plugin ecosystem doesn't reach), not a default; switching shells mid-build is expensive, so this is a one-time call to make early, not revisit per-feature.

Native bits, each a thin Rust binding, no business logic on the native side (the pattern is "native calls a Vyre tool over the socket, same as the Deck's fetch does": the Capsule's brain stays server-side, in core//local/capsule-win, never duplicated into Rust):

  • Global hotkey: tauri-plugin-global-shortcut, default Alt+Space (the lead's call, 2026-09-28: RegisterHotKey wins over the system menu when nothing else holds the key, and matches PowerToys Run/Raycast-for-Windows convention). While the Capsule panel itself has focus, the app's own keydown handler intercepts Alt+Space and closes the panel before it can reach Windows' system-menu handling, so the system menu never opens on a borderless window that has none to show. If RegisterHotKey fails (another app already holds Alt+Space), Ctrl+Alt+Space is the fallback and the person is told once (a toast, not a silent swap). Configurable in Settings either way. Needs a hands-on check on real Windows (app-design's and the lead's shared flag): windows owns verifying the focused-panel case specifically, most reliably via a lumen-win.yml CI job that opens the panel and asserts which handler wins, falling back to a manual pass on the first Windows VM available if CI can't drive real OS keyboard focus.
  • Tray icon + menu: Tauri's tray API, Acrylic backdrop on the menu only (per app-design's Mica/Acrylic split), items exactly as specced (Open Capsule, "Needs you" count, Settings, Quit Vyre).
  • Toast notifications: tauri-plugin-notification (wraps Action Center), same words as the Needs row, inline actions where the platform allows.
  • Windows Hello for presence: stands in for Touch ID's role in the confirm-send floor (DIRECTION.md principle 6, only sends/posts/payments/deletes ask for it). Candidate API: Windows.Security.Credentials.UI.UserConsentVerifier via a small WinRT binding (Rust's windows-rs crate, callable from Tauri), needs a spike to confirm quality/availability parity with the Mac's keychain.swift/touchid helper before committing; this is windows-plan.md section 7's existing open question, not new.
  • Screen context via UI Automation: explicitly later per the lead's message (listed for sequencing, not this pass): mirrors local/screen-mac's shape once started, Tier D territory.
  • Proof to vyred: the same model as the Mac's cdhash pin (e2e's work/e2e-setsid), an Authenticode signature check plus a hash pinned at install time, so vyred only accepts calls from the exact signed binary a person installed, not "anything claiming to be the Capsule." windows implements the Windows-side pin; the pinning mechanism (where the hash is stored, how it's verified on each call) should reuse core/link/se's or e2e's existing pin-storage shape rather than inventing a second one; needs a short sync with e2e before landing, flagged under Needs from others.

Module shape, corrected after reading the actual Mac Capsule module: local/capsule is already the one cross-platform module (capsule.status/show/report, no OS in its tool names), with the native app living in a platform subfolder (local/capsule/native, Swift) that index.js's native() check gates on process.platform === "darwin". A separate local/capsule- win module declaring the same tool names would collide in the registry, and duplicate the tool definitions for no reason: local/hands-mac is a different shape (there is no cross-platform hands module today, only the Mac one) and isn't the right template here. The actual plan: a new sibling local/capsule/native-win (the Tauri project) and native()/the autostart spawn logic in local/capsule/index.js extended to recognize win32 alongside darwin. No fork of core, no new manifest entity, per section 4 above; docs/design/windows-plan.md's own text above (module shape bullets under Native bits) should be read with this correction in mind.

CI, no Windows hardware: a lumen-win.yml workflow on windows-latest, same shape as lumen-mac.yml: build the Tauri shell (cargo tauri build or build.rs-driven, TBD once the project scaffold exists), run its Rust unit tests, and as much of the hotkey/tray/toast surface as CI can actually drive headlessly (likely: unit-test the hotkey-conflict handler and the Authenticode-pin check in isolation; a real focused-window Alt+Space race may not be CI-drivable at all, in which case that specific case is flagged "unverified without hardware" rather than silently assumed to pass, same discipline as section 7).

Sequencing: doc (this section, done) → agree the look with app-design (done, d044f0e1) → scaffold started per the lead's instruction to begin before capsule-pro's contract reply, since the shell hosts Deck's web views and doesn't need it yet: local/capsule/native-win (corrected module shape, above) now holds hotkey.rs, the Alt+Space-default / Ctrl+Alt+Space-fallback decision and the system-menu-preemption logic, host-independent, unit-tested (9 tests), and lumen-win.yml runs them on windows-latest. Not yet built: the actual Tauri app (tray, WebView2 panel, Windows Hello, sign/pin), local/capsule/index.js's native()/autostart gate extended to win32, or core/cli/commands/capsule.js's Windows equivalent of capsule-native.js's build-and-launch flow (deliberately not wired yet: claiming native: true for Windows in capsule.status before there's a real binary to build would be worse than saying nothing). Once capsule-pro's contract reply lands: wire the app shell to it → tray/toast → Windows Hello spike → sign/pin. Each milestone reported to the lead as it lands, per instruction.

Needs from others:

  • capsule-pro: confirm the Windows Capsule calls the same tools/events the Mac Capsule does (no new server-side surface for Windows specifically) before windows starts wiring the Rust side to them.
  • e2e: the pin-storage shape from work/e2e-setsid's cdhash work, to reuse rather than duplicate for the Authenticode pin.
  • A Windows VM, once CI's headless coverage runs out (the Alt+Space focused-panel case, primarily).