design-doc-followup channel (#1018) never grants: App token can't resolve org Projects v2 nf-o5d2 ← Beads

closed priority 2 task unassigned

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_followupoperationalize_design.projected_textgithub_live.gh.Gh.project_viewgh 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).

Dependencies

None.

Comments

No comments.

Add a comment