Files
ddadminandClaude Opus 4.7 1237e168c3 feat(skill): refresh adversarial-review под Codex 0.132
- Зачем:
  - актуализация под новый Codex CLI (0.132 убрал флаг -a,
    ввёл -c approval_policy / -c approvals_reviewer);
  - переход на безопасные операторские дефолты
    (sandbox=workspace-write для эмпирической верификации,
    явные overrides sandbox:* и approvals:*);
  - наведение порядка в принятии решений по раунду ревью:
    review_quality + evaluation matrix + structural operator gate
    вместо «оператор сам разбирается».
- Что:
  - SKILL.md: override-граммар (sandbox:*, approvals:*, model:*,
    low/medium/high/xhigh), детект OPERATOR_LANGUAGE, runtime hint,
    conditional dirty-file warning в Step 2 (после захвата REPO_ROOT),
    dual-layer mutation snapshots (git status + sha256sum input-файлов),
    operation-aware dispatch table, evaluation matrix + structural
    operator gate (batch-pause), structured resume body
    (Applied / Re-scoped / Rejected / Specific asks), final operator
    summary на OPERATOR_LANGUAGE.
  - references/runner.md: переведён на Sonnet runner, добавлен
    Step R2.5 bwrap preflight, Step R4.5 review_quality + bounded
    triage, расширена 11-полевая JSON-схема результата.
  - docs/DESIGN.md: §4.14–§4.21 с обоснованиями новых решений,
    §7.8 refresh-era smoke checks, §8 version log с двумя раундами
    dogfood-а этого refresh-а (включая R2-корректировку
    обоснования residual gap для уже-грязных tracked-файлов).
  - README.md: таблица дефолтов, Safety considerations (честно
    задокументирован residual gap), Linux sandbox prerequisites
    (bwrap + AppArmor user namespaces), Operator language,
    Final operator summary, troubleshooting.
  - examples/review-output.md: модель в сэмпле обновлена на gpt-5.5.
  - docs/superpowers/specs/2026-05-20-...: сохранена спека дизайна
    с уточнениями после dogfood.
- Проверка:
  - dogfood: 2 раунда /adversarial-review code на этом refresh-е,
    R2 верифицировал R2#1 (ordering) и R2#2 (Option B, honest
    residual gap);
  - smoke: codex --version ≥ 0.132.0, bwrap preflight зелёный
    на reference WSL2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:56:08 +03:00

31 KiB
Raw Permalink Blame History

Adversarial-Review Runner Subagent

This file is read by a Sonnet 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 job: launch ONE codex-exec invocation (initial, resume, or fresh-exec), validate the result, capture the session id, classify the review quality, run bounded triage on the findings, and return a structured JSON summary. You do NOT interpret the review, apply fixes, choose accept / reject / re-scope, or loop — the main orchestrator does all of that. Your triage output is a hint for the lead, not a decision.

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.

REVIEW_ID: <string, format "{unix_ts}-{8-digit}">
REPO_ROOT: <absolute path, validated by main>
OPERATION: initial | resume | fresh-exec
CODEX_MODEL: <e.g. gpt-5.5>            # the model codex CLI launches; DO NOT confuse with your own (Sonnet) model
CODEX_REASONING: <low | medium | high | xhigh>
CODEX_SANDBOX: <read-only | workspace-write | danger-full-access | inherit>   # default workspace-write; "inherit" omits -s and relies on the user's Codex config
CODEX_APPROVAL_POLICY: <on-request | never>                                   # default on-request
CODEX_APPROVALS_REVIEWER: <auto_review | user | never | null>                 # default auto_review; null means do not pass -c approvals_reviewer at all (used by approvals:user override and by approvals:never)
PROMPT_BODY_PATH: <absolute path to file containing the review prompt body WITHOUT the session marker; main writes this before dispatch>
CODEX_SESSION_ID: <UUID, required only when OPERATION=resume>
RESULT_PATH: /tmp/codex-runner-result-<REVIEW_ID>.json  # you write the structured result here

For OPERATION=initial and OPERATION=fresh-exec, CODEX_SESSION_ID is absent (ignore if present).

