# Codex Lead Runtime Adapter

## Scope

This adapter maps the neutral Okstra lead operations to the Codex artifact-first CLI path. Read it only when the rendered launch prompt selects `leadRuntime=codex`.

## Capability declaration

| Field | Value |
|---|---|
| `runtime` | `codex` |
| `leadRoleLabel` | `Codex lead` |
| `userPromptMode` | `native-question` |
| `workerDispatchBackend` | `mixed` |
| `initialPromptDeliveryMode` | `eager-include` |
| `sessionAccounting` | `artifact-only` |
| `resumeMode` | `artifact-checkpoint` |
| `teardownMode` | `process-cleanup` |
| `leadEventSource` | `lead-events-jsonl` |

## Wizard interaction relay

Read this contract when `okstra preflight` returns this file as `runtimeReadiness.relayContract`. The JSON is the complete interaction mapping for this host. `semanticFunctions` is an allowlist: the skill may declare a function only when both this list and the live harness expose it. Codex `request_user_input` is single-select only (2–3 mutually exclusive choices, 1–3 questions); it has no native multi-select.

```json
{
  "schemaVersion": 1,
  "runtime": "codex",
  "semanticFunctions": [
    "plain_text_input",
    "native_single_select",
    "native_question_group"
  ],
  "nativeLimits": {
    "minOptions": 2,
    "maxOptions": 3,
    "maxQuestions": 3
  },
  "interactions": {
    "native-single": {
      "function": "request_user_input",
      "input": {
        "callCount": 1,
        "questions": "one",
        "id": "prompt.step",
        "header": "Q1",
        "question": "label-with-progress",
        "options": "all-in-original-order-as-label-description"
      },
      "response": {
        "envelope": "answers",
        "key": "question-id",
        "selection": "answers-array",
        "submit": "matching-option-value"
      }
    },
    "native-group": {
      "function": "request_user_input",
      "input": {
        "callCount": 1,
        "questions": "all-in-original-order",
        "id": "questions[].step",
        "header": "Q<one-based-question-position>",
        "question": "label-with-progress",
        "options": "all-in-original-order-as-label-description"
      },
      "response": {
        "envelope": "answers",
        "key": "question-id",
        "selection": "answers-array",
        "submit": "compact-step-json-values"
      }
    },
    "numbered-single": {
      "function": "host-text",
      "input": {
        "questions": "one",
        "question": "label-with-progress",
        "options": "all-in-original-order-as-numbered-markdown-label-and-description"
      },
      "response": { "source": "next-message", "submit": "raw" }
    },
    "numbered-multi": {
      "function": "host-text",
      "input": {
        "questions": "one",
        "question": "label-with-progress",
        "options": "all-in-original-order-as-numbered-markdown-label-and-description"
      },
      "response": { "source": "next-message", "submit": "raw" }
    },
    "sequential-group": {
      "function": "host-text",
      "input": {
        "questions": "all-one-at-a-time-in-original-order",
        "question": "label-with-progress",
        "options": "all-in-original-order-as-numbered-markdown-label-and-description"
      },
      "response": {
        "source": "next-message-by-question-position",
        "submit": "compact-step-json-raw"
      }
    },
    "plain-text": {
      "function": "host-text",
      "input": { "questions": "one", "question": "label-with-progress" },
      "response": { "source": "next-message", "submit": "raw" }
    }
  },
  "recovery": {
    "native-question-refused": {
      "trigger": "a-declared-native-question-function-is-refused-or-errors-at-call-time",
      "retry": "none",
      "dropSemanticFunctions": [
        "native_single_select",
        "native_multi_select",
        "native_question_group"
      ],
      "continueWith": "text-mapping-for-the-rest-of-this-session",
      "wizardState": "unchanged-refetch-the-pending-prompt-with-no-submit",
      "notifyUser": "one-line-host-picker-unavailable-continuing-as-numbered-lists"
    }
  }
}
```
`recovery` names what to do when a function this relay declares does not work in the live client. `native-question-refused` fires the first time a declared native question function is refused or errors at call time, whatever the reason (the client cannot present the card, the session has no view, the call returns no answer). Do not call it again and do not hunt for another native function: drop every token in `dropSemanticFunctions` from the intersection you computed in Step 1, keep `plain_text_input`, and render every remaining screen through the text mapping (`numbered-single` / `numbered-multi` / `sequential-group`), which preserves every option. Numbered text is the recovery path, so the ban on printing a numbered list does not apply once this fires. The wizard state file is untouched by a refused call — the pending prompt is still the current step, and `okstra wizard step --state-file <path> --no-submit` returns it again. Tell the user in one line that the host picker is unavailable and the run continues as numbered lists, then continue the wizard. Never abandon the run or ask the user to retype an identifier because the picker is gone.

