Skip to content

Client shell

Source: harness/docs/client/shell.md Status: current

What a client shell owes the product — TUI, CLI chrome, remote, or headless — without claiming ownership of PEW/Work.

Day-to-day operator verbs: operate.md.
Input-face contract: ../canon/proposals.md.
Grain (Client loop): ../canon/grain.md.
Approval mechanism: ../runtime/approval.md.
Live tape: transcript.md.


Duty Meaning
Evaluate named actions Send JSON-RPC on the session socket (turn, turn/rewind, compact, eval, kv/set, kv/clear, policy/set, connect/adopt, connect/drop, connect/always-allow, connect/exclude-tool, config/auto-allow, turn/cancel, turn/retry). The daemon writes PEW/Expectation / Job bodies. The Client does not mint them.
Interactive approval Interactive shells write durable approved / approvalComment on the pending ApprovableJob (approvalQueueHead), only on explicit cover confirmation. The cover shadows the composer draft. The eval client never writes approval fields.
Project work + awareness Show authoritative Jobs and their outcomes, PEW/Expectations and their results, and in-motion awareness. A pure Job may be running without an open PEW/Expectation. The transcript is a projection of that graph.
Own paint and input TTY, paint loop, layout, decode — the shell’s process boundary. Quickdraw mailbox is paint transport only.
Sandbox command half Fast local pickers and projections (/connect, /model, slash). Commands are flat Arrival Scheme programs exec’d client-side; session effects are the same JSON-RPC actions.

Shell may Shell must not
Evaluate named actions as a document peer Claim-own PEW/Work (PEW claim apply / settle as product path)
Project the cover from approvalQueueHead (needsHuman) Mint nested “approval” PEW/Expectations or dual pending lists; have the eval client write approved
Implement the interactive input face (ask writes approved on that leaf; turn/cancel; composer confirm) Settle cancel by writing PEW outcome from the shell
Hold mirrors for paint and speculative echo Run a session phase machine as product truth
Dial the session socket (catalog / faces / commands are daemon projections) Orchestrate Actors or treat /kernel as a product dial (/kernel is a lens)

Interactive permission is a direct durable write. Cancel is turn/cancel (daemon terminates in-flight PEW). Confirm is composer chrome. Durable PEW/WorkResult lives on the PEW/Expectation the Actor seals; Job outcomes live on their owning Jobs. Map: ../canon/proposals.md.


harness -e FORM, harness ./program.scm, and harness -p TEXT --agent NAME admit one ordinary SessionTurn containing a ProgrammaticJob. The client submits source and an auto / yolo policy, observes that turn by its stable identities, prints the program result, and can request daemon-owned turn/cancel. --mode always-approve normalizes to yolo before RPC. manual and unknown tokens fail admission (policy_unavailable) rather than falling back. -e, -p, and a .scm file are mutually exclusive. -p requires --agent and is (agent/send AGENT TEXT) on the same admission path.

Authorization stays in the daemon. The eval client never writes approved / approvalComment, answers approvalQueueHead, or runs policy.yaml. Questions / forms that require a person become interaction_required through the ordinary operation failure path. A classifier Indeterminate retries; it is not interaction_required. Floor deny seals policy_denied.

Unattended behavior is persisted on the admitted turn (evalSpec). TTY presence and later TUI attachment do not change the admitted policy. Combining --resume / --session / --continue with an eval input is a usage error. An attached TUI may paint the cover on a faulted auto residual so a present human can beat retries; the eval client still does not write the bit.


runningTurn = session.findRunningTurn()
openWork = live Jobs and PEW/Expectations in the displayed execution scope
parked = approvalQueueHead (needsHuman) | null

SHADOW, DON’T REPLACE. Ask chrome is a second face over the composer, not a mode of the base draft. Tea.draft stays the prompt. The cover holds a process-local comment buffer. Confirm chords write approved / approvalComment on approvalQueueHead and dismiss. The cover does not assign those fields as the operator types. The cover is not a #callback overlay. The pending change itself paints on the ask pin (Diff when one exists; identity only when the rim cannot name the work), between the transcript viewport and the composer — not as a tool sexp in the composer placeholder. Cover dismiss does not take a mutate Diff pin with it: the suggestion stays until the sealed Diff is a tape row. MCP options and shell commands ride the composer stack (same T-join as slash). Spawn does not cover — AgentCallJob / AgentMessageJob are COMM FIRST. The omnibox rim names edit / create / observe / MCP / run command (coverAskAnnotation). No Diff and a named rim → no pin. MCP and shell never mount a pin.