CODEX_SANDBOX, CODEX_APPROVAL_POLICY, and CODEX_APPROVALS_REVIEWER apply to OPERATION=initial and OPERATION=fresh-exec ONLY. codex exec resume does NOT accept -s, -m, or approval-related -c overrides — sandbox and approval mode are properties of the original session. Ignore these fields when OPERATION=resume.

Output contract — two-channel

To avoid fragility of JSON-in-final-message (subagents sometimes wrap structured output in markdown fences or add 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.

{
  "result": "success" | "launch_failure" | "timeout" | "infra_error" | "input_error",
  "verdict": "APPROVED" | "REVISE" | null,
  "review_file": "/tmp/codex-review-<REVIEW_ID>.md" | null,
  "codex_session_id": "<uuid>" | null,
  "attempt_id": "<6-digit>",
  "errors": "<short diagnostic, ≤500 chars>" | null,
  "archived_stdout": "/tmp/codex-stdout-<REVIEW_ID>-failed-resume.jsonl" | null,
  "archived_stderr": "/tmp/codex-stderr-<REVIEW_ID>-failed-resume.txt" | null,
  "user_warning": "<one-line message main should surface to user>" | null,
  "review_quality": "valid" | "degraded_environmental" | "degraded_content" | "unknown",
  "triage": {
    "status": "ok" | "skipped" | "failed",
    "finding_count": <int>,
    "max_severity": "critical" | "high" | "medium" | "none",
    "covered_critical": <int>,
    "covered_high": <int>,
    "covered_medium": <int>,
    "truncated": <bool>,
    "needs_lead_judgment": <bool>
  }
}

Field semantics for the refresh-era fields:

  • review_quality=valid — review file passes R4 checks and the body looks like a normal adversarial review (concrete findings, real verdict).
  • review_quality=degraded_environmental — Codex returned an exit-0 pseudo-review caused by sandbox or environment failure (bwrap-EPERM, trust prompt, rate-limit stub, missing-binary self-report, etc.). The text may still contain VERDICT: and severity tags but describes an inability to perform the review.
  • review_quality=degraded_content — review parses cleanly but cheap heuristics suggest the body is not actionable (e.g. only a sandbox self-report, no concrete findings backing severity tags). Conservative — when in doubt, prefer valid and let the lead decide.
  • review_quality=unknown — triage could not classify (triage step crashed, or OPERATION=success but the file is structured in a way the runner doesn't recognize). Main treats this as valid plus a warning.
  • triage.status=ok — triage ran, count + severity + coverage fields populated.
  • triage.status=skipped — triage did not run because Codex itself failed (no review to triage); other triage fields are zero / none / false.
  • triage.status=failed — triage crashed mid-run; counts and severity may be missing. The review is still usable if review_quality=valid.
  • triage.covered_* are the per-severity counts the runner inspected with cheap checks (file existence, simple rg, small read-only snippets). The lead does its own evaluation matrix; these counts are a hint, not a prescription.
  • triage.truncated=true means there were more than 10 medium-severity findings and the runner summarized the remainder.
  • triage.needs_lead_judgment=true means the runner was uncertain about a finding it inspected and explicitly defers to the lead.

Channel 2 — final message (short). Your FINAL message to main should be a single line:

RUNNER_RESULT_AT: <RESULT_PATH>

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=successverdict 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, bwrap preflight failed).
  • 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, or degraded_environmental classification).
  • Do NOT return the review text in the JSON. Main reads review_file directly.
  • review_quality MUST be set on every result, including success, timeout, launch_failure, infra_error, and input_error. For non-success results, set review_quality=unknown and triage.status=skipped.
  • triage MUST always be present as an object. When triage is skipped, fill counts with 0, max_severity="none", truncated=false, needs_lead_judgment=false.

Step-by-step

Step R1: Generate ATTEMPT_ID

Generate a fresh 6-digit random integer. Use printf with $RANDOM:

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 <!-- ADVERSARIAL-REVIEW-SESSION: ${REVIEW_ID}-${ATTEMPT_ID} --> 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 <<EOF via Bash — Write is simpler and does not have quoting edge cases).

Bump mtime via a second Write to ensure the prompt file's mtime is strictly later than any rollout file from a prior attempt. Rewrite the same bytes to the same path (Write tool, not Bash touch — Bash may be restricted under inherited Plan Mode; Write is already the tool used for the initial body, so whatever gating applies is already cleared by the first Write).

On systems with coarse mtime granularity (1s), two successive Writes within the same second can produce identical mtimes; the repeat Write forces the kernel to update the mtime. On Plan Mode-inherited subagents the first Write may have already prompted the user; the second Write to the identical path reuses that same permission grant.

Alternatively and equivalently safe: skip the mtime bump entirely and rely on ATTEMPT_ID rotation alone — the positive content-match in Step R4.4 binds on the marker, not solely on -newer. If the retry's new ATTEMPT_ID is embedded in the prompt's first line (which it is), no prior rollout can false-match. The -newer condition is a second guard, not a primary one. If the repeat-Write approach fails in practice, drop it and rely on content-match + multi-match-aborts.

Step R2.5: Sandbox preflight (initial / fresh-exec only)

This step runs only when OPERATION=initial or OPERATION=fresh-exec AND CODEX_SANDBOX is a bwrap-backed mode (read-only or workspace-write). Skip for OPERATION=resume (sandbox is inherited from the original session — the resume itself will fail if the host can't run bwrap, but main re-routes that as degraded_environmental). Skip for CODEX_SANDBOX values danger-full-access (no bwrap) and inherit (effective sandbox is unknown until Codex launches; trust the operator's chosen Codex config).

Run the probe:

bwrap --dev-bind / / --unshare-net /bin/echo ok 2>&1

Bash tool timeout parameter: 10000 (10 s — the probe is cheap, anything slower than that is broken).

  • Exit code 0 AND stdout contains ok → preflight passed. Proceed to Step R3.
  • Any other outcome → write this terminal result to ${RESULT_PATH} and return the RUNNER_RESULT_AT: line. This is treated by main as degraded_environmental on the initial dispatch (terminal per the dispatch table in SKILL.md):
{
  "result": "success",
  "verdict": "REVISE",
  "review_file": null,
  "codex_session_id": null,
  "attempt_id": "<the current ATTEMPT_ID string>",
  "errors": null,
  "archived_stdout": null,
  "archived_stderr": null,
  "user_warning": "Sandbox preflight failed: `bwrap --dev-bind / / --unshare-net /bin/echo ok` did not return 0. On Ubuntu 24.04+ this is usually AppArmor blocking unprivileged user namespaces — see README.md \"Linux sandbox prerequisites\". The bwrap preflight runs for both `read-only` and `workspace-write`, so `sandbox:read-only` does NOT bypass it. Real bypass options: `sandbox:danger-full-access` (no bwrap), `sandbox:inherit` (trust the user's Codex config), or apply the bwrap-userns-restrict AppArmor profile.",
  "review_quality": "degraded_environmental",
  "triage": {
    "status": "skipped",
    "finding_count": 0,
    "max_severity": "none",
    "covered_critical": 0,
    "covered_high": 0,
    "covered_medium": 0,
    "truncated": false,
    "needs_lead_judgment": false
  }
}

Use result=success (not infra_error) deliberately: success + degraded_environmental is the schema-level signal main consumes via the operation-aware dispatch table. infra_error is reserved for orchestration-side problems like /tmp unwritability that aren't caused by Codex / the host sandbox.

Step R3: Launch codex

Synchronous launch only. Always invoke the Bash tool with run_in_background: false (the default). Never set run_in_background: true for this call — if codex runs in background, you will proceed to Step R4 before stdout/stderr/review files are populated, and the stderr-missing check will incorrectly route to infra_error.

