Skip to content

Run review history work in an abortable worker thread - #170

Merged
mchwang merged 6 commits into
mainfrom
fix/review-load-worker
Oct 14, 2026
Merged

mchwang merged 6 commits into
mainfrom
fix/review-load-worker

Conversation

@mchwang

@mchwang mchwang commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

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. This PR moves history reading and linking behind a worker boundary with an abortable protocol, and keeps the deadline as a worker-side budget.

  • Bounded workers: at most 2 history workers run at once, using FIFO slots that are held until each worker thread has ended. A queued job that is aborted, or whose budget runs out while it waits, never starts.
  • Worker per load (runner/history-job.ts, history-work.ts, history-worker.ts). On abort, or when the budget passes, the parent sends a cancel. The worker kills its running Git process, waits for it to exit, and replies. A worker that has not replied within 1 s (it is in synchronous linking or in the storage walk) is terminated. The job settles only after the thread has ended, and an abort rejects with the signal's own reason.
  • Abortable Git (git/history.ts). readHistory is async and takes a signal. It waits for each child's close, so it settles only after Git has exited, and it settles on the callback alone when Git never started. This was needed because worker.terminate() cannot interrupt execFileSync (measured: it waits for the child).
  • Budget. The budget crosses threads as relative milliseconds, counted from the worker's start message. A 5 s start grace applies until that message arrives. A shared timeOrigin clock was rejected because it drifts after system sleep.
  • ReviewService. load/act are async, and a synchronous apply(view, input, actionId) runs inside Store.userAction. Store reads and writes stay on the server thread. If the Store moved while the worker ran, the load reads again, at most three reads in all within the deadline. It reports the deadline, not staleness, when time runs out.
  • Callers. Every load takes the request's or operation's abort signal. No worker await follows a final remote read:
    • Merge admission inspects the PR once more after its final review load, then checks trust and the review's generations synchronously before the attempt.
    • Pre-merge preparation inspects the PR after its last review load.
    • Merge replays use a coordinator shutdown signal, and close() waits for them.
    • Pre-merge rechecks task generations after each worker await, before beginRebase and recordHistory.
  • Server behaviour.
    • A load failure before a review command is not recorded under its action ID, so the same ID can be resent.
    • Any failure after a command (or question retry) was applied answers with outcomeUnknown.
    • A request aborted after the shutdown drain keeps the existing 409.
    • A question saved during shutdown still gets its retryable "Server stopped" answer.
  • Browser specs. The specs run 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 (test/browser/review-page.ts). Playwright timeouts were raised (test 60 s, expect 15 s), because server Git time now counts.
  • Docs: README.md and docs/implementation/read-only-review.md ("History work off the server thread").

Test Coverage

New tests are in test/history-job.test.ts and test/history-spawn.test.ts, plus new cases in test/pre-merge.test.ts, test/merge.test.ts and test/merge-task-pr.test.ts. A gated git fixture (test/fixtures/gated-git.ts) holds a chosen Git call until the test releases it. The tests cover:

  • an aborted read kills Git and settles after it has exited;
  • a load runs while the server thread keeps working;
  • the worker enforces the deadline;
  • re-read on a Store change, and stale after three reads;
  • overlapping loads that both see a moved HEAD;
  • the deadline reported rather than staleness;
  • worker termination, proven by a heartbeat that stops;
  • the start grace and the never-start path;
  • a spawn failure (EMFILE);
  • the server answering other requests during a load, and shutdown ending the load and its Git process;
  • outcomeUnknown after an applied command, a retry and a failed reload;
  • an unrecorded load failure, and a replay loading less;
  • "Server stopped" answers when shutdown stops load 3 or load 4;
  • merge-replay abort at shutdown, and a trust revocation during the final merge load;
  • pre-merge: trust revoked and task changed during the final load, during the old-history read, and during both #refresh paths.

Each new regression test was run against the change with its fix removed, and it failed.

Pre-Landing Review

