Skip to content

feat(pr-pipeline): clear a review-lane hold recorded on an older head once the new head reviews clean - #10959

Merged
kyle-sexton merged 16 commits into
mainfrom
feat/clear-stale-head-holds
Oct 30, 2026
Merged

kyle-sexton merged 16 commits into
mainfrom
feat/clear-stale-head-holds

Conversation

@kyle-sexton

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

Copy link
Copy Markdown
Member

No related issue: owner standing direction 2026-10-29, needs-human policy brief Part A2

Summary

A review lane's hold (claude-lane-hold sha=...) names the head it judged, but until now only a person could lift it. So a hold recorded against an older head stayed on the pull request after the fix was pushed and reviewed clean. #10743 shows this: a CRITICAL hold at 272c9a2 stayed while 23ee4b1 was fixed and reviewed.

pr-merge's scheduled clear-escalations now removes a human-gated label that a review lane added, recording the reason stale-review-hold. It does so only when all of these hold:

  • Every hold comment since the label was last removed names an earlier head, and none names the current head.
  • A trusted move took the branch off the newest held head: every push since the one that landed that head was made by a human on the base trusted-actor list. A push by the lanes App never counts.
  • Both review lanes passed on the current head with a whole review each. That means the status job succeeded, the job posted a review-complete notice and no hold notice for the head, and the head changes neither review caller.
  • No lane leaves a HIGH or CRITICAL finding. The security lane reports critical-findings: 0, and every finding is inline for the security lane and for any lane a hold names. No unresolved lane thread is at CRITICAL or IMPORTANT (security-review IMPORTANT counts as HIGH). For those strict lanes, an open thread with no readable severity also keeps the hold.
  • The head and the hold comments still read the same when re-read just before the delete.

A lane that still finds the cause holds the new head again, with its own hold comment and label.

The lanes App's own comment is never taken as proof. ci-status's review-lane lift step accepts the lanes App's removal only when all of these hold:

  • The App made the removal directly, after its own stale-review-hold comment written since the hold.
  • That comment names a head that no hold comment names, and no hold comment names the event's head.
  • recheckStaleHold, re-run from the base commit's copy of clear-escalations.mjs with the job's own token, passes at the head the App's newest marker names (CLEARED_SHA), judged as it was when the App removed the label. It is the same predicate the App ran. A later head is judged by its own review lanes, as after a person's lift.
  • Hold comments count from the label's last removal before it went on, as holdsSince counts them.

Any other App or bot removal still fails.

This touches the gate paths .github/scripts/pr-merge/clear-escalations.mjs and .github/workflows/pr-require-checks.yml (plus a comment-only edit to pr-merge.yml), so a person arms it.

Fix

  • clear-escalations.mjs:
    • New staleHoldEvidence predicate (lane reviews, open-thread severities, trusted move), and recheckStaleHold for ci-status.
    • A stale-review-hold path in verdict for labels a review lane added.
    • A re-read of the head and the hold comments just before the delete.
  • pr-require-checks.yml:
    • The lift step accepts the App's stale-review-hold removal only under the rule above. It passes HEAD_SHA to the step and the marker's head to the recheck as CLEARED_SHA.
    • The base-config sparse checkout gains clear-escalations.mjs's import closure.
  • docs/conventions/pr-pipeline/README.md: a new row in the clear table. Changelog fragment in .changes/pr-pipeline/.
  • The predicate stays inside clear-escalations.mjs, not a new module. A new module would need a hold-floor row, and sync-hold-floor.py would then rewrite merge-holds.mjs.
  • ci-status checks that no hold names the head, rather than requiring the marker's head to equal HEAD_SHA, and rechecks at the marker's head. Under strict equality, every clean push after a valid clear would turn ci-status red until a person acts.
  • A clear needs a commit since each held head that is not a merge, and a critical-security hold needs its lane's CRITICAL inline finding at the held head in a file changed since; a summary-only finding stays for a person. feat(pr-pipeline): bind workflow-edit reviews to a run from main #10743's CRITICAL finding was inline at its held head 272c9a2, so it meets this rule.

