fix(eval): capture stdout when a scenario subprocess fails - #3375
fix(eval): capture stdout when a scenario subprocess fails#3375kovtcharov-amd wants to merge 1 commit into
Conversation
Every scenario in a failing eval run reported ERRORED with an empty error field, so a CI failure said nothing about what actually went wrong. The runner read only stderr, but `--output-format json` puts the CLI's own error on stdout — stderr is routinely empty on exactly the failures worth reading. It now reports both streams, and falls back to naming the exit code when a process dies silently rather than storing an empty string.
Verdict: ApproveA failing eval scenario now tells you why it failed. The runner only read stderr, but the Claude CLI in JSON mode writes its own error to stdout — so every failed scenario recorded an empty error and CI failures had to be reproduced locally to triage. This reports both streams, and names the exit code when a process dies silently. The change is small, the three new tests pin each branch (stdout-only, both streams, and the never-empty fallback), and the before/after evidence shows the real fix landing. Nothing blocking. One thing worth a follow-up, not a merge blocker: a scenario that times out still records no explanation at all, which is the same triage gap this PR closes for the crash path. Real-world evidenceThe PR ships an evidence bundle, and the fix is exercised through the real All four branches were exercised live — stdout-only/exit 1, both streams/exit 2, silence/exit 3 ( The bundle is explicit that no real LLM turn ran (no Lemonade/GPU on this runner) and that a scorecard-vs-baseline comparison is pending strix-halo lane, and that the failing 🔍 Technical detailsIssues🟢 Minor — a timed-out scenario still records no explanation ( The 🟢 Minor — the JSON-parse failure path still reads stdout only (
Strengths
|
The server is a grandchild — the runner starts `claude -p`, and `claude -p` starts the server — so when it dies the client reports only CONNECTION_CLOSED and the server's own error goes nowhere. Capturing the scenario subprocess's output (#3375) does not reach it either. Three runs of this gate were spent inferring a cause from timings that one line of this log would have stated outright. The launcher execs the real server with stderr tee'd to eval-out/, which the workflow already uploads. stdout is deliberately untouched: it carries the MCP protocol, and one stray byte on it desynchronises the client.
A failing eval run told you nothing. Every scenario reported
ERROREDwith an emptyerrorfield, so triaging a CI failure meant re-running the eval locally and hoping it reproduced.The runner read only stderr — but
--output-format jsonputs the CLI's own error on stdout, so stderr is routinely empty on exactly the failures worth reading. It now reports both streams, and when a process dies silently it names the exit code instead of storing an empty string.Test plan
PYTHONPATH=$(pwd)/src python -m pytest tests/test_eval.py -q— 143 passmain, copytests/test_eval.pyover, re-run: the three new cases fail (test_nonzero_exit_captures_stdout,test_nonzero_exit_captures_both_streams,test_nonzero_exit_error_never_empty)python -m black --checkandpython -m isort --check-onlyon both files — clean