For `request_user_input`, send one to three questions. Each question carries `id` (`prompt.step` or `questions[].step`), a header of at most 12 characters (`Q1`…`Q3`), the rendered question text including the progress suffix, and every wizard option as `{label, description}` in original order — the tool has no option `value` field. **Send the wizard's free-input option (`__free_input__`) as the last row like any other choice**; it is the row that routes the answer into the wizard's own text step, and dropping it leaves the user with no way to answer anything the options do not cover. Do not invent an extra `Other` row of your own — the client already adds its own free-form row beside the options. Native plans are emitted only when the prompt fits `nativeLimits` (unique labels, two or three options, one to three questions) and no grouped question is multi-select. Other prompts use the text mapping so no option is dropped. Look up each answer by question `id`. The selected strings are in `answers[id].answers`. Match those strings to option labels, emit their `value` fields in original option order, and join with `,` when more than one is present. If the user typed into the client's own free-form row and the text matches no option label, do not submit that text to a `pick` step — it accepts option values only. Submit `__free_input__` when the step offers it, then submit the typed text unchanged as the answer to the text step the wizard asks next. Submit the text unchanged directly only when the step has no free-input option.

For a `host-text` mapping, render each numbered item as its option label followed by its description verbatim; preserve every item and its order. The next user message is the raw answer: do not translate a number such as `1`, a CSV reply such as `1, 3`, an option label, or an option value before `okstra wizard step`. For `sequential-group`, collect one raw reply per question in order and build one compact JSON object keyed by the corresponding `questions[].step`; the wizard owns all normalization.


### Confirm step

The `confirm` prompt's `label` is the selection summary (one line per resolved input, then the question). Send it verbatim as that question's text — every line, including `(none)` values — and offer its three options. Do not replace the summary with a table or a prose digest of your own; the user is confirming exactly what the wizard resolved, and a line you drop is a setting the user never saw.

### Runtime-generated selectable screens

`wizard/engine.py` adapts choice screens before returning `next` when the session declares `native_single_select`. Lists use same-screen question tabs only when the host supports the complete grouped selection. Otherwise, choices beyond `nativeLimits` and unsupported multi-selections use a complete numbered list. Codex has no native multi-select, so a single choice with more than three options uses `numbered-single`; multi-selection uses `numbered-multi`. Oversized or unsupported groups are presented one member at a time, with every option for that member visible. These paths are exercised by `tests/domain/wizard/test_picker_navigation.py` and `test_role_model_selection.py`.

Render the returned screen using its `interaction.kind`. For numbered interactions, show every option label and description in original order in one message, then accept a number, label, or value; multiple choices accept comma-separated input. Submit that reply unchanged through `okstra wizard step`. Do not add a next-page choice, truncate the list, or require the user to remember a model identifier.

The complete numbered list is the intended fallback when a choice cannot fit the native control or same-screen tabs, including when the user generally prefers a selector. Text steps remain text steps; selecting the direct-input option opens the wizard's custom-value step.

### Plan decisions and execution permissions

Classify the requested action, not the word "approval". The wizard's `approve_plan_confirm` is a workflow decision about adopting the selected plan; it is a `pick` prompt built by `_build_approve_plan_confirm` in `wizard/steps_plan.py`. Render its existing options through the available native selector, just like task type and plan selection. Explain the effect of each option in the question tool and map the selected label back to its original value (`yes`, `no`, or `yes_apply` when offered). Recording that decision in okstra artifacts does not turn the question into a host permission request. Do not ask the user to type an approval word or switch to prose merely because this step is called approval.

Host permission requests concern execution privileges, sandbox escalation, or access to a protected resource. Use the host's permission mechanism for those requests; a wizard answer does not grant those privileges. If a subsequent command requires such permission, handle it separately at that command.

### Client-aware question tool selection

Before intersecting `semanticFunctions` with live capabilities, prefer `request_user_input` when the current session permits that call. Use its live restrictions rather than assuming it is always Plan-only: Codex CLI can enable it in Default mode with `default_mode_request_user_input`. The JSON mapping above remains the terminal picker contract.

Use `request_user_input_async` only when the current client explicitly supports interactive asynchronous question cards, such as the Codex desktop app, and the synchronous tool is unavailable. Callable does not mean selectable: the Codex terminal (`codex-tui`, session source `cli`) can accept an asynchronous question but render it as a plain bulleted agent message. Do not count that terminal delivery as `native_single_select` or `native_question_group`. For a supported desktop client, replace the `function` of both native entries above with `request_user_input_async` and use the mapping below. Keep the declared `nativeLimits`; the runtime generates the selectable screens within those limits.

