Skip to content

fix(boards): Retry on the boards list error state, a caught Metrics load, translated timeout copy and an error clear on success - #2690

Merged
Chris0Jeky merged 8 commits into
mainfrom
issue-2689/boards-retry
Sep 5, 2026
Merged

fix(boards): Retry on the boards list error state, a caught Metrics load, translated timeout copy and an error clear on success#2690
Chris0Jeky merged 8 commits into
mainfrom
issue-2689/boards-retry

Conversation

@Chris0Jeky

Copy link
Copy Markdown
Owner

Summary

PR #2688 bounded the shared board-list read with timeout: BOARD_REQUEST_TIMEOUT_MS and skipRetry: true. That bound is now load-bearing on the boards list, and its review named four costs. This PR pays all four. The boards list gets a Retry control, so a one-off 503 or an API restart during mount no longer strands the alert until the user navigates away and back. MetricsView.loadBoards catches, so a bounded failure is no longer an unhandled rejection out of onMounted. The board store's own boundary maps an axios timeout and an axios cancel to translated copy, so "timeout of 10000ms exceeded" stops appearing verbatim inside a localized alert. And a list read that succeeds clears state.error, so a concurrent fail-then-succeed pair cannot leave the alert standing over a populated list.

Frontend only. No API, no shared utils/errorMessage.ts, no Paper Inbox files.

Refs #2689 (items 1 to 4; item 5, the four direct boardsApi.getBoards callers, stays open on the issue and is not in this slice). Refs #2685, PR #2688.

Changes

1. Retry control on the boards list error state (src/views/BoardsListView.vue, src/locales/{en,es,it}/boards.ts)

The error block now renders the message in a role="alert" paragraph with a real <button data-action="retry-board-load"> beside it, the same shape PaperTriageTable uses. role="alert" moved from the wrapper to the paragraph so the announcement is the sentence alone and the button is a focusable sibling outside the live region, tied to it by aria-describedby. The mount read and the retry share one loadBoards function, so a retry produces the sequence a mount does: skeleton in flight, then the grid or the alert again. A retry is never swallowed by the store's 5 s throttle, which stamps only after a success and releases the shared in-flight promise on rejection.

New keys: boards.error.retry in all three catalogs, wording mirrored from inbox.triage.boardPick.retry. The same commit lands boards.error.timeout and boards.error.cancelled so the three catalogs stay structurally parallel in one step; the store starts using them in change 3.

2. A caught board-list read in Metrics (src/views/MetricsView.vue)

loadBoards had a try with a finally and no catch, and is awaited inside an async onMounted callback, so the store's rethrow escaped the hook. Vue routes a lifecycle-hook rejection to app.config.errorHandler; with none installed it rethrows from its own .catch, which reaches the window as an unhandledrejection and is forwarded to Sentry by installWindowErrorListeners. The store already set error and toasted, so the catch reports nothing further. boards keeps its previous value on failure: a failed read learned nothing about the list and must not be reported as empty. The finally is unchanged.

3. Translated transport copy at the board store boundary (src/store/board/boardStoreHelpers.ts)

getErrorMessage prefers response.data.message then err.message, which is right for an API error and wrong for a transport one: a transport failure carries no server message, so err.message is axios' own English. handleApiError now answers the two transport shapes from the catalog before falling through to getErrorMessage, so every other error keeps exactly the behaviour it had. Timeouts are detected by the axios codes ECONNABORTED and ETIMEDOUT, with a message check behind them for an adapter that reports a timeout without a code; that arm is gated on isAxiosError and on the absence of a response, so a server error whose own wording mentions a timeout keeps the server's wording.

A cancel is mapped rather than dropped. It should not reach here at all: both read paths return on axios.isCancel first, and the only aborter today (the logout reset) bumps the read generation before aborting. If one ever did, silence would leave error null with boards still empty, and BoardsListView's v-else-if chain renders that as the EMPTY state, an unconfirmed claim that the account has no boards. That is the #1961 class of lie the shared read exists to prevent, so a short honest line plus the Retry control is the smaller failure.

The copy resolves through i18n.global.t rather than useI18n(), the pattern recorded in locales/en/review.ts section 1 and used by useReviewActions / useReviewProposals / usePaperReviewSelectors: this is a plain factory called from stores and from specs that never mount a component, and i18n.global.t still reads the live locale, so the copy follows a language switch. src/utils/errorMessage.ts is deliberately untouched, being cross-surface.