Seven independent review rounds were run by separate review agents, read-only, each over the full diff against origin/main 22e76a8. The diff was uncommitted during review; the reviewed content is commit 2f9672e.

  • Round 1: cross-thread clock skew; merge replays not abortable; a markStopped reload abortable at shutdown; comment overclaims; missing tests. All fixed.
  • Round 2: loads no longer atomic (read-only GETs failing as stale); missing failing-before tests for the merge and pre-merge reorders; a question missing its answer when the post-apply load was aborted; the backstop counting from worker creation. All fixed.
  • Round 3: a possible hang when Git fails to spawn (EMFILE/ENFILE); doc wording; retry-question not marking applied; refreshReview passing without a refresh. All fixed.
  • Round 4: a task change during the rebase old-history read could be adopted, and the refreshReview doc comment overclaimed. Both fixed.
  • Round 5: #refresh recorded a head without the task guard; outcomeUnknown limited to aborts; "Stale" reported on deadline expiry. All fixed.
  • Round 6: no runtime defects. Three test and doc gaps, fixed.
  • Round 7: no new findings (content of 2f9672e).

Copilot round 1, on 2f9672e:

  • merge.ts: the final review load came after the last GitHub read.
  • pre-merge.ts: a review load ran after the final remote.inspect.

Both were valid and are fixed in 771a683, with failing-before tests. The threads are answered and resolved.

Independent rounds after Copilot, on base 22e76a8 with the content of 771a683:

  • Round 8: verified both fixes. It found two test gaps: the retargeted pre-merge tests fired too early, and the merge generation check had no test. Both are fixed, and each test now fails only through the guard it names.
  • Round 9: no new findings. Its one observation was declined: in merge-queue mode the final load now runs after the queue watermark. That widens a window origin/main already had (an async inspect after the watermark), and the final status read still validates the PR's head, base, queue mode and identity.

Copilot round 2, on 771a683:

  • Unbounded worker concurrency (inline thread).
  • A rejected terminate() settled a job before its thread exited (in the summary).

Both were valid and are fixed in 3de30ec, with failing-before tests. The thread is answered and resolved.

Independent rounds after Copilot round 2, on base 22e76a8 with the content of 3de30ec:

  • Round 10: no new findings in the code. During this round the full Vitest run exposed one test whose mocked clock now crossed the new queue wait; the test was fixed to move the clock once the worker held its budget.
  • Round 11: no product findings. It noted that this test could leak its stubs if it timed out; its cleanup moved to afterEach.
  • Round 12: no new findings.

Copilot round 3, on 3de30ec: 0 open findings. It noted that the exact final head lacked a full-suite run while Docker validation was blocked. Both are now covered by the test plan below.

Declined: the round 9 observation above.

Review lessons

Each review finding is classified below. The new rules are in AGENTS.md ("Async jobs and polling"), commits ca97365, 7b8b698 and c4667ad.

Finding (round) Classification
Cross-thread clock skew from timeOrigin (independent round 1) New rule: never compare clock readings across threads; pass a relative budget.
Loads no longer atomic (round 2); a task change during the rebase old-history read (round 4); #refresh recorded a head without the task guard (round 5); the final review load after the last GitHub read in merge and pre-merge (Copilot round 1) New rule: when synchronous work moves behind an await, "list every await it adds and apply the local-generation rule under 'Guarded external actions' to each one"; "a local await must not follow the final remote validation".
Unbounded worker concurrency (Copilot round 2) New rule: work that leaves a serializing path needs its own concurrency bound.
Browser specs relied on the in-process server blocking Playwright (validation) New rule: tests that host the server wait for observable state.
Merge replays not abortable or awaited at close (round 1); a rejected terminate() settled a job before its thread exited (Copilot round 2) Covered: "Do not release a concurrency slot when cancellation is requested…" and "Begin shutdown… then cancel and await jobs".
Comment and doc overclaims (rounds 1, 3, 4, 6) Covered: "Treat every behavioural claim… as something to verify".
Missing or weak tests (rounds 1, 6, 8); missing failing-before tests for the merge and pre-merge reorders (round 2); pre-merge tests firing early (round 8); refreshReview passing without a refresh (round 3) Covered: "give it a test that fails if the claim is false" (Review readiness).
"Stale" reported when the deadline had passed (round 5) Covered: "Preserve the original timeout, cancellation, and shutdown reason".
A failure after an applied command was reported as a refusal (rounds 3, 5) Covered: "Once an irreversible external command succeeds, do not convert later refresh or rendering failures into action failure".
Spawn failure (EMFILE) never emitting close (round 3) One-off: a Node child_process detail with no general rule beyond the existing subprocess-settlement race.
Backstop counted from worker creation (round 2) One-off: specific to this protocol's start message.
markStopped reloading, abortably, at shutdown (round 1); a question saved during shutdown missing its answer (round 2) One-off: specific to Questions.markStopped.
A test's mocked clock crossing the new queue wait (round 10 validation); test cleanup leaking on timeout (round 11); the gated-git numbering race (validation) One-off: test-fixture fixes in this PR.
Exact final head lacked a full-suite run and Docker validation (Copilot round 3) One-off: validation evidence, now recorded in the test plan.
Merge-queue watermark window (round 9) Declined, as recorded above.

