fix(evals): capture stderr separately so diagnostics cannot corrupt the json envelope - #1896
Merged
Aaronontheweb merged 1 commit intoAug 13, 2026
Conversation
The eval harness sent stderr into the same file as stdout. A stderr diagnostic line then broke the JSON parse for --json cases. The CLI correctly writes a line like "[error] Unknown output type from daemon: ..." to stderr on some turns. That line is correct CLI behavior, not a CLI bug. The extra line in the stdout file broke stdout_json_envelope_valid and produced a false eval failure. A controlled A/B run proved this: two of three failures for one case came from this exact issue, and one of the two "failed" runs had in fact behaved correctly. This change writes stdout and stderr to two separate files, in both run_prompt (single-turn) and run_prompt_resume (multi-turn --resume). An audit of every assertion helper (stdout_contains, stdout_json_*, and all direct $STDOUT_FILE reads) and every case assertion function found none that reads stderr text from the old shared file. So this change does not alter any case result; it only removes a source of false failures. The archive step now copies the stderr file next to the stdout file for each run, in evals/runs/<run-id>/stdout/. A failed run stays diagnosable with both streams present. Verification: bash -n evals/run-evals.sh passes. dotnet slopwatch analyze finds no new issue. The harness has no self-test or lint mode. A live eval run needs a real model endpoint, so no live eval happened for this change.
Aaronontheweb
force-pushed
the
skunkworks/eval-capture-split
branch
from
August 13, 2026 01:55
166a7d2 to
0ffd986
Compare
Aaronontheweb
marked this pull request as ready for review
August 13, 2026 01:58
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.
Problem
The eval harness ran the CLI under test with a merged redirect:
> "$STDOUT_FILE" 2>&1. This command sent stderr text into the same file as stdout. The checkstdout_json_envelope_validthen reads that file as pure JSON, for--jsoncases. The CLI writes a correct diagnostic to stderr on some turns, for example[error] Unknown output type from daemon: .... That diagnostic shows correct CLI behavior. It is not a CLI defect.The extra line broke the JSON parse. The harness then scored a correct run as a failure. A controlled A/B run proved this defect: two of three "failures" for one case came from this exact issue. One of the two failed runs in fact behaved correctly.
Fix
Stdout and stderr now write to two separate files:
$STDOUT_FILEand the new$STDERR_FILE. The fix applies at two call sites:run_prompt, for single-turn casesrun_prompt_resume, for multi-turn--resumecasesStdout stays pure JSON for every
--jsoncheck. The archive step also copies the stderr file next to the stdout file, inevals/runs/<run-id>/stdout/. A failed run stays diagnosable, because both streams stay present.Audit
The audit read every assertion helper and every case function in the file. It found that none of them read stderr text from the old merged file.
Assertion helpers checked:
stdout_contains,stdout_not_containsstdout_response_contains,stdout_response_not_containsstdout_tool_calledstdout_json_envelope_valid,stdout_json_tool_called,stdout_json_tool_call_argumentsstdout_skill_file_read_calledCase functions that read
$STDOUT_FILEdirectly:assert_coding_context_worktree_handoffassert_approval_set_working_directory_positiveassert_approval_set_working_directory_retryTwo commits changed
evals/run-evals.shondevafter this branch split off: PR #1886 and PR #1890. The rebase ontodevapplied with no conflict. The stream split needed no manual merge.The audit ran a second time, after the rebase. It checked every new case function those two PRs added:
assert_skill_cron_tz_timezoneandassert_grounding_cron_tz_schedule, from PR feat(reminders): surface reminder scheduling failures loudly (Tier 1) #1886assert_approval_session_scratch_disposable, from PR Add typed shell approval store schema 3 #1890Each new function reads only a
stdout_contains,stdout_tool_called, orstdout_json_*helper, or the daemon log. None of them read stderr text. This change does not alter the result of any case, old or new.Validation
bash -n evals/run-evals.shpasses.dotnet slopwatch analyzefinds zero issues.Companion
PR #1895 carries the product-side half of this same fix. That PR blocks a protocol diagnostic at the source. A protocol diagnostic then never reaches stdout. This PR closes the harness gap that let a stderr diagnostic corrupt the stdout capture.