Repository navigation
Run one merge poll chain at a time in the browser (#48) - #175
Merged
Merged
Conversation
prepare-merge now pushes a rebased head whose review is still clear over exactly the PR head it rewrote, before running command checks. A schema v18 marker (tasks.push_in_progress) is written before the first external write and cleared only after a read of the branch settles the outcome; while it exists, merge, runner admission, status changes, rebases and other pushes refuse. Preparation and a tracked startup job settle an interrupted push. GitBranchPusher gains an exact expected-head lease and a branch read; remotePair reports the task PR's branch. Part of #22 (F6a). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Push settlement reads now abort only on shutdown, so a cancel can no longer strand the marker; the in-run read is bounded by the remaining preparation deadline. Startup settlement runs under the verified runner lock and closes the PRs of a task it closed. After a push, preparation waits (bounded) until GitHub's PR API reports the pushed head, and a move seen then is reviewed from the pushed head. Adds regressions for each, plus production push wiring and server startup wiring tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitHub can report a moved base while its PR API still shows the pre-push head. The visibility wait now ends only when the head changes, so the review is never refreshed back onto a head the branch no longer holds. The test remote's branch read honours its abort signal, so a cancel that stopped push settlement would fail a test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The visibility wait starts no inspection after 30 seconds, so it lasts at most 30 seconds plus one inspection. Say so in the code and the docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cancel requested while no preparation runs and a push marker remains now starts the tracked settlement job, so the task and its PRs close without a restart. After a refused push, preparation waits for GitHub's PR API to report the branch's new head before reviewing it, as it does after a push. Renames settleAtStartup to settlePendingPush and corrects the architecture doc's not-built list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A preparation that fails waiting for GitHub to show the pushed head now binds its failure to the post-push task state, so it is not reported as stale at once. The startup-settlement server test now builds the state a crash leaves (a push marker plus a pending cancel) instead of having the fake job cancel the task itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repeating a cancel that waits on an unsettled push now reads the branch again instead of refusing, so the task can close without a restart. Adds push-boundary regressions for a pending cancel and a changed snapshot, and documents the retry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a push or a refusal, a moved PR head is now reviewed by tracing history from the reviewed head and its rewrite lineage, nearest first, so an earlier rewrite codeboost already pushed is reviewed against its own base instead of the original one. Any failure after the push or refusal is now bound to the post-push task state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds regressions for a plan revision applied at the push boundary (only the task state version changes, so that check is load-bearing), for the visibility wait ending at the preparation deadline, for the settlement read's deadline bound, and for shutdown aborting a startup settlement. States in the docs that a killed push may still land later, harmlessly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds regressions for a cancel during the next preparation's push settlement, attempt admission and reassignment refusing while a push is in flight, and shutdown starting no branch read. The push-boundary cancel test now cancels through the coordinator while the push reads the remote, as production does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed push's settlement read can take up to 30 seconds. The failure now adopts the task state version that clearing the marker produced only when nothing else wrote during the read; otherwise it stays bound to the claim's version, so a plan revision applied meanwhile marks it stale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Success, refusal and settlement now share one rule: the version clearing the push marker produced counts only when nothing else wrote since the claim; otherwise a later failure stays bound to the claim's version, so a plan revision applied while the push was in flight marks it stale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After the pushed head appears, preparation now requires the task state, review version and snapshot to be exactly what the push left before any cmd: check runs, so a plan revision or review edit applied during the push fails it instead of being adopted. The publish guard returns to its rebase-only form: publishing cannot overlap a push by status, which the docs now state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…serve settlement - The push claim uses the task state version captured before the asynchronous trust refresh, so a reassignment during it refuses. - Production returns the last guard as the pusher's synchronous finalizer, closing the gap between validation and the Git push. - A settlement read keeps the process-settlement reserve inside its limit and does not start with less than that reserve left. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the preparation's own deadline ends the push, less than Git's settlement reserve is left, so no branch read starts and the marker stays, as for an unreadable branch. The docs, the code comment and the test now say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pre-merge rebase moves the PR's base past base commits the pre-PR already-fixed check never saw. Merge inspections of a runner-published PR now scan from the base of the snapshot the last clear check ran on, excluding the ledger's owned commits. Adds the merge-handoff design. Part of #22 (F6b). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a runner, Merge PR now starts one merge job and returns. The job prepares (refresh, rebase, push, cmd: checks), waits up to 30 minutes for GitHub's required checks on the pushed head (pending or missing waits, a failure stops at once), then hands the exact pair to the guarded merge. A failing or slow check, or GitHub's refusal, leaves the task approved, merge blocked; review needs and other blockers end the job for a person. Jobs live in schema v19 merge_jobs, so their progress never moves the task state version that readiness is bound to; startup fails an interrupted job. The merge status offers Merge PR while only blockers the job resolves remain, and the browser follows the job. Part of #22 (F6b). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The job passes its signal into the guarded merge, so shutdown or a cancel stops it as it stops a click. A task cancelled while the job runs ends the job cancelled however the job learns of it. Adds regressions for the queued outcome, a busy refusal, retry after a blocked job, startup settlement and shutdown wiring; corrects the design doc. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The #22 acceptance cases through the job now run with a runner-published PR, so the guarded merge checks the readiness real preparation records. Another blocker now outranks a failing check, so checks failing on a collaborator's head end the job failed rather than blocked. Shutdown stops a merge job in its first synchronous step, before requests drain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Openings now record the clear already-fixed check they followed (schema v19), and the merge's scan starts at that check's base, so a re-publish from a rebased base cannot skip earlier base commits. Once the job's merge attempt exists, its recorded state decides the job's outcome, so a refusal that lands during shutdown still blocks. Adds server regressions for the cancel wiring and the settlement hook that closes PRs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The job saves its merge action ID before the guarded merge starts, so a restart settles it from its merge attempt. A pre-v19 opening scans from the latest clear check. A merge conflict caused by a moved base no longer disables Merge PR: the job's rebase resolves it. Moves a misplaced comment back to its method. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F6b is the first code to move a task to approved, merge blocked; start now accepts it as it accepts a task in review, so an amended plan can fix what blocked the merge. The cancel tests now cancel the task before telling the job, and the server test's preparation holds a real push marker, as production does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Revert starting an amended plan from approved, merge blocked: a blocked task always has runner commits, so the existing guard refuses that start from review too; the doc now points to the scope-amendment flow. Blocking now ends the job and moves the task in one Store transaction. Adds regressions that a retried job is never decided by an earlier refused attempt, in process or at startup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merge's already-fixed scan now starts at the base-branch tip the PR's own pre-PR check scanned through, the tightest sound start. The check's 250-commit cap still fails closed for long-lived PRs, as it did before any rebase; the docs say so, and that failing required checks keep Merge PR available. Adds tests that blocking advances the task state version and that a click during a running preparation is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A pre-v19 opening scans from the task's oldest clear check: a compare range excludes its start, so only that start is provably continuous. - A definite admission refusal is saved under the click's action ID and replayed on resend; only a shutdown refusal stays resendable. - No required-check inspection starts after the 30-minute limit, and one running into it is stopped, so a job never merges after its limit. - While a job runs, the focused Merge button stays enabled and marked aria-disabled; a repeat activation sends nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A refusal saved for a second click while a job ran became the newest merge_jobs row, so the browser saw the running job as ended and Merge PR was offered again. latestMergeJob now skips refused rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merge route admitted a job without the trust read prepare-merge performs. It now reads issue access first: an untrusted click is refused and the refusal saved; a failed read saves nothing. Docs: the wait's limit bounds which checks count, not when GitHub is asked to merge; merge_jobs also holds refused clicks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mport does meanwhile A run admitted during a merge job's preparation or check wait (after a plan import and approval) moved the head and published under the job. Runner admission now refuses, as prepare-merge does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onse job guard A render while a Merge PR click's request is in flight (selecting an item, a plan reload) re-derived the control from merge.ready and offered "Merge PR" again. A click-pending flag now keeps it marked and labelled "Requesting merge…" until the request settles. Add browser regressions for the newerMergeJob guards at the review action, Plans refresh, plan import and Apply draft reload sites, and give the two no-reload checks time for a background reload to arrive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k's latest run GitHub counts a required check run that completed NEUTRAL or SKIPPED as passing; the inspection read both as failure, which now ends a merge job blocked. When a check has several runs with one name (a re-run), the first listed won, so an older failure could outrank a newer pending or passing run. The run that started last now counts (a run not started yet counts as the latest; on a tie, the one that passes least). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the limit's own abort reason classifies an inspection as the wait timing out; a stage failure that settles after the limit ends the job failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ecord A full review response held across another tab's click no longer replaces a running newer job with an earlier ended one: jobs carry a sequence, and a kept running job keeps Merge PR not ready. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the page learns a merge job ended (its poll, the click's answer, or a newer record kept over an older full response), the review it holds was read before or during the job and still lists preparation, base, cmd: check, required-check and mergeability blockers. Drop those, keep the job-refresh blocker and unrelated ones, and label the control with the outcome, as a merge-queue merge reads. A job's Retry confirmation no longer promises the exact reviewed head: it prepares again and may rebase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e server's list An ended merge job hid preparation, base, acceptance, check and mergeable blockers whatever its outcome, though a blocked, failed, review-required or cancelled job settled none of them for certain (a failed job reports only its first blocker, so a merge conflict stayed hidden until Refresh). Now only merged, queued and submitted jobs, which ask GitHub to merge after no blocker remains, drop them; every other outcome keeps the blockers the page last read. The browser no longer keeps its own copy of the server's JOB_RESOLVES: the merge status carries settledBeforeJobMerge, built from JOB_RESOLVES plus mergeable, and a unit test pins it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the browser test made Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # docs/implementation/merge-handoff.md # github/merge.ts # runner/merge-job.ts # runner/merge.ts # runner/store.ts # test/browser/review.spec.ts # test/merge-job.test.ts # test/merge-task-pr.test.ts # test/merge.test.ts # test/runner-production.test.ts # test/runner-publishing.test.ts # web/public/app.js # web/server.ts
The control already read "Merge queued" before poll 2 answered, so the check proved nothing; a third poll shows poll 2's answer was applied and the one chain continued. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing. Review the Copilot code review billing settings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #48: the browser part. The server part merged as #171. This PR was stacked on F6b (#161), which rewrote merge polling in
web/public/app.js; F6b is now merged (b4a1977) and this PR targetsmain.Defects
Both were reproduced on F6b at 2a9534d:
renderMerge()callsscheduleMergePoll()on every render. While a poll was in flight its timer was already cleared, so any render (for example selecting another item) started a second poll chain in the samemergeGeneration. Nothing ordered the two answers, so an older "queued" answer could be applied after a newer "merged" one, and the control went back to "Merge queued".Fix
One merge poll chain at a time (
web/public/app.js):mergePollInFlight = { generation, sequence }is set when a poll starts.scheduleMergePoll()starts nothing while a poll of the current generation is in flight; that poll reschedules itself when it settles.ready, which excludes a running job or an active attempt, so no timer is pending then (traced, not tested; removing it fails no test).docs/implementation/merge-queue.mddescribes the single poll chain.Traced, not tested:
/api/merge.Tests
test/browser/issue-48-poll-ordering.spec.ts:test/browser/issue-48-stale-errors.spec.ts:Break checks:
app.js, the specs fail with 2 polls where 1 is expected.F6b merged into
mainas squash commitb4a1977.6744f95brings that in: since the squash duplicates F6b's commits, the merge takesmain's tree and re-applies this PR's own diff (0857fa2→792581a) on top withgit apply -3; its diff againstmainis exactly this PR's four files, identical to the diff it had against F6b.121769cstrengthens one assertion and a later commit labels the click's timer clear defensive (below).Validation
Current head
47d496b, basemainb4a1977(47d496bchanges a comment and a doc sentence only since121769c):test/browser, one worker)6744f95; the cross-generation spec passes on121769cnpm run typecheck6744f95test47d496bEarlier checkpoints:
8bdeaf9on F6bcffdf4c;d21cb0eon F6b7b1b5fa(CItestpass; Copilot 2 clean);ec2e2e4on F6b2a9534d(browser 102/102, CItestpass).Review record
settle()applying every answer, the test fails.settle()applying every answer, the new check alone fails. Removing the generation check fails three existingplans.spec.tstests.settle()accepts it; a full response that keeps a newer job record starts no second applied chain.b4a1977/6744f95(new basemain)settle()). Findings: this body was stale (fixed here); the cross-generation spec's last assertion could not fail, since the control already read "Merge queued" before poll 2 answered — fixed in121769cby waiting for a third poll (fails with 2 polls when a settled answer stops the chain).b4a1977/121769csettle's sequence check,current ??= settle()and the new third-poll wait each fail a spec (settle's generation check fails threeplans.spec.tstests). One finding: the merge click's timer clear is untested and unreachable today (a click needsready). Labelled defensive in the code,merge-queue.mdand this body.b4a1977/47d496bwithMergeJob,#withAttempt,statusAfterMerge) and every browser path that replacesdata.merge; mutation checks match each break-check claim.b4a1977/47d496b47d496b. Blocked: Copilot did not review — the requesting user's Copilot review quota is used up. Not a clean result; re-request once quota is available.b4a1977/47d496bReview-lesson audit
A second poll chain started by a render: covered by AGENTS.md "Never apply a background response without proving it is still current", and the race "Poll starts, then a user action completes, then the old poll returns".
The swallowed error and stopped polling: covered by "Preserve the original timeout, cancellation, and shutdown reason through every layer. Do not replace actionable errors…".
Specs that did not assert their interleaving, or could hang: covered by "A race regression must assert that the interleaving it names actually happened" (added in Keep merge stop reasons and GitHub answers through every layer (#48) #171).
The merge click timer and docs: covered by "Treat every behavioural claim … as something to verify".
An assertion on a banner that a re-render had already cleared (round 6): covered by "A race regression must assert that the interleaving it names actually happened" and the Design system and plan format #1 rule of E2E tests.
No new rules.
🤖 Generated with Claude Code