Twinned from https://github.com/cfsh/ninefold/issues/1051 by tools/beads/import_github.py's reconcile pass.
Live repro on PR #1044 (design 011's --apply followup, 2026-08-18), while attempting the exercise Aedan asked for: sync to head and see if the new Tier-1 channel from #1018 lets a design-doc-followup PR merge without his approval.
Finding: it never can, for the exact PR shape #1018 was built for. decide() gives two different answers depending only on which token calls it:
``` $ ./tools/pr/perimeter_check.py 1044 --dry-run # jcantsp-bot user token SUCCESS Tier 1 — inside the perimeter
$ GH_TOKEN=$(./tools/reviewer/app_token.py) ./tools/pr/perimeter_check.py 1044 --dry-run # ninefold-reviewer App token FAILURE Needs a human approval — no authority — governance: design_docs/011-task-tracking-on-beads.md ```
Root cause, isolated directly (nflib.channels.design_doc_followup → operationalize_design.projected_text → github_live.gh.Gh.project_view → gh project view <n> --owner cfsh):
``` $ gh project view 9 --owner cfsh --format json # user token {"closed":false,...,"title":"011 — Task tracking...",...} # resolves fine
$ GH_TOKEN=$(./tools/reviewer/app_token.py) gh project view 9 --owner cfsh --format json # App token GraphQL: Could not resolve to a ProjectV2 with the number 9. (organization.projectV2) ```
Why this matters more than an isolated flake: the required merge-authority check can *only* be published by the ninefold-reviewer GitHub App — perimeter_check.py's own docstring: "check runs cannot be created by a user token AT ALL (403)". So the App token is the *only* identity whose answer is ever actually published to the real check GitHub enforces. channels.grants() fails closed on any exception (design-doc-followup's docstring: "refuse, never guess") — correct in isolation, but combined with the App's org-Projects blindness it means the channel cannot ever grant for a doc that already has a Project, which is every --apply followup after the first apply on that doc (i.e. the primary case #1018's own PR description names: "the one operationalize_design.py --apply produces"). The --dry-run path (no credential) will keep reporting SUCCESS forever, which is exactly the trap: it looks like it works, and only fails at the one moment that's actually load-bearing.
Likely fix, not verified further (needs org-admin hands, so flagging rather than building): GitHub Apps need the organization-level *Projects* permission explicitly granted — separate from the repo-level contents/issues/checks permissions the App already has (confirmed via /installation/repositories, which returns the ninefold repo fine). This is set in the App's settings under github.com, likely requiring re-approval of the org installation afterward. Cheapest verification once granted: re-run the two gh project view commands above with the App token.
Filed unclaimed per the file-then-don't-claim rule — this is a defect in #1018's landed code, not part of any chunk I'm working. Session nf-dev-drip, hit while executing design 011 (#1010, #1044).
None.
No comments.