Uh oh!
There was an error while loading. Please reload this page.
fix(2590): the sync opens its PRs as a human, so Bugbot reviews them - #348
Conversation
`standards-sync.py --create-prs` authenticated `gh pr create` with the tracebloc-release-train App token, making the PR author `tracebloc-release-train[bot]` (type: Bot). Cursor Bugbot keys its review on the author's Cursor seat and a Bot has none, so Bugbot reviewed NONE of the 14 open sync PRs -- zero `Cursor Bugbot` check runs, measured 2026-08-26 -- and `bugbot / review` failed closed on every one. An explicit `bugbot run` is attributed to the author too, so it answers "Bugbot is not enabled for your user on this team" and cannot rescue the PR. `gh` now takes an explicit identity and PR CREATION ALONE uses it, from SYNC_PR_AUTHOR_TOKEN. The fleet reads and the branch push keep the App token: `owner:`-scoped, short-lived, not tied to one account, which is backend#2036. An empty PAT is a hard per-repo refusal, never a quiet downgrade -- the downgrade IS the bug, and it looks identical to success. Reviewer and assignee become SYNC_REVIEWER rather than GITHUB_ACTOR. The dispatcher is now the author, and GitHub refuses an approving review from a PR's own author, so keeping GITHUB_ACTOR would deadlock every sync PR. The 4 open PRs already requesting LukasWodka were reassigned by hand. Mutation-proved: 5 mutations, each caught by the assertion named for it. Writing them surfaced two harness defects worth keeping out of the sibling runners -- a mutation that applies but does not COMPILE reads exactly like good coverage, and a suite that CRASHES on a mutation is red without a verdict (rule 10). The new runner reports MALFORMED and CRASHED separately, and the empty-PAT case scripts the whole happy path so the assertion is what reddens. Closestracebloc/backend#2590
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
saqlainsyed007
left a comment
There was a problem hiding this comment.
Really solid core — I verified the credential handling end to end and it's clean: the gh() helper passes the token via env=dict(os.environ, GH_TOKEN=token, GITHUB_TOKEN=token) to subprocess.run([...], env=…) with no shell=True, so the PAT is never in argv, never spliced into a shell string, never written to $GITHUB_ENV/output or the PR body; fleet reads/branch push/commit stay on the App token and only gh pr create/pr edit carry the PAT. The mutation-harness tests are genuinely rigorous. But a few things to resolve before I approve — this is the .github repo and the change is about fleet-wide credentials, so the bar is high.
Blockers
F5 (same-PR doc obligation) — the workflow's own TOKEN header is now false. Lines 25-34 still say create-prs's Pull requests: RW need is "satisfied by the tracebloc-release-train App installation … it carries contents:write and pull_requests:write." After this PR the App token no longer opens the PRs — your new step comment (117-123) says exactly that. Same-file contradiction; please update the header in this PR. It also still asserts "a second fleet-wide credential is the thing backend#2036 exists to stop" — which is precisely what
SYNC_PR_AUTHOR_TOKENnow is, so that paragraph needs reconciling with the new reality too.F4 (config-coupling deadlock) —
SYNC_REVIEWER = "saqlainsyed007"is a hardcoded literal and nothing reads the PAT owner at runtime. The reviewer≠author invariant rests on that literal plus a selftest that hardcodes!= "LukasWodka". IfSYNC_PR_AUTHOR_TOKENis ever re-provisioned to saqlainsyed007's PAT, author==reviewer, every--add-reviewer422s, and the #2590 deadlock this PR fixes silently returns — with tests still green. Please assert reviewer ≠ (token owner) at runtime (resolve the PAT's login once viagh api userand compare), rather than trusting a literal.
Strong recommendation (security)
- Least privilege — use a fine-grained PAT, not a classic
repoone.SYNC_PR_AUTHOR_TOKENis a classicrepo-scoped, long-lived, human-owned, fleet-wide credential — full code RW + issues + settings + webhooks on every repo the owner can reach — to do work that needs onlypull_requests: write+contents: read. A fine-grained PAT scoped to those on the target repos is dramatically tighter and honors backend#2036's intent. The in-code rationale ("Needsreposcope … contents:write alone cannot open a PR") conflates "the App's contents:write can't" with "a fine-grained pull_requests PAT can't" — the latter can.
Should-fix (non-blocking)
- F1 —
_ensure_prearly-returns before the reviewer/assignee block when an open PR already tracks the branch, so a PR that failed to get its (now merge-blocking) reviewer on an earlier run is never repaired on later runs. Re-request the reviewer on the existing-PR path too. - F3 — the fail-closed PAT-empty guard lives inside
_ensure_pr, afterremediate()has already pushed the branch and committed CLAUDE.md fleet-wide; move the emptiness check intomain()before any write so a missing token can't leave a partial rollout. - F2 — a failed
--add-reviewerbeing a non-fatal::warning::is a defensible call, but combined with F1 (won't self-heal) it means a reviewer-less, un-mergeable PR can ship green — the same class this PR exists to kill. Worth making the reviewer add fatal.
Great work on the token split and tests; holding on F5 + F4 (and please weigh the fine-grained PAT).
…fore the writes @saqlainsyed007's review on #348. Both blockers, both should-fixes, and the security point as far as code can take it. F4 (BLOCKER) -- reviewer != author rested on two literals that agreed with each other and with nothing else. `SYNC_REVIEWER = "saqlainsyed007"` here, and the selftest pinning the other side as `!= "LukasWodka"`. Re-provision SYNC_PR_AUTHOR_TOKEN to saqlainsyed007's PAT and author == reviewer, every `--add-reviewer` 422s, the backend#2590 deadlock returns -- and neither literal moved, so the suite stays GREEN through it. A credential's owner is a fact about the credential, so `author_login()` asks GitHub, as the PAT rather than as the ambient App identity, and `main()` refuses when it equals SYNC_REVIEWER. An unresolvable token is its own refusal rather than a guess. F3 (should-fix, and the one with teeth) -- the PAT-empty guard lived inside `_ensure_pr`, which runs AFTER `remediate()` has created the branch and pushed CLAUDE.md. A missing token therefore produced a fleet-wide HALF ROLLOUT: branches pushed to every drifted repo, PRs opened nowhere. Validation moved into `main()`, before the first repo is touched. The in-function check stays as belt-and-braces for a caller that skipped the gate, and says so. F1 + F2 -- `_ensure_pr` returned as soon as an open PR tracked the branch, so a PR whose `--add-reviewer` had failed once was never repaired on any later run; and that failure was a `::warning::`. Together: a reviewer-less PR, which branch protection makes unmergeable, shipping on a green run for ever. The role assignment now runs on BOTH paths, and the reviewer half is FATAL. The assignee half stays cosmetic on purpose -- it blocks no merge, and failing the run over it would be the opposite error. F5 -- the workflow's TOKEN header still said the App satisfied both needs, which the step comment 90 lines below already contradicted. Reconciled, including the paragraph arguing that "a second fleet-wide credential is the thing backend#2036 exists to stop": that argument was right about the risk and is overtaken by a harder constraint, and saying so is better than deleting it. SECURITY -- the in-code rationale claimed the PAT "needs `repo` scope; contents: write alone cannot open a PR". That conflates two claims. The true one is that the APP's contents:write cannot open a PR BUGBOT WILL REVIEW -- an authorship problem, not a scope one. A fine-grained PAT with `pull_requests: write` + `contents: read` opens PRs perfectly well and is dramatically tighter. Corrected in place; the re-provisioning itself is a decision, not a code change, so it is recorded rather than asserted as done. TWO DEFECTS I FOUND IN MY OWN TEST EDITS, worth recording because both are the shape this repo keeps paying for: * The new checks were appended AFTER the summary line and `sys.exit(1)`. They ran only when everything else passed, were never counted, and a failure among them could not affect the exit code -- a test block that inspects nothing. Moved above the summary; the count went 36 -> 42, which is how it was caught. * Stubs scripted `(0, "[]", "")` for "no open PR". The real call carries `--jq '.[0].number // empty'`, so no-PR is an EMPTY string and `"[]"` is truthy. Now that the existing-PR path also makes two edit calls, a `"[]"` stub consumes the same number of entries down the OPPOSITE branch -- passing while testing the case it is named against. Three stubs corrected. Verified: selftest 42 checks / 0 failed, `py_compile` clean, ruff clean, actionlint clean on the workflow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The mutation harness reported "SYNC_REVIEWER becomes the account that authors the PRs" as UNCAUGHT once the literal-vs-literal check was retired. A guard nothing can exercise is the shape this repo keeps removing, so the gate is a function now and each refusal is pinned individually. And the two mutations are re-aimed at what can actually break. Flipping the SYNC_REVIEWER literal is SURVIVABLE now -- the runtime gate refuses the run -- so the mutation that matters is dropping the comparison, not changing one of its operands. The reviewer-reverts marker follows the reviewer call into _assign_roles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`failed = [...]` was computed a hundred lines above the last checks, so anything appended below it PRINTED its FAIL and did not count -- the summary read `0 failed` while two checks had failed, and the script exited 0. The mutation harness is what surfaced it: a gate mutation came back UNCAUGHT because the suite it was measured against was structurally incapable of failing there. A test file that cannot go red is the same class as a guard that cannot fire, one level up. Also re-aimed the unresolvable-token mutation. Skipping the `is None` branch crashes on `login.lower()` -- red without a verdict, which tells the harness nothing. Making `author_login` return a login it could not resolve is the same defect with a readable outcome. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LukasWodka
commented
Aug 26, 2026
All five addressed in F4 — the invariant is now asked of the tokenYou're exactly right that it rested on two literals agreeing with each other and with nothing else. I extracted the gate into a function rather than leaving it inline in That also changes what the right mutation is. Flipping the F3 — the guard moved ahead of the writesThis was the one with teeth. The check ran inside F1 + F2 — together they were the silent shapeRole assignment runs on both paths now, so a PR whose F5 — header reconciledIncluding the paragraph arguing "a second fleet-wide credential is the thing backend#2036 exists to stop." That argument was right about the risk and is overtaken by a harder constraint — no App token can author a PR Bugbot will look at — so I reconciled it rather than deleting it. The security pointYou're right and I corrected the rationale in place: "needs The re-provisioning itself is a decision, not a code change, so I recorded it rather than asserting it done. It needs a new token minted and the secret swapped — worth doing before this is relied on fleet-wide. Two defects in my own test edits, both the local class
Verification
|
LukasWodka
commented
Aug 26, 2026
bugbot run |
saqlainsyed007
left a comment
There was a problem hiding this comment.
Re-review of 2c0b40be — every code finding is addressed, and well. Verified each against the new source:
- F4 (runtime identity) —
author_login(token)now asks GitHub who the PAT is (gh api user --jq .login,Nonewhen unresolvable), andcheck_author_identity(token)compares it toSYNC_REVIEWER. The invariant is derived on both sides now, not literal-vs-literal, and the mutation harness catches "SYNC_REVIEWER becomes the PR author" (it was UNCAUGHT before). Exactly the fix. - F3 (guard before writes) — the identity gate is extracted and called before
remediate()touches any repo, ordered emptiness → resolution → identity, each with its own refusal. No more branch/commit fleet-wide then refuse-to-open. - F5 (stale header) — the TOKEN header is rewritten to "TOKENS, PLURAL … the split is the point," correctly describing App(Contents:RW, branch push) + human PAT(PR author with a Cursor seat). No longer claims the App satisfies PR-creation.
- F1 (existing-PR reviewer) — the existing-PR path now returns
_assign_roles(...)instead of early-returning, so a PR whose reviewer add failed on an earlier run gets repaired. F2 is addressed as a bonus: a failed--add-reviewernow returns an error string (fatal) rather than a non-fatal warning.
All my threads are resolved and verified. The only thing left is CI (pending) — once it's green I'll approve.
One non-code note (won't hold the approval): the least-privilege point stands at the provisioning layer — SYNC_PR_AUTHOR_TOKEN should be a fine-grained PAT scoped to pull_requests:write + contents:read on the target repos, not a classic repo-scoped one. The code accepts whichever, so this is a secret-setup choice; worth doing so the fleet-wide human credential is as narrow as the job it does.
Uh oh!
There was an error while loading. Please reload this page.
…bot on #348) `check_author_identity` proves the token EXISTS, RESOLVES and is not the reviewer. Bugbot's point is that none of that proves it can open a PR -- the wrong fine-grained permissions, or a token never SSO-authorized for the org, passes every one of those checks and then fails at `pr create`, by which point `remediate` has pushed a branch and a commit to every drifted repo. That is the half-rollout the gate was added to prevent, arriving through the one failure mode the gate cannot see. AND NOTHING READ-ONLY CAN CLOSE IT. "Can this token open a PR in this org" has exactly one proof, which is opening one; a `GET /user` or a repo-permissions probe narrows the odds and still cannot answer it. So the guarantee is BOUNDED rather than claimed: `pr create` failing now raises `AuthorUnusable`, `main()` aborts the remaining targets, and at most ONE repo is left with a branch and no PR instead of nineteen. A DISTINCT TYPE, not a string the loop matches on prose. The loop has to tell "this repo failed" from "this credential fails everywhere", and those are different facts with different correct responses -- continue versus stop. The report says it happened. A run that stopped early and did not say so reads as a complete sweep of a smaller fleet, which is the same shape as every other silent-success defect this PR has been about. Verified: selftest 49 checks / 0 failed, py_compile and ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LukasWodka
commented
Aug 26, 2026
bugbot run |
saqlainsyed007
left a comment
There was a problem hiding this comment.
Re-review of fa9c0d1a — the Bugbot follow-up is a sound fix and doesn't regress anything I flagged.
The AuthorUnusable addition is exactly right: check_author_identity proves the token exists, resolves, and isn't the reviewer, but none of that proves it can open a PR (wrong fine-grained permission, or never SSO-authorized). Raising on the first failed pr create — rather than returning per-repo — aborts the fleet loop so at most one repo is left branch-without-PR instead of all of them, and the docstring is honest that "the only proof is opening one," so the guarantee is bounded rather than claimed. That's consistent with the guard-before-writes reasoning from the earlier round.
All my findings (F1–F5) remain addressed, and the Bugbot thread is resolved. Only CI is left (pending) — I'll approve once it's green.
Uh oh!
There was an error while loading. Please reload this page.
…red (Bugbot on #348) The existing-PR path repaired roles and returned None, so every sync PR already open -- all of them opened as tracebloc-release-train[bot] -- would have been reported ensured while staying exactly as unreviewable as before. A PR's author cannot be reassigned, so the only honest outcome is a per-repo error naming the remedy. is_bot is read from GitHub rather than pattern-matched off the login; an unparseable row fails closed rather than reading as human. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LukasWodka
commented
Aug 26, 2026
bugbot run |
Uh oh!
There was an error while loading. Please reload this page.
…udit (Bugbot on #348) Moving the credential gate into main() stopped the half-rollout, but it was written as die() -- which also skipped the read-only fleet audit. standards-sync.yml argues against exactly that twenty lines above the secret: aborting before the audit turns "PRs could not be opened" into "fleet state unknown", strictly less information. The gate exists to stop writes, and the audit is a read. A refusal now switches remediation off: every target is still classified and reported, each drifted row reads NOT REMEDIATED, the report carries a REMEDIATION DISABLED block naming the cause, no branch is pushed, and the run exits 2 rather than the --create-prs 0 (which would claim every drifted repo has a PR open). The workflow comment is updated so it no longer describes a placement the code has moved past. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LukasWodka
commented
Aug 26, 2026
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 1962556. Configure here.
LukasWodka
commented
Aug 26, 2026
Green on
All 14 checks green (
One operational consequence worth flagging before this merges: there are open bot-authored sync PRs on the fleet today ( |
saqlainsyed007
left a comment
There was a problem hiding this comment.
Approving 1962556a — CI green, mergeable, no open threads, and I re-verified the net change through the Bugbot rounds: all my original findings still hold (author_login + check_author_identity gate the PAT before any write, AuthorUnusable aborts the fleet on a failed create, and the TOKENS-PLURAL header correctly describes the App-vs-PAT split), and the three follow-up commits are all on-topic identity/refusal hardening from Bugbot's own review — exactly what this PR set out to enable: a refused PAT now disarms the writes rather than half-rolling-out, and an open bot-authored sync PR is refused rather than treated as ensured.
Nice piece of work — this closes the loop that had all 14 sync PRs skipping Bugbot. (Non-code, for provisioning: keep SYNC_PR_AUTHOR_TOKEN a fine-grained PAT scoped to pull_requests:write + contents:read.)
LukasWodka
commented
Aug 26, 2026
This PR is ahead on every axis — 7 commits vs 1, two human re-reviews and three Bugbot rounds all resolved vs none, 14/14 green vs I have not closed #351 — that is a human call. Recommendation is to close it in favour of this one. Full comparison posted on #351. |
Uh oh!
There was an error while loading. Please reload this page.
…ckend#2690) (#368) * fix(standards-sync): a PR refusal stops the writes, not the audit (backend#2690) `remediate()` raising `AuthorUnusable` used to `break` the target loop, so a credential that GitHub stopped taking on repo 1 of 16 left the report covering a PREFIX of the fleet -- while the summary line went on printing `{len(targets)} targets` and the drift counts described only the repos it had reached. A mechanism reporting a result it never established. Stopping the WRITES is correct and stays (#348: the same credential opens every one of these, so carrying on would push a branch to all of them and open a PR on none). Stopping the AUDIT was never correct -- classification is a read that needs no credential, and `standards-sync.yml` argues exactly this twenty lines above the secret: aborting before the audit "would turn 'PRs could not be opened' into 'fleet state unknown' -- strictly less information". The `author_refusal` path already obeyed that. This path contradicted it. So the refusal now sets `remediating = False` and carries on classifying: every target gets a row, repos after the halt read NOT REMEDIATED, and the footer says which of the two things happened -- remediation halted at `beta`, audit complete for all N. Six selftest checks (60 -> 67), and two mutations rather than one: the properties "the audit continues" and "no further branch is pushed" are independent, and a single mutation cannot prove both. Closestracebloc/backend#2690 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(standards-sync): scope the post-halt row check to the ROW (backend#2690) Bugbot on .github#368. The footer this change adds contains the words "NOT REMEDIATED", so searching the whole report was satisfied before any row was consulted -- gamma could be a bare drifted classification and the check still passed. The missing-repo check beside it already matches `| {r} |` per target; this one now does too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Summary
The sync opened its per-repo PRs with the
tracebloc-release-trainApp token, sothe author was
tracebloc-release-train[bot](type: Bot). Cursor Bugbot keysits review on the author's Cursor seat and a Bot has none — so Bugbot
reviewed 0 of 14 open sync PRs and
bugbot / reviewfailed closed on all ofthem.
gh()now takes an explicit identity, and PR creation alone uses it(
SYNC_PR_AUTHOR_TOKEN, a PAT owned by a seated user). Fleet reads and thebranch push keep the App token —
owner:-scoped, short-lived, not tied to oneaccount, which is backend#2036 and stays intact.
Evidence
Measured 2026-08-26, all 14 open sync PRs — zero
Cursor Bugbotcheck runs:Bugbot itself is healthy — every human-authored PR the same day got a verdict
(
.github#347success,backend#2568neutral,backend#2569pass,model-zoo#188pass). A manualbugbot runnames the cause, because Cursorattributes the request to the author and not the commenter: "Bugbot is not
enabled for your user on this team."
Reviewer and assignee
Both become
SYNC_REVIEWER(saqlainsyed007) instead ofGITHUB_ACTOR. Thedispatcher is now the author, and GitHub refuses an approving review from a
PR's own author — keeping
GITHUB_ACTORwould deadlock every sync PR with arequired review nobody eligible could give. Four of the 14 were already in that
shape (
docs#143,release-train#130,.github#344,claude-skills#39) andwere reassigned by hand.
The two roles are deliberately collapsed (against D31's usual split): nobody
"does the work" on a machine-generated prose sync.
Fails closed
An empty PAT is a hard per-repo refusal, not a fallback to the App token. The
fallback is the bug — it opens a bot-authored PR that looks identical to a
working one and that Bugbot silently skips.
standards-sync.ymlalready refusesa fallback in the other direction for the same reason.
Test plan
make selftests— 29 passed, 0 failed. The existing meta-selftest picked up thenew runner unprompted: "standards-sync-mutations.py calls its baseline guard
before its first write".
make lint/make check— both PASS.make mutation-standards-sync— 5 mutations, each caught by the assertionnamed for it, 0 stale / 0 malformed / 0 uncaught:
pr createlosestoken=, reverting to the App identitypr createruns as the author PATGITHUB_ACTOR, who is now the authorSYNC_REVIEWERbecomes the PR authorselftests-coverinitially refused this PR — "standards-sync-mutations.pyis not run by any mutation target" — because
MUTATION_TARGETSis an explicitlist, not the glob. Wired, and it now reports all 19 selftests and 9 mutation
runners covered.
Two harness defects worth naming
Writing the mutations surfaced two ways a mutation tier can look like coverage
and be none. Both are fixed here and are not guarded in the sibling runners:
pr listcasespliced
token=mid-call →positional argument follows keyword argument.The suite could not import the file, crashed, printed no
FAIL:line, and therun recorded UNCAUGHT — indistinguishable from a vacuous test. Every mutation
is now
compile()d first and reported asMALFORMEDrather than counted.gh pr list, so a fallback over-ran the stub and raised out of the suite: red,but with no assertion naming the behaviour. The selftest now scripts the whole
happy path so the assertion is what reddens, and the runner reports
CRASHEDseparately so the distinction cannot be silently re-lost.
Before this can be dispatched
SYNC_PR_AUTHOR_TOKENmust exist as an org Actions secret — arepo-scoped PATowned by a seated Cursor user. Until it does,
--create-prsrefuses per repowith a message naming the variable, which is the intended failure: no PRs beats
bot-authored PRs nothing reviews.
Out of scope
Dependabot.
bugbot-gate-caller.yml:22-32already records the firstinstance of this class — "BUGBOT DOES NOT REVIEW DEPENDABOT PRs", sampled
2026-08-25 — and it is why
bugbot / reviewis still advisory rather thanrequired. Dependabot's PRs cannot be re-authored, so this PR does not close that
half and step 3 of backend#2284 stays blocked on it.
Closes tracebloc/backend#2590
🤖 Generated with Claude Code
Note
Medium Risk
Changes fleet-wide remediation auth and PR authorship for org standards sync; misconfigured
SYNC_PR_AUTHOR_TOKENdisables remediation or aborts mid-run, but read-only audit still runs and the script fails closed rather than silently opening bot PRs.Overview
Fixes org-standards sync PRs that Bugbot never reviewed because
gh pr createused the release-train App token and made the author a bot with no Cursor seat.The sync now splits credentials: the App token still does fleet reads, branch pushes, and
CLAUDE.mdwrites;SYNC_PR_AUTHOR_TOKEN(a seated human PAT) is used only forgh pr createand reviewer/assignee edits. Reviewer and assignee are bothSYNC_REVIEWERinstead ofGITHUB_ACTOR, so the author and required reviewer are not the same person.Fail-closed behavior moved ahead of any writes:
check_author_identity()validates the PAT (present, resolvable, not equal toSYNC_REVIEWER) inmain()before the first branch push. A bad credential disarms remediation but still runs the read-only fleet audit, reports NOT REMEDIATED / REMEDIATION DISABLED, and exits 2. Existing open PRs with bot authors are errors (close and reopen as human);AuthorUnusableaborts the fleet on the first failedpr createto limit half-rollouts.Workflow comments document the two-token model;
Makefilewiresmutation-standards-sync.standards-sync-selftest.pyand newstandards-sync-mutations.pypin identity split, gates, and bot-author handling.Reviewed by Cursor Bugbot for commit 1962556. Bugbot is set up for automated code reviews on this repo. Configure here.