If the user requested a selectable interface in the terminal and `request_user_input` is unavailable, keep the current wizard step pending instead of printing its options. `okstra install` enables `features.default_mode_request_user_input` when a Codex home exists, through `ensureCodexQuestionPicker` in `src/lib/host-config.mts`. Restart or resume the Codex session after installation and reread this relay. Changing the feature does not replace the current session's tool catalog. Preserve the existing wizard state-file path and call `okstra wizard step --state-file <existing-path> --no-submit` after resuming; do not initialize a replacement wizard or submit a guessed answer. If installation reports a configuration shape it cannot safely edit, its recovery command is `codex features enable default_mode_request_user_input`. Apply a configuration change only when authorized by the user, otherwise present the recovery command. The feature's availability is reported by `codex features list`; older clients without it need a client update or a mode in which the synchronous tool is permitted.

For each asynchronous question send `{title, options}`. Put the question label, progress suffix, recommendation context, and option descriptions in `title`; put every original option label in the `options` string array in original order. Preserve the wizard's free-input option and do not add an extra Other option. Keep the correspondence between question position, wizard step, option label, and option value for this call. Send a group in one call.

The immediate `{accepted: true}` response acknowledges delivery; it is not a user answer. Keep the call pending until the user's asynchronous reply arrives. Read each reply's `answer` and map its question position to the stored step. Map the selected label to the original option value; apply the synchronous free-form routing rule above for unmatched text. Submit one option value for a single question or one compact step-to-value JSON object for a group.

Display the question only through the selected tool. Do not print it or its options again in commentary or final text. Do not call a second question tool, re-emit the same wizard step, or resubmit a question while its answer is pending. A preselected option is not an answer. Re-prompt only after an explicit answer fails wizard validation or the user requests a change. These are lead relay instructions; the runtime does not observe host UI emissions.

## Semantic operation mapping

| Operation | Mapping |
|---|---|
| `read_artifacts` | Read the manifest-provided paths through the current host's file interface. |
| `write_artifact` | Write only core-authorized `.okstra/` artifacts and preserve their schemas. |
| `prompt_user` | Use the client-appropriate, mode-available question tool selected above for clarifications that fit `nativeLimits`. Show the question once through that tool and wait for the actual answer. Emit the question as the last thing in that turn: assistant text emitted after the call renders below the question and separates it from the user's answer. Follow the host's separate restrictions for permission requests. For lists that cannot fit the native control or same-screen tabs, show the complete numbered list and accept the next message as the answer. |
| `dispatch_worker` | Verify each materialized invocation first. Dispatch `runner=native-session` with the current Codex host's primitive, the returned `promptPath`, and `hostModelValue`. Pass `runner=cli-wrapper` assignments to `okstra worker-dispatch --project-root <root> --run-manifest <path> --workers <ids>`; use `--dry-run` first when required. **Not in a cmux run:** when `terminalBackend` is `cmux-pane`, the cmux adapter overrides this row. |
| `await_workers` | Await native host workers through the host primitive and CLI workers through synchronous dispatch, then verify team-state terminal records and Result Paths for both. |
| `redispatch_worker` | Materialize and verify a fresh invocation, then start a fresh native worker or `okstra worker-dispatch` attempt according to the persisted runner. |
| `shutdown_workers` | Perform process cleanup when a wrapper remains live; otherwise this operation is a no-op recorded in state. |
| `record_lead_event` | Append progress and activity records to the manifest-provided `leadEventsPath`. Use `okstra lead-progress append --phase <phase-id>` for a checkpoint and `okstra agent-activity append --kind <kind>` for an activity record; both resolve the ledger path from the run manifest. Emit the matching `PROGRESS:` line — the command prints it as `progressLine` — and, when an activity record is required, the immediately following `ACTIVITY:` line from the same structured fields. |
| `collect_usage` | Collect artifact/rollout-backed usage through the existing Okstra token-usage path; never read Claude session JSONL as a substitute. |

## Codex execution permissions

### Permission before bundle preparation

Before the first `okstra render-bundle` invocation, check the current host's declared filesystem restrictions and approval policy. Bundle preparation writes run artifacts and can create a task or stage worktree with `git worktree add -b`, which writes to the source repository's Git metadata. `--render-only` does not make this command read-only, and writable access to the project directory or `~/.okstra` does not imply writable access to protected `.git` metadata.

When those writes are restricted, use the host's supported execution permission on the original `render-bundle` command (`sandbox_permissions: "require_escalated"` for `exec_command` when available and permitted), preserving every `outcome.renderArgv` token. Explain that the command prepares the selected run and may create its branch and worktree. Do not first run it under restrictions already known to block these writes. Apply the same check to subsequent phase or stage bundle preparations. A wizard confirmation does not grant host privileges; follow the host's approval decision. If escalation is prohibited, report the constraint and retain the invocation instead of changing filesystem permissions or choosing an alternate launcher.

