The largest silent stuck class
prReadyForReview (src/queue/processors.ts ~3944-3992) can defer a PR forever, with no output of any kind. The path:
const isLiveBaseConflict = (liveMergeState ?? pr.mergeableState) === "dirty";
if (ci.hasMissingRequiredContext && !isLiveBaseConflict) { …audit… return false; } // every pass, forever
hasMissingRequiredContext (src/github/backfill.ts ~3016-3027, ~3066) is set when a branch-protection-required (or expectedCiContexts) context name never appears at all in a fully-read check/status set. It is orthogonal to hasVisiblePending (~3067), which requires a required check to be visibly queued/in_progress. A check that is never created is missing, not visible-pending — so the guard above falls through and returns false on every pass. The defer cap is only MISSING_REQUIRED_CONTEXT_DEFER_MS = 2 minutes (~4090), after which it just… keeps deferring.
Contrast the generic stuck-CI path (~3993-4064), which correctly finalizes once per SHA and pages at error level. The missing-required-context branch has no finalize escape — by deliberate design (#3947), to avoid publishing a premature "passed" gate. But the safety goal (never auto-merge unverified) does not require silence.
Because the caller returns immediately on false (~3686-3698), the PR gets no public surface, no review comment, no disposition, no label, and no alert — the only trace is low-signal github_app.review_deferred_ci_pending audit rows with outcome queued. The PR silently vanishes from the pipeline. In production this event fired 1,800 times across 342 distinct PRs in 48h, which makes genuinely-stuck PRs indistinguishable from PRs healthily waiting on CI.
Real-world triggers: a required workflow with paths: filters that exclude this PR's files; a renamed or deleted required workflow; an uninstalled third-party required App; a required context configured that no workflow actually emits.
Fix
Add a terminal cap to the missing-required-context branch mirroring the generic stuck-CI finalize: once a required context has been missing for STUCK_CI_DEFER_MS, stop deferring and route the PR to a loud held-for-human state — a visible hold naming the exact missing context, plus the once-per-SHA error-level page. This preserves #3947's safety (a hold never merges) while eliminating the silence. Keep the SHA-scoped guard so it doesn't re-spend.
Also: make the defer audit event carry a distinguishable reason (missing_required_context vs ci_running) and the elapsed defer duration, so "waiting normally" and "stuck for hours" are separable in the ledger — today they are not.
Acceptance
- A PR whose required check is never created gets a named, visible hold within the cap, and an operator alert — instead of silence.
- Normal CI waits still produce no user-visible noise.
Refs #9003 (the decision-reason invariant this is the worst instance of).
The largest silent stuck class
prReadyForReview(src/queue/processors.ts~3944-3992) can defer a PR forever, with no output of any kind. The path:hasMissingRequiredContext(src/github/backfill.ts~3016-3027, ~3066) is set when a branch-protection-required (orexpectedCiContexts) context name never appears at all in a fully-read check/status set. It is orthogonal tohasVisiblePending(~3067), which requires a required check to be visibly queued/in_progress. A check that is never created is missing, not visible-pending — so the guard above falls through and returnsfalseon every pass. The defer cap is onlyMISSING_REQUIRED_CONTEXT_DEFER_MS = 2 minutes(~4090), after which it just… keeps deferring.Contrast the generic stuck-CI path (~3993-4064), which correctly finalizes once per SHA and pages at error level. The missing-required-context branch has no finalize escape — by deliberate design (#3947), to avoid publishing a premature "passed" gate. But the safety goal (never auto-merge unverified) does not require silence.
Because the caller returns immediately on
false(~3686-3698), the PR gets no public surface, no review comment, no disposition, no label, and no alert — the only trace is low-signalgithub_app.review_deferred_ci_pendingaudit rows with outcomequeued. The PR silently vanishes from the pipeline. In production this event fired 1,800 times across 342 distinct PRs in 48h, which makes genuinely-stuck PRs indistinguishable from PRs healthily waiting on CI.Real-world triggers: a required workflow with
paths:filters that exclude this PR's files; a renamed or deleted required workflow; an uninstalled third-party required App; a required context configured that no workflow actually emits.Fix
Add a terminal cap to the missing-required-context branch mirroring the generic stuck-CI finalize: once a required context has been missing for
STUCK_CI_DEFER_MS, stop deferring and route the PR to a loud held-for-human state — a visible hold naming the exact missing context, plus the once-per-SHA error-level page. This preserves #3947's safety (a hold never merges) while eliminating the silence. Keep the SHA-scoped guard so it doesn't re-spend.Also: make the defer audit event carry a distinguishable reason (
missing_required_contextvsci_running) and the elapsed defer duration, so "waiting normally" and "stuck for hours" are separable in the ledger — today they are not.Acceptance
Refs #9003 (the decision-reason invariant this is the worst instance of).