fix(agents,scripts): PIPESTATUS/pipefail are exit-code-safe only when the downstream reads to EOF - #13427
Merged
Conversation
… the downstream reads to EOF
The os-dev exit-code discipline listed `set -o pipefail` and `${PIPESTATUS[0]}`
as safe capture forms. Measured on a refusing gate: `| head -N` closes the read
end early, the producer takes EPIPE/SIGPIPE and exits 0 — the pipe changes the
producer's exit code, so PIPESTATUS reports that 0 faithfully and pipefail reads
the same changed status. Only redirect-then-capture is immune. Downgrade both
forms to EOF-conditional, name `| head -N` as the measured counterexample, and
state the corollary: any `cmd | head` followed by an exit-code read is hit,
not just gates.
Same PR, second copy of the same wrong fact: the i18n gate's refusal banner
warned against `| tail -4` (which reads 1, correctly, via PIPESTATUS) and left
`| head` — the shape that actually reads green — unnamed. The advisory now
names the true hazard and the safe capture.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
This was referenced Aug 30, 2026
This was referenced Aug 30, 2026
os-zhuang
approved these changes
Aug 30, 2026
os-zhuang
marked this pull request as ready for review
August 30, 2026 13:35
os-zhuang
enabled auto-merge
August 30, 2026 13:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12979
Both copies of the same wrong fact are fixed in this one PR, per the 2026-08-28 grading comment on the card (which explicitly permitted this one-time cross-domain pairing: splitting the halves would leave one copy still misleading).
Half 1 —
.claude/agents/os-dev.md(governed face: this PR stays draft, human merge)set -o pipefailandPIPESTATUS[0]unconditionally. Per the grading's fence ("修正的形态要具体,不要只删一项"), both are downgraded to "safe only when the downstream reads to EOF", with| head -Nnamed as the measured counterexample and the mechanism kept in the text:head -Ncloses the read end early, the producer takes EPIPE/SIGPIPE and exits 0 — the pipe changed the producer's exit code, so PIPESTATUS reports that 0 faithfully andpipefailreads the same changed status. Only redirect-then-capture is immune. TheEXIT=$?-after-tailtrap stays in the paragraph (the advisory is not simplified to "do not pipe").cmd | headfollowed by an exit-code read is hit (git grep,nodealike); reading the output is fine, reading the exit code is a false green.pnpm --filterbullet, which formerly recommended a bare PIPESTATUS read) now points to redirect-then-capture and cross-references the mechanism paragraph.Half 2 —
scripts/check-i18n-bundles.mjsrefusal-banner advisoryThe banner warned against
| tail -4— measured, that shape forwards the true status 1 via PIPESTATUS[0]; the dangerous shape is| head, which the old advisory left unnamed, steering readers away from the safe shape and toward the only one that lies. The advisory now names the true hazard, the mechanism, and the safe capture (redirect-then-capture, exit code taken before any pipe). Prose only: same exit codes (self-test 0, refusal 1), no behavioural change.Measurement — reproduced on this tree before writing (base 94efa2a, unbuilt CLI,
pnpm check:i18nrefusing with PREREQUISITE NOT MET)2>&1 | head -20: PIPESTATUS[0]=0 — false green2>&1 | tail -4: PIPESTATUS[0]=1 — faithfulset -o pipefailwith| head -20: EXIT=0 — false greenpnpm check:nul-bytesdirect EXIT=0 — the instrument returns both values, so the zeros above are readings, not a stuck valueVerification — union run at 9f1652c (the final commit)
Derived with
node scripts/pm/dispatch-gates.mjs(no paths; 2-path changeset, header names this tree at 94efa2a). All matched families run locally and green, quoting each gate's own verdict line:check:pm-skill-ratchet— "os-dev.md is 466 lines (ceiling 466; headroom 0)" and "widest table row is 0 bytes (pin 0)" — re-run at 9f1652ccheck:nul-bytes— "OK (scanned 7438 text file(s) … no raw ASCII control bytes)" — re-run at 9f1652cnode scripts/check-i18n-bundles.mjs --self-test— "all go red, and stay distinct" — re-run at 9f1652c, plus the live refusal path re-exercised after the edit: still exit 1, new banner prints with the literal PIPESTATUS spelling intactcheck:agent-model-declared·check:agent-test-spelling·check:doc-authoring·check:pm-governed-merges(206 assertions) ·check:pm-skill-id-lint(23 files clean) ·check:skill-frame-sync(4 copies isomorphic) — greencheck:bash32-floor(117 cases) ·check:cli-command-ids·check:cross-package-test-inputs(both spellings; 24 packages declared) ·check:entry-guard(184 files) ·check:parse-guard(183 files) ·check:pnpm-filter-targets(141/179 resolve) ·check:watch-hint-literal(22 declarations) ·check-ci-filter-parity(123 globs covered) ·check-shard-attestation(2 aggregate gates, 3 legs) — greennode scripts/pm/bare-root-worklist.mjs --self-testandpnpm check:pm-dispatch-gates("dispatch-gates self-test: 906 cases pass") — greenpnpm --filter @objectstack/lint run check:doc-formula-expressions— "22 record-scoped formula example(s) … judged clean" after building the lint closureNOT MEASURED locally, by each gate's own declaration (CI owns them):
check:i18nfull bundle comparison (needs the 10-package build closure; the diff is confined to the refusal-banner template literal, so the comparison path is untouched — and CI builds first);check-test-completeness(grades a saved CI test log; exits 3 with "record this gate as NOT MEASURED");check-governed-queue-guard(reads the CI event payload; runs on this PR).skip-changeset
The diff releases nothing — an internal agent definition plus a CI-internal gate script's stderr banner. lint.yml's own comment calls the CI-internal-script edit "the textbook
skip-changesetcase". Label applied at PR-open per the standing terms.Out of scope, reported on the card
Sibling copies of the same wrong-shape advisory exist in
scripts/pm/ci-failure.mjs,scripts/pm/check-half-states.mjs,scripts/import-prerequisite.mjs,scripts/check-i18n-coverage.mjsandscripts/check-test-completeness.mjs. They are outside this card's graded surface (the grading permitted exactly the check-i18n-bundles half) and are left for triage — details in the os-dev report on the card. #12979 names only the two halves fixed here; the sibling copies remain open work.Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
Generated by Claude Code
Generated by Claude Code