Skip to content

fix(pr-pipeline): let a kill-switch stop skip cleanly and re-fire pr-unblock heads after the switch lifts - #10844

Merged
kyle-sexton merged 4 commits into
mainfrom
fix/kill-switch-strand
Oct 29, 2026
Merged

kyle-sexton merged 4 commits into
mainfrom
fix/kill-switch-strand

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

No related issue: kill-switch stranding seen on #10828 #10829 #10808

Summary

A lanes kill switch turned on while a pr-unblock run is in flight stopped it red, and the head then stayed stranded after the switch lifted. The run's own kill-switch step had already passed, so only the token broker's live read stopped it: pr-run-activity-write.yml requested the lane token without kill-switch-ok, so the refusal failed the job (lane-token-denied ... kill-switch, seen on #10828). A failed publish-feedback then mapped to feedback-publish-failed, which routes fail. The dispatchers treat a head that already has an activity check run as handled, and maintenance-reconcile-lost-runs re-fires only ci-status after a lift, so nothing re-fired those heads until a new push.

This PR edits .github/workflows/, so the owner arms it.

Fix

  • pr-run-activity-write.yml passes kill-switch-ok: ${{ inputs.issue-number == '' }} on the token request. On a pull request run the refusal now ends green with no token, every later step skips, and the check is neutral kill-switch. An issue run (work-resolve-issue) stays red, since work-unblock re-fires its issue from the failed mint step. Three later steps that read the token now also gate on it explicitly (token != ''); each already ran only after a step that required the token, so nothing changes at run time.
  • New skip-reason kill-switch in pr-pipeline.schema.json. report-check-run refused an enum miss as failure, so the token-path skip needed a member. The gate's own kill-switch stop now maps to kill-switch instead of prerequisite-missing too, so both paths post one title. This applies to every lane check report-check-run posts, so the fragment bumps minor, as lane-stopped did.
  • Recovery, dispatcher side, in one shared helper in limit-stop.mjs (KILL_SWITCH_STOP, killSwitchStop, releasesHead):
    • anyHandles never counts a kill-switch skip on the activity's own check as handling the head. It counts toward limits.limit-stops-per-head, like a held skip, so a switch that keeps flipping mid-run still ends the head.
    • stopRelease (resolve-conflicts, and review-dependency's verdict, which main now routes through it) releases a head on a kill-switch emitter check as it does on a lane stop.
    • feedback-dispatch counts a reply check's Skipped: kill-switch among its releasing skips. That also feeds the half-hourly sweep, address-feedback's only trigger after a lift, under SWEEP_HEAD_CAP.
    • fix-ci-dispatch reads an attempt its push job's kill-switch skip followed, or a kill-switch skip of fix-ci itself, as paused. It is retried like a held skip and spends no fix-ci restart.
  • Why the dispatcher option and not extending reconcile.mjs's Switch recovery: with (a) the runs no longer fail, so there is no failed run to re-run. A bot re-run past attempt 1 is also refused by check-trusted-trigger (.github/actions/check-trusted-trigger/README.md:63). The dispatchers already run only with the switch off (pr-unblock-dispatch.yml:98-119, pr-feedback-dispatch.yml:137-146), so not counting a kill-switch check re-fires the head on their next run after the lift.
  • Docs: README.md lists the new reason and the lift recovery. pr-run-activity.md replaces "a kill-switch denial is red" with the PR-run skip and the issue-run exception.

Verification

  • node --test under .github/scripts/pr-unblock (943 pass), .github/scripts/pr-merge (627), .github/actions/resolve-config after npm ci (744), .github/actions/report-check-run (109), .github/actions/request-lane-token (76) and .github/scripts/maintenance-reconcile-lost-runs (117): all pass, 0 fail, on the head merged with main.
  • actionlint .github/workflows/pr-run-activity-write.yml: clean.
  • New tests:
    • callers.test.mjs: the kill-switch-ok contract now also covers expression-valued callers and the dependents of every job in another workflow that calls the write file. Today every such dependent also reads another output of the job, so that branch passes vacuously; it guards future dependents.
    • callers.test.mjs: kill-switch-ok renders true with an empty issue number and false with one set.
    • decide.test.mjs: a broker kill-switch refusal after the gates proceeded posts neutral Skipped: kill-switch.
    • limit-stop.test.mjs: anyHandles, permanentlyHandles and stopRelease on kill-switch stops, including the cap.
    • feedback-dispatch, dependency-dispatch and fix-ci-dispatch: a head stopped by the kill switch at the emitter or at its own check dispatches again, and the feedback sweep stops at SWEEP_HEAD_CAP.
  • Not covered:
    • The read file's broker-check kill-switch denial stays red, as before: a red address-feedback or review-dependency check with no limit record still holds its head.
    • fix-docs-dispatch counts check runs by totalCount, so a pr-refine fix-docs head the switch stopped still strands, as before this change.
    • A kill-switch skip of apply-patch-rung-up reads as a clean apply to report-escalation, so the rung-up stop posts no status. It is silent, not red.
    • dispatch.mjs (resolve-conflicts) is covered through the stopRelease unit test only.

Related

🤖 Generated with Claude Code

kyle-sexton and others added 3 commits October 28, 2026 22:05
…unblock heads after the switch lifts

The write lane's token request now passes kill-switch-ok on a pull request
run, so a broker kill-switch refusal ends neutral kill-switch (a new
skip-reason) instead of red, and the gate's own kill-switch stop posts the
same title. The pr-unblock dispatchers read a kill-switch check as a stop
that did no work (limit-stop.mjs killSwitchStop), so a head stopped this
way is dispatched again once the switch lifts, under the per-head caps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mp the new skip reason minor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	.github/scripts/pr-unblock/dependency-dispatch.mjs
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 29, 2026 02:29
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-29T02:31:59.660718Z 9c1573f Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed 71d2b41835d1ca7f3ba4a194df89050d6cee5836: 0 finding(s), 0 as inline comments.

Incremental review of the four files changed since 9c1573f. I found no security issues.

critical-findings: 0

The change makes the kill-switch skip narrower. kill-switch-ok in .github/workflows/pr-run-activity-write.yml:1060 is now inputs.issue-number == '' && inputs.activity != 'report-escalation'. A report-escalation run that the token broker's live kill-switch read refuses now ends red, as it did before the PR. It no longer ends green with no token. This fails closed, so it does not widen the kill switch's reach.

  • Input: inputs.activity is a declared input of the reusable workflow (line 27). It is already passed to the token request on line 1068, so the new condition reads a value the workflow already trusts.
  • Skip path: a non-escalation pull request run still ends green with no token. Every later step skips, because each is gated on proceed or on a non-empty token. No token is minted in any case.
  • Tests and docs: the callers.test.mjs change covers the new false case for report-escalation. It also checks that the pr-unblock.yml caller passes that activity. The change note and pr-run-activity.md match the code.

No manifest fields are touched. manifest lines: none apply.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

claude-review: 1 inline finding(s) on 9c1573f8d4d066024b8fd43fad68bd4a7704a14e; the summary is in this lane's pull request comment.

claude-lane: claude-review

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed 71d2b41835d1ca7f3ba4a194df89050d6cee5836: 0 finding(s), 0 as inline comments.

Incremental review of the four files changed since 9c1573f. I found nothing to flag.

  • Workflow change. kill-switch-ok in .github/workflows/pr-run-activity-write.yml is now inputs.issue-number == '' && inputs.activity != 'report-escalation'. A kill-switch refusal on a report-escalation run therefore stays red, as an issue run does. The comment explains why: no dispatcher re-fires it, so a green skip would drop its comment and label.
  • Caller match. I confirmed pr-unblock.yml job report-escalation (line 2678) passes activity: report-escalation (line 2732). The new test also asserts this, so the guard cannot silently stop matching if the activity is renamed.
  • Test evaluator. The test's evaluate helper handles && and !=. The inputs.activity and inputs.issue-number substitutions do not collide. The unresolved-reference assertion still holds.
  • Existing caller-list test. The test that lists every kill-switch-ok caller keys on the expression containing ${{, so it still matches. The caller list is unchanged because report-escalation is still a caller.
  • Docs and changelog. The pr-run-activity.md and .changes text agree with the code. One changelog line is now long, which is a style point and not a finding.

I only read files and did not run the test suite, so test results are unverified. This change spends no money.

No dispatcher re-fires report-escalation, so a neutral skip would drop a
real escalation's comment and label. The write lane passes kill-switch-ok
only for a pull request run of any other activity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@melodic-automation-lanes

Copy link
Copy Markdown
Contributor

Unattended merge: risk HIGH at head 71d2b41835d1ca7f3ba4a194df89050d6cee5836. Only LOW, or MED on class, size and reported review-finding reasons alone, merges without a human. A floor path holds it: a human merges it in person, never an agent or a lane.

HIGH high-path:.github/actions/resolve-config/**: .github/actions/resolve-config/callers.test.mjs
HIGH review-path-unbindable:.github/scripts/pr-unblock/feedback-dispatch.mjs: .github/scripts/pr-unblock/feedback-dispatch.mjs; it also changes a lane source, so no security review binds to this head
HIGH review-path-unbindable:.github/scripts/pr-unblock/limit-stop.mjs: .github/scripts/pr-unblock/limit-stop.mjs; it also changes a lane source, so no security review binds to this head
MED evidence-unbound:claude-review: not bound: lane-source-changed
MED evidence-unbound:claude-security-review: not bound: lane-source-changed

@melodic-automation-lanes

Copy link
Copy Markdown
Contributor

Unattended merge: risk HIGH at head 71d2b41835d1ca7f3ba4a194df89050d6cee5836. Only LOW, or MED on class, size and reported review-finding reasons alone, merges without a human. A floor path holds it: a human merges it in person, never an agent or a lane.

HIGH high-path:.github/actions/resolve-config/**: .github/actions/resolve-config/callers.test.mjs
HIGH review-path-unbindable:.github/scripts/pr-unblock/feedback-dispatch.mjs: .github/scripts/pr-unblock/feedback-dispatch.mjs; it also changes a lane source, so no security review binds to this head
HIGH review-path-unbindable:.github/scripts/pr-unblock/limit-stop.mjs: .github/scripts/pr-unblock/limit-stop.mjs; it also changes a lane source, so no security review binds to this head
MED evidence-unbound:claude-review: not bound: lane-source-changed
MED evidence-unbound:claude-security-review: not bound: lane-source-changed

This refusal has stood unchanged at this head, so a person is needed: the pr-merge lane added the human-gated label.

@melodic-automation-lanes melodic-automation-lanes Bot added the needs-human Human-in-the-loop required; autonomous sessions must not resolve items carrying this. label Oct 29, 2026
@kyle-sexton kyle-sexton removed the needs-human Human-in-the-loop required; autonomous sessions must not resolve items carrying this. label Oct 29, 2026
@kyle-sexton
kyle-sexton enabled auto-merge October 29, 2026 12:33
@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit bd6c37d Oct 29, 2026
72 of 73 checks passed
@kyle-sexton
kyle-sexton deleted the fix/kill-switch-strand branch October 29, 2026 12:43
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.

1 participant