Skip to content

dispatch

Deliver a prompt to a live agent over its rendezvous socket — socket-only, fail-loud — plus the merge intent.

raw source

Dispatching a prompt to a session — a message, a continue, the merge instruction, a node directive — is control, separate from the tmux pane, which is display only. Control must be scoped (only sessions this product launched) and fail-loud (a dead dispatch is seen, never silently degraded to typing into the pane). And merge is an intent the human expresses, not a server-run git script. A dispatched prompt states only the task; the git flow's mechanics are carried by product mechanism, not restated in every prompt, so the prompt and the flow never duplicate.

expanded spec

Prompt control goes through a per-session rendezvous socket only, never PTY keystrokes. The socket path is derived from the session id (set up at launch), so only our own sockets are addressed — control never reaches a Claude Code session outside the product. Writing one {"type":"reply","text":…} line injects the text and submits it deterministically, so multi-line prompts and Enters can't be corrupted the way tmux send-keys could.

sendKeys is socket-only with no send-keys fallback and confirms the agent actually accepted the prompt, not mere write-success. The daemon acks no accepted reply, so acceptance is an in-order round-trip: it writes the reply line then a repaint line; the daemon handles lines strictly in order, so a repaint-done with no preceding reply-rejected proves the reply was taken (repaint is auth-exempt, a reliable probe even if a future daemon gates reply). A missing/socketless session, a connect error, a reply-rejected/shutting-down, or a timeout all return a loud DispatchResult {ok,error} that propagates: POST …/keys answers 502, spex session send prints it, mergeSession returns it.

Merge is a dispatch, not a script. mergeSession carries no git merge logic: it reopens the session (clears the proposal → active, --resumes via reopen if tmux died — which waits for the rendezvous socket, closing the just-relaunched-no-socket race), then dispatches a merge prompt through this same sendKeys. The prompt tells the agent to merge its branch into the base branch from the main checkout (-C <main>, not its node worktree), resolve any conflicts (it knows the work's intent), verify the base HEAD advanced with no merge left in progress, git merge --abort if anything went half-merged, and propose close once verified — so the guarantee lives in the agent's verification, never a server check, and the base is never left half-merged. Async: POST /api/sessions/:id/merge returns {dispatched:true} once the prompt is confirmed accepted (409 if unreachable). The server no longer bumps merges on a click.

Prompts state the task; the git flow is mechanism, not duplicated prose. Every prompt the server builds — the merge prompt above and the directive new-node / delete-node prompts (each a placeholder the server set up, handed to the agent to name+spec+code or refactor-away by git history) — states only the task plus its own safety steps. It deliberately does not re-state the git flow's mechanics, because each is enforced by a product mechanism, not injected prose: the node/<id> branch by launch's newSession, the Session: trailer by the prepare-commit-msg hook, commit-before-declare by the core system config node materialized into the agent's contract (see launch), and the --no-ff / merge node/<id>: <reason> style by the merge prompt at merge time (the one place no other mechanism carries it). No standing ritual config node is needed — the flow is the product default, not a per-project opinion. The one task-level detail the directive prompts keep is "propose merge, don't merge".

The separate raw nav-key channel (rawKey) keeps its own tmux send-keys path — the per-keystroke channel for driving the agent's TUI menus, carrying named keys, printable chars, and // modifier combos (as C-/M-/S- tokens) so nav mode drives the terminal, not a prompt fallback.