Skip to content

fix(gate): harden the remaining guards' final write against a closed pipe (#1838) - #1881

Merged
macanderson merged 4 commits into
mainfrom
fix/1838-guard-epilogue-residue
Aug 6, 2026
Merged

fix(gate): harden the remaining guards' final write against a closed pipe (#1838)#1881
macanderson merged 4 commits into
mainfrom
fix/1838-guard-epilogue-residue

Conversation

@macanderson

@macanderson macanderson commented Aug 6, 2026

Copy link
Copy Markdown
Owner

What & why

The nine scripts/check-*.sh guards that #1815's sweep (PR #1844) left out already decide their verdict before printing, but their final green OK line was still an unguarded pipe write. With stdout piped into a reader that has already exited (| true is the deterministic repro), that write dies of SIGPIPE (exit 141) or fails with EPIPE and set -e turns it into exit 1 — either way a green verdict reports as a failure. Only the exit code was forged, which is exactly the half a caller reads.

This PR copies the epilogue PR #1844 established (exemplar: scripts/check-left-behind.sh): the verdict is decided first, then the final write runs under trap '' PIPE with its failure discarded (|| true). Output text is byte-identical.

Scripts hardened:

  • scripts/check-action-pins.sh — the non-fatal missing-tag-comment report is part of the same decided-green tail, so it rides under the same trap; the unreachable skip branches are left alone, matching fix(gate): decide guard verdicts before printing so a closed pipe cannot forge a failure (#1815) #1844's treatment of check-cargo-install-pins.sh
  • scripts/check-brand-case.sh
  • scripts/check-design-refs.sh
  • scripts/check-empty-diff.sh
  • scripts/check-license-allowlist-parity.sh
  • scripts/check-no-scratch.sh
  • scripts/check-no-secrets.sh
  • scripts/check-stat-portability.sh
  • scripts/check-wire-schema.sh

scripts/test-guard-sigpipe.sh (make guard-sigpipe-test) grows a case pair (| true, | head -1) per hardened guard: the seven scanning guards join the main loop, check-empty-diff.sh runs against the real HEAD~1 HEAD pair (it takes a <base> <head> pair rather than scanning the tree), and check-wire-schema.sh — which compiles the two schema exporters — is gated on a cargo toolchain being on PATH, skipping loudly otherwise, per the issue's allowance.

Based on fix/1815-guards-survive-sigpipe (PR #1844) because that PR carries the harness and the epilogue pattern and has not merged yet; retarget to main after it lands.

Closes #1838
Refs #1815

The witness

  • This PR includes a witness test (fails on the old scripts, passes here)

scripts/test-guard-sigpipe.sh, extended with 16 new cases, run both ways on this tree (cargo masked off PATH, so the wire-schema case skips):

check-wire-schema.sh could not be exercised here (its harness case needs a cargo toolchain and pays a workspace build; this change was verified shell-only) — its epilogue is byte-for-byte the same shape as the eight witnessed ones, and its harness case will run wherever cargo is present.

The gate

  • shellcheck clean on all 10 touched scripts (make shellcheck set)
  • No Rust touched — fmt/clippy/test unaffected by this diff
  • Docs: no behavior/flag changes; harness header already documents the posture
  • CLA signed
  • Closes #1838 appears both above and as a commit trailer

Nothing left behind

Ground-rule check

  • No I/O added to stella-core; no new deps
  • No new outbound network calls

Anything reviewers should know?

trap '' PIPE is process-wide from the point it is set, but in every script it is set only after the last verdict-bearing computation, so it can only affect the best-effort report writes — the same placement PR #1844 reviewed nine times.

Summary by Sourcery

Harden gate guard shell scripts against closed-pipe failures, align their reporting patterns, and add a dedicated test harness and Make target to ensure guards remain robust when their output is piped to early-exiting readers.

New Features:

  • Add a dedicated guard-sigpipe-test Make target and scripts/test-guard-sigpipe.sh harness to verify gate guard behavior when their output pipes are closed early.

Bug Fixes:

  • Prevent gate guard scripts from failing with SIGPIPE/EPIPE when their stdout is piped to readers that exit early by ensuring final report writes are best-effort and do not affect exit codes.

Enhancements:

  • Refine multiple guard scripts to buffer verdict output before emission and use consistent reporting patterns that avoid subshell state loss and pipe races, including a more robust membership check in check-god-files.sh and simplified failure propagation in check-role-names.sh.

Build:

  • Extend the Makefile with a guard-sigpipe-test target to exercise guard robustness against closed pipes.

Tests:

  • Introduce a comprehensive test harness in scripts/test-guard-sigpipe.sh that runs key guard scripts under early-terminating pipe readers (e.g., | true, | head -1) to ensure exit codes remain correct and stable.

Stella Test and others added 3 commits August 6, 2026 03:53
…not forge a failure

Piping a guard's output is the normal way to read it — `| tail`,
`| rg FAIL`, `| head` — and check-god-files.sh reported a FALSE failure
naming a different crate on each such run. Two mechanisms, both SIGPIPE:

* Its crate-membership test was `printf ... | grep -qx`. `grep -q` exits
  the moment it matches, printf then dies writing into the closed pipe,
  and `set -o pipefail` reports the pipeline as failed — so a crate that
  IS present intermittently read as absent, and the guard blamed
  whichever crate the race landed on. Replaced with the pure-shell case
  membership check check-command-docs.sh already uses.

* Guards printed their report incrementally while still deciding it.
  When stdout (or stderr under `2>&1`) is a pipe whose reader has
  exited, the next write dies of SIGPIPE — 141 outright, or EPIPE plus
  `set -e` = exit 1 where SIGPIPE is inherited ignored — and whatever
  partial state the scan had reached becomes the verdict.

Every swept guard now decides its verdict fully before printing: failure
lines are buffered into a variable and emitted in one final write, and
that write runs under `trap '' PIPE` with its failure discarded, so an
early-exiting reader can change neither the verdict nor the exit code.
check-file-size.sh and check-left-behind.sh already buffered their
reports and needed only the hardened final write.

Swept: check-god-files, check-invariants, check-left-behind,
check-file-size, check-command-docs, check-gate-parity,
check-role-names, check-cargo-install-pins, check-repro-wiring. Output
text is byte-identical in every case. check-role-names' subshell marker
file is gone: the `sed | while` loop is now redirected from process
substitution, so `fail=1` and the buffered report survive.

Witness: scripts/test-guard-sigpipe.sh (make guard-sigpipe-test) pipes
each guard into `head -1` and `true` and asserts exit 0. On the old
scripts all nine `| true` cases die with exit 141 (18 of 27 cases pass);
on the new ones all 27 pass.

Closes #1815
…pipe

The nine guards #1815's sweep left out already decide their verdict before
printing, but their final green OK line was still an unguarded pipe write:
with stdout piped into a reader that has already exited (| true is the
deterministic repro), the write dies of SIGPIPE (exit 141) or EPIPE under
set -e (exit 1), forging a failure out of a green verdict.

Copy the epilogue #1815 established: the verdict is decided first, then the
final write runs under trap '' PIPE with its failure discarded. Output text
is byte-identical. check-action-pins.sh's non-fatal missing-tag-comment
report is part of the same decided-green tail, so it rides under the same
trap; the unreachable skip branches are left alone, matching #1815's
treatment of check-cargo-install-pins.sh.

scripts/test-guard-sigpipe.sh grows a case per hardened guard:
check-empty-diff.sh runs against the real HEAD~1 HEAD pair, and
check-wire-schema.sh — which compiles the two schema exporters — is gated
on a cargo toolchain being present, skipping loudly otherwise.

Closes #1838
Refs #1815

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @macanderson, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
stella-cli-docs Ignored Ignored Preview Aug 6, 2026 7:07pm

@sourcery-ai

sourcery-ai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Hardens various gate shell guard scripts against SIGPIPE/EPIPE when their stdout is piped to an early‑exiting reader by buffering output, guarding final writes with trap '' PIPE and || true, and adds a dedicated test harness plus Makefile target to assert these guards remain green under such conditions.

Sequence diagram for hardened guard stdout pipe handling

sequenceDiagram
  actor Caller
  participant GuardScript as check_guard.sh
  participant PipeReader as pipe_reader_true

  Caller->>GuardScript: run check_guard.sh | true
  GuardScript->>GuardScript: run checks, decide status
  GuardScript->>GuardScript: note() appends to report
  GuardScript->>GuardScript: emit()
  GuardScript->>GuardScript: trap '' PIPE
  GuardScript->>PipeReader: printf '%s' "$report" >&2 || true
  PipeReader-->>PipeReader: exit early
  GuardScript-->>Caller: exit with decided status (e.g. 0)
Loading

Sequence diagram for guard-sigpipe-test harness execution

sequenceDiagram
  actor Developer
  participant Makefile
  participant TestHarness as test-guard-sigpipe.sh
  participant GuardScript as check_guard.sh
  participant ConsumerTrue as true
  participant ConsumerHead as head -1

  Developer->>Makefile: make guard-sigpipe-test
  Makefile->>TestHarness: ./scripts/test-guard-sigpipe.sh
  TestHarness->>GuardScript: run guard | true
  GuardScript->>GuardScript: compute verdict, emit() with trap '' PIPE
  GuardScript-->>TestHarness: exit status 0
  TestHarness->>GuardScript: run guard | head -1
  GuardScript->>GuardScript: compute verdict, emit() with trap '' PIPE
  GuardScript-->>TestHarness: exit status 0
  TestHarness-->>Developer: report guards survive SIGPIPE/EPIPE
Loading

File-Level Changes

Change Details Files
Buffer failure/diagnostic output in several guard scripts and emit it once at the end under a SIGPIPE‑ignored context so verdicts are decided before any writes.
  • Introduce report accumulator variables and note()/plain() helpers to append lines instead of echoing immediately in multiple scripts.
  • Add emit() helpers that set trap '' PIPE and printf the buffered report to stderr with failures ignored via
Guard final OK/status lines and some non-fatal informational output in guard scripts so closed pipes cannot flip green runs red.
  • Wrap final echo/printf OK lines in trap '' PIPE and append
Fix subshell/pipefail interactions and membership checks in role/god-file guards to avoid races and subshell-lost state.
  • Refactor check-role-names.sh to avoid while-read-in-pipeline subshell by using process substitution and directly updating fail in the parent shell.
  • Remove the temporary flagfile mechanism in check-role-names.sh that tracked failures via a marker file, relying on in-process fail instead.
  • Replace printf
grep -qx membership tests in check-god-files.sh with a pure-shell case-based contains function over newline-delimited lists to avoid grep -q early-exit SIGPIPE races.
Add a guard SIGPIPE regression test harness and Makefile target to exercise all relevant guards under early‑closing readers.
  • Introduce scripts/test-guard-sigpipe.sh, which runs selected guard scripts piped into true and head -1, capturing PIPESTATUS[0] and asserting a zero exit code on green trees.
  • Special-case parameterized and expensive guards: invoke check-empty-diff.sh with HEAD~1 HEAD, gate check-wire-schema.sh on cargo presence, and exercise check-god-files.sh multiple times to detect prior intermittent races.
  • Add a make guard-sigpipe-test phony target that runs the new test harness and documents it as hermetic and not part of gate.
scripts/test-guard-sigpipe.sh
Makefile

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Base automatically changed from fix/1815-guards-survive-sigpipe to main August 6, 2026 18:34
@macanderson
macanderson merged commit fccd775 into main Aug 6, 2026
4 checks passed
@macanderson
macanderson deleted the fix/1838-guard-epilogue-residue branch August 6, 2026 19:07
macanderson added a commit that referenced this pull request Aug 6, 2026
… and a duplicate Makefile target (#1904)

## Problem

Local `make gate` on main (`11c4ca1e`) after the ~20-PR merge flurry
(merged untested during the GitHub Actions outage, #1899) found
stella-pipeline **does not compile**: the Spend-struct refactor and
#1789's degradable budget-abort arms merged textually clean but
semantically never met — both `Aborted` arms in `witness_stage.rs` still
referenced the pre-refactor `total`/`budget` bindings (E0425 ×4). This
one break cascades: `lint`, `test`, `doc-warnings`, and the
`wire-schema` serve-frame exporter all go red behind it.

Separately, PR #1844 and its stacked PR #1881 each added the identical
`guard-sigpipe-test` Makefile target, so every `make` invocation warns
about the override.

## Fix

- Align the two `Aborted` arms with their sibling `Completed` arms:
`*spend.total += cost_usd` and `budget_abort(spend.budget.evaluate())`.
Restores exactly the intent both parent PRs tested independently.
- Delete the duplicate Makefile target block (kept the first).

## Verification (local — Actions is in a major outage, stated per
policy)

- `cargo check -p stella-pipeline -j 2`: clean (main: E0425 ×4).
- `cargo test -p stella-pipeline -j 2 witness`: **123 passed, 0 failed**
— the entire witness-stage suite, which cannot even compile on main, is
the witness here; a new test would be dishonest (no new behavior, only
the restoration of two intents that were each already tested).
- `cargo clippy -p stella-pipeline --all-targets -- -D warnings`: clean.
- `make` no longer emits the override warning.

## Remaining main breaks (sibling PRs, from the same gate run)

- `toolset.rs` new god file (1756 > 1500) + the
`MAX_SERVER_SCHEMA_BYTES` → private `crate::client::ingest` rustdoc link
— in the toolset split PR.

Refs #1899.

## Summary by Sourcery

Fix witness-stage budget handling after a spend-struct refactor and
clean up a duplicate Makefile target.

Bug Fixes:
- Correct witness-stage aborted-turn accounting to update the spend
tracker and evaluate the budget from the spend structure, restoring
compilation and intended budget behavior.

Build:
- Remove a duplicated guard-sigpipe-test Makefile target to eliminate
override warnings on make runs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gate: the remaining buffered guards still exit 141 when stdout closes before the final OK line

1 participant