What can the user do, how do they reach it, and what would prove it works? This maps existing behavior. ROADMAP.md owns future work; AGENTS.md owns implementation invariants.
Initial source baseline: September 1, 2026, 9e9da53aa9ed. Public macOS release:
2.3.1, with custom-domain downloads, a GitHub DMG mirror,
and model-download recovery guidance. The initial map was source-only. Listed checks are
locators unless an executed result is explicitly recorded, as in the
keyboard-layout verification.
Hex // macOS release
├── Get ready // install, permissions, local model
├── Dictate // hold OR double-tap lock
├── Use the result // paste, paste-last, History
├── Adjust behavior // shortcut, microphone, recording preferences
├── Process text // Output tab + context-selected Modes
├── Opt in // Meetings: off by default
├── Recover // prove the next attempt works
└── Maintain the app // menu bar, login, updates, quitDictation, Meetings, and Recovery have detailed maps. Agents can read retained transcripts and control the running app with the release CLI. Labs is one switch, on by default, for every unfinished feature. Its Restore shortcut default toggle (off by default) puts a Restore default button beside Paste last dictation's shortcut (#613). The vocabulary biases transcription toward names you list. Annotate (macOS, Linux, and Windows) records the screen while you draw and talk and sends it to an agent. The other capabilities stay here until they need their own verification recipe.
Install the signed macOS app with brew install --cask anomalyco/tap/hex or the
manual DMG. The cask uses a versioned, checksum-pinned
DMG, requires Apple silicon/macOS 15+, and leaves updates to Hex's own updater. It does
not require OpenCode, launch the app automatically, migrate the legacy
Swift app, or forcibly replace an existing app destination. Homebrew uninstall
leaves runtime settings, models, and retained data intact.
Download Hex // setup.download-recovery
├── anoma.ly/hex names the visitor's platform // setup.download-button
│ ├── Mac -> DMG | Intel Mac -> disabled, "needs Apple silicon"
│ ├── Windows x64 / ARM64 (UA-CH) -> hex-setup-<version>-windows-<arch>.exe + SmartScreen note
│ ├── Linux -> copyable `curl … install-linux.sh | sh` + Arch/glibc requirement
│ ├── Phone -> Send yourself the link
│ └── Other platforms -> every build, Homebrew, the GitHub release
├── Website, README, installers, apps, feeds, appcast -> anoma.ly/hex/files -> R2 bytes (Worker hex-production-site)
├── Releases up to 2.3.1 -> downloads.hex.kitlangton.dev (Kit's frozen bucket) -> the same bytes
├── GitHub v<version> (app-v through 2.3.1) -> identical DMG, Setup.exe, and Linux bytes
└── Older app cannot reach its update feed -> manually install the DMG
Install on Windows // setup.windows-installer
├── Setup.exe, per user, no admin -> managed versions\<version>\hex.exe
│ ├── x64 without vulkan-1.dll -> stop with driver guidance
│ ├── another version installed (e.g. self-updated) -> install this one over it
│ └── finish -> Open Hex -> first-run tour downloads a model
└── Settings › Apps -> uninstall every version, shortcut, Launch at login; keep data
Install on Linux // setup.linux-installer
└── curl … | sh -> signed feed -> verified artifact -> launcher + hex.service
└── cannot start -> name the glibc floor or the missing Arch packages
Prepare local models // separate network dependency
└── Dictation -> huggingface.co and its download CDN
└── Transfer fails -> host-specific guidance -> restore access -> RetryThe website shows the pinned macOS version and platform requirements beside its
download links. App mirrors use explicit v… release tags (app-v… through 2.3.1) rather than GitHub's
repository-wide latest release, which may refer to the SDK. Builds on the release
service update from https://anoma.ly/release (docs/releasing.md); the 2.3.1
feeds, appcast, and the legacy r2.dev endpoint stay frozen for installed
clients. Hosting does not guarantee access through every corporate policy.
The site's hero is an animated, optional show: an idle head and an ⌥ keycap until the visitor presses it, then a voiced demo of messages, OpenCode prompts and fixes (WebGPU, with a Canvas 2D fallback). Audio loads only after that press; the website has no microphone button or microphone access. Reduced motion keeps a still until the press and then plays only the calm stage. See site authoring. Headless Chromium renders of the WebGPU, Canvas 2D, reduced-motion and phone paths showed no console errors; a deterministic 60 fps capture was reviewed frame by frame. These checks do not exercise real speakers or a physical phone.
The Windows installer is silently installed, reinstalled, installed over
another version, and uninstalled on x64 and ARM64 runners by
test-windows-installer.ps1; no
interactive install, SmartScreen prompt, or first launch on a real PC has been
observed. The Linux library diagnosis has no automated check. The site's
platform states were captured once in headless Chromium with ?platform=.
Sources: website, installer, release publisher, mirror publisher, macOS release publisher, Linux installer, and updater. Model transfer failures in transcription_models.rs explain the source host, retry, and separate prerequisites. These messages do not diagnose a particular firewall or establish a successful subsequent model download. Executed hosting checks and deployment details are recorded in Download Hosting. In-app feed defaults and recovery copy ship with 2.1.17; the initial 2.1.16 mirror preserves that release's original bytes.
Launch Hex, setup incomplete // setup.tour; full-window, own column
├── Upgrading from 2.3.1 or earlier (same `hex` folder) // setup.upgrade; macOS
│ ├── `anomaly.hex` is a new identity: macOS kept none of the old grants
│ ├── Settings without `anomaly-hex-identity` in `migrations` -> checked once at launch
│ │ ├── Permissions or the selected model not ready -> the old completion
│ │ │ no longer counts -> the tour opens at Welcome
│ │ └── Everything ready -> no tour; nothing but the ID changes
│ └── Saved settings are kept; the ID is recorded either way
├── Tour opens -> speech model download starts at once // setup.background-download
│ ├── Model -> the selection recommended for the system's first preferred
│ │ language (English -> Parakeet Unified English; e.g. French ->
│ │ Parakeet v3, Japanese -> Qwen3-ASR); no picker in the tour
│ ├── Progress sits in the top corner on every step (under the practice
│ │ field while Give it a try waits for it); failure -> Retry
│ └── Change or delete the model later in Settings // setup.delete-model
├── macOS: opens centred at one fixed size, so tiling managers float it
├── Quit part-way -> next launch resumes at the saved step // setup.resume
│ └── Permissions are read again at launch: a setup they now finish (macOS's
│ Quit & Reopen after a grant) is passed; past setup, it waits until they do
├── Welcome -> Get started // setup.welcome-hero
│ ├── The Hex glyph backlit by a cool corona, drawn live on the GPU
│ │ (`rune_ui/hero.rs`); core, middle ring, outer ring light in turn,
│ │ then the lamp breathes. Light theme: ink rings over a pale blue glow
│ └── Reduce Motion -> the settled frame, lamp at rest
├── Setup: one screen, every permission // setup.checklist; skipped when granted
│ ├── Each row leads with its glyph: mic, text cursor, sparkle (model)
│ ├── No Input Monitoring row: Accessibility also lets the shortcut taps listen
│ ├── Every row -> Allow; asked -> pulse + Allow; granted -> Allowed // setup.permission-control
│ │ └── Grant: the pill narrows into the green label, the dot shrinks into
│ │ the check as it draws on (0.3 s, no bounce; Reduce Motion: no travel)
│ ├── Microphone -> macOS prompt; refused -> Privacy pane + "switch Hex on" guide
│ ├── Accessibility -> Privacy pane + drag guide
│ │ ├── The app tile lifts out of the row and flies, blurred, into the guide
│ │ └── macOS 27 names the list "Device Control and Data Access"; row and guide follow
│ ├── Rows re-read every 400 ms and flip to a check as macOS grants
│ │ └── Accessibility flips live (`AXIsProcessTrusted`); its stale preflight never asks for a restart
│ ├── On in System Settings, unusable until relaunch -> "Turns on when Hex restarts"
│ │ └── Every permission on -> primary "Restart Hex" -> relaunch at setup // setup.restart
│ ├── The first unfinished row carries the screen's one accent
│ └── Last grant -> 0.7 s beat -> Give it a try (Continue also works)
├── Give it a try // setup.practice; every OS (rune_ui/practice.rs)
│ ├── Model still downloading -> "Almost ready" + "Hex starts listening once…";
│ │ progress (or Download stopped + Retry) under the field, not in the corner;
│ │ model ready -> the normal Give it a try heading, body, and hint
│ │ // check: try_it_waits_for_the_model_until_it_is_ready
│ ├── Focused practice field; hold the shortcut, speak, release
│ ├── Started -> "Listening…" ; Transcribing -> "Transcribing…"
│ ├── Discarded before transcribing -> quick-tap hint
│ ├── Discarded after transcribing -> nothing-heard hint (mic, distance)
│ ├── Microphone line under the hint: input in use + live level while recording
│ │ ├── Click -> General's microphone menu; a choice saves the same setting
│ │ ├── 1.5 s of silence while recording -> "No sound. Pick another mic?"
│ │ └── Lid closed, no other input -> "The lid is closed, so the built-in mic is off."
│ ├── Pasted -> words land in the field + success -> Continue
│ └── Skip -> Done // practice is never required
├── Listener cannot start after setup // setup.relaunch; event tap refused
│ ├── First time -> Hex records a marker, reopens itself, resumes at Give it a try
│ └── Within 2 min of that relaunch -> "Reopen Hex" button; dev binaries ask to reopen
└── Done -> paste last, Modes tips -> Start using Hex
// Windows and Linux: Welcome -> Setup (the model row only) -> Give it a try -> Done
General > Speech model or Language
-> Prepare candidate
├── Success -> Save selection
└── Failure -> Preserve previous selection and show error
Permissions revoked later
-> General > Permissions needed: the same rows and Allow control as the tour
└── On, waiting for a relaunch -> "Restart Hex"Apple silicon, macOS 15+. Choices come from the compiled model/language catalog; Apple Speech is currently excluded from the desktop picker.
General > Dictation > Speech model, then Language below it // setup.language-select; every desktop root
├── Speech model -> Picker lists every available model; never changes the language
│ ├── Saved language's recommendations first with their tags, then the
│ │ models that support it, then those that do not
│ ├── Model without the saved language -> Card: "Doesn't support Japanese;
│ │ switches the language to English"
│ └── Choose it -> ChooseModel(model, model's default: English, else its first)
│ └── Saved -> Language row: "<Model> doesn't support Japanese, so the
│ language changed to English" until the next choice
├── Language -> Select of the saved model's languages (catalog order, Auto first
│ when it detects); one language -> the field shows it, disabled
│ └── Choose -> ChooseModel(same model, language): the same preparation as a
│ model choice, so per-language assets (Apple Speech locales) install
└── The saved TranscriptionSelection is always a pair the model supportsSources: shared picker (choose,
choose_language, language_row) and picker_models, language_choices,
language_for in transcription_models.rs.
Checks: the_picker_lists_every_model_with_the_language_s_recommendations_first,
a_model_switch_keeps_a_supported_language_or_falls_back_to_the_model_s_default,
each_model_offers_exactly_the_languages_it_supports, and the Rune tests
a_model_without_the_saved_language_falls_back_to_its_own_visibly,
browsing_the_picker_never_changes_the_language,
changing_only_the_language_prepares_the_same_model_again, and
settings_opens_the_picker_and_names_the_model_and_language_apart. Scenes:
picker-japanese, language-fallback, language-menu. Fixture renders and
recorded requests only; a live model switch with a fallback has not been
observed. Apple Speech stays excluded, so its per-locale install through the
Language row is unexercised.
Choose a supported language // setup.language-conditioning; macOS + Linux picker
├── Parakeet v3 -> Detect language from audio; selected language does not constrain output
├── Whisper -> Use selected language to guide decoding; accuracy is not guaranteed
│ └── Recognition hints -> initial prompt; output that only repeats them is dropped
├── Qwen3-ASR 0.6B, QwenCleo-ASR 1.7B -> Recognition hints -> context prompt in the
│ system message; output that only repeats them is dropped
└── Bangla -> Whisper only, with the `bn` hint; Auto lets Whisper guess per clipWhisper reads the recognition hints as its initial prompt and Qwen3-ASR
(including QwenCleo) as the context prompt in its system message
(#129); apply_recognition_hints
in gguf_session.rs picks the slot on every OS, so
dictation, meetings, and the local API on the listener's model all get them.
Live streams run without them. Observed on apollo (Vulkan, 2026-10-31,
LibriSpeech test-clean): with Qwen3-ASR 0.6B, hints "Kaffar, Rodolfo,
Bragelonne, …" fixed Caffer, Rudolpho, Redolfo, Rudolph, and Bragolon in 10
clips without changing the others checked. Both families, on silence or noise,
often return the hints verbatim (Qwen3-ASR 0.6B did on 3 s of silence and of
noise). A segment whose text equals the hints after
normalization (case, ASCII punctuation, spelled-out digits; commas, semicolons
and line breaks separate words) is removed, in dictation, meetings, and the
local API alike. The filter needs at least two hints, so dictating the one
listed name still pastes it; speech that contains a hint among other words is
kept, and so is a repeat of only some hints. Checks: prompt_echo_is_dropped,
speech_containing_the_hints_is_kept, only_the_echo_segment_is_removed,
single_hint_dictation_is_kept, and
hints_without_spaces_after_commas_are_matched in
gguf_session.rs.
Whisper, Qwen3-ASR 0.6B, and QwenCleo-ASR 1.7B take recognition hints, and
Settings shows the field only for them. Saved hints for a model that can't use
them (for example Parakeet Unified English, set by hand in settings.json) are
cleared with a logged warning when settings load, rather than failing the whole
load and leaving the listener off
(#448). Check:
loading_clears_recognition_hints_the_model_does_not_support in
app_settings.rs.
Settings picker or setup > model card // setup.model-card; shared by every desktop root
├── Title row -> Name, recommendation tag, "Streams" tag // at most two tags
│ ├── Recommendation -> For the saved language from the catalog (English: Unified English first)
│ └── Streams -> Catalog `streams` (pinned artifact advertises streaming)
├── Detail line -> "Transcribes while you speak" (non-macOS: "… on a GPU"),
│ coverage, benchmark, timestamps, Installed
├── Saved language unsupported -> Warning line naming the language it switches to
├── Trailing edge -> Active as dot + label; Cancel while preparing; else Use/Download
├── Preparing -> Progress bar with its "Downloading 37%" / "Loading model" label
└── Speed · Size · Accuracy -> 100% minus the benchmark word error rateOnly Parakeet Unified English is marked streams
(only_the_buffered_parakeet_artifact_is_marked_streaming). The mark describes
the artifact; dictation still streams only after the load probe succeeds on a GPU
device (Streaming Finish). The menu bar lists
names only and shows no tags. Scenes: hex app -- --snap OUT --scene picker
(also picker-downloading, picker-japanese).
Parakeet v3 remains available for Portuguese and its other existing language
choices. Its card explains the limitation and suggests Whisper for
language-guided transcription. This does not change saved selections or expand
the Auto picker choices. Multilingual audio support is not language conditioning:
the pinned v3 GGUF has no stt.parakeet.prompt.num_prompts metadata, which gates
language prompting in transcribe-cpp-sys 0.1.3.
Sources: model catalog,
shared picker, and the runtime
options in parakeet.rs and
local_transcriber.rs. The regression
parakeet_v3_supports_portuguese_without_language_conditioning checks that the
Portuguese selection remains valid and available, sends no runtime language
hint, and contrasts Whisper's pt hint. It does not run inference.
The regression was observed failing with the old metadata and passing after the
correction. The picker's layout is checked by Rune goldens (rune-picker-*), not on a
Mac screen.
Unreproduced recognition report: #68 reports Portuguese speech misrecognized as English. Source and pinned-model metadata inspection establish the prompting limitation, not the cause or repair of those transcripts. No v3 audio reproduction or transcription-quality fix has been demonstrated.
Bangla (bn) is offered because Whisper large-v3-turbo was trained on it; no
other catalog model supports it, so Whisper is its only, recommended choice and a
Mac whose first preferred language is Bangla starts on Whisper. Auto is not a
substitute: in a manual test, Whisper's per-clip detection labeled Bangla speech
as Arabic, Turkish, and Spanish. bangla_is_transcribed_by_whisper_with_its_language_hint
checks the recommendation, the bn hint, and that Qwen3-ASR rejects it. It
runs no inference, and Bangla accuracy has not been measured.
Checks start in onboarding.rs, transcription.rs, and transcription_models.rs. Setup/picker previews prove rendering, not clean-account permission grants or model preparation.
The tour is the pure onboarding::Flow (steps, row states, background
download, practice hints, one-relaunch guard) drawn by
rune_ui/onboarding.rs; permission_control
in rune_ui/lists.rs is the one Allow / dot /
Allowed control Settings and the tour share (a_grant_gathers_then_releases_into_allowed,
reduce_motion_swaps_allow_for_allowed_at_once; scene meetings-allow). The
welcome hero is a compositor pass after the UI, so goldens show its stage
empty; hex app -- --scene onboarding-hero (-still for Reduce Motion) runs
it live, and --video records it. Its springs passed motion-lint as level
channels; native visual review is still by eye. The guide
beside System Settings is permission_guide.m.
On Windows the setup step adds a Microphone row only while the microphone
switch in Settings › Privacy & security is off (device, user, or desktop
apps; CapabilityAccessManager\ConsentStore\microphone says Deny). Allow
opens ms-settings:privacy-microphone, the row describes the "Let desktop apps
access your microphone" switch, and setup finishes once it is on
(a_switched_off_windows_microphone_adds_its_row_until_setup_ends; the
registry read is cross-compiled only, not run on a Windows PC).
Unit tests (onboarding::tests) cover step order, the download starting on the
welcome screen and retrying from any step, permissions-only setup with the
model waiting for practice, the auto-advance beat, live row flips, a refused
microphone, platforms without permissions or practice, practice hints, resume,
the relaunch guard and resume marker, resuming past a setup the relaunch
finished (resuming_at_setup_after_a_relaunch_passes_grants_that_are_now_real),
grants waiting for a restart
(a_grant_the_process_cannot_use_yet_waits_for_a_restart,
setup_offers_a_restart_once_every_permission_is_on), an Accessibility grant
advancing setup without one (an_accessibility_grant_applies_without_a_restart),
and the saved step
(the_tour_step_is_saved_for_the_next_launch), and the upgrade check
(a_fresh_install_onboards_without_the_identity_check,
an_upgrader_without_permissions_gets_the_tour_once,
an_upgrader_whose_setup_is_ready_skips_the_tour; a real 2.3.1 to
anomaly.hex upgrade on a Mac is not yet observed). Rune tests drive the tour
(the_mac_tour_asks_each_permission_then_practices,
a_listener_that_will_not_start_relaunches_once_then_asks_to_reopen,
a_refused_microphone_points_to_system_settings,
a_grant_waiting_for_a_restart_makes_setup_restart_hex,
onboarding_downloads_the_model_through_the_service), and every macOS step
is a scene (mac-onboarding, -setup, -progress, -denied, -restart,
-download-failed, -try, -listening, -landed, -reopen, -done).
Reaching the last step in a live tour plays the onboarding success sound
(Classic: its stop sound)
(remember_step; fixtures stay silent), checked by reading the code.
The default model comes from desktop_i18n::preferred_language (macOS:
NSLocale.preferredLanguages) through recommended_selection; existing
settings keep their saved model.
Not yet observed: a clean-account run with real grants, a real practice paste
into the field, or a real self-relaunch after an event-tap refusal. Windows and
Linux show the model row only and skip the practice step.
Menu bar > Transcription Model // setup.quick-model-switch; macOS
├── Downloaded models -> Last selected language shown beside each model
├── Choose -> Verify installed artifact -> Prewarm -> Save selection
│ ├── Success -> Checkmark moves; Settings reflects the saved selection
│ └── Failure -> Previous selection remains; open Settings for the error
├── Preparing -> Cancel Model Switch // never cancels dictation/output jobs
└── Manage Models… -> Settings picker // downloads; languages in the Language rowThe menu does not open Settings merely to switch models, download a missing model,
or replace a corrupt artifact. Each model retains one last-used language/hints
selection in transcription_recents; the current selection takes precedence.
Older settings start with no remembered alternatives. Select an alternative's
language in Settings' Language row once to establish it. No additional model is loaded at startup
just to populate the menu, and the checkmark means selected, not live-worker readiness.
transcription_preparation.rs is shared by Settings and the menu bar: one worker plus one replaceable pending choice. Closing Settings does not abandon preparation. A superseded/cancelled completion cannot save, and delivered menu controls are drained before accepting completions. The desktop root commits through an existing editor to retain unrelated unsaved settings, or through persisted settings when no window exists.
Sources: status_item.rs and
macos_rune.rs. Key checks are
preparation_has_one_worker_and_only_keeps_the_latest_pending_choice,
cancelling_a_completed_unpolled_preparation_never_commits_it,
model_switches_remember_language_and_hints_without_changing_other_settings, and
failed_model_selection_save_preserves_selection_recents_and_unsaved_edits in
the preparation owner and app_settings.rs.
Additional checks cover worker failure/retry, installed-only checksum verification,
and menu choice/tag projection. These do not establish a physical menu click or
an installed-app switch. Remembering a language does not change the language
conditioning limitations described above.
Observed September 4, 2026 (2.1.16), existing model picker: all 455 Rust tests passed in debug and release, with ten opt-in tests skipped per profile. All twelve keyboard-layout child scenarios passed per profile; formatting, strict all-target/all-feature Clippy in both profiles, and the release build passed. The Cohere fixture inference check was run separately; see dictation model windows. Native menu interaction, installed-app switching, and Linux runtime behavior were not exercised.
Labs > Switch model with keyboard language // setup.input-source-model; macOS, Windows, Linux X11; off by default; General shows the mappings while on
├── Each enabled keyboard input source -> Don't switch, or a downloaded model and language
│ ├── New mapping -> The source's own language when the model has it
│ ├── One-language model -> Its language shown fixed
│ └── New/unmapped source -> Don't switch; keep the current model
├── Source changes (shortcut, Input menu, automatic app/document switch)
│ └── 300 ms after the last change -> Existing installed-only picker preparation
│ ├── Same model and language already selected -> Nothing loads
│ ├── Success -> Save the selection, keeping the model's remembered hints
│ └── Failure -> Existing picker error; no download or guessed fallback
├── Manual model choice -> Supersede pending automatic work; do not reapply at recording start
├── Enable -> Apply the current source's mapping once it is observed
├── Edit the active mapping -> Apply now
├── Disable -> Cancel automatic work; stop observing; leave the selected model alone
└── Relaunch -> Observe current source without applying it; preserve the manual choiceMappings are keyed by native input-source IDs (macOS input-source IDs, Windows
keyboard layout handles, X11 XKB layouts such as de(neo)), not inferred speech languages or
physical keyboard keys, and store a model with a language it supports. Only
downloaded models from the picker are offered (Apple Speech is never mapped);
downloads stay in the picker. A mapped model that is no longer downloaded is
labeled Not installed and can be cleared; a mapping to a model or language Hex no
longer offers is dropped when settings load. Removing a source hides its row
without erasing its saved mapping.
Hex reads input sources only while the setting is on: the macOS main-thread TIS distributed-notification observer, the Windows runtime's poll of the foreground window's layout, and the Linux host's XKB connection (polled every 250 ms) exist only then and work without a Settings window; the window lists enabled sources only then. Windows lists the language bar's layouts, named by language and keyboard. On Linux the row appears only in X11 sessions: Wayland compositors keep the active layout to themselves, with no general protocol (IBus and Fcitx report input-method engines, not the compositor's layout), so Wayland is unavailable. Source changes cancel obsolete automatic work but keep manual preparation until a mapped choice is due. An unmapped source cancels only automatic work. This reuses the shared model selection, recording/stream fallback, and meeting-engine behavior; it adds no recording-time routing, model cache, or notifications.
Sources: observers for macOS,
Windows, and
X11,
switch policy,
runtimes (macOS,
Windows, Linux), and
settings UI.
Checks: the switch policy and settings/CLI validation tests, the runtime
scheduling tests in macos_rune/tests.rs (macOS),
the settings interaction tests in
keyboard_model_tests.rs, and the
mac-input-source-models golden, and the XKB symbols-name tests in the X11
observer. No check delivers native input-source notifications or switches
sources through the Input menu or a shortcut.
Observed October 30, 2026, X11 observer: on a headless Xvnc with
setxkbmap -layout us,ru,de -variant ,,neo, Hex listed English (US), Russian,
and German (Neo 2) as us, ru, de(neo) with languages en, ru, de, and
reported us, then ru after an XKB group lock. The Windows observer and both
new runtime paths compile (x64 clippy) but were not run on Windows or a live X11
desktop.
Settings picker > Downloaded model card // setup.delete-model; macOS
├── Delete -> "Delete <size>?" -> Delete again
│ ├── Success -> Model file, receipt, and partial download removed; card offers Download
│ └── Download in progress -> Nothing removed; "Model could not be deleted." with reason
├── Selected dictation model (any language) -> No Delete
├── Preparing or Apple Speech model -> No Delete
└── Model choice or closing the picker -> Confirmation clearedDeletion takes the shared model-directory download lock without waiting, so it cannot race a Settings, menu-bar, or local API download. The menu bar drops a deleted model because it reads installation state when it builds the menu. Linux Settings does not offer deletion: the service owns model files and has no delete request yet.
Sources: shared picker and remove_installed
in transcription_models.rs. The check is
removing_a_model_deletes_its_artifacts_unless_a_download_is_active (real files
and lock); no Rune test covers the picker's two-step confirmation yet.
Known gap: is_installed checks size, not checksum. Not every native-load
path requires verification. Picker Active means selected/installed, not live
worker readiness; a later worker-load failure does not roll back saved selection.
Saved shortcut // dictation; Option by default; Windows and X11 accept mouse buttons, macOS with Labs > Mouse shortcuts
├── Hold -> Speak -> Release
├── Tap, tap -> Speak hands-free -> Press again // Double-tap to lock enabled
└── Escape
├── Capture active -> Cancel that capture
└── Otherwise -> Cancel newest cancellable job
General > Advanced > Double-tap only // optional for key bindings
-> Disable single-hold activation
-> Keep the double-tap hands-free path
Labs > Second dictation shortcut (macOS, off) // labs.md; issue 627
-> General shows "Second dictation shortcut" under Shortcut (unbound, clearable)
-> it dictates like the shortcut: Hold/Toggle, double-tap lock, Escape, release
-> the shortcut that starts a capture finishes it; the other stands down meanwhile
Labs > Unload model when idle (off; 5/15/30/60 min, default 15) // labs.md; transcription_engine
-> nothing used the model that long and nothing records -> the model unloads
-> next press, meeting chunk, or API request -> reloads first; audio waits, nothing lostThe detailed map covers timing, successive captures, output
commitment, named checks, and native-proof gaps.
The macOS CLI entry is hex listen, with an optional authoritative --device.
Do not start it beside an existing listener.
Completed output // output
├── Paste -> Current focus, in submission order
│ ├── Previous clipboard restored 500 ms later, unless something newer replaced it
│ ├── Labs > Keep dictation on clipboard (macOS, off) -> the dictation stays on the clipboard (labs.md)
│ └── Labs > Copy dictation only (macOS, off) -> clipboard only: no paste, typing, or restore (labs.md)
├── Paste Last Dictation // menu or Option-Shift-V / saved binding
│ └── Last completed dictation
└── History
├── Inspect / search / copy (row hover or the card) / delete
├── Retention ("Keep: 7 days" menu) -> Seven days by default, always hard-capped
├── Off -> Stop new entries; preserve existing entriesChecks in paste.rs, parakeet.rs, and history.rs cover output and retention boundaries. They do not prove that an arbitrary target consumed its clipboard contents. Send is not an atomic target-app transaction. Chromium 155 and 156 get the text typed instead of pasted for a Chrome paste bug.
Easy to misread: History Off does not stop diagnostic logging. Diagnostic logs are separate and currently unbounded on disk. See the privacy guide.
The shared EventReader streams a file-length snapshot in 16 KiB chunks rather
than copying the entire log into memory. It retains up to 1,024 event rows and
the latest context, even when that context predates the retained rows.
Diagnostic records over 1 MiB are skipped through their terminating newline;
the original log and dictation output remain intact. Partial UTF-8 records carry
over to the next refresh, and truncation or file replacement resets the reader.
Checks: the reader_* regressions in events.rs. These
exercise synthetic files, not an installed app. The GPUI window's
on-demand-loading regression went with GPUI; when the Rune window reads the
log is not covered by a test. A first Commands/Activity refresh still scans the
full log snapshot; disk rotation and background parsing remain separate work.
Observed September 26, 2026: the reader and pane-loading regressions passed. An optimized isolated harness using the production reader and a synthetic 64 MiB log reduced peak process RSS from about 204 MB to 2.4 MB; refresh took 254 ms before and 222 ms after. This is reader evidence, not a native Settings latency measurement.
Sidebar // settings.nav
├── General, Output, History, then each feature that is on: Meetings and
│ Voice Filter (Labs toggles), Annotate, Playground (Labs, an item)
└── Labs -> one flat page of toggles, each off by default (labs.md)
// Command-1…9 follow the panes' order (labs::order)
General // settings
├── Basics card -> Shortcut, Speech model, Language, Microphone, Launch at login
│ ├── Each row leads with an icon; choices are inline dropdowns, not segments
│ ├── Language -> fixed text for a one-language model, else a dropdown
│ └── A row that hides or shows (Advanced, a microphone error, Double-tap only)
│ -> Folds under a clip as the rows below slide over it; no fade in place
├── Microphone -> Value names the input in use, with live level bars // settings.microphone
│ ├── "System default · Kit Pods", or a saved device
│ ├── Level bars -> peak of the audio Hex receives, ~30 fps while General shows them above rest // input_level
│ │ ├── Read from one atomic the audio owner stores per chunk; never opens or blocks the mic
│ │ ├── Quiet room (bars at rest) -> no 30 fps frames; the window's 250 ms tick notices a rise.
│ │ │ An open General window with a warm mic woke 34.5 times a second in a quiet room before
│ │ │ (dev-364, headless sway on apollo, Oct 31) and 4.1 after; macOS was reasoned about only
│ │ ├── No audio for 250 ms (released while idle, failed, not allowed) -> flat, dimmed
│ │ └── Scenes: mac-settings-mic-level (a recorded WAV, `--video`), mac-settings-mic-released
│ │ // check: the_microphone_level_follows_the_voice (Linux)
│ ├── Re-listed on a device or default change while General shows (Windows: also every 3 s)
│ ├── System default -> The OS default input, followed live
│ │ ├── Default changes -> Switch once capture is idle; new stream opens before the old drops
│ │ ├── Default is Hex Microphone -> Built-in mic, else first real input; row says so
│ │ ├── Stand-ins never include virtual or aggregate inputs (macOS: e.g. Microsoft Teams Audio)
│ │ │ ├── Nothing real left -> "lid is closed" / "no microphone" error, not silence
│ │ │ └── A virtual default, saved device, or `--device` is still used as named
│ │ └── Default is Bluetooth (Advanced switch on) -> Built-in mic, else first wired/USB input
│ │ ├── Row says "KIT PODS is Bluetooth, so Hex listens to MacBook Pro Microphone"
│ │ ├── Windows: WASAPI `BTHENUM`, or a headset/handset endpoint of unknown bus (`BTHHFENUM`)
│ │ ├── Linux: a `bluez5` default source; Hex opens ALSA's default, then moves its own
│ │ │ stream to the PCI input, else the first other hardware source (pactl, PipeWire);
│ │ │ the stream is marked so WirePlumber never remembers the move. A Bluetooth
│ │ │ default chosen while Hex's stream is open is followed until the next open
│ │ ├── No other usable input (or lid closed, Bluetooth only) -> the Bluetooth default
│ │ └── A saved Bluetooth mic or `--device` is always used as named
│ ├── Saved device missing -> System default stands in, value names it
│ │ ├── Row says "SHURE MV7 isn't connected; using your Mac's input"
│ │ ├── Menu keeps "SHURE MV7" checked with a faint "not connected"
│ │ └── Device returns -> Switch back once idle; the setting never changes
│ ├── Menu lists real inputs only // never Hex Microphone
│ └── Open menu covers the controls beneath it; click elsewhere dismisses
├── Advanced ▸ -> one fold per pane, closed by default, even while a setting in it is in use
│ ├── Collapse -> drops a pending release confirmation
│ ├── Dictation -> Shortcut side, Shortcut mode (How it works), Double-tap to lock,
│ │ Double-tap only, Microphone mode, keyboard-language mappings (while its Labs toggle is on),
│ │ Use the Mac's microphone instead of Bluetooth (Linux, Windows: "Use another
│ │ microphone instead of Bluetooth"),
│ │ Recognition hints
│ ├── Output -> Paste last shortcut, While dictating, Dictation HUD, Terminal paste (Linux)
│ │ (Dictate without processing sits in the Output pane; see dictation.md)
│ └── Application -> Enable on launch, Show Dock icon, Sound volume, Sounds,
│ Interface language, Update channel (self-updating installs)
├── Use the Mac's / another microphone instead of Bluetooth (Advanced, on)
│ └── Off -> System default follows a Bluetooth default too; re-resolves once idle
├── Labs > Share dictation shortcut with other apps (Windows, off)
│ └── On -> the hook passes the shortcut's key-down/up on instead of consuming it
├── Terminal paste (Advanced on Linux, Labs on Windows; off)
│ └── On -> paste sends Ctrl+Shift+V instead of Ctrl+V; applies to the next paste
├── Microphone mode (Advanced)
│ ├── Keep ready (fast) -> Open while idle; pre-roll available
│ └── Release when idle -> Confirm -> Open on press; no pre-roll; startup delay
├── While dictating -> Mute / Pause media / Lower audio / Do nothing
│ ├── Lower audio -> Lower to slider, 0–100% of the previous volume (default 20%)
│ ├── Intentional capture only, not ordinary shortcut chords
│ ├── Pause/resume only players already reported running by macOS
│ ├── Linux restore -> Waits 300 ms; a new capture keeps it suppressed // double-tap lock
│ └── Labs > Also pause the browser (macOS, Pause media only; off by default); its permission row shows here while on
│ ├── Turning it on without System Audio Recording -> ask now, never while dictating
│ ├── On -> Screen & System Audio Recording row: Allow / waiting / Allowed
│ └── Dictation -> browser_media sequence below
├── Sound volume -> immediate feedback; zero suppresses tones
│ └── Default output changes or disappears (macOS) -> next tone reopens on the current default
├── Sounds -> Sheet: hear each set on Hex's real HUD, pick one
└── Linux / Windows -> listener card and desktop commands below the basics
Pause media + Also pause the browser // browser_media
├── A browser uses the microphone (a call) -> leave it alone
├── No browser process has its audio output open -> leave it alone (no tap)
├── Listen to those processes' own output (private process tap), 3 windows of 100 ms
│ └── Any window at or below -60 dBFS, or no level -> leave it alone
├── Send the system pause command (MediaRemote 1; never the toggle)
├── Poll 5 windows of 100 ms
│ ├── 2 windows in a row below -90 dBFS -> Hex paused it
│ └── Otherwise (another tab, a sound that ended later, or the pause went elsewhere) -> no play later
└── Dictation ends and Hex paused it -> first, before the players: listen 300 ms to every browser process
├── Above -60 dBFS (you started it again) -> no play
├── No level, a browser runs and every one has closed its output -> still paused: play
│ (a pause longer than about 10 s closes the output; a tap on it delivers nothing)
├── No level otherwise (browser quit, tap failed) -> no play
└── Otherwise -> send the system play command (MediaRemote 0), then poll 20 windows of 100 ms
├── A window above -60 dBFS -> resumed
├── No level before the first level (the output is opening again) -> keep polling
├── No level after a level -> stop listening
├── No browser output open after 10 windows (tab closed: the play reached another app) -> send pause once
├── Never a level and a browser opened its output -> stop listening
└── Silent throughout (tab closed or reloaded: the play reached another app) -> send pause once
Intentional recording // recording.environment, idle_sleep
├── Prevent idle sleep
│ ├── macOS -> IOKit no-idle-sleep assertion
│ ├── Linux -> logind `idle:sleep` inhibitor; `idle` only where polkit refuses a
│ │ process outside the seat session (Hex's user service) to block sleep
│ ├── Windows -> system-required power request (from the press, as its While dictating does)
│ └── Failure -> Warn and continue recording
└── Finish/cancel -> Release it
// no caffeinate child process or executable-policy dialogFeedback tones keep their output open for five idle minutes. On macOS the
CoreAudio listeners on the default output, the device list, and an audio service
restart advance an output route generation; the next tone drops an output opened
under an older one and opens the current default at once, so unplugging a USB
or Bluetooth output no longer silences tones until Hex restarts
(#428). Check:
a_route_change_reopens_the_output_on_the_current_default in
feedback.rs, with injected opens; a real unplug has not
been observed by a test.
Lower audio (recording.environment.duck) is an explicit fourth
choice, off by default, on every OS. The slider appears only while selected and saves on
release. It remembers its value across behavior changes. Intentional captures
ramp the original output channels to that fraction over
one second, preserving balance and silent channels, then restore on finish,
cancel, or orderly shutdown. Changes apply to the next capture. The stop tone
waits for restoration. A manually changed volume after settling is left alone;
a newly selected output device is not lowered. Fixed-volume devices and outputs
whose channel balance cannot be safely restored are left unchanged. Meetings do
not acquire this dictation environment. Linux lowers the default sink's
channels through pactl (one volume through wpctl without it), so Hex's own
tones are lowered too and the stop tone waits, as on macOS. Windows lowers
every other app's audio session instead, leaving Hex's tones at full volume;
sessions that start while recording are left alone.
HEX snapshots the volumes and applies a smooth ramp in
output_ducking.rs over the CoreAudio adapter in
ducking.rs, the sink adapter in
linux_audio_control.rs, and the session
adapter in windows_audio_control.rs. The worker runs inside the app and cannot restore volume after a
hard process crash or SIGKILL. Changes during the ramp itself cannot be
distinguished from delayed Bluetooth acknowledgements.
Fake-device tests cover relative volume, balance, silence, interrupted ramps,
manual changes after settling, and partial-write rollback; settings/UI tests
cover defaults, bounds, persistence, conditional visibility and platform gating.
On 2026-10-25, the macOS logic/settings checks and strict Clippy passed.
The seven reference images were regenerated on Ubuntu 24.04 ARM64 with Mesa
lavapipe, using the unmodified Linux-only Rune test module and
HEX_UPDATE_GOLDENS=1 scripts/check ducking. A subsequent
scripts/check rune_ui --nocapture without the update flag passed all 113 tests,
including the unchanged control goldens; no golden skipped for lack of a GPU
adapter. The five existing images differ from the base branch above the existing
12-level channel tolerance only within the While dictating control; the sidebar,
Quit Hex, and shortcut layouts remain unchanged. Dark and light ducking previews
were visually inspected. The earlier macOS-generated references were replaced
because these fixtures retain build-OS-dependent layout and defaults. This
verifies headless Linux rendering, not audible playback, physical devices,
crash recovery, or native Windows execution.
The Linux and Windows ports (2026-10-30) were checked by unit tests (pactl and
wpctl volume parsing, the Linux Bluetooth stand-in choice), the Settings test
that Lower audio and its slider appear on every OS, Linux Clippy, and Windows
x86_64 cross-Clippy. On apollo (niri, PipeWire), the ignored
native_idle_sleep_prevention_acquires_and_releases took a logind idle
inhibitor (polkit refused idle:sleep over SSH), and a marked arecord
stream moved with pactl move-source-output without WirePlumber storing the
target. Not verified: audible Linux or Windows ducking, Windows idle-sleep
and Bluetooth detection on hardware, a real Bluetooth headset on Linux, and
Windows ARM64 (its cross-build stops in rquickjs-sys on this host).
Successful settings saves persist and project changes at safe runtime boundaries.
Immediate settings controls commit a candidate before changing the saved UI
choice or applying dependent native/history effects. A failed shortcut save
stays in capture instead of showing Saved; failed retention saves keep the old
choice and surface the error. Failed debounced text saves retain their drafts
for a subsequent edit or close-time flush; no Rune test injects a save failure
yet.
Persistence, conflict, and ownership checks live in
app_settings.rs,
recording_environment.rs, and
audio.rs. Settings previews do not prove physical device
switching or native mute support; muting is best-effort, not universal. The
recording-environment ownership checks cover overlapping acquisition and release,
not a managed-device policy or native power assertion. The pause-script regression
checks that inactive players are absent before AppleScript application resolution;
when no supported player is running, acquisition skips AppleScript entirely.
The script check does not prove subprocess avoidance or exercise Music, Spotify,
or VLC through Automation.
The browser pause runs after the AppleScript players and only when System Audio
Recording is reported granted (an unknown answer skips it, because the tap would
open the macOS dialog mid-dictation). macOS 15.4 and later no longer tell other apps
what is playing, so Hex hears the browsers through a private process tap
(aggregate anomaly.hex.browser-tap.<pid>, apart from the meeting tap) and sends
play only after it saw the browser go silent right after its own pause. The
sequence checks in browser_media.rs run on fakes:
a_confirmed_pause_is_resumed, two_sounding_tabs_never_get_play,
a_browser_already_playing_gets_no_play, a_call_is_never_paused,
no_level_sends_nothing, a_play_the_browser_never_takes_is_paused_again,
a_browser_that_closed_its_output_gets_play,
a_closed_browser_that_never_opens_after_play_is_paused_again; the ignored native_tap_hears_a_playing_browser
heard Brave at -25.4 dBFS on macOS 27.0.1 (2026-10-28). Which app receives the
system command is macOS's Now Playing choice; Safari is heard only when its
WebKit process reports Safari's name. An opt-in native
assertion smoke exercises IOKit without starting microphone capture.
Changed after 2.3.1: Automatic was a compiled preference list (Universal
Audio Thunderbolt, Studio Display Microphone), then the macOS default with a
Bluetooth default swapped for the built-in microphone (the 2.1.22 fix for
#106 and
#88). That skipped a user's
AirPods default, and Hex Microphone then passed the wrong microphone on to
calls. Automatic is now System default: the OS default input, followed live.
A Bluetooth default (CoreAudio transport Bluetooth or Bluetooth LE) is passed
over for the built-in microphone, else the first other real input, so AirPods
keep their music out of call quality while you dictate; it is kept when nothing
else is usable. Only System default is affected: a saved Bluetooth microphone
and --device are honoured, and General › Advanced › "Use the Mac's microphone
instead of Bluetooth" (avoid_bluetooth_microphone, default on) turns it off.
Hex Microphone carries whichever input Hex records from. Linux and Windows
don't detect Bluetooth inputs yet and follow the default. Checks in
audio.rs:
system_default_passes_over_a_bluetooth_default,
a_bluetooth_default_is_kept_when_nothing_else_is_usable,
a_virtual_input_never_stands_in (CoreAudio transport Virtual, Aggregate, or
AutoAggregate; #429),
inputs_resolve_to_the_selection_then_the_system_default,
device_override_wins_and_never_falls_back,
a_default_change_switches_only_while_capture_is_idle, and
a_missing_selection_uses_the_default_until_it_returns, with injected device
lists and opens; scene mac-microphone-bluetooth shows the row and switch. Not
verified: the CoreAudio listeners against a real default change, the transport
read against real AirPods, or Windows' default-change report on a real PC.
Fixed after 2.1.20: through 2.1.20 the microphone menu was painted without
occluding the panel, so hovering or clicking a device also reached the setting
under the pointer (#94), and it
only closed by choosing or reopening. The menu is now anchored to its button,
occludes the controls beneath it, and closes on any outside mouse-down. In the
Rune window the menu is a rune-kit Menu in the overlay layer (scene menus);
the GPUI occlusion regression went with GPUI and has no Rune counterpart.
Easy to misread: an open microphone is not an active recording.
Output (main sidebar, replaces the Modes editor) // rune_ui/output.rs; scenes output*
├── Edit with agent -> Copy a prompt naming the steps' keys and `hex settings`
├── Open file -> settings.json in the default editor
├── Every dictation -> Global's steps, numbered, read-only
├── Try -> Global's corrections and built-ins on a typed sentence
│ └── Scripts and rewriting run only when dictating
└── In some apps -> Each mode's apps and its steps (Only: …), opens to list them
// the old Modes editor stays in the code for its scenes, unreachable
Foreground context -> Select processing
├── Matching website // macOS: scriptable browsers, then AXWebArea; Windows: UIA; Linux: AT-SPI
├── Otherwise matching application
└── Otherwise Global
Selected mode // replaces Global, does not layer over it
-> Corrections
-> OpenCode transformation // optional provider-backed rewrite
-> Transformations // ordered text chain
-> Paste or Send
Stage fails -> Preserve its input // failed chain discards partial resultsBuilt-in text transformations run natively. Custom transformations come from
~/.config/hex/hex.config.ts and run in Hex's embedded script engine with a 1 s
limit per call. OpenCode rewriting sends documented
context to the configured provider; speech transcription stays local.
The built-ins are Lowercase, SpongeBob case, and No trailing punctuation. The
last removes only the final contiguous run of sentence marks (including
full-width marks), preserving internal punctuation, trailing whitespace, and
closing quotes/brackets. Punctuation inside a final closing quote is preserved.
Select it after Lowercase for lowercase messages without the final period.
The no_trailing_punctuation_* and built_in_transformations_chain_in_selected_order
tests in personal_commands.rs cover text behavior
and native dispatch; transformations_chain_with_built_ins_and_failures_keep_nothing
and runaway_scripts_stop_at_the_time_limit_and_the_config_keeps_working run
real scripts. None is a live target-app paste.
Selection and stage checks start in dictation_processor.rs, text_replacements.rs, and personal_commands.rs. They do not prove live provider availability or real application/browser context changes.
Website context per OS: macOS asks the front tab of Safari, Orion, Chrome,
Brave, Edge, Arc, Vivaldi, Opera, Helium, and Chromium over AppleScript (one
Automation prompt per browser; a refusal keeps the application context) and
reads any other app's focused AXWebArea AXURL (Firefox, Zen, web views).
Linux matches the focused window from X11 or the Wayland foreign-toplevel list
to an AT-SPI window by title and process and reads its showing web
document's DocURL (Firefox) or URI (Chromium, only with
--force-renderer-accessibility). linux_context_reads_the_focused_window
(ignored, manual) printed google-chrome-stable with https://example.com/path
under headless Sway and chrome with the page under its XWayland, and hex
on niri (Oct 30); the macOS adapters are compiled on CI only and not run.
OpenCode transformation talks only to an already running OpenCode service,
found through its registration file (opencode_service.rs);
Hex runs no OpenCode executable and starts nothing. The catalog loads only while
the Modes pane shows. Without a service, "OpenCode isn't running" shows beside
the switch and the setup/retry notice below;
unavailable_opencode_is_explained_beside_each_switch covers it headlessly, and
the opencode_service tests cover registration, stale-PID, loopback, chunked,
deadline, and cancellation handling against a local fixture, not a live
provider.
ordinary_processing_snapshots_modes_before_stage_and_keeps_transformations in
parakeet.rs checks processing-time profile acquisition,
conditional Processing-stage emission, and retained transformation selection.
It uses controlled profiles, not a live provider or custom transformation host.
modes_add_select_and_delete, mode_corrections_and_names_save_from_their_fields,
and mode_transformations_add_reorder_and_remove in
rune_ui/tests.rs cover the Rune mode editor and
its debounced drafts headlessly. They do not exercise native typing or
foreground mode activation.
Application activations compare the picker's bundle name with the foreground
application's localized name. When Finder shows all filename extensions, the
picker name arrives as Ghostty.app; context.rs strips
that suffix on both sides and app_settings.rs
rewrites already persisted activations on load, checked by
application_matching_ignores_finder_bundle_extensions and
loading_strips_finder_bundle_extensions_from_mode_applications.
Fixed in 2.1.14: through 2.1.13, a mode saved with that Finder
preference enabled never activates and every dictation falls back to Global.
Update to repair saved selections automatically. For older builds, the workaround
is to quit Hex, remove the .app suffix from
dictation_processing.modes[].applications in settings.json, then relaunch.
Meetings (beta) maps the opt-in (the Meetings pane's card, then Meetings › Settings), call detection and the focus-free offer, menu-bar recording state, rolling transcription, echo suppression, and the Markdown copy in the user's transcript folder.
Labs › Voice Filter (experimental.hex_microphone = true), HEX_EXPERIMENTAL_MIC=1,
or the driver installed; off, the pane is hidden // fun.hex-microphone; macOS, Linux
├── Sidebar > Voice Filter // the only home of these controls
├── Hex Microphone card
│ ├── Linux -> no driver: a PipeWire virtual source (Audio/Source "Hex Microphone",
│ │ node hex_microphone) that Hex plays into through `pw-cat` while the Voice Filter is on
│ │ ├── Status -> Starting… / Available ("Choose Hex Microphone as your input…") / Unavailable + reason
│ │ ├── pw-cat missing or exits -> Unavailable with its last error line; retries every 2 s
│ │ ├── Off or Hex quits -> the source disappears (pw-cat ends with its input); no root, nothing installed
│ │ └── Session priority 1 -> WirePlumber never picks it as the default input Hex records
│ ├── macOS driver status -> Installed v<ver> / In use by <app> + Uninstall
│ │ ├── Version mismatch -> v<old> installed + Update + Uninstall
│ │ ├── Pre-Anomaly com.kitlangton driver at the same path -> Update replaces it in place
│ │ ├── Not installed -> Install -> macOS admin prompt -> copies bundled HexMicrophone.driver + restarts coreaudiod
│ │ └── Inline state -> Installing… / Uninstalling… / Cancelled / Failed with reason
│ ├── Mode while dictating -> Muffled (default), Muted, Quiet, Normal, Chipmunk, Robot, Walkie, Underwater, Far
│ ├── Key sliders -> Live mode tuning + Effect strength + Reset
│ └── Try it -> Example clip OR Record 5s (in-memory only)
│ ├── Dual waveform -> You vs Call hears with highlighted dictation span
│ └── Explicit playback -> Play what call hears | Play your voice
├── Live microphone capture -> Raw audio to Hex transcription (unmodified)
│ ├── Source -> Hex's dictation microphone (System default follows the OS default, never Hex Microphone itself), never a meeting's second stream
│ ├── Lock-free ring -> 48 kHz virtual sink (macOS IOProc; Linux 11 ms blocks into a one-page pipe) -> Raised-cosine fade on intentional hold
│ │ └── Not dictating -> bit-exact pass-through; non-48 kHz inputs resample continuously
│ └── Ring underflows/overflows -> process.log each minute while an app reads it, and on sink stop
└── Offline CLI -> hex dev voice-filter render <in.wav> --mode <m> --dictation <start>-<end> -o <out.wav>Sources: voice_filter.rs,
hex_microphone.rs,
HexMicrophoneDriver.c,
build-app.sh,
and rune_ui/fun.rs. Checks:
default_mode_is_muffled,
all_nine_modes_render_finite_and_expected_levels,
raised_cosine_fade_has_no_step_discontinuity,
ring_buffer_transfers_samples_and_handles_underflow_and_overflow,
a_meeting_stream_on_the_same_microphone_does_not_double_the_feed,
a_callback_larger_than_the_latency_cap_passes_through_whole,
resampling_is_continuous_across_callbacks,
only_the_listeners_current_input_feeds_hex_microphone,
driver_plugin_loopback_streams_at_48khz_under_20ms_latency,
driver_plist_version_and_status_mismatch_are_detected,
osascript_failure_classifies_user_cancel_and_errors,
audio_service_restart_interrupts_stream_and_recovers,
voice_filter_saves_the_mode_and_applies_the_tuning_live,
try_it_records_and_plays_through_the_host, and
voice_filter_needs_labs_or_hex_microphone.
Scenes: mac-voice-filter, mac-voice-filter-muted,
mac-voice-filter-driver-missing, voice-filter, voice-filter-unavailable
(Linux).
Linux observed (apollo, PipeWire 1.6): the source lists in pactl list sources
and pw-record --target hex_microphone records the fed audio at 48 kHz; no
automated check covers it, and no call app was tried.
Proof limit: in-process dlopen loopback proves the compiled driver's 48 kHz
WriteMix/ReadInput path at 5.33 ms (256 frames), not an interactive macOS
admin authorization prompt or an active Slack/Zoom call. The pass-through
checks simulate callback timing and sizes; clock drift between the microphone
and the virtual device is not corrected, so a skip or zero-fill click every
minute or two remains possible.
Cancellation / interruption / failure // recovery
-> Scoped stop, fallback, or error
-> Retry or fresh gesture
-> Next capture succeeds // this is the proof, not a cleared error
Menu bar and Settings // maintenance
├── Icon -> 18 pt monochrome Hex mark template (recording variant + elapsed time)
├── Settings / Paste Last -> Reachable with Dock icon hidden
├── Launch at Login -> macOS owns registration state
├── Check for Updates -> The updater (maintain.updates)
└── Quit -> Orderly shutdownLaunch Hex // maintain.startup
├── Setup incomplete / permissions or model missing -> Open setup or Settings
├── Show Dock icon on -> Open Settings
└── Show Dock icon off + usable menu-bar item -> No startup window
├── Dictation starts independently of the Settings window
└── Menu-bar Settings / Finder or Spotlight reopen -> Open or focus the window
Menu-bar installation fails -> Show Dock icon and open the window // recovery access
Show Dock icon off // maintain.dock-policy
├── Settings window open -> Dock icon stays // hiding it would deactivate Hex
└── Settings window closes -> Menu-bar onlyGeneral > Launch at login // maintain.login-item
├── Open -> Checking until macOS returns the initial state
├── Refresh / toggle / Open settings -> One persistent background worker
│ └── One active request; latest pending user action precedes the next refresh
├── Toggle -> Switch moves at once; macOS's answer settles it
│ ├── Answers behind a later click -> Switch keeps the last click
│ └── Settled answer differs -> Switch springs back
├── Failed toggle -> Show error and reconcile with actual macOS state when available
└── Close Settings -> Release channels without waiting for an active native calllogin_item.rs serializes ServiceManagement calls on one
window-owned worker, with bounded request and response channels and an autorelease
pool per operation. Settings reads responses without blocking on its 250 ms tick;
refreshes do not accumulate while macOS is busy. The toggle is unavailable until
the first successful status lookup. A click is held in memory
(PlatformState::login_item_requested) only until the worker is idle again, so
the switch need not wait for SMAppService (about 120 ms per toggle, measured
October 24, 2026, plus up to one tick). macOS owns registration state; no login
Boolean is persisted. Scenes never start the worker or call macOS.
The worker tests in login_item.rs exercise thread reuse, nonblocking polling,
latest-action coalescing, action priority, and closing during a blocked call.
launch_at_login_asks_the_login_item_and_needs_approval_first in
rune_ui/tests/panes.rs checks the Rune row's
requests and the approval state, and the login_item_* tests there check the
switch while macOS answers: confirmed, refused, a status answer behind a click,
and rapid clicks. The GPUI regression for error rollback and
external enablement went with GPUI. Native RequiresApproval and error transitions remain unverified;
the simulated checks do not establish Developer ID signed distribution behavior.
Observed September 23, 2026, on d1930b5 after the final simplifications: the
full debug suite passed 486 tests and the release suite passed 485, each with
twelve opt-in tests ignored and all twelve keyboard-layout child scenarios
passing. Formatting, strict debug all-target/all-feature Clippy, and diff
whitespace checks passed.
Computer-use checks on an ad-hoc signed temporary bundle of the optimized build
confirmed shortcut capture/cancellation, Settings/Modes navigation, registration
and unregistration, the final state after three consecutive toggle clicks, and
refresh after removing that bundle's login item in macOS Settings. The temporary
item was left disabled/removed; no new microphone, input, or Accessibility grants
were made. A five-second sample profile found SMAppService frames on the
login-item worker and none on the main thread. This is sampled-stack evidence,
not an input-to-frame latency measurement or a signed-release smoke test.
Closing Settings left the app process alive with zero login-item threads;
reopening produced one new worker. An earlier apparent surviving worker came
from computer-use observation reopening the window. Cmd+Q and Quit Hex each
terminated the process. Local profiles are /tmp/hex-login-worker-current.sample.txt
and /tmp/hex-login-cycle-{before,closed-no-observe,reobserved}.sample.txt.
The tested temporary executable's SHA-256 is
d1a3be1e219e7371726273937eeef6c7e16434637e4297ae183f9bdaf4d28b04.
No Developer ID signing identity was available for a new distribution build.
The existing Show Dock icon preference controls quiet startup; there is no additional launch-window setting. This applies to normal and login launches, does not change login registration, and does not hide an already open window when the preference changes. Any open Settings window keeps the Dock icon: macOS deactivates an app asynchronously when it becomes menu-bar only, which dropped the window behind other applications (#103). Turning the preference off therefore takes effect when Settings closes.
Checks in meeting_watcher.rs:
dockless_startup_stays_quiet_only_when_setup_and_menu_bar_are_ready and
dock_visible_startup_always_opens_the_app cover the startup decision. The
Dock/Finder reopen Apple event and the status item's OpenSettings bypass it.
These checks do not establish signed-app login behavior or native Finder/Spotlight
reopening; those still need an installed-app smoke test. This addresses the
menu-bar-only case in #64, not an
independent window preference for users keeping the Dock icon visible.
Executed September 3, 2026: 441 Rust tests passed (nine opt-in tests ignored), along with all twelve keyboard-layout child scenarios, formatting, and Clippy. The release Settings preview built and launched, but window screenshot capture failed; no visual or installed-app startup verification is claimed.
Hex checks the release service // maintain.updates
├── 10 s after launch, hourly (Dev channel: every 10 min and on window focus), and Check for updates -> anoma.ly/release/hex/desktop/<channel>.json
│ ├── User-Agent: hex/<build channel>/<version> (hex/dev/0.0.0-dev-171, hex/stable/2.3.1); nothing else about the install
│ ├── Failed check -> backs off, never past hourly
│ └── <channel> -> updates.channel (General › Application › Update channel: Stable | Dev,
│ or `hex settings set updates.channel`), else the channel the build was published on
│ (HEX_BUILD_CHANNEL; older builds: dev for a dev version, else the receipt's);
│ package managers always stable
│ └── Choosing a channel -> checks now; a staged update stays staged
├── Versions never compared: channel's current release != running build -> take it
│ ├── Running build -> Linux/Windows: executable SHA-256 = target's; macOS: same version
│ ├── Another channel than the build's -> sidebar, tray, menu bar: Switch to Stable 2.3.1
│ │ └── Only after the user confirms -> download -> stage (never switches by itself)
│ ├── required -> install without waiting for the rollout
│ └── Rollout -> device-local bucket in <support>/update-bucket, never sent
├── Direct install -> download (resumable) -> size + SHA-256 -> code signature -> stage
│ ├── macOS -> unpack beside Hex.app -> Developer ID 5NZ4Q7NXJ4, anomaly.hex, notarized
│ ├── Windows -> Authenticode valid, signer AUTHENTICODE_SIGNER -> new version directory
│ ├── Linux -> SHA-256 only -> new version directory, `current` switched
│ ├── Staged -> checks continue; Restart to update stays offered meanwhile
│ │ ├── Another release this install takes (update, required) -> download -> replaces the staged one
│ │ └── Same release, rollout deferred, unconfirmed switch, rollback, failure -> keep the staged one
│ └── Sidebar Update, menu bar / tray: Restart to update
│ ├── One more check first (manifest within 15 s) -> Checking / Downloading -> newest staged
│ │ └── A release published after the relaunch -> the next launch's check
│ └── macOS -> renamex_np swap (also at quit) -> relaunch -> old bundle removed at launch
├── macOS location not replaceable (read-only, translocated, root-owned) -> Download update (DMG)
├── Package manager (pacman, Nix) -> its command, once it serves another version
├── Source build, test build -> never checks
└── Failure -> keep the running version, back off, Check for updates retries
DisableUpdates at launch // maintain.managed-updates
├── No checks, downloads, or installs; sidebar: Managed by your organization
├── macOS app menu -> Check for Updates omitted
└── macOS menu bar -> Disabled "Updates Managed by Your Organization"Administrators set DisableUpdates on macOS as a Boolean in the anomaly.hex
defaults domain, normally through a configuration profile
(#100), and on Windows as a
non-zero DWORD under SOFTWARE\Policies\Anomaly\Hex in HKLM or HKCU. Hex
reads it once at launch, so a forced value wins and a change applies after
relaunch. Speech-model downloads are unaffected. Releases through 2.3.1 read
com.kitlangton.hex2; there is no fallback to it, so profiles must name
anomaly.hex.
updater.rs owns the worker and the shared status the
window, menu bar, and tray show; updater/protocol.rs passes the release
service's shared test vectors (tests/fixtures/release-protocol), and the
per-OS modules have tests for activation, the macOS bundle swap and
signature checks (ad-hoc fixtures against the real requirement), the rollout
bucket, receipts, the channel choice, never comparing versions (a legacy
2.3.2-dev.29 follows 0.0.0-dev-N; dev to stable is a switch), the switch
waiting for confirmation, only a different self-taken release replacing a staged
update (only_a_different_release_this_install_takes_replaces_a_staged_update),
Restart to update's final check and one-time report
(restart_to_update_checks_once_more_and_reports_once), Linux re-staging keeping
the running version (replacing_a_staged_update_keeps_the_running_version), and
Windows Authenticode (a Microsoft-signed system file). No test drives the worker
through a real re-stage and restart; a channel rolled back while an update is
staged still restarts into the staged release. The
Update channel row shows only while this install updates itself (not test,
source, managed, or package-manager installs); the mac-settings-dev-channel
scene renders it, and rune_ui tests cover saving and hiding it.
Not yet observed: a published dev channel release, a switch between channels on
a real install, a real update on any OS from the release service, the macOS
swap and relaunch of a Developer ID build, a Windows update of a signed build.
Manifests are unsigned; on Linux a compromised HTTPS origin could serve any
executable with a matching manifest, which Linux code signing cannot catch.
Fixed in 2.1.13: older releases through 2.1.12 used app-global dispatch from inside the sidebar click, losing the action while the active window was borrowed. The click regression passed in debug and optimized release builds. The menu-bar path was exercised on the installed local 2.1.11 build and opened the native Software Update window; no installation was performed. That path remains the workaround for older versions, including after Remind Me Later. A repaired installed-sidebar click remains unverified.
The historical release verification records publication evidence through 2.1.20, including signing, artifact checks, CI results, and skipped native checks. Those release-specific results do not verify the current working tree, installed-app behavior, or physical Linux compatibility; retain each entry's stated limits when using it as evidence.
Recovery separates supported recovery from known defects. login_item.rs, status_item.rs, updater.rs, the identity test, and app validation are maintenance evidence starting points. Preview badges do not prove installed-app login registration or a signed update from an older supported build.
Linux beta // not macOS feature parity
├── hex start -> One per-user Hex -> Alt-Space capture -> Stream (Vulkan GPU) -> Paste
│ └── CPU or a model that cannot stream -> Transcribe the whole clip after release
├── hex / hex app -> Window in the same process; closing it leaves Hex running
├── Recording sounds -> Shortcut press / capture stop / active cancellation
├── Shortcuts -> One state machine, three sources // linux.shortcuts
│ ├── X11 -> Root-window key grabs; General records the chord
│ ├── Wayland + GlobalShortcuts portal -> Desktop grants the triggers
│ │ ├── General > Change shortcut -> Press chord (Esc cancels)
│ │ │ └── Saved -> New portal session asks for it as preferred_trigger
│ │ │ under an id named for it (`dictation-CTRL-ALT-space`), so a desktop
│ │ │ that keeps an id's stored trigger (GNOME, KDE) binds the new one (#575);
│ │ │ an unchanged shortcut keeps its id and any trigger set in the desktop
│ │ │ // observed on the niri fork: a new session gets it, a rebind in the same one fails
│ │ │ // #575 fix checked by a unit test only, not on a GNOME session
│ │ ├── Each row: "Your desktop granted <trigger>" (it may assign another);
│ │ │ an empty trigger (KDE's for one it keeps unbound) counts as refused
│ │ │ └── Hyprland (XDG_CURRENT_DESKTOP) -> its portal names no trigger, since
│ │ │ its config binds them: muted "Your desktop assigns this key itself,
│ │ │ so Hex can't confirm it", no refusal note, info log (#635)
│ │ │ // check: a_missing_trigger_is_unconfirmed_where_the_desktop_assigns_keys
│ │ │ // (the row's data only); not run on a Hyprland session
│ │ ├── Portal version 2 -> Desktop shortcuts > Open -> ConfigureShortcuts,
│ │ │ the desktop's own dialog; ShortcutsChanged updates each row
│ │ │ // a desktop keeps a stored binding over a new preferred_trigger for the same id
│ │ └── Refused shortcut -> Danger row + how to free it (niri: config.kdl)
│ │ under that shortcut's own row (Dictation, Output);
│ │ the rest keep working
│ ├── Wayland without the portal -> Bind `hex` commands in the desktop
│ │ └── General hides shortcut rows and lists the commands
│ └── Listener starting or stopped -> `hex status` reports Unregistered,
│ never the X11 mode
├── hex dictation start|stop|toggle|cancel, hex paste-last
│ -> Same events as a shortcut // linux.commands
│ ├── start|toggle --raw -> Dictate without processing's events
│ ├── Idempotent: start while recording / stop while idle -> Nothing
│ └── Hex missing or not listening -> One-line error, non-zero exit
│ // as for hex quit|restart|status
├── Escape -> Cancel active capture, not newest accepted job
│ ├── X11 -> Grabbed only while recording; a grab conflict warns, dictation goes on
│ └── Portal -> Never bound: "Cancel dictation" joins the one bind with no key;
│ the user assigns one, idle presses do nothing (#482)
│ ├── Desktop gave it a key anyway -> General shows a "Cancel dictation"
│ │ row, "Your desktop granted <key>", and process.log warns (#624)
│ └── KDE startup -> before the bind, the pre-#579 `hex` set (with the
│ pre-#492 `cancel` on Escape) and `cancel` under `anomaly.hex` are
│ removed from kglobalaccel (`unregister`); other apps untouched
│ // check: a_key_the_desktop_gave_cancel_dictation_is_shown (the row's
│ // data only); the KDE removal is not run on a Plasma session
├── Recording HUD -> Dot scope, as on macOS, on the
│ │ top or bottom edge, recording with the voice's level, then transcribing
│ │ until accepted output finishes; Hidden position -> none // linux.hud
│ ├── Display -> picked at each capture start, kept until it hides (#486)
│ │ ├── X11 -> the monitor under the pointer (XRandR monitors)
│ │ ├── Layer-shell (niri, Sway, Hyprland) -> no output pinned: the
│ │ │ compositor's focused output
│ │ └── GNOME, KDE (XDG_CURRENT_DESKTOP) -> the primary display
│ └── Rune overlay in Hex's process (rune_hud, rune_hud.wgsl on the GPU)
│ ├── Wayland -> layer-shell overlay layer, no keyboard, empty input region
│ ├── X11 -> override-redirect window, empty input shape
│ ├── Wayland without layer-shell (zwlr_layer_shell_v1; GNOME) and XWayland
│ │ (`DISPLAY`) -> only the HUD opens on XWayland: override-redirect ARGB
│ │ window, notification type, empty input shape, primary monitor's edge,
│ │ scale from Xft.dpi; the rest of Hex (window, portal shortcuts) stays
│ │ on Wayland (#465)
│ │ // seen in headless mutter 51 + Xwayland on apollo: HUD at the top
│ │ // centre, animating, focus unchanged, the pointer over it hits the
│ │ // window below; not yet on a real GNOME Shell session
│ ├── Neither (Rune refused it, or no layer-shell and no XWayland)
│ │ -> process.log warns once; `hex status` reports `hud_limitation`
│ │ and the listener warning
│ └── No window loop (no GPU or display, `hex dictate`) -> no HUD
├── Tray host (StatusNotifierWatcher) -> Tray icon: click opens the window; menu as Windows' (tray_menu, linux_tray, rune::Tray)
│ └── No tray host -> No icon; status and sounds remain available
├── Microphone -> System default or a saved device; `--device` overrides
│ └── System default -> ALSA's default, resolved at each stream open // no live follow
├── General > Launch at login -> writes or removes ~/.config/autostart/anomaly.hex.desktop (X-Hex-Managed; linux_login_item)
│ └── An entry without the mark -> error beside the row; Hex never replaces it
├── Microphone failure -> Same recovery as macOS (RecoveringAudioInput)
│ ├── Stream fails -> Cancel an unfinished capture, "reopening" failure, retry with backoff
│ ├── Startup open fails -> Retry with backoff; the listener keeps running
│ └── Released while idle, on-demand open fails -> Failed entry; stays released
│ └── Overload (Xrun) glitch -> Logged; not a failure
├── Paste -> Restore the previous clipboard 500 ms after the shortcut // linux.clipboard-restore
│ ├── Before the transcript -> snapshot every format (32 MiB, 1 s); empty -> cleared again
│ ├── Later paste, copy, or another client replaced the transcript -> left alone
│ ├── Over the cap or unreadable -> transcript stays; the log says why
│ ├── Paste only reached the clipboard -> transcript stays for Ctrl+V
│ └── Wayland without wlr-data-control -> transcript stays
│ └── Wayland: wl-copy, then the shortcut, chosen at the first paste // linux.wayland-paste
│ ├── Asked by a modifier shortcut -> wait for its portal release + 250 ms
│ │ (2 s cap without a release); after a hex command -> 250 ms
│ │ // Hyprland merges held physical modifiers into virtual-keyboard keys (#353)
│ ├── Compositor offers zwp_virtual_keyboard_manager_v1 and wtype is installed -> wtype
│ │ └── V sent on evdev KEY_V (placeholder releases first), so Hyprland's
│ │ binds read Ctrl(+Shift)+V, not Ctrl(+Shift)+Escape (#444)
│ ├── Otherwise -> RemoteDesktop portal: keyboard-only session, approved once
│ │ (restore token, persist_mode 2) -> Ctrl+V as keysyms; session lives with Hex
│ │ ├── New session -> 250 ms for the approval dialog to hand focus back
│ │ └── Existing session -> 150 ms after wl-copy, so the target holds the new
│ │ selection before Ctrl+V (GNOME pasted nothing or the old clipboard, #578)
│ │ // fixed delay, no acknowledgement; not observed on a real GNOME session
│ └── Neither works -> Failed: "Copied to the clipboard; press Ctrl+V…" naming
│ the RemoteDesktop portal; the clipboard keeps the transcript
└── Portal identity -> Register as `anomaly.hex` (reverse-DNS: GNOME 49/50's
shortcuts provider refused the bare `hex`, so binding failed, #579)
├── Installs ship anomaly.hex.desktop (StartupWMClass=anomaly.hex); windows
│ report app_id / WM_CLASS anomaly.hex -> one launcher entry
├── Icon=anomaly.hex -> packages install hicolor 16–512 px PNGs (deb/rpm refresh
│ the icon cache); otherwise Hex writes them to ~/.local/share/icons/hicolor
│ at startup and fixes Icon= in managed entries it wrote (#652)
├── Startup -> Hex's own managed hex.desktop (launcher, autostart) renamed
│ to anomaly.hex.desktop; the installer does the same; unmanaged ones kept
└── No anomaly.hex.desktop in XDG data dirs -> Hex writes
~/.local/share/applications/anomaly.hex.desktop (X-Hex-Managed),
NoDisplay when an older package's hex.desktop already shows Hex,
removed once a package ships anomaly.hex.desktop
// KDE bindings and RemoteDesktop grants stored under `hex` are asked again once
// checked by unit tests only, not on a GNOME 49 session
Enable / Disable (General, tray) // linux.listener-persist
├── Saves enable_on_launch -> a disabled Hex stays disabled after a restart (#575)
└── GNOME (XDG_CURRENT_DESKTOP) Disable -> after the listener's session closes, a
fresh session binds no shortcuts: GNOME Settings stores that empty set, so
Hex's entries leave its list; Enable shows GNOME's approval dialog again
// never on KDE: an empty bind drops the user's keys (#482)
GNOME startup -> pre-#579 `hex` set listed in GNOME Settings -> removed through
gsettings (reset its shortcuts, drop it from `applications`); other apps untouched
// observed on apollo with gnome-control-center 51.1's provider, portal-gnome 50,
// a stand-in org.gnome.Shell, not a full GNOME sessionClipboard restoration lives in linux_paste.rs
(generation and timing) and linux_clipboard.rs
(snapshot and serving). Observed on apollo (2026-10-29), in nested headless Sway:
native_wayland_clipboard_shortcut restores a two-format clipboard and keeps an
empty one empty; a full dictation from a file-fed ALSA microphone pasted into a
GTK target, then Ctrl+V pasted the original text; an X11 round trip on Xwayland
restored a 3 MB image through INCR. Not observed on X11 dictation, KDE, or
Hyprland.
The Wayland paste choice lives in linux_paste.rs and
linux_remote_desktop.rs. Observed: the
wtype path in the headless-Sway smoke and the virtual-keyboard probe on niri
(apollo). niri's portal frontend advertises RemoteDesktop but its GNOME
backend fails CreateSession, so wtype stays first there. The RemoteDesktop
paste itself (KWin, Mutter) has unit tests for its key sequence only and is not
yet observed on a real desktop. The modifier wait (ShortcutKeys in
linux_input.rs) has unit tests for its timing only;
it is not yet observed on Hyprland, and a modifier held past the wait still
changes the paste. wtype numbers its keymap from evdev code 1 in order of first
use, and Hyprland matches binds through its configured layout, so a bare
-k v read as Escape and a Ctrl+Shift+Escape bind swallowed Terminal paste
(#444). Hex now releases 46
never-pressed placeholder keysyms first so V lands on KEY_V;
wtype_sends_v_on_the_v_key checks the numbering against wtype's rule, not a
real Hyprland session.
Windows beta // x64 (Vulkan GPU) and ARM64 (CPU)
├── Install -> Setup.exe or install-windows.ps1 -> hex.exe + install.json receipt
├── Launch -> DLLs load from System32 and Hex's folder only, never PATH (#587)
│ └── DX12 compiles shaders with FXC (System32), never a dxcompiler.dll on PATH
├── Hex app -> General, tray, HUD; Enabled = shortcut works in every app
├── HUD display -> the monitor under the pointer (GetCursorPos) at each capture
│ start, inside that monitor's work area, at its own DPI (#486)
├── Launch again -> Running app shows its window; hidden (login) launch -> no-op
├── Tray icon -> White Hex mark; light taskbar (SystemUsesLightTheme) -> black mark
│ └── Theme read when the tray is created; a later switch applies at the next launch
├── Tray menu (tray_menu, shared with Linux) // maintain.tray
│ ├── Recording / Transcribing… status line and tooltip while the listener works
│ ├── Show Hex · Paste last dictation (refocuses the app the menu covered)
│ ├── Speech model: downloaded models, ✓ on the selected one; none while one prepares
│ └── Update item · Enable / Disable Hex · Quit Hex
├── Single-modifier shortcut -> Left / Either / Right side picker, as macOS
├── Ctrl+Space -> Hold or Toggle -> Capture -> Stream (GPU) -> Paste
├── Escape -> Cancel; a held modifier alone never ends or cancels a capture
├── One modifier alone (Right Alt, Right Ctrl) -> Shortcut; Alt/Win release masked (no menu, Start)
├── Microphone switched off in Settings › Privacy -> Onboarding Microphone row -> Allow opens it
├── Microphone -> System default or a saved device
│ └── Windows default input changes -> Switch once capture is idle // cpal's default-device report
├── Paste -> Waits for Shift, Alt, and Win; a held Ctrl is kept and harmless
│ └── Ctrl+V carries scan codes from the foreground layout, so Chromium apps
│ see KeyboardEvent.code: VS Code's terminal takes it as Paste instead of
│ sending ^V to a TUI inside it (#618; from VS Code's source, not run)
├── Update staged (managed install) -> Tray, sidebar footer: Restart to update
├── Another release (unmanaged install) -> Tray, sidebar footer: Download update
├── Another channel chosen -> Tray, sidebar footer: Switch to Stable 2.3.1 (confirm)
├── Otherwise -> Sidebar footer: Check for updates
└── Modes, History, Activity, transformations -> as on macOSWindows updates follow maintain.updates (updater.rs,
updater/windows.rs). CI test builds never check. The tray item and tooltip
(windows_rune.rs) follow the published update
state. They pass the cross-compiled Clippy check, but no one has clicked them
on a real Windows PC. The white tray mark vanished on a light taskbar
(#460); a light taskbar now gets
the same mark in black (light_taskbar_gets_a_dark_mark_with_the_same_shape
checks the pixels, not a real taskbar). Releases are Authenticode-signed once Azure Artifact
Signing is configured (windows-signing.md).
Known defect, outside Hex: Discord's game detection matches the running
hex.exe to HEX: Shards of Fate
(#445). The executable name stays
as it is because the managed install layout, updater, shortcut, and login entry
use it. The workaround is to hide the entry in Discord's Registered Games
(Windows guide). No check covers it.
No Windows Meetings or local transcription API. See the Windows guide for the parity table, streaming notes, and release flow; check-windows builds, lints, and tests x64 and builds and tests ARM64 on every pull request. Physical smoke tests on Windows 10 and on ARM64 hardware have not been run.
Linux updates // maintain.linux-updates
├── Direct install (versions/ + install.json receipt) -> release service -> staged -> sidebar footer: Restart to update
├── Source install (source-build marker) -> Sidebar footer: Updated from your source checkout
├── Local build (HEX_LOCAL_BUILD=1) -> Sidebar footer: Local build · updates off; Version 2.3.1 (local <commit>); no checks, no channel row
├── pacman (AUR hex-bin: /usr/bin, root-owned) or Nix store -> Sidebar footer: Updated by your package manager
└── Anything else (a downloaded executable) -> Sidebar footer: Download updateThe install kind comes from updater/linux.rs. The 2026-10-23 apollo
observation (a staged update and the sidebar button) predates the release
service and is not evidence for it.
No Linux meetings. Wayland shortcuts need either the desktop's GlobalShortcuts portal
or the user's own keybindings for the hex commands; Hex reads no input
devices. Click-through and a physical portal or keybinding press still need
native evidence. Its smoke explicitly
isolates HEX_APPLICATION_SUPPORT_DIR, pastes while another keyboard holds
Ctrl and Alt, opens the window through a second launch, and checks that
closing it leaves the same process answering.
Shortcut rules are pure and tested once for every source in
linux_input.rs: hold, double-tap lock, toggle,
double-tap only, Escape, Paste Last, and command idempotence,
plus X11 auto-repeat filtering and a portal release that never arrives
(Hyprland's Lua binds drop it when the modifier comes up first; see
dictation.md). The X11 grab tests run on an isolated display in
CI; commands_and_grabs_share_the_x11_listener drives grabs and commands
through one listener. scripts/test-linux-service.py
checks that commands fail without a listening Hex.
On 2026-10-22 the portal path was observed on niri with xdg-desktop-portal-gnome
using synthetic uinput keys against an isolated service: granted triggers,
hold and Escape while held (since replaced by Cancel dictation), double-tap lock, Paste Last, and commands. A
physical keyboard press has not been recorded.
linux.hud: linux_rune_hud.rs keeps the
listener's generation slot (an old listener cannot hide a restarted HUD) and
turns its reports into rune_hud.rs's events;
reports before the window's app exists are dropped. The shaders render
offscreen with cargo test --bin hex rune_hud_frames -- --ignored
(2026-10-28 on apollo: Dot scope frames match the Metal look; 2026-10-29 on
apollo: recording, processing, and done after Dot scope became the default).
The GTK HUD it replaced is deleted. The text pill was observed on
2026-10-22 in a headless sway on apollo (isolated data root, real microphone,
hex service): hex dictation start showed the pill at the top edge and it
was gone after hex dictation stop and transcription. The Rune overlay itself
was also observed passing a virtual-pointer click through to the window below
and leaving keyboard focus where it was (Rune examples/background). Not
observed: X11 with Hex, niri, KWin, Mutter.
See the Linux guide, linux_app.rs,
linux_portal.rs, and
Linux CI for existing checks.
The direct installer/updater and
installer tests do not establish
a published, supported-host-validated signed release. Nix owns its
package updates; Hex updates only managed direct installs. Its Home Manager
module merges programs.hex.settings into Hex on every activation through
hex settings apply (control-cli.md); nix/checks.nix
evaluates the module, not a live activation.
Linux process // maintain.linux-service
├── hex / hex app -> Hex running? -> Activate{activation token} over the socket -> exit 0
│ ├── No Hex, hex.service loaded -> session.env -> systemctl start (Type=notify) -> Activate
│ ├── No Hex, no unit or custom data dir -> This process becomes Hex
│ └── Two racing launches -> One Hex; the other forwards and exits 0
├── hex start / desktop autostart / hex service -> Same, without the window
├── hex dictation|paste-last -> Socket worker -> Listener queue (never the UI thread)
├── hex status -> Published status; hex quit / Quit Hex / launcher action -> Drain, exit 0
├── Open the window -> Maps at 980×760 (min = max), tiling WMs float it -> Resizable from 860×560
├── Close the window -> Keep microphone, accepted jobs, model preparation, and HUD
│ └── Uncommitted shortcut capture -> Cancel and restore the prior listening state
├── Crash -> systemd Restart=on-failure -> Hex without a window
├── Update -> Restart the unit -> Hex reopens the window
├── hex restart without a display (SSH) -> Keep the saved session.env
├── Saved XAUTHORITY file gone (a reboot: Xwayland's cookie is new each login)
│ -> Take the user manager's current XAUTHORITY, else drop it (#465)
├── Window fails (no display, no GPU) -> Logged; Hex keeps running
└── Startup -> transcribe.cpp's Vulkan instance first, on the main thread
└── then the model load and the window's wgpu instance never create one at onceSources: the process, host, and window seam in linux_app.rs,
the single-instance socket and systemd lifecycle in
linux_service.rs, the CLI in
linux.rs, and the managed installer/Nix user units.
hex status queries the running Hex; --lines explicitly reads historical
observations. hex app --hidden and hex start start Hex without the window;
hex stop equals hex quit. Closing the window does not mean Disable; the
General's listener card Enable/Disable controls the listener, Quit Hex the process. No
root daemon or new input grants. The window runs on Rune's app lifetime: it
opens or focuses with the forwarded activation token and keeps its state
across close and reopen. Observed in a headless sway on apollo on 2026-10-22
with test-wayland-paste.sh, then with
GPUI and Rune builds (open in the same process, close, still answering,
reopen, quit); not rerun since GPUI was removed. On niri
(apollo, 2026-10-22, isolated data directory, listener off) launches spawned
through niri msg action spawn opened the window in the instance's process,
close-window left the same process answering, a relaunch opened a new
window in it, and hex quit exited; the window never took focus although niri
passed XDG_ACTIVATION_TOKEN (an IPC spawn's token, without user input, so
this does not show whether a launcher's token focuses it). Not yet on KWin,
Mutter, or i3.
Listener starts consume the host's settings snapshot; the foreground
hex dictate command loads its own snapshot once. Invalid startup settings leave
defaults available only for editing and block capture until a successful settings
commit or a restart with valid settings. Dismissing the load error does
not authorize recording with defaults. Feedback is initialized at these entry
points, so a delayed listener start cannot reset a newer live volume selection.
Checks: test-linux-service.py exercises
the real socket and process lifetime without a display, installed model, or
audio device; it passed on apollo (Arch, niri) on 2026-10-22.
closing_the_window_cancels_only_an_uncommitted_capture,
commands_route_only_to_a_started_listener, and
adopting_saved_settings_restarts_only_for_listener_settings use controlled
runtime workers. The virtual-microphone and Wayland scripts open the window
through a second launch; the Wayland script passed on apollo (headless sway,
WLR_RENDERER=gles2, before GPUI was removed), the X11 virtual-microphone script
has not run since the single-process change. A throwaway Type=notify user unit
on apollo (HEX_SYSTEMD_UNIT) started ready through hex start, restarted
after SIGABRT and SIGKILL without a window, stopped cleanly on
systemctl stop and hex quit, and an older Type=simple unit still started.
Before the Vulkan ordering fix, a launch with an installed model and
enable_on_launch SIGSEGVed in the Vulkan loader 3/3 in a headless sway on
apollo (the model load raced the window's wgpu instance); with it, 6/6 ran.
These checks do not establish a physical desktop logout, cross-version signed
update, or microphone/target-app success on the user's devices.
malformed_settings_cannot_start_even_after_dismissing_the_error in
linux_app.rs checks the failure gate and recovery after a
simulated successful settings commit. It selects a model never installed on
Linux, so the resumed start stops at Model required instead of opening the
microphone on a machine with the real model; it passed on apollo on 2026-10-22
beside the live service.
listener_startup_ignores_the_previous_session_until_a_new_one_arrives checks the
listener owner's startup observation and stop signal; the exact production owner
and test passed in an isolated standard-library-only Rust harness on macOS. That
harness does not compile the Linux application or prove its runtime integration;
the test has not been executed on Linux. These checks do not exercise physical
devices or persistence failure on disk.
The September 4 service-build evidence records an Arch/i3 client-independent ownership check, not transcription accuracy or physical logout, Wayland, or signed cross-version update proof.
Linux General > Sound volume // dictate.feedback.linux; X11 + Wayland
├── Default / older settings -> 50%
├── Off -> No recording tones
└── 25% / 50% / 75% / 100% -> Save and apply live; preview the start sound
Linux General > Sounds // dictate.sound-theme
└── Signature / Classic -> Save and apply live; preview the start sound
Capture // hex app and hex dictate
├── Shortcut starts capture -> Queue start sound immediately, before audio arrives
├── Hold passes 300 ms -> While dictating mute/pause begins, never at the press
├── Finish retained recording -> Stop sound at the release, while the 200 ms
│ tail is still captured; a muted output unmutes first // dictate.stop-tone
│ └── Clip turns out near-silent or speechless -> Stop sound already played,
│ nothing pasted
├── Brief discarded tap -> Start sound, but no stop sound or transcription
├── Dictation pasted -> Done sound; failures play the error sound
└── Cancel active capture -> Cancel sound; idle Escape stays silentThe shared feedback player uses Hex's bundled sounds
(Feedback) on every platform; only macOS wake/sleep
use afplay.
Decoding happens before the Linux capture loop; the output device opens for
the first tone and is released five minutes after the last one, as described
in macOS feedback. Playback admission is bounded and
nonblocking. Output preparation failures are
logged without blocking dictation. The Settings window saves the volume to
settings.json and the service reloads it without restarting the listener. The start sound does not wait
for the 300 ms capture-retention threshold; input delivery and audio-device
latency still apply. While dictating mute or pause does: the audio loop
requests it when the capture's audio reaches 300 ms past the press, and not
during a release tail. Until 2026-10-22 the service muted at the press. This
ordering is checked by reading the loop, not by a test or a muted device.
Checks: bundled_sounds_decode_without_an_audio_device and
feedback_admission_never_waits_for_playback in the player.
start_sound_precedes_audio_and_does_not_change_short_tap_discard in
linux_dictation.rs exercises the production start
path with a sound spy: immediate feedback without any audio, while a 100 ms tap
still discards. Existing timing checks in dictation.rs
cover the unchanged intentional-hold boundary. These are not proof of audible
native playback or physical hotkey-to-sound timing.
The September 4 sound-build evidence records the initial and immediate-start builds. It does not establish audible native playback, measured physical key-to-sound latency, or Wayland UI behavior.
Both Linux CI triggers include tests/**, and the
Nix source fileset includes those sources. The
keyboard_layout harness skips on Linux; packaging it does not provide macOS
layout evidence.
TypeScript SDK + local service // macOS, Linux, Windows
├── Caller records audio -> Explicit helper -> Raw transcription
│ └── Linux, Windows -> No packaged helper; command: hex service --embedded
├── Caller streams audio -> Running app's /streams -> Partials, segments, final
│ └── Dictation first; uses the loaded model only (docs/streams.md)
├── Running app's /transcriptions -> Listener's engine when it holds the model
│ └── Otherwise the service's own copy of the requested model
├── Running desktop capture API -> Separate microphone ownership protocol
│ ├── No silence gate // model output, unlike hotkey dictation;
│ │ // only recognition-hint echoes are dropped
│ └── Linux, Windows -> serviceCapture false, 404 service-capture-unavailable
└── Distribution
├── SDK 0.3.0 -> Published
└── Turnkey signed helper -> UnfinishedLinux and Windows serve the same local API from the running app
(linux_app.rs, windows_rune.rs)
and from hex service [--embedded]; a failed start only logs, and Hex runs on
without it. On Linux, the embedded service answered /health,
/capabilities, /models, and transcribed a WAV with Parakeet Unified
English on the Vulkan iGPU (October 30). The running app's shared-engine path,
/streams on a live Linux or Windows listener, and anything on Windows beyond
cargo xwin clippy (x64) are unverified.
The SDK guide, service contract, and SDK tests distinguish those paths. Helper-only service mode is not a physical hotkey/microphone test. Embedded/packaged service smokes now expect API 2, matching service/SDK compatibility checks. Helper packaging requires an explicit signing team and notarization profile, with the same Developer ID identity validation as the app; identity fixtures do not establish a signed helper distribution. Low-level blocking-peek and FIN/half-close limitations in local_api.rs must not be generalized to every SDK cancellation path.
Live streams (/streams, contract) are exercised by fake
engines only: transcription_stream tests cover windows, partials, the
backlog during dictation, preemption and offline fallback, the lease, cancel,
and busy; local_api::streams tests cover routes, auth, and bounds; and
transcription_engine tests cover yielding streams closed by dictation. No
real-model or live-app stream has been run.
Model progress and dictation levels share bounded SSE framing in sse.ts. In-memory SDK regressions cover transport-chunk independence, line/event limits, callback cancellation, reader cleanup, and bounded level observations. A CR-delimited terminal event is also checked with an open response body, so completion does not depend on EOF or subsequent data. These checks do not establish live service or microphone behavior.
macOS debug runtime // not release navigation
├── Meetings -> Always available; release builds need the Meetings opt-in (its pane's card or Meetings › Settings)
├── Activity / HUD Lab / hex dev -> Observation and developer controls
└── HUD Lab in release -> Only in the playground (Labs › Playground)
`hex app -- --snap OUT --scene NAME` -> Rendered fixtures // not proof capture/download/update happened
Linux `hex` -> the Linux window on Rune, in the Hex process // dev.rune-ui
├── Calls the host directly: saves settings through it; woken on host changes
├── Sidebar: General, Modes, History
├── Transcription picker -> ChooseModel / CancelModel; progress from the snapshot
├── First-run setup -> onboarding::Flow without permissions or practice
├── Modes: "Choose a file…" through file_dialog (FileChooser portal); drag to reorder
├── Modes: app icons (hicolor theme or pixmaps, PNG or SVG) in the picker,
│ chips, and mode rows; monogram while loading or without an icon
├── Model and application searches: Up/Down highlight, Enter picks it
├── Not yet: right-click delete
└── macOS/Windows surfaces as scenes too (Capabilities + preview data):
Meetings (Recordings | Settings, opt-in card), Voice Filter,
HUD Lab, Activity, permission rows, Launch at login, Dock icon, shortcut
sides, the full tour
macOS `hex app` -> one menu-bar process, Rune window // dev.rune-ui-macos
├── rune::App on the main thread: accessory, windowless unless setup/onboarding/Dock icon open it
├── Same AppKit loop: status item, updater, Hex/File/Window menu, Dock reopen event, the HUD pump (16 ms timer; the HUD itself is a Rune overlay)
├── Runtime thread: model preparation (menu bar: installed only), starts the listener once setup is ready
├── Listener, meeting controller, local API: their own threads
├── macos_host: AppKit event monitor records chords (Option, Fn); NativeHost does permissions,
│ login item, Hex Microphone, meeting start/stop/rename/trash/reveal, relaunch
├── Meetings pane lists the store through meeting::query (1 s rescan, sent only when changed);
│ a controller failure shows in the pane; rune::FileDialogs (file_dialog) chooses the
│ meetings folder and Modes' "Choose a file…" .app
├── Tour: practice dictations from the listener's event log; a listener that failed to start
├── Sidebar and pane header move the window (Rune El::window_drag; the title bar is transparent)
├── While the window shows, its poll re-checks timed state every 250 ms (Rune Tx::wake_after); close hides it (Dock policy restored)
├── ⌘Q / menu-bar Quit / Ctrl-C quit and join
├── Meeting capsules: Rune interactive overlays (offer, call ended, ready, recording
│ with Copy new (N) / Copy all of the in-call transcript; compact until hovered,
│ dragged to any corner)
└── Modes app icons: Finder's icon (NSWorkspace) in the picker, chips, and mode rows
Windows `hex app` -> one tray process, Rune window // dev.rune-ui-windows
├── Runtime thread owns WindowsDesktopHost (listener, model preparation, OpenCode, transformations, updater)
├── Named event: a second launch shows the window; tray Show / Enable-Disable / Quit
├── rune::App: --hidden starts windowless, close hides (keep_running); Quit or no tray ends it
├── General adds Launch at login; no terminal paste; chord capture polls
├── HUD: shared indicator model + CPU shader in a Rune overlay
├── Modes: "Choose a file…" opens rune::FileDialogs (file_dialog) on its own thread
├── Modes app icons: the executable's first icon (PrivateExtractIconsW)
└── Not yet: custom caption
iOS prototype -> Record/transcribe/copy and keyboard handoff
Unmerged Windows / deferred features -> Not part of this mapSources: meeting.rs,
meeting_watcher.rs,
developer_control.rs,
SCENES in rune_ui.rs, iOS guide.
iOS physical-device proof and temporary-WAV failure cleanup remain gaps;
best-effort deletion is not guaranteed deletion.
dev.rune-ui lives in rune_ui.rs and its pane modules;
its tests drive the Rune runtime headlessly (navigation; General rows per
shortcut source, fitting the default window with Advanced collapsed, Double-tap to lock folding under the rows below, saves through the host, Quit Hex, microphone-release
confirmation, shortcut recording and Escape, conflicts; History select, delete, two-step
clear, copy; Modes add/delete, corrections, transformations (drag reorder saves once
on release), application conflicts, a chosen file joining the mode, arrow
keys in the model and application searches; the picker's ChooseModel/CancelModel; setup's download; draft
saves; and, from macOS/Windows preview data, each OS's panes and rows, Launch
at login, permission warnings, the
Meetings opt-in card and Settings view,
Meetings select,
search, copy, rename, start/stop, two-step trash, and the overflow menu, and
the macOS tour's permissions, practice, and relaunch; Voice Filter's
experiment gate, modes, sliders, and demo requests, HUD Lab's indicator requests, Activity, and shortcut
sides) and compare 62 offscreen
GPU renders with tests/golden. In these tests the macOS and
Windows requests are recorded, never performed; the macOS native host
(dev.rune-ui-macos) has no automated test of its own. They skip the goldens without a GPU
adapter and do not prove a real window, input method, or physical shortcut.
Before the single-process change, a headless script against an isolated
service saved a new mode and a correction. See
the Rune port plan for what was observed.
dev.rune-ui-macos lives in macos_rune.rs and
rune_ui/macos_host.rs. On hailmary
(macOS 26.2, Command Line Tools only) it compiles, passes clippy
-D warnings and the macOS unit tests (including macos_host's chord
capture), and a debug hex app with an isolated data root started, showed
its 980×760 window (CGWindowList; lsappinfo: Foreground while open),
answered hex dev status/show/hud, and quit cleanly on Ctrl-C, also with
the meeting fixtures copied into its data root and the Meetings pane shown.
macos_host's tests cover the meeting feed over tests/fixtures/meetings
(order, recovery to Interrupted, turns and pauses, no resend when unchanged)
and the practice feed over a written event log. After GPUI's removal the
same host built with a local Command Line Tools stub passed clippy and all
648 macOS tests on hailmary (2026-10-22); it was not launched again. Those
builds stub the HUD's Metal shader and Apple Intelligence (Xcode is missing),
so the HUD failed to load its shader there; window contents (including the
Meetings list and the practice step), dragging the window, the folder
panel, the status item, reopen, shortcut capture, and Sparkle have not been
seen or exercised, and no screenshot could be taken over SSH.
dev.rune-ui-windows lives in windows_rune.rs and
rune_ui/windows_host.rs. It has only
been cross-compiled (cargo xwin clippy -D warnings for
x86_64-pc-windows-msvc and aarch64-pc-windows-msvc, with the
transcribe.cpp build stubbed out, last after GPUI's removal on 2026-10-22); it
has never run on Windows, and no test covers it. It replaces the GPUI app
earlier Windows releases shipped.
each_os_lists_only_the_panes_it_can_run and
developer_panes_show_only_in_developer_builds in
rune_ui/tests/panes.rs check pane
availability from the capabilities, the developer flag, and the Meetings
opt-in; they do not exercise native navigation clicks.
Scenes and tests use fixture state (Service::Fixture, PlatformHost::Fixture):
requests are recorded, never performed. They do not establish native device
discovery or installed-app behavior.
default_paste_hotkeys_follow_build_capabilities in
app_settings.rs checks the production default runtime
and settings projections: Paste Last remains available in both build profiles;
meeting paste is present in debug or after the Meetings opt-in. meeting_paste_is_only_active_in_developer_builds
in suppression.rs checks the corresponding constructed
hotkey event, including ordinary-chord discard in release. These replace an
unconditional debug-only expectation and a test-only copy of the reservation rule;
they do not establish native keyboard delivery or meeting insertion.
structural_event_equality_preserves_serialized_identity in
events.rs covers Activity's full-event equality across
event variants, equal timestamps, non-ASCII text, and optional/error payloads.
It does not exercise native Activity selection or scrolling.
Executed September 3, 2026, source simplification pass: 444 Rust tests passed in both debug and release (nine opt-in tests ignored in each), with all twelve keyboard-layout child scenarios passing in each profile. Formatting and strict Clippy passed in both profiles. These checks cover the working-tree cleanup, not a new published app; no installed-app, microphone, or native Linux test was run.
The working-tree cleanup passed 481 Rust tests in debug and 480 in release, with twelve opt-in tests ignored in each profile and all twelve keyboard-layout child scenarios passing per profile. Strict all-target/all-feature Clippy, formatting, app/helper signing-input guards, and changed shell-script syntax checks passed. The command SDK passed 52 tests; the public SDK passed 56; both typechecks and builds passed. A packed public SDK imported both Promise and Effect entry points from an isolated consumer.
The isolated embedded-service smoke passed API 2 handoff, health/models routes,
absence of discovery-file publication, and shutdown on host-lease closure. It did
not open a microphone or run inference. No signed helper artifact, installed-app
settings flow, provider, physical capture/paste, or Linux integration was tested.
The Linux listener-owner harness and source review do not replace Linux CI.
The optimized Modes preview built and launched, but screenshot capture failed
with could not create image from window; native visual layout is unverified.
One feature file
├── Sub-features -> Small action/outcome diagrams with stable IDs
├── Entry points -> Every user route and its prerequisites
├── Existing checks -> Source/test links and what they do not prove
└── Gotchas -> Exceptions, known defects, missing native proofThis is the map-only stage of the verification workflow, not a working driver. Keep checks beside the behavior and tests with the implementation. Split new files only when useful; link shared failure behavior to Recovery.
Future evidence should name the behavior ID, entry point, revision/binary, platform, prerequisites, outcome, and artifact location. Report skipped paths. Never publish tokens, personal transcripts, or unredacted diagnostics as proof. This map does not authorize operating a live installation.