Repository navigation
Conversation
A reader that exits before the CLI finishes writing (for example `| head`) made the stdout write fail with EPIPE. The CLI crashed with "Unknown: Stdio.stdout", and in JSON output it then wrote the error envelope to the same closed stdout and hung forever. The shared stdio writer now treats EPIPE as the end of that stream: the failed write succeeds, later writes to the stream are skipped, and the command finishes with exit 0. Other write failures still surface as defects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/ai-review |
There was a problem hiding this comment.
Superseded by a newer AI review
🤖 AI Review
Confirmed Claude's single test-coverage nit. Codex reported no findings, and code inspection found no additional correctness issues. Tests were not run; dependencies are absent from the checkout.
Findings
| Severity | Location | Category | Sources | Claim |
|---|---|---|---|---|
| ⚪ NIT | apps/cli/src/shared/output/output.layer.unit.test.ts:804 |
test-coverage |
claude | The new failure tests exercise only jsonOutputLayer. They do not directly cover EPIPE handling through the text layer's spinner wrapper or verify that stderr remains writable after stdout closes. |
Stats
Claude findings: 1 · Codex findings: 0 · Confirmed: 1 · Refuted: 0 · Uncertain: 0
Models: claude-opus-5-5 + gpt-6.1-sol · Trigger: manual · Workflow run
This review runs once per PR. A maintainer can request another with a /ai-review comment.
There was a problem hiding this comment.
Superseded by a newer AI review
🤖 AI Review
Both independent reviews completed; Codex reported no findings. Code inspection confirms Claude's streaming-lifecycle regression and test-coverage nit. Runtime tests were not run because dependencies are absent.
Findings
| Severity | Location | Category | Sources | Claim |
|---|---|---|---|---|
| 🟠 MAJOR | apps/cli/src/shared/output/output.layer.ts:76 |
error-handling |
claude | Swallowing stdout EPIPE without signalling cancellation leaves streaming commands such as `supabase stack logs --follow |
| ⚪ NIT | apps/cli/src/shared/output/output.layer.unit.test.ts:804 |
test-coverage |
claude | The new EPIPE and EIO regression tests exercise only jsonOutputLayer, leaving text and stream-json without explicit coverage of the broken-pipe contract. |
Stats
Claude findings: 2 · Codex findings: 0 · Confirmed: 2 · Refuted: 0 · Uncertain: 0
Models: claude-opus-5-5 + gpt-6.1-sol · Trigger: auto · Workflow run
This review runs once per PR. A maintainer can request another with a /ai-review comment.
Skipping writes after a broken pipe left follow-style commands such as `stack logs --follow | head` running with their output discarded. A closed stdout now ends the command with StdoutClosedError, which carries exit code 0 through every finalizer, so the run stops cleanly. The closed state is shared by every output layer on the same process streams, so the final error renderer can no longer hang on a dead stdout. A closed stderr only drops diagnostics. Broken-pipe behavior is covered across the text, json and stream-json layers, and an e2e test closes the real stdout pipe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/ai-review |
There was a problem hiding this comment.
Superseded by a newer AI review
🤖 AI Review
Verified all four Claude findings against the PR code. Confirmed both error-handling gaps and the post-success work after stdout closes. Refuted the WeakMap-comment finding because the root command provides its own Output layer. Codex's independent review completed with no findings. Runtime tests were unavailable because dependencies are absent.
Findings
| Severity | Location | Category | Sources | Claim |
|---|---|---|---|---|
| 🟠 MAJOR | apps/cli/src/shared/output/output.layer.ts:87 |
error-handling |
claude | A failed command can exit 0 when its JSON error envelope is the first write to closed stdout. The StdoutClosedError defect aborts withJsonErrorHandling before it records the original error's exit code. |
| 🟡 MINOR | apps/cli/src/shared/output/output.layer.ts:87 |
error-handling |
claude | A first stdout EPIPE during the runner's final JSON error rendering escapes as an uncaught StdoutClosedError instead of terminating quietly through processControl.exit. |
| 🟡 MINOR | apps/cli/src/shared/output/errors.ts:38 |
behavior |
claude | Aborting command execution on closed stdout still triggers post-success hooks and queued success trailers, potentially performing a release check and writing notices after the output abort. |
Refuted findings (kept for transparency, not posted as review comments)
apps/cli/src/shared/output/output.layer.ts:66(documentation): The WeakMap comment overstates its purpose because command execution and final-error rendering use the same Output instance.
Refuted: The finding overlooks the root command's Command.provide layer. root.ts:254-264 supplies command output independently of the runner's layer, including a distinct quiet-progress wrapper for machine output flags. The claimed absence of command-level Output provisioning is contradicted by this code.
Stats
Claude findings: 4 · Codex findings: 0 · Confirmed: 3 · Refuted: 1 · Uncertain: 0
Models: claude-opus-5-5 + gpt-6.1-sol · Trigger: manual · Workflow run
This review runs once per PR. A maintainer can request another with a /ai-review comment.
A failing command whose JSON error envelope was the first write to a closed stdout could exit 0, because the envelope's StdoutClosedError replaced the failure before its exit code was recorded. The exit code is now set before the envelope is written, and a closed stdout while rendering the final error is ignored instead of escaping as an uncaught defect. A run that ends because stdout closed now skips the failure report, the post-success hook and the success trailers, since nobody is left to read them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/ai-review |
There was a problem hiding this comment.
Superseded by a newer AI review
🤖 AI Review
Verified all three findings against the checked-out code. The duplicate exit-code reports merge into one confirmed major bug; the E2E assertion gap is also confirmed as minor.
Findings
| Severity | Location | Category | Sources | Claim |
|---|---|---|---|---|
| 🟠 MAJOR | apps/cli/src/shared/cli/run.ts:752 |
error-handling |
claude+codex | A broken pipe can suppress an independent command or cleanup failure and produce exit 0 when no explicit exit code was recorded. In text mode, it also suppresses the failure diagnostic despite stderr remaining open. |
| 🟡 MINOR | apps/cli/src/shared/cli/run.e2e.test.ts:143 |
test-coverage |
codex | The successful broken-pipe E2E cases would allow a leaked StdoutClosedError diagnostic. |
Stats
Claude findings: 1 · Codex findings: 2 · Confirmed: 2 · Refuted: 0 · Uncertain: 0
Models: claude-opus-5-5 + gpt-6.1-sol · Trigger: manual · Workflow run
This review runs once per PR. A maintainer can request another with a /ai-review comment.
A closed stdout skipped every report, so a typed failure or a cleanup defect in the same run could exit 0 without a diagnostic. The runner now sets aside only the closed stdout and the interruptions it caused, and classifies and reports whatever failure remains. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/ai-review |
There was a problem hiding this comment.
Superseded by a newer AI review
🤖 AI Review
Reconciled both review outputs against the checked-out code and trusted conventions. Claude's single minor finding remains uncertain: the EPIPE-only check is verified, but the claimed alternative errors on supported runtimes are unproven. Codex reported no findings, and this pass found no additional concrete issues. Tests were not run because dependencies are absent.
Findings
| Severity | Location | Category | Sources | Claim |
|---|---|---|---|---|
| 🟡 MINOR | apps/cli/src/shared/output/output.layer.ts:61 |
error-handling |
claude | Closed-reader errors reported as ERR_STREAM_DESTROYED or EOF would bypass the EPIPE-only check and become generic Stdio defects. The initial closed-stream check would not protect writes already in flight. |
Stats
Claude findings: 1 · Codex findings: 0 · Confirmed: 0 · Refuted: 0 · Uncertain: 1
Models: claude-opus-5-5 + gpt-6.1-sol · Trigger: manual · Workflow run
This review runs once per PR. A maintainer can request another with a /ai-review comment.
libuv reports a write to a pipe whose reader exited as EOF on Windows, so that case now ends output the same way as EPIPE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jgoux
left a comment
There was a problem hiding this comment.
Two findings inline: the failure-path e2e cases cannot detect a hang regression (P2), and a co-occurring closed stdout masks a real failure's telemetry classification (P3).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/ai-review |
There was a problem hiding this comment.
Superseded by a newer AI review
🤖 AI Review
Confirmed both Claude findings as minor issues: clack-based text output bypasses the broken-pipe handler, and closed-stdout termination is recorded as a telemetry failure despite exiting successfully. Codex reported no findings. Verification was by code inspection; tests were not run.
Findings
| Severity | Location | Category | Sources | Claim |
|---|---|---|---|---|
| 🟡 MINOR | apps/cli/src/shared/output/output.layer.ts:86 |
correctness |
claude | The new broken-pipe handling does not cover clack-based text output. The text layer's info, warn, error, success, event, and non-TTY task messages bypass stdioWriter, so these writes neither update the shared closed-stream state nor terminate through StdoutClosedError. |
| 🟡 MINOR | apps/cli/src/shared/output/errors.ts:37 |
telemetry |
claude | A command terminated solely by closed stdout records telemetry exit_code 1 even though runCli exits 0, inflating the recorded command failure rate. |
Stats
Claude findings: 2 · Codex findings: 0 · Confirmed: 2 · Refuted: 0 · Uncertain: 0
Models: claude-opus-5-5 + gpt-6.1-sol · Trigger: manual · Workflow run
This review runs once per PR. A maintainer can request another with a /ai-review comment.
Clack log lines wrote to process.stdout directly, so a closed reader crashed the process with an uncaught EPIPE. Command telemetry now records exit 0 when a closed stdout alone ended the command, matching the process exit code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/ai-review |
There was a problem hiding this comment.
🤖 AI Review
Reviewed both finding sets against the PR code and trusted conventions. Confirmed that text task progress and intro/outro bypass the shared broken-pipe handling. The terminal-width regression remains uncertain: the buffer lacks terminal metadata, but clack's source is unavailable and network access failed. No duplicates or additional findings were found. Tests were not run because dependencies are absent.
Findings
| Severity | Location | Category | Sources | Claim |
|---|---|---|---|---|
| 🟡 MINOR | apps/cli/src/shared/output/output.layer.ts:314 |
behavior-change |
claude | The buffer supplied to clack lacks terminal width and TTY metadata, potentially changing wrapping of text log messages on interactive terminals. |
| 🟡 MINOR | apps/cli/src/shared/output/output.layer.ts:353 |
error-handling |
claude | Text task progress and intro/outro still bypass the shared writer, leaving broken-pipe handling incomplete for commands using these output paths. |
Findings outside the diff
- 🟡 MINOR
apps/cli/src/shared/output/output.layer.ts:353— Text task progress and intro/outro still bypass the shared writer, leaving broken-pipe handling incomplete for commands using these output paths.
Stats
Claude findings: 2 · Codex findings: 0 · Confirmed: 1 · Refuted: 0 · Uncertain: 1
Models: claude-opus-5-5 + gpt-6.1-sol · Trigger: manual · Workflow run
This review runs once per PR. A maintainer can request another with a /ai-review comment.
TL;DR
When the process reading the CLI's output exits early (
supabase … | head, or an agent that stops reading), the CLI crashed withUnknown: Stdio.stdoutand exited 1. With JSON output and a reader that closed before the first write, it hung forever. Now a closed stdout ends the command cleanly with exit 0, the way a Unix tool stops on a broken pipe.Before
After
Why
Since #6212, all three output layers write through the Effect
Stdiosink. The sink reports a write to a closed pipe asPlatformError: Unknown: Stdio.stdout(causeEPIPE), and the shared writer turned that into a defect. It shows up across many commands (db query,status,functions serve,migration list, …), mostly from agents.In JSON output the run's error renderer then writes the error envelope to the same closed stdout. A closed stream never emits
drainorerroragain, so that write can wait forever. 2.120.0 behaves the same way, so this is not a regression from the latest release.What changed
StdoutClosedError. The error carries exit code 0 through every finalizer, so follow-style commands such asstack logs --follow | headstop too, and no error is printed.tag:StdoutClosedErrorfingerprint with thecancelledcategory.Terminal captures
Recorded with
vhsagainst a local stack, runningdevelop(before) and this branch (after) from source.exit=shows the CLI's own exit code from$pipestatus.Text output,
| head -n 3JSON output, reader closed before the first write
🤖 Generated with Claude Code