Skip to content

feat(cli): ADR-005 Phase 2 — codex adapter - #231

Closed
lilyshen0722 wants to merge 3 commits into
mainfrom
feat/adr-005-codex-adapter
Closed

feat(cli): ADR-005 Phase 2 — codex adapter#231
lilyshen0722 wants to merge 3 commits into
mainfrom
feat/adr-005-codex-adapter

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

Summary

Adds the local-CLI-wrapper adapter for codex, enabling commonly agent attach codex + commonly agent run to put a locally-installed codex CLI in a Commonly pod as a first-class agent. First Phase 2 adapter (cursor + gemini follow as separate small PRs).

This is the same shape as Phase 1b's claude adapter — adapters are pure subprocess wrappers per ADR-005 §Adapter pattern, registered in cli/src/lib/adapters/index.js, talking to the kernel only via the run loop.

Argv shape — diverges from the ADR-005 table

ADR-005 §Adapters-shipped-in-v1 was written against an older codex-acp variant. The current codex-cli (tested: 0.125.0) uses:

  • New turn: codex exec --json --skip-git-repo-check -o <file> "<prompt>"
  • Resume: codex exec resume --json --skip-git-repo-check -o <file> <sid> "<prompt>"

Session continuity is via the exec resume <id> subcommand, NOT a --session <id> flag. Documented in the source comment so a future codex bump has one file to update.

Output handling

  • stdout: JSONL events. Adapter stream-parses (handling chunk boundaries) and watches for:
    • thread.started → captures thread_id for next-turn resume
    • turn.failed → rejects with error.message so the run loop surfaces it instead of generic exit-code error
  • stderr: Rust tracing logs (timestamped). Captured for error reporting only.
  • Final reply text: read from the file written by --output-last-message <FILE> after the process closes — cleaner than parsing every JSONL event variant.

Mirrors claude.js convention

  • Memory preamble (=== Context (your persistent memory) === ... === Current turn ===) when ctx.memoryLongTerm is set
  • Test seam via ctx._spawnImpl
  • 5-minute default SIGTERM timeout (ADR-005 §Spawning semantics)
  • Per-spawn temp dir for the output file, cleaned up in finally
  • detect() returns { path, version } or null

Tests

cli/__tests__/adapters.codex.test.mjs — 14 tests:

  • detect(): success, which-fallback, ENOENT, non-zero exit
  • spawn(): first-turn argv, resume-turn argv (subcommand + prompt-position), memory preamble (with + without), turn.failed event surfacing, non-zero exit + stderr trim, timeout + SIGTERM, JSONL chunk-boundary buffering, temp-dir cleanup on rejection
  • adapter registry: listAdapterNames() includes codex
PASS __tests__/adapters.codex.test.mjs (14/14)
PASS __tests__/adapters.claude.test.mjs
PASS __tests__/run-loop.test.mjs
... (11 suites, 124/124 tests pass)

Live smoke — deferred

This PR ships the adapter + tests. Live smoke (attach + run + @mention round-trip against api-dev.commonly.me) is deferred because the local codex auth has an expired refresh token. After codex logout && codex login, the smoke is one-liner:

commonly agent attach codex --pod <dev-pod> --name laptop-codex \
  --instance https://api-dev.commonly.me
commonly agent run laptop-codex
# in pod: @laptop-codex hi

What's next (separate PRs)

  • Stage 2 — gateway-container deploy: add @openai/codex + @commonly/cli to _external/clawdbot/Dockerfile, wire commonly agent run codex alongside the openclaw process, install the codex agent into dev DM rooms. Lets dev agents (theo/nova/pixel/ops) replace acpx_run calls with @codex mentions.
  • Phase 2 cursor adapter (no session, plaintext stdout)
  • Phase 2 gemini adapter (no session, plaintext stdout)

Test plan

  • cli/__tests__/adapters.codex.test.mjs — 14/14 pass
  • Full CLI suite — 124/124 pass, 11/11 suites
  • npm run lint on changed files — no errors
  • Live smoke after codex login re-auth — attach + @mention round-trip on api-dev

🤖 Generated with Claude Code

lilyshen0722 and others added 2 commits April 24, 2026 16:17
Adds the local-CLI-wrapper adapter for `codex`, enabling a developer
(or a Commonly agent) to attach a locally-installed codex CLI as a
participant in a pod via `commonly agent attach codex` + `commonly
agent run`.

The argv shape diverges from the ADR-005 §Adapters-shipped-in-v1 table
(written against an older codex-acp variant). codex-cli 0.125.0 uses:

  - new turn:  codex exec       --json --skip-git-repo-check -o <f> "<prompt>"
  - resume:    codex exec resume --json --skip-git-repo-check -o <f> <sid> "<prompt>"

Session continuity = `codex exec resume <thread_id>`, NOT a `--session`
flag. The adapter captures `thread_id` from the JSONL `thread.started`
event on stdout and reads the agent's final reply from the file written
by `--output-last-message` (cleaner than parsing every event variant).

Mirrors claude.js shape:
  - memory preamble (=== Context ===) when ctx.memoryLongTerm is set
  - test seam via ctx._spawnImpl
  - 5-min default SIGTERM timeout
  - registered in cli/src/lib/adapters/index.js next to claude/stub

Live smoke deferred — local codex auth needs re-login (refresh token
reused) and Stage 2 (gateway-container deploy) is a separate PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…k reading from a pipe

Surfaced live during ADR-005 Phase 2 end-to-end smoke against api-dev.
The codex adapter was using `child_process.spawn`'s default stdio, which
inherits a fresh pipe for stdin. Codex 0.125.0's `exec` then prints
"Reading additional input from stdin..." and blocks waiting for input
that never arrives — the spawn hangs until the adapter's 5-min SIGTERM
timeout fires.

Interactive smoke worked because codex detects a TTY on stdin and uses
the argv prompt directly. Only non-TTY parents (like the `commonly agent
run` loop) trigger the hang.

Fix: pass `stdio: ['ignore', 'pipe', 'pipe']` so codex sees /dev/null →
immediate EOF → falls back to the argv prompt as intended.

Verified end-to-end on dev: `@laptop-codex please reply with the single
word: pong` round-trips through codex 0.125.0 + the run loop and posts
`pong` back to the pod.

Adds a regression test that pins `calls[0].opts.stdio` so a future
"cleanup" PR can't silently revert this. claude.js may have the same
latent issue but currently works in practice — out of scope for this
PR; file follow-up if a TTY-less smoke fails.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Live smoke against api-dev.commonly.me exposed one issue and validated end-to-end.

Issue caught + fixed: the original adapter used child_process.spawn's default stdio, which gives the child a fresh pipe for stdin. Codex 0.125.0 then prints Reading additional input from stdin... and blocks waiting for input that never arrives — the spawn hangs until the 5-min SIGTERM timeout. Interactive runs are fine because codex detects a TTY and uses the argv prompt directly. Only non-TTY parents (the run loop) trigger it.

Fix in 358bb4de91: stdio: ['ignore', 'pipe', 'pipe'] so codex sees /dev/null for stdin, hits immediate EOF, falls back to the argv prompt. Pinned with a regression test.

End-to-end verification:

[19:25:29] laptop-codex: Hi all — I'm laptop-codex. Local codex CLI wrapped as a Commonly agent (ADR-005). Ping me when you need it.
[19:25:42] xcjsam:       @laptop-codex please reply with the single word: pong
[19:33:34] laptop-codex: pong

Flow: commonly agent attach codex --pod <id> --name laptop-codexcommonly agent run laptop-codex → @-mention from another user → wrapper polled chat.mention event → spawned codex with patched stdio → codex returned pong via --output-last-message → wrapper posted to pod.

Tests: 15/15 (added one regression). Full CLI suite still 11/11 green.

claude.js has the same latent default but works in practice — flagged as out-of-scope follow-up in the commit message.

Two important fixes + one consistency nit from the code-reviewer pass.

1. Resume argv ordering. Previously `exec resume --json --skip-git-repo-check
   -o <file> <sid> <prompt>` — clap parses this correctly today, but a
   future codex parser change could silently consume <sid> as the value of
   -o, producing an empty turn every time. Reorder to `exec resume <sid>
   --json --skip-git-repo-check -o <file> <prompt>` so the positional sits
   immediately after the subcommand, matching codex's documented signature
   and parser-independent of clap version. Test pins the exact position
   (first three argv tokens) plus an explicit guard that <sid> appears
   before -o.

2. Strengthen the temp-dir cleanup test. Previous version read
   /proc/self/status and asserted it was a string — always true, no real
   coverage. Now counts `commonly-codex-*` entries in tmpdir() before and
   after a failing spawn and asserts the count doesn't grow, which is
   what the test title claimed all along.

3. Module JSDoc referenced `--output-last-message` while the argv uses
   the `-o` short alias. Update the comment to match what readers will
   see in the source.

All 15 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Self-review pass via code-reviewer subagent — addressed two important findings + one nit in 97e1e1a3ad.

Finding Severity Resolution
Resume argv put <sid> after option flags — future codex parser change could silently consume it as -o's value Important Reordered to exec resume <sid> --json --skip-git-repo-check -o <file> <prompt>. Matches codex's documented signature, parser-independent. Test pins exact position.
Temp-dir cleanup test asserted on /proc/self/status content — always true, no actual coverage Important Replaced with a real before/after count of commonly-codex-* entries in tmpdir()
Module JSDoc referenced --output-last-message while argv uses -o short alias Nit Comment updated for consistency

Reviewer findings deliberately deferred:

  • Q: Resume path not live-smoked — true. The argv reordering above makes the resume positional unambiguous regardless of parser implementation, which neutralizes the original concern. Live resume smoke can ride along with Stage 2 (multiple agents in DM rooms naturally exercises resume on every turn beyond the first).
  • Q: turn.failed + session continuity — open question. Currently a model-rejected turn drops the thread_id and the next delivery starts fresh. Reviewer's right that resuming might be more appropriate, but this is a behavioral decision worth its own discussion, not an in-PR change. Filing as follow-up.
  • Nits about else if on event types, which called even on detect failure, spawn: jest.fn() unused in detect tests — all match claude.js conventions intentionally. Skipped.

What the reviewer flagged as solid (worth surfacing for posterity): adapter purity holds (ADR-005 invariant #1), JSONL parser correct on chunk-boundary / partial-line / non-JSON / turn.failed cases, temp-dir lifecycle clean across all error paths, no sensitive data in source or tests, convention drift vs claude.js minimal and justified.

Verdict: Approve with suggestions → suggestions addressed. Tests still 15/15.

lilyshen0722 added a commit that referenced this pull request Apr 25, 2026
Local-CLI-wrapper adapter for `codex`. `commonly agent attach codex` +
`commonly agent run` puts a locally-installed codex CLI in a Commonly
pod as a first-class agent, mirroring the Phase 1b `claude` adapter
shape (memory preamble, `_spawnImpl` test seam, 5-min SIGTERM timeout).

Argv shape diverges from the ADR-005 §Adapters-shipped-in-v1 table
(written against an older codex-acp variant). codex-cli 0.125.0 uses
`codex exec resume <id>` as a subcommand, NOT a `--session <id>` flag.
Session id is captured from the `thread.started` JSONL event; the agent's
final reply is read from the file written by `-o <FILE>` (the
`--output-last-message` short alias) — cleaner than parsing every
event-type variant the model can emit.

Includes a `stdio: ['ignore', 'pipe', 'pipe']` fix surfaced during the
live smoke against api-dev: codex's `exec` blocks on
"Reading additional input from stdin..." when spawned from a non-TTY
parent (the run loop). Setting stdin to `'ignore'` gives codex /dev/null
→ immediate EOF → it falls back to the argv prompt. Pinned with a
regression test.

End-to-end smoke validated on dev: `@laptop-codex pong` round-trips
through codex 0.125.0 + the run loop and posts back to the pod.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Squashed and merged manually as commit b94bf74 to preserve Lily as author (per repo convention — see memory/feedback-pr-merge-pattern.md).

@samxu01
samxu01 deleted the feat/adr-005-codex-adapter branch April 25, 2026 03:26
samxu01 added a commit that referenced this pull request Apr 25, 2026
…#236)

Adds a `codex-tools-installer` init container to the clawdbot-gateway
deployment that installs `@openai/codex` (pinned via
`agents.clawdbot.codexTools.codexVersion`) and the `@commonly/cli`
(installed from this repo's `cli/` subdirectory at the pinned git ref)
into a shared `/tools` volume the main container mounts read-only with
`/tools/bin` prepended to PATH.

This is the bridge ADR-005 Stage 2 needs so dev agents (theo / nova /
pixel / ops) can eventually mention `@codex` instead of calling
`acpx_run`. The wrapper itself shipped in PR #231; this PR puts the
substrate where it can run.

Why an init container, not a Dockerfile change: `_external/clawdbot`
is a submodule on the openclaw fork; touching its Dockerfile would
require a fork PR + a submodule pointer bump. The init container path
lives entirely in the commonly chart and ships with this deploy.

Why @commonly/cli installs from source: it's not on npm yet (ADR-005
Phase 4 publication hasn't shipped). The cli/ subdirectory is a
self-contained ~200KB package with one runtime dep. The init container
apt-installs git, clones this repo at the pinned ref, copies cli/ into
/tools/lib/commonly-cli, runs npm install --omit=dev, and symlinks the
bin. Pin the ref to a SHA or tag in values.yaml when stability matters.

Soft-fail: if the npm registry or github is unreachable at pod-start
time, the wrapper falls through with a warning rather than failing the
init container. Gateway routing keeps working (acpx_run continues as
fallback); operator can re-trigger on the next pod restart. Hard-failing
the gateway pod for a transient outage in one optional capability would
strand all agent traffic, which is the wrong trade-off.

Auth.json reuse: the existing `clawdbot-auth-seed` init container
already provisions chatgpt account-1's codex `auth.json` to
`/state/.codex/auth.json`, and the gateway container's
`lifecycle.postStart` copies it to `~/.codex/auth.json`. The wrapper
reuses that — no new ESO secret. Trade-off: shared quota with the
existing acpx_run path. A dedicated codex account for the wrapper is a
follow-up if it becomes a bottleneck.

Run loop is operator-driven for now (`commonly agent attach codex` +
`commonly agent run codex` inside the pod). Auto-start in the container
lifecycle is a follow-up after the manual flow validates end-to-end.

Runbook at `docs/runbooks/codex-in-gateway-pod.md` covers the operator
bootstrap end-to-end and the dev-agent HEARTBEAT cutover plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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