Repository navigation
feat(pr-pipeline): resolve answered non-CRITICAL bot threads in a base-only job - #12893
Conversation
…e-only job On a pull request a trusted person opened, publish-feedback's publish-artifact token is no writer, so it answers review threads and leaves them open for the owner. A new pr-unblock activity, resolve-feedback-threads, runs after it in its own write-runner job: a script with no model, granted mutate-branch (contents: write, probed on #12634), with checkout: base so the job holding the write checks out only the trusted base, credentials not persisted, never the PR head. resolve-threads.mjs reads the threads from GitHub itself and resolves one whose comments are all a review bot's or the lanes App's, whose finding no reviewer comment rates CRITICAL and adjudicable() allows, and that is answered (the lane's VALID reply or a finding-answered adjudication is last) or outdated with no lane reply that left it open. It reuses the lanes' own predicates: isReviewerComment and isVerifierRoot (pulled out of threadAuthors), isCritical (adjudicate-feedback's CRITICAL rule) and isResolvingAnswer (publish-feedback's resolving replies). The leaf key checkout (head|base) is new in the schema; resolve-config outputs it for every script, and the write runner skips its head checkout and SHA assert on base. publish-feedback's job and grant are unchanged. The script joins the hold floor as an enforcement path. Refs: #12634 Co-Authored-By: Claude Opus 5.5 <[email protected]>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 60aa4b6f86
ℹ️ 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".
|
claude-security-review reviewed Security review: PR #12893, head
|
|
claude-review reviewed Code review: PR #12893,
|
…ly-script stops - resolve-threads.mjs judges each thread again on the re-read's bodies and edits, so a comment edited between the reads (rated CRITICAL, or edited by a person) leaves the thread open. readThread now returns isOutdated and each comment's body, lastEditedAt and editor. - resolve-feedback-threads also runs after publish-feedback's own stops (lane-stopped, awaiting-human, finding-stands, no-model-turn), which may still have answered other threads; never on a kill-switch or gate skip. - refire-refused.mjs lists resolve-feedback-threads among the write-runner callers whose annotations it reads. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MWmASNtP63MZxdDeoxYztD
The README and changelog fragment say the resolve job judges each thread again on its re-read comments and runs after the reply script's own stops, never after a kill-switch or gate skip. The kill-switch rule's comment now says what it checks, and the rule asserts no dependent admits kill-switch. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MWmASNtP63MZxdDeoxYztD
Refs: #12634
Summary
On a pull request a trusted person opened,
publish-feedbackanswers review-bot threads but cannot resolve them, because itspublish-artifactlane token is no writer there. The owner then resolves them by hand. This addsresolve-feedback-threads, a pr-unblock activity that runs in its own job afterpublish-feedbackand resolves the answered or outdated non-CRITICAL bot threads. CRITICAL findings, security findings a person decides, and every thread a person wrote or joined stay open. This is option (B) from the decision record, as amended, on the token the probe proved.Fix
Job (
.github/workflows/pr-unblock.yml):resolve-feedback-threadscalls the sharedpr-run-activity-write.yml. It runs only whenpublish-feedbacksucceeded with no skip reason, so a kill-switch stop still stops it. Its job permissions, concurrency group and trigger-gate inputs mirrorpublish-feedback's. It takes only the trigger gate's two heads from other jobs, never a thread id or a read-runner output.Grant:
mutate-branch(contents: write,pull-requests: read,issues: read), the grant the probe showed resolves a thread. It is added inlane-grants.json,activity-rules.jsonandpr-pipeline.yaml. Nopull-requests: writeis requested.publish-feedbackkeepspublish-artifactand its job permissions byte for byte.Base-only checkout, set in config with no new reusable input: a new optional leaf key,
checkout: head|base(schema,resolve.mjs, and acheckoutoutput from resolve-config for every script). Onbase, the write runner skips "Check out the PR head" and its SHA assert, and gives the scriptTRUSTED_ACTORSthe way it does for areview-repliesscript. The job then holds only the default-branch checkout at.base, withpersist-credentials: false. The broker reads the schema from the caller's main tip, so the key and its first use land together. No.github/standards/or runner-policy change was needed: local reusable calls are checked against the workflow's own inputs.Filter (
.github/scripts/pr-unblock/resolve-threads.mjs, a script with no model): the script reads threads, review comments, reviews and issue comments from GitHub. It acts only on a PR whose author is aUseron the base trusted-actors list (anything else skipsnot-applicable). It resolves a thread when all of these hold:isCritical, adjudicate-feedback's rule) and thatadjudicable()allows, so an unrated security-review finding, or a listed bot's finding on a held or unreviewed head, stays open;finding-answeredadjudication:isResolvingAnswer, defined next torenderReply) or outdated, with no lane reply that left it open (INCORRECT, UNCERTAIN or VALID-defer, an escalation, orfinding-stands).Before each resolve the script re-reads the thread and resolves it only if it is unchanged. pr-verify notes stay with the pass-only path. The reviewer and verifier predicates were pulled out of
threadAuthors, so one definition serves both paths.Hold floor:
resolve-threads.mjsis added as an enforcement path. The copies are regenerated withscripts/sync-hold-floor.py, and the count and digest pins are updated intest_floor_paths.pyandtest_babysit_risk.py.Docs: the pr-pipeline README (Owner pull requests, Resolve),
pr-run-activity.md(script activities), and changelog fragments for pr-pipeline (minor) and source-control (none, tests only).For the person merging: the contract and this job use "outdated or answered". babysit's worker tier is stricter: it resolves only a thread that is outdated and answered by a lane login. A bulk autonomous resolve there is refused because a worker's own push marks threads outdated (guard-contract.md,
resolve.autonomous-bulk-refused). Here, outdated alone resolves only a bot-only, non-CRITICAL, adjudicable thread that no lane reply left open.Verification
resolve-threads.test.mjscovers answered (VALID, finding-answered), outdated, and an owner PR resolving only those. The live demo happens after merge: a new activity's grant mints only once it is on main.trusted-base: default-tipreads the config at main's tip, so any owner PR's next address-feedback round shows it.resolve-threads.test.mjs: CRITICAL root (answered or outdated), CRITICAL follow-up, unrated security-review, a listed bot on a held or unreviewed head, a human root, a human reply, a bot comment edited by a personresolve-threads.test.mjs: a stranger's PR, the lanes App's PR, and a Bot-typed author all skipnot-applicablewith no resolve callcallers.test.mjs"resolve-feedback-threads is its own base-only script job…"checkout: baseand still run onheador for a skill),write-runner-never-rules.test.sh,live-config.test.mjs,resolve-config.test.mjs(output, and the schema refusing the key on a skill or with another value)persist-credentials: falsescriptwith noskill; the write runner's skill step is gated onkind == 'skill'(callers test)lane-grants.json,live-config.test.mjs); the job token's permissions equal publish-feedback's. apply-feedback-patch already heldmutate-branchbefore this change.live-config.test.mjspins itspublish-artifactgrantcontents: write,pull-requests: read)Local runs:
node --test: resolve-config's live-config, lane-grants, resolve-config, resolve and broker-vendoring suites (290 pass);callers.test.mjs(235 pass);resolve-threads.test.mjs(9 pass); publish-feedback, adjudicate-feedback, address-feedback, feedback-dispatch and workflow-gates (653 pass); log-api-budget (67 pass).scripts/write-runner-never-rules.test.sh(76 pass) andscripts/act-turns-denied.test.sh(63 pass).sync-hold-floor.py --check(clean).scripts/check-affected.sh --base origin/main --no-fetchafter merging main.Related
🤖 Generated with Claude Code