Twinned from https://github.com/cfsh/ninefold/issues/1030 by tools/beads/import_github.py's reconcile pass.
> "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"
Three facts, verified 2026-08-17:
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.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.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).
- 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.
dev.py pr: stamp the label, stop requesting Aedanverb_pr (tools/nflib/devcli.py:1141), delete the request the review step (:1424-1437) and add a stamp approval:* step in its place, calling reconcile_approval_label through the existing step() machinery. Remedy on failure: ./tools/pr/perimeter_check.py <n> --dry-run to see the ruling, plus the PR's Labels picker in the GitHub UI — never raw REST (#657 / #906 F1, per the note at :1404).Next: line already points at review_now — keep it.approval:*; request aedanpope's review (the gh api …/requested_reviewers command already in the Feedback section) when the PR actually wants his eyes — normally after converging with the Tier-0 reviewer on an approval:human PR, immediately for design-doc PRs he is steering. Update verb_pr's docstring, and sweep notes/reviewer.md for any repeat of the at-creation-request claim.dispatch.py decide(): delete the if not state.review_requested: skip arm (:603-604) and PrState.review_requested; fix the docstring's numbered ordering list. ⚠ The arm order is load-bearing (the docstring carries a differential-test war story) — delete the arm, reorder nothing else.gather() (:1153-1159): no longer early-return on an empty timeline_review_requests() — but keep reading the timeline: request events still seed theirs/humans (:1167, :1194), because Aedan re-requesting review after changes is exactly the human-activity signal that triggers a re-review, and that must survive.review_now.py conclusion(): delete the "no review ever requested" arm (:348-357).tools/reviewer/dispatch.py → tools/reviewer/engine.py — it dispatches nothing; it runs one review and reconciles the check and labels. Update review_now.py's DISPATCHER const, the one-line dispatch.sh shim's exec target (retiring the shims themselves stays #789), tools/reviewer/README.md/DESIGN.md/CHARTER.md, and every dispatch.py:<line> citation in other files' docstrings — test_doc_pointers.py::test_every_LINE_CITED_path_is_tracked holds you honest.tools/pr/poll.py → tools/pr/digest.py — its own header has called it a one-shot digest since design 006 piece 11. Update test_poll.py → test_digest.py, .claude/commands/triage.md, .claude/commands/tl.md, CLAUDE.md, and the notes/* references (grep poll.py; ~10 files).sweep_one, "the per-PR sweep", "aborts the sweep" → review-shaped names). #777's list of *adjacent-file* leftovers stays its own issue.test_nflib_authority.py: approval_label and the reconcile helper — want/other pairing, WARN-not-raise on a refused label name.test_perimeter_check.py: existing label-reconcile cases repointed at the moved helper; behaviour unchanged.test_nflib_devcli.py: verb_pr no longer calls request_review; the stamp step appears in the step sequence; a failed stamp lands in failures with the perimeter-check remedy and does not fail the verb.test_dispatch.py: the never-requested skip cases become proceeds; request-timestamp seeding of humans stays pinned.test_review_now.py: the "no review ever requested" outcome arm deleted with its producer../dev.py pr on an inside-perimeter diff → it wears approval:auto and gh pr view <n> --json reviewRequests is empty; ./dev.py review on it spawns a review rather than skip (no review ever requested).rev:requested label. Aedan offered it only "if you need something"; with the gate deleted, nothing needs it — don't create it.dev.py verb or flag for "request Aedan now" — his call: "we need no explicit flag". The documented gh api …/requested_reviewers command is the deliberate signal.perimeter_check.py's check-publish paths and exit codes.dispatch.sh/app_token.sh shims), #777 (poll-loop vocabulary in adjacent files), #726 (digest bloat) stay their own issues — PR 4 only repoints the shim's one exec line.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.engine.py vs another name — default engine.py; anything except dispatch.None.
No comments.