dev.py pr: stamp approval:* at creation, stop auto-requesting Aedan's review; delete the dead review-requested gate nf-6ame ← Beads

closed priority 2 task unassigned activesession:approval-stamp

Twinned from https://github.com/cfsh/ninefold/issues/1030 by tools/beads/import_github.py's reconcile pass.

Aedan's ask, verbatim (chat, 2026-08-17)

> "does dev.py pr apply the approval:{human,auto} label? I see some PRs like [#1014] without the label (so IDK if I need to look at them or not)"

> "its confusing since the PRs appear as \"Review Requested\", i.e. a review is requested from me (aedan), but really the dev should be able to make the PR and not yet request my review until they really want to (e.g. perhaps after they've already iterated with the reviewer for a mechanical change, though for a design doc PR they may want to frontload my review)"

> "in fact, we need no explicit flag, in frontload scenario I'll be in loop with dev so I'll know the PR anyway."

> "we don't need a trigger signal right since devs just invoke review_now. If you need something then use rev:reqeuested" — on how the dispatcher should know a PR is in the review flow

> "dispatch.py and poll.py are anachronisms, since we don't have a background reviewer loop" … "so the work can include renaming / cleaning up those files"

Context

Three facts, verified 2026-08-17:

  1. dev.py pr requests aedanpope's review at creation (tools/nflib/devcli.py:1424-1437; REVIEWER = "aedanpope" at :124). Every fresh PR lands in Aedan's Review-Requested queue from birth, whether or not it needs him — the signal carries no information, which is the confusion above.
  2. That request is load-bearing only for a sweep that no longer exists. dispatch.py's decide() skips any PR with no review_requested timeline event ("not in the flow at all", tools/reviewer/dispatch.py:603-604; gather() early-returns at :1155-1157). The gate protected the *unattended cron sweep* from reviewing PRs nobody asked about; #784 deleted the cron, and every invocation is now a dev explicitly naming a PR through review_now.py. The at-creation request to Aedan survives purely as the side effect that arms this dead gate.
  3. The approval:* label never reaches a fresh PR — and today reaches no PR at all. Design 009 T4 (#994, merged 2026-08-17) reconciles it only inside perimeter_check.post() (tools/pr/perimeter_check.py:243-300), which only merge_pr.py and review_now.py --reconcile-only publish — so a just-opened PR like #1014 carries nothing. Worse: the approval:auto/approval:human labels were never created in the repo (gh label list, 2026-08-17: neither exists), and Gh.set_labels' vocabulary guard (tools/nflib/gh.py:566) refuses unknown names — every publish since #994 has printed WARN could not reconcile approval:* and stamped nothing. T4's "done when: every open PR carries exactly one approval:* label" is not actually done; this issue owns closing that gap.

Fixed looks like: a fresh PR carries approval:auto or approval:human from birth (a glance answers "will this need me?"), appears in nobody's Review-Requested queue, and a review-request to aedanpope becomes a *deliberate* dev act — sent when the PR actually wants his eyes (typically converged + approval:human; immediately for design-doc PRs, where he's in the loop and knows the PR anyway).

Principles: B7 (make the wrong signal impossible, don't document around it), A4 (fix the trigger design, not the email fatigue), C1 (dispatch/poll no longer name what the files do), B12 (delete the dead gate), B10 (reuse path_authority()/the T4 reconcile — one implementation).

Prescription — a stack of 4 PRs (E3: extraction / behaviour / behaviour / mechanical)
PR 1 — nflib: one implementation of the approval-label reconcile

- One-time bootstrap (hand-run once, record the commands in the PR body): gh label create "approval:auto" -R cfsh/ninefold --color 2da44e --description "paths inside the merge perimeter — lands on reviewer convergence, no human approval needed" and gh label create "approval:human" -R cfsh/ninefold --color d93f0b --description "paths outside the perimeter — waits for @aedanpope approval". - Move LABEL_APPROVAL_AUTO/LABEL_APPROVAL_HUMAN and approval_label() from perimeter_check.py into tools/nflib/authority.py beside path_authority(), and add a reconcile_approval_label(gh, number) helper that computes want/other and calls Gh.set_labels(number, add=[want], remove=[other]). Informational: callers WARN on failure, never fail — the exact stance perimeter_check.py:292-297 takes today. perimeter_check.post() becomes the first caller. - Precedent is authority.py's own header: logic moved to nflib the moment a second caller appeared, because two implementations of one answer is the drift this repo keeps paying for (#344/#352, #633 F2). - Labels need only the user token — only *check runs* need the App token — so the helper works on any dev box.

PR 2 — dev.py pr: stamp the label, stop requesting Aedan
PR 3 — retire the sweep-era gate
PR 4 — mechanical renames ("the work can include renaming / cleaning up those files")
Test plan
Non-goals
Open questions (defaults marked — nothing blocks)
  1. Where does a dev learn to request Aedan once converged on an approval:human PR? Default: merge_pr.py --why's needs-a-human refusal text appends the gh api …/requested_reviewers command — one place, and dev.py status prints --why verbatim, so it surfaces in the queue view for free. Fold into PR 2 or 3; it is a few lines.
  2. Label colors/descriptions — defaults in PR 1; adjust freely.
  3. engine.py vs another name — default engine.py; anything except dispatch.

Dependencies

None.

Comments

No comments.

Add a comment