Classic gateway client (initial migration)
Normal hermes --cli, hermes chat, hermes chat -q '…' -Q, and top-level
hermes -z '…' use the gateway authority. Direct cli.main chat launches also
hand off before constructing a local agent. Local bootstrap uses the canonical
ensure lifecycle and a single-use private control ticket, not service installation.
An explicit HERMES_TUI_GATEWAY_URL remains remote-only: failed connection or
authentication never launches a local replacement.
hermes profile create uses the same dial (hermes_cli/profile_provision_gateway.py): a ready
owner for the terminal's profile (discovery only, never started for this) or the URL's gateway
runs profiles.create in the connection's profile and returns the receipt the CLI prints
(report). With no owner, or a local one that predates those fields (-32601 / 4000), the CLI
creates in-process; a URL's gateway is never replaced by a local create. A refused create keeps its
type (error.data.type); a socket lost after the request was sent, or a reply without a complete
receipt, is reported as unknown, never retried. The ~/.local/bin wrapper is written by the CLI,
never by the gateway, and only when the profile the gateway made is the directory -p <name> selects
on this machine (a loopback URL alone is not enough: a tunnel or container can serve another home).
The client prints the persisted Session: ID to stderr. Resume with
hermes --cli chat --resume ID. Input is admitted with a fresh input_id; events
and final replies come from the authority. /quit, /exit, /detach, EOF and
closing the terminal detach only. /stop sends an execution-generation-fenced
interrupt. Pending approvals display /approve ID once|deny|…; clarification
uses /answer ID TEXT. The client uses the displayed prompt ID and its captured
generation, not a locally reconstructed waiter. /discard ID acknowledges an
admission lost across an owner restart (prompt.resolve_unknown). /branch [title],
/model <model> [--provider name] and /compress [here [N] | <focus>] [--preview]
are revision-fenced session.mutate operations (hermes_cli/gateway_mutations.py);
/yolo [on|off] toggles this session's approval bypass on the owner (config.set
key yolo). /title [name] (session.mutate rename), /undo [N] and /retry
(session.mutate rewind; /undo puts the removed message back in the composer),
/new [title] / /reset (session.create with this session's frozen launch
request), /usage (the owner's report plus the last committed turn) and /tools
(launch toolsets) run in the view (hermes_cli/gateway_chat_commands.py). Every
other command goes to the owner as slash.exec: the reviewed reads run there and the
rest print /x is not available on the shared gateway yet with a link to the
command parity table, which lists what every local
client does with every command.
One-shot stdout ends with the final reply. Without -Q, a resumed session first
prints its stored history (user: / assistant: lines), and the reply is followed by
the Resume this session with: block (see Launch options); -Q prints the reply
alone. Exit status is 0 for a completed
admission, 1 for failed execution/connection, 2 for unsupported frontend options,
3 for a pending control that requires interactive reattachment or a turn lost in a
gateway crash (clear it with hermes sessions discard <id> --yes), and 130 for a
keyboard detach. A pending control does not imply cancellation. -z no longer
implicitly bypasses approval policy.
Launch options
The gateway advertises its accepted creation parameters through runtime.describe
(session_create.parameters, from gateway.session_policy.CREATE_FIELDS). The client
sends a creation option only when it is advertised; a requested option an older
gateway does not advertise is refused (Gateway does not support creation options: …). Caller cwd is sent when advertised; an old gateway lacking cwd support prints a
warning that it uses its configured execution directory, while explicit --in
rejects. A resume may repeat the exact creation flags the session was frozen with
(scripts re-run one command line); any flag that would change the frozen route
rejects.
Accepted and frozen into the session at creation (_POLICY in
hermes_cli/gateway_chat.py): --model, --provider, --base-url, --api-key
(memory-only, never persisted), --reasoning, --toolsets, --max-turns,
--skills (rendered once into the frozen prompt), --checkpoints, --yolo,
--accept-hooks (also HERMES_ACCEPT_HOOKS=1), --pass-session-id,
--ignore-rules, --ignore-user-config and --safe-mode (both need an explicit
--model; safe mode runs the turn in an isolated worker that never reads the
profile). Also accepted: --source <label>, --resume <id-or-title>, -c <title>,
-c <title> --create-if-missing, --in <dir>, --query-file, --format stream-json, -z ... --usage-file, bare -c (this terminal's breadcrumb session,
else the most recent CLI session), --resume latest (most recent CLI session, this
workspace first; both resolved by the owner, session.resume latest='cli') and
--list-tools / --list-toolsets (print the catalog and exit; no session). chat -q
without -Q ends with main's Resume this session with: hermes --resume <id> block.
Refused with exit 2 (_UNSUPPORTED plus the selection checks in
validate_options); the error names where each capability lives now:
| Option | Why | Use instead |
|---|---|---|
-w / --worktree | Worktree creation is a launcher-side step the authority does not expose | hermes --tui -w |
--image | No client-side attachment staging on this path | attach in hermes --tui or Desktop |
--run-budget | No per-launch run budget in the creation contract | agent.run_budget_seconds in config.yaml |
-v / --verbose | Display-only; the client has no verbose renderer | hermes logs --follow (the refusal also names hermes chat --tui -v, whose canonical Ink path does not yet carry the flag) |
--no-restore-cwd | The gateway keeps the session's frozen cwd | --in <dir> |
--create-if-missing without -c <name> | Nothing to create by name | -c <name> --create-if-missing |
--compact is not a hermes chat option (argparse rejects it); a direct cli.main
hand-off refuses it and names display.compact.
Current parity limits — not full classic CLI parity
Full legacy presentation, slash registry parity, interactive history editing, auto-reconnect, lost-ACK durable client journals, remote login bootstrap, cold-owner resume and native Windows/macOS QA remain outstanding. The client preserves an explicit remote URL's existing authentication mechanism; it does not acquire or mint remote credentials.
tests/hermes_cli/test_gateway_chat_native.py exercises native tmux/PTY classic
fresh input, detach, persisted-ID resume and one-shot through the ordinary daemon
with a loopback model, isolated HOME/HERMES_HOME and caller cwd. Constructor
instrumentation records no client AIAgent, SessionDB or GatewayRunner. The test
also denies an invalid approval choice, then reconnects and consents before a real
terminal effect on an owned fixture. Stale-control/active-Stop native coverage and
native clarify interaction remain separate acceptance work.
For two-checkout compatibility testing, load the narrowly scoped pytest option plugin and specify the disposable gateway peer's checkout (the normal runner's hermetic environment deliberately removes arbitrary environment overrides):
scripts/run_tests.sh -j 1 --file-retries 0 tests/hermes_cli/test_gateway_chat_native.py \
-p tests.hermes_cli.gateway_client_options --client-test-runtime-root=/path/to/runtime-checkout
The receipt explicitly reports caller_cwd_effect_verified. On a cwd-capable
runtime the native client submits model/toolsets/cwd and the real terminal writes
its actual execution directory into an owned temporary receipt. On an older
runtime, explicit --in must reject without that effect. Do not count the latter
as cwd parity. Both client and daemon use isolated test homes; this option does
not install a service or alter the selected checkout.