If preparation has already failed with a Git lock creation error or `Operation not permitted`, preserve the exact error and retry the same command only after the host permits the required writes. This is host-call guidance, not a permission grant or a runtime-enforced check.

## Codex dispatch details

### Permission at the dispatch boundary

Read `runManifest.userAuthorization` before a live dispatch, including `report-finalize`, which can start the translator internally. It preserves the confirmation text and the user response relayed through the wizard; quote its relevant recipient/material scope and response in the execution justification alongside the prepared assignment. The translator reuses the report writer's provider/model, as disclosed at confirmation. Do not wait until translation to inspect that authorization. A missing record is not consent, and the record does not override a host rejection. If review reports missing evidence already present in the record, submit that existing evidence through the permitted review mechanism; ask the user only when the actual approved scope does not cover the dispatch.

Before a live CLI worker dispatch, check the current host's declared sandbox and approval policy. In a restricted Codex session, request the host's supported execution permission on the dispatch command itself (`sandbox_permissions: "require_escalated"` for `exec_command` when that mechanism is available and permitted). Scope the request to the prepared run and explain that it starts the selected worker processes. Follow the host's approval decision; a wizard confirmation or a successful preflight does not grant this permission.

Carry the user's existing authorization into that request: identify the task, the selected providers/models from the prepared assignments, and the task brief, relevant repository source/documents, selected evidence, and related run results those workers will process. Cite the actual `Proceed` response and the data-transfer scope displayed in that confirmation; `outcome.confirmationText` is the runtime's summary reference, not proof by itself that the user saw or accepted it. Explain this scope in the execution tool's justification rather than describing only process startup. Data-transfer authorization and host execution privileges are separate: preserve the former while requesting the latter.

Do not infer that an older confirmation included the new disclosure, widen the approved recipients or material, or treat model selection alone as blanket data-transfer consent. When the conversation already authorizes the same recipients and scope, use that evidence without asking again. If approval review rejects a dispatch, retain and report its reason; supply omitted existing evidence only through the host's permitted review mechanism. If the stated gap is not covered by the user's actual authorization, ask one focused question naming that gap and wait for the answer. Never bypass the rejection or repeat an unchanged request.

Apply this guidance to both `okstra worker-dispatch` and the cmux override's `okstra team dispatch`, including initial workers, reverify, critic, report-writer, and retries. Read-only previews do not start workers. A session already authorized to execute outside the sandbox does not need another request. When the host prohibits escalation, preserve the pending invocation and report the execution constraint instead of trying an alternate launcher or weakening host controls.

The worker's `--sandbox danger-full-access` flag only selects the child Codex policy; it does not remove restrictions inherited from the parent process. If startup reports `Operation not permitted`, retain the exact error and distinguish initialization failure from a worker verdict. Retry only through the permitted host mechanism after the execution conditions change, within the existing retry limit; do not repeat the same restricted invocation. This is host-call guidance, not a permission grant or a runtime-enforced check.

- For convergence reverify, consume the persisted round plan exactly. This adapter may map and transport each returned batch, but it cannot change batch membership and does not classify findings or branch on task type, provider, or model identity.
- Do not invoke Claude Code team or subagent tools.
- The prepared run manifest and team-state are the dispatch authority. A `runner=native-session` assignment stays in the current Codex host; a `runner=cli-wrapper` assignment uses the registered provider wrapper. Unsupported explicitly requested workers fail; an adapter must not silently change the roster.
- The report-writer follows its persisted provider, model, and runner assignment exactly. It has no Codex-only provider override or separate opt-in gate.
- Reverify and critic retries invoke a fresh worker attempt and persist the core-supplied `dispatchKind` (`reverify-r<N>` or `critic`) in the dispatch record; never reuse a prior rollout as a new vote.
- Native calls use only `hostModelValue`. Immediately before the host primitive, run `okstra agent-prompt record-dispatch` with the project root, run manifest, verified metadata path, and `--enforcement-mode host-native-spec-link-gate`; after the Result Path exists, run `okstra agent-prompt link-result` with `--dispatch-id <invocationId>:attempt-1` and that path before accepting it. CLI calls use only `modelExecutionValue` through `worker-dispatch`, which records its own dispatch. The native linkage proves association with a verified specification, not observed prompt delivery.
- Report-writer completion requires both the data.json Result Path and the worker-results audit path, even when the synchronous dispatch command exits successfully.

## Completion, cleanup, and resume

- A native host completion or successful synchronous CLI return alone is insufficient; verify terminal state, every required completion path, and the corresponding worker-dispatch audit record before counting the worker as complete.
- Resume from run artifacts and lead-events checkpoints. Do not invent a Claude session id.
