Commit and push straight to dev (the default branch), no PRs. Merge any open
PR without asking. After pushing, update the installed copy. See
~/code/control/AGENTS.md. production holds what was released and moves
only by a fast-forward from dev (git push origin dev:production) when Kit
releases; never commit to it. There is no main.
dev is canonical and contains the Rune front end on every OS; GPUI is
removed. The rune-ui integration branch is merged and kept only for history:
branch from and push to dev.
Build HEX as a local, observable macOS voice appliance with explicit Linux X11, wlroots-compatible Wayland, and Windows (x64 and ARM64) beta contracts. Keep the engine native Rust and keep consequential behavior explicit. Protected commands and typed captures remain compiled Rust; ordinary literal commands live in the explicit TypeScript user config. User-facing runtime settings persist in Application Support.
The distributed release starts in hotkey-dictation-only mode. Voice commands and
their catalog remain available as a persisted opt-in that defaults off.
Voice Action also requires its own persisted opt-in, defaulting off even for
existing settings without an explicit enabled flag. Meetings (beta) is a third
persisted opt-in (meetings.enabled, default off) that adds call detection, the
Meetings pane, menu-bar recording controls, and meeting paste in release builds.
DEVELOPER_FEATURES_ENABLED keeps meetings always available in debug builds and
keeps the meeting CLI, Activity, and HUD Lab debug-only.
docs/features/README.md indexes existing user-facing
capabilities, entry points, checks, and gaps. Before designing, changing,
debugging, or reviewing a user-facing feature, read the index and relevant
feature files, then check their referenced implementation. The map is not a
substitute for source inspection or a second roadmap.
- Update the affected map in the same change when behavior, entry points, defaults, prerequisites, platform availability, recovery, or verification coverage changes. Do not churn unrelated maps for internal-only refactors.
- Organize by user capability, not Rust module or UI pane. Dictation, Voice Commands, and Voice Action are distinct features; link shared flows and recovery rather than duplicating their descriptions. Keep the index current when adding or splitting a feature file.
- Prefer small action/outcome diagrams in
tsfences with Unicode tree characters, not tables. Preserve stable behavior IDs and keep source/check references and proof limits beside the behavior they explain. - Distinguish implemented behavior, available checks, and observed verification.
A test's existence is not a passing run, and a preview is not native end-to-end
proof. Record missing coverage and known defects explicitly; never rewrite an
expected outcome merely to hide a regression. A map does not authorize live
device tests. Keep future work in
ROADMAP.md.
audio:cpaldevice enumeration and timestamped mono float PCM delivery, live selection, and bounded stream recovery.moonshine,moonshine_lab: the only Moonshine C adapter, the streaming recognizer, and the debug interactive/batch command-fixture evaluator.suppression: the macOS event taps, shortcut suppression, and the configurable dictation-hotkey state machine.keyboard: active-layout key resolution and balanced synthetic shortcuts.dictation,dictation_diagnostics: warm pre-roll, growable capture, 16 kHz local-transcription resampling, and owner-only diagnostic WAV retention whenHEX_RETAIN_DICTATION_AUDIOis set.pause_segmenter: pure 16 kHz pause detection that cuts meeting audio into rolling transcription chunks.dictation_audio: the authoritative microphone timeline, lazy stream lifecycle, recording owner, exact shortcut boundaries, recovery handoff, disposable bounded command audio projection, and the bounded capture sink (open_capture_sink/close_capture_sink) that copies the same timeline to a Scrawl capture.recording_environment: serialized RAII ownership of idle-sleep prevention, output muting, and supported media-player pause/resume behavior.dictation_processor: the one OpenCode client and ordered processing policy for macOS, Windows, and Linux: context-selected corrections, deadline-bounded OpenCode rewrite profiles with corrected-transcript fallback, the ranked model catalog, Voice Action generation, and the transformation stage. Every OS discovers theopencode2beta executable and links missing installs tohttps://v2.opencode.ai/. On macOS and Linux,opencode2 api getdiscovers or starts the managed service, and generation uses the matching owner-only service registration and authenticated loopback HTTP through the system curl; request bodies and credentials use stdin, never argv or new temporary files. Windows sends the same requests through theopencode2 apiCLI inside a job object.parakeet: the strict-Metaltranscribe.cppadapter plus the dictation worker that hands clips to the transcription engine and runs bounded, ordered processing, output, paste, last-result, and meeting-delta work, plustranscribe_with_words, the capture path's word-timestamped foreground requests on the same engine.apple_speech: the SwiftSpeechAnalyzerbridge with per-locale support checks, asset reservation, and batch transcription.transcription,transcription_benchmark: runtime selection, transactional warm-model activation, and the fixed-corpus benchmark runner behindhex benchmark-transcription.gguf_session: thetranscribe.cppsession owner shared byparakeetandlocal_transcriber; it recreates the native session around each run (and each live stream) so inference scratch stays bounded, and owns chunked long-audio transcription.local_transcriber: the Linux and Windows GGUF loader, prewarm, whole-clip transcription, and stream entry point.transcription_engine: the thread that owns the loaded model on every OS and runs every inference in order: live streams fed during capture, their finish, whole-clip runs, and (macOS) model loads and switches. The capture owns itsLiveStreamand hands it to the finished clip; finishing returns aPendingTranscriptwhose wait enforces the stream deadline and falls back to the whole clip. On macOS it also runs background work (meeting chunks) that yields to any dictation request sent before it starts.transcription_preparation: application-owned model preparation shared by Settings and the menu bar on macOS and by the Windows runtime, with one worker, latest-choice pending admission, cancellation fences, and an installed-only path that never downloads a model.transcription_models: the compiled model catalog, language recommendations, pinned artifact verification, and lazy installation.transcription_service: bounded host-audio admission, hostile WAV validation, normalization, cancellation, and inference. In the app, clips up to 30 s run on the dictation engine's model when it is the one requested; otherwise the service loads its own, released after a long clip when the engine holds it.local_api: authenticated loopback discovery or direct-child endpoint handoff, bounded HTTP parsing, model preparation progress, and raw transcription routes, plus the always-on authenticatedPOST /control.control: the releasehexcontrol surface: the versionedControlRequest/ControlReplyprotocol, the settings registry behindhex settings(one dotted key per persisted setting, with the window's rules), and the CLI client (control::cli).macos_rune::controlis the one host that carries the commands out on macOS; Linux and Windows report "not supported on this OS" for now. Contract:docs/cli.md.sdk/typescript: Promise and Effect v4 host wrappers for direct-child lifecycle, authenticated model preparation, and host-audio transcription.developer_control: the typed debug-only command/reply protocol the local API uses to drive the running app fromhex dev.paste: clipboard insertion, continuation joins, and generation-safe clipboard restoration.accessibility: bounded focused-window and selected-text capture through Accessibility without touching the clipboard.recognition: the semantic coordinator for Moonshine, command mode, hotkey and voice-delimited controls, workers, and observations. It does not own the authoritative microphone timeline.command_grammar,spoken_text: typed command patterns, captures, spoken-number and ASCII-punctuation normalization, overlap detection, and command catalog metadata.keys: the platform-neutral key and modifier vocabulary of command actions.commands: pure contextual resolution plus bounded asynchronous action execution (openand CoreGraphics keystrokes on macOS; shell-opened URLs, paths, and applications on Windows).personal_commands: the Bun-hosted@hex/commandsTypeScript workspace: managed SDK provisioning (builtdiston macOS, the embedded TypeScriptsrcon Windows and Linux), the watch-reloaded user command and transformation host, bounded invocation dispatch, and status snapshots. macOS, Windows, and Linux share it; Windows and Linux host transformations only.text_replacements: phrase-boundary-aware compilation of configured text replacements applied longest-match-first to transcripts.config: compiled commands, preferred input devices, and dictation profiles.context: the foreground context snapshot and selectors shared by commands and modes, plus native macOS capture. Browser host is the domain concept; Brave AppleScript is the first macOS adapter andwindows_contextreads any browser through UI Automation.application_catalog: cached installed-application discovery for the Modes picker, one module per OS: macOS bundles, Windows executables from Start menu shortcuts, App Paths, and open windows, Linux.desktopentries.app_settings: the one persistedsettings.jsonon every OS and its live runtime projection for commands, hotkeys, microphone and transcription selection, recording behavior, processing, sound volume, indicator position, interface language, and Dock policy. It records one-time changes inmigrationsand migrates the Windows and Linux beta files once.login_item: the nativeSMAppService.mainAppServiceadapter and its worker thread. macOS owns registration state; it is deliberately not duplicated insettings.json.app_paths: the Application Support owner for runtime logs and shared state, including the one-time macOSvoice-controltohexfolder rename, which runs inmainbefore logging, the instance lock, or any file in the folder opens.onboarding,permission_guide: required permission health, the native floating System Settings guide, selected dictation-model installation, the release startup gate, and opt-in command-model setup.hex_microphone,voice_filter: the experimental macOS CoreAudio virtual microphone (Hex Microphone), lock-free capture-to-sink ring buffer, real-time voice-filter DSP presets, and the offline WAV renderer behindhex dev voice-filter render.linux,linux_app,linux_dictation,linux_paste,linux_migration: the Linux beta CLI (including thehex dictation,hex voice-action, andhex paste-lastkeybinding commands and launch forwarding), the one Linux process (host thread, window on demand through one seam, Quit), the hotkey capture-transcribe-paste loop with Paste Last and Voice Action, paste and primary-selection reads, and the one-timelinux-settings.jsonmigration.linux_input: the one shortcut state machine (hold, double-tap lock, toggle, double-tap only, Escape, Paste Last, Voice Action, andhexcommands) and the monitor that feeds it from X11 key grabs, the Wayland GlobalShortcuts portal, or, without the portal, thehexcommands alone.linux_portal: the GlobalShortcuts portal client on a private bus connection:Register("hex"), one session for Hex's shortcuts, a second session binding Escape only while recording, granted-trigger descriptions, and session-closed reporting.linux_file_chooser: one blockingOpenFilecall on the FileChooser portal over a private bus connection:file_dialog's Linux adapter.linux_context: the frontmost X11 application for modes; Wayland has none.linux_audio_control: While-dictating mute throughwpctl/pactland MPRIS pause over D-Bus, restoring only what it changed.linux_session: display-backend selection matching winit's (Rune's) nonemptyWAYLAND_DISPLAYrule, independent of persisted preferences.linux_service: the single-instance lock and owner-only, same-user Unix socket that forwardsActivate(with the activation token), shortcut commands,Status, andQuitto the one Hex process, plus the per-user systemd lifecycle (Type=notifyreadiness,session.env, start/restart).linux_rune_hud: the focus-free, click-through recording/processing HUD as a Rune overlay in the Hex process: layer-shell on Wayland, override-redirect on X11, at the top or bottom. The listener reports throughLinuxIndicatorand never waits on it; it shows once the window's app runs, whether or not the window is open. Linux has no tray icon.updater: the in-app updater on every OS, a client of the Anomaly release service (docs/plans 8000 /release-service.md).updater/protocolis the protocol's strict SemVer, lenient manifest parsing, and client rules (the service's shared test vectors intests/fixtures/release-protocol);updater/transportis curl withIf-None-Matchand resumable, size- and SHA-256-checked downloads;updater/{linux,macos,windows}detect the install method, verify (OS code signature on macOS and Windows), stage, and restart into an update.windows,windows_app,windows_rune,windows_dictation,windows_input,windows_paste,windows_migration,windows_rune_hud,desktop_indicator_model,desktop_indicator_shader,windows_context,windows_audio_control,windows_login_item: the Windows beta CLI, the runtime host (WindowsDesktopHost: listener, model preparation, OpenCode catalog, transformations, updater) and single-instance claim, the tray app onrune::App(tray, window icon, and runtime thread;rune_ui/windows_host.rsis the window's host), the WASAPI dictation loop, low-level keyboard hook, clipboard paste, the one-timewindows-settings.jsonmigration, the HUD (desktop_indicator_modelsprings anddesktop_indicator_shaderCPU fragment shader drawn into a Rune overlay), UI Automation context, audio ducking, and the login item.dictation_modes: the mode runtime the Windows and Linux listeners share: mode selection by application and website, then global replacements, mode corrections, OpenCode rewriting, and transformations, and the History entry.desktop_i18n: interface translations keyed by the English source string,tr,tr_fill, andtr_named. A test fails if any string the window translates lacks a language.history: the owner-only bounded retained-dictation store: retention windows with hard entry and byte caps, atomic crash-safe persistence, and search. Text and bounded metadata only, never audio.transcript_cli: read-only macOS release commandshex historyandhex meetings list|show|search, versioned JSON projections and filters. Reads history snapshots without opening the writable store, and shares meeting selection, search, and Markdown withmeeting::query/ MCP.events,dashboard: bounded asynchronous append-only NDJSON observations and bounded incremental reading; the debugdashboardTUI and the Activity pane are read-only projections.desktop_activity: the shared listener, device, session, and latest-failure projection overEventReader. Transcript rows read retained events directly.desktop_host: the in-process listener, transcription, shortcut, and update snapshot-and-action types shared by the Linux and Windows hosts, the window, andhex status.rune_ui: the one window on macOS, Windows, and Linux, on Rune withrune-kitand the OpenCode theme: General, Modes with mode-owned corrections and processing, Voice Action, opt-in Commands, Meetings (each feature owns its settings), History, and under Experiments the Voice Filter and developer-only HUD Lab and Activity, the transcription picker, the onboarding tour, the macOS meeting capsules, and the--snapfixture scenes.platform::Capabilitiesdecides which panes and rows exist;rune_ui/linux_host.rs,windows_host.rs, andmacos_host.rsconnect it to each OS's host. Keep shared UI in the pane modules and only OS behavior in the hosts.opencode_catalog: the OpenCode model catalog as the window shows it: loading off the UI thread, the missing/failing notice, model names, search, and thinking variants.status_item: the persistent macOS menu-bar owner for Settings, Paste Last Dictation, meeting recording state and Stop & Transcribe, update checks, and orderly application shutdown.macos_rune: the macOS app as one menu-bar process onrune::App: the status item, the updater, main menu, Dock reopen Apple event, and HUD on the main AppKit loop, and a runtime thread for model preparation and the window's state.rune_ui/macos_host.rsis the window's host and the panes'NativeHost.dictation_indicator,spring: the click-through native Metal/AppKit capture and processing HUD (anNSPaneldriven frommacos_rune's main-loop timer), withspringfor its frame-rate-independent springs and Reduce Motion.meeting: explicit capture throughmeeting_capture, owner-only WAV and transcript artifacts, Screen Recording permission checks, final local-model publication, the user-facing Markdown copy, and recovery.meeting_capture: independent self-healing microphone (cpal) and system audio (Core Audio process tap, ScreenCaptureKit fallback) sources; one failing never stops the other, and resumed audio is padded onto the timeline.meeting_transcript: streaming 16 kHz resampling, short pause-aware overlapping chunks transcribed as background work on the dictation engine's loaded model (a process without the engine, such as the CLI, loads its own), seam deduplication, echo suppression, the rolling in-call transcript and itsTranscriptReader, and Markdown rendering.meeting_echo: 20 ms energy envelopes of both tracks and the signal-level bleed/local classification of microphone segments.meeting_notes: a bounded provisional title while recording, then one post-publication call over a bounded transcript excerpt for the final title and a two-sentence summary, with deadline, manifest storage, and marker-delimited in-place Markdown updates.meeting_chapters: live chapters (experimentlive-chapters): one on-device pass per ~3 minutes of rolling transcript while recording, with Rust deciding topic changes, the per-boundary fix-up after publication, chapter-extended titles that never touch a user-typed one, and section Markdown for Copy section.meeting_query: read-only meeting queries for agents (meeting::query): lock-aware listing, in-memory BM25 full-text search with stemming and fuzzy matching, transcript paging, stored notes, and Markdown rendering. It never writes to the meetings folder.meeting_mcp: the thinrmcpstdio adapter behindhex mcp, exposingmeeting_queryas read-only tools andhex-meeting://resources.apple_intelligence: the weak-linked FoundationModels Swift bridge: availability, context size, and guided-generation notes at utility priority.microphone_activity: permission-light CoreAudio process input observation.meeting_detection: pure provider classification, debounce, per-call snooze, and call-ended presence.calendar: calendar meetings on every OS as a pure model (which events count as meetings, call-link parsing, the wall-clock reminder scheduler), plus the macOS EventKit bridge (calendar.swift): access, the change counter, bounded event reads, and the Settings snapshot.meeting_watcher: the meeting controller loop: call detection, offers, calendar reminders, recording workers, call-ended presence, notes, and the menu-bar meeting state. The capsules that show its events arerune_ui::meeting_capsules.file_dialog: the one native open dialog for a file or folder (Modes' "Choose a file…", the macOS meetings folder):NSOpenPanelmodeless on AppKit's main thread,IFileOpenDialogon its own STA thread, the portal on its own thread. Answers on a channel; cancel and a dialog that never opened read as nothing chosen.experiments: the compiled list of unsupported experiments and the launch snapshot ofHEX_EXPERIMENTSand~/.config/hex/experimentsbehindexperiments::enabledandhex experiments.captureexists only in builds with its build option.capture_host: Scrawl's capture engine (thescrawl-capturecrate incrates/capture, copied from Scrawl'sengine/splitate351ddband kept out of the workspace so only--features captureon macOS builds it;scripts/checknames it there) hosted in Hex for thecaptureexperiment (docs/plans/capture-in-hex.md), compiled only ascfg(hex_capture): a macOS build with thecaptureCargo feature, a build option off in every public build (HEX_CAPTURE=1 scripts/build-app.shbundles and signsscrawl-helperbesidehex). Hex'sHost,ConfigSource, and Parakeet or AppleTranscriberfor the engine, the listener-ownedCaptureHostbehindhex captureand the capture shortcut, its 16 kHz gap-padded timeline handed to the engine asAudioIn::External, the tap's pointer input forwarded toCapture::input(), Scrawl.app coexistence, the retention thread, and theCaptureSnapshotevery view reads (capture_host::snapshot/watch, published on each change, so a capsule never reaches into the host).rune_ui/scrawl.rsis its pane (Experiments › Scrawl) andCaptureSettingsitscapture.*settings.instance: the exclusive Application Support lock that prevents concurrent command listeners.process_exit: how the macOS app ends. Every quit path (menu,hex quitandhex restart, Ctrl-C, and AppKit's terminate) stops and joins Hex's workers, bounded by a 5 s deadline, kills any personal-command host left, then calls_exit, so C++ static destructors (ggml's Metal device, Moonshine's transcriber map) never run under live threads. A restart schedules its relaunch first; the new copy waits for the old pid to exit.feedback: bounded volume-controlled mode, capture, cancellation, and failure tones.
Keep modules deep. Callers should not coordinate Moonshine stream handles, CoreAudio formats, AppleScript details, or event serialization.
- Every build starts dictation-ready without loading Moonshine or a command executor unless the persisted command opt-in is enabled. Enabling commands loads Moonshine off the audio-consumption loop; disabling them unloads the recognizer and executor.
- On a new Mac, release dictation starts only after Microphone, Input Monitoring, Accessibility, and the selected dictation model are ready. Opt-in command recognition additionally requires Moonshine, but its background preparation or failure must never block hotkey dictation. Failed preparation exposes explicit retry and disable actions without silently changing the command opt-in.
- Among recognized voice commands, sleeping mode accepts only standalone wake phrases. Dictation and explicit paste shortcuts remain available.
- Unmatched completed speech is ignored and logged.
- The dictation shortcut defaults to Option but supports modifier-only, modifier-plus-key, standalone Globe/Fn, and standalone function-key bindings. Capturing a new binding suspends global matching.
- GUI startup must successfully call
keyboard::initialize_layouton the OS main thread before settings, keyboard-using workers, or Rune start. That snapshot is fixed until restart; cached misses must not re-enter TIS/TSM. Uncached headless lookups retain live-layout resolution, with all native lookup and snapshot construction serialized by the same lock. - Another app's global shortcut (Carbon hotkeys, such as Spaceman's Option-1) consumes its key before the session taps. A listen-only HID tap reports key presses as chord evidence only: it discards an early hold or pending gesture like any unrelated key, never tracks key state, and skips Escape and Hex's own shortcuts. Its timestamps may be Mach ticks or nanoseconds.
- Stale shortcut recovery requires at least 100 ms of sampled keyboard neutrality,
no pending input, and no hold or pending gesture. A lock is repaired only when
it tracks a held key, and it stays locked so a fresh press can still finish it.
It emits no capture actions.
Native
SecondaryFnnavigation metadata may be ignored only with timestamped modifier-change evidence that Fn is up; the full key scan and delayed-event fences remain required. Never strip Fn globally from shortcut matching. - Hold the shortcut to dictate and release to transcribe. Captures shorter than 300 ms discard. A 450 ms hotkey pre-roll and one-second voice-trigger pre-roll protect speech onset. Capture has no automatic duration limit; release, explicit stop, or Escape ends it.
- When enabled, a second shortcut tap within 300 ms locks dictation. Press the shortcut again to finish or Escape to cancel.
- When commands are enabled, every dictation or paste hotkey action resets Moonshine so shortcut audio cannot leak into a later command.
- Recording audio behavior and idle-sleep prevention begin only after the intentional-hold threshold. Ordinary shortcut chords must not mute output, pause media, or prevent sleep.
- The start tone confirms the shortcut press itself, not the hold threshold: it is provisional, sounds after a 40 ms chord grace, and a chord, short tap, cancel, or Voice Action takeover retracts it (silently inside the grace, else a 20 ms fade). With the microphone released while idle, the tone waits for the open microphone and the hold.
Release microphone while idleand Commands are mutually exclusive through explicit confirmed transitions and a shared runtime policy. When release is enabled, pressing the shortcut opens the selected device asynchronously with no pre-roll, preserves the physical press for the hold threshold, discards a release before readiness, and closes only after the authoritative capture is idle. Accepted jobs never keep the stream open. A stream failure before the hold threshold returns the press to pending and reopens; later failures fail the capture. Cold opens retry the selected device briefly before falling back.- Dictation remains available while command recognition sleeps.
- Model inference, optional post-processing, paste, and application actions must never block authoritative audio capture. Their queues remain bounded.
- CoreAudio capture timestamps and annotated-session CGEvent shortcut timestamps
share the macOS boot-time nanosecond clock. Do not apply the Mach timebase to
annotated-session timestamps. Physical HID-tap timestamps arrived as raw Mach
ticks on the tested Apple Silicon system, while some HID events already used
nanoseconds; changing tap location requires revalidating timestamp units.
Delayed press handling reconstructs the original onset from the timeline;
delayed release handling excludes audio captured after the physical release
plus
POST_RELEASE_TAIL(200 ms). - A shortcut release or locked press that finishes a transcribed capture keeps audio through that exact event plus the 200 ms tail. The event timestamp is never moved; the hold threshold uses it alone. The audio owner waits at most the tail plus 100 ms for that audio, and a stream stall, failure, or gap in the tail finishes with the audio already delivered instead of failing the capture. Escape, short taps, voice controls, programmatic stops, and meetings get no tail. A press during the tail is handled after the old clip finishes.
- Active dictation capture is lossless with respect to Moonshine, event, UI, context, and worker stalls. Command recognition is explicitly best-effort: its backlog is bounded by duration, and pressure invalidates the generation, discards stale audio and updates, and resets Moonshine without touching the active recording.
- The dictation shortcut has a persisted
shortcut_mode:hold(default) ortoggle. Toggle reuses the lock state on every platform: each press acts as the second tap of a double-tap, so release locks and the next shortcut press finishes. Only Escape cancels, and a modifier pressed alone never ends a key-chord capture. Double-tap settings apply to Hold only. - Streaming dictation is speculative on every OS: a hotkey capture's stream receives exactly the capture's 16 kHz audio, the finish always carries the complete clip, and a failed, empty, overfed, late, or interrupted stream falls back to the whole clip. Nothing pastes, and no partial text shows, before the final text. One engine thread runs every inference because a live stream holds the model's compute lease. Streams run only on GPU devices (Metal, Vulkan); models that cannot stream transcribe the whole clip after release. Dictation is never cut at pauses; meetings are.
- Starting a new capture never cancels accepted dictation work. Pending jobs preserve submission-order output while capture remains immediately available.
- Completing or pasting an older job must not reconcile or finish a newer capture. Shortcut boundaries come from delivered CGEvent timestamps; do not fabricate a release timestamp from later physical state.
- Escape cancels the active capture first, then the newest unfinished dictation. Cancelled jobs never paste, update the last result, or block later output.
- Model switches activate only after the pinned artifact is checksum-verified, loaded on strict Metal, and prewarmed. A failed switch preserves the active model and persisted selection.
- The menu-bar model picker lists only downloaded, available models and remembers each model's last selected language and recognition hints. It works without a Settings window. Settings and the menu bar share one preparation owner; a newer choice supersedes uncommitted work, and preparation/save failures leave the old selection intact. Menu-bar selections never silently download or repair a model.
- Cohere's advertised encoder input capacity is not its recommended inference window. Both GGUF adapters cap Cohere chunks at the reference processor's 35 seconds (or a stricter advertised bound); capture duration remains unlimited.
- A selected microphone switches live only while capture is idle. Opening the
replacement must succeed before the old stream is dropped. If the persisted
device is unavailable at startup, log the failure and fall back through the
compiled preferred-device order to the macOS default. An explicit CLI
--deviceremains authoritative for that listener process. A runtime stream failure cancels an incomplete capture, discards stale chunks, and retries the same selection with bounded backoff; it must not require a settings change or process restart. An initial open failure uses the same bounded recovery without stopping the listener. Cancellation does not stop warm-microphone retries; enabling release-while-idle stops them once capture is idle. - Feedback volume is persisted from zero through one; zero disables tones. Volume changes apply immediately and preview one recording-start tone.
- Launch at Login uses
SMAppServicefor the signed main app. Treat bothNotRegisteredandNotFoundas disabled before registration, representRequiresApprovalwith a link to Login Items settings, and poll macOS as the source of truth instead of persisting a parallel Boolean. - Voice Action is an optional second capture target. Its saved shortcut defaults to Option-Command, but no shortcut is active or reserved until the user enables Voice Action. Disabling it cancels its active capture, not accepted jobs, and preserves the saved shortcut and model. The toggle remains reachable when OpenCode is unavailable. Promoting an active Option capture preserves all recorded audio. Selected text is optional prompt context; inaccessible or empty selections act as no selection. Use the dedicated OpenCode model and deadline, return only paste-ready text, and paste at the current focus. Failed, empty, cancelled, or timed-out actions paste nothing. Voice Action jobs share normal queueing and cancellation but never update the last dictation.
- Mode processing is best-effort and ordered: corrections, optional OpenCode rewriting, then selected transformations. A failed stage preserves its input; the transformation chain is one stage, so failure discards its partial results. It applies to Paste and Send, but not Voice Action or meetings.
- OpenCode availability checks stay off the UI thread. When the app finds the
opencode2executable,opencode2 apidiscovers or starts the managed service; the catalog uses authenticated loopback HTTP to avoid large CLI pipe truncation. HEX never invokesopencode2 serve --serviceor owns that service's lifecycle directly. A missing beta install is retried at a coarse interval so installingopencode2while Settings is open refreshes the model catalog without restarting HEX. Catalog failures require an explicit retry rather than spawning clients in a tight loop. - Ordinary resolver commands execute only from completed Moonshine lines. Voice-delimited activation and stable control phrases are deliberate exceptions.
- Contextual commands enter the candidate set only when their predicate matches.
- Literal and typed command patterns compile into one registry. Reject overlaps at configuration time and generate the catalog from that registry.
- Successful commands rely on their action for feedback. Wake/sleep use quiet tones; execution failures use an error tone.
- Clipboard insertion is the fixed text-insertion path. Restore the previous clipboard only if no newer paste or external clipboard change superseded it.
- Option-Shift-V pastes the last completed dictation. Option-Control-V pastes completed meeting turns added since the previous successful invocation.
- Do not persist captured audio by default. Explicit foreground meeting
recording must remain visibly active. Diagnostic dictation retention is an
explicit, bounded, owner-only opt-in through
HEX_RETAIN_DICTATION_AUDIO. - Retained dictation history records only successful pasted output: raw and final text plus bounded metadata, never audio, full browser URLs, or window titles. History defaults to seven days, and every retention window remains subject to hard entry and byte caps. No new entries are accepted while Off. Turning retention Off preserves existing entries; clearing history is explicit. Time-based retention expires during idle uptime through the shared history owner; searches never return expired entries while waiting for disk cleanup. The hard caps are 10,000 entries and 16 MiB of raw + final text (oldest evicted first), with existing per-entry metadata and text limits. There is no archive.
hex historyandhex meetingsare read-only, release-available on macOS, independent of feature opt-ins. Dispatch before folder migration, process logging, the instance lock, local API, GUI, permissions, or Hex Microphone. Resolve the existing data folder without renaming it. History reads one open file through concurrent atomic replacements, applies the saved retention at query time, and reports malformed files/settings without repairing them. JSON contracts are documented indocs/transcript-cli.md; incompatible changes require a version increment. Debugmeeting list/showuse the same read-only path; recording and other debug commands keep their existing roles.- Meeting detection may inspect process audio metadata but must not capture
samples. Detection offers recording, and starts it automatically only for a
provider (website or app) the user explicitly set to Always record in
Meetings › Settings › Detected calls or an offer's "…"; every provider
defaults to Ask. An automatic start passes the same stability time and
snooze as an offer, so a stopped or dismissed call never restarts until its
app leaves the microphone, and it shows as visibly as a manual recording. An
offer that times out only closes. Release builds keep the meeting controller
idle, without inspecting microphone activity, until the user opts in. When the
detected call app leaves the microphone, offer
Keep RecordingorStop & Transcribe; never stop automatically. An active recording stays visible and stoppable from the menu bar without the Settings window. - Calendar access is asked for only by Meetings › Settings' Connect calendar, never at
launch, and only with Meetings on; Meetings off means no EventKit reads and
no reminders.
meetings.calendar.connecteddefaults off. A meeting has other attendees or a call link and is not all-day, declined, free, or cancelled. Reminders are Hex capsules, not notifications, shown once per occurrence at the lead time (or on a wake within 5 min of the start) and withdrawn when the meeting is cancelled or moved. One reminder shows at a time, never over a recording. EventKit is read on the controller thread on store changes or once a minute and on Settings' reader thread, bounded, with no network, never on the audio or UI thread; nothing touches the store before Connect. A reminder never records without a click; Always record still needs the detected call. Recording or dismissing a reminder snoozes the same call's offer and automatic start; without a call link, only the next call near the start. - Meeting transcription defers to dictation and never loads a second copy of the model: each chunk runs on the dictation engine's loaded model at utility QoS, starts only while no dictation capture (from the shortcut press) or dictation inference is active, never interrupts a live stream, and is retried when a dictation request was sent first. A chunk already running cannot be aborted on Metal; it finishes during the hold threshold and release tail. Chunks are short (first pause after 6 s, hard cut at 10 s of new audio plus 2 s overlap), so a running one finishes well within the hold threshold. It uses whatever model dictation has loaded, so a model switch applies from the next chunk; Apple Speech and a process without the engine use their own transcriber. Its queue (one-second audio blocks) and chunk backlog stay bounded; a source that falls behind skips its oldest audio, keeps going, and is rebuilt from its WAV after Stop. A capture gap restarts that source's chunks after the gap.
- Quitting while a meeting records stops capture and saves its WAVs and
manifest (
Interrupted,final.pending) without transcribing; once quitting begins, no meeting work starts a chunk or loads a model. The next launch with Meetings on runs the final pass on the dictation engine's model (or its own after 30 s without an engine). - Meetings run no Moonshine. Every in-call view and copy (the Meetings pane,
Option-Control-V, provisional titles,
hex meetings, and MCP) reads the rolling Parakeet transcript, which publishes only entries no later chunk can replace; after Stop they read the final transcript. - Meeting system audio uses a Core Audio process tap (System Audio Recording consent), never requiring a display; ScreenCaptureKit is only the fallback. Losing either source never stops the other or the meeting: it retries until Stop, and resumed audio is padded with silence so WAV positions stay on the shared capture clock.
- Meeting recording prevents idle sleep but does not apply dictation mute or pause-media behavior.
- Apple Intelligence meeting notes run only after final publication, on a
utility-QoS thread that yields to dictation, within a bounded chunk count and
total deadline. Failures record
notes_errorand leave the Markdown untouched. Notes never rename the file and rewrite only the heading HEX wrote and the<!-- hex:notes -->block. - Captures (the
captureexperiment) are a build option, off in every public build: only a macOS build with--features capture(debug, or release throughHEX_CAPTURE=1 scripts/build-app.sh) has them. Any other build does not know the experiment, never compilesscrawl-capture, and refusesscrawl-capturewithunsupported; Linux, Windows, and Nix never compile it. The Release and test-build workflows never pass the feature;release-app.shrefusesHEX_CAPTUREandvalidate-app.shrefuses a bundle withscrawl-helperor the capture engine. With the experiment off, the listener builds no capture host: no engine, sink, thread, bundle folder, or work, and the same holds with the experiment on whilecapture.enabled(the Scrawl pane's opt-in, default off) is off. Scrawl's engine records the screen, runs withSharedResources::none()(Hex never callsinstall_shared_resources: no ingest server, ingest token, delivery retries, or global shortcuts), logs to tracing, and writes only to the captures folder (capture.folder, Scrawl's own by default). Hex's capture shortcut (default ⌥⇧S, a key required, never overlapping another binding) is a fresh press that starts or stops; while Scrawl.app (dev.kitlangton.scrawl) runs, Hex registers no capture shortcut and runs no retention, and only the pane's "Use Hex for captures" quits Scrawl. Escape cancels a recording capture before any dictation job. Pointer events and Shift reach the engine from the one session tap, only while a capture records, through a bounded queue that drops rather than block; they never reach the listener. Scrawl's settings are imported once, read-only; Screen Recording is asked at the first capture or from the pane, never in onboarding. A capture never opens a microphone, installs a tap of its own, or loads a model: it copies the dictation timeline through a bounded sink that records a gap rather than block, so a stalled capture never stalls dictation, and while a sink is open the release-while-idle close waits for it. One capture engine per process, one capture recording at a time. Its words are foreground requests on the dictation engine's loaded model in 30 s chunks: after dictation already sent, before meeting chunks, never a second model, and dictation output is unchanged. A chunk waits while dictation captures or transcribes and never interrupts a live stream; the engine holds it until the stream ends.stopreplies at once and the screen recording stops then (Capture::stop), not when the capture is written; one thread writes stopped captures one at a time, at most t 8BD0 wo wait, and a start beyond that is refusedbusy. The engine's log goes to tracing from before it is built, and its settings come from Hex (ConfigSource), never~/.config/scrawlafter the import. Bundles are owner-only. Quitting cancels a recording capture, stops transcription between chunks, and waits at most 2 s for capture threads. hex mcpis read-only and available in release builds regardless of the Meetings opt-in. It dispatches before process logging, the instance lock, GUI, or permission setup; never writes, recovers, or indexes on disk inside the meetings folder; runs no model; and makes no network request. Stdout carries only MCP messages.hexcontrol commands (status,quit,dictation,meetings record,settings set,model use,window show, …) are release-available and drive only the one running Hex through the owner-only discovery file and bearer-authenticatedPOST /control(64 KiB request, 1 MiB reply, 10 s in-app deadline). They dispatch before folder migration, process logging, or the instance lock, and never launch Hex; onlyhex startandhex restartopen it. Every command is carried out by the app's own paths (the menu bar's meeting request, the window's settings save followed byReloadSettings, the shortcut state machine), so CLI-started recordings look and stop exactly like the UI's and opt-ins stay explicit settings. Commands are idempotent (changed: false).settings::every_persisted_setting_has_a_cli_keyfails when a persisted setting has nohex settingskey; a protocol change that breaks either shape incrementsCONTROL_VERSIONanddocs/cli.md.- Meeting echo is judged on one timeline built from WAV sample positions, never
from packet arrival. A
Youline or rolling entry is judged only once the system audio that could explain it has arrived (bounded when the system track stalls). Measure echo changes withmeeting_echo_replayon a recorded speaker-mode call, counting genuineYoulines lost as well as echo kept. - Moonshine sessions run single-threaded (
MOONSHINE_ORT_SINGLE_THREAD, set first thing inmain); ONNX Runtime's default spinning pools cost several times the CPU of the decoding itself. - The rolling transcript remains available if final transcription fails. Final
publication is atomic; adjacent same-source entries within three seconds form
one displayed turn labeled
YouorThem. Speaker echo never shows asYou: the rolling snapshot, live paste, and final transcript drop a microphone utterance when delayed system audio explains its energy, or when its words repeat overlapping remote speech unless the signal shows local speech (double-talk). Each finished meeting is also written as Markdown to the configured folder (default~/Documents/Hex Meetings) without overwriting existing files; an export failure does not fail the meeting. - Meeting paste cursors advance only after successful insertion and reset when HEX restarts. They track, per source, the end of the text already handed out, so rewrites near the live edge and the switch to the final transcript neither repeat nor drop turns.
- Deleting a meeting moves its exported Markdown and then its recording folder
to the Trash (never
rm), after a two-step confirmation, and is refused while it records, transcribes, or writes notes. Renaming rewrites the manifest and only a Markdown heading that still reads the previous title; files keep their names. - Visible state remains
Transcribinguntil every accepted transcription or paste output has completed, including ordered post-processing and insertion. - The HUD is observational and click-through. It must not alter capture boundaries, steal focus, or block controls in the foreground application.
- Desktop UI is governed by
docs/design-system.md; read it before changing any pane, capsule, or picker. Rune is the only UI toolkit: composerune-kitcomponents and Hex's pieces inrune_ui/controls.rsandrune_ui/lists.rs, and name only the OpenCode theme's tokens, never colour literals. Use sentence case and the product name "Hex" in all copy. Reserve the accent (Primary) fill for at most one main action per screen; states such as recording or an available update use a Secondary button with a status dot. Extend a piece instead of adding a parallel helper, and verify visual changes with--snapscenes in light and dark. - Neither Hex nor Rune (
anomalyco/rune, public MIT) is published to crates.io; never runcargo publish. Hex depends onrune,rune-kit, andrune-theme-opencodeat one pinned Git rev inCargo.toml; every pin bump must also updatecargoLock.outputHashes."rune-0.0.1"innix/package.nixand keepwindows0.62.2 forgpu-allocatorinCargo.lock. - At the start of each work round, bump Hex's Rune pin to Rune's
devhead: read the## devsection of Rune'sCHANGELOG.mdsince the current pin, adapt to its breaking changes, updatenix/package.nix's outputHash, and runscripts/check rune_ui, goldens included. If Rune itself breaks, report it to Rune rather than working around it. - Hex runs as one long-lived process per user and data root on every OS: a
menu-bar process on macOS (
macos_rune, nativeNSStatusItem, native MetalNSPanelHUD, Rune meeting-capsule overlays), a tray process on Windows (windows_rune, named-event single instance, Rune overlay HUD), and a single-instance process on Linux (linux_app, socket + lock in the data root, Rune overlay HUD). Closing the window never stops the runtime, listener, HUD, or accepted work. - Updates follow the release protocol's client rules exactly (the shared
vectors must pass): a strictly newer version unless the running one is
revoked, the minimum version before rollout, and a rollout bucket drawn once
from the OS's secure random source, kept in
<support>/update-bucket, and never sent. Requests carry nothing about the install. Checks run 10 seconds after launch, hourly, and on the sidebar footer's Check for updates, backing off on errors; manifests are not signed, so nothing runs before the download's size and SHA-256 match the manifest and, on macOS and Windows, its code signature passes: macOSapp/update-requirement.txt(Developer ID team5NZ4Q7NXJ4,anomaly.hex, notarized), Windows a valid Authenticode signature whose signer isAUTHENTICODE_SIGNER. Linux relies on HTTPS and the SHA-256 alone. - Only direct installs replace themselves: Linux and Windows with our
installer's receipt (
install.jsonin the user-owned data root, which also names the channel) and the version layout, macOS aHex.appthis user can rename in a writable, non-translocated folder. A read-only macOS location shows Download update (the DMG); package-managed installs (pacman, Nix) show their manager's command once it serves the newer version; source and test builds never check;DisableUpdates(macOSanomaly.hexdefaults domain, WindowsSOFTWARE\Policies\Anomaly\Hexin HKLM or HKCU) turns checks off. Never escalate privileges, and never overwrite development, root, or package-manager-owned files. - A staged update waits for Restart to update. Linux and Windows activate a
new version directory and switch
current/current-versionatomically at staging; macOS unpacks beside the bundle and swaps it in withrenamex_np(RENAME_SWAP)at restart or quit, and the new version removes the old bundle at launch. - The Wayland beta requires compatible clipboard, virtual-keyboard, and
layer-shell protocols. Shortcuts come from the GlobalShortcuts portal when the
desktop offers it; otherwise the user binds the
hex dictation,hex voice-action, andhex paste-lastcommands in the desktop's own keybindings. One general path: no compositor-specific code, no raw input devices, no input-group requirement, and no silent fallback to XWayland or privileged input injection. The desktop may grant a different trigger or refuse one; Settings shows what it granted. Escape is bound only while something records. Modifier-only shortcuts stay unsupported on Linux. - Every Linux shortcut source (X11 grabs, portal presses,
hexcommands) feeds the samelinux_inputstate machine and emits the same events. Commands are idempotent: starting while recording or stopping while idle does nothing. They talk only to a running, listening Hex and fail with a clear error otherwise; they never start one. The socket hands them straight to the listener's queue, never through the window's thread. - One Linux Hex process per user and data root owns the microphone, model
workers, saved settings, History, the HUD, and the window. The window is
optional and opened on demand; closing it never stops capture, accepted
work, or the HUD, and cancels only an uncommitted shortcut capture. A
relaunch forwards
Activate(with any activation token) and exits 0; hidden and autostart launches never show the window. With the managed unit loaded and the default data root, launches start Hex through systemd (Type=notify), which owns its lifetime and restart; a crash restart opens no window. Otherwise the launching process becomes Hex. The socket stays owner-only and peer-UID checked, with bounded framing, queues, and I/O, and carries onlyActivate, shortcut commands,Status, andQuit. Window failures (no display, no GPU) are logged and never stop Hex. The unit'sExecStart … service,hex app,hex app --hidden, andhex startstay valid entry points forever: self-updates replace only the executable. - On Linux and Windows, saved settings apply to the running listener in place
while no dictation records; a settings change never stops listening or
changes the shown status. Shortcut changes register the shortcuts again,
a microphone change reopens the stream, and a model or recognition-hint
change loads the new model beside the old one and swaps it in once no
transcription is pending (
local_transcriber::ModelSwap). Recording a shortcut in the window suspends only global matching. - Linux shortcut capture uses focused input in the window. Recording a
shortcut suspends the running listener's global matching and registers the
shortcuts again when it ends; cancellation restores the old binding unless
its save already committed. The window and the CLI must expose Hex's state, recovery, and
errors.
hex quit, the window's Quit Hex, and SIGTERM drain workers before exit. HUD teardown only hides that listener's HUD. - Linux paste keeps transcripts off helper argv and bounds helper I/O. X11 waits
for physical modifiers without discarding accepted output, and shutdown
cancels that wait; on Wayland,
wtype's virtual keyboard carries its own modifiers, so held keys never change its Ctrl+V. New installs use Ctrl-V; the terminal-paste preference selects Ctrl-Shift-V and defaults on for persisted legacy X11 settings. The beta retains the transcript clipboard; arbitrary MIME restoration and consumption acknowledgments remain unimplemented.
The public TypeScript SDK is @kitlangton/hex in sdk/typescript. The first
release, 0.1.0, was bootstrapped manually; subsequent user-facing SDK changes
must include a Changeset under sdk/.changeset/ unless preparing an initial
unpublished package. Run package commands from the sdk workspace root.
Before release, run:
cd sdk
bun install --frozen-lockfile
cd typescript
bun run check
bun run test
bun run build
npm pack --dry-runWhen exports or packaging change, install the packed tarball in a clean consumer
and import both @kitlangton/hex and @kitlangton/hex/effect. The release
workflow is .github/workflows/release-typescript.yml; npm trusted publishing
authorizes anomalyco/hex and that exact workflow filename for npm publish.
The workflow uses npm trusted publishing without requesting a Sigstore
provenance bundle.
Use the configured Changesets release command (cd sdk && bun run release), not
direct npm publish, after the bootstrap release. Pending changesets on dev
keep a version pull request open into dev; the workflow publishes only when
production is fast-forwarded to an unpublished SDK version.
./scripts/setup.sh
./scripts/setup-parakeet.sh
bun run --cwd sdk/commands build
cargo run -- listen
cargo run -- status --json # asks the running Hex (docs/cli.md)
cargo run -- dev dashboard # debug recognition TUI
cargo run -- meeting record --title "Design sync"
cargo run -- meeting probe
cargo run -- mcp --meetings-dir tests/fixtures/meetings
cargo run -- app -- --list-scenes
cargo run -- app -- --snap /tmp/hex-settings.png --scene mac-settings
cargo run -- app -- --snap /tmp/hex-modes.png --scene modes --theme light
cargo run -- app -- --snap /tmp/hex-onboarding.png --scene mac-onboarding
./scripts/install-app.sh
scripts/check # fmt, clippy, every test
scripts/check meeting::query # fmt, then only matching tests
git diff --check
./scripts/test-app-identity.sh # macOS only
./scripts/test-install-linux-release.sh # x86_64 Linux only
cd sdk/typescript && bun run check && bun run test && bun run buildUnfinished features land on dev dormant behind an experiment
(docs/features/experiments.md): add a variant to
Experiment in src/experiments.rs and gate where the feature's work starts
(worker, thread, capture), not just its UI. Turn one on for yourself with
hex experiments enable <name> or HEX_EXPERIMENTS=<name>; it applies at the
next launch. Experiments are unsupported and never on by default, in debug
builds too; remove the variant when the feature ships or is dropped.
Use scripts/check [filter] for the local loop, not raw cargo test. It runs
tests through cargo-nextest (brew install cargo-nextest) with
.config/nextest.toml, which kills a hung test and reports it by name; CI uses
the same config's ci profile. Read its output whole rather than piping it
through grep | head; failures are summarized at the end.
Use Rune's headless renders as the desktop-UI iteration loop: hex app -- --snap <png> --scene <name> draws one named fixture scene from SCENES in
src/rune_ui.rs without starting the runtime, on every OS (--list-scenes
lists them; --theme light|dark, --size WxH, and --scale adjust the
render). Scenes cover every pane and state the goldens check: the Linux and
Windows panes (modes, voice-action, history, picker, onboarding,
windows-settings, …) and the macOS ones (mac-settings, mac-commands,
meetings*, mac-voice-filter*, mac-hud-lab, mac-activity, mac-onboarding*).
live renders the running Linux Hex's real state. On a host without a GPU,
render on lavapipe with VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.json.
cargo test rune_ui compares the light and dark goldens in tests/golden/.
A scene is a fixture, not native end-to-end proof; the meeting capsules and
HUDs have no scene.
The supported Linux beta and release host use x86_64 Arch Linux. Install its native build dependencies with:
sudo pacman -S --needed base-devel git rustup python alsa-lib curl jq openssl xxd \
util-linux glib2 libxkbcommon libxkbcommon-x11 libx11 libxcb openblas \
vulkan-headers vulkan-icd-loader \
shaderc spirv-headers clang cmake pkgconf wl-clipboard wtype
rustup default stableFor a source install, run scripts/install-linux.sh, then hex model install.
The installer owns the user-local version layout, desktop entry, and autostart
entry; only that managed layout participates in automatic updates.
nix develop supplies the same native build environment as the Nix package;
nix flake check checks modules, shell configuration, session readiness, and
the display-free Rust suite. See docs/nix.md. Nix owns package updates and
optional Home Manager autostart. Native Linux PR checks do not replace real
X11/Wayland, portal or keybinding shortcuts, microphone, and target-application
smoke tests.
Automatic microphone selection follows the compiled preference order in
src/config.rs, then falls back to the macOS default. A saved microphone takes
precedence while available; override everything with --device. The app bundle
build requires Xcode 26 for Icon Composer compilation and a Developer ID signing
identity. scripts/release-app.sh prepare builds the notarized and stapled
DMG and ZIP the Release workflow publishes. Hex publishes every platform to
the Anomaly release service (anomalyco/release; https://anoma.ly/release,
dev https://dev.anoma.ly/release) from the Release workflow with curl and its
GitHub OIDC token, under app hex, artifact desktop;
docs/plans/release-service.md pins the
target and file names and the direct-install receipt, and
docs/releasing.md owns the workflow, channels, secrets,
and old clients. The 2.3.1 feeds and appcast at https://anoma.ly/hex/files
(and Kit's downloads.hex.kitlangton.dev/.com, which must keep redirecting
there) stay frozen. GitHub app releases use
app-v<VERSION> tags with --latest=false, keeping them distinct from SDK
releases. Never overwrite a published artifact with different bytes.
https://anoma.ly/hex (in-repo site/) is the only Hex website. One
site/cloudflare.config.ts defines the production Worker hex-production-site (anoma.ly/hex,
bucket hex-production-files) and the dev Worker hex-dev-site (dev.anoma.ly/hex,
bucket hex-dev-files); cf deploy --prebuilt --mode <mode> picks one, and
only CI deploys: a push to dev touching site/ deploys dev, a stable
Release deploys production. No subdomains. The site's download links come
from the service's stable manifest when it builds. After publishing a version,
bump the explicit GitHub mirror/version link in README.md and the Homebrew
cask. bun run --cwd site check:downloads fails until every download link
serves a file; every deploy runs it. Verify the live links afterwards.
The Rust app is named Hex, packaged as Hex.app,
with bundle identifier anomaly.hex (deliberately not reverse-DNS) and executable
hex; its SDK helper is anomaly.hex.service and the virtual microphone driver
anomaly.hex.microphone.driver. Releases through 2.3.1 were com.kitlangton.hex2,
signed by Kit's team QC99C9JE59; macOS treats anomaly.hex as a new app (fresh
permissions, login item, and preferences domain), while the hex Application
Support folder, and with it settings, history, and models, is shared. Signing requires
an explicit VOICE_CONTROL_TEAM_ID and matching HEX_NOTARY_PROFILE; the build
and release scripts verify the selected team rather than assuming an Anomaly team.
scripts/validate-app.sh checks this identity before preparation and publication.
The move to Anomaly's team (5NZ4Q7NXJ4) is planned in
docs/plans/macos-signing.md; the manual macOS signed build workflow signs
and notarizes an unpublished candidate once the Apple secrets are in the
release environment.
scripts/release-app.sh prepare leaves Hex-<version>-macos-arm64.dmg and
Hex-<version>-macos-arm64.zip (root Hex.app, the updater's archive); both
contain the same signed, stapled app. Do not reuse a released version for
changed code. Macs on 2.3.1 or earlier (Sparkle, com.kitlangton.hex2) get
one informational appcast item, packaging/macos/legacy-appcast.xml, that
links to the new app; they never auto-update into it.
There is no Swift migration: never import Swift preferences or data, adopt its
bundle identifier, publish Rust artifacts to its S3 feed, or automatically quit,
delete, or replace the Swift app. Prefer a website-only informational item in
the legacy Sparkle feed, with no enclosure, pointing to the new app for manual
installation and fresh setup. Publish it only after the new artifact is live. See
docs/plans/swift-app-handoff.md. The Rust data root remains unchanged, but the
new app identity requires fresh macOS permission grants. Validate signed
distribution with the selected signing team before publishing an app update.
SMAppService is meaningful only from a signed app installed in
/Applications. When replacing a local bundle outside Finder during a login-item
smoke test, register the new bundle with Launch Services before launch:
'/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister' \
-f /Applications/Hex.appPass the actual installed Rust host path to lsregister if it was renamed.
Verify both registration and unregistration with sfltool dumpbtm, and restore
the user's original login-item state after the test. Do not add release test
hooks or persist a second launch-at-login flag.
Every platform is built, signed, and published by one run of
.github/workflows/release-desktop.yml ("Release"; see docs/releasing.md).
Run it with workflow_dispatch (ref, channel: staging publishes the
release to the dev service with a draft GitHub release, stable to
production), or push an app-v<VERSION> tag, which must match the crate
version. It builds Linux x86_64 in an Arch container (scripts/build-linux.sh)
and Windows x64 and ARM64 on native runners (scripts/build-windows.sh)
through the reusable .github/workflows/build-desktop.yml, and macOS through
build-macos.yml. .github/workflows/test-builds.yml calls the same desktop
workflow on every push to dev with test-build, plus an ad-hoc signed
Hex Test.app (HEX_TEST_BUILD=1 scripts/build-app.sh: bundle ID
anomaly.hex.test, its own hex-test data folder and ~/.config/hex-test;
it must never read the real hex data), as unsigned 14-day artifacts for
teammates (docs/test-builds.md). HEX_TEST_BUILD=1 builds never update
themselves. The collect job checks the files, the absence of Visual C++
runtime imports, and (stable) Authenticode signatures; publish uploads each
target and then the release with scripts/release-service.sh, the only job
holding id-token: write for the service, with no other secrets and actions
pinned by SHA. The GitHub release mirror, the AUR package (built from the
published Linux file), and the AUR/brew/nix distribution records follow.
The release environment is limited to dev (dev site and staging),
production, and app-v* tags; stable runs only from production or a tag. Rerunning
after an interruption is safe: the service stores files by hash and accepts
the same version again.
Hex-Setup-<version>-windows-<x64|arm64>.exe is a per-user Inno Setup
installer (packaging/windows/hex.iss, built by
scripts/build-windows-installer.ps1) that installs exactly the managed
layout install-windows.ps1 creates, so self-updates keep working:
%APPDATA%\voice-control\versions\<version>\hex.exe, current-version, the
install.json direct-install receipt, and a Start Menu shortcut carrying the
managed mark. Its uninstaller lives in voice-control\uninstall because
updates prune old version directories; it removes every version, the
receipt, the shortcut, and Launch at login, and keeps settings. It refuses to
roll back a newer self-updated version. Keep its AppId forever: the updater
rewrites that Apps & Features entry's version. Release and the Windows
installer workflow install, reinstall, refuse a downgrade with, and uninstall
it silently on x64 and ARM64 runners (scripts/test-windows-installer.ps1).
With sign-windows (Release), Build desktop's windows-package job, the only
one in the release environment, Authenticode-signs both hex.exe through
Azure Artifact Signing (GitHub OIDC, SHA-256, RFC 3161 timestamp) before
building the installers, then signs the installers
(scripts/windows-authenticode.ps1 checks them). Compile jobs and test builds
never hold signing credentials, and signing is skipped with a warning until
the Azure values exist in release. See docs/windows-signing.md.
Run scripts/build-linux.sh on x86_64 Linux and scripts/build-windows.sh
from Git Bash once per architecture with HEX_WINDOWS_ARCH=x86_64 (the
default; Vulkan SDK required) or aarch64 (clang-cl and Ninja; best on an
ARM64 host). Builds use -DGGML_NATIVE=OFF so the CPU kernels stay portable,
and the static runtime in .cargo/config.toml keeps the Visual C++
Redistributable unnecessary; the release workflow asserts neither Windows
executable imports it. scripts/install-linux-release.sh and
scripts/install-windows.ps1 (served at https://anoma.ly/hex/) install from
the service's stable manifest and check each file's size and SHA-256;
scripts/test-install-linux-release.sh exercises the Linux one against a
fixture service.
Use termctrl to verify TUI changes at both wide and narrow dimensions. The
hex dev dashboard keys are 1 for commands, 2 for the activity log, 3 for meetings,
Tab to cycle, and q/Escape to quit.
~/Library/Application Support/hex/logs/live.ndjson: state, transcript, command decision, outcome, processing, and context.~/Library/Application Support/hex/logs/process.log: Rust, CoreAudio, Moonshine, and context-adapter diagnostics.~/Library/Application Support/hex/meetings/: manifests, separate tracks, the rolling in-call transcript, and atomically published final transcripts.
Older macOS builds used voice-control; the first launch of a newer build
renames it to hex and leaves a voice-control -> hex symlink. If the rename
fails, Hex keeps using voice-control and logs why. Windows and Linux keep
voice-control.
When diagnosing a missed command, inspect both logs and distinguish microphone capture, transcription, command mode/dictation, context matching, command resolution, and action execution before changing aliases or thresholds.
See ROADMAP.md. Do not add hypothetical seams for roadmap items. Introduce a
seam once there are two real adapters or a current test requires substitution.
ROADMAP.md is the authoritative work list. Keep these constraints visible:
- Release freeze (Kit, 2026-10-22): publish no Hex release on any OS, stable or
staging-to-stable, and don't move
production(it stays atapp-v2.3.1), until Kit lifts it after the Rune front end has been tested thoroughly on real Macs and Windows PCs. Test builds are fine. - Validate public onboarding from a clean macOS account and signed Linux updates on the supported Arch/i3 host.
- Validate native Wayland on a real compatible compositor, including physical
portal shortcuts, the
hexcommands from a desktop keybinding, focus/click-through, target paste, and tray-less shutdown. - Add a second real browser adapter before generalizing browser context.
- Do not turn concrete macOS, Linux X11, or command modules into hypothetical platform or plugin frameworks.