From 8f7a20fefaa8401724f07d37ceadab355826e30c Mon Sep 17 00:00:00 2001 From: kjgbot Date: Tue, 8 Sep 2026 03:27:24 +0200 Subject: [PATCH 1/2] fix(review-swarm): print why the swarm failed, not just that it did The gate polls `agent-relay cloud status --json`, reads `.status` off the response and throws the rest away. When a swarm fails, the only thing that reaches the GitHub log is the word: Review swarm did not complete successfully: failed The actual reason is already in the payload the poll just fetched. For the five failing runs on 2026-09-07 it was: Step "lens-maintainability" failed after 2 retries: Total CPU limit exceeded. Maximum allowed: 250. Nothing in the log said "quota". Diagnosing it meant knowing to pull the run id out of the log and query the run by hand, so the cause went uninvestigated for days while the failures were attributed to a guess. Surface `.result.error` in the wait step, to stderr and to the step summary. Verified against the real 04da7e48 payload: the expression yields the quota text above; a payload without an error yields an empty string rather than the literal "null", and an empty response (the first-call-failed path) is safe. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR --- .github/workflows/review-swarm.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/review-swarm.yml b/.github/workflows/review-swarm.yml index 852f186c4..087909d67 100644 --- a/.github/workflows/review-swarm.yml +++ b/.github/workflows/review-swarm.yml @@ -127,6 +127,24 @@ jobs: sleep 15 done echo "swarm_status=$status" >> "$GITHUB_OUTPUT" + if [ "$status" != completed ]; then + # The status word alone does not say why the swarm failed, and the + # reason never reaches this log: it sits in the run payload we just + # fetched. A quota rejection reads here as a bare "failed", which + # sent one reader inferring for days before querying the run by + # hand. Print what we already have. + reason=$(jq -r '.result.error // .error // empty' <<<"${response:-}" 2>/dev/null) + if [ -n "$reason" ]; then + echo "swarm failure reason: $reason" >&2 + { + echo "### Swarm failure reason" + echo + echo '```' + echo "$reason" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + fi + fi exit 0 - name: Post verdict and transcripts From 99ee41059d1d67661a29013d229df700d924f42e Mon Sep 17 00:00:00 2001 From: kjgbot Date: Tue, 8 Sep 2026 03:57:07 +0200 Subject: [PATCH 2/2] fix(review-swarm): sanitize the failure reason before logging it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cubic P3 on #235, and it is right. The reason string comes from the swarm's status response, which can carry agent output, which can carry content from the PR under review. Two vectors: - a line starting with `::` is parsed by Actions as a workflow command (`::error::`, `::add-mask::`) — log injection; - a line of three backticks closes the fenced block early in the step summary and the remainder renders as markup. Indent every line by four spaces instead of fencing. That defeats both at once: Actions only parses a command at the start of a line, and an indented block is a Markdown code block with no fence to break. Verified against a payload carrying both vectors: no output line starts with `::` or with a fence, and the real quota text still renders intact. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FtQSAcGDta5VH9xiZFT4sR --- .github/workflows/review-swarm.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/review-swarm.yml b/.github/workflows/review-swarm.yml index 087909d67..78d6aad17 100644 --- a/.github/workflows/review-swarm.yml +++ b/.github/workflows/review-swarm.yml @@ -135,13 +135,21 @@ jobs: # hand. Print what we already have. reason=$(jq -r '.result.error // .error // empty' <<<"${response:-}" 2>/dev/null) if [ -n "$reason" ]; then - echo "swarm failure reason: $reason" >&2 + # The reason is not fully trusted. It can carry agent output, + # which can carry content from the PR under review. Two ways that + # bites: a line starting with `::` is parsed by Actions as a + # workflow command, and a line of three backticks would close a + # fenced block early and render the rest as markup. + # Indenting every line defeats both at once — Actions only parses + # a command at the start of a line, and an indented block is a + # Markdown code block with no fence to break. + safe_reason=$(printf '%s\n' "$reason" | sed 's/^/ /') + echo "swarm failure reason:" >&2 + printf '%s\n' "$safe_reason" >&2 { echo "### Swarm failure reason" echo - echo '```' - echo "$reason" - echo '```' + printf '%s\n' "$safe_reason" } >> "$GITHUB_STEP_SUMMARY" fi fi