Brainstorm: Add support for codex CLI
Phase 0 scratchpad for issue-449. Nothing here is a commitment; it converges on the ticket and then becomes
requirements.md.
Problem / opportunity
the-loop today can host a work-item session on exactly one harness: Claude Code. cursor exists only as a critic (one-shot reviews). Issue-440 built the whole per-work-item harness choice gate — the harness-* rows at phase-selection, the frozen choice in work-item-state.json, the adapter registry — precisely so that new harnesses could land "by adapter alone" (docs/specs/issue-440/design.md:112), and its requirements explicitly name codex as the first follow-up (docs/specs/issue-440/requirements.md:158). This work item cashes that cheque: an operator who declares codex in cli-config.yaml should be able to tick harness-codex on a ticket and have the loop run on OpenAI's codex CLI end-to-end, proven against the test repository https://github.com/MadaraUchiha-314/the-loop-testing.
Why it matters: multi-harness is the-loop's credibility claim (it is a PDLC harness, not a Claude wrapper), codex becomes a second hosting harness and a critic for free, and the exercise flushes out every place "harness" still secretly means "claude".
Context & constraints
What already exists (the designed extension point):
cli/the_loop/harness/—HarnessAdapterbase (base.py:79) with the registryADAPTER_TYPES = {"claude": …, "cursor": …}(harness/__init__.py:24). An adapter that overridesinteractive_argvis derived to be session-hosting (hosts_sessions,base.py:234); one that only implements_oneshot_argvis critic-only.- Sessions are always tmux (decision-021):
TmuxRunner.spawn_inbuildstmux new-session … -- <binary> <interactive_argv(prompt, session_id)>(runner.py:420-432); events are delivered into the live TUI byload-buffer/paste-buffer+ CR (runner.py:974) — harness-agnostic, assumes a TUI that accepts bracketed paste. - Respawn uses
interactive_resume_argv(prompt, session_id)(claude:--resume <id>). - Model/effort vocabulary already anticipates codex:
EFFORT_LEVELSis documented as the union of Claude Code and Codex levels (modelchoice.py:73-82;docs/decisions/decision-124.md:47), andusage_from_outputalready parses OpenAI-styleprompt_tokens/completion_tokens(base.py:248). cli/tests/test_dispatcher_harness.py:30-48already stubs a_Codexadapter — a ready-made test scaffold.critics[].harnessandharnesses[].nameare free-form; only three schema enums pinclaude|cursor:routing.defaultHarness,standingSessions.sessions[].harness,selfDiagnosis.harness(× two identical schema copies).
What the codex CLI offers (verified against current docs):
- Interactive TUI
codex, non-interactivecodex exec "<prompt>"with--json(JSONL events incl.thread.startedwith the session/thread id),--output-last-message,--output-schema. - Resume: interactive
codex resume <SESSION_ID>/codex resume --last; non-interactivecodex exec resume <SESSION_ID>. - No pre-assignable session id at launch — Claude's
--session-idhas no codex equivalent; it is an open upstream feature request (openai/codex#13242). The id exists only after launch (rollout files under$CODEX_HOME/sessions/, or the--jsonevent stream in exec mode). - Instructions:
AGENTS.md(global~/.codex/AGENTS.md, repo root, cwd) — no plugin / skill / marketplace system. Custom prompts:~/.codex/prompts/*.mdbecome/prompt-nameslash commands. - Unattended knobs:
--full-auto,--sandbox workspace-write|danger-full-access,--ask-for-approval never,-cconfig overrides; effort viamodel_reasoning_effortconfig. - Lifecycle hooks exist (pre/post turn, pre/post tool) — the possible carrier of the loop's stop-gate; protocol differs from both Claude (exit 2 + stderr) and Cursor (
followup_messageJSON), whichhooks/the-loop-gate.pyalready parameterises by harness name (emit(),:137-150).
Constraints / non-negotiables:
- tmux-only hosting stands (decision-021) — no bespoke runner for codex.
- CLI-as-programmatic-surface stands (decision-016) — drive the
codexbinary, no SDK. - The adapter contract is the boundary: the dispatcher, selection gate, model probing and telemetry must not grow codex special-cases; whatever cannot fit the contract is a contract change, argued in
design.md. - A codex session must still run the loop: read the skill, honour the gates, use the
the-loopCLI verbs. Claude gets that via the plugin (skills/commands/hooks pre-seeded byharness_plugins.py); codex has no plugin system, so an equivalent instruction surface must be prepared per-spawn.
Ideas & options
A — Scope: critic-only codex (like cursor today)
Just _oneshot_argv (codex exec --json <prompt>), registry entry, schema enums.
- Pros: tiny; immediate value (cross-vendor critic reviews); zero session-identity risk.
- Cons: does not meet the ticket — "test by creating an issue in the-loop-testing" means hosting a work-item session, and issue-440's gate only offers hosting harnesses. Kept only as the fallback landing if hosting proves blocked upstream — and as the natural first milestone of option B, not a rival direction.
B — Scope: full hosting adapter (the intent)
CodexAdapter with _oneshot_argv and interactive_argv/interactive_resume_argv plus the environment preparation that makes a codex session loop-capable. Milestoned: critic first (it is the adapter's smallest proof), then hosting. This is the working direction. The sub-problems below are all about B.
B1 — Session identity without --session-id
The contract passes the-loop's chosen session_id into interactive_argv; codex cannot accept one. Options:
- B1a — discover-after-spawn: launch plain
codex "<prompt>", then have the runner (or a small adapter hook, e.g.discover_session_id(cwd, since)) find the newest rollout file under$CODEX_HOME/sessions/for that cwd and record codex's id asharness_session_idin the registry (field already exists). Resume becomescodex resume <discovered-id>. Pros: honest ids, resume-by-id works. Cons: a new adapter-contract hook + a race-prone "newest file" heuristic; polling after spawn. - B1b — per-session
CODEX_HOME: spawn withCODEX_HOME=<state-dir>/<session>so the session store contains exactly one conversation; resume is then unambiguous (codex resume --last). Pros: no id discovery at all, no race, isolation between work items for free;tmux new-session -ealready injects env (runner.py,_env_flags). Cons: hides the operator's global~/.codexconfig unless the prepared home links/copiesconfig.toml+AGENTS.md+ auth; auth material in a copied home needs care. - B1c — wait upstream for openai/codex#13242. Rejected as the plan of record (unbounded wait), but the adapter should be written so that a future
--session-idcollapses the workaround.
Leaning: B1b, with prepare_environment building the per-session home from the operator's real one (auth by reference where possible); B1a as fallback if the home-indirection breaks codex auth/update flows.
B2 — Instruction surface: how a codex session learns the loop
- B2a — render into
AGENTS.md:prepare_environmentwrites/refreshes a cwd-levelAGENTS.md(or appends a managed block) pointing at the repo'sCLAUDE.md-equivalent rules and the installed skill path; spawn prompt stays prose ("run the loop on <ref>") instead of a/the-loop:work-onslash command —routing.spawnPromptTemplatealready allows a per-operator override, but shipping a codex-flavoured default template is cleaner. - B2b — custom prompts dir: also render the 16
commands/*.mdinto~/.codex/prompts/the-loop-*.mdso/the-loop-work-onexists in codex. Nice-to-have; not load-bearing if B2a's prose prompt names the CLI verbs. - The skill text itself is harness-neutral already (markdown +
the-loopCLI verbs);${CLAUDE_PLUGIN_ROOT}references are the only wrinkle.
Leaning: B2a mandatory, B2b optional/follow-up.
B3 — The stop-gate
hooks/the-loop-gate.py keeps a codex session from ending a turn with gate debt. Claude wires it via plugin hooks.json (Stop), Cursor via .cursor/hooks.json. Codex has lifecycle hooks configured in config.toml — the per-session home (B1b) is exactly where a managed config.toml can declare a post-turn hook calling the-loop-gate.py codex, plus a third output-protocol branch in emit(). Open question below on what codex's hook protocol can actually do (can it inject a follow-up message, or only observe?). If codex hooks cannot steer the session, the gate degrades to the daemon-side checks (the-loop check) — acceptable for a first landing, stated honestly in the capability doc.
B4 — Unattended, model and effort mapping
_UNATTENDED_ARGS: likely--full-auto(or--sandbox workspace-write --ask-for-approval never); exact choice is a design-phase measurement against a real codex install, including how it composes with the operator'sharnesses[].args.model_flag = "--model";_EFFORT_ARGSmaps the sharedEFFORT_LEVELSonto-c model_reasoning_effort=<level>, returning()for levels codex cannot express (decision-124 already fixes this semantics: an inexpressible level is not offered).
B5 — Known claude-only edges (surveyed; decide fix-now vs declared-gap)
| Spot | Today | For codex |
|---|---|---|
3 schema enums × 2 copies (routing.defaultHarness, standingSessions, selfDiagnosis) | claude|cursor | widen — trivial, in scope |
core/sessions.py:57 HARNESS_BINARIES + --harness choices | claude/cursor | add codex — in scope |
core/sessions.py:323 transcript derivation (UI "Harness trace") | hard-refuses non-claude | likely declared gap first landing; codex rollout JSONL could back it later |
graph/hooks/mcp.py:41-65 argv fork bypassing adapters | claude/cursor inline | refactor onto oneshot_argv or add branch — decide in design |
install.py (BINARIES, plan_claude) | claude-only | plan_codex (binary check; no plugin surface) — probably in scope, small |
harness_plugins.py / trust.py pre-spawn stores | Claude-specific | codex equivalent folds into B1b's prepared home |
Sketches & notes
Spawn-path shape under B1b (names illustrative):
- Token telemetry:
codex exec --jsonusage events should flow throughusage_from_outputuntouched (OpenAI key aliases already handled) — verify, don't assume. - Proof per the ticket: operator declares
harnesses: [{name: claude…}, {name: codex…}], files an issue inthe-loop-testing, ticksharness-codex, and the loop delivers a trivial change end-to-end on codex. That e2e run is the verification centrepiece.
Open questions
- (human) Scope confirmation: full hosting harness (option B) and not just a codex critic (option A)? The ticket's test instruction implies B; confirming before requirements. (raised on the ticket)
- (human) Is a real codex install + OpenAI/ChatGPT credential available on the operator machine that runs the e2e proof against
the-loop-testing? Without one, verification caps at stubbed adapter tests. (raised on the ticket) - (design-phase measurement) Exact codex flags of record —
--full-autovs sandbox/approval pair,--model,-c model_reasoning_effort, hook configuration keys and their protocol (can a hook inject a follow-up message?). Measured against the installed CLI, per decision-124's "measure, don't declare". - (design-phase) Does per-session
CODEX_HOME(B1b) break codex auth (does login state live inCODEX_HOMEand can it be shared by symlink/copy safely)? Decides B1b vs B1a. - (design-phase) Does the codex TUI accept tmux
paste-buffer+ CR delivery cleanly (bracketed paste)? If not, event delivery needs an adapter-side knob.
Leaning / working hypothesis
Build option B via B1b + B2a + B4, milestones in order: (1) CodexAdapter critic-capable (codex exec --json) + registry + schema enums + HARNESS_BINARIES — codex becomes a critic and the-loop models check-probeable; (2) hosting: interactive_argv/interactive_resume_argv on a per-session CODEX_HOME prepared by prepare_environment (config, AGENTS.md, gate hook, auth by reference), codex-flavoured spawn-prompt default; (3) e2e proof on the-loop-testing per the ticket. Transcript-tab support and ~/.codex/prompts slash commands are declared follow-ups, not silent gaps. A future upstream --session-id (openai/codex#13242) should delete the B1b indirection without touching callers.
Hand-off → requirements
Carries forward when converged: the B scope with its three milestones; EARS criteria around — adapter registration & availability refusal, phase-selection offering harness-codex only when hosting+declared, frozen-choice spawn/respawn on codex, critic one-shot parity, effort/model mapping semantics (inexpressible ⇒ not offered), per-session home preparation, gate-hook behaviour (or its honest degradation), schema enum widening in both copies, and the e2e proof on the-loop-testing. Security considerations seeded here: auth material handling in prepared homes, argument bleed between harnesses (issue-440's review), --full-auto/sandbox blast radius, AGENTS.md as an injection surface in untrusted repos. Stays behind: B1a/B1c mechanics, option A as an endpoint.
Review comments
Appended by the-loop's
record-feedbackhook when a human gate approves with comments (issue-109). Append-only and attributed.