fix(#2494): capture stderr and guard empty output on the claude and gemini reviewer legs (#2592)

* fix(#2494): capture stderr and guard empty output on the claude and gemini reviewer legs

The gemini and claude blocks in gsd-core/workflows/review.md were the only
two of the ten prompt-fed reviewer legs with both `2>/dev/null` and no
empty-output guard. Any failure that wrote no stdout — CLI missing,
unauthenticated, rate-limited, crashed — left a zero-byte review file with
the only diagnostic evidence already discarded.

write_reviews substitutes each file's raw content into a `## <Reviewer>
Review` section with no marker separating "empty/failed" from "ran cleanly,
nothing to report", so a failed lane silently degraded the advertised
N-reviewer consensus to N-1 while present_results reported success. Both
/gsd:review and /gsd:plan-review-convergence share the invoke_reviewers
step, so both were affected.

Both legs now redirect stderr to a `.err` sidecar and write a diagnostic
stub with the captured stderr appended when the review file comes back
empty — the same shape the codex and cursor legs already use.

Scope limit, noted in the block comment: the guard only runs if the block
itself completes. A host Bash-tool timeout that kills the whole block skips
it, the same hard bound the OpenCode block already documents. The existing
timeout guidance (#2194) covers that case and is unchanged.

Regression test extracts the two dispatch blocks verbatim from the workflow
and runs them under bash against a failing CLI stub, asserting the review
file is non-empty and carries a diagnosable message plus the captured
stderr. It fails against pre-fix review.md (4 of 5 cases).

Golden install-parity fixtures and the workflow size baseline are
regenerated: one review.md hash per fixture, one size entry.

Fixes #2494

* fix(#2494): stamp the changeset fragment with the filed PR number

The fragment was authored with the documented `pr: 0` placeholder because
the PR number does not exist until `gh pr create` returns. Now that this
PR is #2592, stamp it — scripts/changeset/parse.cjs requires pr > 0, so
the placeholder would fail the Changeset Required check.
This commit is contained in:
0xdhx
2026-07-24 11:50:34 -05:00
committed by GitHub
parent bf9fe4630d
commit bd18a593a9
23 changed files with 253 additions and 24 deletions

View File

@@ -277,19 +277,39 @@ For each selected CLI, invoke in sequence (not parallel — avoid rate limits):
**Gemini:**
```bash
# #2494: capture stderr to a .err sidecar (not /dev/null) and stub an empty
# result, mirroring the Codex and Cursor blocks. Without the guard a failed
# lane — CLI missing, unauthenticated, rate-limited, crashed, or any exit that
# writes no stdout — leaves a zero-byte file that write_reviews renders as a
# reviewer that ran cleanly with nothing to report, silently dropping a lane
# from the cross-AI consensus while present_results reports success.
# Scope limit: the guard only runs if this block completes. A host Bash-tool
# timeout that kills the whole block skips it, the same hard bound the
# OpenCode block documents below — per the timeout guidance above, treat an
# empty result on a slow lane as a dropped lane rather than a crash.
if [ -n "$GEMINI_MODEL" ] && [ "$GEMINI_MODEL" != "null" ]; then
cat {run_dir}/gsd-review-prompt.md | gemini -m "$GEMINI_MODEL" -p - 2>/dev/null > {run_dir}/gsd-review-gemini.md
cat {run_dir}/gsd-review-prompt.md | gemini -m "$GEMINI_MODEL" -p - 2>{run_dir}/gsd-review-gemini.err > {run_dir}/gsd-review-gemini.md
else
cat {run_dir}/gsd-review-prompt.md | gemini -p - 2>/dev/null > {run_dir}/gsd-review-gemini.md
cat {run_dir}/gsd-review-prompt.md | gemini -p - 2>{run_dir}/gsd-review-gemini.err > {run_dir}/gsd-review-gemini.md
fi
if [ ! -s {run_dir}/gsd-review-gemini.md ]; then
echo "Gemini review failed or returned empty output. stderr:" > {run_dir}/gsd-review-gemini.md
cat {run_dir}/gsd-review-gemini.err >> {run_dir}/gsd-review-gemini.md
fi
```
**Claude (separate session):**
```bash
# #2494: same guard as the Gemini block above — stderr to a .err sidecar
# instead of /dev/null, and a diagnostic stub when the lane produces nothing.
if [ -n "$CLAUDE_MODEL" ] && [ "$CLAUDE_MODEL" != "null" ]; then
cat {run_dir}/gsd-review-prompt.md | claude --model "$CLAUDE_MODEL" $CLAUDE_EFFORT_ARGS -p - 2>/dev/null > {run_dir}/gsd-review-claude.md
cat {run_dir}/gsd-review-prompt.md | claude --model "$CLAUDE_MODEL" $CLAUDE_EFFORT_ARGS -p - 2>{run_dir}/gsd-review-claude.err > {run_dir}/gsd-review-claude.md
else
cat {run_dir}/gsd-review-prompt.md | claude $CLAUDE_EFFORT_ARGS -p - 2>/dev/null > {run_dir}/gsd-review-claude.md
cat {run_dir}/gsd-review-prompt.md | claude $CLAUDE_EFFORT_ARGS -p - 2>{run_dir}/gsd-review-claude.err > {run_dir}/gsd-review-claude.md
fi
if [ ! -s {run_dir}/gsd-review-claude.md ]; then
echo "Claude review failed or returned empty output. stderr:" > {run_dir}/gsd-review-claude.md
cat {run_dir}/gsd-review-claude.err >> {run_dir}/gsd-review-claude.md
fi
```