Repository navigation
Keep input made before the first review load, and explain a refused plan import - #172
Merged
Merged
Conversation
ReviewService.load read Git history (synchronous Git children and an object-storage walk) and linked it on the HTTP server's event loop, for up to 30 s. During that time cancellation, shutdown, request timeouts and every other request were stuck. History reading and linking now run in one worker thread per load (runner/history-job.ts). readHistory spawns Git asynchronously and waits for each process to exit, so a cancel kills the running Git process; a worker that cannot read the cancel within 1 s is terminated. The load's deadline crosses threads as a relative budget counted from the worker's start. Store reads and writes stay on the server thread; a load whose inputs changed while the worker ran reads again, at most three reads in all. load and act are async, and a synchronous apply runs inside Store.userAction. Every load takes the request's or operation's abort signal. Merge admission, pre-merge preparation and merge replays recheck their trust and task generations after each new wait, in the same turn as the write they guard. A failure after a review command was applied now says outcomeUnknown, a load failure before it is not recorded under its action ID, and a question saved during shutdown still gets its retryable answer. Browser specs ran the server in the Playwright process and relied on the old blocking loads to sequence steps. They now wait for the page's review load or refresh, and the Playwright timeouts allow for the server's real Git work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#170 review) The worker-backed review load had become an await after the final GitHub read: merge admission could begin an attempt for a pull request that moved while the load ran, and pre-merge preparation could record readiness for a stale remote pair. Merge admission now inspects the pull request once more after its final review load, compares it with the previous pass, and only then checks local trust and the review's generations synchronously before the attempt. Preparation drops the review load that followed its final remote inspection; readiness uses the view loaded just before it, which the synchronous generation check after the inspection keeps current. Tests cover a pull request moving and the review changing during the final merge load, the load/inspect order in preparation, and trust or task changes during preparation's final load, each asserting the check that refuses it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts (#170 review) Review loads no longer serialize on the server thread, so several tabs or quick refreshes could start any number of worker threads and Git processes. At most two history workers now run at once. A later job waits for a slot in arrival order; the wait counts against its budget, and a job aborted or out of time while it waits never starts. Each slot is held until its worker thread has ended. A rejected worker.terminate() no longer settles the job: it proves nothing about the thread, so the job waits for the thread's exit event. The gated Git test fixture numbers calls under a lock, so calls that start together get distinct numbers. The review deadline test moves its mocked clock only once the worker holds its budget, and its clock and environment stubs are restored in afterEach. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ead (#170) Four rules for async work, from the findings on this pull request: recheck every same-turn guard after a new await and make the final remote validation the last await before an irreversible action; bound work that leaves a serializing path; never compare clocks across threads; and in tests that host the server, wait for observable state instead of relying on the server blocking the test's event loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iew) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lan import Text typed into the composer before the first /api/review answer had no item to belong to, so applying the answer erased it (switching Ask/Request change erased it too). It is now kept under the unselected key and handed to the first item render() selects. "Import next revision" did nothing visible while no plan was loaded. It is now aria-disabled with no plan, and pressing it says why: still loading (written under the load's status ownership, so the load's result replaces it), or not loaded after a failure. The Plans screen shows "Loading plan…" during the first load and Refresh plan. The busy-write refusal is checked first, so an import pressed during a Retry Apply no longer hides the Apply's result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m after Refresh (#172 review) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The file input's `required` let the browser's own check stop the form before importPlan ran, so with no plan loaded and no file chosen the page asked for a file instead of saying why it could not import. The form is now novalidate; importPlan already reports a missing file. The no-plan refusal test also checks its ✕ status colour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p ownership (#172 review) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l claims status (#172 review) The draft tests switched modes before the load answered, which saved the text early, so they passed even without the save made when the response applies. They now type more after the last switch. The importPlan comment now says the file refusal always claims the status line, because feedback on the file just chosen is newer than any read already running (as "does not let an older failed Review refresh replace newer Plans validation feedback" already requires). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
The asynchronous state transitions are guarded appropriately and comprehensively covered by regression tests.
0 open findings
What changed in this PR
Preserves composer input across the initial asynchronous review load and provides clear plan-import feedback when no plan is available.
Changes:
- Retains pre-load drafts and assigns them to the first selected plan item.
- Adds loading, disabled-state, and refusal feedback for plan imports.
- Adds browser regressions for draft retention and import-status races.
| File | Description |
|---|---|
web/public/index.html |
Enables application-managed import validation. |
web/public/app.js |
Preserves drafts and improves plan loading/import state handling. |
test/browser/first-load.spec.ts |
Covers first-load draft and import behavior. |
test/browser/plans.spec.ts |
Tests missing-file import feedback. |
test/browser/plans-authoring.spec.ts |
Tests import refusal during retry apply. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
main's tree equals c4667ad, this branch's former base, so the merge keeps this branch's tree unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 tasks
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.
Follows #170, which added
test/browser/review-page.tsand stopped the server from blocking the page while the first review load runs. This PR was stacked on #170. After #170 was squash-merged as99e58a9, this PR was retargeted tomain, andorigin/mainwas merged in with-s ours. That merge is exact becausemain's tree equalsc4667ad, this branch's former base. The diff againstmainis byte-identical to the former diff againstc4667ad(both hash4ce3d34). This PR fixes what the page does with input made before that first load answers.Problem
The page ends with a top-level
await refresh(). Its/api/reviewrequest can take up to 30 seconds. Before it answered:rememberDraft()keyed drafts by the selected item, and no item was selected yet. Switching between Ask and Request change erased it too.importPlan()returned when!data?.plan.Both break the rule in AGENTS.md "Async review UI": a background response must not erase input made after the request started.
Change (
web/public/app.js)Composer
:<mode>, for both Ask and Request change.render()selects an item, it hands that text to that item. No "Retained draft" row is created.Plans and import
#plan-importisaria-disabledwhenever no plan is loaded. Pressing it explains the refusal, with or without a file chosen:novalidate(web/public/index.html). Before, the file input'srequiredlet the browser's own check answer first, asking for a file instead of explaining that no plan was loaded.importPlanalready reports a missing file itself.Tests
test/browser/first-load.spec.ts(new, 5 tests)Each test holds
/api/reviewwithpage.routeand releases it from the test. None of them usesopenRevieworreloadReviewfor the step under test./api/actionrequest and no notes saved.badstatus colour, no import request is sent, and the plan is still r1.test/browser/plans.spec.ts(+1 test)badcolour and keeps focus. No import request is sent and the plan is still r1. Withoutnovalidateit fails: the status line still shows "✓ Revision r1 is current.".test/browser/plans-authoring.spec.ts(+1 test)Failing before, passing after: all 6 tests were run against the base
app.js(c4667ad). Each failed at this first assertion:""(the Ask/Request change switch erased the text)." Still typing."instead of the full text. The cause is the same as in test 1: switching Ask/Request change before the load erased the earlier text, and only the text typed after the switch survived.#plans-summaryshowed "Plan unavailable" instead of "Loading plan…".#plan-importwas notaria-disabledwhile no plan was loaded.#plans-summaryshowed "Plan unavailable" during Refresh plan.All 6 pass on this branch. The no-file presses in tests 3 and 4 were also run without
novalidate: both failed, because the browser's file check stopped the form beforeimportPlanran.Validation
These are separate gates.
fb14a34(basemain99e58a9; same tree asffd353f):test: passed.99e58a9): CItestandreal-dockerboth passed.ffd353f(former basec4667ad):tsc --noEmitclean.app.jsandindex.html, at the symptoms listed above.13757a9: 142/142.6167c6f: stopped before finishing; head replaced.71a7a4a: 143/143.c576ecb: 144/144.8072620: stopped before finishing; head replaced.main99e58a9.Review record
Passes 1 to 10 and Copilot round 1 reviewed the full diff against the former base
c4667ad; the sync ontomainleaves that diff byte-identical. Passes 1 to 3 ran on the uncommitted working tree; passes 4 onward name their exact commit pair.Pass 1. Finding: Refresh plan reads without
beginBusy, so an import during it claimed the ✕ status and hid the refresh's ✓. Fixed:reviewLoading()now includesplanRefreshPending, and Plans re-renders when a refresh starts and ends; added the test now numbered 5. Also added apageerrorcheck to the test now numbered 4.Pass 2. Finding: Retry Apply can be a write while
datais null, and the import's claimed ✕ hid the Apply's result. Fixed: the busy-write refusal is checked first and writes under the current owner; added the test now numbered 6. ThereviewLoadingcomment was corrected.Pass 3. No defects. Low-severity points:
renderPlanImportcomment overstated behaviour during a running import. Fixed: comment reworded, no behaviour change.Pass 4 (independent subagent, exact pair
c4667ad..13757a9). No code defects. Findings:6167c6f. It fails on base.app.js. The Refresh plan status wording was wrong, and the review record was stale. Fixed: this body was corrected, and each symptom above comes from a run againstc4667ad.#plans-identityis not cleared when no plan is loaded. This is unchanged from base.Pass 5 (independent subagent, exact pair
c4667ad..6167c6f). No defects in status ownership, drafts, or aria-disabled. Findings:requiredcheck stopped the form beforeimportPlan, so the no-plan explanation never appeared. TherenderPlanImportcomment and this body claimed it always did. Fixed in71a7a4a: the form isnovalidate; tests 3 and 4 now press Import with no file first, and both fail withoutnovalidate.badclass of its ✕ message. Fixed: it does now.Pass 6 (independent subagent, exact pair
c4667ad..71a7a4a). No correctness defects. It confirmed thatnovalidateaffects only the import form, and judged that blanking#plans-statusbetween presses in tests 3 and 4 is acceptable: the page writes that element but never reads it. Findings:c576ecb: the comment now names which refusals keep ownership.plans.spec.tstest, which fails withoutnovalidate.novalidatechange with a plan loaded. Fixed above.Pass 7 (independent subagent, exact pair
c4667ad..c576ecb). No correctness defects. All six failing-before symptoms were confirmed against base. Findings:8072620: comment only.aria-disabledor a reason. Since Run review history work in an abortable worker thread #170 the page can be used during that load, so this is now reachable for up to 30 s. Added to Follow-up.Pass 8 (independent subagent, exact pair
c4667ad..8072620). No correctness defects in the PR's fixes, and no inaccurate PR-body claims. Findings:novalidate, a no-file press during a running refresh claims the status line and hides that refresh's result. The reviewer suggested writing it under the refresh's ownership. Declined: the project already requires the opposite for file feedback, in "does not let an older failed Review refresh replace newer Plans validation feedback", and the no-file press now follows that rule.ffd353f: the comment now says the file refusal always claims it, and why.rememberDraft()call just beforerender()inrefreshis removed.showFailuresetsdisabledon#message, so a focused composer drops focus to the page.Pass 9 (independent subagent, exact pair
c4667ad..ffd353f). No code, comment or test defects. Every new test fails if its feature breaks. Finding:rememberDraft()in therefreshcatch predates this PR and is redundant, because the next refresh saves the composer at its start. This PR does not claim that test 2 covers it.Pass 10 (independent subagent, exact pair
c4667ad..ffd353f). No code, comment or test defects. Focused runs on the head:first-load.spec.ts5/5; the newplans.spec.tsandplans-authoring.spec.tstests 2/2. All failing-before symptoms confirmed against base. Finding:Pass 11 (independent subagent, exact pair
99e58a9..fb14a34, after the sync ontomain). No code, comment or test defects. It verified the sync claims: equal trees, an identical diff hash4ce3d34, and the merge commit's parents. All 7 new tests passed on the head (17 s). It checked the failing-before symptoms against99e58a9by reading the code. Findings:Copilot round 1 (requested on
ffd353fafter pass 10; the user approved the request). Review submitted on commitffd353f: "Approval recommended", 0 open findings, 0 inline comments, 0 unresolved threads.Copilot round 2 (requested on
fb14a34after pass 11, on the new basemain99e58a9; the user approved the request). Review submitted on commitfb14a34at 04:22 UTC: "Approval recommended", 0 open findings, 0 inline comments, 0 unresolved threads.Follow-up
#savedaria-disabledand a reasonaria-disabledorreadonlyinstead ofdisabledon#message.Review-lesson audit
Every finding from every round is classified below. Quoted rules are in
AGENTS.md, mirrored inCLAUDE.mdwhere noted, or inDESIGN.md.renderPlanImportcomment overstated behaviour): covered by an existing rule. "Treat every behavioural claim the change makes, in code comments, the PR body or docs … as something to verify" (AGENTS.md and CLAUDE.md).#plans-identity, which is unchanged from base.requiredcheck pre-empted the refusal: covered by an existing rule, the behavioural-claim rule as in pass 3. The comment and body claimed the refusal is always explained.aria-disabled". It is listed under Follow-up, not fixed here.showFailuredisables a focused composer. It falls under the AGENTS.md keyboard-focus rule above and is listed under Follow-up.rememberDraft()in therefreshcatch predates this PR. No change.Result: every finding is covered by an existing rule or recorded as one-off. No new
AGENTS.mdrule is needed, so this branch makes noAGENTS.mdorCLAUDE.mdchange.🤖 Generated with Claude Code