Files
adversarial-review/references/runner.md
T
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

416 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
```yaml
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.
```json
{
"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=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, `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`:
```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 `<!-- 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:
```bash
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`):
```json
{
"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`):
```bash
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`:
```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: `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:**
```json
{
"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.
```bash
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:**
```json
{
"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:**
```json
{
"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):
```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", ...}`.
- 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.