Skip to content

Commit b4a1977

Browse files
mchwangclaude
andauthored
F6b: one-click merge job with a required-check wait (#161)
* F6a: push the rewritten head behind a durable marker 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> * Settle pushes cancel-safely and wait for the pushed head 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> * Wait for the pushed head even when the base moves 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> * State the pushed-head wait bound exactly 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> * Settle a stranded push on cancel; wait for the PR head after a refusal 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> * Bind a post-push visibility failure; test startup settlement faithfully 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> * Retry push settlement on a repeated cancel; test the push boundary 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> * Trace a moved PR head through the whole rewrite lineage 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> * Test the push boundary's state check and every settlement bound 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> * Cover cancel-safe settlement, admission and shutdown reads 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> * Test that a failed push is reported as current Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bind a failed push to the state its own settlement left 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> * Bind every push exit to the state that push left 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> * Refuse checks after a push if the task changed while it was in flight 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> * Describe the pusher's expected-head rule in its doc comments Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the push claim's admission guards and the busy settlement skip Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address Copilot: capture the claim version, finalize at Git start, reserve 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> * State that a deadline-ended push leaves its marker unread 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> * F6b: scan for an earlier fix from the last clear check's base 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> * F6b: one-click merge job with a required-check wait 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> * Stop the job's guarded merge on shutdown; end cancelled at every step 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> * Test the readiness hand-off for real; stop jobs as shutdown begins 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> * Scan from the PR's own pre-PR check; settle a job from its attempt first 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> * Settle a restarted job from its attempt; offer merge for a base conflict 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> * Let a merge-blocked task start an amended plan; fix cancel test setups 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> * Block job and task together; decide a job only from its own attempt 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> * Scan from where the pre-PR check stopped; document the 250-commit limit 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> * Address Copilot: continuous scan, replayed refusals, strict wait, focus - 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> * Keep saved merge refusals out of the task's latest job 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> * Admit issue trust before a merge job starts; correct two doc claims 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> * Say exactly when shutdown stops a merge job's preparation Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Correct the task status the merge-handoff doc gives for queued and failed jobs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test that a retried job's queue leaves the task approved, merge blocked Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the Merge button focused when a merge-job click is refused; qualify 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> * Draw an aria-disabled Merge button as inactive 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> * Update only the merge control when a merge job ends 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> * Scan for an earlier fix from the task's first clear check, whichever 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> * Scan from the first opening into the current base branch 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> * Scan from the task's original base when the first opening's check did 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> * Explain correctly why the already-fixed scan covers a rebase's commits Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Reflow the already-fixed range comment Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Block a task only with its still-running job, and test the shared transaction 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> * Test a queued attempt at startup and a failed GitHub read while waiting Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test that shutdown awaits a merge job before the store closes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test that closing the merge job waits for it to settle Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Name the pre-v19 fallback in the guarded-merge scan description Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Name schema version 18 in the architecture doc Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address Copilot: block only while readiness holds; shutdown answers 503 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> * Fail the job when GitHub refuses a changed head; answer 503 when shutdown 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> * Name the shutdown reload test for what it checks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address Copilot: a branch read never waits on an escaped pipe holder Production readBranch ran Git without allowUnsettledReturn, so a process that left Git's group while holding its output could hold the push settlement read, startup recovery, a repeated cancel or shutdown without bound. A read changes nothing, so it now returns failed after the bounded drain, and the push marker stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address Copilot: bound only escaped pipe holders on branch reads allowUnsettledReturn also stopped waiting for processes still in Git's group, which a read has no durable ownership of. A new process-group option, boundEscapedOutput, bounds only the wait for a process outside the group that holds the output; group members are still waited for, fail-closed. Branch reads use it. Tests cover both cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address Copilot: closed tasks never offered, refusals bound to their review, neutral poll message withMergeJob now requires a mergeable task status read after the inspection. A GitHub refusal blocks the task only while the review, snapshot and plan revision are those the merge attempt was admitted for; otherwise the job ends failed (live and at startup). A failed job poll says 'Could not refresh merge status'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Say which branch-read case the settlement bound excludes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Name both unbounded subprocess cases, and an inspection's full settle time Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Name the review-change case in the merge-handoff shutdown row Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bound cat-file reads like ls-remote; correct stale rebaser doc line cat-file only reads, so a process that left Git's group but holds its output no longer holds fetchCommits or the push's head check: the call fails after the pipe drain. The reserve comment now lists the Git calls that still wait (fetch, update-ref, push). The pre-merge-rebase doc no longer says no production request starts the rebaser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address Copilot: review a collaborator head found after an unknown push outcome When the push's read-back fails because a collaborator moved the branch, the settlement read now returns moved and preparation reviews that head (review-required, attribution and approvals refreshed), as after a refusal, instead of failing with the read-back error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test admission guards; say a retry after a refused merge and a rebase needs review Tests now cover the job's refusal (saved and replayed) for a task that is not mergeable or is being cancelled. When a refused merge attempt is followed by a new snapshot (a retry's preparation rebased), the blocker asks for every item to be approved again instead of repeating GitHub's old refusal text; the doc names the path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the retry after a refused merge and a rebase through real preparation The earlier test used fakes that kept items approved after a rebase, so it claimed the retry ends failed. Through real preparation the items go stale and the retry ends review-required; the merge status then asks for every item to be approved again. Doc corrected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Word the replacement-review blocker by its cause A plan revision without a new snapshot no longer reads as a replacement snapshot: the blocker names the plan. Existing queue tests now assert both messages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address Copilot: recheck the push binding before reviewing a refused or moved push A plan revision or review edit that landed during the wait for the new head (or the fetch) was adopted when the observed head was recorded. The binding the push left is now checked before and after the fetch, as on the pushed path; such a change fails the preparation as stale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address Copilot: new key after a job ends, keep in-flight drafts, answer the latest job The browser drops a retained merge key once the job it started has ended, so Retry merge is a new action rather than a replay. A merge click keeps what was typed while its request was in flight. The merge route answers the job's latest record, which may have ended while the review reloaded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the binding check after the fetch; document the stale outcome Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test that the binding check before the fetch runs first Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drop a retained merge key once another job is the latest One job runs at a time, so when the latest job is not this key's job running, the key's job has ended or never started: a click is a new action rather than a replay of an old job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refuse a push whose post-rebase review reload adopted a change (F6a, #170) Since #170 the review reload after a rebase awaits a history worker. A reassignment or review edit landing during it was adopted into the push's binding, so the push could run under a state the caller never checked. The task and review the rebase left are now checked again after the reload. Regressions for both cases fail without the check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Guard every preparation reload and report its refusals as stale (F6a, #170) Since #170 each review reload in a preparation awaits a history worker. The reload after a command check adopted a reassignment into readiness, and every reload recorded the post-await state before its guard ran, so a refusal was reported as current rather than stale. A guarded reload now checks the state it must still match before recording the view; every reload uses it. Regression for the command-check reload, and stale assertions on every reload refusal; both fail without the change. Both gaps also exist on main since #170. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bind preparation failures as current or stale by their cause (F6a, #170) - A failure of the review reload after a rebase (a worker timeout) is bound to the state the rebase left, so it is reported as current. - A command check that a reassignment settled as stale no longer adopts the reassigned state, so its failure is reported as stale (pre-existing since F5). - Regressions for both, and for a change request added while a check runs, which is refused as stale rather than adopted as review-required. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refuse a command check whose context changed after it settled (F6a, #170) runner.settled() resolves only after the runner releases the check's workspace. A reassignment or plan change in that window left the attempt completed and was adopted into readiness. The check's state is now adopted only while its context equals the current one; otherwise the preparation is refused as stale. The test fixture's failing load now rejects after the real load settles, as a worker failure would, so no abandoned load rejects after the test. Docs note that a refresh's closing reload is not guarded and why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give the preparation reload rules their own doc section Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restore the blank line before the push table Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Settle merge job admission and reload races introduced by #170 (F6b) - Once a click's job is saved, a failure of the click's own review reload (which now awaits a history worker while the job prepares) answers 503, so the browser keeps the key and the resend returns the job, instead of an error that dropped it. - A cancel during one of the job's own review loads ends the job `cancelled`, not `failed`. - The unit fixture's review load is asynchronous, as in production, and can be held; regressions cover a resend during the admission load, shutdown during it, a load stopped by shutdown, a load that fails for another reason, and a cancel during the job's load. Each fails when its fix is removed. merge-handoff.md describes the admission load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Follow a started job when its click cannot reload the review (F6b) - A click whose job was saved but whose review reload then failed is answered with the job and mergeRefreshRequired, not 503: the browser never resent that click, so it showed "Merge blocked" while the job ran. The page now keeps its review and follows the job (new browser test; it fails with the previous app.js). - The job's first step binds its preparation to the task and review read in one turn after its review load, ends at once on a cancel or shutdown during that load, and checks that a preparation can start before saving the preparation's placeholder. - The cancel regression cancels the task first, as the route does. - merge-handoff.md: the check wait's local review load, and the click answer after a failed reload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Ask for a refresh when a click's merge job has already ended (F6b) The click's answer carries the job's latest record, which can already be ended (for example when its first review load failed as the click's reload did). The page now shows the same job-refresh blocker and banner as the job poll does when a job ends, instead of a silent stop. Browser regression; it fails with the previous app.js. merge-handoff.md now describes the click's answer and when merge jobs are used accurately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give the ended-job browser test a runner server's merge view Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Wait for the Merge button before pressing it in the refused-click focus test (F6b) The review now loads asynchronously (#170), so the press could land before the page offered the merge, and the test failed every time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give a second merge request time to arrive before counting clicks (F6b) The focus test counted merge requests straight after the second press, before a request could reach the route, so it passed even without the not-ready guard. It now fails with 2 requests when the guard is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Await the merge job's admission in the check-wait limit test (F6b) start() is async since #170; an unawaited rejection would surface as an unhandled rejection instead of a test failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refuse a rewritten-head push once shutdown has begun, and kill every escaped test process Server shutdown sets the runner's admission flag in its first step but closes the pre-merge coordinator only after the HTTP drain. The push boundary and its failure-path branch read now check the runner's flag, so no push or new GitHub read starts in that interval. The cat-file escape test appends each holder's PID, so both detached sleeps are killed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Answer merge-job clicks from durable records after failed reads and unsaved outcomes (F6b) A fulfilled guarded merge whose outcome could not be saved leaves its attempt submitting: the job now ends submitted, not merged or queued. An admission load or trust read that fails after a resend of the same click was admitted answers that job, and a trust read's own timeout or gh answer survives a shutdown that started while gh stopped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep a retry's key past an earlier job and newer job observations past held reviews (F6b) A key made while an earlier job is shown stays until its own job ends or a newer job is the latest, so an unanswered retry is resent with the same key. A full review response older than the poll's record of the same job keeps that record. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Scope the shutdown comment on the push failure path to its own read Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Log a merge job that could not be ended, and document that it stays running until restart (F6b) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Never reject a merge job's run, log quoted, and test the restart that settles an unended job (F6b) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Log, not reject, when following a settled merge job fails; test the shutdown log The server's `.finally` after a merge job read the task; a throw there rejected a new promise unhandled and exited the server. Catch and log it. Add a regression for a job whose ending write fails during shutdown, and state in merge-handoff.md that the log is written then too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test that the wait between required-check inspections doubles to its cap Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Describe the merge_jobs table as stored: id, plan_key and a JSON record Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Scan from the original base for a pre-v19 opening, never another branch's check No record shows which base branch a check recorded before schema v19 scanned: a publish can record its check and stop before opening, so the task's oldest clear check may belong to another base branch. Start the legacy scan at the task's original base instead, which covers every base commit, and drop the now-unused Store.oldestClearAlreadyFixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Pin that publish is refused while a merge job runs, and document why Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refuse start and resume while a merge job runs, and state what plan import 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> * Describe getAlreadyFixed as the look-up by check ID it is Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the Merge button pending across renders, and pin every full-response 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> * Count neutral and skipped required checks as passing, and read a check'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> * Keep a GitHub failure that precedes the required-check wait limit 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> * Order merge jobs by a per-task sequence when keeping a poll's newer record 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> * Show a merge job's outcome, not the blockers it resolved, once it ends 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> * Drop job-settled blockers only after a job reached the merge, from the 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> * Pin the served settled-codes field on a runner server, not on a body the browser test made Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Word merge-job messages as what the person does, and follow another tab's job - With merge jobs, a moved base and missing preparation read as what Merge PR does (it rebases and prepares), in the served status and in a failed job's reason, instead of asking for a Rebase or Prepare the screen does not offer. - A blocked job's reason ends with its next step: wait after the 30-minute timeout, fix after a failing check or GitHub's refusal. - Failed reasons no longer say "merge again"; the page adds it once. - The running job's dialog names the step as the Merge control does. - A refused merge click carries the task's latest job; a page with an older review (a second tab) follows a later running job instead of asking for a refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Word a merge job's guarded-merge refusal and an unsettled push in job terms The guarded merge's refusal now carries its blocker (MergeBlocked), so a job that fails there words the reason as what merging does; the direct path keeps its own text. The unsettled-push preparation blocker keeps its detail in job mode instead of reading as an unprepared merge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test that a refused merge click follows only a running, later job The server attaches the task's latest job to every merge-job refusal; an ended job, or a running record no later than the one shown, asks for a refresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Require both a check run and a commit status that share a required check's name GitHub requires both to pass when a required name has both. Take the latest observation of each kind and let the one that passes least decide, so a newer passing status no longer hides a failed or pending check run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Order records of one merge job by a durable revision, not its update time Two saves can share an update time, and the clock can step back between them, so a held Refresh response's running record could look as late as the outcome a poll saw and restore the running job. Every saved change to a job now adds one to its revision, and the browser compares revisions within a job (sequence across jobs, as before). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * End a merge job the poll no longer reports without a TypeError When /api/merge answered job: null while the page showed a running job, withEndedMergeJob read job.state and threw, so the banner showed "Could not refresh merge status" and polling repeated forever. Treat a null job as not known to have reached the merge: show the ended message, keep every blocker, and stop polling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Decide merge job endings from cancel, attempt state and the wait clock - Check a committed cancel before shutdown when the job's work rejects, so a cancel followed by shutdown during the job's own review load ends it `cancelled` instead of leaving it running for startup to fail. - End the job from its own merge attempt after a fulfilled guarded merge with the same mapping as a rejected one, so a queue poll that records `removed` or `failed` before the job resumes blocks it (and the task) rather than reporting `submitted`. - Treat a ready inspection that finishes after the check-wait limit, before its timer runs, as the wait timing out; failing checks still decide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Say the checks passed after the limit when a late ready inspection times out Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tighten merge job cancel, readiness and startup settlement tests The cancel test now waits far longer than it allows, so only the job being told can end the wait, and checks no inspection ran after it. The readiness test asserts every inspection read one task version instead of only that one was captured. The startup test no longer claims an ordering that correctness does not need. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Put the gh adapter's doc comment back on its class; say when shutdown stops a job's merge Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test that a job stopped before the guarded merge asks GitHub nothing Covers MergeCoordinator.merge's pre-aborted caller signal check: the job's step-3 review load finishes after shutdown aborted its signal, so merge() is entered with an already-aborted signal and must not reach gh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Save the store's running-job refusal so a resend replays it When a job's ending write failed, the coordinator holds no job in memory but Store.beginMergeJob still refuses a new click because the job shows as running. start() now saves that definite refusal under the click's action ID, as it does for its own admission refusals, so a resend replays it instead of re-checking. Also types the step-3 load wrapper in the pre-aborted merge test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Follow a job's queued attempt, and the later job a resent click's answer carries A merge job polled to its end in `queued` now follows the attempt GitHub's queue holds (the poll's `queue`, matched by the job's mergeActionId), so a page that started without a queue sees the merge or removal without Refresh. A resent click whose answer replays its ended job follows the later job the reloaded review carries (by sequence, or revision within one job), rather than showing the replayed outcome while that job runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Build F6b browser fixtures' job records with every field the server sends The F6b browser tests built merge job records without `sequence` and/or `mergeActionId`, unlike mergeJobView, so the page's laterMergeJob treated cross-job comparisons as "not later". A single mergeJobRecord helper now builds each record with every MergeJobView field (typed against it), and the tests that show several jobs give each its own sequence; a poll follows the clicked job instead of an unrelated one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the attempt a polled job ended queued in when a held full response arrives A full response (Refresh, a review action, Refresh plan, plan import or Apply) read while the job was merging kept the poll's newer `queued` job but took the response's own queue (none, or another attempt's), so the control went back to "Merge queued" and polling stopped after the poll had already shown Merged. The shared newerMergeObservations helper now also keeps the attempt the poll followed for that job (actionId === job.mergeActionId) and presents it as the poll did, at all five full-response sites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test that a job's queue follow ignores another attempt and shows an ended one at once Covers two claims of withEndedJobQueue: a poll whose attempt is not the job's (another action ID) leaves the job's outcome alone and is not polled, and a poll that sees the job end queued with its attempt already merged or removed shows that outcome at once and stops polling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give F6b fixture job records increasing revisions along the job's course Every saved change adds one to a job's revision (runner/store.ts), so a later record of one job with another state or step carries a higher one. The mergeJobRecord helper now defaults the revision by the job's course (preparing 1, checks 2, merging 3, ended 4); fixtures that pass an explicit revision, including the one that sends an older record on purpose, keep it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Show a job's own attempt when the merge click's answer carries it ended A click answered with a job that ended `queued` dropped the attempt the reloaded review carried: another client's poll may already have seen it merged, removed or failed, yet the control read "Merge queued" and, being terminal, nothing polled it until Refresh. The click, the job poll and a newer observation kept over a full response now present an ended job and its own attempt (actionId === job.mergeActionId) through one helper pair, withEndedJobAndAttempt / showEndedJobAndAttempt, so the attempt's status takes precedence in the control and the banner, and an active attempt is polled. Tests cover the click answered with the job's attempt merged, removed and still queued, and a held Plans refresh response beside the existing Refresh cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give merge-job test records production revision sequences The mergeJobRecord helper now follows runner/store.ts #changeMergeJob and runner/merge-job.ts: preparing 1, checks 2 with no waiting checks or 3 after the first wait, merging 4, ended 5. Explicit fixtures that used impossible revisions (merging or blocked at 2, merged at 2, a waiting checks record at 2) now carry possible ones, and every later record of one job has a higher revision; the "no later than" refusal case sends an older wait (3) than the shown blocked record (4). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Serve the queue-handoff tests' review a merging record at the revision the server saves it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give the 30-minute-timeout fixture the revision its waits would reach Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the two merge-job timeout wordings that name no waiting check Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give the 30-minute-timeout fixture the highest revision its waits can reach Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give blocked merge-job fixtures the reason production saves, with its next step Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the merge route's busy refusal while a preparation's command check holds the runner Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the plural wording when several required checks fail Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Assert the exact reason of every merge job startup settles Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Use texts production sends in merge-job and history-failure fixtures Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the control label and banner of each ended merge job, and the merging step's banner Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test that merging clears listed checks and the exact prepare-merge refusal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Pin the merge job's shutdown handling around its first load and preparation A job stopped by shutdown during its own first review load or its preparation stays running with no ending write, and a first load that still resolves after shutdown starts no preparation and saves no prepare-merge placeholder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Replace unreachable refused-click job variants with a reachable one The server attaches the task's latest job to every merge-job refusal, and that only moves forward, so an older running job cannot arrive. Test an ended job later than the one shown instead, and keep the no-later running record as a named defensive pin for the "later than" half of the follow condition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Reload the click's review with a body-less GET in browser merge fixtures route.fetch with method GET resent the click's POST body; the server never reads a GET body, closes the connection over it, and the reset surfaced as read ECONNRESET. One shared helper now issues a separate GET with the page's token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Pin the merge job's admission and status claims with tests Covers the failed-reload answer carrying the job's latest record, shutdown winning over an unrelated admission-load failure, the unsaved refusal for an empty or non-string token, the pre-placeholder startability check (labelled defensive), Retry merge for a task in review, and the server passing the task's current status into withMergeJob. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Pin the merge job's production timing defaults and timing check A coordinator built without a timing, as the server builds it, waits 10 s first, doubles to 60 s and gives up at 30 minutes; a timing that is not a whole, non-negative number of milliseconds is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Pin the merge-job poll back-off, the draft kept on a failed click, and the preparing banner The poll doubles its wait while a running job does not change and starts again at 2 s once its step changes; a draft typed while a merge click is in flight survives a 409 or 503 answer; the preparing banner is asserted in full. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Pin the merge-job poll delays directly, including a checks-only change The back-off test now records the delays the page schedules instead of timing round trips, and a new case pins that a changed check list within the same step neither backs off nor resets the delay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Pin that a foreign ledger commit is not passed as the task's own Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Label the unreachable busy and active-attempt guards as defensive blockMergeJob's attempt and merge terms, and the merge route's executor busy term, cannot decide anything in production; comments say why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test that updating an ended merge job leaves it unchanged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Pin the store's merge-job refusals and say which refusals start() saves Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Say how start() refuses a tokenless click, and use production's 503 and 409 shapes in merge-click fixtures Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test that a resent merge click replays its own job after a later job started Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Say why the stopping check after a merge job is defensive Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refuse a merge click with a too-long token as stale without saving it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give the ended-job browser fixture the revision production saves Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refuse a reused click ID before checking its token, and pin a job's guarded-merge refusal reason exactly Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test that a merge click ID saved under another plan is refused as reused Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the snapshot and plan-revision terms of a refused job merge, and say when each can land Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Label the already-fixed range's outcome check defensive Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test queue-poll and plan-import interleavings in their reachable order In one process the guarded merge saves its outcome and the job reads it in one turn, so a queue poll or plan import can only land while GitHub's merge command is in flight. Hold the gateway's merge in the removed/failed and plan-revision tests, run the poll and import while it is held, and assert the guarded merge rejects. Label the fulfilled path's failed/removed branch defensive. Observe a moved HEAD in the new-snapshot test, as production's load does. Plan writes are refused, not delayed, during a merge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Label the merge click's shutdown check before its trust read as defensive Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the direct Merge button across a render in flight, and deliver the timed-out blocked job through the poll Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the job wording of the guarded merge's later refusals A local HEAD observation during the guarded merge advances the task's state version, so the job's preparation no longer applies. The queue correlation, queue boundary and authorization checks reuse the review loaded before the second pass, see only that blocker, and the job now words their refusal as what merging does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Mark snapshot-observation merge job paths as defensive A merge job needs a runner, and a runner server's review load reads the recorded snapshot head, so no HEAD observation records a snapshot during a job. Rename the tests that reach their state that way as defensive, correct the observeLocalHead docstring, and correct the handoff doc, blockMergeJob and guarded merge comments to match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Never let a completed check run without a start time outrank a started one A completed run with no start time (or the zero time) counted as the latest, so a skipped or neutral run could hide a newer failure and read as passing. Only a run not yet completed counts as the latest without a start time; a completed one counts at its completion time, or as the oldest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Reach the F6b tests' mergeable review as a runner server does The merge job tests' server never set runnerRepository, so review loads observed the user's HEAD, and a user-side commit restoring run.sh's mode made the task mergeable; a runner server never reads that commit. The tests now open the runner-owned repository and set runnerRepository before any load, as setUpRunner does, and reach a mergeable review by amending the plan to declare the run.sh change. The shared setup asserts the review reads the runner repository and that the snapshot never moves to the user's (moved) HEAD. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * End a merge job whose review changed before its first load The click's review token was checked only at admission. A review change from another tab that landed while the job's first review load was pending made the job prepare and merge a review the person never saw. The job now ends failed, saving no prepare-merge action, when that load's token differs from the click's, as the direct merge refuses a review that changed during validation. The guarded merge keeps the current token: a rebase during preparation makes a new one, and readiness binds the review version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Build the F6b browser fixtures' merge status as a runner server does Every runner-mode merge status in the browser fixtures now goes through runnerMerge, which adds settledBeforeJobMerge as withMergeJob always does; many click answers and reviews omitted it. The poll back-off test for a changed check list now sends that list at revision 4, as production saves it, and the end at 6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the no-start-time rule for commit statuses; label a defensive removal Add latest-check regressions for commit statuses without a start time, and say in the docstring and merge-handoff doc that the rule covers statuses. Label the job-refresh removal in withMergeJobObservation as defensive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tie a required check observation with no time to the latest A completed check run or commit status with neither a start nor a completion time was ordered as the oldest, so an older started success outranked a failure with no time and the check read as passing. Such an observation now ties with the latest, and the existing tie rule (the one that passes least counts) decides, so it can never make a check pass. Completion is read from the observed state, which replaces the explicit SUCCESS/FAILURE/ERROR list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fail closed for a completed check with no start time A completed run or status with no start time is dropped only when another of its kind provably started after its completion; otherwise it ties with the latest, so the run that passes least counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Pin the strict boundary of the dropped-record rule, and retitle a case its assertion cannot distinguish Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Say where the already-fixed scan starts in the v19 comment and its test title Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 209707e commit b4a1977

22 files changed

Lines changed: 4944 additions & 115 deletions

‎docs/architecture.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ Writing standard: plain language, ISO 24495-1:2023
4343
- **In use:** plan and linking library, SQLite store, review screen, plan display/import/authoring screen, Ask (questions to a Claude agent), guarded merge with merge-queue support, ranked Issues screen, agent isolation boundary, and the single-runner lock.
4444
- **In use for a non-demo review with a `github` block:** planning. `/api/plan/suggestions` and `/api/plan/drafts` ask Claude, through lane D, for suggestion cards or a whole next plan revision (#117, #124). Nothing becomes a revision until you apply it.
4545
- **In use with an opt-in `runner` block in `review.json`:** the whole loop after planning. `start` and `resume` on `/api/runner` run a task's plan item by item with a Claude agent (#91). They require current plan-item approvals, bind both the task state and review version, recheck approvals before each later item, and refuse when a completed prefix no longer ends at the task's current head (#107). After an out-of-scope pause, an approved amended plan can reconcile the completed prefix and resume only the unfinished suffix (#88, PR #134). When a run ends, codeboost publishes the task's pull request, a draft if the task needs a person (#103). `prepare-merge` refreshes the published PR's exact base/head, rebases through the durable F3/F4 path, recomputes attribution and approvals, pushes a rewritten head whose review is still clear over exactly the PR head it rewrote, and runs allowlisted `cmd:` checks in a credential-free read-only container bound to the resulting head (#22). Approve & merge merges that pull request (#121). Cancelling a task closes its pull requests (#111). These are API actions; the screen has no buttons for them yet.
46-
- **Partly built for pre-merge automation:** the trusted local rebase engine records one-to-one commit mappings, verifies the resulting checkout byte for byte, owns its subprocesses durably, and recovers interrupted work (#22, PR #136). Its production conflict path gives a sandboxed agent only the bounded conflict files and imports only audited results for both foreign and plan-owned commits (PRs #138, #140 and #144). `prepare-merge` now invokes that path after current pull-request base/head reads, refreshes attribution and approvals, pushes the rewritten head behind a durable push marker (F6a), and runs exact-head `cmd:` checks; required-check refresh, the already-fixed rerun, and the one-click guarded merge handoff remain for F6b.
47-
- **Not built:** screens for the runner actions, the required-check refresh and one-click merge handoff (F6b), review rounds, and the Queue and Learning screens.
46+
- **Partly built for pre-merge automation:** the trusted local rebase engine records one-to-one commit mappings, verifies the resulting checkout byte for byte, owns its subprocesses durably, and recovers interrupted work (#22, PR #136). Its production conflict path gives a sandboxed agent only the bounded conflict files and imports only audited results for both foreign and plan-owned commits (PRs #138, #140 and #144). `prepare-merge` now invokes that path after current pull-request base/head reads, refreshes attribution and approvals, pushes the rewritten head behind a durable push marker (F6a), and runs exact-head `cmd:` checks. With a runner, Approve & merge runs one merge job that prepares, waits up to 30 minutes for GitHub's required checks, and hands the exact pair to the guarded merge, whose already-fixed check now covers every base commit after the task's original base (F6b, `docs/implementation/merge-handoff.md`).
47+
- **Not built:** screens for the runner actions, the merge job's full step list and Cancel button, review rounds, and the Queue and Learning screens.
4848

4949
## Terms used
5050

@@ -357,7 +357,7 @@ The diagram shows the designed transitions. With the `runner` block, running, ne
357357

358358
### Stored data
359359

360-
All state is in one SQLite file, opened with WAL and full synchronization. Each write takes an immediate write lock. The schema version is `PRAGMA user_version`, and migrations run in explicit steps (version 18 today); an unknown version fails.
360+
All state is in one SQLite file, opened with WAL and full synchronization. Each write takes an immediate write lock. The schema version is `PRAGMA user_version`, and migrations run in explicit steps (version 19 today); an unknown version fails.
361361

362362
| Group | Tables | Notes |
363363
|---|---|---|
@@ -410,7 +410,7 @@ These are the decisions that shape the structure. The full list, with the reason
410410
| C, K | Guarded merge, merge queue | Done |
411411
| D | Agent isolation boundary | Done. Since the isolation gate: bounded diff export (#76), task-change inspection and the runner's commit as a bounded `git bundle` (#66, PRs #85 and #90), owner-scoped recovery for Ask (#95), the plan schema for Claude (#97), gitlink mounts and the pre-launch tree check (#81, #99, PR #100), and Codex refused in every phase (#93, PR #106). The runner-contract follow-ups (#51) are closed: the last one tests export and removal through a recovery handle after a real restart (PR #109). |
412412
| E | Planning library and suggestions | Done. E4 (PR #45) added the acceptance fixtures and recorded Claude output that replays through validation and the Store. |
413-
| F | Runner | Merged: F1 (lifecycle store, coordinator, shutdown wiring, startup recovery and the lock, planning API, feedback events, and stops that wait for their commit: #53, #74, #57, #59, #60, #96); F2a–F2b (execute prompts, audit, per-item execution, real workspace, durable safety findings: #67, #68, #92, #94, #128); F2d (already-fixed check, PR opening, branch pusher, production publishing, closing PRs on cancel: #84, #101, #110, #115, #116, #119, #123); production wiring and `start`/`resume` (#91, PRs #102, #105); planning through lane D (#117, #124, PRs #118, #120, #125); merging the task's own PR (#121, PR #126); start/resume approval, review-version, recovery and completed-prefix guards (#107, PR #130); continuation after a scope pause (#88, PR #134); the F3 durable trusted local-rebase foundation (#22, PR #136); the F4 foreign-conflict engine plus production sandbox adapter (#22, PRs #138 and #140); and sandboxed owned-commit conflict resolution (#22, PR #144). F5 (PR #149) added current base/head refresh, rebase admission, post-rebase attribution and approval refresh, and head-bound `cmd:` execution. This branch adds F6a: the durable push of the rewritten head. F6b owns fresh required-check refresh, the already-fixed rerun, and the one-click exact-pair guarded-merge handoff; F6c owns common CI. |
413+
| F | Runner | Merged: F1 (lifecycle store, coordinator, shutdown wiring, startup recovery and the lock, planning API, feedback events, and stops that wait for their commit: #53, #74, #57, #59, #60, #96); F2a–F2b (execute prompts, audit, per-item execution, real workspace, durable safety findings: #67, #68, #92, #94, #128); F2d (already-fixed check, PR opening, branch pusher, production publishing, closing PRs on cancel: #84, #101, #110, #115, #116, #119, #123); production wiring and `start`/`resume` (#91, PRs #102, #105); planning through lane D (#117, #124, PRs #118, #120, #125); merging the task's own PR (#121, PR #126); start/resume approval, review-version, recovery and completed-prefix guards (#107, PR #130); continuation after a scope pause (#88, PR #134); the F3 durable trusted local-rebase foundation (#22, PR #136); the F4 foreign-conflict engine plus production sandbox adapter (#22, PRs #138 and #140); and sandboxed owned-commit conflict resolution (#22, PR #144). F5 (PR #149) added current base/head refresh, rebase admission, post-rebase attribution and approval refresh, and head-bound `cmd:` execution. F6a (PR #153) added the durable push of the rewritten head. This branch adds F6b: the one-click merge job (preparation, the required-check wait, the exact-pair guarded merge) and an already-fixed scan covering every base commit after the task's original base; F6c owns common CI. |
414414
| Hardening | Allowlisted subprocess environments | Done: every Git command (#82, #83) and every `gh` runner (#84, #86). |
415415
| Tests | Test suite reliability | Done. The full suite runs ordinary tests first and Docker-backed files serially; CI and the real-Docker gate passed on PR #132 (#129). |
416416
| G | Planning screen | Done (#145). The screen displays/imports the current plan, requests and renders a next-revision draft or suggestion cards, and applies results with revision-bound exact ambiguous replay. G4 composes the browser with the production planning setup, D-backed provider boundary, F endpoints and SQLite persistence; focused adapter, recorded-output and exact-head real-Docker planning suites cover the lower boundary. |
@@ -419,7 +419,7 @@ These are the decisions that shape the structure. The full list, with the reason
419419

420420
## Known limits
421421

422-
- `prepare-merge` invokes the trusted rebase and sandboxed conflict machinery, refreshes the review, pushes the rewritten head, and runs head-bound `cmd:` checks. Until F6b of #22 lands, it does not wait for fresh required checks, rerun the already-fixed check, or hand the exact pair to guarded merge; a person clicks Merge once GitHub's checks on the pushed head pass.
422+
- With a runner, Approve & merge runs one merge job in the background: preparation, a wait of up to 30 minutes for GitHub's required checks, then the guarded merge. A failing or slow check, or GitHub's refusal, leaves the task approved, merge blocked; the browser shows only the job's current step, not the design's full step list, and has no Cancel button for the job yet (cancel the task instead).
423423
- Linking is line-based. It cannot see an unrelated edit inside a declared file; only the review agent and you can catch that.
424424
- The container's guarantees are those of Docker. The agent can always reach its own vendor account.
425425
- `cmd:` acceptance checks run during `prepare-merge`, and their passing evidence is bound to the exact reviewed head. A plan item without current passing evidence still blocks merging.

0 commit comments

Comments
 (0)