Problem
Some checks degrade mergeStateStatus to UNSTABLE without ever completing, blocking a
merge-readiness read even when every REQUIRED check is green. Three observed classes:
- A job stuck
QUEUED forever on an unmatched self-hosted runner label (post-migration sentinel).
- An orphaned
StatusContext: posted pending with null creator / empty target_url / no backing
run — never resolves, no branch-side run to cancel.
- A non-required check that never settles but still holds
UNSTABLE.
Dominant operational wall of the autopilot run — the only thing that blocked reaching literal zero.
Cancelling makes it worse (CANCELLED is a failure state). Evidence: medley#1567 (sentinel),
medley#1597 (orphaned codex-review); retrospective
.work/handoffs/babysit-autopilot-retro.md §1.2 / §1.3 / §1.4 / §2.1.
Recommended shape (re-derived blind to the original "skill" framing)
NOT a skill. Engine signal (detect) + a routing reference doc. Detection is the engine's job;
routing/escalation is a judgment call prose owns.
Detection — in the engine, zero new fetches
mergeStateStatus == UNSTABLE is itself the trigger: it already means "mergeable, required gates
satisfied, a non-required commit status not passing" (per the pull-request skill's
reference/readiness.md). The snapshot engine already captures mergeStateStatus and normalizes
every check with type (StatusContext vs CheckRun), effective_state, target_url/details_url,
and a timestamp — so all three classes are detectable with no new fetch:
- (b) orphaned StatusContext:
type==StatusContext + pending + empty target_url.
- (a) stuck QUEUED:
type==CheckRun + QUEUED + age past a threshold (age = snapshot
generated_at − check createdAt/startedAt). The root cause (unmatched runner label) needs an
Actions-jobs API call — that is remediation diagnosis, fetched on demand by the playbook, not
baked into every cycle.
- (c) non-required never-settler: under UNSTABLE, anything still pending/failing that isn't (a)/(b)
past the age threshold.
Emit as a new snapshot per-PR JSON field (e.g. checks.stuck[] = {name, type, class, target_url, details_url, age_seconds}). Formalize the internal sort timestamp into a public age_seconds;
consumers cite the snapshot CLI + JSON field name, never the internal classifier module.
Correctness guard (critical)
The classified stuck signal must surface as a material_findings entry (report/escalate), never a
blockers string — a sticky blocker would pin classification==active and re-dispatch a worker
every cycle (the fan-out trap the suppressible/unsuppressible delta model exists to prevent).
Branch-config-fixable cases (wrong runs-on: in the branch's own workflow YAML) ride the normal
head_sha_changed delta; org/settings-class (runner pool, orphaned status, branch protection) stay
material + escalate. Fire only under UNSTABLE + age threshold (configurable, mirroring
max_quiet_recheck_seconds) so normal in-flight CI and pending required checks are never flagged.
Routing — in prose
New reference/stuck-checks.md (sibling of freshness.md), wired into SKILL.md References +
operational runbook. Points at — never restates — safety.md's Stop-and-Ask / Never-Do-Automatically
rules (runners/settings/branch-protection), the merge wrapper's requiredChecks JSON (confirm
required-green), and the new checks.stuck field. Route CI-config fixes upstream (branch CI /
ci-workflows select-runner); orphaned-status/settings to github-iac / app config.
Files
scripts/babysit_checks.py — per-check stuck-class from type/state/target_url/timestamp (the
shared classifier, so the merge gate benefits too)
scripts/babysit_delta.py — compute the signal in the per-PR classifier that already derives
blockers/material_findings and holds mergeStateStatus
reference/stuck-checks.md (new) + SKILL.md (References + runbook step)
Rejected shapes
Standalone script (re-implements check normalization = a 2nd source of truth), pure prose skill
(can't detect → reintroduces the survey-without-classifying failure), subagent (no benefit for a pure
function over data in hand).
SSOT note
The fact "UNSTABLE from a stuck non-required check ≠ a required-check failure" currently lives in the
pull-request skill's readiness reference. Optionally lift it to the plugin-scope shared reference/
(where review-discipline.md already serves both skills) and point both at one copy.
Related
Merge-gate stuck-check policy (retro §1.4): the gate correctly refuses UNSTABLE and forbids an
--admin/gh pr merge bypass — this auditor is the clean path, not a gate-bypass mode.
Problem
Some checks degrade
mergeStateStatustoUNSTABLEwithout ever completing, blocking amerge-readiness read even when every REQUIRED check is green. Three observed classes:
QUEUEDforever on an unmatched self-hosted runner label (post-migration sentinel).StatusContext: postedpendingwith null creator / emptytarget_url/ no backingrun — never resolves, no branch-side run to cancel.
UNSTABLE.Dominant operational wall of the autopilot run — the only thing that blocked reaching literal zero.
Cancelling makes it worse (
CANCELLEDis a failure state). Evidence: medley#1567 (sentinel),medley#1597 (orphaned
codex-review); retrospective.work/handoffs/babysit-autopilot-retro.md§1.2 / §1.3 / §1.4 / §2.1.Recommended shape (re-derived blind to the original "skill" framing)
NOT a skill. Engine signal (detect) + a routing reference doc. Detection is the engine's job;
routing/escalation is a judgment call prose owns.
Detection — in the engine, zero new fetches
mergeStateStatus == UNSTABLEis itself the trigger: it already means "mergeable, required gatessatisfied, a non-required commit status not passing" (per the pull-request skill's
reference/readiness.md). The snapshot engine already capturesmergeStateStatusand normalizesevery check with
type(StatusContext vs CheckRun),effective_state,target_url/details_url,and a timestamp — so all three classes are detectable with no new fetch:
type==StatusContext+ pending + emptytarget_url.type==CheckRun+QUEUED+ age past a threshold (age = snapshotgenerated_at− checkcreatedAt/startedAt). The root cause (unmatched runner label) needs anActions-jobs API call — that is remediation diagnosis, fetched on demand by the playbook, not
baked into every cycle.
past the age threshold.
Emit as a new snapshot per-PR JSON field (e.g.
checks.stuck[]={name, type, class, target_url, details_url, age_seconds}). Formalize the internal sort timestamp into a publicage_seconds;consumers cite the snapshot CLI + JSON field name, never the internal classifier module.
Correctness guard (critical)
The classified stuck signal must surface as a
material_findingsentry (report/escalate), never ablockersstring — a sticky blocker would pinclassification==activeand re-dispatch a workerevery cycle (the fan-out trap the suppressible/unsuppressible delta model exists to prevent).
Branch-config-fixable cases (wrong
runs-on:in the branch's own workflow YAML) ride the normalhead_sha_changeddelta; org/settings-class (runner pool, orphaned status, branch protection) staymaterial + escalate. Fire only under
UNSTABLE+ age threshold (configurable, mirroringmax_quiet_recheck_seconds) so normal in-flight CI and pending required checks are never flagged.Routing — in prose
New
reference/stuck-checks.md(sibling offreshness.md), wired intoSKILL.mdReferences +operational runbook. Points at — never restates —
safety.md's Stop-and-Ask / Never-Do-Automaticallyrules (runners/settings/branch-protection), the merge wrapper's
requiredChecksJSON (confirmrequired-green), and the new
checks.stuckfield. Route CI-config fixes upstream (branch CI /ci-workflowsselect-runner); orphaned-status/settings togithub-iac/ app config.Files
scripts/babysit_checks.py— per-check stuck-class fromtype/state/target_url/timestamp (theshared classifier, so the merge gate benefits too)
scripts/babysit_delta.py— compute the signal in the per-PR classifier that already derivesblockers/material_findingsand holdsmergeStateStatusreference/stuck-checks.md(new) +SKILL.md(References + runbook step)Rejected shapes
Standalone script (re-implements check normalization = a 2nd source of truth), pure prose skill
(can't detect → reintroduces the survey-without-classifying failure), subagent (no benefit for a pure
function over data in hand).
SSOT note
The fact "UNSTABLE from a stuck non-required check ≠ a required-check failure" currently lives in the
pull-request skill's readiness reference. Optionally lift it to the plugin-scope shared
reference/(where
review-discipline.mdalready serves both skills) and point both at one copy.Related
Merge-gate stuck-check policy (retro §1.4): the gate correctly refuses
UNSTABLEand forbids an--admin/gh pr mergebypass — this auditor is the clean path, not a gate-bypass mode.