Skip to content

feat(headless): add a Codex runtime alongside Claude#74

Merged
rogeriochaves merged 9 commits into
mainfrom
headless-codex-runtime
May 28, 2026
Merged

feat(headless): add a Codex runtime alongside Claude#74
rogeriochaves merged 9 commits into
mainfrom
headless-codex-runtime

Conversation

@rogeriochaves
Copy link
Copy Markdown
Contributor

What

Lets a headless agent be driven by the Codex CLI instead of Claude Code, by declaring runtime: codex in the agents config. Built for a Codex PR-reviewer agent on the dev agents box.

Stacked on #73 (self-compact-straightaway, the branch currently deployed on the box); once #73 merges this diff is just the Codex commit.

How

  • runtime.ts — a descriptor table (bin, arg building, canResume, selfCompact, config dir) mirroring the macOS CodingAssistant entity, scoped to what the headless path needs. Single branch point instead of Claude assumptions scattered around.
  • config — optional runtime field (defaults to claude), validated.
  • identity / launch / reconcile — thread runtime through; Codex launches codex --no-alt-screen --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust [-m model], always fresh (Codex mints its own session id and a per-PR reviewer needs no cross-reboot resume); the card is tagged assistant: codex.
  • hook correlation — the launcher exports KANBAN_SESSION_ID (the stable uuidv5 of the slug) into the tmux session; the shared hook.sh prefers it, so the daemon/bridge correlate Codex events to the agent's card regardless of the id Codex mints.
  • hooksinstallCodexHooks writes ~/.codex/hooks.json pointing at the same hook.sh; installHooks installs both. Self-compaction is naturally skipped for Codex (no statusline context json), which is correct since Codex auto-compacts.
  • send and Slack-inbound steering are unchanged — they paste into the tmux session by slug, which is runtime-neutral.

Tests

204 pass (7 new): runtime descriptor arg building, config parsing/validation of runtime, installCodexHooks idempotency, and a real-tmux test that a codex agent launches fresh (no resume) and tags its card. Plus a headless-runtime spec scenario.

Follow-up (not in this PR)

Live transcript mirroring of Codex output to Slack would need a Codex rollout (~/.codex/sessions/*.jsonl) parser; for now Slack inbound steering + the daemon's receipt announce work, and the reviewer posts its findings to the PR and Slack itself.

The auto-compact guard queued the 500k/600k/700k self-compact nudges and
relied on auto-send when the agent next emits a Stop. A freshly-resumed
or idle session never emits a Stop, so those gentle nudges never fired
and context coasted up to the 750k hard /compact — which then collided
with whatever prompt landed next (e.g. the morning nudge), losing it.

Paste the nudge straight into the session instead. Claude queues pasted
input and runs it after the current turn, so a busy agent still finishes
gracefully, while a resumed/idle session self-compacts immediately and
is well below the limit by the time its next prompt arrives.
The headless engine was Claude-only. Add a runtime abstraction so an agent can
declare runtime: codex and be driven by the Codex CLI:

- runtime.ts: descriptor table (bin, arg building, canResume, selfCompact)
  mirroring the macOS CodingAssistant entity, scoped to the headless path
- config: optional runtime field (defaults to claude), validated
- identity/launch/reconcile: thread runtime through; build the codex command
  (--no-alt-screen + full-auto bypass flags), always launch fresh (Codex mints
  its own session id and the reviewer is per-PR), tag the card assistant=codex
- launcher exports KANBAN_SESSION_ID so the shared hook.sh correlates events to
  the agent's card regardless of the id the runtime mints internally
- hooks: install Codex hooks (~/.codex/hooks.json) pointing at the same hook.sh;
  self-compact is naturally skipped for codex (no statusline context json)
- tests + headless-runtime spec scenario
Codex's hooks.json nests events under a "hooks" key (mirroring the config.toml
[hooks] table); without the wrapper Codex silently ignores the file, so no
codex hook events fired. Caught dogfooding on the box (empty Slack channel).
…ript

Codex agents now stream to Slack like Claude agents: the bridge discovers the
agent's Codex rollout by workspace cwd (findCodexRollout) and posts its
agent_message + exec_command events (formatCodexRolloutLines). Inbound steering
already worked via tmux paste.

Also stop auto-installing Codex hooks: codex 0.134.0 gates command hooks behind
an interactive trust prompt that --dangerously-bypass-hook-trust does not
suppress in the inline TUI, which hung the headless session on a modal. The
rollout-transcript mirror replaces the hook-based announce for Codex.
…huge)

Codex embeds full base_instructions in the session_meta first line (~20KB), so
the bounded read truncated it and JSON.parse failed, so findCodexRollout never
matched and no agent movement was mirrored. Extract cwd with a regex from a
larger bounded read instead.
…age'

Codex has no UserPromptSubmit hook (trust gate), so injected prompts were
not announced to the channel the way Claude's are. Mirror user_message
events from the rollout tail using the same received-message format, and
guard against echoing a prompt that was relayed from a Slack human.
…arts/compaction

Codex writes a new rollout file per session, so a relaunched agent (or its
own auto-compaction) rotated the file out from under the bridge tail. The
poll loop now switches to the newest rollout for the agent's cwd and mirrors
it from the start, so the channel keeps showing the agent's conversation
without restarting the bridge.
The 500k/600k/700k soft nudges stay queued and auto-send on the next Stop,
as designed: a soft nudge is meant to land at a stop point, and an idle
session simply rides up to the 750k hard /compact, after which the next
prompt arrives post-compaction. Reverts the daemon paste-straight-away
change so the queuePrompt path is preserved.
@rogeriochaves rogeriochaves merged commit 06be666 into main May 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant