Context
Found during the 2026-07-06 incident audit (parent: #1667). During backlog recovery today, the order in which stuck PRs got reviewed looked effectively random to the operator, who expected (and wants going forward) deterministic oldest-first draining per repo.
Verified against src/settings/agent-sweep.ts (selectRegateCandidates) and src/queue/processors.ts (sweepRepoRegate) as they exist today. The candidate sort is repairPriority || regateProgress || number, where regateProgress falls back through lastRegatedAt → createdAt → epoch(0). This is deliberately staleness-based (per the file's own header comment): a PR regated 2 minutes ago sorts after one regated an hour ago, regardless of PR number or creation time. PR number is only the final tiebreaker when regateProgress values are exactly equal, which almost never happens once any PR has been swept at least once. Net effect: after the first sweep pass touches a repo, PR selection order for all subsequent sweeps is driven by "which PR I regated longest ago," not creation order.
Separately, the per-PR fan-out stagger (delaySeconds = Math.min(index * 10, 600)) does propagate the chosen sort's array order into dispatch order, but real-time webhook-driven review (untouched by any of this) can process/merge any PR at any moment regardless of sweep state — so a sort-key change alone cannot make cross-path ordering fully deterministic; it can only control the scheduled-sweep path's own selection order.
Requirements
- Add an explicit, opt-in, default-off
orderMode parameter to selectRegateCandidates, choosing between the existing staleness sort (kept as-is, unchanged default, for its documented convergence guarantee) and a new oldest-first sort keyed on createdAt ascending (falling back to PR number when createdAt is missing). repairPriority (outage-repair PRs) still sorts ahead of both, unchanged.
- Do not silently replace the staleness order globally — its docstring explains it exists specifically so the sweep provably converges over all open PRs even when
updatedAt-driven writes are suppressed (dry-run/paused). The new mode must preserve an equivalent convergence guarantee (verify via a test mirroring the existing staleness convergence tests: full backlog coverage in ceil(open/max) ticks).
- Gate the new ordering behind a resolvable per-repo setting (e.g.
regateSweepOrderMode: "staleness" | "oldest-first", default "staleness") so no existing repo's behavior changes silently, and the owner can opt specific repos in.
- Explicitly document that this is a selection-time preference, not a delivery guarantee — real-time webhook-driven review is not gated by this sort and can still process any PR out of order at any time.
Deliverables
src/settings/agent-sweep.ts: orderMode field on selectRegateCandidates's input, a creationOrder helper, branched sort comparator.
- New
regateSweepOrderMode repo setting end-to-end (DB column + migration if settings are DB-backed, resolver default, .gittensory.yml schema entry, OpenAPI regen per this repo's own contributing skill).
src/queue/processors.ts: thread the resolved orderMode through sweepRepoRegate.
test/unit/agent-sweep.test.ts: new test block mirroring existing staleness convergence tests (basic order, tie-break, multi-tick convergence, repairPriority still wins over both orderings).
- Regression test asserting the fanned-out
delaySeconds sequence is monotonic in the chosen order.
Expected outcome
Repos that opt into oldest-first get scheduled re-gate sweeps that process open PRs in creation-time order per repo per tick, with the same convergence guarantee the existing staleness mode already provides. Repos that don't opt in see zero behavior change. The change is scoped to scheduled-sweep selection order only, not a claim about cross-path (webhook vs. sweep) determinism.
Context
Found during the 2026-07-06 incident audit (parent: #1667). During backlog recovery today, the order in which stuck PRs got reviewed looked effectively random to the operator, who expected (and wants going forward) deterministic oldest-first draining per repo.
Verified against
src/settings/agent-sweep.ts(selectRegateCandidates) andsrc/queue/processors.ts(sweepRepoRegate) as they exist today. The candidate sort isrepairPriority || regateProgress || number, whereregateProgressfalls back throughlastRegatedAt→createdAt→ epoch(0). This is deliberately staleness-based (per the file's own header comment): a PR regated 2 minutes ago sorts after one regated an hour ago, regardless of PR number or creation time. PR number is only the final tiebreaker whenregateProgressvalues are exactly equal, which almost never happens once any PR has been swept at least once. Net effect: after the first sweep pass touches a repo, PR selection order for all subsequent sweeps is driven by "which PR I regated longest ago," not creation order.Separately, the per-PR fan-out stagger (
delaySeconds = Math.min(index * 10, 600)) does propagate the chosen sort's array order into dispatch order, but real-time webhook-driven review (untouched by any of this) can process/merge any PR at any moment regardless of sweep state — so a sort-key change alone cannot make cross-path ordering fully deterministic; it can only control the scheduled-sweep path's own selection order.Requirements
orderModeparameter toselectRegateCandidates, choosing between the existing staleness sort (kept as-is, unchanged default, for its documented convergence guarantee) and a new oldest-first sort keyed oncreatedAtascending (falling back to PR number whencreatedAtis missing).repairPriority(outage-repair PRs) still sorts ahead of both, unchanged.updatedAt-driven writes are suppressed (dry-run/paused). The new mode must preserve an equivalent convergence guarantee (verify via a test mirroring the existing staleness convergence tests: full backlog coverage inceil(open/max)ticks).regateSweepOrderMode: "staleness" | "oldest-first", default"staleness") so no existing repo's behavior changes silently, and the owner can opt specific repos in.Deliverables
src/settings/agent-sweep.ts:orderModefield onselectRegateCandidates's input, acreationOrderhelper, branched sort comparator.regateSweepOrderModerepo setting end-to-end (DB column + migration if settings are DB-backed, resolver default,.gittensory.ymlschema entry, OpenAPI regen per this repo's own contributing skill).src/queue/processors.ts: thread the resolvedorderModethroughsweepRepoRegate.test/unit/agent-sweep.test.ts: new test block mirroring existing staleness convergence tests (basic order, tie-break, multi-tick convergence,repairPrioritystill wins over both orderings).delaySecondssequence is monotonic in the chosen order.Expected outcome
Repos that opt into
oldest-firstget scheduled re-gate sweeps that process open PRs in creation-time order per repo per tick, with the same convergence guarantee the existing staleness mode already provides. Repos that don't opt in see zero behavior change. The change is scoped to scheduled-sweep selection order only, not a claim about cross-path (webhook vs. sweep) determinism.