VISIBLE ≠ ARMED. An ask that mounts while the operator is typing must not treat the next printable, Enter, Space, or a letter (y / n / a) as consent. Cover chrome may paint over a nonempty draft; rendering it is not consent and is not a mode of the draft. Confirm chords write approved / approvalComment only while the cover is armed. Arming: empty draft at mount, or the first-ask throttle elapsing over a nonempty draft. Focus does not arm or unarm. Time-since-last-key does not arm and is not “done composing.” Never single-letter grants. While the cover is visible and not armed, the draft / slash / queue still own printables; confirm chords are reserved and inert.

FIRST-ASK THROTTLE. When the cover arms for a first ask (no pending leaf → a pending leaf) and the base draft is nonempty, confirm chords stay inert so the operator sees the face change. Empty draft: no throttle. Subsequent heads while the cover stays up do not re-arm. Cover down then up is a new first request. TUI: KernelRoot.approvalAskThrottleMs (config.json, default 2000, 0 = off). ../packages/harness/src/tui/tea.ts.

#awaitCallback save / zero / restore is the Question / harness/confirm path. Reusing it for UAC zeros the prompt, paints the ask as the composer body, and mixes cover keystrokes into the restore.

Face Empty-draft law
No running turn, open work, or pending input Idle. Empty ⏎ on a closed interactive head that needsContinue (failed/cancelled/crash between rounds) fires turn { prompt: "", kind: "continue" }. Successful or no head: empty ⏎ still no-op. Persist-mod ⏎ does not continue.
approvalQueueHead (approved === null) Cover up. Confirm chords write only while armed. Empty draft at mount arms immediately; nonempty draft waits approvalAskThrottleMs. Empty armed cover: ⏎ → approved = true; Esc → false on that leaf. Extra-root read with a suggested parent: persist-mod ⏎ Allow that folder. Auto remainder ask of a grantable cluster: persist-mod ⏎ this session in auto; persist-mod ⇧ ⏎ Always in auto. MCP look-up (read floor, not auto-cluster persist): persist-mod ⏎ Allow this session; persist-mod ⇧ ⏎ Always allow. MCP cousin (fails the floor): persist-mod Esc Deny this session; persist-mod ⇧ Esc Deny forever. Persist modifier is config.json superKey (default ctrl; cmd is ⌘). Super is not inferred from the OS. Nonempty armed cover: persist-mod ⏎ / persist-mod Esc write approvalComment. Persist and folder-widen do not write from a comment. Shift+Enter is a newline. Machine allow-side does not raise the cover — it is a chip on the tool row.
Running turn or open work, no ask, no Question, no slash Nonempty draft: ⏎ queues (turn kind: "queue"). Empty draft + nonempty promptQueue: ⏎ steers first. Empty + empty queue: Enter no-op. Empty Esc arms, then cancels (turn/cancel { turnId }). Nonempty Esc is still draft-clear (same arm bit). Live completion retryUntilMs in the future: persist-mod ⏎ retries now (turn/retry); ask persist still wins. The admission deck paints in the composer extender unless slash autocomplete or a confirm stack owns it. Slash catalog, /compact, /rename, and other command surfaces keep their own Enter/Esc.
Printables, no cover Always the base draft

Cover is approvalQueueHead (needsHuman). A faulted auto remainder may raise it even on an eval turn so a present human can beat retries. The eval client does not write the bit. Operator keys: operate.md.


Piece Seat Owns
Protocol + models @inhuman.tools/harness-sdk Plexus images, rooms, JSON-RPC/UI schemas, generic Client. Browser: ./peer (no node:, no unix). Paint stays in each shell.
Daemon — kernel + session planes @inhuman.tools/harness-server Public: ensure/stop and the process log the TUI joins. Internals (reflector, actors, squeeze, drivers) stay in-package. Unix daemon.sock only.
Client shell — TTY, paint, input @inhuman.tools/harness src/tui + src/eval TUI: structure + boxes, command sandbox. Eval client: submission, print-once watch, result, and cancellation request; no approval management.
Browser shell @inhuman.tools/harness-web-app Loopback origin + unix upgrade proxy. Vite SPA dials the same host.

Implementer depth (flat programs, interpreter-owned waits): ../runtime/control-effects.md.


Doc Role
operate.md Operator how-to
transcript.md Live tape
../canon/grain.md Client loop, causal spine
../canon/proposals.md Cancel / Permission / confirm crossings
../runtime/control-effects.md Flat execution law the command sandbox runs under