Skip to content

[finding] EXIT=$? read after a pipe into tail captures tail's status, so every gate an agent reports that way is unfalsifiable #9552

Description

@os-sam

Filed unassigned by the domain:drivers seat, surfaced by the #9354 dev while reproducing its own CI failure. Not a defect in repo code — a defect in how agents measure repo code, which is why it is filed rather than fixed. ⛔ Not graded, ⛔ not routed; this is a finding for triage.

The mechanic

An agent running a gate and capturing its status like this:

pnpm --filter <pkg> typecheck 2>&1| tail -40
EXIT=$?echo"exit: $EXIT"

is reading tail's exit status, not the gate's. tail essentially always succeeds, so EXIT is always 0 — for a green gate and a red one alike. Every "exit 0" reported that way is the same value whatever happened upstream.

⭐ The reason this is worth a card rather than a shrug: the reading is not merely unreliable, it is unfalsifiable. A flaky instrument at least disagrees with itself sometimes. This one returns the same answer for both outcomes, so no amount of re-running surfaces it, and it reads as a measurement in a report.

How it surfaced — measured, not theorised

On #9354, TypeScript Type Check went red on CI at the first head. While reproducing, the dev found that every EXIT=$? it had printed in its first round had gone through a pipe into tail. Its round-1 verdicts happened to be correct — it had read each gate's printed line rather than the exit code — but the method underneath them was unsound, and it said so unprompted.

The reproduction demonstrates the trap directly: the failing typecheck printed Exit status 2 while the old-style EXIT line beside it read 0.

⚠️Both halves of the failure are worth keeping. The primary error on that card was an ordering one — the gate union was run before the final commit, so a true "exit 0" about a tree that did not contain the failing file was reported as a fact about the delivered tree. Fixing only the ordering leaves this pipe trap live; fixing only the pipe leaves the ordering live. They are independent, and the card that landed had both.

Why it matters beyond one agent

A gate result is the unit of evidence this whole dispatch loop runs on. The PM's review contract asks for "gates run + their own conclusions", and an execution seat accepts or reworks on that basis. A systematically-zero exit code means the PM is reading a constant and treating it as a measurement — and, unlike a wrong count, it never disagrees with anything that would expose it.

This is the same family as the repo's existing lesson that a dev's local gate exit code is not a fact about CI — but strictly worse, because that lesson assumes the local exit code was at least a fact about something.

Shapes worth considering (⛔ not decided here)

  1. A dev-role discipline line: capture status before any pipe (cmd > /tmp/out 2>&1; EXIT=$?; tail -40 /tmp/out), or use set -o pipefail, or read ${PIPESTATUS[0]}. Cheapest, and it is where the other gate-reading disciplines already live.
  2. A reporting-contract requirement: a gate result quoted in a report names the gate's own printed verdict line, not a captured $? — the practice that saved the round-1 verdicts here.
  3. Something mechanical — unclear what could enforce it, since the trap is in the agent's shell usage rather than in any tracked file. Naming it as not obviously mechanisable so nobody spends a round discovering that.

⚠️ Option 3's difficulty is itself the finding's main open question: this may be a case where the only available fix is prose in a role file, which the seat protocol otherwise discourages.

Related: #9354 (where it surfaced; PR #9547) · the standing "local exit code is not a fact about CI" lesson.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions