Skip to content

ci: add the stranded-Ally-review reconciler, status-free predicate (BLO-28203) - #1436

Merged
allyblockcast[bot] merged 3 commits into
masterfrom
ci/blo-28203-review-gate-sweep
Aug 23, 2026
Merged

ci: add the stranded-Ally-review reconciler, status-free predicate (BLO-28203)#1436
allyblockcast[bot] merged 3 commits into
masterfrom
ci/blo-28203-review-gate-sweep

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Aug 20, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • Reviewer wakes are how Ally learns a PR needs review; they are fired by the pull_request.opened webhook
  • That path is event-driven and lossy — enqueueWakeup can decline a wake (busy agent, capacity gate, scheduling suppression) and write a terminal status="skipped" row with no run and no reconciler
  • Ally does not patrol the issue board, so a declined wake is never retried: the PR sits unreviewed forever, indistinguishable from one merely waiting its turn. This repo carried the ~13-PR loss set measured on BLO-22892 (2026-08-15/16)
  • Blockcast/trafficcontrol already fixed this with a scheduled reconciler; this repo was left uncovered
  • This pull request ports that reconciler here, on a status-free predicate because this repo has no review/ally-complete gate to key on
  • The benefit is that a lost reviewer wake is re-fired automatically, and if the re-fire also goes unserviced the scheduled job goes red instead of the strand being invisible

