Twinned from https://github.com/cfsh/ninefold/issues/784 by tools/beads/import_github.py's reconcile pass.
@aedanpope, 2026-08-09, verbatim:
> "I think the whole automated reviewer running at ~/.ninefold-reviewer/, I think that's the "dispatcher"? Anyway that whole automated process can be deleted, if devs want reviews their only mechanism is to call review_now. There's no 'eventual consistency on reviews'"
Yes, that is the dispatcher. The scheduled job — 2,32 * * * * in the system crontab on this box, schtasks /TN ninefold-reviewer-dispatch on the sandbox box — runs tools/reviewer/dispatch.py over every open PR, decides whether each is due a review, spawns the ones that are, reconciles rev:* labels and publishes the merge-authority check run. review_now.py invokes the same dispatch.py against one PR, synchronously, with --on-demand. So this is a scheduling deletion: the reviewer stays, the clock goes.
⚠️ This is the same move as design 006 piece 11, one tier up. That deleted the *dev's* poll loop on the same reasoning — *"lets delete all the poll looping first, and only add it back when we need it"* (#773, merged). This is its sibling: the last cron in the workflow.
---
Each of these is a real job the tick does today. None is a reason not to do this — they are the list of things whoever takes it has to re-home or consciously drop.
decide() spawns on three paths: no prior verdict, coder activity past the 240-minute grace (REVIEW_GRACE_MIN, #713), and any human comment. ⚠️ This is the thing being deleted, and it is the intent.rev:pending → rev:pass/rev:gaps is re-derived on a tick. Without the cron, a label only moves when someone runs review_now. ⚠️ #586 already found labels insufficient as a gate, which is why merge authority does not read them — so this is cosmetic drift, not a safety hole. Worth confirming before assuming it.merge-authority check run. ⚠️ This is the one to be careful with, and it is the answer to the question below. The check is what the branch ruleset requires; dispatch.py republishes it every sweep, so today a stale check *self-heals*. Without the cron it only moves when a dev runs review_now --reconcile-only — which was already the documented route, but stops being a convenience and becomes required.heartbeat.json, STALE-DISPATCH, dispatch.log, the flock, --on-demand's whole reason for existing (#352: a dead dispatcher ran 38 hours undetected). ⚠️ With no scheduled tick, this entire apparatus has nothing left to watch — review_now runs synchronously and reports its own outcome. Design 006 piece 12 (#780) has just moved STALE-DISPATCH to review_now.py; this issue should delete it outright rather than leave it rehomed.nf-dev-sg, and schtasks /TN ninefold-reviewer-dispatch wherever deployed. ⚠️ Per-box, not in the repo — tools/reviewer/README.md:307 has the create/delete commands.tools/reviewer/dispatch.py — the cron-shaped half: write_heartbeat, the interval/grace constants, the sweep loop. decide() and the review spawn stay; review_now.py is the only caller.tools/pr/review_now.py — dispatch_health, scheduled_dispatcher_here, dispatcher_line, heartbeat_path, log_age_minutes, heartbeat_writer_warning, _log_phrase, and --on-demand's justification. All of it exists to watch a schedule.notes/operating.md — *Diagnosing STALE-DISPATCH* goes entirely.tools/reviewer/README.md, CHARTER.md, notes/reviewer.md, DEVEL.md — the deployment and heartbeat sections.docs/ — nothing expected, but check.⚠️ Almost all of this is GOVERNANCE-denied (tools/reviewer/*, tools/nflib/*, notes/reviewer.md), so this chunk lands on @aedanpope's approval, not a bot verdict. That is a property of the paths, not a problem with the change.
Do not delete the schedule before the check-run question below is answered, because the cron is currently the thing that repairs a stale merge-authority check, and the ruleset is enforcement: active.
---
> "Devs are forced to get reviews even when I approve a PR now, because of the merge-authority check. Can you confirm merge-authority check requires green from the reviewer even if I approve the PR?"
Confirmed in part, and the precise answer matters — because the mechanism is narrower than "requires green", and the *symptom* he hit is real anyway.
nflib/authority.py::decide() on an approved PR returns Decision("human", …, stale=stale_verdict(...)). stale_verdict (:262-380) is explicit:
> *"It never blocks on a verdict's CONTENT. […] A GAPS verdict AT THE HEAD merges exactly as before — the question asked is only has the reviewer read this code, never did it like it."*
And:
> *"No verdict at all is not staleness. A PR from before the reviewer existed, or one never reviewed, has nothing to be stale against."*
So, precisely:
| state of the PR when @aedanpope approves | merges? |
|---|---|
| Never reviewed at all | ✅ yes — no verdict, nothing to be stale against |
| Reviewed, verdict certifies the head | ✅ yes, including a GAPS verdict |
| rev:off | ✅ yes, even with a stale verdict |
| Reviewed, then the head moved | ❌ refused as stale |
So a green verdict is NOT required. What is required is that no verdict exists which is older than the code. The stated reason (:328-333) is that with dismiss_stale_reviews_on_push OFF, *"the same push that outran the reviewer outran the human who approved — the yes was given to different code."*
⚠️ But there is a second, sharper failure and it is probably what he actually hit: the merge-authority check run is published, not computed at merge time. It is a snapshot. Approving a PR does not republish it, so the check keeps whatever it last said — and the branch ruleset refuses the merge on the stale red. Measured today on #776: approved by @aedanpope, and merge_pr.py 776 still failed with *"the base branch policy prohibits the merge"*, because the last published check was FAILURE Needs a human approval. review_now.py 776 --reconcile-only (~12s, no model) republished it as SUCCESS Approved by aedanpope, and the merge went straight through.
That is the bug worth fixing regardless of the dispatcher decision, and today the cron is what papers over it: a tick would have republished the check within 30 minutes. Delete the cron and this stops self-healing — every approval will need a manual --reconcile-only before it can merge, which is exactly the *"devs are forced to get reviews"* friction, made permanent.
⚠️ So this issue has a prerequisite: something must republish the check on approval. Cheapest candidates, for whoever takes it:
merge_pr.py republishes the check itself before merging, when it holds human authority. One call, at the moment of consequence — the same argument authority.py's docstring makes for siting the freshness rule where it is.pull_request_review webhook, which is new infrastructure and probably not worth it here.And separately: STALE_OK=1 is documented as the override for *"Aedan said land it as-is"*, but authority.py:322 records that once the ruleset requires the check, a red run is not something STALE_OK can override at all. Worth confirming whether the escape hatch still works, because the docs say it does.
---
*Filed unclaimed and unlabelled by nf-dev-front-door-wave0, at @aedanpope's request, for a fresh session. Not part of design 006 Wave 0.*
None.
No comments.