Context
0119 lets an agent merge only throughpnpm pr:merge, which needs an
AI-REVIEW: PASS <head sha> comment. Builders and reviewers both act as the stackra-bot GitHub App, and agents
also hold the owner’s gh login (figentra-usr). pr-merge trusted verdicts from both identities. So it could not
tell an author from its reviewer. On 2026-10-20, builder agents posted AI-REVIEW: PASS on their own pull requests
and merged them, some through plain gh as the owner’s account. That breaks
0021 rule 6: agents are not their own approvers.
We weighed two designs. A signed review record verified by pr-merge needs a key that the builder session can also
read on the same machine. A second GitHub App ties each verdict to an identity that GitHub records, and that app can
hold fewer permissions than the builder app.
Decision
- A second GitHub App,
stackra-reviewer, is the only identity whose verdict counts. Its installation holds pull requests: write and contents: read, never contents: write. It can comment and review, and it cannot push. Its credentials areSTACKRA_REVIEWER_APP_ID,STACKRA_REVIEWER_INSTALLATION_IDandSTACKRA_REVIEWER_PRIVATE_KEYin Infisical. Reviewer agents post throughpnpm bot:review, which runs onlygh pr view|diff|checks|comment|reviewand read-onlygh api(no-X,--method, fields or--input, alone or bundled as in-iX). pr-mergecounts a verdict only from thestackra-reviewerApp, identified by what GitHub records, never by a login string: a REST issue comment withuser.typeBotandperformed_via_github_app.slugstackra-reviewer. It never counts when the verdict’s author is the PR author or the head commit’s author or committer. A verdict from the builder appstackra-bot, or from a user account namedstackra-reviewer, is ignored, and the refusal says why. The verdict format is binding:- the first line is
AI-REVIEW: PASS <full 40-hex head sha> base=<base ref>orAI-REVIEW: FAIL <head sha>. A PASS counts only while the PR still targets that base ref, so re-targeting the PR voids it. The base is a ref name, not a SHA, becausemainmoves under every open PR and the merge is a squash onto its tip; - an edited verdict never counts, and an edit to any reviewer comment voids every reviewer verdict posted before it. Anyone with write access can edit another account’s comment, so the reviewer always posts a new comment;
- a FAIL for the head SHA is sticky: it beats every PASS for that SHA, older or newer. A wrong FAIL is cleared by a new commit or by the owner. Minimized comments are not visible to REST and still count, which is safe for a FAIL; a reviewer retracts a PASS by posting a FAIL.
- the first line is
- The owner approves by hand only with a pull request review: an APPROVED review by the owner user on the head
SHA whose body ends with the token
owner-approval, and not on a commit the owner account authored. AnAI-REVIEW:comment from the owner account never counts. - Agents never write
owner-approvalor theowner-approvedlabel, and never postAI-REVIEW:through plaingh. The bash guard denies all three. This is a tripwire, not the control: the guard cannot see a--body-file. - Agents stop committing and pushing as the owner. Builders commit and push through
pnpm bot:gitand open PRs throughpnpm bot:gh. Agent environments drop the owner’sghlogin once the bot paths cover every write.
Consequences
- A PR built by
stackra-botneeds a reviewer agent’s PASS posted asstackra-reviewer. Self-review is refused with a reason that names the identity. - The owner created the
stackra-reviewerapp on 2026-10-20 (installed onfigentra/stackraonly, pull requests: write, contents: read) and stored its credentials in Infisical/agents, which accepted this record. Reviewer agents post every verdict throughpnpm bot:review; a verdict posted throughpnpm bot:ghno longer counts. - What remains: the same machine can mint both apps’ tokens, so a builder agent that ignores its role could run
pnpm bot:review. Rules 2 and 5 remove the obvious path, because the reviewer never authors and builders never act as the owner. Closing the gap fully needs the review to run where builders hold no reviewer key. That means a CI job or a separate host with its own Infisical identity, which is Phase 3 scope (merge queue, see below). - The owner path is forgeable today. Agents still hold the owner’s
ghlogin (figentra-usr), so an agent can submit an APPROVED review ending inowner-approval, or add theowner-approvedlabel, as the owner. Moving the owner path from a comment to a review only makes the forgery a deliberate act; the guard denies the token on the command line, but not in a--body-file. Rule 5 drops the owner’s login from agent environments, and it also takes away the owner-authored-commit exclusion that used to catch this. The real fix is an owner decision (remove the owner’sghlogin from agent sessions, or require the owner’s review to come from a device agents cannot reach). Until then, the owner-approval path is only as strong as agents’ compliance with rule 4. - Deleted comments are invisible to
pr-merge: deleting a reviewer FAIL makes an older PASS count again. Only accounts with write access can delete another account’s comment, which today includes the owner login agents hold (see above).
Enforcement
tools/scripts/pr-merge.mjs(aiReview,blockers, tests intools/scripts/__tests__/pr-merge.test.mjs): the App identity check, the author and committer exclusion, the full head SHA and base ref, edited and sticky-FAIL verdicts, and the owner’s approving review with theowner-approvaltoken.tools/scripts/bot-gh.mjs(reviewerAllows): the reviewer’s command allowlist, reading bundled short flags letter by letter and refusing--edit-lastand--delete-last..agents/hooks/guard-bash.mjs: denies agent commands that writeowner-approvalorowner-approved, or postAI-REVIEW:through plaingh.- Planned: the review runs in a CI job with its own credentials (the Phase 3 merge-queue design). OpenCode’s guard plugin mirrors the bash rules.
Supersedes and sources
Supersedes: none. Amends 0119: the AI review’s author must be the reviewer identity. Sources: the 2026-10-20 incident (agent self-PASS and merges asfigentra-usr); the SDD/SDLC plan
§16 D5 (bot identity). Numbered 0230, reserved for this record.