From 791af4124f9754ae40aecca2b22b00b613dafb23 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Mon, 24 Aug 2026 13:16:34 -0700 Subject: [PATCH 1/2] fix(cursor-review): give the judge the same timeout as the panel it adjudicates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `consolidate` job was capped at 15 minutes while `review` — one panel cell — gets 30. The judge is the strictly harder call: it reads all 8 cell artifacts with the same top reasoning tier a single cell uses. That asymmetry made the failure mode the expensive one. A diff slow enough to stretch the panel stretches the judge with it, so all 8 cells would run green for ~25 minutes and then the judge would be killed at the last step, discarding every cell's work with nothing posted to the PR. It also hid itself: GitHub reports a `timeout-minutes` kill as `cancelled` rather than `failed`, so the run page reads as though a human stopped it. Observed twice in one day on the cloud repo, both at 15m0Xs — about 3% of recent runs burning a full panel and throwing the result away. Co-Authored-By: Claude Opus 5 --- .github/workflows/cursor-review.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cursor-review.yml b/.github/workflows/cursor-review.yml index ee7daa4..d4ce030 100644 --- a/.github/workflows/cursor-review.yml +++ b/.github/workflows/cursor-review.yml @@ -1500,7 +1500,17 @@ jobs: needs: [gate, diff-size, preflight, review, ledger] if: always() && needs.gate.outputs.should_run == 'true' && needs.gate.outputs.already_reviewed != 'true' && needs.diff-size.outputs.within_cap == 'true' && needs.review.result != 'skipped' runs-on: ubuntu-latest - timeout-minutes: 15 + # At least the `review` cap above, never under it. The judge reads all 8 + # cell artifacts with the same top reasoning tier one cell uses, so it is + # the strictly harder call — yet at 15m it was capped at HALF the panel it + # adjudicates. A diff slow enough to stretch the panel stretches the judge + # with it, so the failure mode was the expensive one: all 8 cells run green + # for ~25m, then the judge is killed at the last step and every cell's work + # is discarded with nothing posted to the PR. GitHub reports a + # timeout-minutes kill as `cancelled`, not `failed`, so it reads on the run + # page like a human stopped it — which is how this went unnoticed. Observed + # twice in one day on Comfy-Org/cloud, both at 15m0Xs (~3% of runs). + timeout-minutes: 30 permissions: contents: read pull-requests: write From 598cc565e2a2a040c432b4711a651d96a6425e64 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Mon, 24 Aug 2026 13:31:09 -0700 Subject: [PATCH 2/2] fix(cursor-review): time-box the judge STEP, and stop naming a consumer repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups on the judge timeout fix. Raising the `consolidate` JOB cap alone only moved the cliff: a `timeout-minutes` kill at the job level takes `Build consolidated findings file` and `Post review` with it, so a hung judge still burns the whole budget and posts nothing — the one failure mode that can never reach the degraded panel-union fallback the workflow already has. Time-box `Run judge` itself instead, `continue-on-error` so the timeout is absorbed, and leave the job cap above it — the same step-under-job idiom the `ledger` job uses. A hung judge now costs the adjudication, not the panel's 8 cells. The step cap is the `review` panel cap (30m), and the job cap sits 10m over it to cover this job's own overhead (fetch-depth: 0 checkout, CLI install, three artifact downloads, the post), so the judge's effective MODEL budget is at least a single cell's rather than smaller than one. Also drops the consumer repo name from the job comment. This repo is public and deliberately not self-enrolled in public-repo-hygiene, so nothing would have caught it; the observation is kept, anonymized. And cross-references the coupling at the `review` cap, so a future bump there does not silently recreate the skew. --- .github/workflows/cursor-review.yml | 41 +++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cursor-review.yml b/.github/workflows/cursor-review.yml index d4ce030..0529d78 100644 --- a/.github/workflows/cursor-review.yml +++ b/.github/workflows/cursor-review.yml @@ -1223,6 +1223,13 @@ jobs: needs: [gate, diff-size, preflight, ledger] if: needs.gate.outputs.should_run == 'true' && needs.gate.outputs.already_reviewed != 'true' && needs.diff-size.outputs.within_cap == 'true' runs-on: ubuntu-latest + # COUPLED to `consolidate` below — bump this and you must bump those too. + # The judge reads all 8 of this panel's artifacts at the same reasoning + # tier, so it needs at least a cell's budget: its `Run judge` step cap + # tracks THIS number, and its job cap sits above the step so a hung judge + # trips the step (degraded fallback still posts) rather than the job (all + # 8 cells discarded). Letting this cap outgrow them recreates exactly the + # silent skew that fix was for. timeout-minutes: 30 permissions: contents: read @@ -1500,17 +1507,28 @@ jobs: needs: [gate, diff-size, preflight, review, ledger] if: always() && needs.gate.outputs.should_run == 'true' && needs.gate.outputs.already_reviewed != 'true' && needs.diff-size.outputs.within_cap == 'true' && needs.review.result != 'skipped' runs-on: ubuntu-latest - # At least the `review` cap above, never under it. The judge reads all 8 - # cell artifacts with the same top reasoning tier one cell uses, so it is - # the strictly harder call — yet at 15m it was capped at HALF the panel it + # ABOVE the `review` cap, never under it. The judge reads all 8 cell + # artifacts with the same top reasoning tier one cell uses, so it is the + # strictly harder call — yet at 15m it was capped at HALF the panel it # adjudicates. A diff slow enough to stretch the panel stretches the judge # with it, so the failure mode was the expensive one: all 8 cells run green # for ~25m, then the judge is killed at the last step and every cell's work # is discarded with nothing posted to the PR. GitHub reports a # timeout-minutes kill as `cancelled`, not `failed`, so it reads on the run # page like a human stopped it — which is how this went unnoticed. Observed - # twice in one day on Comfy-Org/cloud, both at 15m0Xs (~3% of runs). - timeout-minutes: 30 + # twice in one day on a consumer repo, both at 15m0Xs (~3% of runs). + # + # Deliberately longer than the `Run judge` step cap below, same idiom as + # the `ledger` job: raising the JOB cap alone would only move the cliff, + # since a job-level kill takes `Build consolidated findings file` and + # `Post review` with it and is the one failure mode that can never reach + # the degraded panel-union fallback. The step trips first and is absorbed, + # so a hung judge costs the adjudication, not the panel. The slack over + # the step (10m) covers this job's own overhead — a `fetch-depth: 0` + # checkout, the CLI install, three artifact downloads and the post — which + # a panel cell does not all pay, so the judge's effective MODEL budget is + # the step cap, not this one. + timeout-minutes: 40 permissions: contents: read pull-requests: write @@ -1731,6 +1749,19 @@ jobs: - name: Run judge if: steps.aggregate.outputs.ok_count != '0' + # Time-boxed BELOW the job cap and absorbed, so a hung judge loses only + # the adjudication: the steps below then read the `--init` seed left by + # `Configure structured final review` (status=error), take the degraded + # panel-union path and still post what the 8 cells found. Equal to the + # `review` panel cap so the judge is never given less model time than + # one of the cells it reads. `continue-on-error` is what makes the + # timeout absorbable — without it the step failure fails the job and + # lands back on the all-cells-discarded outcome this is fixing. It + # hides no real signal: the cursor-agent exit code is already captured + # in-script, and a judge that did not submit is reported to the PR as + # `judge_status` by `Post review`. + continue-on-error: true + timeout-minutes: 30 env: CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} run: |