Repository navigation
feat(pr-pipeline): bind workflow-edit reviews to a run from main - #10743
Conversation
… gains for a person Every workflow whose token can write, that names a secret other than GITHUB_TOKEN, or that mints a lane token, and the review workflows themselves, is now a `gate` floor path held for a person. A workflow change that gains a write scope (or drops a permissions block onto the repository default) holds as `permission-widening-review`, and one that adds a pull_request_target or workflow_run trigger as `privileged-trigger-review`; both are person codes no cost decision clears. A secrets reference is also read from the head text, so a workflow with no patch is not skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where PyYAML is not installed the workflow privilege holds read the text alone: a write grant the head adds, a permissions key it drops, or an added file with jobs and no permissions key. Tests run both readings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…binding # Conflicts: # .github/workflows/pr-require-checks.yml # plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_risk.py # plugins/source-control/skills/babysit-prs/scripts/tests/test_merge_holds_sync.py
… from main A pull request that changes a lane source (a workflow, Claude Code config, the review criteria) now binds each review lane on the verdict artifact a run of main's pr-review-main.yml uploaded for its exact head: the run's path, workflow_run event, default branch, repository and completed success, its head held by main, and the verdict's lane, pull request and tree. Comments, markers and pull_request runs never count for it. The most findings any main-run review of the head's tree reported count, so a re-run never clears one. With no such verdict it stays unbound as before; an enforcement path beside it now waits for that review instead of a person (`review-path-unbindable` is gone). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pull request pr-review-main.yml runs on pr-require-checks' completion through workflow_run, so its definition is always main's. For a pull request that changes a lane source it reviews the head's diff as data, with each lane's criteria copied from main, on a read-only token and a Read-only model leg, and uploads each lane's verdict as pr-review-main-<lane>-<pr>-<tree>. It posts nothing; the merge gate binds the lanes on that artifact through the run's record. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pr-pipeline README and the babysit safety reference describe pr-review-main.yml, how the gate binds its verdict, the workflows now on the gate floor, and the permission-widening and privileged-trigger holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… verdicts 90 days The verdict writer and prompt under .github/scripts/pr-review-main set the counts the gate trusts, so they join the gate floor: a change to them is held for a person, not cleared by the review they produce. The verdict artifacts keep the repository maximum, so an expired verdict with findings cannot lower the count a later clean review of the same tree leaves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both lanes run as one workflow run, so a lane whose job fails concludes the run failure after the other lane uploaded its verdict. Requiring success before counting dropped that verdict's findings, and a clean re-run of the same tree then bound at zero. As pr-verify does for a FAIL, a verdict of main-run origin now counts toward the findings whatever the run's conclusion; only a completed, successful run's verdict naming the head binds the lane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in the ci-status suites ci-lanes-pr-hold.test.sh now expects ci-status to hold the two workflows and the main-run review's files as floor paths, and prep-warnings uses an enforcement script that stays the lane's as its quiet case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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. |
|
claude-security-review reviewed Incremental security review of 16b2799..6746152 for #10743. I found no security issues. critical-findings: 0 What changed, and the control each part touches
Manifests: no Hold: none. Nothing here widens a trust control, a token scope, the kill switch or a merge hold. The change only adds hold paths and narrows one hold in a way that fails closed. Spend: none changed in this increment. |
|
claude-review reviewed Incremental review of the changes since 16b2799: no findings. What the change does
What I checked
Spend: none. |
…nt-config gaps - pr-review-main diffs with --text and writes no verdict for a patch that still holds a binary file or is over 300000 bytes. A file the files API sends no patch for now holds for a person as unreviewed-patch:<path> unless it is an image, font or PDF. - Claude Code settings, .mcp.json, CLAUDE.md, CLAUDE.local.md, AGENTS.md, REVIEW.md and docs/standards/** are gate floor paths, and the prompt rates widening them CRITICAL. - A record job with no model posts each verdict's counts as a pr-review-main: <lane> check run on the reviewed head; the gate holds on the most CRITICAL findings any head of the pull request recorded, force-pushed heads included, so a whitespace push cannot re-roll a CRITICAL away. Check runs cannot be deleted. - privileged-trigger-review also covers issue, comment, review, discussion and repository_dispatch triggers. - Each leg leaves a report-lane-outcome record, and the lane is in .github/lane-funding.json. - The criteria keep the review plugin's layout so the skill's relative references resolve inside the data directory, and the prompt says not to read files outside it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pos flag Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The pr-verify verifier failed head This pull request leaves the merge queue, and ci-status holds it out while it carries |
- Read a non-job secret as the union of the reference forms and any
`secrets` context in a `${{ }}` span that may hold a lone `}`, so
`format('{0}', secrets.X)`, `'}' || secrets.X`, the multi-line form and
`if: toJSON(secrets)` gate again. The gated set stays 17 of 34.
- Hold `**/.claude/**` and `**/.claude-lane/**` whole in place of the
per-file globs (owner decision Q11). sync-hold-floor.py refuses a glob
whose `*` bash would let cross `/`, and a test runs the same paths
through the Python, mjs and bash matchers.
- Count a `pr-review-main: <lane>` record only when the github-actions app
posted it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…binding # Conflicts: # plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_risk_cost.py
…line holds ci-status (pr-require-checks.yml) posts the required status and runs the hold step, so it is now a `gate` floor path named by file in gated_workflow, not an `enforcement` path a model verdict clears. A workflow whose token has `actions: write` (block, flow, quoted or write-all) can dispatch the gated credentialed workflows, so it is gated too. sync-hold-floor.py regenerates the copies: 23 gated workflows. long-line-review now fires only when needs_main_review, the predicate bind() uses, says the main-run review binds the pull request. RiskInput carries the base SHA, and the head text with_contents read stands in for bind()'s head_file, so a read-only workflow's long line no longer holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Post-merge mutation testingKilled 1, survived 1, timeout 0, no coverage 0, invalid 0, not run 5.
Surviving mutants
The run's summary has the full report. |
…reference The base merge at e924f7d resolved babysit_risk.py's conflict without #10743's `${{ }}` alternative of _SECRET_REF, so `${{ secrets }}`, `contains(secrets, 'A')` and `secrets || 'x'` inside an expression no longer matched it. Restore the alternative and pin those shapes in a test that fails without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on the floor Remove the merge.lane-arm-gate switch, the merge.person-held list, the lane-arm scoring, the clear-escalations same-head clear and their tests: the lane-armed set was empty, so all of it was dead code. Drop the new floor rows the revision added, so the pull request adds no person hold. test_merge_holds_sync.py now checks that what every trust row and the merge-enforcement gate scripts import or run sits on a trust or gate row. The paths it reaches today that do not are pinned in OFF_FLOOR_ALLOWED for the owner's decision. Refs: #10743 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r-merge attestation Owner decision Q5 option 1: babysit_risk.py and the pr-merge scripts merge with no person once both review lanes are bound to a clean main-run review of the head, no review thread is open, the hold floor does not shrink against the base, and pr-merge has attested the head. - sync-hold-floor.py generates LANE_ARMED (the merge gate's own code, trust rows included) and PERSON_HELD (every other trust and gate row); it refuses a gate row outside the merge gate's code left unlisted. - pr-merge's lane-armed job posts pr-merge/lane-armed at the head, uploads the run-scoped artifact pr-merge-lane-armed-<pr>-<head>, and re-runs the head's red ci-status job. Each attestation and refusal is a countable record line. - ci-status (merge-holds.mjs laneArmedHold) passes a lane-armed path only on that check run naming a pr-merge.yml run on the default branch that uploaded the artifact; a trusted approval still lifts it. - pr-review-main.yml reviews a lane-armed pull request after a red ci-status too, which breaks the label and status cycle the same-head clear needed. - Only the lanes App's own pull requests lane-arm; the 8413b31 floor rows are dropped. Refs: #10743 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes #10780
Summary
A pull request that changes a lane source (a workflow, Claude Code config,
CLAUDE.md/AGENTS.md,REVIEW.md,docs/standards/**,.gitattributes,.mcp.json) could not merge without the owner. The review lanes run the head's copy of their callers, and their reviewed-head marker is posted bygithub-actions[bot], so a head could forge it.bind()therefore refused the review (lane-source-changed), and nothing could bind at that head. #10780 records the design. It replaces #10076's re-run fix, which cannot bind:babysit_review_evidence.py:313-318on main unbinds from the changed paths alone. The fix here is a review whose workflow definition is always main's, bound on that run's identity and never on a comment's author.Owner decision (Q12): option (A), approve this design. Owner, 2026-10-28 ~22:25Z, on Q12: "AGAIN GO WITH WHATEVER IS IN LINE WITH THIS:" followed by the standing direction (sections 1-8). Parent session applied it as (A). Basis: babysit_review_evidence.py:313-318 on main raises Unbound("lane-source-changed") from the changed paths alone, so a re-run review at the same head unbinds again and #10076's done-when ("fresh reviews dispatched and no needs-human") cannot be met as written; (A) keeps #10076's intent (lane-source PRs merge without the owner) under §2, keeps the boundary under §3 (review counts only from a run of main's workflow; credentialed workflows, permission widening, privileged triggers, new secrets and model legs still hold for a person), and removes a stop no re-run can clear under §4.
Rule files and
.claudeconfig (second security review, HIGH): decision basis, verbatim: owner Q11 (2026-10-28), whose every option stated "agent settings/instruction files stay gated". That is newer than and supersedes the 2026-10-22 direction the suite's test recorded for rules. Rule files are auto-loaded instructions, like CLAUDE.md.Owner decision (Q11): owner, 2026-10-28 ~22:20Z, on Q11: "ANSWER BASED OFF OF THIS DUMMY:" followed by the standing direction (sections 1-8). The deciding lines are §4 "Aim for the highest throughput with the least friction that is safe" and §3 "Never weaken a high/critical security requirement or boundary to gain throughput". The parent session applied it as option (A): the workflow gate class keeps only the review workflows and the workflows that hold credentials beyond the job's
GITHUB_TOKEN. Basis: a job token dies with its job and cannot push to main past branch protection, while an App key or PAT outlives the run.Expected self-holds: this PR adds a model leg and secret references in a new workflow and edits
gatepaths. It scoresagent-surface-review,secret-exposure-reviewandhigh-path:, so it needs a person by design.Fix
.github/workflows/pr-review-main.yml,.github/scripts/pr-review-main/).pr-require-checkscompletion throughworkflow_run. The trigger is gated bycheck-trusted-trigger, as inpr-verify.yml(ADR 0049):permissions: {}, a read-only review job, a main-only checkout into.base, local actions only from.base, and SHA-pinned actions.lane_sourcepredicate.git diff --textof the head as a data file, so a NUL byte cannot hide a script as binary. A patch that still showsBinary files … differorGIT binary patch, or exceeds 300000 bytes, gets no verdict.reference/, the standards and.claude/rules/are copied from main in the plugin's layout. The model may only Read its data directory and is told never to read outside it.write-verdict.mjsreadCoveragewrites a verdict only when every part was read whole, with no offset or limit.--no-renames, so a partial rename into a lane source is reviewed as the whole new file and meets the size and line caps.pr-review-main: <lane>check run, posted by the github-actions app, with the same counts; a run cancelled beforerecordbinds nothing.long-line-review:<path>), and_main_review_countedcounts no verdict whosemax_line_bytes, measured bywrite-verdict.mjs, is past the cap.verdict.jsonaspr-review-main-<lane>-<pr>-<tree>; the tree comes from git, not the model. Arecordjob with no model and onlychecks: writeposts each lane's counts as the check runpr-review-main: <lane>on the reviewed head. Each leg leaves areport-lane-outcomerecord, and the lane is listed in.github/lane-funding.json.babysit_review_evidence.py_main_bound,_main_lane,_recorded_critical;babysit_gh.collect_main_reviews,collect_main_review_critical;babysit_merge._main_reviews). For a lane-source PR, only a main-run verdict counts. Its run must bepr-review-main.yml, with eventworkflow_run, on the default branch, in this repository, with a head main contains, and the verdict must name the lane, PR and tree.pr-review-main: claude-security-reviewcheck run recorded on any head of the PR, including commits and force-pushed heads. A whitespace push to a new, clean tree therefore cannot re-roll a CRITICAL away.babysit_risk.FLOOR_CLASSEScopies regenerated byscripts/sync-hold-floor.py, under Q11 (A)). These aregatepaths that need a person:GITHUB_TOKEN: a non-GITHUB_TOKENsecret, arequest-lane-tokenmint, orid-token: write. The generator derives these rows from the workflow files (babysit_risk.gated_workflowinto aGATE_WORKFLOWSblock), so a workflow that gains a secret joins the gate and--checkfails on a stale table..github/scripts/pr-review-main/**.**/.claude/**and**/.claude-lane/**whole (settings, hooks and the scripts settings registers, skills, agents, commands, rules, output styles and memory),docs/conventions/instruction-exception-register/**anddocs/conventions/untrusted-content/**(the security review's criteria),**/.mcp.json,**/CLAUDE.md,**/CLAUDE.local.md,**/AGENTS.md,REVIEW.mdanddocs/standards/**. These change what agents may do unattended or what later reviews judge against, and the main-run prompt rates widening them CRITICAL.pr-require-checks.ymlstaysenforcement.gaterows takes feat(pr-pipeline): let a trusted author's gate-path PR clear on a passed security review #10708's trusted-author gate-path lift (babysit_risk._NO_AUTHOR_LIFTand every workflow row). Agents commit as the trusted owner, so the lift applies only to main's othergaterows (the rate guard and the remaining pr-merge scripts).docs/conventions/pr-pipeline.yamltakes feat(pr-pipeline): let a trusted author's gate-path PR clear on a passed security review #10708's strictertrustclass.pr-review-main.yml). Now 17 of 34.babysit_risk._cost_reasons. They run before any cost branch can skip the file, have raw-text backstops, and fall back to a text-only reading without PyYAML.permission-widening-review: a job gains a write scope orwrite-all, or falls back to the repository default.privileged-trigger-review(the text-only reader also reads the keys under a blockon:, so a mapping-formissues:holds):pull_request_target,workflow_run,issues,issue_comment,pull_request_review,pull_request_review_comment,discussion,discussion_commentorrepository_dispatchadded.secret-exposure-review: also read from head-vs-base text.unreviewed-patch:<path>: a file with no patch that is not an image, font or PDF.review-path-unbindableis removed. An enforcement path beside a lane source waits for the main-run security review.The pull_request lanes stay for least churn. They still post the inline comments people read, and ordinary PRs are unchanged.
What still needs a person, and why:
.github/scripts/pr-review-main/**: a review cannot be trusted to judge workflows holding its own or other credentials beyond a job token, or the code that counts its verdict..claude/or.claude-lane/, the instruction-exception register or untrusted-content conventions,.mcp.json,CLAUDE.md,CLAUDE.local.md,AGENTS.md,REVIEW.mdordocs/standards/**: it changes what agents may do unattended or what reviews judge against.long-line-review:).What no longer needs a person: an edit to any of the other 17 workflows (job-token writers included, such as ci-status,
intake-reenter.ymlandpr-feedback-dispatch.yml), or to.gitattributes. Each still needs both main-run reviews of the exact head to be clean and no deterministic hold to fire. An enforcement floor path beside such a change also no longer needs a person.Cost: the new
reviewlane legs run onREVIEWS_CLAUDE_API_KEYand use the model, two legs per lane-source PR. Per the owner's standing direction §3, model usage and the promotional API credits are not cost: spending limits are set and extra usage is off, so a limit stops work and never bills.Known follow-up: a pure rename, an empty file or a mode-only change with no patch still holds for a person as
unreviewed-patch:, because GitHub's file list reports it like a changed binary file. #10873 adds the old-against-new content comparison that lets those through.Limits: the workflow runs only once it is on main, so until this merges lane-source PRs stay unbound as today. Each lane runs one leg on the reviews role's API key. A handoff, kill switch or failure leaves no verdict, so the lane stays unbound until the next push (fail closed).
Verification
plugins/source-control/skills/babysit-prs/scripts: 3469 passed with PyYAML after merging main (feat(pr-pipeline): let a trusted author's gate-path PR clear on a passed security review #10708, fix(pr-pipeline): end designed stops cleanly and keep held pull requests out of the merge queue #10737), including the author-lift exclusion test and fix(pr-pipeline): end designed stops cleanly and keep held pull requests out of the merge queue #10737's carry-refuse closure. The closure required adding.github/recurring-schedule.json,plugins/evals/skills/validate/scripts/**,plugins/testing/evals/browser-probe/**andplugins/work-items/scripts/**toCARRY_REFUSES, because the newly gated workflows load them.workflow_run; another branch, path or repository; an artifact of another run; an expired artifact; a head main does not hold; any malformed verdict field; a clean verdict from a failed run; unread recorded counts.secrets.X,toJSON(secrets), a lane token orid-token: writegates an ungated workflow, while a comment or a quoted path does not. The generator puts a newly credentialed workflow on the floor.--check.issues:permission is not anissuestrigger.unreviewed-patch, and an image does not.pr-review-main(15, adding whole-read coverage and the diff splitter; read-only review job, the record job's single scope,--textand the binary or size refusal, the lane record),pr-merge,pr-verify,pr-unblockandresolve-config(741, which caught the missing lane-funding entry behind the earliertest-nodeand ci-status failure) all pass.actionlint,scripts/run-zizmor.sh,sync-hold-floor.py --check,check-artifact-retention.sh --check,runner-policy.mjs,ci-lanes-pr-hold.test.sh(233),prep-warnings.test.sh, ruff, typos and markdownlint pass.Related
#inside quotes and block scalars (follow-up).pr-require-checks.ymlby path (follow-up).uses:with no base-side check (pre-existing, found in the security review).🤖 Generated with Claude Code