Skip to content

fix(pr-pipeline): end designed stops cleanly and keep held pull requests out of the merge queue - #10737

Merged
kyle-sexton merged 12 commits into
mainfrom
fix/kill-switch-deny-ends-neutral
Oct 29, 2026
Merged

kyle-sexton merged 12 commits into
mainfrom
fix/kill-switch-deny-ends-neutral

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Closes #10759
Closes #10775
Closes #10777
Closes #10785

Summary

This PR closes four issues:

Fix

  • fix(pr-pipeline): end pr-merge and intake-triage runs green-neutral on a kill-switch broker deny #10775. request-lane-token gets a kill-switch-ok input, the same opt-in shape as superseded-ok. In token mode, a 403 with reason kill-switch ends the step green with a lane-token-stopped notice, no token, and skip-reason=kill-switch. Any other status or reason stays red, and check mode refuses the input.
    • pr-merge (merge, enable-auto-merge) and intake-triage (apply-verdict, apply-refinement) opt in. Every later step in those jobs has an if:, and every step that reads the token requires token != ''. Four pr-merge steps that relied on an earlier gated step now name the token gate themselves.
    • Dependent jobs read outputs that only token steps set. intake's refine-select now also requires apply-verdict's new stopped output to be empty. callers.test.mjs pins these rules for every kill-switch-ok caller.
    • work-resolve-issue is not opted in (ci: end a work-resolve-issue kill-switch deny neutral, keeping work-unblock's re-fire #10753): work-unblock re-fires a kill-switch-stopped issue from the failed mint step, so that detector changes first.
  • fix(pr-merge): do not queue a head a pr-unblock lane stopped #10777. merge-holds.mjs gains unblockStops and readStatuses, ci-status's pr-unblock rule as it states it. enable-auto-merge reads the head's statuses and does not queue a head a pr-unblock lane stopped, or one whose statuses do not read. It disarms one already armed and adds a run-summary line for each. ci-status's own steps are unchanged. It reads the statuses on the job token, and the enable-auto-merge job gains statuses: read: GitHub lists GET /repos/{owner}/{repo}/commits/{ref}/statuses under the "Commit statuses" read permission for installation tokens (permissions required for GitHub Apps), and the lanes App's installation holds no statuses permission (gh api orgs/melodic-software/installations: actions, actions_variables, checks read; contents, issues, pull_requests, workflows write; metadata, organization_actions_variables read). The read moves from the lanes App's budget to the job token's and adds no call: one statuses read (at most 10 pages) per pull request a reader run reads.
  • feat(pr-merge): remove the lane's own hold-path needs-human label after a trusted approval lifts the hold #10785. On a reader run, enable-auto-merge deletes needs-human itself only when the label stands for its own hold-path refusal and nothing else:
    • approvalLift: the lane's newest refusal at the head names only hold paths, merge-holds' lifters show a trusted person's approval (bound to the head, or carried), no cost hold stands, and the file listing was read whole.
    • liftBlocker (security review fix), using gate-source's readers as clear-escalations does:
      • no review lane's hold names the head, or has a marker that does not parse, whenever it was written (second security review: a hold on this head under a bot's unlabel and the lane's relabel);
      • the newest labeled event for the label is the lanes App's;
      • the App's newest marked comment at or before that event is enable-auto-merge's person refusal, written within PAIRED_MS (60 s) before the label, with no other marked comment in the same second. A label the lane puts back under a standing comment with no new one does not pair, so it stays for a person;
      • no review-lane hold and no check-signed-commits hold stands since a person last removed the label. Only a User account's removal counts, as liftedByPerson reads it, so a bot's unlabel lifts nothing;
      • no lanes App comment written or edited since the label carries any other marker, so a pr-unblock escalation that added no label event keeps it;
      • no person commented, replied on a review thread, reviewed (a trusted approval aside) or made a label event since.
    • The head is read again just before the delete, and a mismatch skips it. Any failed read keeps the label.
    • It never removes a label a person applied, or one another escalation or hold stands behind. Any review-lane or signed-commits hold written since the label was last removed keeps it, whether it came before or after the approval, so the lift never rests on an approval-time comparison. A hold written before the label was last removed was already lifted by the person who removed it, the rule clear-escalations applies.
    • The delete uses the lanes token, and the unlabel re-runs ci-status at the head (pr-require-checks.yml runs on unlabeled).
  • feat(pr-pipeline): carry a hold-path approval across pushes that touch no hold-path file #10759. Owner decision Q10 (2026-10-28, ~21:15Z), verbatim: "go w/ recommendations". The rule as recommended: an approval of a hold-path head H0 stays valid at a later head H1 when the diff H0...H1 touches no hold-path file, read with the same hold-path rules (pathHoldKind with the hold paths plus HOLD_PATH_FLOOR). A push that touches any gated file needs a new approval, and a test-only or comment-only edit gets no exemption.
    • This is one more path inside approvalCarry (and its Python twin approval_carry), next to fix(pr-merge): count a stale-dismissed approval and carry it over an exact base merge #10671's base-merge carry.
    • The control is unchanged: it is still a person's approval. It must come from a trusted actor and be bound to H0's commit, a dismissal by a User revokes it, the PR must target the default branch, a compare listing 300 or more files refuses, and so does a compare that cannot be read whole.
  • Files a hold path loads. test_merge_holds_sync.py derives them: the floor entries' JS imports, the Python those run, and the actions and scripts that hold-path workflows uses: or run. It fails on any member that is neither on the floor nor in CARRY_REFUSES.
    • Trust and gate code was already on the floor, so it already counts as a hold path.
    • Enforcement-class code and the workflows, actions and scripts hold-path workflows run, plus .github/standards/** and docs/conventions/pr-pipeline/**, stop the carry through CARRY_REFUSES. Owner decision Q26 option B keeps enforcement files cleared by a security review rather than by a person, and holding about 114 CI scripts for a person would hold most PRs.
    • Security review fix: the test's matcher missed bare (python3 .base/plugins/...py) and variable-prefixed ("$scripts/...py") invocations. With those matched it found babysit_merge_prefilter.py (pr-merge.yml:1050), babysit_merge_skip.py (pr-merge.yml:1325), plugins/performance/scripts/ratchet.py and plugins/harness-ops/skills/inventory/scripts/parser_reader.py (pr-require-checks.yml:2025 and :2756). CARRY_REFUSES in both twins now adds plugins/source-control/skills/babysit-prs/scripts/**, plugins/performance/scripts/** and plugins/harness-ops/skills/inventory/scripts/**. Second security review fix: the matcher took scripts only under .github/scripts, .github/actions, scripts and lib. It now takes every tracked script a hold-path workflow names in any directory, from the head checkout or .base/, and found the skill-quality, testing (check-visual-parity), knowledge (course-digest, video-digest), ai-briefing (generate), harness-config (audit-permission-state) and source-control scripts that pr-require-checks.yml runs. Their directories join CARRY_REFUSES in both copies, and the CARRY_REFUSES comment states exactly what the test walks (a shell script's own source is not walked).
  • Owner approval needed to merge. This changes trust and gate paths: request-lane-token, merge-holds.mjs and enable-auto-merge.mjs.

Verification

  • node --test .github/scripts/pr-merge/*.test.mjs .github/scripts/pr-unblock/*.test.mjs .github/actions/resolve-config/callers.test.mjs .github/actions/request-lane-token/*.test.mjs: 1835 pass.
    • The lift tests cover the happy path (label deleted, then armed) and the approvalLift refusals: another head, dismissed, untrusted, cost, hold path beside a risk hold, no escalation, an escalation at an earlier head, and report-only.
    • New liftBlocker cases each change the liftable timeline in one place and assert the reason the label stays: a label a person added, a label a pr-unblock escalation set before the refusal, an escalation in the same second as the refusal, a pr-unblock escalation since the label (the reviewer's scenario), a review-lane hold since the label and one before the approval, a check-signed-commits hold, a person's comment, label event, review-thread reply and non-approval review, a moved head, and an unreadable timeline. A paired case still lifts with the refusal edited after the label and a bot's comment since. Second round adds: the reviewer's probe (a hold on the head, github-actions[bot]'s unlabel, the App's relabel, then the approval), a hold marker that does not parse, an earlier head's hold under a bot's unlabel and a newly paired label, and a refusal posted more than PAIRED_MS before the label. A person's removal of the label still lifts an earlier head's hold.
    • Mutation check: disabling each guard clause in turn (App actor, pairing, same-second tie, PAIRED_MS, head hold, unparsable hold, person-only removal, review hold, signed-commits hold, foreign marker, person speech, label events, thread replies, head re-read) and dropping the approval exclusion: all 15 turn the suite red. Reading statuses on the lanes token again fails the new job-token test.
    • New fix(pr-merge): do not queue a head a pr-unblock lane stopped #10777 cases: a stopped head is not queued and is named in the summary, a later success clears the stop, an unheld head still arms, and unreadable statuses hold, disarming an armed head.
  • Python (python3 -m unittest discover -s tests in babysit-prs/scripts): 3389 run, OK (5 skipped).
    • Removing the babysit-prs glob from CARRY_REFUSES makes the closure test fail on babysit_merge_prefilter.py and babysit_merge_skip.py. Before this change the old matcher passed without them. Removing plugins/skill-quality/scripts/** makes it fail on its 6 scripts; the old matcher passed without it.
    • HOLD_PATH_FLOOR and HIGH_FLOOR are the same set. New test_the_person_floor_is_the_same_list loads HOLD_PATH_FLOOR from merge-holds.mjs under node and asserts it equals babysit_risk.HIGH_FLOOR entry for entry, 32 globs (the existing test's 75 floor entries less its 43 review entries). Both are derived from the generated class list: merge-holds.mjs:349 keeps the non-enforcement HOLD_PATH_CLASSES, babysit_risk.py:310 keeps the trust and gate FLOOR_CLASSES, and scripts/sync-hold-floor.py --check (test_every_copy_is_what_the_generator_writes) keeps the two class lists identical.
    • config.denied_paths is ci-status's configured hold-path list. At babysit_verify_evidence.py:2474 the carry reads config.denied_paths. config comes from merge_policy.config_at(args.repo, args.base_sha) (babysit_verify_evidence.py:3265), which reads merge.diff-check.denied-paths from docs/conventions/pr-pipeline.yaml at the base SHA (babysit_merge_policy.py:980, CONFIG_PATH at :86). ci-status reads the same key from the same file at $BASE_SHA (pr-require-checks.yml:5191-5194). One difference: with_default_branch (babysit_verify_evidence.py:3268) unions in the default branch's list (babysit_merge_policy.py:1050). For a pull request into main the two lists are the same, and otherwise the gate's list is a superset, so the gate holds at least what ci-status holds.
  • bash scripts/ci-hold-pr-unblock-status.test.sh: 27 pass. actionlint on the two workflows is clean, and the changelog fragments are valid.
  • Left out:

Owner merge decision

Q13, 2026-10-29, on merging this pull request at 58c861047402c62d16c02c2e31a06ac83a7d4975 with the label lift included (option A), verbatim: "go w/ recommendations". Three independent security reviews: the first found one HIGH, now fixed; the second and the round-3 delta review found no HIGH or CRITICAL. pr-verify passed this head.

Related

🤖 Generated with Claude Code

kyle-sexton and others added 3 commits October 28, 2026 15:54
The lane breaker turns CLAUDE_LANES_REPO_DISABLED on after a queued
run has read its variables, so the run's own kill-switch check passes
and only the token broker's live read stops it, failing the run red.
12 runs since 2026-10-24 (pr-merge 4, intake-triage 5,
work-resolve-issue 3) ended that way.

request-lane-token takes a kill-switch-ok input (token mode only): a
403 kill-switch ends green with a notice, no token and skip-reason
kill-switch. pr-merge's merge and enable-auto-merge jobs and
intake-triage's apply-verdict and apply-refinement jobs opt in; every
step there that uses the token already runs only when it is set, and
refine-select now skips on apply-verdict's new stopped output.
work-resolve-issue stays red: work-unblock re-fires its issue from the
failed mint step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
callers.test.mjs walks every workflow job whose lane-token step sets
kill-switch-ok: each later step has an if:, a step that reads the token
requires it set, and a job that needs the stopped job's success and
reads none of its outputs skips on its skip output. It fails when
refine-select's stopped clause is removed. Four pr-merge merge steps
that relied on an earlier token-gated step now name the token gate
themselves, which changes no run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ci-status's merge-queue run fails a head whose newest pr-unblock/*
status by github-actions[bot] is failure or error, but only after every
lane leg ran, and the queue then ejected the pull request.
enable-auto-merge already refuses the other ci-status holds (labels,
body and member holds, hold and cost paths, the verifier verdict, the
kill switch) before it arms; this was the one it lacked.

merge-holds.mjs gains unblockStops and readStatuses, the rule as
ci-status states it. enable-auto-merge reads the head's statuses with
the discussion and leaves a stopped head, or one whose statuses do not
read, unarmed (disarming an armed one), with a run-summary line for
each. The merge-job pins follow the explicit token gates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he head

The lane's needs-human refusal for a hold path says a person must
remove the label, and ci-status says an approving review of the head by
a trusted actor lifts the hold, yet the owner still removed the label
by hand after approving (#10665, #10738, #10745).

On a reader run, a labeled lanes App pull request whose newest lane
refusal at its head names only hold paths is read on when a trusted
person approved in its discussion. When merge-holds' lifters (an
approval bound to the head, or one carried over base merges) lift the
hold path and no cost hold stands, the lane deletes the label with its
own token, which re-runs ci-status at the head, and judges the pull
request as unlabeled. Any other reason, head, reviewer or a dismissed
approval leaves the label on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton kyle-sexton changed the title fix(ci): end a kill-switch deny neutral, not failed fix(pr-pipeline): end designed stops cleanly and keep held pull requests out of the merge queue Oct 28, 2026
kyle-sexton and others added 5 commits October 28, 2026 17:05
…ated code

The owner's decision Q10 of 2026-10-28 (#10759): a person's approval of
a hold-path head stays valid at a later head when the diff between them
touches no hold-path file. approvalCarry and its Python twin
approval_carry take that as a second carry path beside the base-merge
one, read from the compare of the approved head to the head.

That diff must also touch no file a hold path loads: the enforcement
floor, and CARRY_REFUSES for the workflows, actions and scripts
hold-path workflows run, the trusted-actor list and the pipeline
config. test_merge_holds_sync.py derives that closure and fails on any
member outside the floor and CARRY_REFUSES. A compare not read whole, a
head not ahead, a dismissal the push did not make, an untrusted or
dismissed approval still carry nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 28, 2026 21:56
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 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-28T21:58:49.755402Z d5cb96a 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 28, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed 58c861047402c62d16c02c2e31a06ac83a7d4975: 0 finding(s), 0 as inline comments.

Incremental review of 58c8610 (since 5e9164c): no findings.

What I checked, by reading the diff and the code it calls. I ran no tests and no commands.

  • Statuses on the job token. readStatuses now takes actions. The arm step sets BABYSIT_VERIFY_GH_TOKEN: ${{ github.token }} (pr-merge.yml:2130), and main builds actions from it (enable-auto-merge.mjs:1292-1296). The job gains statuses: read (pr-merge.yml:1903), and workflow.test.mjs pins the permission list. The new job-token test shows a statuses read that fails on the lanes token no longer stops the arm when the job token reads it.
  • liftBlocker head hold. The check runs before the actor and pairing checks. laneHold returns only the marker line, and HOLD_LINE.exec(marker)?.[1] ?? head treats an unparsable marker as a hold on this head, so it fails closed. This closes the probe in the PR description: a hold on the head, a bot's unlabel, the lane's relabel, then an approval.
  • Person-only lifted. It requires actor.type === "User" and the same <= since bound as labelHistory, so a bot's unlabel lifts no earlier hold. The tests cover both a bot's unlabel and a person's removal.
  • PAIRED_MS pairing. since - paired.at <= PAIRED_MS is false for NaN, so a time that does not parse keeps the label. It matches clear-escalations.mjs:505.
  • CARRY_REFUSES parity. The seven added plugin script directories are identical in merge-holds.mjs and babysit_merge_policy.py. The test's RUNS regex now accepts only tracked files, so a path in a comment or string that is not a file is ignored.

Spend: none. The change adds no billed resources and no extra lane runs.

Closing-issue criteria (#10759, #10775, #10777, #10785): this increment only tightens the #10785 safety constraints (a label another hold stands behind is never removed), and tests cover each new guard. Nothing in the PR context file asked the lane to skip, approve, or post elsewhere.

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

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

Incremental security review of the changes since 5e9164c. I found no security issues. critical-findings: 0

I read the diff (.claude-lane/incremental.diff) and the PR context. I also read the code the changed hold paths gate: merge-holds.mjs, enable-auto-merge.mjs, gate-source.mjs, and the pr-merge.yml job env and permissions. No manifest files changed, so there are no manifest <field> lines.

Each trust-control change, checked against its code:

  • liftBlocker (enable-auto-merge.mjs, needs-human lift): tightened.
    • A review-lane hold that names the head, or whose marker does not parse, now keeps the label whenever the hold was written (.github/scripts/pr-merge/enable-auto-merge.mjs:696-701). laneHold only returns markers from claude[bot], or from github-actions[bot] with the lane footer (gate-source.mjs:58-65), so a person cannot forge one to widen anything.
    • A removal of the label now counts as lifting earlier holds only if its actor.type is User (:689-695). An unlabel with a missing or odd actor, or an unparsable time, leaves lifted lower, so more holds stay open. That fails closed.
    • The refusal comment must come within PAIRED_MS (60 s) before the label (:715). A NaN time makes !(since - paired.at <= PAIRED_MS) true, so the label stays. A label put back under a standing comment no longer pairs.
    • The head is passed in at the call site (:1505) and re-read before the delete (:1507-1508). Nothing here lets through something the base stopped.
  • Statuses read moved to the job token.
    • readStatuses now takes actions (:1489), which is BABYSIT_VERIFY_GH_TOKEN (github.token) when set, per pr-merge.yml:2130 and enable-auto-merge.mjs:1292-1296. It falls back to the lanes token otherwise.
    • A failed or forbidden read leaves stops = null, which the existing hold path treats as unarmed, so the fallback fails closed.
    • unblockStops still counts only github-actions[bot]-created pr-unblock/* statuses, so which statuses count as stops has not changed.
    • The job gains statuses: read (pr-merge.yml:1903), a read-only grant. No write or lane-token scope changed.
  • CARRY_REFUSES (merge-holds.mjs, babysit_merge_policy.py): only grows. Seven plugin script directories were added, identically in the JS and Python copies. Each addition makes a push that touches those files stop an approval from carrying, which is the stricter direction.
  • HOLD_LINE export (gate-source.mjs): an export with no change to the regex.
  • test_merge_holds_sync.py: the derived closure now takes every git ls-files-tracked script a hold-path workflow names, in any directory. That is a broader check. The test only fails on a gap and carries no runtime authority.
  • .changes/*.md and the test files: prose and tests only.

The PR context contains no embedded instructions aimed at this lane.

This change spends no prepaid credits or subscription usage and can cause no real charge.

@github-actions

Copy link
Copy Markdown
Contributor

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

Summary: The PR closes #10759 and delivers the Q10 carry correctly, but it also bundles unrequested changes to merge-gate behaviour, which blocks it. In both `merge-holds.mjs` and `babysit_merge_policy.py`, the new path lets an approval at H carry to H2 only when the H...H2 compare is ahead, fully read, and touches no hold path, floor file or CARRY_REFUSES file. Comment-only and test-only edits get no exemption. The tests cover each acceptance criterion and import new symbols, so they fail without the change. The blocker is that the PR adds a new step where the lane deletes the needs-human label itself (`approvalLift`). That weakens a human gate, and #10759 does not ask for it. The PR also changes the takeover-set action `request-lane-token` (new kill-switch-ok input) and adds a pr-unblock merge-queue hold. None of that is in #10759; the body files it under Refs #10747 and #10753 instead.
- .github/scripts/pr-merge/enable-auto-merge.mjs:1371: unrequested guard weakening. `approvalLift` makes the lane DELETE the needs-human label it escalated with, using lifters that now include approvals carried over later pushes. #10759 only asks that an approval carry to lift the hold-path hold, not that the lane remove a person-gate label. It also does not check who last applied the label. If a person re-adds needs-human at a head where the lane's hold-path-only refusal still stands, the lane takes it off again on the next reader run. Move approvalLift (and its tests at enable-auto-merge.test.mjs:646 and the rewritten case at :692) to its own PR with its own issue. If it stays anywhere, it must refuse to lift a label a person applied after the lane's refusal.
- .github/actions/request-lane-token/action.yml:173: out of scope for #10759, and this file is in the takeover set. The new `kill-switch-ok` input ends a broker 403 `kill-switch` green with no token. It mints nothing and every other denial stays red, so I found no takeover risk. But it changes how a kill-switch stop ends the merge and intake-triage runs, and steps after the mint that don't read the token can still run with GITHUB_TOKEN. Move it, with the pr-merge.yml, intake-triage.yml and callers.test.mjs changes, to a PR for its own issue (the body's Refs #10753 / kill-switch work).
- .github/scripts/pr-merge/merge-holds.mjs:776: out of scope. `unblockStops`/`readStatuses` and the enable-auto-merge unblock hold (enable-auto-merge.mjs:1415) only tighten arming, but #10759 doesn't ask for them. Split them into the PR for #10747.
- .github/scripts/pr-merge/merge-holds.mjs:703: the Q10 path (`approvalCarry` + `ungatedSince`) looks correct. It requires status ahead with behind_by 0, a commit list read whole, under 300 named files, and rename sources counted. It refuses any path matching the hold paths, HOLD_PATH_FLOOR, REVIEW_FLOOR or CARRY_REFUSES. A stale auto-dismissal carries only when its commit is in the compare. Keep this as the content of the #10759 PR.
- plugins/source-control/skills/babysit-prs/scripts/babysit_merge_policy.py:1158: the Python twin `ungated_since` uses HIGH_FLOOR where the JS uses pathHold's floor. test_merge_holds_sync.py checks the two agree on the listed cases, so no change is needed. Confirm HIGH_FLOOR and HOLD_PATH_FLOOR stay the same set.
- plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py:2474: this passes `config.denied_paths` as the Q10 hold paths. Confirm that is the same configured hold-path list ci-status uses.

This pull request leaves the merge queue, and ci-status holds it out while it carries pr-verify-failed. A pr-verify pass at a new commit removes the label.

@github-actions github-actions Bot added the pr-verify-failed pr-verify failed this head; a pass at a new commit removes it. label Oct 28, 2026
@kyle-sexton kyle-sexton added the do-not-merge Hard merge gate: do not merge while applied. label Oct 28, 2026
…'s own refusal

The lane's approval lift deleted needs-human after looking only at its own
newest refusal. The same label also carries holds that live only in it: a
pr-unblock escalation adds no labeled event when the label is already on,
and a review-lane hold, a signed-commits hold or a person's label sit under
it too. liftBlocker now reads the issue timeline, review comments and
reviews with gate-source's readers and keeps the label unless its newest
labeled event is the lanes App's, pairs with enable-auto-merge's refusal,
and nothing else stands behind it since. The head is read again just
before the delete.

The closure test now matches bare and variable-prefixed Python
invocations, which found babysit_merge_prefilter.py, babysit_merge_skip.py
and two scripts ci-status runs outside CARRY_REFUSES; both twins now
refuse the carry on them. A new test pins HOLD_PATH_FLOOR to HIGH_FLOOR.

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

@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 5e9164c0b873e5b0614d796e9822a41d949073f5; the summary is in this lane's pull request comment.

claude-lane: claude-review

…and read statuses on the job token

liftBlocker now keeps needs-human when a review lane's hold names the
head (or does not parse), whenever it was written, counts only a
person's removal of the label as lifting earlier holds, and requires the
refusal comment within PAIRED_MS of the App's label, so a label put back
with no new comment stays for a person.

The lanes App holds no commit-statuses permission, so enable-auto-merge
reads the head's pr-unblock statuses on the job token and its job gains
statuses: read.

The closure test now takes every tracked script a hold-path workflow
names in any directory; the plugin script directories ci-status runs
from join CARRY_REFUSES in both copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton kyle-sexton removed the do-not-merge Hard merge gate: do not merge while applied. label Oct 29, 2026
@github-actions github-actions Bot removed the pr-verify-failed pr-verify failed this head; a pass at a new commit removes it. label Oct 29, 2026
@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 68bdb66 Oct 29, 2026
82 of 83 checks passed
@kyle-sexton
kyle-sexton deleted the fix/kill-switch-deny-ends-neutral branch October 29, 2026 01:06
@github-actions

Copy link
Copy Markdown
Contributor

Post-merge mutation testing

Killed 0, survived 0, timeout 0, no coverage 0, invalid 0, not run 7.
Covered-code score n/a; mutation score n/a.

File Killed Survived Timeout No coverage Invalid Not run
.github/scripts/pr-merge/enable-auto-merge.mjs 0 0 0 0 0 2
.github/scripts/pr-merge/merge-holds.mjs 0 0 0 0 0 2
.github/scripts/pr-unblock/gate-source.mjs 0 0 0 0 0 1
plugins/source-control/skills/babysit-prs/scripts/babysit_merge_policy.py 0 0 0 0 0 1
plugins/source-control/skills/babysit-prs/scripts/babysit_verify_evidence.py 0 0 0 0 0 1

The run's summary has the full report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant