Skip to content

fix(evals): capture stderr separately so diagnostics cannot corrupt the json envelope - #1896

Merged
Aaronontheweb merged 1 commit into
netclaw-dev:devfrom
Aaronontheweb:skunkworks/eval-capture-split
Aug 13, 2026
Merged

fix(evals): capture stderr separately so diagnostics cannot corrupt the json envelope#1896
Aaronontheweb merged 1 commit into
netclaw-dev:devfrom
Aaronontheweb:skunkworks/eval-capture-split

Conversation

@Aaronontheweb

@Aaronontheweb Aaronontheweb commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

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 check stdout_json_envelope_valid then reads that file as pure JSON, for --json cases. 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_FILE and the new $STDERR_FILE. The fix applies at two call sites:

  • run_prompt, for single-turn cases
  • run_prompt_resume, for multi-turn --resume cases

Stdout stays pure JSON for every --json check. The archive step also copies the stderr file next to the stdout file, in evals/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_contains
  • stdout_response_contains, stdout_response_not_contains
  • stdout_tool_called
  • stdout_json_envelope_valid, stdout_json_tool_called, stdout_json_tool_call_arguments
  • stdout_skill_file_read_called

Case functions that read $STDOUT_FILE directly:

  • assert_coding_context_worktree_handoff
  • assert_approval_set_working_directory_positive
  • assert_approval_set_working_directory_retry

Two commits changed evals/run-evals.sh on dev after this branch split off: PR #1886 and PR #1890. The rebase onto dev applied 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:

Each new function reads only a stdout_contains, stdout_tool_called, or stdout_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.sh passes.
  • dotnet slopwatch analyze finds zero issues.
  • No live eval run happened for this change.
  • A live eval run needs a real model endpoint. This environment has no such endpoint.
  • Verification here stays static: a syntax check and a full code audit, not a live pass/fail comparison.

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.

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
Aaronontheweb force-pushed the skunkworks/eval-capture-split branch from 166a7d2 to 0ffd986 Compare August 13, 2026 01:55
@Aaronontheweb Aaronontheweb changed the title [skunkworks/do-not-merge] fix(evals): separate stdout and stderr capture in the eval harness fix(evals): capture stderr separately so diagnostics cannot corrupt the json envelope Aug 13, 2026
@Aaronontheweb Aaronontheweb added bug Something isn't working evals LLM efficacy evals for Netclaw labels Aug 13, 2026
@Aaronontheweb
Aaronontheweb marked this pull request as ready for review August 13, 2026 01:58
@Aaronontheweb
Aaronontheweb merged commit 19ed641 into netclaw-dev:dev Aug 13, 2026
25 of 26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working evals LLM efficacy evals for Netclaw

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant