remote-client¶
The CLI's read/control commands are thin BACKEND CLIENTS, so one install monitors any machine's sessions.
raw source¶
A session's live state lives where its tmux and worktrees are — on the backend's machine. So the spex CLI
must not read or drive sessions in its own process: a manager on one machine should monitor and drive an
agent on another, and there must be exactly one actor on a given tmux socket. The CLI's read and
control verbs are therefore thin clients of the running backend, the same way the dashboard is — the
backend is the single broker, and which machine you point at is just a URL.
expanded spec¶
The read/control commands — ls, watch, wait, capture, send, review, merge, reopen, exit,
close, prompt — call the backend over HTTP (SPEXCODE_API_URL, else the local default). They hold no
in-process tmux/git path, so the backend is the single actor on the tmux socket and the single source of
derived state, and pointing SPEXCODE_API_URL at another machine's backend monitors and drives THAT
machine's sessions with no code change — the dashboard's viewer-points-anywhere model, extended to the CLI.
watch/wait take the board source as a required argument (the backend client), so a poll can never
silently read a local board by default.
Every command speaks the same selector grammar. A caller names a session by full id, id-prefix, node, or
branch — and not just the list verbs: the control verbs accept it too. The backend matches /…/:id
EXACTLY, so resolveClientSession resolves a selector against the live board (the session-selectors
matcher over clientListSessions) and the verb then calls with the resolved FULL id. A non-match is loud and
precise — none → no such session, ambiguous → the candidate ids — never a silent miss against the backend.
The split is load-bearing and is the whole point. State producers stay local: done/ask/park/
idle and the lifecycle hooks write the agent's OWN per-session record in the GLOBAL store directly (keyed
by session_id — see state), so an agent must be able to declare its own state even with no backend up. The
backend learns that state by ENUMERATING the store, not by a write of its own. Launch
(spex new) keeps its own already-justified path (it needs the backend's auth env — see launch). Only
the verbs that observe or drive live tmux route here.
One availability rule, FAIL LOUD. Unlike a best-effort telemetry POST, an unreachable backend throws a
clear no backend reachable at <url> and a non-zero exit — never a silent fall back to a local in-process
path, because that fallback is exactly what would re-create two actors on one tmux socket. watch warns once
and keeps streaming (a backend blip must not read as "all sessions fine"); wait fails loud rather than
reporting a false timeout.
Failure stays distinct from emptiness. A monitoring read must let a manager tell "I couldn't read" from
"the screen is blank": capture returns a genuinely empty pane as success, but maps unknown-session,
offline (no live pane), and a capture error to distinct non-zero outcomes — a blank screen that exits 0 is
never confused with a read that failed.