Documentation

Updated:

  • README.md: the async readHistory example and the limits paragraph.
  • docs/implementation/read-only-review.md: a new section on lifecycle, deadline, cancellation, staleness, review commands, and merge/preparation guards.

The /ship document-release subagent audit was not run.

Not run in this /ship

origin/main had not moved. The repo has no VERSION, CHANGELOG or TODOS file. The /ship review army, adversarial review, coverage audit and document-release audit were not repeated: the seven independent rounds and the runs below cover this exact content.

Docker project: not run locally, because the Docker daemon is unresponsive. CI's real-docker job passed on 3de30ec.

Follow-up (pre-existing, out of scope): the review page loses text typed, or an Import click, before its first review load finishes.

Test plan

  • npm run typecheck: no errors (on 3de30ec).
  • npx vitest run --project parallel on exactly 3de30ec (clean worktree): 2042/2042 passed.
  • npx playwright test: 137/137 passed, with the worker cap.
  • CI on 3de30ec: test and real-docker both passed. real-docker covers the Docker gate; locally, the Docker daemon is unresponsive.

🤖 Generated with Claude Code

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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Final asynchronous review loads reopen remote-state races in merge admission and pre-merge readiness.

2 open findings
What changed in this PR

Moves review-history reading and linking into abortable worker threads so server requests, cancellation, and shutdown remain responsive.

Changes:

  • Adds worker-based history processing with deadlines and cancellation.
  • Makes review, merge, preparation, and planting flows asynchronous.
  • Adds lifecycle, race, shutdown, and browser regression coverage.
File Description
web/​server.ts Propagates abort signals through asynchronous review loads.
test/​store.test.ts Awaits asynchronous history reads.
test/​runner-workspace.test.ts Updates workspace tests for async loading.
test/​runner-start.test.ts Makes review approval setup asynchronous.
test/​runner-shutdown.test.ts Updates shutdown timing and async view typing.
test/​runner-publishing.test.ts Awaits review loads in publishing tests.
test/​runner-production.test.ts Awaits the startup review load.
test/​runner-planning-feedback.test.ts Awaits feedback review loading.
test/​review.test.ts Converts review service tests to asynchronous operations.
test/​questions.test.ts Converts question workflows to async review operations.
test/​question-agent.test.ts Awaits review setup.
test/​pre-merge.test.ts Adds preparation race regressions and async loading.
test/​plant.test.ts Converts planting tests to asynchronous calls.
test/​merge.test.ts Updates merge mocks and adds replay-shutdown coverage.
test/​merge-task-pr.test.ts Adds final-load authorization regression coverage.
test/​history.test.ts Converts history adapter tests to async execution.
test/​history-spawn.test.ts Tests failed Git spawn settlement.
test/​history-job.test.ts Tests worker lifecycle, deadlines, cancellation, and races.
test/​fixtures/​history-worker-slow-start.ts Simulates delayed worker startup.
test/​fixtures/​history-worker-silent.ts Simulates a nonresponsive worker.
test/​fixtures/​history-worker-busy.ts Simulates uninterruptible synchronous worker work.
test/​fixtures/​gated-git.ts Provides controllable Git-process test gating.
test/​browser/​review-page.ts Adds browser review-load synchronization helpers.
test/​browser/​plans-production.spec.ts Waits for review loading before plan navigation.
test/​browser/​plans-authoring.spec.ts Synchronizes authoring tests with async review loads.
test/​browser/​issues.spec.ts Synchronizes issue tests with review loading.
scripts/​plant.ts Makes history-dependent experiment planting asynchronous.
runner/​review.ts Runs history reading and linking through workers.
runner/​questions.ts Uses async review-view typing and shutdown settlement.
runner/​pre-merge.ts Uses abortable workers during preparation.
runner/​merge.ts Makes merge validation loads abortable and tracks replays.
runner/​history-worker.ts Implements the history worker protocol.
runner/​history-work.ts Executes budgeted history reading and linking.
runner/​history-job.ts Supervises worker deadlines, cancellation, and termination.
README.md Documents async history reads and worker behavior.
playwright.config.ts Raises browser-test timeouts.
git/​history.ts Replaces synchronous Git calls with abortable children.
docs/​implementation/​read-only-review.md Documents worker lifecycle and integration behavior.
core/​path-identity.ts Extracts reusable path-identity validation.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread runner/merge.ts
Comment thread runner/pre-merge.ts Outdated
…#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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Worker concurrency is unbounded, and termination failure can settle a job before its thread exits.