Verification

  • node --test in .github/scripts/pr-merge: 665 pass, 0 fail, including 12 stale-hold and 8 recheck tests.
  • scripts/ci-hold-lifted-by.test.sh: PASS=63 FAIL=0.
  • test_merge_holds_sync (unittest): 20 tests OK. scripts/sync-hold-floor.py --check: clean after merging main (feat(pr-pipeline): bind workflow-edit reviews to a run from main #10743).
  • Shell suites that reference the changed workflows or clear-escalations: 31 of 33 green. The other 2 (ci-test-node-stop-commands, run-hygiene-gate) fail locally only because PyYAML is missing.
  • Each test that guards the stale-hold logic, the review-fix races and the recheck was confirmed to fail on the code before its fix. The guards were also mutation-checked.
  • actionlint on pr-require-checks.yml and pr-merge.yml: clean. markdownlint-cli2 on the README and the fragment: clean. ShellCheck on the harness: clean.
  • The recheck was imported from a sparse copy containing exactly the base-config checkout's files. It loads without running the sweep's main.

Related

  • poteto's independent security review of 990c194 found no HIGH or CRITICAL issues. Its two MEDIUM findings are fixed in e18df30: an incremental code-lane review could clear a hold, and there was a race between judgment and delete. The LOW finding is covered by the recheck reading both lanes at the cleared head.
  • Follow-up poteto is taking: enable-auto-merge.mjs:687-695 (liftBlocker) counts only person removals as lifts. After a stale-review-hold clear it still reports "a review lane holds it" when lifting pr-merge's own refusal label later.
  • feat(pr-pipeline): bind workflow-edit reviews to a run from main #10743 (merged): binds reviews to main. The pr-merge half of Part A2 (babysit-merge-risk on a moved head) is already cleared by the existing head-moved path.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VQb9Q3ucKDVyHvpcU5cfU8

kyle-sexton and others added 3 commits October 29, 2026 09:43
…s both lanes at a new head

A review lane's hold names the head it judged (claude-lane-hold sha=...),
but only a person could lift it, so a hold recorded against an older head
stayed after the fix was pushed and reviewed clean (#10743: a CRITICAL
hold at 272c9a2 while 23ee4b1 was fixed and reviewed).

clear-escalations.mjs now judges a label a review lane added as
stale-review-hold: every hold comment since the label was last removed
names an earlier head, a trusted move (trustedMove, anchored at the push
that landed the newest held head; never a lanes App push) took the branch
off it, both review lanes passed on the current head with a
review-complete notice and no hold notice, the head changes neither
review caller, and the security lane leaves no CRITICAL or IMPORTANT
finding (whole review, every finding inline, critical-findings: 0, no
unresolved security-lane thread at CRITICAL, IMPORTANT or unreadable
severity). A lane that still finds the cause holds the new head again.

ci-status's review-lane lift check accepts the lanes App's own removal
only after its stale-review-hold comment since the hold names a head no
hold comment names; any other App or bot removal still fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQb9Q3ucKDVyHvpcU5cfU8
… and close its races

Fixes from the security review of 990c194, and the direction that
ci-status verify the lanes App's stale-review-hold judgment, not trust it.

- Every review lane must review the head whole, so a finding at the held
  head is looked at again; a code-lane open thread at CRITICAL or
  IMPORTANT keeps the hold, and for a lane a hold names (as for the
  security lane) every finding must be inline and an unrated open thread
  counts as CRITICAL.
- clear-escalations reads the head and the hold comments again just
  before the delete, so a hold a lane wrote under the label already on, or
  a moved head, keeps it.
- ci-status counts the App's removal only when no hold comment names the
  event's head (the head a missed hold names), the marker names a head no
  hold names, and the base commit's clear-escalations.mjs recheckStaleHold,
  the same predicate, passes again at the event's head with the job's own
  token. The base checkout gains that module's import closure.

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

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

Copy link
Copy Markdown

Codex Review Summary

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-29T14:25:37.243429Z bee893e 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 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 bee893e2c677805e53e73c9794833afd0eee4adf; the summary is in this lane's pull request comment.

claude-lane: claude-review

Comment thread .github/workflows/pr-require-checks.yml
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

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

I found nothing to block or flag in PR #10959. I only read the code. I did not run the Node or shell tests, so the test results in the PR description are the author's claims and I have not confirmed them.

What I checked

  • Imports and helpers in clear-escalations.mjs. Everything the new code uses resolves in this tree:
    • REVIEW_LANES is now imported from merge-holds.mjs. It is a list of [file, job] pairs, which matches how laneReview destructures it.
    • laneChange, laneHold, lines, HOLD_LINE, ROUND_MARKER, openSignedCommitsHold, cleared, spokeReason, SHA, paginate, createGitHub and loadTrustedList all exist.
    • The old local REVIEW_LANES is renamed to LANE_RECORDS, and the old SEVERITY constant is replaced by the new one. I found no leftover duplicate definitions.
  • Import side effects. Importing the module from ci-status does not run the sweep. The main call is behind the import.meta.url guard at the bottom of the file, and the new recheckStaleHold is a separate export.
  • pr-require-checks.yml lift step. Every variable the new block uses is defined earlier in that step: raw, events, held_at, lifted_at, actor_id, actor_login, via_app, actors and BASE_CONFIG. LANES_APP_ID comes from the job-level env. HEAD_SHA is newly added to the step env, and the shell test asserts it.
  • Fail-closed behaviour in the new block.
    • A missing or non-SHA HEAD_SHA skips the stale path.
    • A missing base clear-escalations.mjs fails.
    • A failed jq read leaves stale empty, which fails.
    • A removal through another App fails.
    • An App id the base trusted list does not name fails.
    • The recheck runs the base-commit copy of the module, not the pull request's.
    • The recheck judges the head the App's marker names (CLEARED_SHA) and re-reads the lanes with the job's own token.
  • Race handling. The head and hold comments are re-read before the comment and again before the delete. A new hold or a moved head keeps the label.

Observations that are not findings

  • If a hold lands in the short window between the comment POST and the second re-read, the stale-review-hold comment is already posted but the label stays. A later sweep judges the label afresh and finds the new hold, so this is harmless.
  • A cost or trust-gate hold can clear this way, because only critical-security holds need an inline finding. That matches the PR's stated design: a lane that still finds the cause holds the new head again.
  • The sparse-checkout list for the base config must cover the full import closure of clear-escalations.mjs. A missing file fails closed, since the import throws and the lift is refused. I did not trace every transitive import.

Spend: this change adds no paid or billed resources.

@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 bee893e2c677805e53e73c9794833afd0eee4adf; 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

@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: bee893e2c6

ℹ️ 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/workflows/pr-require-checks.yml
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

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

Security review of #10959 at e5ff94d. The PR narrows a merge hold. A review lane's claude-lane-hold can now be lifted by pr-merge's clear-escalations (stale-review-hold), and ci-status accepts the lanes App's removal after re-running the base commit's recheckStaleHold. I read the whole diff and the touched trust controls: clear-escalations.mjs, pr-require-checks.yml (hold-lift step and base-config sparse checkout) and the pr-merge.yml comment edit.

critical-findings: 0

What I checked and found sound

  • Base code only. The ci-status recheck imports the base commit's copy of the module from a sparse checkout at BASE_SHA. A pull request cannot rewrite its own predicate. An older base without recheckStaleHold throws, and the step fails closed.
  • Shell inputs. $stale comes from a 40-hex capture and HEAD_SHA is validated against ^[0-9a-f]{40}$, so the new shell block has no injection surface. The App's removal is still gated on actor_id == LANES_APP_ID, an empty via_app, and the App's id appearing on the base trusted-actor list.
  • Trusted move. trustedMove requires the branch head to be in this repository, the newest push to land the head, and every push since the held head to be by a trusted human. A lanes App push never counts, and an activity page that does not reach back keeps the hold.
  • Review proof. Both lanes need a successful status job on the head, exactly one whole-scope review-complete notice, no hold notice, and no change to a review caller (laneChange). An unreadable listing keeps the hold.
  • Re-read. The head and hold comments are re-read before the comment and again before the DELETE. A person speaking, a foreign lanes App marker, or a check-signed-commits hold keeps the label.
  • Hold authorship. Hold comments are only read from claude[bot] or from github-actions[bot] with a lane footer (laneHold), so an untrusted commenter cannot forge or suppress one.
  • Manifests. No plugin.json or marketplace.json change, so no manifest lines apply.
  • Spend. The change adds scheduled-sweep API reads (compare, activity, GraphQL), which use no paid billing.

One finding (IMPORTANT): the "no open HIGH or CRITICAL finding" gate treats a resolved thread as a fixed finding. See the inline comment on clear-escalations.mjs:894.

Protected-class list. No instruction file, rules file, skill body or hook allowlist is deleted or softened here, so the instruction exception register was not needed.

@melodic-automation-lanes

melodic-automation-lanes Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

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

HIGH high-path:.github/scripts/pr-merge/clear-escalations.mjs: .github/scripts/pr-merge/clear-escalations.mjs
HIGH high-path:.github/workflows/pr-merge.yml: .github/workflows/pr-merge.yml
HIGH high-path:.github/workflows/pr-require-checks.yml: .github/workflows/pr-require-checks.yml
MED evidence-unbound:claude-review: not bound: evidence-missing
MED evidence-unbound:claude-security-review: not bound: evidence-missing

kyle-sexton and others added 3 commits October 29, 2026 12:44
…e it to a fix

- ci-status's recheck judges the head the App's marker names, as it was
  when the App removed the label, so a later clean push stays green before
  its lanes run and a lanes App push after the clear does not strand it.
- The hold-lift step counts hold comments only from the label's last
  removal before it went on, as holdsSince does, so a hold a person already
  lifted cannot block a later clear.
- A clear needs a commit since each held head that is not a merge, and a
  critical-security hold needs its lane's CRITICAL inline finding at the
  held head in a file changed since; a summary-only finding stays for a
  person.

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

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

claude-lane: claude-review

Comment thread .github/scripts/pr-merge/clear-escalations.mjs Outdated
Comment thread .github/scripts/pr-merge/clear-escalations.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 3543050d5073176f6f86ad7a1ec15fbc4c1136ed; the summary is in this lane's pull request comment.

claude-lane: claude-review

Comment thread .github/scripts/pr-merge/clear-escalations.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-security-review: 2 inline finding(s) on 3543050d5073176f6f86ad7a1ec15fbc4c1136ed; 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
Comment thread .github/scripts/pr-merge/clear-escalations.mjs Outdated
…the App's whole predicate, comment only on a real clear

- A stale-hold clear counts only commits since the held head that are the
  pull request's own (not reachable from main), so an Update branch that
  brings in main's commits lifts nothing; a critical-security hold needs the
  held finding's file changed by one of those commits.
- ci-status's recheck runs the same signed-commits, lanes App marker and
  person checks clear-escalations runs, as of the removal, through one
  shared function.
- The head and holds are re-read before the clear comment too, so a kept
  label gets no comment saying it came off.

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

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

claude-lane: claude-review

Comment thread .github/scripts/pr-merge/clear-escalations.mjs Outdated
@melodic-automation-lanes

Copy link
Copy Markdown
Contributor

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

HIGH high-path:.github/scripts/pr-merge/clear-escalations.mjs: .github/scripts/pr-merge/clear-escalations.mjs
HIGH high-path:.github/workflows/pr-merge.yml: .github/workflows/pr-merge.yml
HIGH high-path:.github/workflows/pr-require-checks.yml: .github/workflows/pr-require-checks.yml
MED evidence-unbound:claude-review: not bound: evidence-missing
MED evidence-unbound:claude-security-review: not bound: evidence-missing

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

@melodic-automation-lanes melodic-automation-lanes Bot added the needs-human Human-in-the-loop required; autonomous sessions must not resolve items carrying this. label Oct 29, 2026

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

claude-lane: claude-security-review

Comment thread .github/scripts/pr-merge/clear-escalations.mjs
…ged the flagged lines

A trusted edit anywhere in the held file (a comment, whitespace) no longer
counts as a fix: the diff from the held head must remove, or insert beside, a
line the CRITICAL inline finding names there. A finding with no line, or a
file whose patch does not read, keeps the hold.

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

Copy link
Copy Markdown
Contributor

The pr-merge lane escalated this pull request, and its head has moved (head-moved): its escalation names head 36d990854151eee40b3a791bfbe88b9d6216f9f0, and the head is now 35050f84f2302ce4c7e34c88389c357611d3d491. pr-merge removed needs-human so it judges 35050f84f2302ce4c7e34c88389c357611d3d491 afresh, and escalates again if its refusal still holds. To keep a person on it, add needs-human back: a label a person adds is never removed.

@melodic-automation-lanes melodic-automation-lanes Bot removed the needs-human Human-in-the-loop required; autonomous sessions must not resolve items carrying this. label Oct 29, 2026

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

claude-lane: claude-review

Comment thread .github/scripts/pr-merge/clear-escalations.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 7e11c7a13ce725773e5a9739a9f484b7ad372935; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

Comment thread .github/scripts/pr-merge/clear-escalations.mjs
@kyle-sexton

Copy link
Copy Markdown
Member Author

Removing needs-human. The pr-merge lane added it for its unattended-merge risk floor, whose only HIGH reasons are path holds on clear-escalations.mjs, pr-merge.yml and pr-require-checks.yml. The owner's standing direction (Q15, Q16 and §10, 2026-10-29) takes path holds off; none of the three stop reasons (intent change, a design change, a token, account or billing risk) applies. The review findings are fixed: the last security finding, that a trivial edit to the held file could lift a critical-security hold, is fixed in 87cce48 (the push must change the flagged lines), and the earlier ones in 7396af1 and 1c41137.

@melodic-automation-lanes

melodic-automation-lanes Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

The pr-unblock lane stopped resolve-conflicts on this pull request: the merge stages the pull request's own .github/workflows/ changes together with the base's, which the lane token cannot push, so a session merges main by hand. The lane sets no label: the run ends in a failing pr-unblock/conflict status on the head the lane gated.

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

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

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

@kyle-sexton
kyle-sexton enabled auto-merge October 30, 2026 00:26
@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 30, 2026

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

claude-lane: claude-security-review

if (counts.length !== 1 || counts[0] !== "0") {
return `the security lane's summary of ${head} does not read critical-findings: 0`;
}
const open = threads.filter((thread) => !thread.resolved);

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 "no HIGH or CRITICAL finding" gate counts only unresolved lane threads, so a resolved thread counts as a fixed finding. In staleHoldEvidence the held CRITICAL thread at the old head is expected to be resolved (the tests' heldFinding() is isResolved: true). isResolved says nothing about who resolved the thread or why. GitHub lets the pull request author or anyone with write access resolve a review thread, and that person need not be on the trusted-actor list. Resolving adds no comment or review, so the personSince / standingStop "a person spoke" check does not see it.

Path: a lane holds critical-security at H1 with an inline CRITICAL thread. A trusted human pushes a non-merge commit that touches the flagged lines. patchTouches also counts an insertion adjacent to the flagged line. The branch's author then resolves the CRITICAL thread. The sweep clears the hold as stale-review-hold, and ci-status accepts the App's removal after the recheck. The recheck reads thread state at check time and not at the removal, so it passes too.

What stops it today: a trusted human push, and a whole review from the security lane at the new head that must itself read critical-findings: 0 with no new hold. That makes the lane model's re-review the only check on whether the code is actually fixed. It reads the same PR content the attacker controls, and the thread state is the one input a non-trusted account can set.

Suggested fix: count a thread as dealt with only if a trusted human resolved it (GraphQL resolvedBy), or ignore isResolved and require that no lane thread at the held head still anchors to unchanged lines. heldCauseUnchanged already does the line check for critical-security holds only. Extend it to every strict lane's CRITICAL or IMPORTANT thread.

severity: IMPORTANT

claude-lane: claude-security-review

Merged via the queue into main with commit f0f07d5 Oct 30, 2026
50 checks passed
@kyle-sexton
kyle-sexton deleted the feat/clear-stale-head-holds branch October 30, 2026 00:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant