Repository navigation
A conflicting PR is never recovered: the rebase sentinel re-fires ~15x per PR reporting success while resolving nothing, and auto-rebase-retry has no manual trigger #1890
Description
Summary
A PR that goes CONFLICTING/DIRTY is never recovered by the fleet. The designed path exists and fires — auto-rebase posts an <!-- auto-rebase-conflict: --> sentinel, dev-lead-intent.sh maps it to emit_intent "rebase" — but it does not converge. It re-fires indefinitely, reports success, and changes nothing. There is also no way to invoke the retry path by hand, so an operator cannot even nudge it.
Both halves were hit on 2026-09-22 while unblocking #1799 and #1800.
Evidence — the automation fired ~15× per PR and resolved nothing
#1799 and #1800 became CONFLICTING on 2026-09-21T02:02–02:23Z (conflicts created by four unrelated PRs merging) and were still CONFLICTING at 2026-09-22T22:04Z — ~44 hours.
In that window:
| PR | auto-rebase-conflict sentinel comments |
|---|---|
| #1799 | 16 |
| #1800 | 15 |
Two of them landed at 17:20Z and 17:43Z on 2026-09-22 — after needs-human-review had been removed from both PRs, so the suppression gate (pr_resume_suppressed) was not what stopped them. The dev-lead runs triggered by those sentinels completed with conclusion: success while the PRs remained CONFLICTING. This is the false-success shape of #1567, on the rebase surface: a run that reports done having done nothing.
The net effect is worse than no automation: ~31 sentinel comments of pure noise across two PRs, each one an issue_comment event that spawns further dev-lead runs, while the actual blocker never moves.
Prior art: #865 — "rebase handler times out (exit 124) on hard conflicts instead of aborting; sentinel bursts" — landed as PR #1802 on 2026-09-21T00:18Z. The bursts documented above are all after that fix, so #865 did not close this.
The conflicts were mechanical, not semantic
This is the part that makes the gap indefensible. A human resolved both in minutes, and neither required judgement about intent:
scripts/dev-lead-fix-reviews.sh— the branch addedresolve_nochange_disposition_threads()immediately abovehas_hard_blockers, while main (dev-lead treats non-required failing checks as blocking — refuses to post comment dispositions, so pr-review can never approve (completes #1795) #1859) replaced that function's header and inserted the CI blocking gate. One adjacency conflict. Resolution: keep both sides, drop the branch's now-stale two-line comment.scripts/dev-lead-fix-issue.sh— the branch addedsource .../lib/issue-comments.sh(dev-lead reads only the issue title/body — issue comments never reach the prompt, so post-filing steering is silently discarded #1566) at the exact line where main addedsource .../lib/net-diff-guard.sh([#1620 slice 1] dev-lead refuses to open/update/claim a PR with an empty net diff #1786). Resolution: keep both lines.
Both are the "two sides independently added adjacent things" pattern. An agent that can implement a feature can certainly do this.
Half two — the retry path cannot be triggered manually
$ gh workflow run auto-rebase-retry.yml --repo petry-projects/.github-private
could not create workflow dispatch event: HTTP 422:
Workflow does not have 'workflow_dispatch' trigger
auto-rebase-retry.yml triggers only on workflow_run of "Auto-rebase non-Dependabot PRs" (types: [completed], gated on conclusion == 'failure').
Two consequences:
- No manual entry point. An operator watching a stuck PR cannot ask the retry path to run.
- A conflict is not in its scope at all. It retries failed auto-rebase runs — infrastructure flakes.
scripts/auto-rebase-retry.shsays so: "The Auto-rebase workflow handles merge conflicts gracefully (it posts a …)". A conflict is a successful run with a sentinel, so the retry workflow is never even considered.
And auto-rebase.yml, which does have workflow_dispatch, is pure git — zero engine references — so dispatching it cannot resolve a real conflict. The only manually-invocable rebase surface is structurally incapable of the job.
Acceptance Criteria
- A PR in
CONFLICTING/DIRTYstate converges without a human: either the rebase intent resolves the conflict, or it escalates once with a clear reason. The current outcome — neither resolving nor escalating, while re-firing indefinitely — is not acceptable. - The rebase intent cannot report success while the PR is still
CONFLICTING. Assert the post-condition against the PR's realmergeable/mergeStateStatus, not the script's exit code. A run that did not change the state reports failure. - Sentinel re-posting is bounded per head SHA. A PR must never accumulate 15 identical conflict sentinels; after N unsuccessful attempts at the same head, stop posting and escalate to
needs-human-reviewwith the reason. auto-rebase-retry.ymlgains aworkflow_dispatchtrigger taking a PR number, so an operator can invoke the recovery path directly.- Either the retry workflow's scope widens to cover conflict sentinels (not only failed runs), or a separate manually-invocable surface exists that routes a
DIRTYPR into the rebase intent. Document which, because today the naming implies a coverage that does not exist. - Mechanical adjacency conflicts — both sides adding independent, non-overlapping content at the same location — are resolved rather than escalated. The two cases above are the regression fixtures.
- Post-resolution integrity is verified before pushing:
bash -n,shellcheck, andcheck-duplicate-decls.sh(the [Phase 4] Post-conflict-resolution integrity check — catch duplicated/corrupted content before it's trusted #1482/[Incident] Trunk corruption: main's scripts carry 3–6x duplicated function definitions (merged via #1420, #1292) — surgical restoration required #1485 corruption class), since a bad conflict resolution has corrupted trunk here before.
Notes for the implementer
- Do not weaken the guard that stops an agent resolving conflicts it does not understand. The ask is to handle the mechanical class competently and escalate the rest loudly — not to make conflict resolution unconditional.
- The two PRs above were fixed by hand in commits
f828bbe9(feat: implement issue #1743 — Harness cannot resolve a bot review thread whose correct response is 'no change needed' — false positives block merge permanently #1799) ande11169ce(feat: implement issue #1566 — dev-lead reads only the issue title/body — issue comments never reach the prompt, so post-filing steering is silently discarded #1800); their diffs are the worked examples for AC feat: feature ideation framework — adversarial proposer/challenger/synthesizer #6. - Check whether the rebase intent is silently short-circuiting before it reaches the engine at all. It reported
successin seconds, which is not consistent with an engine having attempted anything.
References
- bug(dev-lead): rebase handler times out (exit 124) on hard conflicts instead of aborting; sentinel bursts #865 / PR feat: implement issue #865 — bug(dev-lead): rebase handler times out (exit 124) on hard conflicts instead of aborting; sentinel bursts #1802 — the earlier sentinel-burst fix; these bursts postdate it
- dev-lead reports status=applied / 'Changes committed and pushed' when the requested review changes were not applied #1567 —
status=appliedreported when nothing was applied; same false-success class, different surface - auto-rebase still merges main into every open PR on every push, though the ruleset no longer requires up-to-date branches — the treadmill's driver outlived the strict fix #1881 — auto-rebase merges main into every open PR on every push, which is what creates these conflicts at volume
- [Phase 4] Post-conflict-resolution integrity check — catch duplicated/corrupted content before it's trusted #1482 / [Incident] Trunk corruption: main's scripts carry 3–6x duplicated function definitions (merged via #1420, #1292) — surgical restoration required #1485 — post-conflict trunk corruption; the reason AC Workflow should skip no-op PRs in MAX_PRS count #7 exists
.github/workflows/auto-rebase-retry.yml,.github/workflows/auto-rebase.yml,scripts/dev-lead-intent.sh(~line 436)
Activity
github-actions commentedon Sep 22, 2026
Thank you for this exceptionally thorough report — the evidence table, exact timestamps, and worked conflict examples make the failure mode very clear. One question before implementation begins: you note the
dev-leadruns triggered by the sentinels completed in seconds withconclusion: successwhile the PR stayedCONFLICTING— could you share a link to (or paste the step-level log of) one of those runs so we can confirm exactly wheredev-lead-intent.shexits early, whether that is before the engine is ever invoked or inside it after a no-op? Knowing which short-circuit path is responsible will let us sequence the fixes in AC #2 and #3 correctly and write a regression test against the right exit point.don-petry commentedon Sep 22, 2026
Dev-Lead Implementation Plan
Issue: #1890 — A conflicting PR is never recovered: the rebase sentinel re-fires ~15x per PR reporting success while resolving nothing, and auto-rebase-retry has no manual trigger
Scope
Make the
rebaseintent converge (resolve the mechanical conflict class or escalate once, loudly), stop it reporting success while the PR is stillCONFLICTINGby asserting GitHub's realmergeable/mergeStateStatus, bound the failure loop into a singleneeds-human-reviewescalation, verify post-resolution integrity before reporting applied, and giveauto-rebase-retrya manualworkflow_dispatchentry point that routes aDIRTYPR into the rebase intent.Root cause
The rebase intent determined success from the engine exit code + a local trial-merge (
detect_conflicting_paths). GitHub'smergeablestate is computed asynchronously and can disagree with a locally-clean merge, so a run could reportstatus=appliedin seconds while the PR remainedCONFLICTING. Because it never recorded a failure, the #865 exhaustion counter never advanced and nothing escalated — the sentinel re-fired indefinitely.Implementation Checklist
scripts/lib/rebase-exhaustion.sh— add purerebase_conflict_state <mergeable> <mergeStateStatus>→resolved|conflicting|indeterminatescripts/dev-lead-fix-reviews.sh—pr_mergeable_conflict_state()polling wrapper (handles GitHub's transientUNKNOWN)scripts/dev-lead-fix-reviews.sh— rewire therebasesuccess branch to assert the authoritative post-condition (AC test issue from agent #1/Go-live improvements for PR review agent #2): still-conflicting →handle_rebase_failure(counts toward exhaustion), indeterminate → non-terminal retry, resolved → appliedscripts/dev-lead-fix-reviews.sh—verify_resolution_integrity()runningbash -n,shellcheck,check-duplicate-decls.sh; gate reportingappliedon it (AC Workflow should skip no-op PRs in MAX_PRS count #7)scripts/dev-lead-fix-reviews.sh—post_rebase_exhaustionalso addsneeds-human-reviewso the hold gate stops the loop after one escalation (AC Add @claude delegation, auto-merge, and rebase handling #3).github/workflows/auto-rebase-retry.yml— addworkflow_dispatchwith apr_numberinput (AC Optimize review: small-PR and incremental fast paths #4)scripts/auto-rebase-retry.sh— on manual dispatch route the PR into the rebase intent viarepository_dispatch(dev-lead-reviews-retry / intent_type=rebase) (AC feat: add Copilot engine support via REVIEW_ENGINE toggle #5)prompts/dev-lead/rebase.md— explicit guidance to resolve the mechanical "both sides added adjacent, non-overlapping content → keep both" class (AC feat: feature ideation framework — adversarial proposer/challenger/synthesizer #6)AGENTS.md— update theauto-rebase-retry.ymlexception note to document the widened (manual-conflict) coverage (AC feat: add Copilot engine support via REVIEW_ENGINE toggle #5)Tests to Write
tests/dev-lead/unit/test_rebase_exhaustion.bats—rebase_conflict_statetruth table (CONFLICTING / DIRTY / MERGEABLE / UNKNOWN / empty)tests/dev-lead/unit/test_auto_rebase_retry.bats— manualworkflow_dispatchwith a PR number fires the rebaserepository_dispatchand does NOTrun rerunTest Command
bats tests/dev-lead/unit/test_rebase_exhaustion.bats tests/dev-lead/unit/test_auto_rebase_retry.batsandbash .dev-lead/scripts/dev-lead-lint.shNote on scope / AC #6
The mechanical-conflict resolution itself is performed by the engine driven by
prompts/dev-lead/rebase.md; the deterministic, testable fixes here are the convergence/verification machinery around it plus the sharpened prompt. The two hand-fixed commits (f828bbe9,e11169ce) are the worked examples for the adjacency class.don-petry commentedon Sep 22, 2026
Dev-Lead — issue #1890 (in progress)
Implementation is on the branch
dev-lead/issue-1890-20260922-2207; CI opens the PR. This is a work-in-progress status note, not a completion report — the changes still need PR review and a live conflict-recovery run to confirm behavior end-to-end.What the change set does so far
Authoritative post-condition for the rebase intent. The rebase handler no longer infers success from the engine exit code or a local trial-merge — both lied here (a locally-clean merge that GitHub still reported
CONFLICTINGlet a run poststatus=appliedin seconds while resolving nothing). A new pure helperrebase_conflict_state(mergeable, mergeStateStatus)classifies GitHub's real state asresolved/conflicting/indeterminate, wrapped by a short polling read (gh pr view --json mergeable,mergeStateStatus) that tolerates the transientUNKNOWNright after a force-push. A run that did not change the state is now recorded as a failure, not success.Bounded escalation. A
conflictingverdict feeds the existing per-PR failure counter; once the threshold is reached the handler adds theneeds-human-reviewlabel (which the hold gate honors, so subsequent sentinels for that PR are skipped) and disables auto-merge. The loop converges to a single escalation with a stated reason instead of re-firing.Post-resolution integrity gate. Before reporting
applied, the resolved tree must passbash -n,shellcheck --severity=warning -x, andcheck-duplicate-decls.shon changed shell files. A failure escalates to a human rather than merging a possibly-corrupted resolution (the [Phase 4] Post-conflict-resolution integrity check — catch duplicated/corrupted content before it's trusted #1482/[Incident] Trunk corruption: main's scripts carry 3–6x duplicated function definitions (merged via #1420, #1292) — surgical restoration required #1485 class).Manual recovery surface.
auto-rebase-retry.ymlgains aworkflow_dispatchtrigger taking apr_number. Rather than widening theworkflow_runscope to conflict sentinels, the manual path routes a PR stuck inCONFLICTING/DIRTYstraight into dev-lead'srebaseintent via adev-lead-reviews-retryrepository_dispatch. Documented in AGENTS.md.Mechanical adjacency guidance.
prompts/dev-lead/rebase.mdnow instructs the engine to resolve (union, keep both) independent non-overlapping additions at the same location rather than escalate, while still escalating genuinely contradictory edits — with the two hand-fixed commits called out as the reference.Verification run locally
Unit tests for the two touched libraries pass (
test_rebase_exhaustion.bats,test_auto_rebase_retry.bats),dev-lead-lint.shpasses, andshellcheck/bash -nare clean on all changed scripts. Pre-existing, environment-dependent failures intest_engine_fallback.bats/test_fix_issue.bats(engine-binary/auth fixtures) reproduce identically with my changes stashed, so they are unrelated to this work.Still pending: PR review and a real conflicting-PR recovery to confirm the mergeable-state assertion and escalation behave as intended against live GitHub timing.
don-petry commentedon Sep 22, 2026
Dev-Lead: Implementation Complete — PR #1891
The implementation for issue #1890 is durable: commits are pushed and a pull request is open. This record is backed by verifiable artifacts you can confirm in one click.
2b3db240f963ba56d45a2edb26bc074dcb5b0f15main: 8 file(s), +359/-35 linesReview happens on the PR. Acceptance criteria and test results are evidenced by the PR's diff and CI checks — not asserted here. If a later stage fails, this claim is retracted in place.
feat: implement issue #1890 — A conflicting PR is never recovered: th…