Repository navigation
Conversation
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>
There was a problem hiding this comment.
🟡 Changes recommended
Merge idempotency, legacy scan safety, deadline enforcement, and keyboard-focus handling have unresolved defects.
4 open findings
What changed in this PR
Adds F6b’s durable one-click merge workflow for runner-published pull requests.
Changes:
- Adds merge-job preparation, required-check polling, guarded merge handoff, cancellation, and recovery.
- Extends schema v19 with merge-job state and already-fixed scan bindings.
- Adds browser status reporting, documentation, and regression coverage.
| File | Description |
|---|---|
web/server.ts |
Wires merge jobs into API and shutdown. |
web/public/app.js |
Displays and polls merge-job progress. |
runner/merge-job.ts |
Implements the merge-job lifecycle. |
runner/merge.ts |
Adds abort handling and scan ranges. |
runner/store.ts |
Adds schema v19 persistence. |
runner/production.ts |
Settles interrupted jobs at startup. |
github/merge.ts |
Supports bounded already-fixed ranges. |
test/merge-job.test.ts |
Tests merge-job lifecycle behavior. |
test/runner-publishing.test.ts |
Tests server integration and cancellation. |
test/pre-merge.test.ts |
Tests acceptance scenarios end-to-end. |
test/merge-task-pr.test.ts |
Tests published-PR scan ranges. |
test/merge.test.ts |
Tests merge adapter inputs. |
test/runner-production.test.ts |
Tests startup recovery. |
test/browser/review.spec.ts |
Tests browser job progression. |
test/store.test.ts |
Updates migration expectations. |
test/runner-lifecycle-store.test.ts |
Updates schema expectations. |
test/publish.test.ts |
Updates schema expectations. |
test/planning-drafts.test.ts |
Updates schema expectations. |
docs/implementation/merge-handoff.md |
Documents the new workflow. |
docs/implementation/guarded-merge.md |
Updates guarded-merge behavior. |
docs/architecture.md |
Records F6b architecture status. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- 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>
…iled jobs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ify the scan-range claim In job mode the button is marked aria-disabled, never disabled, so a refusal or a job's end keeps keyboard focus. The docs now say that status shown while a merge attempt is in flight scans from the PR base. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The #merge primary rules outranked the aria-disabled rule, so in runner mode a button that could not act kept the primary look and hover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reloading the whole review from the background poll erased the person's selection and input. The page now shows the outcome on the merge control and banner and asks for Refresh, as the merge-queue poll does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…opening merges A new opening after a pre-merge rebase (the person closed the PR, then published again) scanned from its own check, which started at the rebased base, so base commits the rebase brought in were never scanned. Every runner-published PR now scans from the tip the task's first clear pre-PR check reached. The per-opening check_id column is no longer needed and is dropped from schema v19. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Starting every scan at the task's first clear check broke tasks whose github.baseBranch changed: that check scanned another branch, so the compare diverged and merging stayed blocked for good. Each opening again records its pre-PR check (schema v19), and the scan starts at the check of the task's first opening into the current base branch; a pre-v19 first opening falls back to the oldest clear check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… not The first opening into the current base branch may follow a rebase (the base branch setting changed after one), so its check scanned only from the rebased base. Its tip is now used only when that check scanned from the task's original base (its first snapshot's); otherwise the scan starts at the original base. 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>
…nsaction blockMergeJob now leaves the task alone when the job already ended. A test shows a failure to end the job rolls the task's move back. The github/merge.ts comments now describe the runner-chosen scan start. 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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failing or slow check read before a review edit no longer moves the changed task to approved, merge blocked: the block re-checks the preparation's readiness in its transaction and otherwise ends the job failed. A click refused because the server is shutting down (including a preparation stopping for it) now answers 503, so the browser keeps the click's key. Comment wording fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…down stops a merge click A refusal that needs a fresh review (the head changed) no longer moves the task to approved, merge blocked, live or at startup. Shutdown stopping a merge click's trust read, or its reload after the job was saved, now answers 503 so the browser resends the same click. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also names schema version 19 in the architecture doc. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| // A conflict after the base moved is the job's too: its preparation rebases, through the sandboxed conflict path. | ||
| const rebaseResolves = status.remote?.mergeable === 'CONFLICTING' && status.blockers.some(blocker => blocker.code === 'base'); | ||
| const personal = status.blockers.filter(blocker => !JOB_RESOLVES.has(blocker.code) && !(blocker.code === 'mergeable' && (computing || rebaseResolves))); | ||
| const ready = !running && !attempt && status.remote !== null && personal.length === 0; |
Comment on lines
+2198
to
+2202
| if (task.status === 'in review' && !this.#activeAttempt(key) && !this.#activeMerge(key) | ||
| && task.rebase_in_progress === null && (task.push_in_progress === null || task.push_in_progress === undefined)) { | ||
| this.#run('UPDATE tasks SET status=? WHERE plan_key=?', 'approved but merge blocked', key); | ||
| this.#touch(key); | ||
| } |
| data = { ...data, merge: { ...data.merge, ready: false, action: null, job, blockers: [{ code: "job-refresh", message: ended }, ...blockers] } }; | ||
| $("banner").textContent = ended; | ||
| renderMerge(); | ||
| return; |
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.



Summary
F6b of #22, stacked on F6a (#153): one-click merge. With a runner, Merge PR starts one merge job and returns; the job prepares, waits for GitHub's required checks, and hands the exact pair to the guarded merge. Design:
docs/implementation/merge-handoff.md.runner/merge-job.ts): admits the click (fresh token, mergeable task, issue trust read from GitHub, nothing busy; a definite refusal is saved and replayed to a resend), records its ownprepare-mergeaction and runs preparation (refresh, rebase, push,cmd:checks); then inspects the PR (fresh) at once and with backoff (10 s → 60 s) for up to 30 minutes. Pending or missing checks and mergeability GitHub is still computing wait; a failing check or the 30-minute limit ends the jobblocked; any other blocker (PR moved, readiness lost, already-fixed) or a failed GitHub read ends itfailedand outranks a failing check. No inspection starts after the limit, and one running into it is stopped. Then it saves a merge action ID and calls the guarded merge (MergeCoordinator.merge, which now takes the job's abort signal).merged,queued,submitted,blocked(task → approved, merge blocked, in one transaction with the job),review-required,failed,cancelled. Once the job's own merge attempt exists, its recorded state decides the outcome.merge_jobskeeps job progress off the task, so the readiness preparation records stays bound while the job waits. Startup fails a job that had not asked GitHub to merge, or settles it from its own merge attempt.withMergeJob);GET /api/mergereturns the task's latest job (saved refusals excluded);prepare-mergeis refused while a job runs; cancel stops the job, and its PRs close when the task ends cancelled; shutdown stops the job in its first synchronous step.aria-disabled, never disabled, so it keeps focus; when the job ends, only the merge control and banner change ("Refresh to see the result"), so selections and input survive.F6c (common CI / T9) comes next.
Validation
Exact pair: base
f3bc3e1176c9c7436e82faa216b67582854e9fb5(current F6a head, which containsmain22e76a8), head2c5ccc3704116303a6d0a53e782c13b613ee7349(current head; F6a merged in).f3bc3e1..2c5ccc3(round 29), including the integration with F6a's new commits andmain's Size the plan import request limit from the parser bound (#146) #158, Close out Lane G status records (#145) #154 and Read an oversized body before refusing it, and close on early refusals (#157) #160; 10 focused files, 740 of 740 tests passed; merge browser tests 15 of 15pre-merge.test.tsat load average ~195) passed alone;npm run typecheckpassedcbacd19(base old F6a9e1f695): independent review clean (round 28); 8 focused files, 679 of 679 passed777c9b5: full non-Docker suite 75 files / 2,079 passed; browser suite 139 of 139; CI and real-Docker passedKnown limitations
pull-request-opening.md).failedrather than retrying (fails closed; documented and tested).Review ledger
4a3d23efailed; a cancel without the job's abort could strand it; untested claims (queued, busy, retry, startup and shutdown wiring); a non-existentmergingresponse in the doc7cc6ab27cc6ab279b7fd579b7fd5e9575aae9575aafailed; a misplaced commentffb995effb995e922007f(start change later reverted)922007fdef4b0adef4b0a19d5abc(scan starts at the pre-PR check's scanned tip; limit documented)19d5abc19d5abc18fc56818fc568latestMergeJob; stalestart()docstring727f279727f279prepare-mergeperforms; two doc claims (limit vs merge time,merge_jobsrows)db189aedb189ae841e395841e395fc57d20,76c81f276c81f2368495c368495caria-disabledMerge button kept the primary look9e0ee399e0ee3912340911234091ab74aecab74aecbe4f391be4f391f6eb190f6eb190887f0cd,7302d687302d68github/merge.ts; the one-transaction block claim untested (also: a job that already ended no longer moves the task)b3ba376b3ba3764ea87244ea8724329327832932782e3e860(unit test onclose(); server test narrowed)2e3e860777c9b5777c9b5777c9b5018025601802567180aad7180aadcbacd19cbacd192c5ccc3f3bc3e1(withmain22e76a8) with no conflicts; architecture doc now names schema 192c5ccc3Declined: none. Every regression added from round 9 on was shown to fail with its fix removed (mutation probes). Round 5's finding was accepted, then shown by round 6 to rest on a false premise; its change was reverted and the doc corrected.
Review-lesson audit
AGENTS.mdAGENTS.mdGate status
f3bc3e1176c9c7436e82faa216b67582854e9fb5/ head2c5ccc3704116303a6d0a53e782c13b613ee73492c5ccc3; passed on777c9b519d5abc) and 2 (777c9b5) addressed; round 3 requested only after exact-head CI passescodex/f6a-rewritten-head-push, F6a: push the rewritten head behind a durable marker #153) atf3bc3e1, which containsmain22e76a8. After F6a: push the rewritten head behind a durable marker #153 merges, this PR is retargeted tomainand re-reviewed and revalidated.🤖 Generated with Claude Code