Skip to content

feat(pr-pipeline): stop lanes at their start gate on a low GITHUB_TOKEN budget - #15385

Merged
kyle-sexton merged 12 commits into
mainfrom
feat/kill-switch-start-gate-budget
Nov 11, 2026
Merged

kyle-sexton merged 12 commits into
mainfrom
feat/kill-switch-start-gate-budget

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

No related issue: owner decision Q1-Q5 (2026-11-11), hub research .work/research/start-gate-breaker

Summary

Lanes that start new work now check the live GITHUB_TOKEN budget at their own start gate, so no desktop script has to flip a kill switch. CLAUDE_LANES_DISABLED and CLAUDE_LANES_REPO_DISABLED become manual brakes: nothing in this repository writes them, and they still proceed only on exactly false. Gates that finish or judge existing work keep checking the switches only, so a low budget never throws that work away or stalls a head.

.github/actions/log-api-budget/rate-limits.json is on every merge hold list's floor, so this PR waits for a person.

Fix

  • .github/actions/check-kill-switch:
    • New budget input, default "false" (switches only).
    • With budget: "true" and both switches off, the gate makes one GET /rate_limit with the token input (default ${{ github.token }}, the same default check-trusted-trigger/action.yml uses). It reads the core pool from x-ratelimit-limit, -remaining and -reset, else from the body's resources.core, and stops while remaining < start-gate x limit and the reset is still ahead.
    • The share is github-token's start-gate in rate-limits.json (0.1), read from that file alone. If it cannot be read, the gate warns and proceeds without a budget read, so there is no copy of it in the gate. A budget read that fails (network error, timeout, 401, 500, an unreadable body) proceeds with a warning; the rate guard's stop-floor (fix(pr-pipeline): stop a GitHub API pool at its stop floor, not only at zero #15305) is the backstop. A read GitHub refuses as limited (403 or 429, any retry-after, or x-ratelimit-remaining: 0, a bare 403 included) stops the lane with cause budget until the header's reset, else now + retry-after, else now + 60 s (refusal() in check-kill-switch.mjs). That keeps a limited token from making more calls (owner Q3: no ban risk). It is fail-closed on purpose: a persistent bare 403 on GET /rate_limit stops every start gate, one window at a time, and switch-only gates keep finishing existing work. The hub kept this over a reviewer's suggestion to proceed on a bare 403 (hub decision D129).
    • Each read logs one start-gate notice with the body's core block and the response headers, the production probe for research gap B1.
    • Outputs: proceed and reason=kill-switch are unchanged. New cause (switch, budget or ok) and reset (the window's reset in epoch seconds, on a budget stop).
  • Start gates, 15 in all (14 jobs with budget: "true", plus broker-check's first call):
    • the read runner's broker-check (pr-run-activity-read.yml) reads the budget only on a starting caller's first call: budget: ${{ inputs.start-gate && (inputs.fallback == '' || inputs.fallback == 'credit-stop') && 'true' || 'false' }}. A new start-gate input (default false) is set by the issue lane's two first-call legs in work-resolve-issue.yml: resolve-issue, and resolve-issue-subscription, which is the slot's first call when the key leg was skipped. The fallback leg and handed-off legs check the switches only. pr-refine and pr-unblock fix existing PRs and pass none;
    • intake-triage.yml kill-switch and refine-select, and intake-reenter.yml reenter;
    • work-unblock.yml label-unblocked (the sweep);
    • schedule ticks that start work: maintenance-reconcile-lost-runs reconcile, maintenance-audit-lane-runs collect and triage-sweep, maintenance-delete-stale-branches plan, maintenance-file-recurring-issues, maintenance-monitor-signals monitor, the gate job of maintenance-measure-skill-edit, maintenance-tidy-code-upkeep and upstream-apply-changelog, and post-merge-test-mutation prep.
  • Every other gate (69 of 84) checks the switches only:
    • reviews (pr-review-hosted, pr-review-security-hosted, pr-review-main), pr-rate-merge-risk, pr-verify and pr-verify-capture;
    • pr-merge (merge, enable-auto-merge, post-merge-dispatch) and pr-feedback-dispatch (post-verify-notes, dispatch, rerun-ci-status);
    • the write runner and other writes after a model turn, and the read runner's model job (broker-check already read the budget seconds earlier);
    • pr-unblock-dispatch (dispatch, refine), pr-unblock's select jobs and pr-refine, which fix existing PRs;
    • work-resolve-issue admit and pick-slot (added on main): the issue lane reads its budget at broker-check's first call, the one place label-unblocked reads a budget stop from;
    • model legs that take over a handed-off turn, and evals, which a person or a pull request starts.
      check-kill-switch.test.mjs enumerates every uses: of a path ending in actions/check-kill-switch in .github/workflows, and checks which read-runner calls pass start-gate. It fails on any gate it does not classify, and on a start gate without budget: "true" or a switch-only gate with any budget line.
  • Every step that reports a gate stop now prints cause beside reason. That is 33 report steps in 21 workflows, plus measure-skill-edit's gate check. In the 28 of them that say "the lanes kill switch stops", plus measure-skill-edit's check, a budget stop reads "the GITHUB_TOKEN budget is low until (epoch seconds), which stops". The review steps (pr-review-hosted, pr-review-security-hosted, pr-review-main) print cause too, for the same log shape, but keep their switch wording: their gates are switch-only, so cause there is always switch or ok.
  • On a budget stop, broker-check sets skip-reason: budget. The run job skips on any non-empty skip-reason, so it never reaches "Assert the broker allowed this attempt". The report job treats the stop like the broker's kill-switch refusal: on a PR it posts a neutral check under the skip-reason enum's kill-switch, which the dispatchers and limit-stop already handle, and no lane-run comment. In issue mode no lease was taken, so it skips. In work-resolve-issue, apply-issue-patch and report-outcome skip (there is no scan output) and the run ends green. The sweep then takes the budget branch in label-unblocked.mjs didNoWork(): budget from the budget step, killed, a check-stage breaker trip carrying budget, and in refireRefusal() due = the step's reset. Main's pre-model rate-limit stop (limited, reset) is kept beside it: a budget stop never reaches that branch, and across runs a mint-failed stall outranks a breaker trip. REVIEW_STOP_NOTE in reconcile.mjs stays as it is: review gates are switch-only, so that note can only mean a switch.
  • pr-run-activity-read.yml: broker-check gets a top-level Stop on a low GITHUB_TOKEN budget until ${{ steps.kill.outputs.reset }} step, which runs only on cause == 'budget'. The model job no longer reads the budget and carries no such step.
  • .github/scripts/work-unblock/label-unblocked.mjs: the sweep reads that step from the job listing it already fetches, with no new API call. A budget stop is a check-stage breaker trip whose record carries <!-- work-unblock cause=budget --> on its second line. It never counts toward limits.limit-stops-per-issue or the flat kill-switch wait. It re-fires only once the reset named in the step has passed (an hour after the run ended when the name carries none), so each stop gets one re-fire, not one per sweep. An issue keeps one budget record: a later budget stop edits it in place (PATCH) rather than posting a new comment, and its marker time is the last edit.
  • true@<time> is not in this PR. It returns later as one shared parser that every switch reader uses.
  • start-gate is valid in rate-limits.json (rate-guard.mjs FIELDS, BUILT_IN_LIMITS and a new FILE_ONLY_FIELDS). The pr-pipeline.yaml limits.api schema rejects it. rate-guard.test checks both. No other share changed. intake-triage's one sparse checkout that lacked log-api-budget now has it.
  • Changelog fragment .changes/pr-pipeline/feat-kill-switch-start-gate-budget-16d00f6e.md (minor).

Call cost: one GET /rate_limit per start gate a job reaches (15 of 84 gates), none on the other 69, and none when a switch already stops. label-unblocked adds no call; the tests assert no log read. GitHub notes this call can count against the secondary limit (not the primary one), so it is made once, with no retry and a 10 s timeout.

Verification

  • check-kill-switch: 53 tests pass at the merged head. They cover:
    • the default making no call, start-gate stops and proceeds at 499, 500 and 501 of 5000, the reset now or past, and share 0;
    • headers winning over the body, a missing header falling back to the body, and failed reads (network, 401, 500, unreadable) proceeding;
    • refused reads stopping with a reset ahead: a 403 with the window's reset, a 429 with retry-after, a bare 403, a bare 429, and a 200 with retry-after;
    • the share read from rate-limits.json, an unreadable share (no call, warning), the reset output, the sparse-checkout case, and the classification of every gate.
  • label-unblocked's budget fixture is the run shape a real stop now leaves: broker-check green with its budget step, every later job of the call and of work-resolve-issue skipped. A second budget stop PATCHes the one record and posts none. A budget stop waits for its reset, so there are no writes at 12:00 with a reset at 12:30, and one record and re-fire at 12:30 and at 12:45. With no reset in the name it waits until ended + 1 h. It never counts toward the per-issue bound, and a skipped budget step is a switch trip.
  • After merging main (conflict in label-unblocked.mjs resolved to keep both stops; independent review of the merge commit: no bug, MERGE): label-unblocked 395 of 395, resolve-config callers 284 of 284, check-kill-switch 53 of 53.
  • Before that merge, node --test --test-concurrency=4 .github/actions/*/*.test.mjs .github/scripts/*/*.test.mjs: 9190 of 9191 pass. The one failure was credential-legs.test.mjs's pin of broker-check's outputs. That pin is updated for the new skip-reason source, and its file then passes 3 of 3.
  • Shell suites, 11 of 11 pass: read-lane-broker-check, check-read-caller-keys, act-turns-denied, pr-unblock-trusted-base, read-runner-dispatcher-gate, write-runner-never-rules, ci-lanes-pr-hold, check-loop-lane-floor-drift, plan-test-lanes, ci-review-lanes and collect-patch-lane.
  • scripts/check-changelog-fragments.sh --check and --check-required origin/main: exit 0.
  • zizmor --persona=regular --no-online-audits .github/workflows .github/actions/check-kill-switch/action.yml: no findings.

Related

session: GitHub Support

🤖 Generated with Claude Code

https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7

kyle-sexton and others added 4 commits November 11, 2026 09:44
…EN budget

check-kill-switch now gates on the GITHUB_TOKEN budget as well as the two
manual switches. With both switches off it makes one GET /rate_limit with
the job's token, reads the core pool from the x-ratelimit-* headers (else
the body's resources.core), and stops the lane while remaining is under
limits.api github-token start-gate (default 0.1) of the limit and the reset
is ahead. A failed read proceeds with a warning. Each switch also accepts
true@<UTC time>, which stops until that time. Every stop keeps
reason=kill-switch; a new cause output names switch, expiry, budget or ok.

The read runner marks a budget stop with a top-level step, so work-unblock's
sweep tells it apart from the job listing alone: it re-fires such a run on
the next sweep, and its record never counts toward
limits.limit-stops-per-issue or the flat kill-switch wait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7
The gate reads its start-gate share through rate-guard.mjs; the one sparse
checkout that carried check-kill-switch without log-api-budget fell back to
the built-in share with a warning on every run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7
The gate reads limits.api github-token start-gate from a RESOLVED_CONFIG its
job's env sets, and no workflow sets one for the gate yet, so every gate
uses the rate-limits.json value today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7
@kyle-sexton
kyle-sexton marked this pull request as ready for review November 11, 2026 14:59
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-11T17:29:11.636444Z d7fc23a 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 07d327a262

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

kyle-sexton and others added 4 commits November 11, 2026 10:09
check-kill-switch takes a budget input (default true). budget: "false"
skips the GET /rate_limit read, so the gate checks the switches only. It is
set on the 13 gates in jobs that run after a model turn and consume its
output (the write runner, intake-triage apply jobs, the audit, tidy, skill
edit and changelog push and publish jobs, and pr-unblock's
adjudicate-feedback), so a low budget never throws finished model work
away; the rate guard's stop-floor stays their backstop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7
check-kill-switch's budget input now defaults to "false" (switches only).
budget: "true" is set on the 17 gates that start new work: the read
runner's broker-check, intake triage and refine select, intake re-entry,
the work-unblock sweep, the pr-unblock dispatcher and the schedule ticks
that start work. Reviews, merges, verdicts, verify notes, writes after a
model turn, handed-off model legs and evals check the switches only.
check-kill-switch.test.mjs classifies every gate in .github/workflows and
fails on one it does not know.

A budget stop writes the window's reset as an output. broker-check's
budget step carries it in its name, and work-unblock's sweep re-fires a
budget-stopped issue run only once that reset has passed (an hour after the
run ended when the name carries none), so each stop gets one record.

The true@<time> switch form is dropped: the other switch readers do not
honor it. Each kill-switch report step prints the cause beside the reason.
The start-gate share is read from rate-limits.json alone, with no copy in
the gate; the docs say a pr-pipeline.yaml value has no effect on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7
On a start-gate budget stop, broker-check sets skip-reason budget, so the
read runner's run job skips instead of failing at "Assert the broker
allowed this attempt". The report job posts the same neutral check and
skips the lane-run comment, as it does for the broker's kill-switch refusal.

broker-check reads the budget only for a caller that passes the new
start-gate input (the issue lane and pr-refine), and only on the first call
in the slot. Handed-off and fallback legs, and pr-unblock, which fixes
existing pull requests, check the switches only.

work-unblock's sweep keeps one budget record per issue and edits it in
place for each window. Report steps name a budget stop as the low
GITHUB_TOKEN budget and its reset. The pr-pipeline.yaml schema rejects
start-gate, which stays valid in rate-limits.json. The gate test matches
any uses: path to the action and checks which read-runner calls pass
start-gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7
…ff it

A refused GET /rate_limit (403, 429, retry-after, or
x-ratelimit-remaining 0) now stops a start gate with cause budget and a
future reset (the header's, else now + retry-after, else now + 60), so a
spent token never leads to more calls. Transport errors, timeouts, 5xx,
401 and unreadable bodies still proceed. A share of 0 makes no read.

The skill-edit gate ends a budget stop neutral with a notice, and its
later steps wait on the gate. The pr-unblock dispatcher, pr-refine and
the issue lane's fallback leg check the switches only. Gates that never
read the budget keep main's stop wording.

The gate test now checks that no step a budget stop runs fails the job,
and that every budget job checks out log-api-budget beside the gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as draft November 11, 2026 17:26
@kyle-sexton
kyle-sexton marked this pull request as ready for review November 11, 2026 17:26

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d7fc23abf9

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed 8ba6f5f7a602a45a6fe95907eaaff2016864e082: read 30 of 30 changed file(s); 0 finding(s), 0 as inline comments.

Security review: PR #15385 at 8ba6f5f7

No security issues found. I read every hunk of all 30 changed files in .claude-lane/pr.diff. This lane already reviewed an earlier head (blocking only: yes), so this review reports only CRITICAL and IMPORTANT findings. The two settled threads (both lines changed since resolution: no) were about the budget check's reach and backoff counting. Neither raised a security point, and I re-raise neither.

critical-findings: 0

Kill switch (.github/actions/check-kill-switch/, trusted path)

The kill switch can still stop every lane.

  • Switch logic is unchanged. checkKillSwitch still proceeds only when both switches are exactly false (check-kill-switch.mjs#L192-L198).
  • The budget check can only add stops. In main, it runs only after result.proceed === "true" and BUDGET_CHECK === "true". It can turn proceed from true to false, never the reverse (L308-L322).
  • Errors cannot unset the outputs. checkBudget and startGateShare both catch their own errors. A writeOutputs failure leaves the outputs missing, which still stops the lane.
  • Failed or unreadable budget reads fail open, by design. That affects only the new budget stop; the switches still fail closed.
  • Missing outputs are still safe in maintenance-measure-skill-edit.yml. If the outputs are missing, cause is empty, so Check the run may spend still runs and stops on KILL != true. On a real budget stop, the new proceed == 'true' conditions keep Check the inputs and Check the suite from running.

Token handling

  • No new token scope. token defaults to ${{ github.token }} (action.yml#L134-L139), and no caller overrides it. The token is sent only to ${GITHUB_API_URL}/rate_limit.
  • Unchanged: no permissions: block, and neither request-lane-token nor the broker.
  • Logs carry no credentials. The start-gate notice and warnings log only x-ratelimit-* headers, the body's core block and error messages, never the token. clean() strips CR, LF, % and :, so a response value cannot start a workflow command.

Read runner (pr-run-activity-read.yml)

  • A budget stop runs no lane. It skips the broker ask, because proceed is false, so allowed stays false. The new skip-reason: budget skips the run job.
  • start-gate is a caller-set boolean. It can only add the budget check.
  • Report-path changes are neutral. BROKER_STOP and the skipped lane-run comment treat budget exactly like the existing kill-switch refusal.
  • The new step name's expression is never executed. It is only a display name, and RESET reaches the shell only through env:.

Workflow report steps

  • Values reach the shell only through env:. The new CAUSE, WHY and RESET values are passed that way and never interpolated into a run: script. reset is always numeric: a header that matched ^\d+$, a finite body number, or a computed epoch second.

work-unblock sweep (label-unblocked.mjs)

  • Budget-stop detection reads only trusted data: this repository's own run job listings (step name and conclusion).
  • The budget record is lanes-bot-only. It is read and PATCHed only from comments whose user.login is the lanes bot and whose id passes RUN_ID. budgetEarlier parses dates only from that bot-authored body. An outside commenter cannot forge a budget marker to dodge limit-stops-per-issue.
  • A budget stop skips the per-issue bound, which is not a security control. It re-fires an issue only after the window's reset.

Other checks

  • No hold removals: none of the owner's friction holds is weakened, and no untrusted-provenance hold is touched.
  • No standing instructions: none are deleted or narrowed.
  • No plugin manifests: none changed.
  • rate-limits.json: the only change adds start-gate: 0.1, and the limits.api schema rejects a config override of it.

Spend: one GET /rate_limit per start gate reached. That draws only on the GITHUB_TOKEN rate pool; nothing can be billed.

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed 8ba6f5f7a602a45a6fe95907eaaff2016864e082: read 30 of 30 changed file(s); 1 finding(s), 0 as inline comments.

Code review (full diff at 8ba6f5f, blocking findings only)

This lane already reviewed an earlier head of this PR, so this pass reports blocking findings only.

Scope. I read every hunk of all 30 changed files in .claude-lane/pr.diff. I also read the unchanged code those hunks rely on: recorded in label-unblocked.mjs and the assertKillSwitchJob callers in callers.test.mjs. I ran no tests. The test counts in the PR body are the author's claims, and I did not check them.

What I checked and found sound:

  • The gate. check-kill-switch.mjs reads the budget only when both switches proceed and BUDGET_CHECK=true. A share of 0, or a share that cannot be read, makes no call. A refused read (403, 429, retry-after, remaining 0) stops with a reset that is always ahead of now. Every other failed read proceeds. Every value from a response passes through clean before it is logged.
  • The read runner. On a budget stop, skip-reason: budget skips the run job. The report job maps it to the neutral kill-switch check and posts no lane-run comment. Only work-resolve-issue.yml's key leg and subscription leg pass start-gate.
  • The sweep. A PATCH of the one budget record keeps recorded right. recorded reads the record's latest updated_at against the label's last add. So when a label move fails after a successful PATCH, the next sweep retries the label and does not edit the record again. The settled threads (check-kill-switch.mjs original L143; label-unblocked.mjs L2685-L2687) have no lines changed since they were resolved, and I did not re-raise them.
  • No manifest changes. No .claude/rules/ file's paths: matches a changed file.

Findings

  1. The PR description still contradicts the code at this head. This restates the earlier summary finding (feat(pr-pipeline): stop lanes at their start gate on a low GITHUB_TOKEN budget #15385 (comment)); a summary finding is restated each round. rate-limits.json is on the hold floor, so a person approves this PR, and they approve it against the description. The body still says:

    • pr-refine. pr-refine's "three" calls set start-gate. pr-refine.yml is not in this PR, and check-kill-switch.test.mjs L372 asserts that only work-resolve-issue.yml passes it.
    • The issue lane. It has "three calls" with start-gate. Only two set it (work-resolve-issue.yml L437 and L635). The fallback leg does not.
    • pr-unblock-dispatch. Its dispatch and refine are listed as start gates. They are classified switch at check-kill-switch.test.mjs L335, and pr-unblock-dispatch.yml is not in this PR.
    • The counts. "17 in all" start gates (and "17 of 82 gates" under Call cost) is wrong. The GATES table has 14 start gates and one first-call gate, 15 in all.
    • Failed reads. The Fix section says "A failed, timed-out or unreadable budget read also proceeds with a warning". The code stops on a refused read: a 403, a 429, a retry-after header, or remaining 0 (check-kill-switch.mjs L70-L76). The README and the changelog say so too, but the PR body never does.

    Fix: update the PR body to match the code. No code change is needed.

Spend (no hold). One GET /rate_limit per start gate a job reaches. GitHub counts it against the secondary limit, so it spends GITHUB_TOKEN quota only and creates no billed resource.

…cord

Editing the single budget record moved its date to the latest edit, so the
backoff charged earlier budget re-adds as infrastructure re-fires. Each
edit now carries the dates of the edits before it, and the record reads as
one re-fire marker per date.

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

melodic-automation-lanes Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

The pr-unblock lane stopped resolve-conflicts on this pull request: the lane could not resolve every conflict. The lane sets no label: the run ends in a failing pr-unblock/conflict status on the head the lane gated.

When a retry on this head can change the outcome, the lane runs one on its own, up to its per-head cap, and edits this comment if it stops again; a run that does not stop clears the status. When no retry can (a review thread the lane does not take or settle, or a failing check after the retries are spent), a person resolves the threads or gets the checks green, then dispatches the workflow below: that run finds the cause gone and clears the status. On a pull request the lanes App opened, the dispatchers start that run once per stop; on any other, a person does. A new commit to the branch starts clean.

To run it again on this head now, dispatch the pr-unblock workflow:

gh workflow run pr-unblock.yml -R melodic-software/claude-code-plugins -f pr-number=15385 -f activity=resolve-conflicts

kyle-sexton and others added 3 commits November 11, 2026 14:20
…t-gate-budget

# Conflicts:
#	.github/scripts/work-unblock/label-unblocked.mjs
…start-gate calls once

Main added check-kill-switch gates to work-resolve-issue.yml's admit and
pick-slot jobs. They pass no budget input, so they are switch gates; the
issue lane's budget is read at broker-check's first call, the one place
label-unblocked reads a budget stop from.

The read-runner start-gate test asserted only inside an if, which the
cant-fail scan rejects; it now collects each workflow's start-gate calls
and asserts the whole map once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q1HHi6JACp77x2ZbE7
@kyle-sexton
kyle-sexton added this pull request to the merge queue Nov 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

The pr-verify verifier failed head 8ba6f5f7a602a45a6fe95907eaaff2016864e082. What it wrote, as data:

What it wrote (33 lines)
Summary: No issue is linked, so the spec is PR #15385's own trusted body. The core mechanism is
  sound and CI is green, including test-node
  (https://github.com/melodic-software/claude-code-plugins/actions/runs/43091806430/job/130770882006)
  and every test-bash shard. What it delivers: an opt-in `budget` input on check-kill-switch that
  does one GET /rate_limit, the share read only from rate-limits.json, `cause` and `reset` outputs,
  the read runner's first-call `start-gate`, and work-unblock's budget re-fire that is never counted
  and edits one record in place. Changing the takeover-set action check-kill-switch weakens nothing:
  the switches still proceed only on exactly `false`, and the budget can only add stops. No
  credentials or settings are touched, and no text tries to steer the reviewer. The verdict is fail
  because the diff does not match the body that serves as its spec. The body lists 17 start gates,
  including pr-unblock-dispatch `dispatch` and `refine` and pr-refine's three read-runner calls. The
  diff makes 15 start gates, and its test pins those gates as switch-only. The body says every
  failed read proceeds, but the code stops on 403, 429, `retry-after` or a remaining of 0. A few
  smaller statements in the body also no longer match the code.
- .github/actions/check-kill-switch/check-kill-switch.test.mjs: GATES classifies
  pr-unblock-dispatch.yml `dispatch` and `refine` as switch-only, and START_CALLS lists only
  work-resolve-issue. The spec (PR body, Fix > Start gates) says these are start gates and that
  pr-refine.yml's three calls pass `start-gate`, and pr-refine.yml is not in the diff. Either add
  `budget: "true"` / `start-gate: true` there, or edit the PR body to match the code (15 start
  gates, not 17; drop pr-unblock-dispatch and pr-refine from the start-gate list; fix the '17 of 82'
  call cost).
- .github/actions/check-kill-switch/check-kill-switch.mjs:70: refusal() stops the lane with cause
  `budget` on a 403, 429, `retry-after` or `x-ratelimit-remaining: 0`, and a bare 403 counts too.
  The spec says 'A failed, timed-out or unreadable budget read also proceeds with a warning', and
  its Verification lists only 500, 401, network and unreadable reads. Either make a refused read
  proceed as the spec says, or state the fail-closed rule for refused reads in the PR body. As
  written, a persistent bare 403 would stop every start gate.
- PR body (Fix > Start gates): it says the issue lane's three calls set `start-gate`, but
  .github/workflows/work-resolve-issue.yml sets it on two (resolve-issue and
  resolve-issue-subscription; the fallback leg has none). Correct the body.
- .github/workflows/pr-review-hosted.yml, pr-review-security-hosted.yml, pr-review-main.yml: the
  diff adds `cause` to the review stop notices, but the PR body says 'The review steps ... are
  unchanged'. Correct the body, or drop those edits.

This pull request leaves the merge queue, and ci-status holds it out while it carries pr-verify-failed. A pr-verify pass removes the label, at a new commit or at this same head: once a trusted person edits the body, and only trusted people have edited it since the fail, this head is judged again, so a fail on the body or the issues it links needs only the body fixed, no new commit.

@github-actions github-actions Bot added the pr-verify-failed pr-verify failed this head; a pass at a new commit, or after a trusted body edit, removes it. label Nov 11, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 11, 2026
@github-actions github-actions Bot removed the pr-verify-failed pr-verify failed this head; a pass at a new commit, or after a trusted body edit, removes it. label Nov 11, 2026
@kyle-sexton
kyle-sexton added this pull request to the merge queue Nov 11, 2026
Merged via the queue into main with commit 713d146 Nov 11, 2026
86 of 87 checks passed
@kyle-sexton
kyle-sexton deleted the feat/kill-switch-start-gate-budget branch November 11, 2026 22:25
@github-actions github-actions Bot added the merge-path: owner-agent Post-merge accounting record (README.md, Merge paths) label Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-path: owner-agent Post-merge accounting record (README.md, Merge paths)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant