Control the running app from a terminal

Agent or terminal                                  // control.cli; every OS
├── One tree and protocol over the local API's POST /control on every OS;
│   OS can't do it -> unsupported at runtime, exit 1
├── hex status [--json] -> Listener, capture, meeting, model, features,
│   permissions, window, update, version, pid, memory
│   └── Hex not running -> exit 3, "Hex is not running; run `hex start`"
├── hex start -> Already running: nothing | else open Hex.app (Linux and
│   Windows: a detached `hex app --hidden`), wait ≤ 20 s
│   └── The caller's HEX_* variables -> open --env (values never logged)
├── hex quit | restart [--force]
│   ├── Meeting or dictation in progress, no --force -> refused, exit 1
│   ├── Listener didn't answer, no --force -> busy (it might be recording)
│   └── restart -> reopens with the running Hex's HEX_* variables (macOS);
│       Linux unit -> systemctl restart; else waits for the drain, then start
├── hex dictation start|stop|toggle|cancel
│   ├── Same shortcut state machine as the hotkey; start locks the capture
│   ├── start -> intentional at once: tone once the mic is open, recording
│   │   behavior, live stream
│   ├── stop -> done only when the clip went to transcription;
│   │   no audio yet or silent -> discarded, nothing transcribing
│   ├── cancel -> discards the recording in any mode, nothing pasted or saved;
│   │   jobs already transcribing finish; idle -> unchanged
│   ├── Repeat a command -> unchanged, exit 0
│   └── dictation stop --print -> stops at once, then waits ≤ 60 s and prints
│       the transcript (processed as a paste) instead of pasting it
│       ├── No speech -> no-speech, exit 4, prints nothing
│       ├── Not recording -> not-recording, exit 1
│       ├── Failure or timeout -> exit 1, prints nothing, never pastes
│       └── History keeps it as Printed; Paste Last skips it (also on Linux)
│   └── dictation start|toggle --raw -> the dictation it starts pastes the
│       transcript as heard (Dictate without processing); History keeps it as Raw;
│       --raw with stop -> usage error, exit 2
├── hex paste-last -> pastes the last dictation
│   ├── Setup gates the listener -> not-ready
│   └── Recording or shortcut gesture in progress -> busy (menu bar: nothing)
├── hex meetings record [--title] | stop | copy-last
│   ├── Meetings off -> feature-off; recording shows in the menu bar and HUD
│   │   exactly like a menu-bar recording
│   └── No meetings cancel: Hex cannot discard a recording
├── hex settings list | get <key|prefix> | set <key> <value>
│   ├── The window's save, then ReloadSettings -> live, no restart
│   ├── Unknown key, bad value, read-only key -> refused, nothing written
│   ├── Shortcut conflict -> refused, as the window's recorder would
│   └── apply <file|-> -> a JSON object of key: value, all or none
│       ├── Undeclared settings keep their values; dependent keys in any order
│       ├── transcription.model / .language -> selected, never downloaded
│       ├── Hex running -> SettingsApply: saved and adopted live, like set
│       └── Hex stopped -> written to settings.json under the service lock
│           (Linux); a Hex holding it is waited for (≤ 35 s) instead
├── hex model list | use <id> | install [id] | cancel
│   ├── use -> installed models only; never downloads or repairs; keeps the
│   │   model's remembered language and hints (--language overrides)
│   └── Linux/Windows: install with Hex stopped downloads here; status,
│       devices, check, transcribe run here (macOS: unsupported)
├── hex window show [pane] | hide
│   ├── settings -> the General pane (status window.pane stays "settings")
│   ├── meetings pane with Meetings off -> the pane's opt-in card
│   └── story <story> [--variant V] [--play] -> a playground story, as a
│       hex://story link opens (labs.md); playground off -> feature-off
├── hex history clear --yes -> without --yes refused before Hex is asked
├── hex check-for-updates -> The updater checks now
├── hex launch-at-login [on|off] -> SMAppService (changes need Hex.app);
│   Windows Run key; Linux XDG autostart entry
└── hex logs [--process] [-n N] [-f] -> local tail; works with Hex stopped

Every command except start, restart, logs, settings apply, and the read commands talks only to the running Hex; none of them launch Hex. Every OS reaches it through the local API's owner-only discovery file and the bearer-authenticated POST /control route. Commands dispatch before logging and the instance lock on every OS, so they never compete with the running app for its files. Opt-ins stay explicit settings: no command turns Meetings on as a side effect.

On Linux and Windows control::host::answer carries every command out through the OS's adapter (linux::app::LinuxControl, windows::rune::WindowsControl): dictation and paste-last go to the listener's own queue (Windows: the same hotkey loop, as a toggle press would), settings save and reload as the window's do, and settings set refuses a key whose capability the OS lacks (settings list marks it "not on this OS"). meetings record|stop|copy-last go to the meeting recorder (meeting/runtime) as the tray's items do; window story answers unsupported; Windows sends every annotate command to the running Hex. hex history, hex meetings list|show|search, and hex mcp read files on every OS.

Sources: control.rs (protocol, status, quit rules), control/host.rs (the Linux and Windows host), control/settings.rs (settings registry), control/cli.rs and control/cli/human.rs (clap, output, exit codes, start, logs), macos_rune/control.rs (the macOS host), the /control route in local_api.rs, and early dispatch in main.rs. Command and JSON contracts document flags, exit codes, and schema.

Checks: control::tests round-trip every command and reply (including a_printing_stop_answers_through_its_reply_channel), refuse other protocol versions, and cover the quit rules (an unanswered listener refuses) and model_selection (remembered hints, wildcard-language validation); control::settings::tests include every_persisted_setting_has_a_cli_key, value parsing, shortcut conflicts, and zero volume; control::cli::tests pin exit codes, output, and hex start's open arguments; desktop::onboarding::tests::relaunch_forwards_only_the_hex_environment pins restart's; macos_rune::control::tests check that model use prepares its exact selection installed-only; dictation::audio::tests::marked_captures_are_intentional_without_a_hold covers the immediate stream and ReadyIntentional; recognition::tests cover the stop replies and the paste-last refusal; control::tests::cancel_parses_and_takes_no_flags covers cancel and its flags; local API's control_requests_are_authenticated_versioned_and_typed covers authentication, bounds, and error codes; tests::control_commands_parse_beside_the_read_commands covers parsing next to history and meetings; control::host::tests cover the quit refusal, printing stops, and runtime unsupported replies; linux::tests parse the shared tree beside Linux's own commands; rune_ui::platform::parity_tests fail when a capability differs across OSes without a recorded reason; settings_name_real_capabilities_and_keys checks the settings-to-capability table.

Observed (Oct 30, apollo, an isolated data root and hex service without a display): status, settings get/set (and refusing show_dock_icon), vocabulary add/list/remove, model list/use/status, history clear, window hide, check-for-updates, commands check, quit, start, and restart all answered as documented. Windows compiled with cargo xwin clippy (x64 only; the ARM64 cross-build fails in rquickjs-sys's build script, unrelated); it has not run on a Windows PC.

Gaps: no command has run against a live app; the unit checks substitute the host. The control loop's start/stop wiring (tone timing, recording behavior) has no end-to-end test. A CLI volume change does not preview a tone as the window does. Onboarding permission prompts, calendar connection, and meeting deletion or renaming have no command. Linux and Windows dictation through the CLI has not run with a microphone, and meeting recording through the CLI has not run on Linux or Windows.