Parent: #1936
Problem
The update_branch action (which merges the current base into head and would naturally re-trigger check-migrations.mjs/CI on a truly-rebased branch) only fires when liveMergeState === 'behind' inside the review-gating pass (maybeSkipForRebaseOrCi, src/queue/processors.ts) — not immediately before the merge action itself in runAgentMaintenancePlanAndExecute. The merge decision's own gate (mergeableClean = input.pr.mergeableState === 'clean', src/settings/agent-actions.ts) treats GitHub's mergeable_state as the sole base-freshness signal, and mergeable_state only detects git-level TEXTUAL conflicts — a base that has advanced with a new, non-conflicting sibling commit (e.g. a second PR's distinct-but-colliding migration file) still reads clean.
So a PR sitting green + approved + clean gets merged without ever being forced through a fresh rebase-and-CI-recheck cycle against whatever landed on main moments earlier from a concurrent merge. mergePullRequest pins the merge to the PR's own head SHA (protects against the PR's OWN branch moving) but has no equivalent base-freshness parameter — GitHub's merge API offers none, so this has to be enforced by the gate itself.
Requirements
- Add a config-driven
gate.requireFreshRebaseWindow (minutes, off by default) setting: if main has advanced within that window of the actual merge-decision moment, force an update_branch + fresh CI-recheck cycle before merging, rather than trusting a mergeable_state: clean read from before the base moved.
- Must not create a live-lock scenario where a high-velocity
main (frequent merges) causes a PR to never clear the freshness window — cap retries or fall back to a normal merge after N attempts with a clear audit trail.
- Reuse the existing
update_branch autonomy class and its existing write-permission/dry-run/kill-switch gate stack — this is not a new action class, just a new trigger condition for the existing one.
Deliverables
- The
gate.requireFreshRebaseWindow setting wired through the full config-as-code chain (migration, schema, type, DB round-trip, .gittensory.yml, OpenAPI, docs).
- A base-freshness check added to
runAgentMaintenancePlanAndExecute immediately before the merge action executes, using the same live-facts pattern as the existing refreshLiveMergeState.
- Tests: a PR merged normally when
main hasn't moved recently; a PR forced through update_branch when main advanced within the configured window; a bounded-retry test proving no infinite loop under a fast-moving main.
Acceptance criteria
- With
requireFreshRebaseWindow configured, a PR whose base advanced within that window in the moments before merge is rebased and CI-rechecked before the actual merge call, not merged on stale mergeable_state.
- Off by default — zero behavior change for any repo that hasn't opted in.
- No infinite-rebase-loop failure mode under a fast-moving
main.
Expected outcome
A PR can no longer merge on a mergeable_state: clean read that's technically true but stale relative to a sibling commit that landed on main in the intervening seconds — closing the base-side half of the same race that already has head-SHA pinning on the PR's own side.
Parent: #1936
Problem
The
update_branchaction (which merges the current base into head and would naturally re-triggercheck-migrations.mjs/CI on a truly-rebased branch) only fires whenliveMergeState === 'behind'inside the review-gating pass (maybeSkipForRebaseOrCi,src/queue/processors.ts) — not immediately before the merge action itself inrunAgentMaintenancePlanAndExecute. The merge decision's own gate (mergeableClean = input.pr.mergeableState === 'clean',src/settings/agent-actions.ts) treats GitHub'smergeable_stateas the sole base-freshness signal, andmergeable_stateonly detects git-level TEXTUAL conflicts — a base that has advanced with a new, non-conflicting sibling commit (e.g. a second PR's distinct-but-colliding migration file) still readsclean.So a PR sitting green + approved + clean gets merged without ever being forced through a fresh rebase-and-CI-recheck cycle against whatever landed on
mainmoments earlier from a concurrent merge.mergePullRequestpins the merge to the PR's own head SHA (protects against the PR's OWN branch moving) but has no equivalent base-freshness parameter — GitHub's merge API offers none, so this has to be enforced by the gate itself.Requirements
gate.requireFreshRebaseWindow(minutes, off by default) setting: ifmainhas advanced within that window of the actual merge-decision moment, force anupdate_branch+ fresh CI-recheck cycle before merging, rather than trusting amergeable_state: cleanread from before the base moved.main(frequent merges) causes a PR to never clear the freshness window — cap retries or fall back to a normal merge after N attempts with a clear audit trail.update_branchautonomy class and its existing write-permission/dry-run/kill-switch gate stack — this is not a new action class, just a new trigger condition for the existing one.Deliverables
gate.requireFreshRebaseWindowsetting wired through the full config-as-code chain (migration, schema, type, DB round-trip,.gittensory.yml, OpenAPI, docs).runAgentMaintenancePlanAndExecuteimmediately before the merge action executes, using the same live-facts pattern as the existingrefreshLiveMergeState.mainhasn't moved recently; a PR forced throughupdate_branchwhenmainadvanced within the configured window; a bounded-retry test proving no infinite loop under a fast-movingmain.Acceptance criteria
requireFreshRebaseWindowconfigured, a PR whose base advanced within that window in the moments before merge is rebased and CI-rechecked before the actual merge call, not merged on stalemergeable_state.main.Expected outcome
A PR can no longer merge on a
mergeable_state: cleanread that's technically true but stale relative to a sibling commit that landed onmainin the intervening seconds — closing the base-side half of the same race that already has head-SHA pinning on the PR's own side.