Linked Issues or Issue Description

  • Refs Paperclip issue BLO-28203 — "Port the stranded-Ally-review reconciler to paperclip and pim-multicast-gateway"
  • Follow-on from BLO-22892, which landed the original on Blockcast/trafficcontrol (#1383)
  • Sibling PR landing the same script on the other uncovered repo: Blockcast/pim-multicast-gateway#2399
  • Searched this repo for duplicates before opening (sweep / ally / review-gate, all PR states) — nothing existing or prior covers this.

What Changed

  • Adds .github/workflows/review-gate-sweep.yml — scheduled 9,39 * * * *, plus workflow_dispatch. Offset from trafficcontrol (7,37) and pim-multicast-gateway (8,38) so three repos' sweeps do not fire reviewer re-requests at Ally in the same instant.
  • Adds .github/scripts/sweep-stalled-ally-reviews.py — finds non-draft open PRs with no Ally consolidated report at the exact head past a threshold, re-fires via DELETE+POST on requested_reviewers (a bare POST for an already-requested reviewer returns 200 and creates no event, so it delivers no wake), posts a marker comment as audit trail and cooldown input, and exits non-zero when a PR is still stranded past the alarm threshold.
  • Adds .github/scripts/test_sweep_stalled_ally_reviews.py — 56 tests, run as a workflow step.

Two things differ from the trafficcontrol original, both forced by measurement:

  • PREDICATE_MODE: status-free. This repo has no review/ally-complete producer — measured 2026-08-20, 0 of 100 open PRs carry that status. A status-keyed sweep here would classify every PR as "not pending" and silently do nothing forever, which is the failure this reconciler exists to prevent, reintroduced one layer up. Standing up the gate here first was the considered alternative and was rejected: it adds a commit status to a repo with a merge queue, changing merge behaviour for a reconciler meant to be observe-and-re-fire only — larger blast radius, no gain in detection.
  • The head-attestation predicate is inlined, not imported. The original importlibs a sibling require-ally-review.py; this repo has no such file. claude[bot] (id 209825114) is deliberately absent from ALLY_REVIEWER_LOGINS — counting it is the exact false negative that hid trafficcontrol#1448.

Because status-free mode has no status pre-filter to bound the candidate set, MAX_REFIRES_PER_RUN (default 5) caps writes per run so the first run cannot request review on the whole open-PR tail at once. Over-budget PRs are reported as deferred, still counted, and still alarm — a rate limit that silenced the alarm would reinstate the defect being fixed.

Verification

Unit testspython3 -m unittest discover -s .github/scripts -p 'test_sweep_*.py'56 tests, OK. They run as a workflow step, so a broken predicate fails the sweep job rather than silently mis-sweeping. New tests cover unreviewed_since() in both directions, the deferral cap (including that a deferred PR still alarms), --dry-run issuing no write calls, and the mode-aware comment body.

Measured before-state on this repo (dry run of the exact script in this PR; writes nothing):

considered=136 refired=5 alarming=20 failed=0 deferred=18

23 open PRs are stranded right now; 20 are past the alarm threshold, so the first scheduled run will exit non-zero and go red. That red run is the point — the strand becoming visible without anyone checking by hand.

The predicate was verified in both directions against live data before shipping, because a wrong predicate here either spams Ally or suppresses recovery:

direction check result
no false positives all 5 PRs it would re-fire (#1431 #1428 #1424 #1418 #1406) 0 Ally reviews at any head — genuinely never reviewed
no false negatives 6 of 6 sampled PRs it excluded (#1427 #1426 #1425 #1423 #1420 #1417) all carry a real Reviewed head: <sha> attestation at the exact head
drafts 28 drafts in the open set all skipped

Reproduce read-only:

GITHUB_REPOSITORY=Blockcast/paperclip PREDICATE_MODE=status-free \
  python3 .github/scripts/sweep-stalled-ally-reviews.py --dry-run

(with a token exported for GITHUB_TOKEN).

Risks

Low. No merge behaviour changes — this adds no commit status and no required check; it only reads PRs and, for a genuinely stranded one, issues a review request plus one marker comment, throttled by a 2h cooldown and capped at 5 per run.

The one behavioural risk is predicate error, and it is deliberately biased: a false "already reviewed" would suppress recovery and leave the PR stranded silently (the original defect), while a false "not reviewed" costs one redundant review request. The predicate therefore requires positive evidence — the ## Ally … Consolidated PR Review envelope and an exact Reviewed head: match and user.type == "Bot" — rather than erring toward "reviewed". review.commit_id is deliberately not trusted; it is mutable and has been observed reporting a head it never reviewed.

The script is byte-identical to the copy in pim-multicast-gateway#2399; only the workflow configuration differs, so the two cannot drift.

Model Used

Claude (Anthropic) — claude-opus-5[1m], 1M context, extended thinking, run via Claude Code with tool use and code execution.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots — n/a, CI-only
  • I have updated relevant documentation to reflect my changes — the workflow and script carry the rationale inline
  • I have considered and documented any risks above
  • All Paperclip CI gates are green
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups
  • I will address all Greptile and reviewer comments before requesting merge

🤖 Generated with Claude Code

…LO-28203)

Follow-on from BLO-22892, which landed this reconciler on
Blockcast/trafficcontrol (#1383). A reviewer wake fired by
`pull_request.opened` can be silently declined or starved with no retry and
no alert; Ally does not patrol the board, so a lost wake stays lost and the
PR sits unreviewed, indistinguishable from one merely waiting its turn.

This repo carried the ~13-PR loss set measured on BLO-22892 (2026-08-15/16),
so the exposure here is demonstrated, not theoretical.

Runs on a schedule, not an event, so it does not depend on the wake path it
backstops. Exits non-zero once a head has been awaiting review past the alarm
threshold despite an automated re-fire, so the stranded condition is visible
on the Actions tab rather than not visible at all.

PREDICATE_MODE=status-free, and that is the load-bearing decision here.
Unlike trafficcontrol and pim-multicast-gateway, this repo has NO
`review/ally-complete` producer: measured 2026-08-20, 0 of 100 open PRs
carried that status. A status-keyed sweep would classify every PR as "not
pending" and silently do nothing forever -- the exact failure this exists to
prevent, one layer up. So the sweep keys on the load-bearing half instead:
non-draft, open, and no Ally consolidated report at the exact head, past a
threshold.

Standing up the `review/ally-complete` gate here first was the considered
alternative and was rejected: it adds a commit status to a repo with a merge
queue, changing merge behaviour for a reconciler meant to be
observe-and-re-fire only -- larger blast radius, no gain in detection.

Because status-free mode has no status pre-filter to bound the candidate set,
MAX_REFIRES_PER_RUN (default 5) caps writes per run so the first run cannot
request reviews on the whole open-PR tail at once. Over-budget PRs are
reported as deferred and still counted and still alarm -- they are not
dropped, because a rate limit that silenced the alarm would reinstate the
defect this exists to clear.

Measured before-state on this repo (dry run, 2026-08-20):
  considered=136 refired=5 alarming=20 failed=0 deferred=18

The predicate was verified in both directions against this repo before
shipping: all 5 PRs it would re-fire (#1431 #1428 #1424 #1418 #1406) have no
Ally review at any head, and 6 of 6 sampled PRs it excluded carry a real
`Reviewed head: <sha>` attestation at the exact head. 28 drafts skipped.

The head-attestation predicate is inlined rather than imported: the
trafficcontrol original `importlib`s a sibling `require-ally-review.py`, and
this repo has no such file. `claude[bot]` (id 209825114) is deliberately not
in ALLY_REVIEWER_LOGINS -- counting it is the exact false negative that hid
trafficcontrol#1448.

The script is byte-identical to the copy landing on
Blockcast/pim-multicast-gateway; only the workflow env differs, so the two
cannot drift.

56 unit tests pin the pure decision logic and run as a workflow step.

Refs: https://paperclip.blockcast.net/BLO/issues/BLO-28203

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@allyblockcast
allyblockcast Bot requested a review from kkroo as a code owner August 20, 2026 00:22
@allyblockcast

allyblockcast Bot commented Aug 20, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-22892
🔗 Paperclip issue: BLO-28203

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 42dd9b2

This is a carefully argued port and the predicate work is genuinely good — the bias correction (fail toward re-firing), the two-surface check, and the user.type == "Bot" requirement are all right, and the 56 tests pass. I reproduced the test suite (Ran 56 tests ... OK) and the dry-run counts are consistent with the live repo (108 non-draft + 28 draft = 136 open PRs).

The blocking finding is that the reconciler's own alarm — the AC4 mechanism this PR exists to deliver — fails open. I verified it by executing the script from this head, not by reading it.

Critical Issues (1)

  • [native-codex] .github/scripts/sweep-stalled-ally-reviews.py:798 — The run exits 0 (green) even when every PR failed to evaluate, so a systematically broken sweep is indistinguishable from a healthy one. failed is computed at line 745 and rendered into the step summary, but the exit decision keys only on alarming. Per-PR isolation in sweep() records a failure as (pr, head_sha, None, False, "skip: error ...")pending_since is forced to None, and is_alarming() returns False for pending_since is None. So every unevaluated PR is silently laundered into "not alarming".

    Reproduced against the script at this exact head, mocking a 403 rate-limit response for all 30 PRs:

    considered=30 refired=0 alarming=0 failed=30 deferred=0
    === EXIT CODE: 0 ===
    

    Nothing was reconciled, 30 PRs went unevaluated, and the scheduled job reports success. This contradicts the file's own contract in two places — the AC4 comment at line 798 ("the stranded condition must surface without a human noticing by hand") and the summary comment at line 762 ("A PR we could not even evaluate is not 'clean'"). It is also the precise defect class this reconciler exists to eliminate: a silent failure that reads as health.

    Note the ordering makes it worse than the aggregate suggests. GET /pulls?state=open returns newest-first, so when calls start failing partway through, the PRs dropped are the oldest — the most stranded ones, exactly the population the alarm is for.

    • Fail the run on systematic failure, not just on alarming. I'd avoid "any error ⇒ red" (one transient 5xx shouldn't page anyone) — key it on a proportion instead, e.g. exit non-zero when len(failed) > 0 and len(failed) >= max(3, 0.1 * len(results)), and keep a distinct exit message so a failed-sweep red is not misread as a stranded-PR red. Please also add a main()-level test asserting the exit code for the all-failed case; the suite currently covers the reason string (test_failed_pr_is_marked_with_the_error_prefix_not_a_clean_skip) but never asserts the exit status.

Important Issues (3)

  • [gstack/review] .github/scripts/sweep-stalled-ally-reviews.py:214urllib.request.urlopen(req) is called with no timeout=, so it inherits the global default socket timeout, which is None (block forever) — the script never calls socket.setdefaulttimeout(). The workflow also sets no timeout-minutes, and concurrency.cancel-in-progress: false (.github/workflows/review-gate-sweep.yml:55) means queued runs will not displace a hung one. One stalled TCP connection therefore hangs the job to the Actions 6-hour default while every subsequent 30-minute run queues behind it — a multi-hour reconciler outage from a single bad socket. The sibling ally-review-consistency.yml in this repo already sets timeout-minutes: 10.

    • Pass an explicit timeout= (15–30s) to urlopen, and add timeout-minutes: 15 to the sweep job to match the sibling workflow.
  • [pr-review-toolkit/errors] .github/scripts/sweep-stalled-ally-reviews.py:214 — No rate-limit handling or backoff, and the per-run call volume is high enough that exhaustion is a routine outcome rather than an edge case. In status-free mode every non-draft PR costs at least 3 calls (head commit + comments page + reviews page), so with the 108 non-draft PRs measured on this repo: 108 × 3 + 2 list pages + ~20 re-fire writes ≈ 346 calls per run, more where a PR exceeds one page of comments or reviews. At 9,39 * * * * that is ~700 calls/hour from this job alone, against the documented github.token budget of 1,000 requests/hour/repository, shared with the 26 other workflows in .github/workflows. When the limit trips, every remaining PR raises HTTPError — and by the Critical above, the run goes green. These two findings compound; fixing only the exit code leaves the sweep silently degraded, and fixing only the volume leaves the blind spot.

    • Detect 403/429 with x-ratelimit-remaining: 0 and treat it as a distinct fatal outcome (non-zero exit, explicit message) rather than 108 individual per-PR errors. Cheap volume reduction available from the list payload alone: pending_since = max(created_at, committer_date) ≥ created_at, so any PR with now - created_at < STALL_THRESHOLD_SECONDS cannot re-fire — skip its three fetches entirely.
  • [pr-review-toolkit/code] .github/scripts/__pycache__/sweep-stalled-ally-reviews.cpython-313.pyc:1 — Two compiled bytecode artifacts are committed (83KB total: this file plus test_sweep_stalled_ally_reviews.cpython-313.pyc), evidently a side effect of running the tests locally. I confirmed both are present in the tree at this head and that .gitignore contains no __pycache__ or *.pyc entry, so they will be re-staged by anyone who runs the suite and will churn or conflict on every subsequent change to these scripts.

    • Delete both files and add __pycache__/ + *.pyc to .gitignore.

Suggestions (2)

  • [native-codex] .github/scripts/sweep-stalled-ally-reviews.py:743is_alarming({"is_draft": False, "pending_since": r[2]}, now) hardcodes is_draft: False rather than carrying the real flag. Correct today only because _consider_pr leaves pending_since at None for drafts, so the draft branch is unreachable — but that is an invariant held in a different function, and the guard silently stops guarding if it ever changes. Carrying the real is_draft through the result tuple would make the safety local.

  • [gstack/review] .github/scripts/sweep-stalled-ally-reviews.py:559unreviewed_since() uses commit.committer.date, which is client-settable and not guaranteed to be in the past. A head with a future-dated committer timestamp (clock skew, or a rewritten date) yields a negative age, so it can never reach STALL_THRESHOLD_SECONDS or ALARM_THRESHOLD_SECONDS — the PR becomes permanently invisible to both re-fire and alarm. That is the suppression direction the module docstring explicitly identifies as the dangerous one. Clamping with min(candidate, now) would close it.

Strengths

  • The predicate bias argument at ally_has_reviewed_head() is the right call and well documented — requiring positive evidence (consolidated envelope and exact head attestation) so a false "reviewed" cannot suppress recovery, rather than the intuitive-but-backwards direction.
  • Rejecting mutable review.commit_id in favour of the immutable body attestation, with the frr#29 precedent cited, is exactly right.
  • The user.type == "Bot" check on both surfaces correctly separates the App from the allyblockcast User account — a subtle distinction that has caused real false negatives.
  • The DELETE+POST discovery (a bare POST for an already-requested reviewer returns 200 and fires no event) is a genuinely non-obvious API behaviour, measured rather than assumed.
  • Deferred PRs still counting toward considered and still alarming is the right design — a rate limit that silenced the alarm would reinstate the defect.
  • PREDICATE_MODE is well justified: a status-keyed sweep on a repo with no producer would silently no-op forever, and that reasoning is recorded where the next reader will find it.

Recommended Action

  1. Fix the Critical exit-code gap before merge, with a main()-level test asserting the exit status — without it the reconciler's alarm cannot be trusted, which is the PR's central deliverable.
  2. Address the three Important issues this cycle. The timeout and rate-limit items are small changes that materially affect whether this job keeps running unattended; the .pyc removal is a one-liner plus a .gitignore entry.
  3. Consider the two Suggestions opportunistically.

PlatformSREEngineer and others added 2 commits August 21, 2026 09:10
…8203)

Addresses Ally's review of #1436 at 42dd9b2.

CRITICAL -- the reconciler's alarm failed open. main() keyed its exit solely
on `alarming`, but per-PR isolation records an unevaluated PR as
`pending_since=None`, and is_alarming() reads None as not-alarming. So a run
where every PR raised printed `alarming=0 failed=30` and exited 0: a
systematically broken sweep was indistinguishable from a healthy one, which
is the exact "silent failure that reads as health" defect this reconciler
exists to remove, reintroduced one layer up. Reproduced independently before
fixing (30 mocked 403s -> exit 0) and again after (exit 2).

- sweep_is_degraded() keys the new exit on a proportion, not `failed > 0`, so
  one transient 5xx does not page anyone: failed >= max(3, 10%).
- EXIT_ALARM=1 and EXIT_SWEEP_DEGRADED=2 are distinct. Both are red, but "go
  review PR #N" and "the PR list was never read" are different instructions
  and must not be conflated. The alarm is checked first, so a known-stranded
  PR still reports as one. The top-level HTTPError/URLError handlers exit
  DEGRADED too -- reaching them means nothing is known about any PR.
- main() reads is_draft off the PR payload instead of hardcoding False. The
  literal was safe only via an invariant held in _consider_pr.

Rate limits and timeouts, the two ways this job dies unattended:

- Every request is bounded (REQUEST_TIMEOUT_SECONDS, 30s). urlopen's default
  is None -- block forever -- and with `cancel-in-progress: false` one
  stalled socket would hang the job to the Actions 6h ceiling and queue every
  later run behind it. `timeout-minutes: 15` is the outer bound.
- 403/429 carrying `x-ratelimit-remaining: 0` (or retry-after) raises
  RateLimitExhausted, which aborts the loop instead of grinding out one
  identical failure per remaining PR. The unevaluated remainder is still
  recorded as failed, so the run goes red rather than reporting a short list
  it never finished reading. A permissions 403 stays an HTTPError -- that is
  not the budget and retrying never fixes it.
- Cadence halved to hourly. Measured 2026-08-21: 147 open PRs, 119
  non-draft/unlocked -> ~359 requests per run, so `9,39` was ~718/hour
  against github.token's 1,000/hour/repository shared with 26 other
  workflows. Ally's suggested predicate-level cut was measured too and saves
  only 3 of those 359 calls here (one open PR is younger than the stall
  threshold); it is kept because it is free, but cadence is the lever that
  moves the number. ALARM_THRESHOLD_SECONDS goes 4.5h -> 5.5h to absorb the
  coarser granularity and keep "a re-fire and its cooldown have come and
  gone" true before alarming.

Also: clamp commit.committer.date to now. It is client-settable, so a
future-dated head yielded a negative age that could never reach the stall OR
alarm threshold -- permanently invisible to both, the one suppression
direction this module's predicate bias refuses. Thread `mode` through to
build_comment_body. Soften the ALARM_THRESHOLD comment, which claimed the
alarm proves "the re-fire didn't work either" when the code checks only
elapsed time. Drop two committed .pyc artifacts and ignore bytecode.

Tests 56 -> 84, including the main()-level exit-code assertions the review
asked for: all-failed exits DEGRADED, one transient failure still exits 0, a
stranded PR exits ALARM, and a stranded PR outranks a degraded run.

Live dry-run at this head: considered=147 refired=5 alarming=27 failed=0
deferred=22, exit 1.
…as the alarm (BLO-28203)

Follow-up to Ally's review of the sibling port in
Blockcast/pim-multicast-gateway#2399. Two of its three Important findings
do NOT apply to this repo and are deliberately not ported:

- checkout pinning: @v5 is this repo's MAJORITY convention (23 of 49
  checkout usages, vs 2 on @v4), so pinning here would be importing
  another repo's convention, not fixing a defect.
- request/job timeouts: already present -- REQUEST_TIMEOUT_SECONDS on
  every urlopen and timeout-minutes 15 on the sweep job.

What does apply, and is fixed here:

1. The sweep's 1013-line decision suite ran ONLY inside the scheduled
   review-gate-sweep job, where a non-zero exit is the deliberate
   stranded-PR alarm (EXIT_ALARM=1). A test regression therefore read as
   "a PR is stranded", and a change to the reconciler could merge without
   its suite ever running at PR time. Now runs in pr.yml's policy job
   beside the other gate-policy tests.

2. New: a top-level OSError arm. A REQUEST_TIMEOUT_SECONDS expiry during
   the response *read* raises bare TimeoutError (== socket.timeout),
   which is an OSError but NOT a URLError, so it escaped every existing
   arm as an uncaught traceback -- and CPython exits 1, which is
   EXIT_ALARM. The timeout that exists to bound a hung request could
   therefore report itself as "a PR is stranded, go review it", sending a
   human to look for work that does not exist. It now exits
   EXIT_SWEEP_DEGRADED like every other transport failure.

setup-python rather than a bare python3 because the policy job runs on
arc-merge-queue for merge_group events, that scale set is at zero
replicas so its image's python is unverifiable, and this job gates every
PR and merge-queue candidate. setup-python is image-independent and is
this repo's own pattern (docker-figma-bot.yml, docker-webflow-bot.yml),
verified green on the ARC fleet.

Verified: 84 tests pass on 3.10-compatible syntax (no 3.11+ constructs);
pr.yml and review-gate-sweep.yml both parse.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Aug 22, 2026

Copy link
Copy Markdown
Author

Ally review on the sibling port — what ports here, and what deliberately does not

Ally reviewed the sibling port (pim-multicast-gateway#2399) and raised three Important findings. Rather than copy all three across, I checked each against this repo's measured state. Pushed 21169c6.

finding (on pim) applies here? evidence
actions/checkout@v5 should be SHA-pinned v4 no @v5 is this repo's majority convention — 23 of 49 actions/checkout usages, vs 2 on @v4 and 1 SHA-pinned. On pim it was the sole @v5 of 160. Pinning here would be transferring another repo's convention, not fixing a defect.
bound urlopen; add timeout-minutes no — already done this copy already carries REQUEST_TIMEOUT_SECONDS (30s) on every urlopen and timeout-minutes: 15 on the sweep job. The two copies have diverged; this one was already ahead.
run the sweep's test suite at PR time yes same defect, fixed below.

1. The decision suite now runs at PR time

It previously ran only inside the scheduled review-gate-sweep job — where a non-zero exit is the deliberate stranded-PR alarm (EXIT_ALARM = 1). Two consequences: a test regression read as "a PR is stranded", and a change to the reconciler could merge with its 1013-line suite never having run against it at PR time.

Now a step in pr.yml's policy job, beside the other twelve gate-policy tests.

setup-python rather than a bare python3, and the reason is repo-specific: policy runs on arc-merge-queue for merge_group events, that scale set is currently at zero replicas, so its image's python is unverifiable — and this job gates every PR and every merge-queue candidate. setup-python is image-independent and is this repo's own pattern for python (docker-figma-bot.yml, docker-webflow-bot.yml), verified green on the ARC fleet (Set up Python -> success on arc-dind-zpq4z-*). Pinned to 3.10 to match those two; the suite uses no 3.11+ constructs (checked).

On pim the opposite choice was correct — bare python3, because arc-light is an org-level scale set with a python3 step already running green on it there.

2. A socket timeout could report itself as the stranded-PR alarm

Not in Ally's findings — surfaced while implementing the timeout half on pim, and it is worse here precisely because this copy has distinct exit codes.

A REQUEST_TIMEOUT_SECONDS expiry during the response read raises a bare TimeoutError (== socket.timeout). That is an OSError but not a URLError — urllib only wraps the connect phase:

TimeoutError is OSError:   True
TimeoutError is URLError:  False

So it escaped every arm in the __main__ handler as an uncaught traceback, and CPython exits 1 — which is EXIT_ALARM. The timeout that exists to bound a hung request would therefore have announced itself as "a PR is stranded, go review it", sending a human to look for work that does not exist. Exactly the conflation the two distinct exit codes were introduced to prevent.

Now caught and mapped to EXIT_SWEEP_DEGRADED, ordered last so HTTPError/URLError/RateLimitExhausted keep precedence.

Verification

84 tests pass locally; pr.yml and review-gate-sweep.yml both parse. CI at this head is the real check — I will confirm the policy job's Python steps green on this PR rather than assert it.

@allyblockcast

allyblockcast Bot commented Aug 22, 2026

Copy link
Copy Markdown
Author

CI confirmation at 21169c6

As promised above, verified rather than asserted — policy job, run 32602110872, runner arc-light-54ks8-runner-2ccxd:

26. Set up Python for the review-gate sweep suite            -> success
27. Test stalled-Ally-review sweep decision logic (BLO-28203) -> success
28. Validate Designer fast-uri security floor                -> success

Successfully set up CPython (3.10.21)
Ran 84 tests in 0.057s

policy -> completed/success overall, so the added steps did not push the job past its 10-minute budget.

The Ran 84 tests line is the one that matters: unittest discover exits 0 on zero pattern matches, so a green step by itself would not have proved the suite was found. Sibling PR is green the same way (Ran 56 tests, runner arc-light-54ks8-runner-zwpkx).

Two notes on remaining state:

  • behind_by: 73 — deliberately not updating the branch. master here runs a REBASE merge queue, which rebases the candidate onto live master before landing, and an update-branch merge would both eject the PR and make the branch non-rebasable. Topology is clean for that path: all 3 commits single-parent, rebaseable=true.
  • Not enqueuing or merging: this is my own lane's PR and it has had no review at this head.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 21169c6

Prior Findings Dispositioned (4)

  • prior:42dd9b2 critical 1 — fixed — .github/scripts/sweep-stalled-ally-reviews.py:952 — degraded sweeps are detected from failed evaluations and exit with EXIT_SWEEP_DEGRADED instead of reporting a green run.
  • prior:42dd9b2 important 1 — fixed — .github/scripts/sweep-stalled-ally-reviews.py:270 — every API request uses the explicit REQUEST_TIMEOUT_SECONDS timeout, and the workflow adds a 15-minute job timeout at .github/workflows/review-gate-sweep.yml:85.
  • prior:42dd9b2 important 2 — fixed — .github/scripts/sweep-stalled-ally-reviews.py:274 — exhausted primary or secondary rate limits raise RateLimitExhausted, abort the remaining scan, and produce a degraded non-zero result; young PRs are also skipped before per-PR fetches at .github/scripts/sweep-stalled-ally-reviews.py:746.
  • prior:42dd9b2 important 3 — no-longer-applicable — .gitignore:82 — the previously committed bytecode artifacts are absent from the current tree and Python cache/bytecode patterns are now ignored.

Critical Issues (0)

Important Issues (0)

Suggestions (0)

Strengths

  • The reconciler now distinguishes stranded PR alarms from an unevaluable or rate-limited sweep.
  • The two-surface, exact-body head-attestation predicate is covered by focused tests, and the suite passes with 84 tests.
  • The status-free mode avoids relying on a producerless check and reduces unnecessary API calls for young PRs.

Recommended Action

  1. No Critical or Important issues remain from the reviewed head; this PR is suitable for merge.

@allyblockcast
allyblockcast Bot added this pull request to the merge queue Aug 23, 2026
Merged via the queue into master with commit eaf1502 Aug 23, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants