Skip to content

fix(pr-pipeline): route pr-unblock stops to a failing status, not needs-human, unless a person must decide - #10252

Open
kyle-sexton wants to merge 40 commits into
mainfrom
fix/pr-unblock-route-not-park
Open

kyle-sexton wants to merge 40 commits into
mainfrom
fix/pr-unblock-route-not-park

Conversation

@kyle-sexton

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

Copy link
Copy Markdown
Member

No related issue: owner rule 2026-10-26 (a person only for intent, design or HIGH/CRITICAL security)
Refs: #10136
Refs: #10125
Refs: #10118

Summary

The pr-unblock lane put needs-human on a pull request for every stop, defects included (a CI fix the rung-up retry could not land, an uncertain review reply, a round cap). This routes each stop through one table, .github/scripts/pr-unblock/escalation-routes.json:

  • hold: the label and the resume-key comment stay as they are. These are SAFETY reasons (paths the lane token may not push, supply-chain refusals), intent, design and security questions, a person's own thread or hold, and findings no adjudicator may judge (security-review or CRITICAL).
  • fail: no label. The run ends in a failing commit status on the head the activity gated, pr-unblock/conflict for resolve-conflicts and pr-unblock/<activity> otherwise. It also posts one lanes App comment per activity, which the next stop edits in place. A new head carries no status.

A stop that came after a model turn is retried on the same head, under the per-head cap each dispatcher already keeps. fix-ci and address-feedback retries run one model rung up. A retry that acts and does not stop clears the status; one that ends on a skip leaves it. Once the retries are spent, the monitoring lane's new stalled lane stop signal names the PR in its rolling issue, so a red PR always reaches someone without needs-human.

