From 826b0f7ac7d5dd2e2536dc54266388cd18724d36 Mon Sep 17 00:00:00 2001 From: Fuad Tabba Date: Fri, 29 May 2026 15:55:21 +0100 Subject: [PATCH] cli: surface the review subprocess's stderr when it fails `sashiko-cli local` runs the review in a subprocess and streams its stderr only to drive the four-phase progress display, discarding every other line. When the subprocess failed before producing a result, the user saw only "Review subprocess produced no output (exit code: N)" with the actual cause (a stage error, an auth or quota failure, a panic) silently dropped. Keep a bounded tail (the last 100 lines) of the subprocess's stderr while rendering progress, and replay it on the no-output failure path. Successful runs are unchanged (clean progress), while failures now explain themselves. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Fuad Tabba --- src/bin/sashiko-cli.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/bin/sashiko-cli.rs b/src/bin/sashiko-cli.rs index 6378a8659..461e20e20 100644 --- a/src/bin/sashiko-cli.rs +++ b/src/bin/sashiko-cli.rs @@ -1585,9 +1585,16 @@ async fn handle_local( drop(stdin); } - // Stream stderr for progress in a background task + // Stream stderr for progress, while keeping a bounded tail of the + // subprocess's log lines. The progress UI consumes these lines, so + // without the tail a failed review would collapse to a bare "no output" + // message with its actual cause (a stage error, an auth/quota failure) + // discarded. On failure we replay the tail so the run explains itself. let stderr = child.stderr.take(); let stderr_handle = tokio::spawn(async move { + use std::collections::VecDeque; + const STDERR_TAIL_LINES: usize = 100; + let mut tail: VecDeque = VecDeque::with_capacity(STDERR_TAIL_LINES); if let Some(stderr) = stderr { use tokio::io::{AsyncBufReadExt, BufReader}; let reader = BufReader::new(stderr); @@ -1609,8 +1616,13 @@ async fn handle_local( eprint_phase(4, 4, "Review complete."); eprintln!(); } + if tail.len() == STDERR_TAIL_LINES { + tail.pop_front(); + } + tail.push_back(line); } } + tail }); // Capture stdout @@ -1619,12 +1631,17 @@ async fn handle_local( .await .context("Failed to wait for review subprocess")?; - let _ = stderr_handle.await; + let stderr_tail = stderr_handle.await.unwrap_or_default(); let exit_code = output.status.code().unwrap_or(1); let stdout = String::from_utf8_lossy(&output.stdout).to_string(); if stdout.trim().is_empty() { + // The subprocess failed before emitting a result; replay its log + // tail so the actual cause is visible rather than swallowed. + for line in &stderr_tail { + eprintln!("{line}"); + } return Err(anyhow::anyhow!( "Review subprocess produced no output (exit code: {})", exit_code