92be912d73
The Desktop's Update button hands off to the staged Tauri binary (HERMES_HOME/hermes-setup.exe). That binary has no self-update path (copy_self_to_hermes_home no-ops during --update), so every updater-side fix only reaches users when a new installer is built, signed, and published. In practice the published binary lags main by months and users hit long-fixed bugs on every GUI update: the 2026-08-09 incident chain was four distinct failures (stale install.ps1 cache resolver pre-#67369, marker adoption pre-#74782, straggler teardown) all caused by a June 4 binary running against an August repo. This inverts ownership: scripts/desktop-update.ps1 lives in the repo checkout, so every `hermes update` refreshes the code that drives the NEXT update. Only PowerShell itself - an OS component - stays frozen. Desktop side (apps/desktop/electron): - resolveUpdateScriptHandoff() (updater-process.ts): returns the spawn recipe when scripts/desktop-update.ps1 exists in the checkout; Windows-only (POSIX updates in place via applyUpdatesPosixInApp); null on old checkouts -> caller falls back to the staged binary path completely unchanged. - applyUpdates() prefers the script hand-off. The marker pre-write is ALWAYS safe on this path - no stagedUpdaterSupportsPrewrittenMarker() mtime heuristics - because hermes_cli/update_lock.py's UpdateLock adopts a live marker held by a process ANCESTOR, and the script is the `hermes update` child's parent. This closes the unguarded marker-gap window that pre-#74782 binaries force today (the 23:56 failure in the incident: 'skipping marker pre-write: staged updater predates self-adopt' -> renderer respawned a backend into the gap -> update refused). - CLI-installed users (no staged binary) now get the script hand-off too instead of the manual `hermes update` card, when the script exists. Script (scripts/desktop-update.ps1): waits for the Desktop pid to exit (bounded 30s), waits for the venv shim to unlock (mirrors the Rust is_locked probe, bounded 20s), runs `hermes update --yes --gateway --force --branch <ref>` from the CURRENT checkout with one retry for the update-boundary class (skipped for exit 2), removes the marker on every exit path, relaunches the Desktop. ASCII-only (the #67193 lesson), logs to logs/desktop-update-handoff.log. Verification (real Windows box): - apps/desktop: typecheck (3 projects) clean, eslint clean, vitest updater-process.test.ts 12/12 (3 new resolver tests). - Script E2E against a sandbox HERMES_HOME with a compiled fake hermes.exe: correct argv (update --yes --gateway --force --branch main), stale marker removed, exit code propagated (0 and 1 paths), retry-once fires exactly once on failure, PS 5.1 parse + windows footguns check clean. - Contract E2E with the real UpdateLock: ancestor-owned marker adopted (True), left in place on release, foreign live holder still refused.