Fix

  • escalation-routes.json is the one routing table: each reason's activity, route and text. report-escalation.sh, gate-source.mjs (escalationActivity, escalationRoute) and clear-escalations.mjs all read it.

  • report-escalation.sh:

    • hold: unchanged.
    • fail: upserts a <!-- pr-unblock:lane-stopped activity=<a> --> comment and writes nothing when the text is unchanged.
    • status mode posts the failing status for any fail-routed reason and nothing for a held one.
  • pr-unblock.yml: report-conflict-status becomes report-stop-status. It runs when report-escalation does, on the same reason, never on a cancelled run, and posts on the select job's head. It keeps its own GITHUB_TOKEN with statuses: write; no token scope changes.

  • Bounded step for feedback-uncertain:

    • UNCERTAIN and VALID-defer replies now go through the existing adjudicator: adjudicate-feedback.mjs DISPUTED, and the scan job's replies-disputed gate in pr-run-activity-read.yml.
    • A lane verdict (the finding does not hold) never resolves a VALID-defer thread, where the fixing lane agreed the finding holds (settleDisputes): the thread stays open with no adjudication reply, so the ruleset's thread resolution still holds the merge, and the round ends feedback-unadjudicated (fail). A reviewer verdict hands it to the next round, and human asks the question, as for any other dispute.
    • A pr-verify note left open ends feedback-verifier-open (fail). A stood finding left open again ends feedback-stood-open (fail).
    • Security-review, CRITICAL and unknown-lane threads stay held: feedback-disputed for INCORRECT, feedback-uncertain otherwise.
    • A held reason now outranks a fail-routed one within a round.
  • ci-fix-*: every reason routes to fail except ci-fix-protected-path and the new ci-fix-weaken-required. Both are held, and both are now also emitted when the model itself escalates protected-path or weaken-required (apply-patch.mjs heldEscalation).

  • Caps (repeat-cap, feedback-not-converging, dependency-round-cap, ci-fix-round-cap) route to fail, per D3.

  • gate-source.mjs and clear-escalations.mjs treat the lane-stopped marker as the lane's own.

  • clear-escalations.mjs removes a label an older run set for a reason now routed to fail. A ci-fix-* reason clears only after it reads the head's pr-require-checks.yml runs and their check runs, and a lane check other than ci-status reads failed (failingChecks, now in gate-source.mjs on the merge path floor). ci-status itself fails on the label, so it proves nothing. An advisory lane (pr-test-windows) does not count, because ci-status would go green once the label is off. A cancelled lane check counts as failed too, as main's failingChecks now reads it (fix(pr-pipeline): let fix-ci take a job cancelled past its time limit #10269), and ci-status reads a cancelled lane as red. Green, skipped, missing or unreadable keeps the label. The read uses the merge job's own token (actions: read, checks: read, passed as CHECKS_TOKEN), because the lane token holds neither. Every other fail-routed reason keeps the label for a person. A lane posts a pr-unblock/* status on a head only when it stops there (failure) or clears its own failure (report-escalation.sh clear), so a moved head carries nothing that shows the cause gone or fails that head in the label's place. The pr-merge job's token has no statuses: read either, so reading the new head's status there would need a scope change.

  • The merge path floor holds escalation-routes.json beside escalation-routes.mjs, so moving a reason from hold to fail needs a person: merge-holds.mjs HOLD_PATH_FLOOR, babysit_risk.HIGH_FLOOR, ci-status's hold_patterns, both review lanes' hold-paths, and the tests that pin them. test_merge_holds_sync.py now also follows a module-relative file a floored module reads (new URL("./…", import.meta.url)), so the next such data file fails there. It names one file it reaches that is not on the floor: docs/conventions/pr-pipeline/pr-pipeline.schema.json, whose API-limit defaults rate-guard.mjs reads.

  • The pr-run-activity-write.yml enum gains feedback-stood-open, feedback-verifier-open and ci-fix-weaken-required.

  • New skip reason lane-stopped (pr-pipeline.schema.json), emitted for every fail-routed stop through escalation-routes.mjs stopSkip, in place of awaiting-human:

    • On a select job's own check, it ends the head, so deterministic stops are not re-run. These are the caps and a conflict no model can resolve. fix-ci-not-run now runs on the round-cap stop as well.

    • On the push, reply or verdict job's check, the dispatcher releases the head for a same-head retry (limit-stop.mjs stopRelease / releasedBy), under its existing cap:

      activity stopping job's check cap
      resolve-conflicts apply-conflict-patch limit-stops-per-head = 3
      review-dependency apply-dependency-verdict limit-stops-per-head = 3
      fix-ci apply-patch a stopped run counts as a failed one, so fix-ci-restarts = 1 retry, within maxRunsPerHead = 3
      address-feedback publish-feedback SWEEP_HEAD_CAP = 3, with the other releasing skips
  • Rung-up on retry:

    • fix-ci-context and the address-feedback select read the head's own lane-stop record and output model-rung 1 (escalation-routes.mjs retryRung). The record must come from the GitHub Actions app.
    • No workflow_dispatch input sets the rung.
    • review-dependency stays at rung 0, because its review profile names no escalate-to. resolve-conflicts retries as a fresh session, because its select job holds no checks: read; adding that scope was out of bounds for this change.
  • report-stop-status also runs after an attempt whose push, reply or verdict job acted and ended without a stop. report-escalation.sh clear then posts success over a failure the lane left on that head. A job that succeeded on a skip (the reply script's no-model-turn, a gate or head-recheck skip) landed nothing, so the failure stays. The write runner hands up a new skip-reason output for this.

  • address-feedback select: with no kept thread and an unresolved one it does not take (a Codex-root thread, a review of an older head, a thread it already answered), it escalates the new feedback-untaken-threads (fail) instead of skipping not-applicable. It skips not-applicable only with no unresolved thread, and awaiting-human on a held PR. threadAuthors' reviewer set is unchanged.

  • The feedback dispatcher now fires once per ungated head that has any unresolved thread, workable or not, so select reaches feedback-untaken-threads. A select job's own lane-stopped check now ends the head: the reply job's matching lane-stopped skip no longer releases it, so the next tick does not fire again. What select keeps is still decided by onReviewedHead, shared by keepThreads and hasWorkableThread; THREADS_QUERY reads each first comment's pullRequestReview { author commit }. A gated head still dispatches only for a workable thread.

  • A reply job that fails with no escalation reason (fix(app-control): find plugin control skills at manifest-declared paths #10092's run failed at its lane token) now stops as the new feedback-publish-failed (fail). pr-unblock.yml derives it from needs.publish-feedback.result == 'failure', so it does not depend on a failed job's outputs.

  • ci-status (pr-require-checks.yml, new step Fail a head a pr-unblock lane stopped) fails while the newest pr-unblock/* commit status on the head by github-actions[bot] is failure or error, and names each such context. Only ci-status is required, so without this the merge queue and a hand merge ignored a failing pr-unblock status. It reads newest per context as report-escalation.sh clear does, so the lane's later success on the same head supersedes the failure, and another account's status neither holds nor clears. It runs on pull request runs (the event's head) and in the merge queue (the queued pull request's head, read live; the group ref's SHA is the base). An unparsable ref, a bad head SHA or an unreadable pull request or status list fails closed. A status post re-runs nothing, so a stop posted after ci-status passed on a head holds at the merge queue's run, not on the pull request's check.

  • Each lane-stopped comment names its run, so every stop edits it and its last edit is the newest stop's time.

  • Known gap: a stop cleared on its head (a same-head retry, or stop-resolved) can still be listed by the monitor's stalled-stop signal after two hours. The signal reads comments and check runs, and the monitor job has no statuses: read, which GitHub lists the commit-status endpoints under (https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps). It is a false alarm, not a weakened hold, so no scope is added for it.

  • fix-ci-context and the fix-ci dispatcher read check-runs?filter=all (both listCommitCheckRuns adapters), so a contract-only run's later skip of a lane no longer hides that lane's failure from failingChecks or lets stop-resolved clear a red head. Each still reads at most three pages.

  • Monitoring lane signal 10, stalled lane stop (.github/scripts/maintenance-monitor-signals/):

    • It lists open PRs of any author, because review-dependency works Dependabot's PRs and fix-ci adopts owners' PRs.
    • A PR is listed when, for the current head, it has a lane-stopped comment, or a needs-human-cleared comment for a reason now routed to fail, that is older than 2 hours, and no pr-unblock / <activity> attempt has started on that head since.
    • A Skipped: check does not count as an attempt. That covers a select job's own stop, and a no-op feedback retry that skipped not-applicable.
    • It reads the comment and check runs, because the monitor token has no statuses: read.
    • One GraphQL query lists the 40 most recently updated open PRs with each one's newest 100 comments and the open count. At most two more queries, one alias per PR, read older comments for a PR whose newest comments name no stop on its current head. It makes at most 20 check-run reads. It records a gap past 40 open PRs, past 20 candidate stops, or for a PR whose older comments stay unread. An older head's marker never ends the read.
    • Its worst case is 23 calls (1 + 2 + 20) of CALL_BUDGET 250. The whole run's reads come to at most 166 from LIMITS, assuming one page of filed issues: 1 rate-limit, 25 pr-merge budget, 31 jobs in use, 49 failed runs, 7 main run, 11 merge-queue, 7 sweep, 1 lane merges, 5 lane stops, 23 stalled stops, 4 audits, 1 filed issues, 1 label. That leaves 84 for writes.
    • N = 2 hours, because pr-unblock-dispatch.yml runs on cron 9,39, every 30 minutes, and the feedback sweep runs off each tick. A retry the lane will make starts within one tick, so four ticks with none mean the retries are spent.

What still holds the merge once the label is gone: unresolved review threads for every address-feedback stop (the main ruleset sets required_review_thread_resolution: true), a conflict for resolve-conflicts, red ci-lanes, and so ci-status, for fix-ci, ci-status itself on any failing pr-unblock/* status, and the merge gate's failing-check blocker (babysit_checks.classify_checks, which counts a commit status) for all of them.

Bounded step before each fail ends for good:

  • fix-ci empty or escalated: the in-run rung-up, then same-head retries at rung 1.
  • feedback-uncertain and feedback-disputed: the adjudicator, which never resolves a VALID-defer thread on a lane verdict.
  • Caps: the cap itself, plus the fresh fixer round for feedback-not-converging. These are final for the head.
  • Every other stop after a model turn: same-head retries under the dispatcher's cap above. fix-ci gets one of these (fix-ci-restarts = 1).
  • A same-head retry cannot change four outcomes: feedback-partial, feedback-unadjudicated, feedback-stood-open and feedback-verifier-open. The lane's reply is already the thread's last comment, so the retry's select escalates feedback-untaken-threads. A person resolving the threads frees the head: the next address-feedback dispatch finds no unresolved thread and clears the status (below). Otherwise these end at the stalled-stop signal.
  • A select job that finds its stop gone on the head clears the status (stop-resolved, then report-escalation.sh clear in report-stop-status, the existing writer):
    • address-feedback: no human-gated hold and no unresolved review thread.
    • fix-ci: a whole listing of the head's check runs (total_count read, past three pages it is not whole), every one completed, none failing. fix-ci now reads the checks before the round cap, so a head re-run green after the cap clears too.
    • It clears only inputs.activity's own context, on the head the select read, and only when the select job succeeded; a failed read writes no outputs. resolve-conflicts and review-dependency are unchanged.
    • Nothing dispatches it on its own: the dispatch the lane-stopped comment gives does. A ci-status that already failed stays failed until something re-runs it (the next review lane's rerun-ci-status, or a person).
  • Trust boundary of the status hold: ci-status and clear count a pr-unblock/* status by creator github-actions[bot], the identity of every workflow's GITHUB_TOKEN. A status can be forged only by a same-repository pull request whose own workflow posts one, which needs an account that can push workflow files: not the lanes App (no workflows permission) and not a fork (read-only token). That account can already remove a needs-human label, so the status hold is exactly as strong as the label hold it replaces. Posting with the App token would need a statuses grant the broker does not give, and checking target_url proves only that a run exists, so neither is added. Every merge to main goes through the merge queue (ruleset "merge-queue", no bypass actors), where ci-status runs again.

Workflow files changed: pr-unblock.yml, pr-run-activity-read.yml, pr-run-activity-write.yml, pr-merge.yml (the clear step gets the job's own token as CHECKS_TOKEN) and pr-require-checks.yml (the ci-status step above). The lane token cannot push these. pr-run-activity-write.yml also copies escalation-routes.mjs and its table beside apply-patch.mjs. No token scope changed.

Verification

  • node --test in .github/scripts/pr-unblock: 812 pass, 0 fail. New tests cover:
    • Same-head retries per dispatcher: [model check succeeded, later stop] dispatches; the cap ends it with a warning and a record; a retry that did not stop handles the head; a select job's lane-stopped is final.
    • fix-ci: one restart, then past fix-ci-restarts.
    • feedback: the sweep stops at SWEEP_HEAD_CAP.
    • dependency: stops at limit-stops-per-head.
    • The retry rung: "1" only after a GitHub Actions lane stop.
  • bash .github/scripts/pr-unblock/report-escalation.test.sh: 803 ok, 0 not ok. It covers:
    • every route in the table
    • each fail reason: no label, one comment, the run named
    • edit-in-place
    • each fail status and its context
    • each held reason still labelling and posting no status
    • re-arm on a new head
    • clear: success over the lane's own failure only
  • node --test in .github/scripts/apply-patch: 135 pass, including the held-escalation split. .github/scripts/pr-merge: 476 pass, including a fix-ci clear on a red pr-require-checks lane only (green, ci-status-only red, a red pr-test-windows, skipped, another app's, or a failed read keeps it; a newer skipped duplicate does not hide a failure).
  • .github/scripts/maintenance-monitor-signals: 78 pass, including the 23-call worst case and an older head's marker that no longer hides the current head's stop. The detector:
    • lists a stalled PR, including a Dependabot one
    • lists a stop followed only by a lane-stopped or not-applicable skip
    • lists a cleared ci-fix-escalated label, the shape feat(planning): give acceptance criteria ids and trace them to plan phases and decompose slices #10136 has
    • ignores a fresh stop, a stop with a later attempt, a stop on an older head, a marker a person wrote, and a cleared held reason .github/actions/resolve-config: 698 pass, including a skip that does not clear the stop status and feedback-publish-failed from a failed reply job. .github/actions/report-check-run: 101 pass.
  • Clearing a stop the select finds gone: address-feedback.test.mjs (every thread resolved by hand clears; an untaken, a taken or a held thread does not; a failed thread read throws and writes no outputs; a check listing past three pages is not whole), fix-ci-context.test.mjs (no failing check clears, at the round cap too; a failing, cancelled or in-progress check, a cut-short listing, a hold or a closed PR does not; a failed listing read throws and writes no outputs), callers.test.mjs (report-stop-status runs clear on a resolved select that succeeded, not on a failed one). Each failed before the change.
  • The floor: test_merge_holds_sync.py failed before the list change (the table unheld, 38 != 39). Babysit Python suite: 3073 pass. scripts/ci-lanes-pr-hold.test.sh: 156 pass. scripts/ci-review-lanes.test.sh: 98 pass. node --test .github/scripts/pr-merge: 476 pass.
  • scripts/write-runner-never-rules.test.sh: 76 pass. scripts/plan-test-lanes.test.sh: 45 pass.
  • Precedence: a held reason outranks a fail reason in one round (adjudicate-feedback.test.mjs). feedback-protected-path and the other held reasons still skip awaiting-human.
  • scripts/ci-hold-pr-unblock-status.test.sh: 27 pass, 0 fail; it fails on the base, which has no such step. It covers no pr-unblock/* status, a success and a pending one passing; a failure and an error each failing and named; a newer success over a failure passing and a newer failure over a success failing; another account's status neither holding nor clearing; an unreadable status list, a bad head, an unreadable queued pull request and an unparsable merge-group ref failing closed; and the merge queue reading the queued pull request's head, not the group's base. scripts/check-lane-coverage.sh --check is green (the step is not continue-on-error, so its exit code is the hold).
  • After the security lane's review at b34abd39, with origin/main merged: clear-escalations.test.mjs ("a label for a reason now routed to a failing status: fix-ci comes off on a red head, others stay after the head moves", over every non-fix-ci fail-routed reason in the table) and adjudicate-feedback.test.mjs ("settleDisputes never resolves a VALID-defer finding on a lane verdict") each failed before the fix and pass after; publish-feedback.test.mjs's dispute test now expects the same. node --test over .github/scripts/pr-unblock, .github/scripts/pr-merge, .github/actions/resolve-config, .github/scripts/apply-patch and .github/scripts/maintenance-monitor-signals: 2211 pass, 0 fail. Babysit Python suite: 3073 pass. scripts/ci-hold-pr-unblock-status.test.sh 27 pass, report-escalation.test.sh 0 not ok, scripts/ci-lanes-pr-hold.test.sh 156 pass, scripts/check-lane-coverage.test.sh 41 pass, scripts/check-lane-coverage.sh --check green, actionlint 1.7.12 clean. The origin/main merge composed pr-run-activity-write.yml's apply-patch base copy (main's merge-holds.mjs beside this PR's escalation-routes.mjs and its table, one cp line each); scripts/write-runner-never-rules.test.sh 76 pass, and the other write-runner tests scoped to that workflow pass. The filter=all advisory is fixed: the new tests in address-feedback.test.mjs and fix-ci-dispatch.test.mjs each failed before the change. The stalled-stop false alarm is accepted, not fixed (above). On the final head, the node suites pass 2213, report-escalation.test.sh 803 ok and 0 not ok, the babysit suite 3073, and the shell suites, lane coverage, actionlint and shellcheck are clean.
  • actionlint, shellcheck, shfmt, markdownlint-cli2, typos, editorconfig-checker and check-changelog-fragments.sh --check are clean.

Related

🤖 Generated with Claude Code

kyle-sexton and others added 5 commits October 26, 2026 00:02
…ds-human, unless a person must decide

One table, .github/scripts/pr-unblock/escalation-routes.json, gives each
pr-unblock stop reason its activity and route. report-escalation.sh,
gate-source.mjs and clear-escalations.mjs read it. `hold` keeps the
needs-human label and resume-key comment for SAFETY reasons and for
questions only a person settles. `fail` adds no label: report-stop-status
posts a failing pr-unblock/<activity> (or pr-unblock/conflict) status on
the gated head, which a new head does not carry, and one lane-stopped
comment per activity is edited in place.

UNCERTAIN and VALID-defer replies now go through the existing
adjudicator. A pr-verify note left open ends feedback-verifier-open, and
a stood finding left open again ends feedback-stood-open. Both route to
fail. clear-escalations lifts labels that older runs set for reasons now
routed to fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…existing caps, and report the ones left red

A stop routed to a failing status now skips lane-stopped, which is a new
skip reason in the schema. On a select job's own check it ends the head,
so a deterministic stop is no longer re-dispatched blindly. On the push,
reply or verdict job's check, the activity's dispatcher releases the head
for a same-head retry, through limit-stop.mjs stopRelease. Each activity
keeps the per-head cap it already has:
- resolve-conflicts and review-dependency: limit-stops-per-head
- fix-ci: fix-ci-restarts and maxRunsPerHead
- address-feedback: SWEEP_HEAD_CAP

The fix-ci and address-feedback retries run one model rung up. The rung
is read in the run from the head's own stop record by the GitHub Actions
app, never from a dispatch input.

A retry that does not stop posts success over the failing status, through
report-escalation.sh clear. Each stop comment now names its run, so every
stop edits it.

fix-ci model escalations of protected-path or weaken-required stay with a
person, as ci-fix-protected-path and the new ci-fix-weaken-required. fix-ci's
round-cap stop now posts its own check instead of leaving the head
undispatched-but-unrecorded.

The monitoring lane gains signal 10, stalled lane stop. It lists open pull
requests, of any author, whose stop comment names the current head and
has had no attempt of that activity on the head for over 2 hours.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, and cover labels clear-escalations lifted

The stalled lane stop signal treated any later activity check as an
attempt. That hid two kinds of PR: one with a select job's own
lane-stopped skip, and one with a no-op feedback retry that skipped
not-applicable. A skip is no longer an attempt.

The signal also reads clear-escalations' needs-human-cleared comment. A
cleared fail-routed reason, such as #10136's ci-fix-escalated, is a stop
on that head, so a lifted label is still reported once its retries are
spent.

The reads now stay within the call budget: at most 20 pull requests and
20 check-run reads, and a full comment page with no marker is recorded
as a gap.

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

# Conflicts:
#	.github/scripts/pr-unblock/dependency-dispatch.mjs
#	.github/scripts/pr-unblock/dispatch.mjs
#	.github/scripts/pr-unblock/fix-ci-dispatch.mjs
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 26, 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-26T10:32:14.947727Z 5488e10 PR opened
ℹ️ 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 26, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed ea2b0eea312aeae046b2e83b69841247fd71bbfe: 1 finding(s), 1 as inline comments.

Security review: PR #10252 (pr-unblock stops route to a failing status)

I read the whole diff and the PR context. Most of the review went on the trust-control files: merge-holds.mjs, clear-escalations.mjs, gate-source.mjs, escalation-routes.json/.mjs, the new ci-status step in pr-require-checks.yml, the pr-merge.yml token wiring, babysit_risk.py and pr-pipeline.yaml. I also read report-escalation.sh, adjudicate-feedback.mjs and the pr-unblock.yml / pr-run-activity-*.yml changes.

critical-findings: 0

I found no CRITICAL issue. None of these widened:

  • the trusted-trigger list
  • lane token scope (the new CHECKS_TOKEN is the job's own github.token, passed only to clear-escalations)
  • the kill switch
  • the broker's signer inputs (the pr-pipeline.yaml change is a comment only)

Held reasons. Safety, security, intent and design stop reasons stay hold. Both new floor entries are mirrored in HOLD_PATH_FLOOR, HIGH_FLOOR, ci-status's hold_patterns and both review lanes' hold-paths. An unlisted reason still holds, because escalationRoute returns null and stopSkip falls back to awaiting-human.

New ci-status hold step.

  • It fails closed on a bad merge-group ref, a bad head SHA or an unreadable pull request or status list.
  • It counts only github-actions[bot] statuses, newest per context.
  • A same-repo pull request whose workflow can post statuses can forge a success. That needs write access, which also lets it remove the needs-human label, so the hold is no weaker than the label it replaces.

Label clearing.

  • clear-escalations removes a label only for ci-fix-* reasons, and only when a lane check of the head's pr-require-checks runs reads failed.
  • Unreadable or cut-short listings keep the label.
  • The change only makes a label go away when the head is red anyway.

Adjudicator now handles UNCERTAIN and VALID-defer replies (not counted as a finding).

  • A lane verdict can now resolve an UNCERTAIN thread without a person.
  • It can never resolve a VALID-defer thread (settleDisputes).
  • Security-review, CRITICAL and unknown-lane threads stay held.
  • This adds no new capability: the INCORRECT path already let the adjudicator resolve a thread on a lane verdict. It is still a change to who decides, made on the owner's rule.

Retries (not counted as a finding). Same-head retries and rung-ups run the model more often, up to the per-head caps. That spends only prepaid API credit or subscription usage.

manifest: no plugin.json or marketplace.json changed, so there are no manifest lines to report.

Finding

  • IMPORTANT: the hold floor does not cover the files that now decide hold versus fail, so "moving a reason from hold to fail needs a person" is not fully true. The inline comment on merge-holds.mjs has the details.

@github-actions github-actions Bot added the needs-human Human-in-the-loop required; autonomous sessions must not resolve items carrying this. label Oct 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

claude-security-review holds this pull request for a person (critical-security) at 5488e10cf2119f7a724bfc85d4e166ad7b57c113.

CRITICAL trust-control finding: .github/scripts/pr-merge/clear-escalations.mjs now removes the needs-human merge-hold label for every fail-routed reason, unconditionally for ci-fix-* reasons. This narrows a merge hold, and the replacement hold is not shown by the code. A person should confirm the replacement holds (failing pr-unblock/* status, the thread-resolution ruleset, the gate's failing-check blocker) cover every cleared reason. The IMPORTANT companion finding is that escalation-routes.mjs and escalation-routes.json are outside HOLD_PATH_FLOOR.

claude-lane: claude-security-review

@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: 5488e10cf2

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

Comment thread .github/scripts/pr-unblock/fix-ci-dispatch.mjs Outdated

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

claude-lane: claude-review

Comment thread .github/scripts/maintenance-monitor-signals/collect.mjs Outdated
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed ea2b0eea312aeae046b2e83b69841247fd71bbfe: 1 finding(s), 1 as inline comments.

I reviewed the diff in .claude-lane/pr.diff (63 files) and the PR context. I read the routing table and its readers, report-escalation.sh, the pr-unblock.yml and pr-run-activity-*.yml changes, the ci-status step in pr-require-checks.yml, clear-escalations.mjs, the adjudicator changes, the address-feedback select, and the monitor's stalled-stop collector. I did not run any tests (no shell in this lane). The test results quoted in the PR body are the author's claims and I did not verify them.

Finding (advisory): the lane-stopped comment text tells readers "This needs no person" for every fail-routed reason. The PR body says four of them (feedback-partial, feedback-unadjudicated, feedback-stood-open, feedback-verifier-open) cannot change on a same-head retry, so a person has to resolve the threads. feedback-untaken-threads can need a person too, for example an open human thread. Details are in the inline comment.

Checked and found consistent:

  • Routing table: the table covers the write runner's reason enum. ci-fix-weaken-required is in the enum, the apply-patch held-escalation split, and the table.
  • Merge-path floor: both new files are in HOLD_PATH_FLOOR, babysit_risk.HIGH_FLOOR, ci-status's hold_patterns, and both review lanes' hold-path lists. The sync tests pin the new count of 39.
  • ci-status step: it reads statuses newest-first per context, counts only github-actions[bot], and fails closed on an unreadable list, a bad head, or an unparsable merge-group ref. The trust argument for the status hold (same strength as the label hold) holds up on the diff.
  • Tokens: no token scope changed. CHECKS_TOKEN is the merge job's own github.token.
  • Changelog bumps: minor for pr-pipeline and patch for source-control match the change.

Closing issues: none (the PR states "No related issue"), so there are no acceptance criteria to check.

Cost: this change spends only prepaid or subscription usage (same-head retries, rung-up model runs, adjudicator turns, and one more monitor GraphQL query run). It starts no real charge, so there is no hold.

kyle-sexton and others added 7 commits October 26, 2026 07:18
clear-escalations.mjs and gate-source.mjs now import
escalation-routes.mjs, so the floor's import closure loads it and
test_merge_holds_sync requires it on the floor. List it beside its
sibling pr-unblock modules in babysit_risk.HIGH_FLOOR, HOLD_PATH_FLOOR,
the review lanes' hold-paths and ci-status's hold_patterns, and pin the
floor at 38 entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix-ci and the in-run fix-ci-rung-up both post a `pr-unblock / fix-ci`
check. When the rung-up's apply-patch stopped, withStops reclassified
only the second check as failed; the first stayed "Passed: no changes",
so skipReason held the head at `!runs.every(retryable)` and the
same-head restart never ran. withStops now groups the checks by check
suite (one workflow run), reads each group from its last check, and
marks the group failed when a stop in the same suite (or, with no suite
id, by timing) followed any of its checks.

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

report-check-run creates the fix-ci and apply-patch checks over the API,
and GitHub attaches such a check to a check suite of its choosing on the
head (ci-status lands in pr-require-checks' suite), so two runs' checks
can share a suite id and collapse into one attempt, which would keep the
per-head cap from ever tripping. Read the posting run from the check's
details_url, which report-check-run sets to that run's page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stalled-stop signal listed open pull requests oldest update first
and read only the first 20, so a pull request with a fresh lane-stopped
comment sorted behind idle ones and was never read once more than 20
were older; with 37 open today, the permanent gap also left signal 10
unread on every run. It now pages the open list most recently updated
first (at most 3 pages), reads comments for the first 40, and names the
true open count in the gap instead of a single page's 100.

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

# Conflicts:
#	.github/scripts/pr-unblock/fix-ci-dispatch.test.mjs

@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-security-review: 1 inline finding(s) on c9e643f6126d62780514ab7378e3a9f6996bd7ea; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

Comment thread .github/scripts/pr-merge/clear-escalations.mjs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

claude-security-review holds this pull request for a person (critical-security) at c9e643f6126d62780514ab7378e3a9f6996bd7ea.

Trust-control change: the PR removes the needs-human merge hold for about 25 pr-unblock stop reasons and makes clear-escalations.mjs (.github/scripts/pr-merge/clear-escalations.mjs, lines 155-159) clear fix-ci labels on an unchanged head without reading CI state. The replacement is a non-required pr-unblock/<activity> commit status, and nothing in this diff makes that status block auto-merge. unexpected-path-staged and several dependency refusals also move from hold to fail. A person should confirm the narrowing is intended and that a required check enforces the hold.

claude-lane: claude-security-review

@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: 3 inline finding(s) on c9e643f6126d62780514ab7378e3a9f6996bd7ea; the summary is in this lane's pull request comment.

claude-lane: claude-review

Comment thread .github/workflows/pr-unblock.yml Outdated
Comment thread .github/scripts/maintenance-monitor-signals/collect.mjs
Comment thread .github/scripts/maintenance-monitor-signals/collect.mjs Outdated
kyle-sexton and others added 8 commits October 26, 2026 08:00
…s read

clear-escalations took needs-human off any ci-fix-* reason routed to a
failing status without reading a CI result. It now reads the current
head's ci-lanes commit status (one GraphQL read per PR) and clears only
when the GitHub Actions bot recorded it failed. Green, pending, missing,
another creator, or an unreadable read keeps the label. ci-status itself
is not read: it fails on the label, so it proves nothing.

The ci-lanes reader moves from fix-ci-dispatch.mjs into gate-source.mjs,
already on the merge path floor, so clear-escalations imports no new file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
report-stop-status posted report-escalation.sh clear after any write job
that succeeded with no stop, so a same-head address-feedback retry whose
reply script skipped no-model-turn posted success over the earlier
feedback-not-landed or feedback-invalid failure. The write runner now
hands up its skip reason (the gate's reason or the check run's skip), and
clear runs only when the push, reply or verdict job acted. The sweep
already counts a no-model-turn skip toward SWEEP_HEAD_CAP, so a stuck
head stays red and the stalled-stop signal lists it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ci-lanes commit status needs statuses: read, which neither the lane
App token nor the merge job's token holds, so that read always failed
closed and a fix-ci label could never clear. clear-escalations now reads
the head's check runs with the job's own token (checks: read, passed as
CHECKS_TOKEN) and clears only when a GitHub Actions check other than
ci-status failed, through fix-ci-context's failingChecks, which moves to
gate-source.mjs on the merge path floor. Green, skipped, another app's,
a listing past three pages, or a failed read keeps the label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stalled-stop source listed open pull requests over REST and read one
comment page per pull request, up to 3 + 40 + 20 of the run's 250-call
budget. One GraphQL query now lists the 40 most recently updated open
pull requests with each one's newest 100 comments and the open count;
at most two more queries read older comments, batched one alias per pull
request (batchQuery, the stacked branch's pattern). Worst case: 1 + 2 +
20 calls.

A lane-stopped comment for an older head also set `marked`, so it hid the
current head's stop in an older comment and recorded no gap. Only a
marker naming the current head now ends the read; a pull request whose
older comments stay unread past the rounds is a gap.

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

#10092 stalled with no stop on record. Three gaps:

- The address-feedback select skipped not-applicable when it kept no
  thread, even with an unresolved one it does not take (a Codex-root
  thread, a review of an older head). It now escalates
  feedback-untaken-threads, routed to a failing status, and skips only
  when no thread is unresolved (or awaiting-human when a person holds
  the PR). threadAuthors' reviewer set is unchanged.
- The dispatcher's hasWorkableThread skipped keepThreads' review-head
  check, so it dispatched for a stale-head thread the select job then
  skipped, and that neutral skip marked the head handled for good. Both
  now judge it with onReviewedHead, and THREADS_QUERY reads each first
  comment's pullRequestReview author and commit.
- A reply job that failed before its script decided (the 20:57 run
  failed at its lane token) left an empty escalation reason, so nothing
  was posted. The write runner now names feedback-publish-failed, routed
  to a failing status, when a publish-feedback run job fails before its
  script succeeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fix-ci clear counted a failure from any GitHub Actions check, so a
red advisory pr-test-windows run kept clearing the label while ci-status,
which does not aggregate it, went green with the label gone. The read
now lists the head's pr-require-checks.yml runs (actions: read on the
same job token) and counts only check runs in their check suites. It
reads every check run on the head (filter=all), so a contract-only run's
later skip of a lane does not hide that lane's failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ailed reply's stop

The previous commit let select escalate feedback-untaken-threads, but the
dispatcher still refused a PR whose only unresolved threads select will
not keep, so the escalation could not fire for #10092's shape. On an
ungated head the dispatcher now fires for any unresolved thread. Select
escalates, and its not-run job's lane-stopped check ends the head: a
reply check's lane-stopped skip no longer releases a head whose
address-feedback check is a select stop, so the next tick does not fire
again. A gated head still dispatches only for a workable thread, and
onReviewedHead still decides what select keeps.

feedback-publish-failed is now derived in pr-unblock.yml from
needs.publish-feedback.result == 'failure' with no escalation reason,
instead of a failure() step's output in the write runner, which relied on
a failed job's outputs reaching the caller.

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: 2 inline finding(s) on a92de9da3714eba90a51e0c838bafbe4880a8f43; the summary is in this lane's pull request comment.

claude-lane: claude-review

Comment thread .github/scripts/pr-unblock/escalation-routes.json
Comment thread .github/workflows/pr-unblock.yml
kyle-sexton and others added 5 commits October 26, 2026 10:41
…not-park

# Conflicts:
#	.github/scripts/pr-merge/clear-escalations.mjs
#	.github/scripts/pr-merge/clear-escalations.test.mjs
#	.github/scripts/pr-unblock/fix-ci-context.mjs
#	.github/scripts/pr-unblock/fix-ci-dispatch.test.mjs
#	.github/workflows/pr-merge.yml
A fail-routed pr-unblock stop ends in a failing pr-unblock/<activity>
commit status, but only ci-status is required, so the merge queue and a
hand merge ignored it. ci-status now fails while the newest pr-unblock/*
status on the head by github-actions[bot] is failure or error, naming
each context; a later success on the same head supersedes it, as
report-escalation.sh clear reads it. In the merge queue it reads the
queued pull request's head. Unreadable reads fail closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's failingChecks now counts a cancelled check as failing (#10269);
merged into gate-source.mjs it also reaches clear-escalations, where a
cancelled lane keeps ci-status red once the label is off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 98798d92bbc03ccf3f256f15c9f820e16e6f047a; the summary is in this lane's pull request comment.

claude-lane: claude-review

Comment thread .github/workflows/pr-unblock.yml

@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-security-review: 1 inline finding(s) on 98798d92bbc03ccf3f256f15c9f820e16e6f047a; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

Comment thread .github/scripts/pr-unblock/escalation-routes.json
kyle-sexton and others added 4 commits October 26, 2026 11:05
…finds gone

- escalation-routes.json joins escalation-routes.mjs on every hold list:
  HOLD_PATH_FLOOR, HIGH_FLOOR, ci-status's hold_patterns and both review
  lanes' hold-paths, with the tests that pin them.
  test_merge_holds_sync.py now follows a module-relative file a floored
  module reads, so the next such data file fails there.
- address-feedback select and fix-ci-context output stop-resolved when the
  stop is gone on the head (no hold and no unresolved thread; a whole,
  settled check listing with none failing). report-stop-status then runs
  report-escalation.sh clear for that activity on that head. fix-ci reads
  the head's checks before its round cap. A select that fails to read
  writes no outputs and clears nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix-ci-context reads the head's checks before the round cap, so the
resolver test's fake lists one failing check.

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: 2 inline finding(s) on b34abd393e0e9bf21531769afbefca7c6731d5f8; the summary is in this lane's pull request comment.

claude-lane: claude-review

Comment thread .github/scripts/pr-unblock/fix-ci-context.mjs
}
checks += 1;
const runs = (await get(`/repos/${repo}/commits/${head}/check-runs?check_name=${encodeURIComponent(`pr-unblock / ${stop.activity}`)}&filter=all&per_page=100`)).check_runs ?? [];
if (runs.some((run) => attempted(run) && Date.parse(run.started_at) > Date.parse(stop.stoppedAt))) continue;

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.

Advisory: a stop the lane already cleared stays listed as a stalled lane stop.

A stop counts as stalled when no non-skip pr-unblock / <activity> check started after the lane-stopped comment's last edit. The stop-resolved path (a person resolves every thread, or CI is re-run green) ends the select job with Skipped: not-applicable, then posts success over the failing status. attempted() ignores that skip. The comment is not edited or removed, and the monitor cannot read statuses.

So after 2 hours, any head whose stop was cleared this way and is still open is listed in the rolling issue as stalled. The PR body says only that a no-op feedback retry skipping not-applicable is not an attempt. It does not address this case, and the added tests do not appear to cover it.

Options: have clear edit the lane-stopped comment (for example, mark it cleared), or have the signal treat a not-applicable skip that started after the stop as a resolution for address-feedback and fix-ci.

claude-lane: claude-review

@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-security-review: 2 inline finding(s) on b34abd393e0e9bf21531769afbefca7c6731d5f8; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

Comment on lines +250 to +269
// A reason escalation-routes.json now routes to a failing status rather
// than a person, on a label an older lane run set: fix-ci's once the head's
// checks read failed, so it stays red without the label (green, missing or
// unreadable keeps it); any other once the head is not one the lane stopped
// on, since the lane checks a new head itself.
...Object.fromEntries(
Object.keys(ROUTES)
.filter((reason) => escalationRoute(reason) === "fail" && reason !== "unresolved-conflict")
.map((reason) => [
reason,
async ({ pull, keys, failing }) =>
escalationActivity(reason) === "fix-ci"
? (await failing())
? "the lane now ends it in a failing status, not a person, and the head's own checks read failed"
: null
: keys.length > 0 && !keys.includes(pull.head.sha)
? "the lane now ends it in a failing status, not a person, and checks the new head itself"
: null,
]),
),

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.

severity: CRITICAL

Trust control: merge hold. clear-escalations now lifts needs-human on a head move alone.

For every reason escalation-routes.json routes to fail, this check returns "cleared" once keys.length > 0 && !keys.includes(pull.head.sha). The only condition is that the head differs from the resume key. Nothing is read about the new head. A fix-ci reason does at least read the head's checks. Every other fail reason has no such read.

The replacement hold is the failing pr-unblock/* status. It is posted on the stopped head only, and "a new head carries none" (report-escalation.sh). So after the label comes off, nothing holds the new head.

Exploit path: a label set by an older run for feedback-not-landed, feedback-invalid, feedback-partial, feedback-unadjudicated, dependency-round-cap, dependency-control-uncited, unexpected-path-staged or repeat-cap was a hold a person had to lift. Any push to the branch now makes the next sweep remove it. No lane has to run on the new head first, so no check proves the stop cause is gone. For unexpected-path-staged, the conflict resolution touched paths outside the conflicted ones. For the dependency reasons, the lane refused to mark a Dependabot update for merge. A one-line push drops that hold and leaves no status on the new head.

This is the same weakening the PR describes for older labels as an intended change. The code gives no condition that the lane has re-run and stopped being stopped on the new head. Reading the new head's own pr-unblock/<activity> status, or a later successful lane check run, before clearing would keep a lane that cannot reach the head from being treated as cleared.

The file is on the merge floor, so a person has to approve this change.

claude-lane: claude-security-review

Comment on lines +43 to +45
// The dispositions that leave a thread open, each one an adjudicator settles
// on an adjudicable thread instead of a person.
export const DISPUTED = Object.freeze(["INCORRECT", "UNCERTAIN", "VALID-defer"]);

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.

severity: IMPORTANT

Narrowed hold: UNCERTAIN and VALID-defer replies now go to a model adjudicator, not a person.

DISPUTED adds UNCERTAIN and VALID-defer to the dispositions that disputes stages and settleDisputes settles. publish-feedback.mjs also drops the old line that set feedback-uncertain for any such reply. Before, a finding the fixing lane called uncertain, or called valid but deferred, always ended in a held feedback-uncertain for a person. Now an adjudicator model that returns verdict lane produces the fixed reply and resolves the thread (settleDisputes). The repository ruleset's required_review_thread_resolution then no longer blocks the merge.

Exploit path: a non-CRITICAL, non-security-review code-review finding (for example an IMPORTANT correctness or access-control finding that the lane marked VALID-defer). One model verdict on text the PR author can influence closes it, and the thread no longer blocks merge. CRITICAL and security-review threads stay held through the adjudicable check, and that part is correct.

VALID-defer is the lane agreeing the finding is valid. Sending that to an adjudicator, whose lane verdict means "the finding is incorrect", is a different case from the existing INCORRECT dispute. Consider keeping VALID-defer held, or accepting only a reviewer or human verdict for it.

claude-lane: claude-security-review

@github-actions

Copy link
Copy Markdown
Contributor

claude-security-review holds this pull request for a person (critical-security) at b34abd393e0e9bf21531769afbefca7c6731d5f8.

Trust-control change that weakens a merge hold. In .github/scripts/pr-merge/clear-escalations.mjs (CHECKS), the sweep now removes the needs-human label from a PR when its head moves, for every reason escalation-routes.json routes to fail. The failing pr-unblock/* status that replaces the label is on the old head only, so the new head carries no hold. Related: adjudicate-feedback.mjs sends UNCERTAIN and VALID-defer replies to a model adjudicator instead of a person.

claude-lane: claude-security-review

kyle-sexton and others added 11 commits October 26, 2026 12:50
…ALID-defer on a lane verdict

clear-escalations lifted needs-human for a non-fix-ci fail-routed reason
once the head moved, though that activity's own escalation keeps its lane
off the PR (gate-source laneEscalationAllows) and the new head carries no
pr-unblock status in the label's place. Only fix-ci reasons, whose head's
own checks read failed, now clear; every other fail-routed reason stays
for a person.

settleDisputes resolved a VALID-defer thread on a `lane` verdict, which
says the finding does not hold where the fixing lane had agreed it does.
That thread now stays open with no reply and the round ends
feedback-unadjudicated.

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

# Conflicts:
#	.github/workflows/pr-run-activity-write.yml
… open

publish-feedback's dispute test still expected a lane verdict to resolve
a VALID-defer thread; it now expects the thread left open with only the
lane's reply and feedback-unadjudicated, and a stands verdict to hand it
to the next round.

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

The clear-escalations comments, README and changelog fragment said an
activity's own escalation keeps its lane off the pull request, which does
not hold for address-feedback (it still works a lane-escalated PR). State
the reason that holds for every activity: a lane posts a pr-unblock/*
status on a head only when it stops there or clears its own failure, so
a moved head carries nothing in the label's place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e cp lines

The origin/main merge brought write-runner-never-rules' check that the
apply-patch base copy names each sibling module in its own
`cp .base/... "$RUNNER_TEMP/<dir>/"` line; the combined cp of
escalation-routes.mjs and escalation-routes.json failed it.

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

fix-ci-context and the fix-ci dispatcher read a head's check runs with
filter=latest, which returns only a contract-only run's later skip of a
lane; failingChecks then saw no failure, and stop-resolved could clear a
failing pr-unblock/fix-ci status. Both listCommitCheckRuns adapters now
read filter=all, changed together so the dispatcher and the select agree;
failingChecks already prefers a real result over a skip. The read stays
at most three pages per head.

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

report-escalation.sh clear posts success over the lane's failing status,
which the monitoring lane cannot read, so a stop cleared by a same-head
retry or stop-resolved stayed a stalled lane stop. clear now adds a
`- Cleared:` line naming the head to the activity's lane-stopped comment,
through the same lookup and edit the stop uses (stopped_comment,
edit_comment); a failed edit only warns. collect.mjs ends that activity's
stop on a marked head, a cleared needs-human of it included, and spends
no check-run read on it. report-stop-status gains issues: write for the
edit. The changelog fragment also records the filter=all listing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ps listing it"

This reverts commit f8a1c3e: its comment edit needed issues: write on
report-stop-status, a scope not added for a monitor false alarm.

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

The fragment line for 17e542d went out with the revert of f8a1c3e,
which carried it.

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

claude-lane: claude-review

fi
cat >"$body" <<EOF
$marker
The pr-unblock lane stopped \`$activity\` on this pull request: $why. This needs no person: the run ends in a failing \`$context\` status on the head the lane gated.

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.

The comment says "This needs no person" for every fail-routed reason, but the PR says four of them end with nothing left for the lane to do. feedback-partial, feedback-unadjudicated, feedback-stood-open and feedback-verifier-open cannot change on a same-head retry. A person has to resolve the threads, or the stalled-stop signal reports it after two hours. feedback-untaken-threads has the same problem when the open thread is a human reviewer's. A reader who trusts this line will wait on a lane that will not act again, while ci-status stays red.

To reproduce, run ESCALATION_REASON=feedback-unadjudicated bash report-escalation.sh. The comment still opens with "This needs no person", though the retry text further down is the same for every reason.

Suggestion: make the opening claim depend on the route entry, for example a per-reason retry flag in escalation-routes.json. Reasons that retry would keep this sentence. The others would say that a person resolves the threads or pushes a new head.

claude-lane: claude-review

@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-security-review: 1 inline finding(s) on ea2b0eea312aeae046b2e83b69841247fd71bbfe; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

Comment on lines +275 to +276
".github/scripts/pr-unblock/escalation-routes.mjs",
".github/scripts/pr-unblock/escalation-routes.json",

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.

severity: IMPORTANT

The floor covers the routing table but not the code that acts on it. The PR says that putting escalation-routes.json on the floor means moving a reason from hold to fail needs a person. That holds only for edits to the table. The code that turns a route into a hold or a thread resolution is not on the floor (HOLD_PATH_FLOOR, babysit_risk.HIGH_FLOOR, ci-status hold_patterns and the review lanes' hold-paths):

  • .github/scripts/pr-unblock/report-escalation.sh branches on route == hold to add the needs-human label, and on status/fail to skip it.
  • .github/scripts/pr-unblock/adjudicate-feedback.mjs settleDisputes, ORDER and adjudicable decide which threads an adjudicator may resolve instead of a person.
  • .github/scripts/apply-patch/apply-patch.mjs heldEscalation and HELD_ESCALATIONS decide which fix-ci escalations hold.
  • .github/scripts/pr-unblock/publish-feedback.mjs and limit-stop.mjs decide the stop skip and retry release.
  • pr-unblock.yml and pr-run-activity-write.yml wire the reason to those scripts.

Exploit path: a lanes-App-authored pull request, which takes the exempt-author branch in ci-status, edits report-escalation.sh to ignore hold and post a status instead. It could also edit adjudicate-feedback.mjs so a security-review thread counts as adjudicable. None of those paths matches a floor glob, so no person-hold applies. Every held reason, including the safety ones, would then stop labelling, and a thread no person should resolve could be resolved by a model verdict. test_merge_holds_sync.py now follows a module-relative file a floored module reads, but these scripts are not floored modules, so nothing flags them.

Suggested fix: floor the hold/fail decision points (at least report-escalation.sh, adjudicate-feedback.mjs and apply-patch.mjs) in the same four places, or state that the review lanes alone cover them.

claude-lane: claude-security-review

This branch has not been deployed

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

Labels

needs-human Human-in-the-loop required; autonomous sessions must not resolve items carrying this.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant