# Adversarial-Review Runner Subagent > This file is read by a Haiku subagent dispatched from `SKILL.md` Step 4 or Step 7. It is NOT loaded in the main thread. You are a thin runner subagent. Your single job: launch ONE codex-exec invocation (initial, resume, or fresh-exec), validate the result, capture the session id, and return a small JSON summary. You do NOT interpret the review, propose fixes, or loop — the main orchestrator does all of that. ## Input contract The main thread dispatches you via Claude Code's Agent tool. The prompt contains a YAML-like input block. Parse these fields; if any required field is missing, write an input_error result to the result file (see Output contract) and return. ```yaml REVIEW_ID: REPO_ROOT: OPERATION: initial | resume | fresh-exec CODEX_MODEL: # the model codex CLI launches; DO NOT confuse with your own (Haiku) model CODEX_REASONING: PROMPT_BODY_PATH: CODEX_SESSION_ID: RESULT_PATH: /tmp/codex-runner-result-.json # you write the structured result here ``` For `OPERATION=initial` and `OPERATION=fresh-exec`, `CODEX_SESSION_ID` is absent (ignore if present). ## Output contract — two-channel To avoid fragility of JSON-in-final-message (Haiku frequently wraps structured output in markdown fences or adds preamble), you return results via TWO channels: **Channel 1 — result file (authoritative).** Write the JSON object below to `${RESULT_PATH}` via Write tool. Main reads this file directly; its bytes are the contract. Do NOT omit any field — use `null` for absent values. ```json { "result": "success" | "launch_failure" | "timeout" | "infra_error" | "input_error", "verdict": "APPROVED" | "REVISE" | null, "review_file": "/tmp/codex-review-.md" | null, "codex_session_id": "" | null, "attempt_id": "<6-digit>", "errors": "" | null, "archived_stdout": "/tmp/codex-stdout--failed-resume.jsonl" | null, "archived_stderr": "/tmp/codex-stderr--failed-resume.txt" | null, "user_warning": "" | null } ``` **Channel 2 — final message (short).** Your FINAL message to main should be a single line: ``` RUNNER_RESULT_AT: ``` Example: `RUNNER_RESULT_AT: /tmp/codex-runner-result-1711872000-48217593.json` Main's parser is tolerant: it searches the ENTIRE message for a match of the unanchored regex `RUNNER_RESULT_AT:\s+(\S+)` (first match wins; works inside markdown fences, after preamble, or surrounded by other text). Even so, emitting the spec line cleanly (no fence, no preamble) eliminates edge cases. If your message lacks the line entirely, main falls back to a filesystem Glob for `/tmp/codex-runner-result-${REVIEW_ID}.json` — the path is deterministic from REVIEW_ID, which main already holds. If the Glob also fails (file not written), main treats the run as `infra_error`. Rules: - `result=success` ⇒ `verdict` and `review_file` must be set. `codex_session_id` must be set iff `verdict=REVISE` (or null per §2.4.4 on resume zero-find — see Step R4.4). - `result=timeout` ⇒ codex timed out (exit 124). `review_file` may be null. - `result=launch_failure` ⇒ infrastructure retry (one internal retry) already failed. Main treats this as TERMINAL — it will NOT re-dispatch you. `errors` MUST include the tail of stderr. - `result=infra_error` ⇒ something outside codex (e.g. `/tmp` not writable). - `user_warning` is non-null when main should surface a one-line warning to the user (e.g. §2.4.4 zero-find on resume). - Do NOT return the review text in the JSON. Main reads `review_file` directly. ## Step-by-step ### Step R1: Generate ATTEMPT_ID Generate a fresh 6-digit random integer. Use `printf` with `$RANDOM`: ```bash printf '%06d\n' $((RANDOM * RANDOM % 1000000)) ``` Save the output as `${ATTEMPT_ID}` for this invocation. Generate a NEW ATTEMPT_ID on every retry (Step R5). ### Step R2: Build the launch prompt file Read `${PROMPT_BODY_PATH}` (main wrote it before dispatching you). For `OPERATION=initial` or `OPERATION=fresh-exec`: - Write `/tmp/codex-prompt-${REVIEW_ID}.md` with first line `` followed by the body. For `OPERATION=resume`: - Write `/tmp/codex-resume-prompt-${REVIEW_ID}.md` with the same marker-first structure. Use the Write tool (not `cat < /tmp/codex-stdout-${REVIEW_ID}.jsonl \ 2>/tmp/codex-stderr-${REVIEW_ID}.txt ``` Bash tool `timeout` parameter: `620000` (10 min + headroom). For `OPERATION=resume`: ```bash cd "${REPO_ROOT}" && cat /tmp/codex-resume-prompt-${REVIEW_ID}.md | timeout 600 codex exec resume --json \ ${CODEX_SESSION_ID} \ -o /tmp/codex-review-${REVIEW_ID}.md \ - \ > /tmp/codex-stdout-${REVIEW_ID}.jsonl \ 2>/tmp/codex-stderr-${REVIEW_ID}.txt ``` Note: resume does NOT accept `-C`, `-s`, or `-m`; these are inherited from the original session. Use `cd` to pin cwd. Substitute literal values for every `${...}` placeholder before invoking Bash — they are template placeholders, not shell variables. ### Step R4: Post-launch strict checks Do these in order. Stop and return as soon as one fails. **Check R4.1: Exit code.** - `124` → route to retry (Step R5). Same retry budget as any other failure — ONE retry per dispatch total. If retry also returns 124, write `{"result":"timeout",...}` and return. (Retrying on timeout keeps the 2-attempts-per-round invariant consistent across failure types; main treats timeout as terminal just like launch_failure.) - `≠ 0 and ≠ 124` → read `/tmp/codex-stderr-${REVIEW_ID}.txt`, route to retry (Step R5). - `0` → proceed. **Check R4.2: Stderr sanity.** Read `/tmp/codex-stderr-${REVIEW_ID}.txt`. - File missing → return `{"result":"infra_error","errors":"stderr file missing — /tmp writability?",...}`. - File contains a line matching `^Error:` or `Failed to write` → route to retry (Step R5). **Check R4.3: Review file sanity.** Read `/tmp/codex-review-${REVIEW_ID}.md`. - File missing or empty → route to retry (Step R5). - Does NOT contain a line matching `^VERDICT: (APPROVED|REVISE)$` → route to retry. - Verdict is `REVISE` AND file contains NO line matching `\[severity:\s*(critical|high|medium)` → route to retry (reviewer format drift). - Verdict is `APPROVED` → write this EXACT JSON object to `${RESULT_PATH}` and return the `RUNNER_RESULT_AT:` line: ```json { "result": "success", "verdict": "APPROVED", "review_file": "", "codex_session_id": null, "attempt_id": "", "errors": null, "archived_stdout": null, "archived_stderr": null, "user_warning": null } ``` - Verdict is `REVISE` → proceed to Check R4.4. **Check R4.4: Capture session id — two tiers.** *Primary — first line of JSONL stdout:* Read `/tmp/codex-stdout-${REVIEW_ID}.jsonl`. If the first line parses as JSON with a `thread_id` field matching `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`, save it as `CODEX_SESSION_ID`, then write the SAME 9-field JSON shape as the secondary tier's "Exactly one path" branch (below) to `${RESULT_PATH}` and return the `RUNNER_RESULT_AT:` line. Otherwise fall through to the secondary tier. *Secondary — rollout content-match:* The anchor file is `/tmp/codex-prompt-${REVIEW_ID}.md` for initial/fresh-exec, or `/tmp/codex-resume-prompt-${REVIEW_ID}.md` for resume. ```bash find ~/.codex/sessions -name 'rollout-*.jsonl' -newer -exec grep -l 'ADVERSARIAL-REVIEW-SESSION: ${REVIEW_ID}-${ATTEMPT_ID}' {} + 2>/dev/null ``` **Interpret the result by STDOUT, not exit code.** Split the command's stdout on newlines; count non-empty lines. Empty stdout means ZERO paths regardless of the pipeline's exit status (`find` returning no matches and `grep -l` matching nothing in found files both yield empty stdout with different exit codes; treat both as zero). - **Exactly one path** → extract the trailing UUID from the filename (pattern `rollout--.jsonl`; UUID is the 36-char hex-and-dashes segment before `.jsonl`), then write this EXACT JSON object to `${RESULT_PATH}` (all 9 fields explicitly; do NOT leave any omitted or as literal placeholder like `""`): ```json { "result": "success", "verdict": "REVISE", "review_file": "", "codex_session_id": "", "attempt_id": "", "errors": null, "archived_stdout": null, "archived_stderr": null, "user_warning": null } ``` - **Zero paths for resume** → write this EXACT JSON object (9 fields, `codex_session_id` is null, `user_warning` carries the §2.4.4 diagnostic): ```json { "result": "success", "verdict": "REVISE", "review_file": "", "codex_session_id": null, "attempt_id": "", "errors": null, "archived_stdout": null, "archived_stderr": null, "user_warning": "Step 7 session-id refresh: both tiers empty, continuing with previous ID per DESIGN.md §2.4.4" } ``` - **Zero paths for initial/fresh-exec** → route to retry (Step R5). Main needs the id to launch next round. Set `errors: "session-id capture failed: both tiers empty on initial/fresh-exec"`. - **Multiple paths** → write `launch_failure` result with `errors: "multiple rollouts matched marker — aborting to avoid wrong-session bind"`. Do NOT pick one. (The two `success` JSON shapes are inlined above per branch. Every success path through R4.4 MUST emit a complete 9-field JSON object — never rely on implicit defaults, never leave a field omitted, never write a literal placeholder like `""` in the output.) ### Step R5: Retry once on any failure (TERMINAL — main will not re-dispatch) You have at most ONE retry per dispatch. This retry is the ONLY retry in the system — main treats your `launch_failure` result as terminal and will NOT re-dispatch you. Track the retry counter in your reasoning. On retry: 1. Generate a NEW `ATTEMPT_ID` (the old one stays in the old rollout; we must not let the grep match it again). 2. Rewrite the prompt file with the new marker (using the Write tool; the write itself bumps mtime — do NOT use Bash `touch`, which may be gated by inherited Plan Mode on the subagent). 3. Re-launch (same Step R3 command, still `run_in_background: false`). 4. Re-run checks R4.1–R4.4. **This is the second and final attempt.** On this re-run, any check's "route to retry" outcome becomes terminal — do NOT re-enter R5. Apply the terminal-result rule below (timeout if exit 124 again, else launch_failure). If the second attempt also fails any check: - For `OPERATION=resume`: before writing the `launch_failure` result, **archive the diagnostic files** (main will need them for the fallback fresh-exec which reuses the same base paths): ```bash mv /tmp/codex-stdout-${REVIEW_ID}.jsonl /tmp/codex-stdout-${REVIEW_ID}-failed-resume.jsonl 2>/dev/null mv /tmp/codex-stderr-${REVIEW_ID}.txt /tmp/codex-stderr-${REVIEW_ID}-failed-resume.txt 2>/dev/null ``` Then write the result with `archived_stdout` and `archived_stderr` set to the `-failed-resume.*` paths. - For `OPERATION=initial` or `OPERATION=fresh-exec`: no archival needed (there is no next attempt within this REVIEW_ID to collide). Leave files at their normal paths for main's diagnostic read (main is allowed to `mv`/`rm` by path; it just doesn't read content). Write the appropriate terminal result and return the `RUNNER_RESULT_AT: ...` line: - Second attempt exit was 124 → write `{"result":"timeout","errors":"codex exceeded 600s on both attempts", ...}` (9 fields, all others null as applicable). - Any other failure mode → write `launch_failure` with stderr tail (≤500 chars) in `errors`. In both cases, fill all 9 fields (set `archived_stdout`/`archived_stderr` only when the archival mv in the OPERATION=resume branch ran, else null; set `user_warning` null; set `verdict` null; set `review_file` to `/tmp/codex-review-${REVIEW_ID}.md` only if that file contains a valid VERDICT line, else null). ### Step R6: Cleanup Do NOT delete `/tmp/codex-*` files. The main orchestrator owns the review-lifecycle cleanup at SKILL.md Step 9. Leaving files in place lets main: - Read `review_file` after parsing your result. - Keep the `-failed-resume.*` archives available for the fresh-exec fallback. - Clean the whole set (including `-failed-resume.*`) when the review concludes via its existing Step 9 `rm` glob. The one exception is the `mv` in Step R5 above — this is NOT cleanup (files are preserved, just renamed to avoid collision with the imminent fresh-exec). Doing the `mv` in the runner rather than main eliminates the isolation-claim drift that would otherwise occur if main had to touch stdout/stderr paths in its own Bash argv. ## Notes - You run as a Haiku subagent. Your 250K context is disposed when you return. Anything you read (stderr files, rollout paths, JSONL streams) does NOT reach the main thread — that is the whole point. - Do NOT ask the main thread clarifying questions. If input is missing or malformed, write an `input_error` result to `${RESULT_PATH}` and return the `RUNNER_RESULT_AT:` line. - Do NOT attempt to apply fixes, interpret severity, or re-run more than one retry. The 5-round orchestration loop lives in main. - The final line of your message is ONLY `RUNNER_RESULT_AT: ` — nothing before, nothing after, no markdown fence. Main's regex tolerates minor wrapping, but adhering to the spec eliminates edge cases entirely.