codex-headless¶
Codex's non-interactive app-server thread form is an independent harness-adapter with id
codex-headless, not a mode of the interactive Codex adapter. It keeps Codex's materialized project surface,
trust, and hook path exactly, while replacing only the visible runtime half.
expanded spec¶
The adapter is literal object composition over codexHarness: its .codex shim and linked-worktree anchor,
AGENTS.md contract, skills, trust writer, hook events, slash commands, and bypass-hook-trust thread config are
unchanged. There is no second materializer and no headless branch in materialize or session product code.
The launch command reuses Codex's existing app-server bootstrap and spex internal codex-launch path. A fresh
launch starts the stable per-project app-server, calls thread/start for the governed worktree, sends the launch
prompt as the first turn, waits for the rollout to persist, and stores the returned thread id. It then exits;
there is deliberately no codex --remote ... resume TUI attach and no resident controller in the pane. The
shared app-server remains alive for every thread in the project. Its adapter marks this launch as one-shot so
the generic fast-exit recovery loop does not replay a successful first prompt into a duplicate thread.
Delivery directly reuses Codex's existing app-server JSON-RPC transport. It reads the owned thread and sends
turn/steer while a turn is in progress or turn/start when the thread is idle, so an idle spex session send
starts the next turn without spawning a process or waking a pane. Socket, thread, and RPC failures fail loudly
through the public session API; there is no PTY typing or wake fallback.
When the one-shot first-turn process exits non-zero, the adapter reports that exit through the shared
harness-adapter turn-outcome seam before returning its failure. An active undeclared record becomes error
with the Codex exit code; a zero exit and any declaration already written are left untouched. The shared
app-server remains the liveness address only when it can accept another delivery.
The session record is the liveness address: while it exists and is not explicitly stopped, the adapter reports
online regardless of the empty pane or process probes. headless: true keeps it out of the dashboard launcher
picker by default and the note conversation is the console trunk. Human stop tears down the session runtime
and marks the retained record stopped, so it reads offline until a proven resume clears the marker. Resume is
the no-TUI form: resumeArg is empty because there is no TUI to reattach, but an absent shared server must be
recreated through the canonical delegated spawn before the record can return online. The adapter's launch
readiness is stricter than its steady-state record liveness: it proves one unchanged version-4 detached-launch
receipt through the shared process adapter (exact PID/start and process group everywhere, plus /proc session on
Linux, never Darwin ps sess) and composes it with the exact socket generation, the target's existing thread loaded under that
shared root, and exactly one governed record owner for that thread across every adapter record sharing the
descriptor. That owner must be the current SpexCode session; a deduplicated loaded-ID set is never ownership
evidence. The adapter returns those facts as one readiness fence. Resume durably records an internal pending
candidate only while the parent maintenance ticket is still live; every public projection continues to expose
the exact stopped/offline original. The same fence then repeats the complete generation/reference/owner proof
before one final write clears pending and publishes online state. An unload, shared-root restart,
duplicate/reassigned owner, refused helper, bounded timeout, or stale pending recovery retains/restores the
original record without a false lifecycle event and returns non-success. Closing remains the terminal
operation that removes the record, worktree, branch, pane, and shared runtime references owned by the session.