1 open finding
2 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Do not settle jobs before rejected worker termination has exited

runner/​history-job.ts:62

terminate() rejecting does not prove the worker thread has exited, but this rejection branch calls deliver() and settles the job anyway. During cancellation/shutdown that can release ownership while the worker (and potentially its Git child) is still alive, contradicting the method's lifecycle guarantee. Let the exit handler be the only path that delivers the decided outcome; a rejected termination request should not settle the job.

🧠 Review effort: Balanced

Comment thread runner/history-job.ts
…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The concurrency changes are broad, and the exact final head lacks a passing full-suite run while Docker validation remains blocked.

0 open findings

1 resolved since last review

🧠 Review effort: Balanced

mchwang and others added 3 commits October 13, 2026 15:35
…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>
 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iew)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Cross-thread cancellation, subprocess settlement, shutdown, and merge admission span multiple critical lifecycle boundaries requiring final human review.

0 open findings

🧠 Review effort: Balanced

@mchwang
mchwang merged commit 99e58a9 into main Oct 14, 2026
3 checks passed
@mchwang
mchwang deleted the fix/review-load-worker branch October 14, 2026 01:49
mchwang added a commit that referenced this pull request Oct 14, 2026
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>
mchwang added a commit that referenced this pull request Oct 14, 2026
Resolves runner/merge.ts #replay (keeps #170's async load, shutdown abort
and tracked replays, plus #48's aborted-click answer and the click's own
signal on its fresh read) and web/server.ts (#170's async loadReview with
#48's statusAfterMerge). Adapts #48's new tests to the async review
service; one test now waits for the click's gh call before shutdown, as
production does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang added a commit that referenced this pull request Oct 14, 2026
…ctions

Since #170 a click can be in a review load when its deadline fires, and
the load's history worker gets 1 s before it is terminated, so a click
can settle about 1 s or a little more after its deadline, not 0.75 s.
The merge-queue poll tests read the recorded snapshot from the Store
instead of running a review load inside every mocked inspection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang added a commit that referenced this pull request Oct 14, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang added a commit that referenced this pull request Oct 14, 2026
Since #170 the review load awaits a worker; reading the queue snapshot
before it could pair an older queue state with a newer attempt status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang added a commit that referenced this pull request Oct 14, 2026
Resolves the test/review.test.ts import conflict by keeping both imports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang added a commit that referenced this pull request Oct 14, 2026
…lan import (#172)

* Run review history work in an abortable worker thread

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>

* Make a remote read the last await before merge and preparation writes (#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>

* Bound concurrent history workers and settle only after the thread exits (#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>

* Record the review lessons from moving review loads off the server thread (#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>

* Trim the new async rules to what existing rules do not already say (#170 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Say plainly which process the in-process test rule is about (#170 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep input made before the first review load, and explain a refused plan 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>

* Test that text typed before a failed first load reaches the first item after Refresh (#172 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Explain a refused plan import even when no file is chosen (#172 review)

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>

* Test an import with a plan loaded and no file; say which refusals keep ownership (#172 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Say exactly which import refusals keep status ownership (#172 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test text typed after the last mode switch; state why the file refusal 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>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants