Skip to content

feat: Codex first-class harness adapter - #124

Open
kennymcavoy wants to merge 2 commits into
AltanS:mainfrom
kennymcavoy:feat/codex-first-class
Open

feat: Codex first-class harness adapter#124
kennymcavoy wants to merge 2 commits into
AltanS:mainfrom
kennymcavoy:feat/codex-first-class

Conversation

@kennymcavoy

Copy link
Copy Markdown

Summary

Codex CLI panes become first-class: the boxless chrome stripped and re-surfaced natively, and the three probed dialogs lifted into tappable buttons. Independent of the Grok adapter PR — cut straight from main, no shared new model fields.

  • Chrome (Tier 1)web/src/lib/harness/codex/: Codex paints a bare prompt row that word-wraps onto indented continuation rows, a dot-separated status row beneath, and exactly one blank row between sections. The composer is located by the status row at the tail (the · Context N% left|used token behind two opaque ·-separated fields — model names and paths are never matched) with the prompt row above; a transcript echo of a submitted message paints the same prefix and never has the status row beneath it. Wrapped drafts rejoin with single spaces (verified against the typed original). Known safe limit, documented: a custom or disabled tui.status_line never matches, so those configs fall back to the raw mirror with replies refused.
  • Folder-trust prompt (family trust) — the exact two-option layout lifts; digits confirm directly (2 quit on the spot, 1 accepted through the guarded send path — both probed, TRUST_NOTES.md).
  • Exec approval (family permission) — classified: the first row must be exactly Yes, proceed, the last exactly No, and tell Codex what to do differently (post-shortcut-strip), and every row between must prove itself persistent (don't ask again …) — those never become buttons; any unclassified or suffix-altered row refuses the whole card. Digits probed with negative controls: 1 ran the approved command, 3 rejected it with the command verified never run on disk (Codex still paints a • Ran <cmd> transcript row for the cancelled call — documented rendering quirk). Only the exec approval is captured; patch/MCP approvals have different headers and fail closed until probed. Config note: with approvals_reviewer = "auto_review" eligible requests route through a reviewer subagent and no dialog paints — captures used -c approvals_reviewer=user.
  • request_user_input (family select) — Question X/Y cards lift one digit per row; a digit answers the current question, advances a multi-question set, and submits on the last (all probed, including the auto-added None of the above row). The notes-box state refuses to raw (tab or esc to clear notes footer or the › Add notes row): a digit there would type into the box. Esc interrupts the whole conversation and is never emitted.

The #99 bar

  • Exact agent string only (agent: "codex"); variant spellings pinned raw in the registry test.
  • Dated corpus (10 codex--*.txt, Codex v0.149.0), three notes files with the probe tables, describeAdapterConformance running codex against the claude+omp cohorts and vice versa.
  • Live verification: run by me against a real pane per the arrangement in feat: add AGY (Antigravity) harness adapter, interactive prompts, and slash commands #99 — trust accepted, a reply typed-verified-submitted through the new draft extraction, an approval landing its command on disk, and an ask answer arriving in the tool result, every press through the guarded /keys path with its region signature.

Privacy

The corpus is sanitized with length-preserving substitutions (row padding stays byte-identical): username, hostname, the Darwin per-user temp-dir token, and the Codex session UUIDs from codex resume lines.

Suggested CHANGELOG entry

Codex CLI gets a first-class harness adapter — strips the boxless composer chrome, re-surfaces the status row, and lifts the folder-trust prompt, exec approvals and request_user_input question cards into native buttons.

Verification

cd web && bun run typecheck clean; full web suite + conformance green; bun test bridge/ green; live pass as above. Versions untouched at 0.32.0 per the fork-PR rule.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PFH7jQVDVhPGC6FqLetNfd

Codex panes get the Claude-level treatment: the boxless chrome stripped
and re-surfaced natively, plus native buttons for the three probed
dialogs.

Registered by exact agent string only (the AltanS#99 bar):
adapterFor stays Object.hasOwn over adapter.agent; variant spellings
stay raw. Every keystroke the adapter emits comes from a dated live
probe recorded in the notes files, against a byte-faithful fixture
corpus (identifying strings length-preservingly genericized — public
repo), with describeAdapterConformance running codex against the claude
and omp cohorts and vice versa.

Chrome (Tier 1): Codex paints a bare `› ` prompt row that word-wraps
onto indented continuation rows, a dot-separated status row beneath,
and exactly one blank row between every section. The composer is
located by the status row at the tail (the ` · Context N% left` token;
everything before it is opaque — models and paths change) with the
prompt row above it — a transcript echo of a submitted message paints
the same `› ` prefix and never has the status row beneath it. The
status row feeds the strip, the prompt row the draft (wrapped drafts
rejoin with single spaces, verified against the typed original) and
composerPrompt.

Trust prompt: the exact captured two-option layout lifts as family
trust. Digits confirm directly — 2 quit on the spot, 1 accepted through
the guarded send path (both probed).

Exec approval: classified, not layout-pinned — the first row must be
the one-shot Yes, the last the reject, and every row between must prove
itself persistent (`don't ask again …`); those never become buttons.
An unclassified row refuses the whole card. Digits confirm directly:
1 ran the approved command, 3 rejected it with the command verified
never run. The header, Reason and `$ command` rows stay in the raw
mirror above the buttons. Only the exec approval is captured; other
approval kinds fail closed until probed. Note the config dependency:
approvals_reviewer = "auto_review" (the host default here) approves
silently and the dialog never paints — probing used
`-c approvals_reviewer=user`.

request_user_input: the Question X/Y card lifts as family select, one
digit per row — a digit answers the current question, advances a
multi-question set, and submits on the last (all probed). The
notes-box state refuses to raw (footer `tab or esc to clear notes`, or
the `› Add notes` row): a digit there would type into the box. Esc
interrupts the whole conversation and is never emitted.

Live-verified end to end through the bridge on a real pane: trust
accepted, a reply typed-verified-submitted through the new draft
extraction, an approval Yes landing its command, and an ask answer
registering in the tool result — every press through the guarded /keys
endpoint with its region signature.

Versions untouched per the fork-PR rule; CHANGELOG line in the PR body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PFH7jQVDVhPGC6FqLetNfd
@AltanS

AltanS commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Reviewed — this is a strong contribution: fail-closed, tail-anchored detectors, exact-label matching, conformance cohorts extended in all three directions, and the version files correctly left alone. All gates pass here (root 669, web 2709, both typechecks clean).

Two changes requested before merge:

  1. MAX_DRAFT_ROWS = 8 (web/src/lib/harness/codex/chrome.ts:31) — a draft that wraps past 8 rows makes locateComposer return null, so the composer goes dark and the app reports "a dialog may be waiting", which is wrong. It fails safe, but the operator's text is stranded. Please raise the bound or anchor the search without the cap.
  2. approval.ts:79startLine swallows the whole option run, so the persistent 2. …don't ask again… row vanishes from both the buttons and the raw mirror; the phone shows digits 1 and 3 with no sign of 2. The mirror should keep showing the row even if it isn't offered as a button.

Two smaller notes, take or leave: ask.ts:83 uses family: "select" whose documented contract is digit-then-Enter, while Codex submits on the digit alone — a one-line note at the family doc in prompt-model.ts that option.keys wins would keep the enum honest. The tui.status_line caveat in markers.ts:33 deserves an operator-facing docs line eventually; fine to leave that to us.

On our side: the Tier-2 bar means we live-verify the dialog recipes against a real Codex pane before merge — that's on us, not you. Plan is to merge this PR first, then ask for a quick rebase of #123 (they overlap in six files, all trivially).

An 8-row cap made locateComposer return null on a wrapped phone draft, so
the composer went dark. Same 100 as the other adapters.

Approval startLine swallowed the whole option run, hiding the persistent
"don't ask again" row from both the buttons and the raw mirror. The block
now starts at the reject so that row stays visible.
@kennymcavoy

Copy link
Copy Markdown
Author

Review follow-up:

  1. MAX_DRAFT_ROWS. Raised from 8 → 100 (same bound as Grok/OMP/Claude). A wrap past 8 rows no longer makes locateComposer return null.

  2. Persist row. Approval startLine now starts at the reject, so 2. …don't ask again… stays in the raw mirror. Buttons are still only the one-shot Yes (1) and No (3).

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.

2 participants