/** * CodexHarness — CodingHarness implementation for OpenAI Codex CLI (codex). * * Codex is a Rust TUI whose sessions persist as rollout JSONL files under * `$CODEX_HOME/sessions/`. Integration is pinned to v0.144.5 — see * docs/harness-specific-notes/openai-codex.md for the empirical basis. * * Key characteristics (see the `codex-harness` capability): * - **Always-synth per-spawn CODEX_HOME** (pi-style topology, design D1): * every spawn gets a layer-C synthesized home (base = the user's * `CODEX_HOME ?? ~/.codex`), even with NO configured config-dir (empty * overlay). One decision resolves three problems: race-free rollout * discovery (exactly one rollout per home), the directory-trust pre-seed * lives in the ephemeral synth config.toml (the user's real config.toml is * never written at spawn time), and teardown already reaps the synth * location. Trade-off: subagents run memory-clean (no state SQLite DBs — * they cannot be safely shared via symlink). * - **Spawn-scoped settings ride the synth config.toml, not argv** (D2): * trust entry for the (realpath'd) launch cwd, the `notify` sentinel argv, * and `model_reasoning_effort` are injected post-merge by * `applyCodexChildSettingsOverride`. Argv carries only `-m ` and * the sandbox/approval bypass. * - **Notify-based lifecycle-end sentinel** (D4): the synth config.toml * registers the env-gated `codex-end-signal.js` notify program — fires per * completed turn, NEVER on Esc-interrupt, no install into the user's home, * no hook trust. * - Real token usage AND the model's real context window come from the * rollout's `token_count` events (`analyzeCodexSessionFile`). * - Readiness: capture-pane poll for the composer marker (hermes precedent); * launch failure surfaces as a ready timeout. * - `Escape` interrupts a running turn; `C-c` would QUIT the TUI, so the * declared stop combo is `Escape`. * - OpenAI-only v1: routing `provider` is ignored with a warning * (claude-code precedent). */ import type { CodingHarness, HarnessConfigProfile, HarnessEnvironment, HarnessSpawnParams, SessionAnalysis } from "#src/harness/interface"; import type { TmuxManager } from "#src/tmux-manager"; import type { SubagentRecord } from "#src/types"; /** * Routing `thinking` → codex `model_reasoning_effort` (config-key only; codex * has no CLI effort flag). Codex efforts are minimal|low|medium|high, so * `off` floors at `minimal` and `xhigh` caps at `high`. Unknown values are * skipped with a warning (codex falls back to its config/default effort). */ export declare const THINKING_TO_EFFORT: Record; export interface CodexChildSettings { /** Directories whose trust entries prevent the TUI trust screen. */ trustPaths?: string[]; /** The notify sentinel argv (`notify = [...]`). */ notify?: string[]; /** `model_reasoning_effort` value (already mapped from `thinking`). */ reasoningEffort?: string; } /** * Post-merge injection of spawn-scoped settings into the synth `config.toml` * (design D2 — the analog of `applyPiChildSettingsOverride`): * (a) `[projects.""] trust_level = "trusted"` so the TUI's * directory-trust screen never appears (`-c` overrides verifiably cannot * do this — spike S2); * (b) the `notify` argv pointing at the codex end-signal script; * (c) `model_reasoning_effort` when the role resolves a thinking level. * * Malformed/absent existing TOML is treated as empty (best-effort, matching * the synth merge). The write is atomic. */ export declare function applyCodexChildSettingsOverride(configTomlPath: string, settings: CodexChildSettings): void; /** * Late-binding rollout lookup: the single * `sessions/YYYY/MM/DD/rollout-*.jsonl` under a per-spawn synth home. The * file appears only when codex creates it, so this is called from * `waitForReady` (and re-callable), never computed as a fixed path at spawn. * Returns the newest match when several exist (a re-spawn into a reused home * cannot happen with per-spawn synth dirs, but be tolerant anyway). */ export declare function findCodexRolloutFile(home: string): string | undefined; export declare class CodexHarness implements CodingHarness { readonly id = "codex"; readonly roleDefinitionDelivery: "prompt-context"; /** Self-described widget identity (promotes the reserved row in harness-colors). */ readonly widgetIdentity: { colorHex: string; sigil: string; }; readonly completionSentinelKind = "notify-program sentinel"; /** * `Escape` interrupts a running codex turn (the busy line itself says * "esc to interrupt"); `C-c` on an idle composer QUITS the TUI, so the * default `C-c` combo must never be sent to codex. */ readonly defaultStopKeyCombo = "Escape"; /** No live tool stream — `currentTool` derives from parsed rollout tool calls. */ readonly providesToolStatusStream = false; /** * Codex v0.144.6 drops tmux's bracketed-paste path during automated launch, * while literal `send-keys -l` preserves multiline text in the composer. * Submission remains a separate Enter so the confirmed-delivery retry loop * can recover a keypress sent before the composer redraws. */ readonly promptDeliveryStyle: "literal"; readonly repastePromptIfMissing = true; /** * Config-dir profile (layer B). `CODEX_HOME` redirects the entire config * tree (pi's `PI_CODING_AGENT_DIR` analogue), so the synth dir IS the * config dir (`configSubdir "."`). `config.toml` is TOML — handled by the * synth's extension-dispatched merge. `auth.json` is SYMLINKED from the * base: codex rewrites it in place on OAuth token rotation (truncate+write, * no rename-over — spike S1), so refreshes pass through to the real file. */ readonly configProfile: HarnessConfigProfile; resolveSessionDir(record: SubagentRecord): string; /** * Late-binding rollout locator: codex creates the rollout only when the * FIRST prompt is submitted (verified live — the file does not exist at * composer-ready time), and its name embeds a timestamp + uuid we cannot * predict. Prompt delivery's confirmation loop and the * `LifecycleController`'s session-file wait poll this until the glob under * the per-spawn home resolves (race-free: exactly one rollout per home). */ discoverSessionFile(env: HarnessEnvironment): string | undefined; resolveEndFilePath(agentId: string, runtimeDir?: string): string; resolveOwnedTempPaths(agentId: string, runtimeDir?: string): string[]; /** * Prepare the per-spawn CODEX_HOME: * 1. Resolve the home — the spawner-produced override when a config-dir is * configured (layer synth or replace target), else self-synthesize a * layer home with an EMPTY overlay (always-synth, design D1). Codex * hard-errors on a nonexistent `CODEX_HOME`, so the dir is ensured. * 2. Inject spawn-scoped settings into the home's config.toml (D2): trust * for the spawn cwd (the worktree path is added later in * `prepareAfterWorktree`), the notify sentinel argv, and the mapped * reasoning effort. In `replace` mode this writes the user's curated * dir (cmd precedent: the sentinel registration follows the redirected * home in both modes) — the notify script stays env-gated and inert * outside our launches. */ setupEnvironment(params: HarnessSpawnParams): Promise; /** * Trust the ACTUAL launch cwd once it is final (the worktree is created * after `setupEnvironment`). The entry rides the synth config.toml — no * user-file pre-seed (unlike claude-code's `.claude.json`). Realpath'd: * codex canonicalizes trust lookups, and /tmp worktree paths are often * symlinked. */ prepareAfterWorktree(env: HarnessEnvironment): Promise; /** * Build the codex launch command: * `cd && env PI_TMUX_END_FILE=… codex --dangerously-bypass-approvals-and-sandbox [-m ]`. * * - The bypass flag is the `--yolo` analog (hermes/claude-code parity): * approval dialogs would wedge an unattended agent, and plain * `workspace-write` has no network access (a research role would * silently fail). It does NOT bypass the trust screen — that is the * config.toml pre-seed's job. * - Everything else spawn-scoped (notify, effort, trust) rides the synth * config.toml (D2), not argv. * - `CODEX_HOME` is applied by the spawner's `redirectEnv` wrap. * - `PI_TMUX_END_FILE` is inherited by the notify subprocess through codex. */ buildLaunchCommand(params: HarnessSpawnParams, env: HarnessEnvironment): string; /** * Wait for the codex TUI composer, then resolve the rollout session file. * * There is no file-based ready signal, so the pane content IS the readiness * signal (hermes precedent): ready = the composer prompt `›` is visible AND * the directory-trust screen is not (the trust screen renders `›` as its * selection cursor; with the pre-seed it never appears — seeing it means * the pre-seed failed and delivery would be swallowed). Launch failures * (bad model, missing auth) surface as a ready timeout. * * The rollout file does NOT exist yet at ready time (codex creates it on * first prompt submit — verified live), so readiness does not block on it; * `discoverSessionFile` resolves it late (an opportunistic hit here covers * resume-style cases where the home already has one). */ waitForReady(_params: HarnessSpawnParams, env: HarnessEnvironment, agentId: string, tmux: TmuxManager, signal?: AbortSignal): Promise; readTurnCount(env: HarnessEnvironment): Promise; analyzeSession(filePath: string): SessionAnalysis; /** * Authoritative completion: the notify sentinel's LAST line parses with * `completed === true`. Codex's notify fires per completed turn and NEVER * on Esc-interrupt (the rollout gets `turn_aborted` instead — verified * live), so an interrupt leaves the file without a new line and a * re-steered agent re-arms naturally with its next completed turn. */ isComplete(env: HarnessEnvironment): boolean; /** * Remove the per-agent end sentinel. The synth home lives under the * subagent teardown scoping (`/tmp/subagents/...`) and is * reaped there; a `replace`-mode user dir is never touched. */ cleanup(env: HarnessEnvironment): Promise; } //# sourceMappingURL=codex-harness.d.ts.map