fix(runtime): surface harness stderr on non-zero exit so failures aren't blank

Merged
WI
4mo ago
+74-3

Problem

When a harness run fails, the persona sees a blank error. A real production failure looked like:

{ "error": "grok harness failed (exit 1): ", "durationMs": 8 }

The persona builds that message from run.output:

if (run.exitCode !== 0) {
  throw new Error(`grok harness failed (exit ${run.exitCode}): ${truncate(run.output, …)}`);
}

…but run.output was empty, so the actual cause was invisible.

Root cause

createProcessHarnessRunner returns only stdout in HarnessRunResult.output. On failure the reason almost always lands on stderr:

  • CLI auth/usage errors print to stderr.
  • Spawn failures (spawn grok ENOENT when the binary is missing) surface via child.on('error'), which spawnAndCapture reports as exitCode: 1 with the message on stderr and an empty stdout.

spawnAndCapture captured stderr, but the runner dropped it — so the diagnostic never reached the caller. (This is exactly what hid the cloud#2195 grok-binary-missing failure: an 8ms ENOENT with a blank message.)

Fix

  • Add an optional HarnessRunResult.stderr (trimmed, when non-empty) so callers can read it explicitly.
  • On a non-zero exit, fold trimmed stderr into output so existing callers that only read run.output see the cause. On success, output is left untouched (harnesses routinely log progress/warnings to stderr — that must not pollute output).
  • Extracted the fold into an exported pure helper foldHarnessFailureOutput and unit-tested it. The integration seam can't be used directly because the spawned binary is derived from the harness type, not injectable.

With this, the same failure would have read:

grok harness failed (exit 1): spawn grok ENOENT

— immediately pointing at the missing binary.

Tests

4 new unit tests (ENOENT/empty-stdout, partial-stdout+stderr, success-with-noisy-stderr stays clean, failure-with-empty-stderr). Full runtime suite green:

# tests 110
# pass 110

Related

  • AgentWorkforce/cloud#2195 — the actual grok failure this blank error was hiding (grok CLI missing from the Daytona snapshot builder).

🤖 Generated with Claude Code