4. An error clear on the list read's success path (src/store/board/boardCrudStore.ts)

fetchBoards cleared error on entry to a read but never on success. Two list reads genuinely overlap, because a filtered (includeArchived) read never joins the share and never populates it: the activity selector's can still be on the wire when the boards list mounts its unfiltered one. When the failing half writes its alert after the surviving half has already cleared on entry, error was left set beside a populated boards, and the v-if loading / v-else-if error / ... / v-else grid chain showed the alert instead of the boards it already held. The clear sits after the generation check, so a superseded read still writes nothing.

Test plan

Verified, all from frontend/taskdeck-web:

  • npx vitest --run --maxWorkers=2 src/tests/views/BoardsListView.spec.ts src/tests/views/MetricsView.spec.ts src/tests/views/MetricsView.coverage.spec.ts src/tests/store/board/boardCrudStore.spec.ts src/tests/store/board/boardStoreHelpers.spec.ts src/tests/store/boardStore.spec.ts src/tests/i18n/catalogs.spec.ts gives Test Files 7 passed (7), Tests 187 passed (187). Seven paths named, seven files run.
  • npx vitest --run --maxWorkers=2 src/tests/store src/tests/i18n gives Test Files 51 passed (51), Tests 906 passed (906). The whole store tree, because the handleApiError change sits on every board-store failure path.
  • npx vitest --run --maxWorkers=2 src/tests/views gives Test Files 78 passed (78), Tests 1422 passed (1422). MetricsView.coverage.spec.ts (held by PR test: wait for MetricsView error alert #2653) is inside that run and is green and untouched.
  • npm run typecheck clean.
  • npx eslint over all eleven changed files: no output, no warnings.
  • git diff --check origin/main..HEAD clean.

Red-first. Every new assertion was run against the unmodified source (the four source files and the three catalogs restored to origin/main, the specs kept). Result: Test Files 4 failed (4), Tests 9 failed | 110 passed (119).

  • BoardsListView.spec.ts, three failures: "renders a focusable Retry button beside the alert" (no [data-action="retry-board-load"] element exists); "re-issues the read, shows the skeleton in flight, and leaves the error state on success"; "shows the alert again, still retryable, when the retry also fails".
  • MetricsView.spec.ts, one failure: "mounts through a rejected board-list read without escaping the mounted hook" fails with expected "vi.fn()" to not be called at all, but actually been called 1 times, the app error handler receiving the escaped rejection.
  • boardStoreHelpers.spec.ts, four failures: the timeout mapping, the it locale check, the ETIMEDOUT code, and the cancel mapping, each expected 'Failed to fetch boards' to be the catalog copy.
  • boardCrudStore.spec.ts, one failure: "clears an overlapping failure's alert when a current-generation read succeeds" fails with expected 'Failed to fetch boards' to be null.

One new test is a guard rather than a red-first assertion and passes both before and after: "leaves a server error message alone even when it mentions a timeout", which pins the response gate on the message arm of the timeout check.

How the Metrics assertion works, since it is not obvious: it installs an app.config.errorHandler spy through the mount options and asserts it is never called. Vue intercepts a lifecycle-hook rejection before it can reach the window, so a process-level unhandledrejection listener would not observe anything; the app error handler is the first thing Vue reaches on that route, which makes "never called" precisely "the rejection did not escape the callback".

NOT verified:

  • No browser or E2E run. The Retry button's rendered appearance, focus ring and hover state are asserted only through the unit DOM and the CSS is untested; the styles mirror .paper-triage__retry.
  • No npm run build. vue-tsc -b and the mounted-component specs cover the changed templates, but the Vite production build was not run.
  • No screen-reader verification of the role="alert" move. The DOM relationship (alert text alone, aria-describedby from the button to the alert's id) is asserted; the announcement itself is not.
  • The Italian and Spanish strings are machine-translated, consistent with MACHINE_TRANSLATED_LOCALES and Seed an i18n translation layer (vue-i18n) with Italian and Spanish locales #1770. Structural parity is proven by catalogs.spec.ts; tone and correctness are not.
  • The ETIMEDOUT arm is covered by a synthetic AxiosError, not by a real axios request with transitional.clarifyTimeoutError on.

Boundaries and risks

Changed files, all frontend and all inside the claimed set: src/views/BoardsListView.vue, src/views/MetricsView.vue, src/store/board/boardStoreHelpers.ts, src/store/board/boardCrudStore.ts, src/locales/{en,es,it}/boards.ts, and the four matching specs. Not touched: src/utils/errorMessage.ts (cross-surface, out of lane), src/api/**, the Paper Inbox files (PR #2654), src/tests/views/MetricsView.coverage.spec.ts (PR #2653).

Two decisions a reviewer should weigh.

The cancel is mapped rather than skipped, for the reason in change 3. The alternative reading, that a cancel is the app's own decision and deserves silence, is defensible for the detail surface but not for the list one, where silence renders as a false empty state.

The detail read has the same missing success-path clear and is deliberately left alone. Fixing it is not the one line the list needed: a successful BACKGROUND detail refresh must not clear an alert it does not own, so the change needs an intent gate, and the concurrent pair item 4 describes is far less reachable there because the detail path supersedes itself. It is called out here rather than fixed silently.

Risk on change 4: error is a single shared ref across the list and detail surfaces, so a succeeding list read now also clears an alert a detail failure set. That is last-writer-wins either way, and the issue accepts it for the list; a per-surface error ref would be a larger refactor than this slice.

Risk on change 3: boards.error.timeout and boards.error.cancelled live in the boards namespace but can surface on any screen the board store backs, since handleApiError is shared. There is no common namespace in src/locales, and boardDetail.ts is a different owner's surface, so a duplicate key there would be worse. The catalog comments in all three locales say so explicitly for the next translator.

Docs: no canonical doc changed. This restores intended behaviour on an existing surface rather than shipping new capability, so docs/STATUS.md, docs/IMPLEMENTATION_MASTERPLAN.md and docs/decisions/ were left alone; flag it if the lane reads it otherwise.

Worktree: built in .worktrees/codex-2689-boards-retry. The only gitignored content is frontend/taskdeck-web/node_modules from npm ci, which is reproducible and nothing was copied out. Ready for a plain git worktree remove once this lands.

The list read has been bounded with skipRetry since #2685, so a one-off
503 or an API restart during mount no longer heals itself in the retry
layer. The alert stood until the user navigated away and back, and
because the read is shared, every unfiltered caller on the page failed
with it.

The error block now renders the message in a role="alert" paragraph with
a real button beside it, carrying the same data-action="retry-board-load"
shape PaperTriageTable uses. role="alert" moved from the wrapper to the
paragraph so the announcement is the sentence alone and the button is a
focusable sibling outside the live region, described by the alert through
aria-describedby. Both the mount read and the retry go through one
loadBoards function, so a retry produces the same sequence a mount does:
skeleton in flight, then the grid or the alert again. A retry is never
swallowed by the store throttle, which stamps only after a success.

This commit also lands the two board-store boundary keys (boards.error
.timeout and boards.error.cancelled) in en, es and it, so the catalogs
stay structurally parallel in one step; the store starts using them in a
later commit of this branch.

Refs #2689 (item 1), #2685, PR #2688.
loadBoards is awaited inside an async onMounted callback and had a try
with a finally but no catch, so the store rethrow escaped the hook. Vue
routes a lifecycle-hook rejection to app.config.errorHandler, and with no
handler installed it rethrows from its own catch, which reaches the
window as an unhandledrejection and is forwarded to Sentry by
installWindowErrorListeners. The failure was pre-existing; the #2685
bound made it routine.

The store already set its error surface and toasted through
handleApiError, so the catch reports nothing further. boards keeps its
previous value on failure: a failed read learned nothing about the list
and must not be reported as empty. The finally is unchanged.

The spec asserts through a mount-level app error handler, which is the
first thing Vue reaches on that route, so "never called" is precisely
"the rejection did not escape the callback". A process-level listener
would not work here because Vue intercepts the rejection before it can
reach the window.

Refs #2689 (item 2), #2685, PR #2688.
getErrorMessage prefers response.data.message and then err.message,
which is right for an API error and wrong for a transport one: a
transport failure carries no server message, so err.message is the axios
string "timeout of 10000ms exceeded", rendered verbatim inside a
localized alert and a localized toast. Since #2685 bounded every board
read, that is a routine 10 s outcome rather than an exotic one.

handleApiError now answers the two transport shapes from the catalog
before falling through to getErrorMessage, so every other error keeps
exactly the behaviour it had. Timeouts are detected by the axios codes
ECONNABORTED and ETIMEDOUT, with a message check behind them for an
adapter that reports a timeout without a code; that arm is gated on
isAxiosError and on the absence of a response, so a server error whose
own wording mentions a timeout keeps the server wording.

A cancel is mapped rather than dropped. It should not reach here at all
(both read paths return on axios.isCancel first, and the only aborter
today bumps the read generation before aborting), but if one ever does,
silence would leave error null with boards still empty, and the
BoardsListView v-else-if chain renders that as the EMPTY state: an
unconfirmed claim that the account has no boards, which is the #1961
class of lie the shared read exists to prevent.

The copy is resolved through i18n.global.t rather than useI18n for the
reason recorded in locales/en/review.ts section 1: this factory is called
from stores and from specs that never mount a component, and
i18n.global.t still reads the live locale, so the copy follows a language
switch. utils/errorMessage.ts is deliberately untouched: it is
cross-surface, and this is the board store's own boundary.

Refs #2689 (item 3), #2685, PR #2688.
…ead succeeds

fetchBoards cleared error on ENTRY to a read but never on the success
path. Two list reads genuinely overlap, because a filtered
(includeArchived) read never joins the share and never populates it: the
activity selector's can still be on the wire when the boards list mounts
its unfiltered one. When the failing half writes its alert after the
surviving half has already cleared on entry, error was left set beside a
populated boards, and the BoardsListView chain of v-if loading,
v-else-if error, then the grid showed the alert instead of the boards it
already held. The #2685 bound made the failing half deterministic at
10 s, so this stopped being a race nobody reaches.

The clear sits after the generation check, so a superseded read still
writes nothing.

The detail read has the same gap and is deliberately left alone: a
successful BACKGROUND detail refresh must not clear an alert it does not
own, so the fix there needs an intent gate rather than the one line this
slice authorizes, and it has no coverage in this branch.

Refs #2689 (item 4), #2685, PR #2688.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Review record (alpha product-trust lane, review-and-ship round 1 at head 468ce3594).

Reviewer: one fresh-context independent reviewer subagent (read-only; Codex credits exhausted, SC-9), given the worktree at the head and the merge-base diff. Verdict: SHIP, no CRITICAL or HIGH; three MEDIUMs on the new behaviour, all fixed in round 2 because each is a defect in the control this PR introduces.

Lenses that found nothing: the success-path clear sits after the generation check, so a superseded post-logout read cannot clear the next session's error or write the throttle stamp; i18n.global.t works outside setup under this app's composition-mode createI18n, the test setup resets the locale per test, and the review store already uses the pattern; en/es/it carry structurally identical boards.error.{retry,timeout,cancelled} keys and the catalog parity spec would catch a miss; nothing in api/http.ts re-types a cancel, both read paths return on axios.isCancel before handleApiError, so the mapped cancel copy is defensive and its comment says so; a 408 or 504 carries a response so the message arm leaves server wording alone and ERR_NETWORK is untouched; MetricsView resets its loading flag in finally and keeps its previous boards rather than a falsely empty list, and the errorHandler proof has an in-repo precedent; aria-describedby always resolves because the paragraph and the button render in the same branch; scope is exactly the 11 files.

Findings, triaged once:

  • MEDIUM, fixed in round 2: Retry is a silent no-op inside the store's 5 s throttle window when the alert came from another action after a successful read (a mutation failing within 5 s of the mount read), and the view's docblock asserted the opposite. An explicit retry now bypasses the throttle (not the share), the docblock states what holds, red-first specs on the store and the view.
  • MEDIUM, fixed in round 2: the success-path clear was unconditional, so a list read that succeeded after another surface failed erased that surface's alert. The list read now clears only the error a list read itself wrote (the guarded-clear precedent in BoardView.vue), red-first spec for the create-failure-during-flight case, the fail-then-succeed case kept green.
  • MEDIUM, fixed in round 2: activating Retry unmounted the focused button and a failed retry left focus on the body. Focus is restored to the new Retry button after a user-initiated retry fails; the alert paragraph is a new node on each failure, so it re-announces.
  • LOW, declined: the ETIMEDOUT arm is unreachable under this app's axios configuration (no clarifyTimeoutError); it is a documented defensive arm and stays.
  • Informational: the docs/STATUS.md entry ships in the lane's fifteenth-block PR, as every entry in this wave has.

Round count: 2 after the fix push. Merge gate: CI green at the fix head, the three-minute age, and one read-only verification pass scoped to the fix diff (the fixes change store and view logic).

…ndow

Round-2 review finding 1. The Retry control this PR added was a silent
no-op inside the store 5 s throttle window, and the BoardsListView
docblock asserted the opposite.

The premise the docblock rested on is true: the throttle stamp is
written only after a success, so a retry that follows a FAILED list read
was never blocked. But the stamp from an EARLIER success survives, and
state.error is shared by every board action. A list read succeeds at T0,
createBoard (or any board, column, card or label mutation) fails at
T0+2s and sets error, the v-else-if error branch renders the alert with
a live Retry button, and the click hit the throttle check and returned
before loading was set or any request was made: no skeleton, no request,
a dead button until the window passed.

fetchBoards takes an optional third argument, options: { force?: boolean
}, which skips the throttle check only. BoardsListView routes the mount
read without it and the Retry click with it. Every existing caller keeps
its behaviour: the parameter is optional and defaults to no force. The
boardStore facade passes fetchBoards straight through, so it needs no
change.

force deliberately does NOT bypass the in-flight share, which is checked
first and unconditionally: joining a read already on the wire is the
correct answer to a second caller, and forcing a parallel one would
reintroduce the #1961 fan-out the share removed.

The docblock now states what actually holds instead of the half of it
that did.

Refs #2689, PR #2690.
Round-2 review finding 2. The success-path clear added earlier in this
PR was unconditional, so it erased alerts the list read never raised.
state.error is one ref shared by every board action: the mount read is
slow, the user submits the create form at T+2s, createBoard fails and
sets its message, the mount read commits at T+5s and wipes it. The user
was told about a failure and then silently was not. At the merge base
that alert survived, so this was a regression introduced by the fix for
item 4.

The guard follows the repo precedent in BoardView.vue, which clears only
when the error is still the one it observed. handleApiError now returns
the message it wrote (callers that ignore the return are unaffected),
the crud store keeps lastListReadError in its closure and sets it from
that return in the list read's catch, and a current-generation success
clears state.error only when it is still strictly equal to that message.

The marker is dropped on ANY current-generation success, matched or not,
so a message from an older failed read can never authorise a clear later
on. resetForLogout drops it too, since the alert it would authorise
clearing is cleared there anyway.

The item 4 case stays green: a failing overlapping read followed by a
successful one still clears.

Not addressed here, and unchanged from the merge base: the clear at the
START of a read is still unconditional, so beginning a list read drops
another surface's alert. That is pre-existing behaviour every current
spec depends on, and it is at least the user asking for fresh data
rather than a background commit overwriting them.

Refs #2689, PR #2690.
Round-2 review finding 3. Activating Retry unmounts the button that was
just activated, because the loading branch replaces the whole error
block, and a failed retry rebuilds that block with a brand new button.
Focus fell back to body, so a keyboard or screen-reader user had to tab
from the top of the page to reach the control they had just used, on
every attempt.

A template ref on the button plus an await nextTick() after the load
settles puts focus back. It runs only in retryLoad, never after the
mount read: this follows a user action, so moving focus finishes what
the user started rather than stealing it. The ref is null when the retry
SUCCEEDED, since the error block is gone by then, so the optional call
is also the only-on-failure guard.

The alert paragraph is a new node on each failure, so the re-announcement
holds independently of the focus move.

The spec attaches the wrapper to document.body, because focus and
document.activeElement are meaningless for a detached tree, and unmounts
it at the end. It asserts the loss as well as the restore: the button is
absent and activeElement is body while the read is in flight, then the
rebuilt button (a different element from the original) has focus.

Refs #2689, PR #2690.
…mocks

Three pre-existing handleApiError mockImplementationOnce overrides on the
detail-read tests returned void, which vue-tsc rejected once the shared
mock took the real helper's string return type. They now mirror the real
helper: resolve the message, write it to state, return it.

Behaviour of those tests is unchanged; they assert the call arguments and
state.error, never the return.

Refs #2689, PR #2690.
@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Round-2 record (alpha product-trust lane; fix head 4b8138444, four commits on top of 468ce3594).

Fixed, one commit per finding:

  • An explicit retry now bypasses the store's 5 s throttle and nothing else: fetchBoards(search?, includeArchived, options: { force? }), the in-flight share check staying ahead of the throttle and unconditional; the boards list's mount read passes no force, its retryLoad() does; the docblock now says the stamp-after-success premise does not cover a stamp left by an earlier success. Red-first: the store spec expected two requests and got one; the view spec expected the forced call.
  • The success clear is guarded to the list read's own error: handleApiError returns the message it wrote, the crud store remembers the list read's last message and clears state.error on a current-generation success only when it is still that message, dropping the marker on any current-generation success and in resetForLogout. Red-first: a create-failure message raised during the read's flight was cleared; a stale list-read message authorised a later clear.
  • Focus returns to the new Retry button after a failed user-initiated retry (template ref plus await nextTick() inside retryLoad only; the ref is null after a successful retry, so no call on mount or success). Red-first: document.activeElement was the body.
    One typecheck follow-up: three pre-existing handleApiError.mockImplementationOnce overrides in the detail-read specs return the message now that the mock carries the helper's string return type; they assert call arguments and state.error, never the return.

Verified at the fix head: the seven named specs 7 files, 194 passed; src/tests/store plus src/tests/i18n 51 files, 911 passed; src/tests/views 78 files, 1424 passed (MetricsView.coverage.spec.ts untouched, green); npm run typecheck clean; eslint on the six changed files clean; git diff --check clean. Diff against the base is exactly the 11 owned files.

Not verified: no browser run; the focus restore is asserted in happy-dom; the forced path is proven with a mocked store and no real 5 s window. Unchanged and recorded in the commit body: the clear at the start of a read is still unconditional (pre-existing; every current spec depends on it).

Base: main moved 57 commits under this PR during the coordinator's D-12 sweep; no commit on main touched any of the 11 files (checked by the worker), the PR is MERGEABLE, and CI at this head runs against the merge ref, so the moved base is re-proved by that run. Merge gate remaining: CI green at 4b8138444, the three-minute age, and the read-only verification pass scoped to this diff, running now. Round count: 2.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Round-2 verification record (scoped to the fix diff 468ce3594..4b8138444; read-only pass, fresh context).

Verdict: SHIP for the fix diff, no CRITICAL or HIGH.

What it checked: force bypasses exactly the throttle (the share check stays ahead of it and unconditional, so a forced read during a flight joins; the demo branch and both generation gates untouched; the stamp still written after a forced success so the next unforced call inside 5 s is throttled as before; all five existing callers pass no options); the guarded clear on all four traced cases (fail-then-succeed clears; a create failure during the flight survives; a stale marker cannot authorise a later clear because it is dropped on any current-generation success before the comparison; the marker is assigned only on the live path after the generation and cancel gates and nulled in resetForLogout); the focus mechanics (the template ref is bound to the rebuilt button when nextTick resolves, the element ref is null after a successful retry, mount never reaches the restore, aria-describedby still resolves); the specs discriminate (a real earlier-success stamp under fake timers, the create failure landing after the read's entry clear, the stale-marker consumption, document.activeElement in all three states) and the mockImplementationOnce edits are behaviour-neutral; scope is the six files.

Findings, triaged once at the two-round ceiling, all recorded on #2689 as items 6 to 10 rather than a third round:

  • MEDIUM: the focus restore is unconditional, so a failed retry steals focus from the create form if the user moved there during the 10 s bound (item 6, the guard is document.activeElement being lost).
  • LOW: the throttle docblock is still narrower than the truth (item 7); the identical-message collision is routine because timeout and offline copy collapse to one string each (item 8); BoardListFetchOptions is not re-exported (item 9); one spec comment over-claims (item 10).

Round count: 2. Merge gate remaining: CI green at 4b8138444 and the three-minute age.

@Chris0Jeky
Chris0Jeky merged commit a2fe247 into main Sep 5, 2026
35 checks passed
@github-project-automation github-project-automation Bot moved this from Pending to Done in Taskdeck Execution Sep 5, 2026
Chris0Jeky added a commit that referenced this pull request Sep 5, 2026
Chris0Jeky added a commit that referenced this pull request Sep 5, 2026
…s line, SC-10 scope, item-5 count, carry-forward)

Answers the docs review of PR #2693: the header points forward to the coordination-lane subsection the coordinator adds after the block lands instead of at one that does not exist yet; a corrections line retires the thirteenth block's present-tense claim that #2629 and #2654 were unmerged; the SC-10 sentence scopes the platform lane's five to its own checkpoint and leaves the ten-PR measurement to the coordinator's sweep record; item 5's caller count is the measured five; the not-shipped line carries the fourteenth block's non-ruling residuals forward; items 5 to 10 named on the #2690 bullet; one bare issue number backticked.
@Chris0Jeky
Chris0Jeky deleted the issue-2689/boards-retry branch September 6, 2026 02:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant