The helper now emits its verdict token on stdout with diagnostics on
stderr, and both consumers validate the PAIR rather than the status
alone. This closes the macOS defect CI found: a shell that cannot
execute the helper exits 1, which the status-only ABI read as
`ignored`, so a broken guard told the operator their environment
ignores SIGINT.
Gate (shell consumer):
- guard-local capture directory, created before the gate's own
temporary roots exist, with cleanup armed BEFORE the helper runs and
disarmed on the safe path so the gate's later trap is undisturbed;
- `|| sigint_status=$?` retained --- a bare invocation dies under
`set -eu` before the status is read, which was the original bug;
- `expected_token` selected by an explicit status case before any
`set -u`-sensitive use, since an out-of-range status has none;
- byte comparison via `cmp` against both permitted encodings, because
a shell variable neither preserves NUL nor carries the child status;
- the helper's stderr is surfaced ONLY for validated verdicts; a
boundary failure prints the gate's own wording and withholds the
untrusted child output;
- every refusing branch prints status= and token=.
R-d (Rust consumer) validates the same pair from Command::output()
bytes. It needs no capture files, and its spawn-error path has no status
at all --- the boundary the shell cannot represent.
Conformance: 45 shared cases generated as a cross-product over token
class, encoding and status, run by BOTH validators so they cannot
diverge, plus Rust's X2 for 46 overall. 34 gate rows, 16 GPU rows, full
gate green.
Mutations, each biting its row: accepting any status 2 regardless of
token; surfacing child stderr on a boundary failure; emitting the token
to stderr. The first is caught by the dedicated error row rather than
the conformance set --- most of the set's boundary cases have empty
stderr, so they cannot tell which branch produced the exit 2 --- and
that limitation is recorded rather than left implicit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai