- Зачем:
- round-4 self-review на ветке нашёл HIGH-bug (пропущен плейсхолдер в Step 2, буквальный Opus мог не подставить литерал) и три MEDIUM (лишний abort на APPROVED при пустом `~/.codex/sessions/`, ложное обещание детекции wrong-session, same-epoch race при быстром codex). Исправляю до PR, чтобы не оставлять известных дефектов.
- Что:
- SKILL.md Step 2: `${CODEX_SESSIONS_BEFORE}` добавлен в список placeholders с явным пояснением роли.
- SKILL.md Step 4/7 + Rules: timestamp capture через `$(($(date +%s) - 1))` — сдвиг на секунду защищает от same-epoch race против `-newermt` strict-greater.
- SKILL.md Step 4: перестановка проверок — review-sanity (check 3) раньше session-id capture (check 4); на APPROVED session-id не нужен → skip entirely. Убирает лишний abort валидной APPROVED round.
- SKILL.md Step 4 check 4: parallel-codex caveat переписан честно — wrong-session resume НЕ детектируется автоматически, риск silent corruption зафиксирован.
- SKILL.md Step 4 check 4: при zero-lines перед abort выводятся диагностические артефакты (jsonl, stderr, 3 недавних rollout).
- DESIGN.md §4.8: обновлён с 4-check порядком и объяснением почему review раньше session-id.
- DESIGN.md §4.1: alternative-considered про filesystem-only-primary переписан — убрано ложное обещание §4.8-детекции; trade-offs расширены `-1` shift и skip-on-APPROVED.
- README.md: добавлены permissions `Bash(ls -t ~/.codex/sessions*)` для диагностики.
- Проверка:
- Заново прогнать §7.1 smoke-test: EXIT=0, rollout-UUID извлекается даже при пустом JSONL.
- Edge case: codex exec с cached response <1s + `CODEX_SESSIONS_BEFORE=T` → `find -newermt "@$((T-1))"` всё равно находит rollout.
1311 lines
58 KiB
Markdown
1311 lines
58 KiB
Markdown
# Design Notes — adversarial-review
|
|
|
|
This document records the *why* behind the current implementation of the
|
|
`adversarial-review` skill: the empirical facts about external tools it
|
|
depends on, the rationale for each non-obvious design decision, and the
|
|
alternatives that were considered and rejected. Read `SKILL.md` for
|
|
*what* the skill does step by step; read this file when you need to
|
|
understand *why it does it that way* before changing something.
|
|
|
|
---
|
|
|
|
## §0. Purpose and audience
|
|
|
|
### Why this file exists
|
|
|
|
The skill orchestrates two AI systems (Claude as the lead, OpenAI Codex
|
|
as the external reviewer) through a CLI subprocess interface. The
|
|
observable behavior of the skill depends on details of the Codex CLI
|
|
(stream splitting, exit codes, flag support), on details of the
|
|
Claude Code harness (Bash-tool output size, Opus interpretation of
|
|
instructions), and on a set of trade-offs between token cost,
|
|
robustness, and operator ergonomics. Those details drift with each new
|
|
Codex release and each new Claude model version. A maintainer reading
|
|
only `SKILL.md` sees the instructions but not which facts are
|
|
load-bearing and which are historical accidents — and so may undo a
|
|
subtle fix during a refactor.
|
|
|
|
This file fixes that. It names the facts, the decisions, the rejected
|
|
alternatives, and — importantly — the prior diagnostic reports that
|
|
turned out to be wrong. The goal is that a contributor six months from
|
|
now can change the skill confidently instead of re-discovering the
|
|
terrain.
|
|
|
|
### Audiences
|
|
|
|
1. **A future Claude** resuming work on the skill in a fresh session.
|
|
It knows Claude Code in general but has no memory of the discussion
|
|
that produced the current design.
|
|
2. **A human developer** who knows git and bash but does not know the
|
|
Claude Code internals or the Codex CLI quirks.
|
|
3. **A contributor** who wants to add a feature (new reviewer backend,
|
|
CI integration) and needs to know what invariants to preserve.
|
|
|
|
### What this file is NOT
|
|
|
|
- Not a user guide — that is `README.md`.
|
|
- Not authoritative on current behavior — `SKILL.md` is. This file
|
|
explains why `SKILL.md` is written the way it is. If the two
|
|
disagree, `SKILL.md` wins; fix this file.
|
|
|
|
### How to use this file
|
|
|
|
If you are about to modify the skill:
|
|
|
|
1. Skim `§1. System context` to recall the flow.
|
|
2. Find the step you want to change in `§4. Design decisions` — each
|
|
decision lists which `SKILL.md` step it ties into.
|
|
3. Check `§2. Codex CLI empirical facts` for the environment
|
|
assumptions you are about to lean on. If the date in `§8. Version
|
|
and verification log` is older than a few Codex releases, re-verify
|
|
before trusting.
|
|
4. Run `§7. Smoke test protocol` before and after your change. If the
|
|
before-run already fails, stop and investigate — don't layer a
|
|
change on top of undetected drift.
|
|
|
|
---
|
|
|
|
## §1. System context
|
|
|
|
### One paragraph
|
|
|
|
`adversarial-review` is a Claude Code skill. The user types
|
|
`/adversarial-review` in a Claude Code session; Claude (the "lead")
|
|
writes an adversarial review prompt to a temp file, launches
|
|
`codex exec` as a subprocess with the prompt on stdin, and captures the
|
|
reviewer's response to a `-o` file. Claude then shows the review to the
|
|
user verbatim, applies fixes to the plan or code, and re-submits the
|
|
revised state to the reviewer through `codex exec resume`. The loop
|
|
runs up to five rounds, or until the reviewer emits
|
|
`VERDICT: APPROVED`.
|
|
|
|
### Roles
|
|
|
|
- **Lead (Claude).** Orchestrator. Reads `SKILL.md`, runs the Bash /
|
|
Write / Read / Edit tools, authors the fixes, decides when to stop.
|
|
- **Reviewer (Codex).** External AI process invoked per round. Receives
|
|
the adversarial prompt, reads repo/plan content in a read-only
|
|
sandbox, emits a structured review with `VERDICT:`.
|
|
- **User.** Reads the verbatim review Claude shows each round,
|
|
accepts/rejects the skill's final result.
|
|
|
|
### Flow
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[launch: codex exec --json] --> B{checks: exit, stderr, review file}
|
|
B -- fail --> RETRY{retry budget}
|
|
RETRY -- yes --> A
|
|
RETRY -- no --> ABORT([abort, leave temp files])
|
|
B -- ok --> SHOW[show review verbatim to user]
|
|
SHOW --> V{VERDICT}
|
|
V -- APPROVED --> APPROVED([Step 8: approved])
|
|
V -- max rounds --> MAX([Step 8: max reached])
|
|
V -- REVISE --> FIX[apply fixes]
|
|
FIX --> RESUME[resume: codex exec resume --json]
|
|
RESUME --> C{checks: exit, stderr, review file}
|
|
C -- ok --> SHOW
|
|
C -- fail --> FB{fallback}
|
|
FB -- interactive --> ASK[ask user: fresh exec or conclude]
|
|
FB -- headless --> SEV{max severity}
|
|
ASK -- fresh --> FRESH[fresh codex exec + conv history]
|
|
ASK -- conclude --> NOTVER([Step 8: NOT VERIFIED])
|
|
SEV -- critical/high --> FRESH
|
|
SEV -- medium only --> NOTVER
|
|
FRESH --> B
|
|
```
|
|
|
|
The diagram collapses retries and round counting; for the exact
|
|
ordering see the strict check lists in `SKILL.md` Steps 4 and 7.
|
|
|
|
### Why an external reviewer at all
|
|
|
|
An adversarial review from the *same* model as the writer tends toward
|
|
validation bias. Running the review through a different model family
|
|
(GPT via Codex CLI) reduces shared blind spots. The cost is an external
|
|
dependency and a CLI-level integration — which is exactly what most of
|
|
this document exists to manage.
|
|
|
|
---
|
|
|
|
## §2. Codex CLI empirical facts (verified on v0.121.0)
|
|
|
|
All facts in this section were verified on `codex-cli 0.121.0` on
|
|
2026-04-17 (see `§8. Version and verification log`). If you are reading
|
|
this more than a few Codex releases later, re-verify before relying on
|
|
a specific behavior. `§7. Smoke test protocol` is a minimal suite you
|
|
can run in a few minutes.
|
|
|
|
### §2.1. Invocation shapes
|
|
|
|
Two relevant subcommands:
|
|
|
|
```
|
|
codex exec [OPTIONS] [PROMPT]
|
|
codex exec resume [OPTIONS] [SESSION_ID] [PROMPT]
|
|
```
|
|
|
|
Both accept PROMPT either as a trailing argument or as stdin. Two stdin
|
|
shapes are accepted by codex itself:
|
|
|
|
```bash
|
|
# A. stdin redirect from file
|
|
codex exec ... - < /tmp/codex-prompt-*.md
|
|
|
|
# B. pipe through cat
|
|
cat /tmp/codex-prompt-*.md | codex exec ... -
|
|
```
|
|
|
|
**Both shapes work in some environments; only (B) works reliably in all
|
|
observed Claude Code sandboxes.** In at least one sandbox configuration,
|
|
form (A) exits 1 with empty stderr (no codex error message) while form
|
|
(B) produces an identical `-o` review file. We do not have a root-cause
|
|
diagnosis for (A)'s failure — it is consistent across codex 0.120.0 and
|
|
0.121.0 in the affected environment, so it is not a codex version issue.
|
|
See `§6.6` for the observation log.
|
|
|
|
The skill therefore uses form (B) as canonical (`§4.1` captures the
|
|
decision). Long XML prompts still need file delivery to avoid shell
|
|
quoting issues, so the file is written via the Write tool and fed
|
|
through `cat | ... -` rather than embedded as a command-line argument.
|
|
|
|
### §2.2. Output streams
|
|
|
|
Two output modes, with different stream semantics:
|
|
|
|
**Default mode (no `--json`):**
|
|
|
|
- stdout: only the final agent message, plain text. Identical to what
|
|
`-o FILE` writes to disk.
|
|
- stderr: a metadata block printed before the agent response, followed
|
|
by a tail including token usage. The metadata block includes the
|
|
line `session id: <uuid>`.
|
|
|
|
Verify:
|
|
|
|
```bash
|
|
echo "respond PONG" | codex exec -m gpt-5.4 -s read-only \
|
|
-C "$(git rev-parse --show-toplevel)" - > /tmp/a.out 2> /tmp/a.err
|
|
cat /tmp/a.err | grep 'session id:'
|
|
cat /tmp/a.out
|
|
```
|
|
|
|
**JSON mode (`--json`):**
|
|
|
|
- stdout: newline-delimited JSONL events. The first event is always
|
|
`{"type":"thread.started","thread_id":"<uuid>", ...}`. Subsequent
|
|
events are `turn.started`, `item.completed`, `turn.completed`, etc.
|
|
- stderr: empty on success. Populated only on errors.
|
|
|
|
Verify:
|
|
|
|
```bash
|
|
echo "respond PONG" | codex exec --json -m gpt-5.4 -s read-only \
|
|
-C "$(git rev-parse --show-toplevel)" - > /tmp/a.out 2> /tmp/a.err
|
|
head -1 /tmp/a.out # expect {"type":"thread.started",...}
|
|
wc -c /tmp/a.err # expect 0
|
|
```
|
|
|
|
**Environment-specific suppression.** In at least one observed Claude
|
|
Code sandbox, `--json` stdout is empty (0 bytes) when redirected to a
|
|
file, even though the `-o` path is populated correctly and the process
|
|
exits 0. The `-o` file has the review, stderr is empty, only stdout
|
|
JSONL is missing. This is reproducible across codex 0.120.0 and 0.121.0
|
|
in that environment and not reproducible in the reference environment.
|
|
Root cause is not diagnosed; see `§6.6` for the observation log. The
|
|
skill handles it with a secondary filesystem-based session-id capture
|
|
path (`§4.1b`) so resume still works.
|
|
|
|
**`-o FILE` flag:**
|
|
|
|
Writes the final agent message to FILE as plain text, *regardless* of
|
|
whether `--json` is set. In JSON mode, this is the only way to get a
|
|
human-readable review — stdout is machine-readable events only.
|
|
|
|
### §2.3. Session persistence
|
|
|
|
Every non-ephemeral `codex exec` run creates a rollout file:
|
|
|
|
```
|
|
~/.codex/sessions/YYYY/MM/DD/rollout-TIMESTAMP-UUID.jsonl
|
|
```
|
|
|
|
Filename format: `rollout-<ISO-8601 timestamp with dashes>-<UUID>.jsonl`.
|
|
The UUID is the session / thread id and is accepted verbatim by
|
|
`codex exec resume`.
|
|
|
|
`--ephemeral` disables persistence. Not used by the skill — resume
|
|
requires persistence.
|
|
|
|
**Session-id recovery from the filesystem.** Because the UUID is a
|
|
deterministic suffix of the filename, session id can be recovered from
|
|
disk after the fact, independent of whether `--json` emitted the
|
|
`thread.started` event to stdout. The skill uses this as a secondary
|
|
capture path (`§4.1b`) when stdout is empty.
|
|
|
|
Verify:
|
|
|
|
```bash
|
|
BEFORE=$(date +%s)
|
|
echo "respond PONG" | codex exec -m gpt-5.4 -s read-only \
|
|
--skip-git-repo-check -o /tmp/x.md - >/dev/null 2>&1
|
|
find ~/.codex/sessions -name 'rollout-*.jsonl' -newermt "@${BEFORE}" \
|
|
| sort | tail -1 | xargs -n1 basename \
|
|
| grep -oE '[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}'
|
|
# expect: a UUID, and that UUID accepted by `codex exec resume`
|
|
```
|
|
|
|
### §2.4. Resume semantics
|
|
|
|
```
|
|
codex exec resume [OPTIONS] [SESSION_ID] [PROMPT]
|
|
codex exec resume --last # pick newest in cwd
|
|
```
|
|
|
|
**Supported flags (per `codex exec resume --help`):** `--json`, `-o`,
|
|
`-m`, `-i`, `--last`, `--all`.
|
|
|
|
**NOT supported:** `-s` (sandbox inherited from original session; always
|
|
`read-only` in this skill), `-C` / `--cd` (cwd is whatever the shell
|
|
had when `codex` was invoked — see §2.4.2).
|
|
|
|
**§2.4.1. `--last` filters by cwd.** `resume --last` only considers
|
|
sessions whose rollout records the current cwd. Invoking from one repo
|
|
cannot pick up a session in another repo. But within the same cwd, it
|
|
picks the *newest* session regardless of origin — including one-shots,
|
|
unrelated tool invocations, or user-initiated codex work happening in
|
|
parallel.
|
|
|
|
Verify:
|
|
|
|
```bash
|
|
mkdir -p /tmp/cwd-a /tmp/cwd-b
|
|
cd /tmp/cwd-a && git init -q
|
|
cd /tmp/cwd-b && git init -q
|
|
cd /tmp/cwd-a && echo "respond ALPHA" | codex exec -s read-only \
|
|
--skip-git-repo-check - 2>&1 | grep 'session id:'
|
|
cd /tmp/cwd-b && echo "respond BRAVO" | codex exec -s read-only \
|
|
--skip-git-repo-check - 2>&1 | grep 'session id:'
|
|
cd /tmp/cwd-a && echo "which?" | codex exec resume --last \
|
|
--skip-git-repo-check - 2>&1 | tail -5
|
|
# expect: resumes ALPHA session, not BRAVO
|
|
```
|
|
|
|
**§2.4.2. cwd is inherited from the shell, not the rollout.** The
|
|
original `codex exec -C /repo ...` pins the session to `/repo` for its
|
|
initial turn. A subsequent `codex exec resume <UUID>` from a *different*
|
|
cwd does NOT inherit `/repo`. It either fails with
|
|
`Not inside a trusted directory` (exit 1, no `-o` written), or silently
|
|
runs with the new cwd. The skill works around this with `cd '<REPO_ROOT>' &&`
|
|
as a command prefix before every resume (see `§4.3`).
|
|
|
|
**§2.4.3. Bad UUID exit code.** `codex exec resume <non-existent-uuid>`
|
|
exits with code **1** (not 0). stderr contains:
|
|
|
|
```
|
|
Error: thread/resume: thread/resume failed: no rollout found for thread id <uuid>
|
|
```
|
|
|
|
stdout is empty. `-o` file is NOT created.
|
|
|
|
Verify:
|
|
|
|
```bash
|
|
echo "hi" | codex exec resume 00000000-0000-0000-0000-000000000000 \
|
|
--skip-git-repo-check - 2>/tmp/e.err
|
|
echo "EXIT=$?" # expect EXIT=1
|
|
cat /tmp/e.err # expect "thread/resume failed..." line
|
|
```
|
|
|
|
**§2.4.4. Thread ID does not rotate across successful resumes.** Each
|
|
successful `codex exec resume --json` emits a `thread.started` event
|
|
whose `thread_id` equals the original session's UUID. It is *not* a new
|
|
id. The skill's rule "update `CODEX_SESSION_ID` on every successful
|
|
resume" therefore is functionally a no-op on the current version, but
|
|
remains defensive for future Codex versions that might rotate ids.
|
|
|
|
Verify: compare the UUID in the first `thread.started` event of an
|
|
initial `codex exec --json` with the UUID in the first `thread.started`
|
|
event of `codex exec resume --json <that-UUID>`. Should be identical.
|
|
|
|
### §2.5. Known failure modes
|
|
|
|
| Trigger | Exit code | stdout | stderr | `-o` file |
|
|
|---|---|---|---|---|
|
|
| Success (reference env) | 0 | final text / JSONL | empty (json) or metadata+token block (non-json) | written |
|
|
| Success (affected sandbox, §6.6) | 0 | empty (`--json` suppressed) | empty | written |
|
|
| Timeout (wrapped `timeout 600`) | 124 | partial or empty | partial | may be missing or partial |
|
|
| Model not available (`-m bogus`) | 1 | empty | error line | not written |
|
|
| `-o` path unwritable | 0 | final text / JSONL | `Failed to write last message file ...` line | not written |
|
|
| Not in git work tree, no `--skip-git-repo-check` | 1 | empty | `Not inside a trusted directory ...` | not written |
|
|
| Resume with bad UUID | 1 | empty | `thread/resume failed ...` | not written |
|
|
| `- < file` stdin redirect (affected sandbox, §6.6) | 1 | empty | empty (!) | not written |
|
|
|
|
The `-o` unwritable case is dangerous: exit code is misleading. The
|
|
skill defends by always reading stderr even on exit 0 (see `§4.8`).
|
|
The `- < file` exit-1-with-empty-stderr case is why the skill uses
|
|
`cat | pipe` instead (see `§4.13`).
|
|
|
|
### §2.6. CLI gaps relevant to the skill
|
|
|
|
- `codex exec resume` does not accept `-C`.
|
|
- `codex exec resume` did not accept `-o` before issue openai/codex#12538
|
|
was resolved. On current versions it does.
|
|
- `codex --version` prints `codex-cli 0.121.0` — not empty, despite one
|
|
earlier agent's diagnostic claim (see `§6`).
|
|
|
|
---
|
|
|
|
## §3. Claude Code / harness facts
|
|
|
|
These facts apply to the Claude Code runtime (the "harness") that
|
|
executes the skill, verified during work on this refactor.
|
|
|
|
### §3.1. Bash tool
|
|
|
|
- Returns combined stdout + stderr as the tool result text.
|
|
- Truncates output at approximately 30 KB. Under truncation, the *tail*
|
|
is retained; the *head* is dropped. This is why Codex's `session id:`
|
|
metadata line (printed before a potentially long reasoning trace) can
|
|
disappear from the Bash result under load — the real root of the old
|
|
session-id bug, not anything wrong with Codex.
|
|
- Current working directory is treated as transient between calls. Do
|
|
not depend on `cd` persisting, and do not rely on `$(pwd)` in
|
|
composed commands; capture the absolute path once via a deterministic
|
|
source (see `§4.2`).
|
|
|
|
### §3.2. Write and Read tools
|
|
|
|
- Bypass the Bash truncation limit entirely. Prompts longer than a few
|
|
kilobytes should be written to a file and fed to the external
|
|
subprocess via pipe (`cat file | cmd -`) rather than embedded as a
|
|
Bash argument. See `§4.13` for why pipe is preferred over the
|
|
`- < file` redirect form.
|
|
- Read can open any file — there is no skill-level restriction.
|
|
|
|
### §3.3. Safety rules on destructive git operations
|
|
|
|
The harness includes a built-in "Git Push to Default Branch" safety
|
|
rule that blocks `git push origin master` (or `main`) unless the user
|
|
has explicitly granted a permission. The skill does not push, so this
|
|
does not affect runtime, but it is relevant when releasing skill
|
|
changes: maintainer must push master themselves or add a permission.
|
|
|
|
### §3.4. Opus interpretation of instructions
|
|
|
|
Later Opus releases tend to interpret SKILL.md instructions more
|
|
literally than earlier ones. An instruction like "Show the user
|
|
verbatim" without a hard procedural anchor can be internally
|
|
rationalized as "the context already contains the review, the user
|
|
sees the context" and skipped. Current design compensates with:
|
|
|
|
- A strict "YOUR NEXT MESSAGE to the user must begin with ..." clause
|
|
(`SKILL.md` Step 5) that names the message, not just the act.
|
|
- Architectural enforcement: the `--json` invocation puts machine-
|
|
readable JSONL in stdout, so the review text exists only in the
|
|
`-o` file. The lead *cannot* satisfy the "show the review" contract
|
|
by quoting from the Bash result, because the Bash result has no
|
|
review text in the first place.
|
|
|
|
See `§4.9` for the rejected-marker-file alternative.
|
|
|
|
---
|
|
|
|
## §4. Design decisions
|
|
|
|
Each decision below follows the same template:
|
|
|
|
- **Decision** — one sentence.
|
|
- **Where in SKILL.md** — step reference.
|
|
- **Context** — the problem it addresses.
|
|
- **Alternatives considered** — with reasons for rejection.
|
|
- **Chosen because** — the load-bearing argument.
|
|
- **Trade-offs accepted** — what we gave up.
|
|
|
|
### §4.1. Two-tier session ID capture (`--json` primary, rollout filename secondary)
|
|
|
|
- **Decision.** Every `codex exec` and `codex exec resume` invocation
|
|
uses `--json` with stdout redirected to
|
|
`/tmp/codex-stdout-${REVIEW_ID}.jsonl`. Session ID capture tries
|
|
**primary first, then secondary**:
|
|
- **Primary** (`§4.1a`): parse `thread_id` from the first line of
|
|
JSONL stdout.
|
|
- **Secondary** (`§4.1b`): if stdout is empty, extract the UUID from
|
|
the filename of the newest `~/.codex/sessions/**/rollout-*.jsonl`
|
|
with `mtime > CODEX_SESSIONS_BEFORE` (a timestamp captured
|
|
immediately before the exec).
|
|
- **Where in SKILL.md.** Step 4 (launch), Step 7 (resume). Both tiers
|
|
live inline in each Step.
|
|
- **Context.** The primary path covers the reference environment
|
|
cleanly (JSONL events reliably land in the redirected file). In at
|
|
least one Claude Code sandbox, the JSONL stdout is suppressed (0
|
|
bytes) even on exit 0 and a populated `-o` review file (`§2.2`,
|
|
`§6.6`). Without a secondary path, the skill cannot resume in that
|
|
environment — every round would need a fresh `codex exec`, wasting
|
|
tokens on project re-reads. The filesystem path was previously
|
|
rejected (see "Alternatives considered" below) but the rejection
|
|
only applies when it is the *primary* capture; as a *secondary*
|
|
fallback its failure modes are acceptable.
|
|
- **Alternatives considered.**
|
|
- *Keep parsing `session id:` from stderr.* Rejected: Bash tool
|
|
truncates output at ~30 KB from the head (§3.1); long reasoning
|
|
traces pushed the session-id line out of the retained window.
|
|
(Historical reason for moving to `--json` in the first place.)
|
|
- *Redirect stderr to a file, Read via Read tool.* Rejected: an
|
|
earlier skill version did exactly this and a diagnostic dump still
|
|
reported empty stderr files. §3.2 says Read bypasses Bash
|
|
truncation, so this might have been viable — but since `§4.1a`
|
|
covers the reference environment and `§4.1b` covers the sandboxed
|
|
one, adding a third path is not worth the complexity.
|
|
- *Use only the filesystem path as the single source.* Rejected:
|
|
relies on a filesystem race window against any parallel codex
|
|
invocation in the same second. As a secondary (only consulted
|
|
when stdout is empty) the race is rare, but — honestly — NOT
|
|
auto-detectable by the skill: a wrong-session resume returns a
|
|
normally-shaped review (VERDICT + `[severity:` markers), so the
|
|
Step 7 post-resume checks (`§4.8`) pass and the skill applies
|
|
fixes based on an unrelated artifact. The `CODEX_SESSIONS_BEFORE`
|
|
timestamp narrows the race window to seconds, but does not
|
|
eliminate it. Risk is acknowledged in `SKILL.md` Step 4 check 4
|
|
("Parallel-codex caveat") as a silent-corruption hazard, not a
|
|
fallback-handled hazard.
|
|
- *Drop `--json` entirely and use plain-text stdout.* Rejected:
|
|
`--json` makes stdout machine-readable only, which is *load-
|
|
bearing* for the show-review gate (`§4.9`). Plain-text stdout
|
|
would re-expose the "Opus sees the review in Bash result, skips
|
|
the user-visible show step" failure mode.
|
|
- **Chosen because.** Two-tier keeps primary cheap and documented on
|
|
the Codex side (the `thread.started` event is in the CLI contract),
|
|
while the secondary isolates the skill from env-specific stdout
|
|
quirks we cannot control (`§6.6`). Neither tier alone covers both
|
|
observed environments; together they do.
|
|
- **Trade-offs accepted.**
|
|
- Human-readable review is no longer in stdout (it went to `-o`
|
|
only) — load-bearing for `§4.9`.
|
|
- Secondary path introduces a filesystem race against parallel
|
|
codex invocations (§9.1 scope). Mitigated (not eliminated) by
|
|
the pre-exec timestamp (`CODEX_SESSIONS_BEFORE=$(($(date +%s) -
|
|
1))`) narrowing the window to "files created within ~1-2 seconds
|
|
of the exec start". The `-1` shift against `-newermt`'s strict-
|
|
greater semantics prevents same-epoch miss; the race window is
|
|
one second wider as a result, still negligible compared to a
|
|
real codex exec duration.
|
|
- Session-id capture happens only after review-file sanity passes
|
|
AND only when verdict is `REVISE` (Step 4 check order in
|
|
`SKILL.md`). This avoids aborting a valid round-1 APPROVED over
|
|
a secondary-capture failure: APPROVED means no resume, no
|
|
session-id needed.
|
|
- Extra permission surface: `Bash(find ...)` is now in the
|
|
recommended permissions list.
|
|
|
|
### §4.2. Capture `REPO_ROOT` at Step 2, substitute literally
|
|
|
|
- **Decision.** At Step 2, run `git rev-parse --show-toplevel` once,
|
|
save the absolute path as `REPO_ROOT`, and substitute it verbatim
|
|
(quoted) into every Codex command. Do not use `$(pwd)` in composed
|
|
commands.
|
|
- **Where in SKILL.md.** Step 2 (capture), Steps 4 and 7 (use).
|
|
- **Context.** Codex commands need a stable cwd: `-C` for initial exec,
|
|
`cd '...' &&` prefix for resume. If the cwd is evaluated at Bash-call
|
|
time via `$(pwd)`, it is susceptible to Claude Code's weak
|
|
cwd-persistence between calls (§3.1).
|
|
- **Alternatives considered.**
|
|
- *`$(pwd)` everywhere.* Rejected: cwd drift.
|
|
- *`pwd -P` at each call.* Same problem, plus added complexity.
|
|
- **Chosen because.** One capture, many uses, all deterministic.
|
|
- **Trade-offs accepted.** Requires error handling for bare repos
|
|
(exit 128) and awareness of submodule scoping — the skill aborts on
|
|
bare repos with a clear message and warns on submodules (see
|
|
`SKILL.md` Step 2).
|
|
|
|
### §4.3. Hybrid cwd pinning: `-C` for initial, `cd` prefix for resume
|
|
|
|
- **Decision.** Initial `codex exec` uses `-C "${REPO_ROOT}"`. Resume
|
|
uses a shell prefix: `cd '${REPO_ROOT}' && codex exec resume ...`.
|
|
- **Where in SKILL.md.** Steps 4 and 7.
|
|
- **Context.** Codex's `exec` accepts `-C`; `resume` does not. Resume
|
|
inherits cwd from the invoking shell.
|
|
- **Alternatives considered.**
|
|
- *Use `cd` prefix for both.* Rejected: `-C` is more precise for
|
|
initial (it is the documented way to pin), and uses less
|
|
permissions surface.
|
|
- *Rely on the harness's ambient cwd.* Rejected: see §3.1 —
|
|
insufficiently reliable.
|
|
- **Chosen because.** This is the minimal-surgery solution that matches
|
|
what each subcommand actually supports.
|
|
- **Trade-offs accepted.** Two slightly different command shapes in
|
|
`SKILL.md`; one extra permission pattern in README
|
|
(`Bash(cd * && timeout 600 codex exec resume *)`).
|
|
|
|
### §4.4. Conditional `CODEX_SESSION_ID` update (only on full success)
|
|
|
|
- **Decision.** Update `CODEX_SESSION_ID` from the JSONL stdout's first
|
|
line only when ALL of the following hold: exit code 0, stderr has no
|
|
`Error:` or `thread/resume failed` line, `-o` file contains a valid
|
|
`VERDICT:` line (and, for REVISE, at least one `[severity:` marker).
|
|
- **Where in SKILL.md.** Step 7.
|
|
- **Context.** A failed resume (bad model, model error, infrastructure
|
|
failure) still emits a `thread.started` event with a fresh but dead
|
|
`thread_id`. An unconditional update would poison the session id and
|
|
cause subsequent resumes to fail against a non-existent session.
|
|
- **Alternatives considered.**
|
|
- *Unconditional update.* Rejected: demonstrated poisoning on bad
|
|
model invocations during review.
|
|
- *Update on exit 0 only.* Rejected: `-o` unwritable returns exit 0
|
|
with a broken session; stderr error line needs checking too.
|
|
- **Chosen because.** All three checks together give a reliable "the
|
|
session actually produced a review" signal.
|
|
- **Trade-offs accepted.** More conditions to specify and execute, but
|
|
they are already required for the review-file sanity check — marginal
|
|
cost.
|
|
|
|
### §4.5. No `--last` in any fallback
|
|
|
|
- **Decision.** The fallback chain does not use `codex exec resume --last`.
|
|
On resume failure the skill either asks the user (interactive) or
|
|
chooses by severity (headless); the "retry" option is always a fresh
|
|
`codex exec`, not `--last`.
|
|
- **Where in SKILL.md.** Step 7 fallback.
|
|
- **Context.** `--last` silently picks the newest session in the
|
|
current cwd (§2.4.1), which can be an unrelated one-shot or a
|
|
parallel user invocation.
|
|
- **Alternatives considered.**
|
|
- *`--last` as first fallback.* Rejected: wrong-session hazard; if
|
|
the user happens to be running Codex interactively in the same
|
|
repo, the skill's "I've revised based on your feedback ..."
|
|
message would be injected into the user's unrelated work.
|
|
- **Chosen because.** The safety failure mode is catastrophic (silent
|
|
incorrect reviews, context injection into user's sessions); the cost
|
|
of skipping this shortcut is modest (one more fresh exec per failure,
|
|
which is rare).
|
|
- **Trade-offs accepted.** Slightly higher token cost on the rare
|
|
resume-failure path.
|
|
|
|
### §4.6. Fresh-exec fallback rebuilds context from conversation history
|
|
|
|
- **Decision.** When the fallback triggers a fresh `codex exec`, Claude
|
|
reconstructs the "previous rounds" section of the prompt from the
|
|
conversation — the round-1 review, round-1 fixes, round-2 review,
|
|
round-2 fixes, etc. — which were already shown verbatim to the user
|
|
in earlier Step 5 outputs.
|
|
- **Where in SKILL.md.** Step 7 fallback prompt template.
|
|
- **Context.** The `-o` file at `/tmp/codex-review-${REVIEW_ID}.md` is
|
|
overwritten on every round. Round-1 review content is gone from disk
|
|
by the time a round-3 fallback triggers.
|
|
- **Alternatives considered.**
|
|
- *Per-round file naming* (`-r1.md`, `-r2.md`, ...). Rejected: user
|
|
preference for minimizing file proliferation. Also required
|
|
matching changes in Step 9 cleanup globs.
|
|
- *Archive previous -o file before overwrite.* Rejected: adds
|
|
complexity (pre-write copy step) for a case that triggers rarely.
|
|
- **Chosen because.** The Step 5 "show review verbatim" contract
|
|
already ensures the content is in conversation history. Leveraging
|
|
that makes a new step unnecessary.
|
|
- **Trade-offs accepted.** Depends on the conversation context window
|
|
preserving prior outputs. If Claude Code compacts the context
|
|
mid-review, the fallback template may be degraded. No mitigation
|
|
currently; see `§9. Known limitations`.
|
|
|
|
### §4.7. Semantic VERDICT + findings check (no byte threshold)
|
|
|
|
- **Decision.** Step 5 sanity checks the review file by regex only. The
|
|
file must contain a line matching `^VERDICT: (APPROVED|REVISE)$`; if
|
|
the verdict is REVISE, it must also contain at least one line
|
|
matching `\[severity:\s*(critical|high|medium)`.
|
|
- **Where in SKILL.md.** Step 5.
|
|
- **Context.** An earlier draft used a byte-size threshold (`< 50
|
|
bytes = launch failure`). A legitimate terse approval (`VERDICT:
|
|
APPROVED\n`, 17 bytes) would be misclassified. Worse, a REVISE
|
|
verdict with no findings would pass a byte check but create an
|
|
infinite-loop hazard: nothing to fix, resubmit empty fixes, same
|
|
verdict, loop until max rounds.
|
|
- **Alternatives considered.**
|
|
- *Byte threshold.* Rejected as above.
|
|
- *Require only `VERDICT:`.* Rejected: REVISE-without-findings loop.
|
|
- **Chosen because.** Semantics over heuristic; catches both the
|
|
terse-approval false positive and the empty-REVISE infinite loop.
|
|
- **Trade-offs accepted.** Depends on the reviewer emitting the
|
|
`[severity:` marker format prescribed by the prompt. Enforced via
|
|
prompt wording; prompt drift would be a separate failure mode (see
|
|
§6).
|
|
|
|
### §4.8. Strict check order: exit → stderr → review → (session-id when needed)
|
|
|
|
- **Decision.** After every `codex exec` / `codex exec resume` call,
|
|
checks run in a fixed order:
|
|
1. Exit code.
|
|
2. Stderr file (even on exit 0).
|
|
3. `-o` review file semantic sanity.
|
|
4. Session-id capture (two-tier, `§4.1`). In Step 4 this is skipped
|
|
on `VERDICT: APPROVED` — no resume will happen, so no session
|
|
is needed. In Step 7 it is a defensive refresh (thread id
|
|
doesn't rotate per `§2.4.4`) and is skipped identically on
|
|
APPROVED.
|
|
- **Where in SKILL.md.** Steps 4 and 7 post-launch.
|
|
- **Context.** Non-zero exit implies `-o` may not exist; reading it
|
|
would crash. Exit 0 does not imply everything is fine (e.g., `-o`
|
|
unwritable case, §2.5). Review-sanity before session-id keeps the
|
|
two concerns orthogonal: a broken review aborts on its own merits,
|
|
and a valid APPROVED review completes without depending on
|
|
session-id capture. An earlier draft ordered session-id *before*
|
|
review-sanity, which meant a secondary-capture failure (e.g.,
|
|
empty `~/.codex/sessions/` on a first-ever codex run, or a super-
|
|
fast codex exec hitting the `-newermt` same-epoch edge) would
|
|
abort an otherwise-successful APPROVED round. The current order
|
|
avoids that.
|
|
- **Alternatives considered.**
|
|
- *Ad-hoc checks in whatever order.* Rejected: invites null-pointer-
|
|
style crashes on missing files.
|
|
- *All-at-once check, aggregate errors.* Rejected: harder for Claude
|
|
to follow step-by-step; harder to recover partial information.
|
|
- **Chosen because.** Deterministic, fail-fast, each check's output
|
|
points to the next action.
|
|
- **Trade-offs accepted.** Slightly verbose to describe.
|
|
|
|
### §4.9. Hardened "show review" gate, no marker file
|
|
|
|
- **Decision.** The gate enforcing verbatim review output is
|
|
text-based, repeated in three places (Step 4 anti-confusion note,
|
|
Step 5 YOUR-NEXT-MESSAGE instruction, Step 6 precondition check),
|
|
and backstopped by the architectural fact that `--json` stdout is
|
|
non-human-readable so the review can only be accessed via Read on
|
|
the `-o` file.
|
|
- **Where in SKILL.md.** Steps 4, 5, 6.
|
|
- **Context.** Earlier design used only "Show the user verbatim" in
|
|
Step 5. Observed behavior: Opus received the review (began applying
|
|
fixes that referenced it) but never displayed it to the user. Soft
|
|
instructions can be internally rationalized away (§3.4).
|
|
- **Alternatives considered.**
|
|
- *Marker-file precondition* — Claude writes
|
|
`/tmp/codex-review-${REVIEW_ID}.shown` as the act of showing; Step
|
|
6 verifies the file exists. Rejected: cargo-cult risk. A literal
|
|
reader that skips the show step may still write the marker,
|
|
producing a false audit trail that is worse than no trail. And the
|
|
marker cannot be programmatically enforced from within SKILL.md
|
|
— it is still instructional.
|
|
- *Leave as soft "Show verbatim".* Rejected: the observed bug.
|
|
- **Chosen because.** The `--json` switch does more to prevent the bug
|
|
than any textual instruction: with stdout as JSONL events, there is
|
|
no review text in the Bash tool result for Opus to accidentally
|
|
consume instead of re-reading the `-o` file. The triple text gate
|
|
is belt-and-braces.
|
|
- **Trade-offs accepted.** Relies on model compliance with the text
|
|
gate. If a future Opus slips past all three, we will need another
|
|
architectural hook (possibly a verifiable marker after all).
|
|
|
|
### §4.10. Conditional cleanup (keep temp files on abort)
|
|
|
|
- **Decision.** Step 9 cleans up `/tmp/codex-*-${REVIEW_ID}.*` only on
|
|
success paths (APPROVED, MAX rounds, NOT VERIFIED). On abort paths
|
|
(launch failure, infrastructure error), the files are left in place.
|
|
- **Where in SKILL.md.** Step 9.
|
|
- **Context.** When the skill aborts, diagnostics live in the stderr
|
|
and stdout temp files. Cleaning them up immediately makes post-hoc
|
|
debugging impossible.
|
|
- **Alternatives considered.**
|
|
- *Always clean up.* Rejected: loses forensics on the cases you
|
|
most want forensics for.
|
|
- *Per-round naming to preserve history across rounds.* Rejected:
|
|
see §4.6.
|
|
- **Chosen because.** The abort path is rare and the files are small.
|
|
The trade-off between a few KB of residual `/tmp` content and the
|
|
ability to understand a failure is lopsided.
|
|
- **Trade-offs accepted.** Residual files accumulate in `/tmp`. The OS
|
|
will clear them on reboot; a later successful invocation with the
|
|
same `REVIEW_ID` collides with probability ~10⁻⁸ per same-second
|
|
invocation (8-digit random).
|
|
|
|
### §4.11. Ask-user on resume failure in interactive mode; severity-based in headless
|
|
|
|
- **Decision.** When resume fails (one of the three checks in §4.8):
|
|
- Interactive: ask the user to choose between (a) fresh exec with
|
|
prior context, or (b) conclude the review as NOT VERIFIED.
|
|
- Headless: auto-fresh-exec if max severity of last round is
|
|
critical/high; auto-NOT-VERIFIED if only medium.
|
|
- **Where in SKILL.md.** Step 7 fallback.
|
|
- **Context.** Fresh exec is expensive (token-wise) and sometimes
|
|
unnecessary (if the last round's findings were medium, skipping
|
|
re-verification is often acceptable). An unconditional fresh exec
|
|
wastes tokens; an unconditional conclude-as-not-verified risks
|
|
shipping with a critical finding unaddressed.
|
|
- **Alternatives considered.**
|
|
- *Unconditional fresh exec.* Rejected: cost and user-interruption
|
|
model.
|
|
- *Unconditional NOT-VERIFIED.* Rejected: critical/high findings
|
|
could be silently skipped.
|
|
- *Always ask the user.* Rejected: no way to ask in headless
|
|
(scheduled, CI) contexts.
|
|
- **Chosen because.** Respects the user's "minimize interruptions"
|
|
preference while preserving correctness for serious findings. The
|
|
severity parsing uses a tight regex (§6's lesson on format drift);
|
|
zero matches defaults to critical for safety.
|
|
- **Trade-offs accepted.** Severity parsing is a soft dependency on
|
|
reviewer output format; see `§5` for alternatives considered.
|
|
|
|
### §4.12. Per-round retry budget separate from the round counter
|
|
|
|
- **Decision.** Step 5 launch-failure retry is capped at 1 per round
|
|
and does NOT consume the 5-round counter. The counter advances only
|
|
when a *valid* review (one that passes §4.7 checks) has been
|
|
produced.
|
|
- **Where in SKILL.md.** Step 5.
|
|
- **Context.** A launch failure is an infrastructure issue, not a
|
|
failed review. Counting it against the round budget would be
|
|
inappropriate — the user would get four rounds of review instead of
|
|
five because of a flaky codex launch.
|
|
- **Alternatives considered.**
|
|
- *Retry consumes a round.* Rejected: see above.
|
|
- *Unlimited retries.* Rejected: creates an infinite loop on
|
|
persistent infrastructure failure.
|
|
- **Chosen because.** The one-retry cap bounds the cost; the
|
|
separate-counter rule preserves the user's 5-round expectation.
|
|
- **Trade-offs accepted.** The retry counter lives in the lead's
|
|
round-local reasoning — if an implementer treats it as global across
|
|
rounds, retry budget is inconsistently available. Rules section of
|
|
`SKILL.md` states this explicitly.
|
|
|
|
### §4.13. Canonical prompt delivery via `cat file | codex exec -`
|
|
|
|
- **Decision.** The skill feeds prompts to `codex exec` via
|
|
`cat /tmp/codex-prompt-*.md | timeout 600 codex exec ... -` instead
|
|
of `codex exec ... - < /tmp/codex-prompt-*.md`.
|
|
- **Where in SKILL.md.** Step 4 (launch), Step 7 (resume), Step 7
|
|
fresh-exec fallback.
|
|
- **Context.** Both shapes are accepted by codex itself (`§2.1`). In
|
|
the reference environment they are interchangeable. In at least one
|
|
Claude Code sandbox, the `- < file` form exits 1 with empty stderr
|
|
— codex never actually runs, and without a stderr error line the
|
|
skill has nothing to diagnose. The `cat | pipe` form is unaffected
|
|
in the same sandbox and produces identical `-o` output.
|
|
- **Alternatives considered.**
|
|
- *Keep `- < file` as canonical.* Rejected: verified-broken in one
|
|
target environment.
|
|
- *Branch by environment (detect sandbox, switch form).* Rejected:
|
|
over-complicated for a Pareto case. `cat | pipe` is universal.
|
|
- *Use the trailing-argument form (`codex exec ... "$(cat file)"`).*
|
|
Rejected: shell quoting on long XML prompts is the exact problem
|
|
file delivery solves.
|
|
- **Chosen because.** `cat | pipe` works in every observed environment
|
|
at no extra cost. The only visible artifact is in the permission
|
|
rule (`Bash(cat /tmp/codex-prompt-* | timeout 600 codex exec *)`)
|
|
which is still prefix-matchable.
|
|
- **Trade-offs accepted.** One extra process (`cat`) per codex launch
|
|
— negligible. The pipeline exit code semantics are `$?` = last
|
|
command (codex), which matches what the skill already checks; no
|
|
`pipefail` needed.
|
|
|
|
---
|
|
|
|
## §5. Rejected ideas
|
|
|
|
Ideas that came up in adversarial review rounds or in exploration and
|
|
were rejected. Documenting them so a future contributor doesn't
|
|
re-propose them without reading why.
|
|
|
|
### §5.1. Marker file (`.shown` precondition)
|
|
|
|
Round 2 adversarial review proposed requiring the lead to write
|
|
`/tmp/codex-review-${REVIEW_ID}.shown` as an atomic "I have shown the
|
|
review" signal, with Step 6 hard-gating on its existence.
|
|
|
|
Rejected: the marker is itself an instructional artifact. A literal
|
|
reader who skips the show step may also write the marker, producing a
|
|
false audit trail — strictly worse than a visible skip, because it
|
|
masquerades as compliance. The real fix was architectural (`--json`
|
|
making review text inaccessible from Bash output, forcing a Read).
|
|
|
|
### §5.2. Per-round file naming
|
|
|
|
Proposed during Round 2 review: name temp files with both `${REVIEW_ID}`
|
|
and round number (`/tmp/codex-review-${REVIEW_ID}-r1.md`,
|
|
`...-r2.md`). Benefits: prior-round diagnostics survive later rounds;
|
|
fresh-exec fallback can read prior review text from disk.
|
|
|
|
Rejected: user preference for minimizing file proliferation; the Step
|
|
5 verbatim-output contract already places prior reviews in the
|
|
conversation history, which is where `§4.6` draws from.
|
|
|
|
### §5.3. Stderr parsing for session ID
|
|
|
|
The historical approach. Works in non-`--json` mode (the `session id:`
|
|
line is reliably printed to stderr), but fragile under Bash tool
|
|
output truncation (§3.1). Replaced by `--json` + first-line JSONL
|
|
parsing (§4.1).
|
|
|
|
### §5.4. `$(pwd)` in composed commands
|
|
|
|
An intuitive but fragile shortcut. Claude Code's Bash tool does not
|
|
persist cwd reliably between calls, so `$(pwd)` can resolve to an
|
|
unexpected directory. Replaced by the single-capture `REPO_ROOT`
|
|
pattern (§4.2).
|
|
|
|
### §5.5. Auto-fresh-exec without user consent on resume failure
|
|
|
|
Earlier skill drafts silently ran a fresh `codex exec` whenever resume
|
|
failed. This burned significant tokens (each fresh exec is a full
|
|
project re-read) for cases that sometimes were not worth re-verifying
|
|
(e.g., only medium-severity findings). Replaced by ask-user /
|
|
severity-based fallback (§4.11).
|
|
|
|
### §5.6. Always cleaning up temp files on exit
|
|
|
|
Earlier Step 9 unconditionally ran `rm -f` on all temp files at
|
|
end-of-run. That erased diagnostic trail for abort paths. Replaced by
|
|
conditional cleanup (§4.10).
|
|
|
|
---
|
|
|
|
## §6. Prior diagnostic errors and lessons
|
|
|
|
During the refactor that produced the current design, a diagnostic
|
|
dump from an earlier agent-auditor contained several assertions that
|
|
turned out to be wrong when verified empirically. We document them
|
|
here as a lesson, not as blame.
|
|
|
|
### §6.1. "Codex stderr is 0 bytes — no `session id:` line is printed"
|
|
|
|
**Claim.** The auditor's dump claimed that redirecting stderr to a file
|
|
during `codex exec` produced an empty file, across eight observed
|
|
invocations.
|
|
|
|
**Reality.** In non-`--json` mode, stderr contains a multi-line
|
|
metadata block that includes `session id: <uuid>`. In `--json` mode,
|
|
stderr is empty on success. The auditor probably tested in `--json`
|
|
mode and generalized.
|
|
|
|
**Impact on the design.** We initially believed stderr was unreliable
|
|
in general. Empirical verification showed stderr is reliable — it is
|
|
*Bash tool truncation* that makes stderr parsing unreliable (§3.1).
|
|
Different root cause; different fix (`--json` for first-line parsing,
|
|
§4.1).
|
|
|
|
### §6.2. "`codex --version` prints nothing"
|
|
|
|
**Claim.** The binary was said to emit no version string.
|
|
|
|
**Reality.** `codex --version` prints `codex-cli 0.121.0`.
|
|
|
|
**Impact on the design.** Minor — but indicative of measurement sloppiness.
|
|
|
|
### §6.3. "`codex exec resume --last` is unsafe"
|
|
|
|
**Claim.** The dump implied `--last` picks an arbitrary session.
|
|
|
|
**Reality.** `--last` filters by cwd but picks the newest session in
|
|
that cwd (§2.4.1). So it is "unsafe" in a narrower, more specific sense
|
|
than stated — it is safe across repos, unsafe within the same repo
|
|
against parallel or unrelated one-shots. The distinction matters: the
|
|
fix (drop `--last` entirely, §4.5) was driven by the specific risk of
|
|
context injection into user's parallel codex sessions, not by any
|
|
general unsafety.
|
|
|
|
### §6.4. "`codex exec resume <bad-uuid>` exits with code 0"
|
|
|
|
**Claim.** The dump (and the first round of adversarial review of our
|
|
own plan) both asserted that `codex exec resume` with an invalid UUID
|
|
exits 0 — making exit code useless as a success signal.
|
|
|
|
**Reality.** It exits **1**. stderr has `thread/resume failed`. stdout
|
|
is empty. Exit code is a reliable signal.
|
|
|
|
**Impact on the design.** We originally planned an elaborate stderr-
|
|
error-line check as the primary fallback trigger. That check is still
|
|
present in §4.8 as a defense against future Codex versions, but on the
|
|
current version exit code alone is sufficient.
|
|
|
|
### §6.5. Lesson
|
|
|
|
Single-source diagnostic reports are hypothesis, not fact. Every
|
|
load-bearing claim should be verified empirically before a design
|
|
decision is built on top of it. This document's `§2` and `§7` are
|
|
structured so future contributors can replicate the verification in
|
|
minutes, not hours.
|
|
|
|
### §6.6. 2026-04-17: Environment-specific stdout suppression, not a version bug
|
|
|
|
**Claim trajectory.** An agent running in a different Claude Code
|
|
sandbox reported three issues with the skill:
|
|
|
|
1. `codex exec ... - < /tmp/prompt.md` exits 1 with empty stderr.
|
|
2. `cat file | codex exec --json ... -` exits 0 with review file OK
|
|
but `/tmp/codex-stdout-*.jsonl` empty (0 bytes).
|
|
3. Same symptoms under `timeout 120 bash -c '...'` wrapper.
|
|
|
|
Initial hypothesis: codex version bug. The agent was on 0.120.0,
|
|
reference environment on 0.121.0. Upgrading the agent to 0.121.0 left
|
|
all three symptoms unchanged. Therefore: not a codex version issue.
|
|
|
|
**Reality.** In the reference environment (WSL2, 0.121.0) all three
|
|
shapes produce non-empty JSONL stdout and EXIT=0. In the agent's
|
|
environment (containerized, 0.121.0) they do not. Same codex binary
|
|
version, different outputs. The `codex --version` command itself
|
|
prints to stdout in the reference env but not in the agent's env —
|
|
further evidence of environment-level stdout interception or
|
|
suppression rather than codex misbehavior.
|
|
|
|
**Root cause.** Not diagnosed. Plausible hypotheses: Node.js stdout
|
|
buffering interaction with the sandbox's process-wrapping (short
|
|
writes lost on exit), or a sandbox-level stdout tee that discards
|
|
output, or a libuv/file-descriptor interaction specific to the
|
|
container environment. None confirmed.
|
|
|
|
**Mitigation.** The skill cannot control the environment, so the
|
|
skill adapted: the filesystem-based session-id recovery path (`§4.1b`)
|
|
was promoted from "rejected alternative" to "secondary capture".
|
|
Both tiers coexist — primary for reference-env performance, secondary
|
|
for affected-env correctness. The `cat | pipe` form (`§4.13`) was
|
|
chosen as canonical prompt delivery because it works in both envs
|
|
while `- < file` fails in the affected one.
|
|
|
|
**Impact on the design.** The §4.1 rejection of filesystem-UUID
|
|
parsing was reread: it was correct as a reason to reject *primary*
|
|
reliance on it (race hazards against parallel codex), but not as a
|
|
reason to reject it as *secondary*. Re-reading old rejections with a
|
|
specific role in mind is sometimes more useful than re-verifying
|
|
claims.
|
|
|
|
**Lesson (augmenting §6.5).** When a single-source report reproduces
|
|
after verification, the follow-up question is still "what exactly did
|
|
I verify?" The first verification (running §7.1 in the reference
|
|
environment) proved the skill contract worked — in that environment.
|
|
It did not prove the contract worked universally. Contract verification
|
|
is env-specific until demonstrated otherwise.
|
|
|
|
---
|
|
|
|
## §7. Smoke test protocol
|
|
|
|
Minimal set of checks to run after editing `SKILL.md`, changing the
|
|
Codex invocation pattern, or upgrading Codex CLI. Purpose: detect
|
|
regressions in the external-CLI contract the skill depends on.
|
|
|
|
Each check is copy-pasteable bash. All use a real git repo; run from
|
|
the repo root. Expected outputs are in comments.
|
|
|
|
### §7.1. Initial launch (mirrors Step 4 flow)
|
|
|
|
```bash
|
|
REVIEW_ID=$(date +%s)-$(printf '%08d' $RANDOM)
|
|
REPO_ROOT=$(git rev-parse --show-toplevel)
|
|
CODEX_SESSIONS_BEFORE=$(date +%s)
|
|
cat > /tmp/codex-prompt-${REVIEW_ID}.md <<'EOF'
|
|
<role>
|
|
You are a senior adversarial reviewer of implementation plans.
|
|
</role>
|
|
<task>
|
|
Confirm you received this prompt.
|
|
</task>
|
|
<output_format>
|
|
End the LAST line with exactly: VERDICT: APPROVED
|
|
</output_format>
|
|
EOF
|
|
|
|
cat /tmp/codex-prompt-${REVIEW_ID}.md | timeout 300 codex exec --json \
|
|
-m gpt-5.4 -c model_reasoning_effort=low \
|
|
-s read-only -C "${REPO_ROOT}" \
|
|
-o /tmp/codex-review-${REVIEW_ID}.md \
|
|
- \
|
|
> /tmp/codex-stdout-${REVIEW_ID}.jsonl \
|
|
2>/tmp/codex-stderr-${REVIEW_ID}.txt
|
|
|
|
echo "EXIT=$?" # expect 0
|
|
head -1 /tmp/codex-stdout-${REVIEW_ID}.jsonl # reference env: thread.started; affected env: empty
|
|
wc -c /tmp/codex-stderr-${REVIEW_ID}.txt # expect 0
|
|
grep -E '^VERDICT:' /tmp/codex-review-${REVIEW_ID}.md # expect VERDICT: APPROVED
|
|
|
|
# Verify the filesystem secondary path also works (§4.1b)
|
|
find ~/.codex/sessions -name 'rollout-*.jsonl' -newermt "@${CODEX_SESSIONS_BEFORE}" \
|
|
| sort | tail -1 | xargs -r -n1 basename \
|
|
| grep -oE '[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}'
|
|
# expect: one UUID. If head -1 stdout had thread_id, this UUID should match.
|
|
```
|
|
|
|
### §7.2. Resume with cd prefix
|
|
|
|
Continuing from §7.1 — extract the thread id and resume.
|
|
|
|
```bash
|
|
# Primary session-id capture (may be empty in affected sandboxes)
|
|
THREAD_ID=$(head -1 /tmp/codex-stdout-${REVIEW_ID}.jsonl \
|
|
| grep -oE '"thread_id":"[^"]+"' | cut -d'"' -f4)
|
|
# Secondary: rollout-filename UUID (always works)
|
|
if [ -z "${THREAD_ID}" ]; then
|
|
THREAD_ID=$(find ~/.codex/sessions -name 'rollout-*.jsonl' \
|
|
-newermt "@${CODEX_SESSIONS_BEFORE}" 2>/dev/null \
|
|
| sort | tail -1 | xargs -r -n1 basename \
|
|
| grep -oE '[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}')
|
|
fi
|
|
echo "THREAD_ID=${THREAD_ID}" # expect a UUID
|
|
|
|
cat > /tmp/codex-resume-prompt-${REVIEW_ID}.md <<'EOF'
|
|
Still there? Reply with VERDICT: APPROVED.
|
|
EOF
|
|
|
|
CODEX_SESSIONS_BEFORE=$(date +%s)
|
|
cd "${REPO_ROOT}" && cat /tmp/codex-resume-prompt-${REVIEW_ID}.md \
|
|
| timeout 300 codex exec resume --json \
|
|
"${THREAD_ID}" \
|
|
-o /tmp/codex-review-${REVIEW_ID}.md \
|
|
- \
|
|
> /tmp/codex-stdout-${REVIEW_ID}.jsonl \
|
|
2>/tmp/codex-stderr-${REVIEW_ID}.txt
|
|
|
|
echo "EXIT=$?" # expect 0
|
|
head -1 /tmp/codex-stdout-${REVIEW_ID}.jsonl # reference env: same thread_id (§2.4.4); affected env: empty
|
|
wc -c /tmp/codex-stderr-${REVIEW_ID}.txt # expect 0
|
|
grep -E '^VERDICT:' /tmp/codex-review-${REVIEW_ID}.md # expect VERDICT: APPROVED
|
|
```
|
|
|
|
### §7.3. Resume with bad UUID
|
|
|
|
```bash
|
|
echo "test" > /tmp/codex-bad-resume-prompt.md
|
|
cat /tmp/codex-bad-resume-prompt.md | timeout 60 codex exec resume --json \
|
|
00000000-0000-0000-0000-000000000000 \
|
|
- \
|
|
> /tmp/codex-bad-resume.stdout \
|
|
2>/tmp/codex-bad-resume.stderr
|
|
|
|
echo "EXIT=$?" # expect 1
|
|
cat /tmp/codex-bad-resume.stderr | head -3 # expect "thread/resume failed" line
|
|
wc -c /tmp/codex-bad-resume.stdout # expect 0
|
|
```
|
|
|
|
### §7.4. Bare repo detection
|
|
|
|
```bash
|
|
TMPBARE=$(mktemp -d)
|
|
git init --bare "${TMPBARE}" -q
|
|
( cd "${TMPBARE}" && git rev-parse --show-toplevel 2>&1 )
|
|
# expect: "fatal: this operation must be run in a work tree" or similar
|
|
# expect exit 128
|
|
rm -rf "${TMPBARE}"
|
|
```
|
|
|
|
### §7.5. `--last` cwd filter
|
|
|
|
```bash
|
|
# setup two dummy git repos
|
|
mkdir -p /tmp/smoke-cwd-a /tmp/smoke-cwd-b
|
|
( cd /tmp/smoke-cwd-a && git init -q )
|
|
( cd /tmp/smoke-cwd-b && git init -q )
|
|
# run codex in each; capture session ids
|
|
ID_A=$(cd /tmp/smoke-cwd-a && echo "ALPHA" | \
|
|
codex exec -s read-only -m gpt-5.4 \
|
|
-c model_reasoning_effort=low - 2>&1 | grep 'session id:' | awk '{print $3}')
|
|
ID_B=$(cd /tmp/smoke-cwd-b && echo "BRAVO" | \
|
|
codex exec -s read-only -m gpt-5.4 \
|
|
-c model_reasoning_effort=low - 2>&1 | grep 'session id:' | awk '{print $3}')
|
|
echo "A=${ID_A}"
|
|
echo "B=${ID_B}"
|
|
# resume --last from cwd-a; expect to resume ID_A, not ID_B
|
|
( cd /tmp/smoke-cwd-a && echo "which?" | \
|
|
codex exec resume --last - 2>&1 | grep 'session id:' )
|
|
# expect: session id matches ID_A
|
|
rm -rf /tmp/smoke-cwd-a /tmp/smoke-cwd-b
|
|
```
|
|
|
|
### §7.6. Cleanup smoke artifacts
|
|
|
|
```bash
|
|
rm -f /tmp/codex-prompt-${REVIEW_ID}.md \
|
|
/tmp/codex-resume-prompt-${REVIEW_ID}.md \
|
|
/tmp/codex-review-${REVIEW_ID}.md \
|
|
/tmp/codex-stdout-${REVIEW_ID}.jsonl \
|
|
/tmp/codex-stderr-${REVIEW_ID}.txt \
|
|
/tmp/codex-bad-resume-prompt.md \
|
|
/tmp/codex-bad-resume.stdout \
|
|
/tmp/codex-bad-resume.stderr
|
|
```
|
|
|
|
### §7.7. If anything fails
|
|
|
|
If §7.1–§7.5 do not produce the expected outputs:
|
|
|
|
1. Note the exact codex CLI version (`codex --version`).
|
|
2. Check against the facts in `§2`. If your observation contradicts a
|
|
`§2` fact, the design may need adjustment — propose via PR and
|
|
include a new entry in `§8. Version and verification log` for the
|
|
new Codex version.
|
|
3. If the contradiction is severe (e.g., `--json` format change,
|
|
`-o` no longer works in `resume`), mark `§2` entries outdated
|
|
before modifying `SKILL.md`. Future contributors should know which
|
|
facts they can still trust.
|
|
|
|
---
|
|
|
|
## §8. Version and verification log
|
|
|
|
| Date | Codex CLI | Claude Code | Verifier | Notes |
|
|
|------|-----------|-------------|----------|-------|
|
|
| 2026-04-17 | 0.121.0 | current at time of refactor | initial author | All §2 facts verified; §7 smoke test passes end to end. Initial commit of this document. |
|
|
| 2026-04-17 | 0.121.0 | containerized sandbox (yantar-k8s) | external agent + lead | §7.1 `- < file` form fails EXIT=1 with empty stderr. `cat \| pipe` form works for `-o` review, but `--json` stdout is empty. Filesystem secondary session-id capture (§4.1b) verified functional: UUID extracted from rollout filename successfully resumes. Not a version issue (reproduced on 0.120.0 and 0.121.0). Root cause undiagnosed — see §6.6. Skill adapted: `§4.1` now two-tier, `§4.13` switches canonical form to `cat \| pipe`. |
|
|
|
|
When you re-verify (either during routine maintenance or when
|
|
triggered by §7.7), add a row. Keep the log chronological.
|
|
|
|
---
|
|
|
|
## §9. Known limitations and future work
|
|
|
|
### §9.1. Parallel codex in the same cwd
|
|
|
|
If the user runs `codex exec` manually in the same working tree while
|
|
the skill is mid-review, `~/.codex/sessions/` will contain sessions
|
|
from both processes. The skill does not use `--last`, so this does not
|
|
cause wrong-session hazards, but the user's parallel work may produce
|
|
rollout files that are filesystem-level noise during debugging.
|
|
|
|
No mitigation. Rules in `SKILL.md` mention the constraint indirectly
|
|
by forbidding `cd` between rounds, but they don't prevent the user's
|
|
own parallel invocations.
|
|
|
|
### §9.2. Context compaction risk in fresh-exec fallback
|
|
|
|
The fallback template (§4.6) rebuilds prior rounds from conversation
|
|
history. If Claude Code's context compaction kicks in mid-review and
|
|
summarizes earlier rounds, the rebuilt template is degraded — the new
|
|
reviewer may see a summary instead of the original verbatim findings.
|
|
|
|
Not mitigated today. If encountered in practice, options include
|
|
archiving each round's `-o` file to a per-round name (§5.2 idea,
|
|
currently rejected), or persisting the review chain to a single
|
|
append-only file in `/tmp` that the fallback reads directly.
|
|
|
|
### §9.3. Reviewer prompt format drift
|
|
|
|
The §4.7 sanity check and §4.11 severity parsing rely on the reviewer
|
|
using the format prescribed by the skill's adversarial prompt
|
|
(`VERDICT: APPROVED|REVISE` on a line; `[severity: <level>]` in
|
|
finding sub-headers). If the reviewer drifts (e.g., outputs
|
|
`**Severity:** High` instead), the skill falls through launch-failure
|
|
paths inappropriately.
|
|
|
|
Mitigation today: case-insensitive regex tolerating bracketed and
|
|
non-bracketed severity forms. If drift becomes frequent, prompt
|
|
engineering is the first response; a last-resort option is more
|
|
permissive regex at the cost of weaker signal.
|
|
|
|
### §9.4. Path containing shell-special characters
|
|
|
|
If `REPO_ROOT` contains `'`, `"`, `$`, backtick, or newline, the skill
|
|
aborts at Step 2 with a clear message rather than attempting
|
|
sanitation. This is a user-visible limitation; in practice repo paths
|
|
rarely contain these characters.
|
|
|
|
If this becomes an issue, the fix is to sanitize / escape before
|
|
substitution, which requires careful handling of double-quoted `-C`
|
|
argument and single-quoted `cd` prefix.
|
|
|
|
### §9.5. GNU find dependency in the filesystem session-id fallback
|
|
|
|
The secondary session-id capture (`§4.1b`) uses `find -newermt "@<epoch>"`
|
|
and `-printf`, both GNU extensions. On macOS (BSD `find`) the commands
|
|
do not accept these flags. The skill does not detect the platform and
|
|
does not translate commands automatically.
|
|
|
|
Mitigation today: `SKILL.md` Step 4 check 3 includes a one-paragraph
|
|
platform note that states the *goal* of the command ("list rollout
|
|
files modified since `CODEX_SESSIONS_BEFORE`, pick newest, extract
|
|
UUID from filename") and invites the operator or the lead to substitute
|
|
an equivalent BSD-compatible command (`find ... -type f` + `stat -f
|
|
'%m %N'`, or `ls -t ... | head -1` against a reference marker file).
|
|
This is a "template + understanding" approach: rely on the lead's
|
|
adaptability rather than branching the skill for every platform.
|
|
|
|
If BSD support ever becomes load-bearing (a macOS-running user base,
|
|
a CI on macOS runners), this can be upgraded to a bundled portable
|
|
command variant or a platform-detection branch.
|
|
|
|
### §9.6. No automated tests
|
|
|
|
`§7. Smoke test protocol` is manual. Automating it would require:
|
|
|
|
- A `scripts/smoke.sh` with the checks from §7.
|
|
- A way to run under a non-interactive codex auth. Today codex auth is
|
|
per-user; CI integration requires exchanging an `OPENAI_API_KEY`
|
|
via environment variables.
|
|
|
|
Deferred until there is a CI story for the repo.
|
|
|
|
---
|
|
|
|
## §10. Update protocol — when and how to revise this file
|
|
|
|
### §10.1. When to update
|
|
|
|
Revise this document when any of the following happens:
|
|
|
|
- **Codex CLI releases.** If a release changes any of §2's facts —
|
|
e.g., a new `resume` flag, a change in `--json` event format, a
|
|
fix to thread_id rotation, a new exit code — re-run §7, then
|
|
update §2 and add a row to §8. If a design decision in §4 relied
|
|
on the old behavior, evaluate whether the decision still holds
|
|
and revise or rationalize.
|
|
- **A new Claude model generation.** Later Opus releases tend to
|
|
interpret `SKILL.md` instructions more literally. What worked as
|
|
a "soft rule" in the previous generation may fail for the next.
|
|
When a new Opus ships, re-run the skill end-to-end on a small
|
|
artifact (smoke test from the user side, not just from the CLI
|
|
side) and check that review output is actually shown to the user,
|
|
that the fixes are applied, that the verdict is parsed. If not,
|
|
the §3.4 / §4.9 instructions may need tightening — rewrite with
|
|
more explicit "DO NOT" lists, more explicit procedural anchors
|
|
("YOUR NEXT MESSAGE", "BEFORE calling any fix tool", etc.), and
|
|
update §3.4 to record the observed literal interpretation.
|
|
- **Claude Code harness updates.** If Bash tool truncation size
|
|
changes, if cwd behavior between calls changes, if new
|
|
safety rules appear, update §3.
|
|
- **A reviewer finds a new failure mode.** Add a finding to §5 (if
|
|
the idea was considered and rejected) or §9 (if it is an accepted
|
|
limitation).
|
|
- **You encounter a diagnostic report you did not write.** Before
|
|
acting on it, verify its claims empirically. If any claim is
|
|
wrong, add an entry to §6. This file should be the repository of
|
|
lessons, not just of the current state.
|
|
|
|
### §10.2. How to update
|
|
|
|
- Keep the structure. Section numbering is part of the cross-reference
|
|
network; renumbering silently breaks links in `SKILL.md` and in
|
|
this document itself.
|
|
- When a fact in §2 is superseded, mark the superseded version clearly
|
|
(strike through, "was:" prefix, or similar) before adding the new
|
|
fact. Do not delete superseded facts silently — a future reader
|
|
needs to know what *used to* be true, because some old SKILL.md
|
|
behavior still assumes it.
|
|
- When a design decision in §4 is revised, move the old decision text
|
|
into §5 (rejected ideas) with the new reason for rejection. Keep
|
|
the institutional memory explicit.
|
|
- Always add a §8 row when you re-verify against a new tool version.
|
|
- Every major update should run §7 and commit the observations.
|
|
|
|
### §10.3. Writing for literal readers
|
|
|
|
When adding instructions to `SKILL.md` alongside a design change here,
|
|
remember that future models may interpret them more literally than
|
|
the current generation. Rules of thumb:
|
|
|
|
- State the *action*, not the *intent*. "Show the review verbatim"
|
|
is intent; "Your next message to the user must begin with
|
|
`## Adversarial Review — Round N ...` and must contain the file
|
|
contents verbatim" is action.
|
|
- List what must NOT be done in addition to what must be done.
|
|
"Do not wrap the review in a code fence", "Do not call Edit/Write
|
|
in the same message".
|
|
- Anchor procedural steps to verifiable conditions: "before any fix
|
|
tool call", "only after sending the user message", etc. These
|
|
survive literal interpretation better than "first ... then ...".
|
|
|
|
---
|
|
|
|
## §11. References
|
|
|
|
- `SKILL.md` — authoritative source of runtime behavior.
|
|
- `README.md` — user-facing install, permissions, troubleshooting.
|
|
- Codex CLI documentation: https://developers.openai.com/codex/cli
|
|
- Codex CLI non-interactive mode: https://developers.openai.com/codex/noninteractive
|
|
- Codex CLI command-line reference: https://developers.openai.com/codex/cli/reference
|
|
- Codex GitHub issue #12538 (resume `-o` support): https://github.com/openai/codex/issues/12538
|
|
- Codex GitHub issue #14544 (resume exec sessions): https://github.com/openai/codex/issues/14544
|