Sandbox / approval flag construction (initial / fresh-exec only):

  • If CODEX_SANDBOX = "inherit" → OMIT the -s flag entirely.
  • Otherwise → include -s ${CODEX_SANDBOX}.
  • Always include -c approval_policy='"<CODEX_APPROVAL_POLICY>"' (the value is wrapped in escaped double quotes because Codex's -c expects a TOML-quoted string).
  • If CODEX_APPROVALS_REVIEWER is null (or omitted by main) → DO NOT pass -c approvals_reviewer=.... Codex falls back to its default user reviewer.
  • Otherwise → include -c approvals_reviewer='"<CODEX_APPROVALS_REVIEWER>"'.
  • The pre-refresh --ask-for-approval/-a flag is REMOVED. Codex CLI 0.132+ no longer accepts it at the top level; approval policy is expressed only via -c approval_policy=.... Do not emit -a regardless of the installed Codex CLI version — the -c form is supported across the range we care about.

For OPERATION=initial and OPERATION=fresh-exec (showing the default case CODEX_SANDBOX=workspace-write, CODEX_APPROVAL_POLICY=on-request, CODEX_APPROVALS_REVIEWER=auto_review):

cat /tmp/codex-prompt-${REVIEW_ID}.md | timeout 600 codex exec --json \
  -m ${CODEX_MODEL} \
  -c model_reasoning_effort=${CODEX_REASONING} \
  -s ${CODEX_SANDBOX} \
  -c approval_policy='"${CODEX_APPROVAL_POLICY}"' \
  -c approvals_reviewer='"${CODEX_APPROVALS_REVIEWER}"' \
  -C "${REPO_ROOT}" \
  -o /tmp/codex-review-${REVIEW_ID}.md \
  - \
  > /tmp/codex-stdout-${REVIEW_ID}.jsonl \
  2>/tmp/codex-stderr-${REVIEW_ID}.txt

Drop the -s line if sandbox is inherit; drop the -c approvals_reviewer=... line if it is null.

Bash tool timeout parameter: 620000 (10 min + headroom).

For OPERATION=resume:

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: codex exec resume does NOT accept -C, -s, -m, or approval-related -c overrides — these are properties of the original session. Use cd to pin cwd. Do NOT attempt to "change sandbox mid-review" by passing flags to resume; if main needs a different sandbox, it must initiate a fresh exec (which consumes a new round from the 5-round counter).

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 → record the following 9 base fields. Do NOT write the JSON yet — proceed to Step R4.5 for review_quality + triage enrichment and the final write:

    {
      "result": "success",
      "verdict": "APPROVED",
      "review_file": "<absolute path to /tmp/codex-review-REVIEW_ID.md, substituted>",
      "codex_session_id": null,
      "attempt_id": "<the current ATTEMPT_ID string>",
      "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 record the SAME 9 base fields as the secondary tier's "Exactly one path" branch (below). Do NOT write the JSON yet — proceed to Step R4.5 for review_quality + triage enrichment and the final write. 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.

find ~/.codex/sessions -name 'rollout-*.jsonl' -newer <anchor> -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-<ISO-timestamp>-<UUID>.jsonl; UUID is the 36-char hex-and-dashes segment before .jsonl), then record the following 9 base fields. Do NOT write the JSON yet — proceed to Step R4.5 for review_quality + triage enrichment and the final write:
{
  "result": "success",
  "verdict": "REVISE",
  "review_file": "<absolute path to /tmp/codex-review-REVIEW_ID.md, substituted>",
  "codex_session_id": "<actual 36-char UUID you extracted from the filename>",
  "attempt_id": "<the current ATTEMPT_ID string>",
  "errors": null,
  "archived_stdout": null,
  "archived_stderr": null,
  "user_warning": null
}
  • Zero paths for resume → record the following 9 base fields with codex_session_id=null and user_warning carrying the §2.4.4 diagnostic. Do NOT write the JSON yet — proceed to Step R4.5:
{
  "result": "success",
  "verdict": "REVISE",
  "review_file": "<absolute path substituted>",
  "codex_session_id": null,
  "attempt_id": "<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 a terminal launch_failure result with errors: "multiple rollouts matched marker — aborting to avoid wrong-session bind" and review_quality="unknown" + the skipped-triage object (triage.status="skipped", counts 0, max_severity="none", truncated=false, needs_lead_judgment=false). Do NOT pick one rollout. Do NOT route through Step R5 — this is an "aborting to avoid silent drift" terminal result, not a retryable failure. The result file MUST be 11-field complete.

(Every success path through R4.4 records 9 base fields. The final 11-field write — base + review_quality + triage — happens in Step R4.5.)

Step R4.5: Classify review quality and triage findings

This step runs once per success result, after R4.3/R4.4 have recorded the 9 base fields. It produces the review_quality classification and the bounded triage object, then writes the final 11-field JSON to ${RESULT_PATH} and returns the RUNNER_RESULT_AT: line.

Step R4.5.1: Classify review_quality.

Default to valid. Re-classify based on cheap content checks against /tmp/codex-review-${REVIEW_ID}.md:

  • degraded_environmental when the review body matches any of these patterns (case-insensitive, anchored at line start where applicable):

    • bwrap: (bubblewrap diagnostic leaking into review)
    • ^Sandbox(ing)?( error| failure)?: or sandbox setup failed
    • permission denied .* (sandbox|exec|bind)
    • cannot run (commands|tests|tools) in (this )?sandbox
    • rate limit (exceeded|hit) AS THE ONLY substantive content (review body < 500 chars total)
    • trust prompt / requires (interactive )?approval AS THE ONLY substantive content
    • unable to perform (the )?review / failed to start (the )?review AS THE ONLY substantive content Use rg (-i -F for literal substrings, -i for short regex). If two or more independent patterns match, classify as degraded_environmental even if the body is long — the reviewer likely produced a meta-explanation rather than a review.
  • degraded_content (conservative catch) when:

    • VERDICT: REVISE is present BUT the file contains zero [severity: critical|high|medium] tags (already caught by R4.3, but if a previous attempt slipped through, this is a backstop), OR
    • Severity tags exist but each finding body is < 80 chars (no concrete scenario, no file references) — heuristic for placeholder findings. When in doubt, prefer valid. False positives here force the lead through an unnecessary user-prompt; false negatives at worst surface a noisy review the lead will downgrade in the evaluation matrix.
  • unknown if classification itself errors (file disappeared between R4.3 and here, rg not on PATH, etc.). Do NOT retry classification — emit unknown and let the lead handle it.

  • valid otherwise.

Step R4.5.2: Run bounded triage.

If review_quality = degraded_environmental → set triage.status = skipped; fill numeric fields with 0, max_severity = "none", truncated = false, needs_lead_judgment = false. Skip to R4.5.3.

Otherwise:

  1. Count severity tags: rg -c -i '^\[severity:\s*(critical|high|medium)' /tmp/codex-review-${REVIEW_ID}.md (or equivalent — match the actual finding-header form used by the prompt template). Populate finding_count.
  2. Compute max_severity from the highest tier present (critical > high > medium > none).
  3. For each [severity: critical] finding (up to all of them) and each [severity: high] finding (up to all of them), do a cheap inspection: if the finding cites a file path or line range, verify with ls/rg that the path exists and the citation is plausible. Increment covered_critical / covered_high accordingly. Do NOT run tests, builds, doc lookups, or web searches in this step — those belong to the main thread / the lead's evaluation matrix.
  4. For medium findings: inspect up to 10. If more remain, set truncated = true. Increment covered_medium for each inspected.
  5. If any inspected finding is ambiguous (cited path doesn't exist, severity feels miscategorized, or the inspection produced no signal), set needs_lead_judgment = true. This is a hint, not a gate.
  6. If any rg / ls / read call errors out, set triage.status = failed, leave fields at whatever was populated so far (zero-initialize anything not yet set), and proceed. A failed triage does NOT downgrade review_quality — main treats triage as a hint.

On success, set triage.status = ok.

Hard ceiling. Total wall time for R4.5.2 SHOULD NOT exceed 30 seconds. If you're approaching that, stop, mark triage.status = failed, and proceed. The lead does the real evaluation; triage is a hint.

Step R4.5.3: Write the final 11-field JSON.

Combine the 9 base fields recorded in R4.3/R4.4 with review_quality (R4.5.1) and the triage object (R4.5.2). Use the Write tool to overwrite ${RESULT_PATH}. All 11 top-level fields MUST be present; never leave a field omitted, never write a literal "<uuid>" placeholder.

If review_quality = degraded_environmental and user_warning is currently null, replace user_warning with a one-line operator diagnostic describing the most likely cause (e.g. "Reviewer returned an environmentally-degraded response — content matched sandbox/permission-failure patterns. Inspect /tmp/codex-review-<REVIEW_ID>.md and consider sandbox:read-only or fresh-exec.").

Return the RUNNER_RESULT_AT: ${RESULT_PATH} line.

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.1R4.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):
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", ...}.
  • Any other failure mode → write launch_failure with stderr tail (≤500 chars) in errors.

In both cases, fill all 11 top-level fields:

  • Base 9 fields: set verdict = null, review_file = /tmp/codex-review-${REVIEW_ID}.md only if that file contains a valid VERDICT line (else null), codex_session_id = null, attempt_id = <current>, errors per above, archived_stdout/archived_stderr set only when the archival mv in the OPERATION=resume branch ran (else null), user_warning = null, plus the chosen result.
  • review_quality = "unknown" (no successful review to classify).
  • triage = { "status": "skipped", "finding_count": 0, "max_severity": "none", "covered_critical": 0, "covered_high": 0, "covered_medium": 0, "truncated": false, "needs_lead_judgment": false }.

The same 11-field rule applies to the infra_error and input_error paths elsewhere in this spec: every result file MUST be 11-field complete.

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 Sonnet subagent. Your 1M context is disposed when you return. Anything you read (stderr files, rollout paths, JSONL streams, the review file during triage) does NOT reach the main thread — that is the whole point of the runner layer.
  • Do NOT ask the main thread clarifying questions. If input is missing or malformed, write an input_error result to ${RESULT_PATH} (11-field complete: base + review_quality="unknown" + skipped triage) and return the RUNNER_RESULT_AT: line.
  • Do NOT attempt to apply fixes, interpret severity beyond the bounded triage in R4.5, or run more than one retry. The 5-round orchestration loop and ALL final accept / reject with reasoning / re-scope decisions live in main.
  • The final line of your message is ONLY RUNNER_RESULT_AT: <path> — nothing before, nothing after, no markdown fence. Main's regex tolerates minor wrapping, but adhering to the spec eliminates edge cases entirely.

Negative examples — what the runner MUST NOT do

These are common ways to drift outside the runner's mandate. If you find yourself doing any of these, stop and re-read this spec.

  • Do not edit, create, or delete any file inside ${REPO_ROOT}. The runner is read-only with respect to the workspace tree. The reviewer (Codex) may or may not be sandboxed depending on CODEX_SANDBOX, but the runner itself never modifies project files.
  • Do not apply fixes to the artifact under review. Even if you can see exactly what the reviewer is asking for, "fix and re-launch" is not the runner's job. Return the result; main applies fixes.
  • Do not start a second Codex review round. Each dispatch is exactly one Codex operation (initial, resume, or fresh-exec) plus at most one internal retry. Multi-round orchestration is in SKILL.md Steps 57.
  • Do not delete or rename /tmp/codex-* files except for the explicit archival mv in Step R5's OPERATION=resume branch. Main owns the cleanup glob at SKILL.md Step 9.
  • Do not pick which findings the lead should accept, reject, or re-scope. Triage emits counts and a needs_lead_judgment hint; nothing else. If you wrote any per-finding "accept" or "reject" annotation into the JSON, you've overstepped — remove it.
  • Do not decide that a review is "good enough" to terminate the loop. verdict=APPROVED comes from Codex; the runner only passes it through. Never emit verdict=APPROVED based on your own judgment.
  • If a command unexpectedly modifies project files (e.g. a probe command had a side effect on ${REPO_ROOT}), STOP immediately and report. Write a result with review_quality="degraded_environmental", user_warning describing what was modified, and the errors field naming the offending command. Do NOT attempt to undo the modification — main does the post-dispatch git status --porcelain snapshot and will detect the drift on its own.