diff --git a/docs/STATUS.md b/docs/STATUS.md index bc85b9a69..92dde6e31 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -77,7 +77,7 @@ v0.3 integration wave, sixth block (2026-09-04, `main` `65abe3e2f` to `becd85fd4 - **Webhook persisted failures are generalized and ProposalTools redaction is aligned (`#2351` partial, PR `#2474`, merge `8dbeb81cf`).** `OutboundWebhookDeliveryWorker` now persists only the stable generic failure message for unknown exceptions (previously pattern-redacted raw exception text reached `LastErrorMessage`) and logs the redacted exception summary exactly once with the delivery id; HTTP-status, invalid-URI, and shutdown-cancellation arms keep their stable messages and retry/dead-letter transitions are unchanged. MCP `ProposalTools` uses `SanitizeLlmFailureMessage` like the read and write tools, so known-domain messages are redacted rather than passed through raw. Evidence: full API project 2812 passed with 4 declared skips; fresh-context review with no CRITICAL/HIGH. - **The standalone CLI has an unknown-exception boundary (`#2351` partial, PR `#2466`, merge `57df469e2`).** `Taskdeck.Cli` had no top-level catch: an EF/SQLite exception printed its raw message and stack trace to stderr and exited with a crash code. `Program.cs` now wraps the run and delegates to `CliUnexpectedFailure`, which prints only the generic failure line plus an explicit "diagnostics were not captured" notice and exits with the normal failure code; `DomainException`, usage/validation, recovery and connector-verification codes, `PreMigrationBackupException` (redacted, no stack), and the first-run bootstrapper's operator-path guidance are unchanged. The harness-only companion failure file is owner-read/write on POSIX. **Stated limit:** the CLI clears every logging provider, so outside the harness trace the exception is retained nowhere; an always-on bounded sink is `#2468`. Evidence: CLI tests 191/191, Architecture 28 with the pre-existing single skip, one end-to-end real-process case; fresh-context review with one fix round. - **The correlated unknown-exception HTTP boundary is proven end to end (`#2351` partial, PR `#2471`, merge `ca3f3e6b4`).** `UnknownExceptionBoundaryProofTests` runs the real host with a capturing logger and a synthetic exception carrying a secret-like token, a Windows path, a SQLite constraint, and a provider URL: no marker reaches the decoded response body, headers, log messages, structured properties, or scopes; exactly one correlated Error entry exists; the `Result` mapper logs nothing on `UnexpectedError`; deliberate domain, validation, and conflict `Result`s reach the client byte-identical at 400/404/409. The review found the first body scan was escape-blind (a leaked path would have been JSON-escaped and missed); the decoded-JSON walk was added with red-then-green evidence. **Decision recorded on `#2351` for the maintainer:** the shipped SEC-14 middleware deliberately logs a bounded classification, not the raw exception, which conflicts with the umbrella's "full diagnostic exceptions remain in protected logs" wording; the lane proceeds on the shipped policy. -- **Configured API paths stay out of PWA runtime caches and installed workers are retired (`#2411`, PR `#2416`, merge `bb65732cb`).** The parked PR's missing real-browser proof ran and found two defects the mocked unit tests had hidden: the cleanup script's `activate` listener never received the event under vite-plugin-pwa's asynchronous AMD wrapper, and the install-time sweep that replaced it left a re-poisoning window before activation while the old worker still controlled the page. Both are fixed: a one-time evaluation-time sweep keyed by a `taskdeck-pwa-cache-policy-v2` marker cache plus a forced re-sweep inside `waitUntil` at activation that rejects activation on failure; `taskdeck-share-target` and the precache are untouched; the marker version is test-coupled to the policy constant. Evidence: strict Chromium probe 3/3 (old marker to v2 controller, static cache invalidated, share-target preserved), gated preview suite 3/3, `src/tests/pwa/` 51/51, generated-worker suite 8/8 that runs the emitted `dist/api-cache-cleanup.js` and fails when the forced sweep is removed. The ledger record for the earlier "idle Playwright" runner blocker is resolved: it was output buffering from a `| tail` pipe. **Not verified:** a browser-level regression for the install-to-activation window (`#2475`). +- **Configured API paths stay out of PWA runtime caches and installed workers are retired (`#2411`, PR `#2416`, merge `bb65732cb`).** The parked PR's missing real-browser proof ran and found two defects the mocked unit tests had hidden: the cleanup script's `activate` listener never received the event under vite-plugin-pwa's asynchronous AMD wrapper, and the install-time sweep that replaced it left a re-poisoning window before activation while the old worker still controlled the page. Both are fixed: a one-time evaluation-time sweep keyed by a `taskdeck-pwa-cache-policy-v2` marker cache plus a forced re-sweep inside `waitUntil` at activation that rejects activation on failure; `taskdeck-share-target` and the precache are untouched; the marker version is test-coupled to the policy constant. Evidence: strict Chromium probe 3/3 (old marker to v2 controller, static cache invalidated, share-target preserved), gated preview suite 3/3, `src/tests/pwa/` 51/51, generated-worker suite 8/8 that runs the emitted `dist/api-cache-cleanup.js` and fails when the forced sweep is removed. The ledger record for the earlier "idle Playwright" runner blocker is resolved: it was output buffering from a `| tail` pipe. **Not verified:** a browser-level regression for the install-to-activation window (`#2475`). (Three fragments of this entry are corrected in the thirteenth block after PR `#2660`, two of them safety-relevant: the activate-listener reading did not reproduce, and a rejected activate `waitUntil` does not abort activation, so "rejects activation on failure" is not an operative safety property.) - **Docs only (`#2469`, merge `487cd5c35`):** the alpha lane's fifth-block record above. - **Unknown-exception surface inventory and future-flow guard (`#2351` partial, PR `#2470`, merge `5b2a3c742`; reconcile PR `#2483` open and unmerged at the time of writing, waiting on its last hosted job).** `docs/security/UNKNOWN_EXCEPTION_SURFACE_INVENTORY.md` records every HTTP, batch, persisted-state, MCP, CLI, SignalR, and provider-health surface with file:line evidence, sanitizer, classification, and pinning test, and `scripts/check-unknown-exception-boundary.mjs` runs in the docs-governance CI job: per-occurrence checks that MCP tool and resource files never return or throw a raw `Result` `ErrorMessage` without the sanitizer, and that `catch (Exception)` blocks in the persisted-state services, the webhook worker, and the MCP files never assign raw exception text to persisted or returned members; logging and known-domain catches are exempt on their merits and the allowlist is line-keyed. Evidence: guard tests 25/25, real tree 0 findings with no allowlist growth. Open residuals stay listed there: legacy persisted `FailureReason` replay (R3), SignalR `EnableDetailedErrors` pinned only by default (R4), circuit-breaker `LastFailureReason` (R5), two generic strings (R6); guard blind spots are `#2473`. - **The Windows launcher regression suite fails fast on a hang (`#2378` partial, PR `#2478`, merge `becd85fd4`).** The `Run source launcher regression suite` step in `reusable-frontend-unit.yml` has `timeout-minutes: 10` (measured passing step 6m31s), the suite has a 9-minute in-process watchdog that names still-live launcher process trees before failing, and a timed-out launcher gets a tree kill. Measured tonight: seven code-bearing beta heads passed the leg in 11 to 18 minutes while three docs-only heads hung to the 25-minute budget. A red-first repro disproved the grandchild-holds-the-pipe hypothesis on Node 24/Windows, and every case is already bounded at 30 s (slowest 22.8 s), so the hangs are not a single case overrunning; their logs are gone. This bounds and instruments; the root cause is still open on `#2378`. @@ -252,6 +252,38 @@ Platform lane (beta-platform-integrity, recorded from the lane's own merge packe Corrections to earlier blocks in this file: (1) the `#2214` residual list recorded on 2026-09-03 after PR `#2388` names as open three items that a read-only audit on 2026-09-05 found shipped on `main`: `#2214` item 3 (the transient-failure counter resets after an intervening non-transient failure without fabricating recovery; `useReviewProposals.ts`, covered by its spec), `#2215` Part-C item 1 (a revision resync cannot overwrite typed editor text; the guard is the editor payload pinned for the whole edit session in `PaperReviewView`, not the poll hold, so it also covers an explicit load) and `#2215` Part-C item 4 (`usePaperReviewSelectors` keys its reads on the proposal, capture-reference and revision-identity triple and re-fetches on a genuine revision move, `#2460`); the still-open items on both issues have Paper-side as well as Legacy-side work, so the earlier reading that their remaining slices are Legacy-only is withdrawn. (2) The `#2008`/`#1968` sentence in the tenth block ("Legacy help residuals and two quarantined source literals keep `#2008`/`#1968` open") is stale on both halves: the Legacy help residual is retired by `#2615` and the notation guard's release-lane quarantine has been empty since before this wave. (3) The `#2494` sentence "`CreateProposalDto` carries the two values as `[JsonIgnore]` init-only properties" is three values after `#2600` and four fields after `#2611`, and the `#2586` sentence "Option (b), a sentinel allow-list, is recommended for decline because `ProvenanceModelId` is body-bound on `CreateProposalDto` without `[JsonIgnore]`" is superseded on its reason: the field is no longer body-bound, and option (b) stays declined because a recorded model must never be rendered as the producer without the provider that ran (the `#2499` fail-closed projection). +v0.3 integration wave, thirteenth block (2026-09-05, `main` `bf93efc4d`, the twelfth block's own merge, to `540bc2abb`; the commits between `5f2d3fab1` and `bf93efc4d` are the coordination PR `#2616` and the twelfth block `#2628`, both recorded in the eleventh and twelfth blocks; alpha product-trust lane continued, plus the platform lane's merge packets). Codex review credits stayed exhausted (SC-9), so every PR below carried one fresh-context independent reviewer subagent, with a second read-only pass scoped to the round-2 fix diff where that diff changed production logic (`#2630`, `#2631`, `#2635`, `#2659`); `#2663`'s round 2 changed guard rules and comments only and carried no second pass; control-plane PRs stay parked under SC-10. + +Review and proposal frontend: +- **The degraded-queue warning is localized, recovery is announced, and the Paper warning stays visible when the reviewer scrolls (`#2214` residual items 1, 4, 5 and 6, PR `#2630`, merge `627fbdefe`; `Refs #2214`, whose items 2, 7 and 8 stay open for the next slice).** The "may be out of date" warning was hardcoded English in three renders (both Paper branches and Legacy); it now comes from `review.queue.degraded.body` in en, it and es, with the stale `LegacyReviewView` comment corrected. Recovery announced nothing because every `role="status"` warning was unmounted when the flag cleared; `useReviewProposals` now exposes `queueRefreshRecovered`, true only when a successful background refresh or the explicit-load clear ends a visible degraded state (never on success after success, never on a non-transient reset without a visible stale) and retired on the following successful poll, and both skins render one always-mounted sr-only region (`paper-review-queue-recovered`, hoisted above Paper's active/empty branch pair so a recovery that also flips the branch cannot remount it with its text; `review-queue-recovered` in Legacy) that withholds its text until then, gated on `!queueAccessRevoked`; the count live regions are untouched. The Paper active-proposal warning is `position: sticky; top: 0; z-index: 1` and the decision rail's sticky rule is `top: var(--paper-review-sticky-offset, 0)`, a property the view writes on the main column from the warning's rendered height (ResizeObserver; 0 where unavailable) only while the warning is on screen, intended to sit the rail below the pinned warning with both visible (the verification pass found the rail's own sticky rule scoped to a container that never scrolls, so the handshake is inert today; `#2638`); the warning stays below the rail in stacking order so the controls can never be covered; no node moved and the parent and root-children pins stay green. A regression pins the filtered-to-empty co-render (foreign-authored rows, Mine filter, three 500 polls: the warning's role, aria-live and aria-atomic, its parent being the empty element, the "No matches in Mine." title, the count region still carrying the real awaiting count); it was green on arrival, a pin not a fix. Evidence: round 1, five composable recovery specs and two view specs per skin red first, 5 files / 378; round 2, the retire, branch-flip and rail-offset specs red first against the round-1 sources, 5 files / 381 plus the rail and main specs 2 files / 45; the emitted production CSS carries both rules; typecheck, build, ESLint, diff check; the three literal assertions were repointed at the catalog (they did not go red because the English moved verbatim). Not verified: Playwright; the pinned warning and the rail offset in a real browser (the specs assert the class, the declared rules and the property's presence, not layout); screen-reader behaviour; the Italian and Spanish wording is the lane's, not native-reviewed. Accepted and recorded: a 403-then-restored cycle can re-speak a still-fresh recovery, and a recovery landing with an unavailable-pin empty state announces "Showing current proposals", both true at that instant. One fresh-context review SHIP (two MEDIUMs and two LOWs taken in round 2) and one verification pass scoped to the round-2 diff, SHIP with two MEDIUMs tracked as `#2638` rather than a third round: the decision rail's own sticky rule is scoped to a container that never scrolls, so the rail stays in flow and the offset handshake is inert (the pinned warning itself sticks; the comments describing a two-sticky band are recorded on `#2638` for correction), and the recovery sentence is retired by any successful load, so an explicit post-decision reload within a poll interval of a recovery can blank the region before it is announced (poll-only retire goes there). +- **A malformed deep-link target no longer freezes the queue refresh, and refused pins stop logging as API errors (`#2214` residual items 7 and 8, PR `#2658`, merge `21e7753e0`; `Refs #2214`, which stays open for item 2).** A hash-pinned target whose by-id re-authorization read answered 400 (a non-GUID route value is a model-binding 400 on `GET /api/automation/proposals/{id}`) fell to the failure path and returned before the successfully read list was applied, on every tick, and because 400 is non-transient no degraded state ever rose: a bad deep link silently froze the whole queue refresh. On the background pin leg alone, 400 now joins 403 and 404 as an unavailable pin: the fresh list lands, `unavailableProposalId` is set so the Legacy and Paper unavailable-pin states already shipped by `#2357` and `#2593` render, the transient counter is untouched, and the pin read keeps retrying each tick exactly as 403 and 404 do; 5xx, no-response and 401 on the pin leg are unchanged, and the explicit `openProposalFromHash` path still toasts. `AutomationReadOptions` gains `expectedStatuses`, forwarded verbatim only when supplied, and the background pin read passes `[400, 403, 404]` so the shared HTTP layer no longer logs every refused pin as an API error; the explicit path is pinned as not carrying it. Evidence: composable red first (the just-read list discarded), the type red before it existed, the Legacy view pin red against a reverted predicate; 3 files / 201, 37 files / 531 across the Review composables, views and API specs, the Paper review specs 14 files / 346 unchanged; typecheck, ESLint, build, diff check. Not verified: Playwright; the real backend's 400 for a non-GUID id (reasoned from the controller attributes, never observed); a Paper assertion naming 400 (that spec is held by `#2629`). One fresh-context review SHIP; two comment LOWs taken in round 2; two MEDIUMs and a LOW recorded on `#2214` for item 2 (a 400 on the list leg, from a non-GUID `boardId` query, still freezes the poll silently; the unavailable-panel copy is wrong for a malformed id that never named a proposal; the explicit path still toasts a generic failure for the same hash). +- **The similar-past and confidence cards tell the truth before their disclosures open (`#1940`'s `#2166` retained residual, PR `#2662`, merge `68a8f888c`; `Refs #1940`, whose D-9 questions stay with the maintainer).** `ReviewSimilarPast` rendered a "Show similar decisions" expander unconditionally and put "No comparable past decisions" inside the collapsed region, so a reviewer had to open a control to learn there was nothing to open; the sentence now renders at card level at first paint when there are no rows, the disclosure stays (removing it would have broken the held `PaperReviewView` spec, the language spec and the required E2E) with an honest label that extends the base label, and the region opens onto a distinct explanation line rather than a duplicate or nothing (the required E2E asserts the region is visible after the click and a zero-height element is not visible to Playwright). `ReviewAuthorCard` kept the confidence-source sentence inside its collapsed region, which on an Applied record was the only copy on screen because `ReviewMain`'s primary badge is gated off there; the sentence is hoisted above the disclosure whenever the breakdown has no components, the region keeps heading, bars and note, and the heading derives from the rendered bars so the fixture-only model-reported-with-zero-components pair can no longer contradict itself. Both regions gain the `:hidden` binding their sibling `ReviewProvenance` carries. Two new component specs (none existed; the only coverage was one shared assertion in the why-now spec). Evidence: 11 red-first failures across the two new specs, then 4 files / 39 with the catalog parity spec; the regression gate on the unedited held specs 2 files / 182; the Paper review directory 16 files / 362; typecheck, ESLint, build, diff check. Not verified: the required E2E locally (it runs in the E2E smoke lane, including the axe scan over both cards); a visual check; Spanish and Italian tone. Recorded on the issue: while proposal B loads, the rail still shows A's confidence and similar-past rows under B's identity (needs `PaperReviewView.vue`, held by `#2629`). One fresh-context review SHIP with a MEDIUM taken in round 2 where reachable: the hoisted sentences asserted emptiness while the selector batch was still in flight or after it failed (the composable leaves both refs at empty defaults on a failed batch with no flag, so a failed similar-past read rendered "(none found)" as a positive claim); round 2 found the only settled signal lives in `PaperReviewView.vue` (held by `#2629`; the rail is a pure props component with no loading input), so the cards now document the three situations that arrive as the same empty value and the residual is recorded on `#1940` with the fix direction; the copy predates the PR, which exposes an existing false claim rather than creating one. + +Inbox frontend: +- **The Inbox poll-truth residuals close: the foreground detail state is pinned, the per-tick count refresh is pinned, and the two generation maps are cleared at logout behind a session epoch (`#2571`, PR `#2631`, merge `6f665bcc7`; closes `#2571`).** Of the six residuals from PR `#2567`'s review, one is a behaviour change: `captureStore.resetForLogout()` clears `latestSummaryGenerationById` and `latestDetailWriteGenerationById` and advances a session epoch that every detail-path read captures when it issues its request (`fetchDetail`, which also serves the poll's and the batch's terminal-detail reconciliation, and the single-item triage poll's read), so a read still in flight across a logout is dropped before any generation compare; the epoch exists because clearing alone inverted a guard (a first-open read with observed generation 0, crossed by a write and a logout, compared 0 with 0 and cached the pre-write body; round-1 MEDIUM). The call sits next to `workspace.resetForLogout()` in the shell's session watcher; eviction happens at logout only and in-session growth stays one entry per touched capture id. The rest are pins over behaviour that was already correct and are recorded as such: `refreshTerminalDetails` keeps `trackLoading` false for the foreground batch reconciliation because `loadingDetail` is one store-wide boolean (a two-id fixture with a concurrent genuine detail load proves the first settled leg cannot clear a flag the genuine load owns; the Legacy list panel's `batchBusy` "Processing" state is the batch's feedback, and Paper binds neither); the count refresh already fires at most once per poll tick behind the observed-new-outcome flag (the issue's ~20 calls are ~20 ticks each observing one outcome); a poll stopped by its caller or aborted by the 60 s deadline mid-flight does not clear a standing foreground `listError`; the `#2304` spec comment no longer says "an unrelated open detail". Evidence: the reset and wiring red first in round 1; the two epoch specs red first against the round-1 sources (the pre-write body was cached); the three pins proved load-bearing by temporary store mutations; round 1, 9 files / 348 across every spec that mounts `AppShell` or exercises a logout reset and 61 files / 1171 across the Inbox, resilience and property sets; round 2, 5 files / 279; typecheck, build, ESLint, diff check. Not verified: Playwright; the full frontend suite. One fresh-context review SHIP (a MEDIUM and three LOWs taken in round 2) and one verification pass scoped to the round-2 diff, SHIP with a post-logout-only MEDIUM and four LOWs tracked as `#2640` (the list snapshot path does not capture the epoch, so an older in-flight background snapshot can win in the sub-millisecond window between the reset and the Inbox unmount; dropped reads are unreported to callers; the docstring overstates `peekDetail`; a spec mock hygiene note). +- **The session epoch covers the batch poll's list snapshot, dropped detail reads are reported, and the spec mock hygiene is fixed (`#2640`, PR `#2659`, merge `a1f1d0f63`; closes `#2640`).** The five post-logout-only edges from PR `#2631`'s verification close: the batch poll's tick captures the session epoch when it issues its request and its currency check carries it, so an older in-flight background list snapshot issued before a logout can no longer win the summary-side stale-high guard after the map clear (the spec that asserted the regressed behaviour as intended is retargeted, with a sibling proving a snapshot issued after the logout applies normally; the post-logout contract for both guard halves is now the same: a response issued before the reset is dropped, one issued after it caches normally); `refreshTerminalDetails` fires `onRefreshed` only when the read cached; `fetchDetail` reports its outcome through an additive `onCacheOutcome` callback (not a return-type change, because a Paper row editor awaits it as a `CaptureItem` and views were out of scope), and `selectItemById` restores the pre-call selection and returns false only when a dropped read left no detail for the id, so the Legacy panel's "Unable to load capture detail." cannot render for a read that succeeded but was dropped; the `resetForLogout` docstring names the exact epoch-capturing reads and states that `peekDetail` writes neither cache; the spec's persistent never-resolving mock became per-test setups plus a settles-versus-hangs guard. Evidence: four red-first outputs (`expected 'Triaging' to be 'Failed'`, `expected "vi.fn()" to be called 1 times, but got 2`, `expected 'dropped' to be 'kept'`, `expected 'hangs' to be 'settles'`); 5 files / 285 across the store, orchestrator, Inbox view and shell specs; 61 files / 1178 across the Inbox, resilience and property sets; typecheck, ESLint, build, diff check. Not verified: Playwright (no E2E crosses a logout with an in-flight Inbox read); the Legacy detail panel post-logout in a real browser; the full frontend suite. One fresh-context review SHIP with two MEDIUMs taken in round 2: the dropped-read restore branch was reachable in a live session (a first-open detail read crossed by a batch triage that includes the item resolved as not cached with an empty detail, stripping a deep-link hash and turning a click into a silent no-op), so the store now reports the drop reason and a generation drop with no detail re-reads once, only the epoch case restores and returns false; and the restore had set the selected row without the active row, desynchronising the list from the panel. Round 2: `fetchDetail` reports a `DetailCacheOutcome` (`cached`, `superseded`, `generation`, `epoch`) through the additive callback; `selectItemById` re-reads once on a generation drop with no cached detail, keeps the selection on a superseded read, and restores the previous selection and active row only on the epoch case, so every live-session drop either opens or recovers and the restore path is reachable only after a logout; three orchestrator specs red first (the deep-link hash kept and the click opening after a batch write crossed the read; the logout case restoring both rows), 5 files / 288 and 61 files / 1181 at the round-2 head. One verification pass scoped to the round-2 diff, SHIP; a contract-doc MEDIUM recorded for the next touch of the store (the outcome named `superseded` is also what a poll-owned read crossed by a logout reports, because the poll's currency check is tested first; no live path reads the distinction today) and two LOWs. +- **A superseded Paper Inbox history-detail load can no longer mask or unspin the reopened one (`#1999` recorded race, PR `#2668`, merge `0b13caf71`; `Refs #1999`, which stays open for item 2's ruling and for item 3 behind another session's `#2654`).** `PaperInboxView`'s history-detail loader guarded its three writes only on the item id, so opening a row, closing it with the `#2426` control and reopening the same row let the first read's rejection write a stale error that the table's error branch ranked above the successfully reopened detail, and let the first read's completion end the second read's spinner. A monotonic request generation is captured per load and gates the detail write, the error write and the loading end together with the id; Close advances the generation before clearing, so a settle after Close writes nothing, and the archived-history and board-scope watcher inherits that through the same function; the `#2426` regression (late payload hidden, one read) is unchanged. Evidence: two red-first specs (the stale error rendered; the superseded payload rendered and the spinner ended), then 7 files / 209 across the Paper Inbox view and table specs (210 at the round-2 head with a cross-item supersession net); typecheck, ESLint, build, diff check. Not verified: Playwright; the full frontend suite. One fresh-context review SHIP with LOWs only (comment precision and two test assertions taken in round 2; the `closeLoadingDetail` comment in the held `PaperTriageTable.vue` that still credits the id check alone is recorded for that file's next touch). + +Shortcuts and help: +- **Board keys stay behind an open modal and the shell key guards are tightened (`#2621`, PR `#2635`, merge `86ca9078c`; closes `#2621`, `Refs #1968` for its keyboard-guard LOWs).** With the shell help dialog open over a Legacy board, `f` toggled the filter panel behind the modal and `n` pulled focus out to the add-card input, because `AppShell`'s capture-phase keydown handler returned without stopping a key no shell binding consumed. The shell now stops such keys while a keyboard-owning surface is active (an open native dialog, an alertdialog, or an ARIA dialog explicitly marked modal; the bare `role="dialog"` branch is gone, because the Paper desktop card inspector uses that role without `aria-modal` and the round-1 gate had made `?`, `mod+k` and quick capture dead while a card was open in it, the 2026-08-29 `#1968` MEDIUM), with two carve-outs: Escape is never stopped (the board's own close paths need it) and a keydown whose target is inside the surface is untouched. Per-action ownership through a `data-shell-surface` marker on both help twins and both command palettes: `?` toggles the help dialog it owns and never opens over another modal; `mod+k` still closes the palette it owns and never opens over another modal; quick capture never stacks (the `#1968` LOW that `?` and `mod+k` fired over modals). `strokeMatches` requires Shift up for a bare-letter binding that declares none (Shift+H no longer navigates Home) but not for punctuation, since `?` is typed with Shift on most layouts; a layout-produced `?` ignores Alt, and Ctrl while Alt is also down, so AltGr layouts reach the help key (the `#1968` AltGr LOW). The keyboard-owning-surface scan is memoised per event so typing no longer sweeps the DOM on every keystroke (the `#1968` scan LOW, taken as lazy evaluation rather than an editable-target short-circuit because `mod+k` is allowed in text entry and needs the surface state inside a modal textarea). The overlay spec's source-text assertion is replaced by running the shipped matcher over each displayed shell row's advertised stroke. Known residual, tracked as `#2636`: a key pressed with focus inside the surface still bubbles to page-level listeners; closing that needs a document-level bubble listener wider than `#2621` authorizes. Evidence: `AppShell.spec.ts` red first (6 failed / 41 passed: the filter toggle behind the dialog, the palette and quick capture over a modal, `?` over another modal, Shift+H and Shift+T navigating, AltGr `?` never opening) and the scan pin red against `main`; 14 files / 195 across every spec that mounts `AppShell` or drives a palette; the whole unit suite 371 files / 5741 passed / 3 skipped; typecheck, build, ESLint, diff check. Not verified: Playwright; a physical AltGr or Shift+H in a real browser (both rules are proved against synthesized `KeyboardEvent` flags); the board probe in the shell spec uses the real `useKeyboardShortcuts` composable and the board's own bindings rather than a mounted `BoardView`. One fresh-context review FIX-FIRST (the inspector regression, HIGH, plus two spec-harness MEDIUMs: tautological matcher loops and a detached mount that hid the surface gate) taken in round 2 with a red-first non-modal `role="dialog"` probe, and one verification pass scoped to the round-2 diff, SHIP with three LOWs recorded (`dialog[open]` would also match a non-modal native dialog, none exists; `?` closes a background help dialog under a clicked-open palette; the ledger walk lacks a default arm); the final selector is `dialog[open], [role="alertdialog"], [aria-modal="true"]`; the two tautological matcher loops became descriptor-driven assertions that can fail (the printed key and the dispatched stroke must agree) plus a walk over every shell binding through the mounted shell asserting the ledger action ran; the specs that rely on the surface gate mount attached, and the stacked-surface case is built by mouse because a keyboard-built stack is no longer reachable; ownership is order-independent (a key is allowed when some open surface is the action's own and every open surface carries the shell marker; any unmarked surface takes every shell key) rather than topmost-wins, because Teleport anchors sit in template order, not opening order. Round-2 evidence: the non-modal probe red first (`?` dead with the inspector open; `mod+k` unable to close the palette); 18 files / 391 across the shell, shortcut, board and card specs; the whole unit suite 371 files / 5753 passed / 3 skipped; re-proved locally after the base moved under it (`#2631` touched `AppShell.vue`; 14 files / 348 across the shell, shortcut, board, card and capture-store specs at the updated head); hosted CI green at the exact head. + +Board frontend: +- **Titles-only card density on the Paper board (`#2090`, PR `#2666`, merge `540bc2abb`; `Refs #2090`, which stays open for one routed inspector residual (a pending navigation is cancelled rather than continued after a successful save while the discard dialog is open) and the control-localization, global-key and card-cap residuals recorded on the issue).** The last open acceptance item on `#2090` after width presets (`#2158`) and accessible column collapse (`#2165`) shipped: a `td.paper.board-card-detail.v1` preference (`full` or `titles`, total type guard, hydrated and persisted like density, browser-local, board-agnostic like the other two), a `data-card-detail` attribute on the board root, and scoped rules that hide the card excerpt and meta under titles-only so the card DOM, its opener and its drag handle stay intact and column counts and drag order are unaffected; a toggle beside the density toggle with `aria-pressed`, the same keydown stops so the board keymap does not double-handle, no shortcut, localized in en, es and it. Paper-only by the issue's own delivery history and the rc.1 note (a named assumption; the general Legacy question is D-3(a)). Evidence: six mounted specs red first (persist, hydrate on remount, invalid stored value falls back, title and count and opener and drag handle present under titles, drag reorder under titles with the attribute asserted so it cannot pass vacuously, localized label and accessible name); 4 files / 126 across the board, card, management and catalog specs; typecheck, ESLint, build, diff check; an E2E leg inside the existing Paper board axe test at 1280 and 390 by 844 (written, not executed locally; the required E2E smoke lane runs it). Not verified: the hiding itself in a browser (jsdom applies no scoped CSS; the mounted specs prove the attribute and the intact DOM). Residuals recorded on the issue: the existing width and density controls are still English-only, the collapse set is keyed globally, the Wide preset widens the lane but not the 248 px card. One fresh-context review SHIP; three MEDIUMs taken in round 2 (the masterplan and revival plan lines saying titles-only was open, the closing link replaced by Refs because of the undelivered routed residual, and the Test plan stating that the hiding is proven only by the E2E leg in the required lane) with three LOWs (a vacuous hidden assertion, the catalog docblock, the E2E restore in a finally). Round 2 also corrects `docs/IMPLEMENTATION_MASTERPLAN.md` and `docs/REVIVAL_PLAN.md`, which said titles-only was open, in the same PR; 2 files / 74 at the round-2 head with the docs governance check green. + +Guards, tests and docs: +- **Wrapper buttons are scanned for dead affordances and the primary-action registry names its inventory (`#1949`, PR `#2663`, merge `34cb4b8bd`; `Refs #1949`, which stays open for AC4's route-walking contract and the recorded parser LOWs).** The dead-anchor guard matched only native `button` and `form` tags, so an enabled `PaperHLBtn`, `TdButton` or `TdIconButton` call site with no click binding shipped green (the `#1941` class; about 150 call sites across 35 files were unscanned). The guard now scans those wrapper tags: a call site is dead unless it binds `@click` or a `v-on` object with a `click` key, is a static `type="submit"` with a proven form owner, is permanently disabled, or carries the visual-specimen marker; an untyped wrapper inside a form is not a submit (the components default `type` to button), and a pointer-only listener stays reported because a button reachable only by pointer is not keyboard-operable. The repo-wide scan found seven offenders, all intentional style-guide specimens, now marked; zero product offenders. The `guardedPrimaryAction` registry (two entries since `#2514`) now states its inventory (a primary action writes or enqueues) and registers the reachable preconditions of the Paper triage row editor's save, the capture nib's submit and the triage table's board pick, each pinned to its disabled branch and reason; the header lists every precondition of each control and marks the ones not registered. The keystroke half of `#1949` is descoped to the shortcut-ledger guards (`#2615`, `#2635`). Evidence: the repo-wide scan red before the markers (exactly the seven specimens); mutation checks (finder returning nothing fails six tests, exemptions disabled fail four, forcing the registered controls enabled fails seven of eight registrations); 4 guard files / 45; 3 inbox specs / 95; typecheck; ESLint; `npm run lint` 0 errors; build; diff check. Not verified: the style-guide view in a browser (it has no spec); the full frontend suite. One fresh-context review SHIP; two MEDIUMs (the inventory header overstated completeness; `v-on` object syntax redeemed a wrapper on any key) and four LOWs taken in round 2. Round 2: a `v-on` object redeems a wrapper only when it spells out a `click` key (a spread-only or identifier value is reported, matching the native rule for unprovable listeners); the inventory header lists every conjunct of each control and marks the unregistered ones; `TdIconButton` (no `type` prop, hard-coded button) no longer counts as submit-capable; the kebab spellings are scanned; the vacuity canary counts call sites (156, threshold 100); 4 guard files / 47. + +Platform lane (beta-platform-integrity, recorded from the lane's own merge packets, verbatim facts): +- **The unknown-exception boundary guard closes the five precision gaps from PR `#2602`'s reviews (`#2606`, PR `#2617`, merge `9cf28bbc6`; closes `#2606`).** Condition polarity including `== false` and `is false`; taint on every value-carrying initializer but not on comparison or length reads; `ex?.Message` matched; sanitizer wrappers matched as exact `Type.Member` pairs, so `StripControlChars` or an unrelated `.Redact` no longer excuses exception text. 69 node tests; the real tree still yields zero findings; the allowlist is untouched. Exact head `f8301bf5e`. +- **The CLI failure sink evicts only after a successful write and never reports a written record as not captured (`#2577`, PR `#2619`, merge `679a8235a`; closes `#2577`).** The correlation reference is validated (12 or 32 hex, either case, the same predicate `CliStartupTrace` uses) before any filesystem call, so traversal fails open; the argv retention policy keeps only command words and flag names and replaces every value, attached or dash-leading with whitespace, with `[value]` before the redactor runs, documented exactly in `docs/security/SECURITY_LOGGING_REDACTION.md`. Exact head `0ea70d6ce`. +- **The PWA cleanup and share-target listeners attach ahead of the AMD factory (`#2639`, PR `#2660`, merge `58c5a3bfe`; hardening).** The emitted `importScripts` call for `api-cache-cleanup.js` and `share-target-handler.js` is hoisted to offset 0 of `dist/sw.js` by a fail-closed post-build rewrite (`src/pwa/hoistWorkerImportScripts.ts` from `vite.config.ts`), so the cleanup and share-target lifecycle listeners attach synchronously ahead of vite-plugin-pwa's AMD factory instead of from its promise continuation; the generated-worker suite pins the placement structurally (9 of 9, red on the pre-hoist build); the `#2475` browser-level regression for the install-to-activation window ships without `test.fail()`, waits for `active.state === 'activated'`, and is red only when the forced activate-time sweep is removed. Not measured: non-Chromium engines; residual: Workbox's own precache install and `cleanupOutdatedCaches` activate handlers remain inside the AMD factory. Exact head `3a676efc1`. +- **The CLI connector encryption key file is created atomically with owner-only permissions (`#1262`, PR `#2665`, merge `1611cc855`; closes `#1262`).** `backend/src/Taskdeck.Cli/RestrictedFileWriter.cs`, a character-identical copy of the API's `#1264` helper: `FileMode.CreateNew` with `FileShare.None`, Unix 0600 pinned through the open handle, a Windows owner-only protected DACL supplied at creation and read back so non-ACL filesystems fail closed, the payload written through the same handle, then the existing atomic move; before, the key was written 0644 and chmod'ed afterwards on Unix and inherited the directory DACL on Windows. Red-first tests on the unmodified bootstrapper, Cli.Tests 228 of 228 on both CI legs, full backend solution 8886 passed / 0 failed. Not shipped, tracked in `#2667`: forward remediation of key files created before the fix, an API/CLI helper parity guard, and the doc note that a FAT32, exFAT or SMB data directory now yields a per-run transient key with a warning. Exact head `21697ef47`. +- **The worktree-helper regression suite waits on a deadline instead of fixed polls (`#2664`, PR `#2669`, merge `140213288`; closes `#2664`).** `scripts/git/Test-New-CodexIssueWorktree.ps1` waits on a 90 s deadline for the initializer's scheduled orphan-worktree removal instead of fixed 5 s and 10 s polls, with the elapsed wait and the current `git worktree list` in the failure message; the assertion still fails on a genuine orphan. This closes the Docs Governance / Worktree Helper (Windows PowerShell) red first seen on PR `#2662` (a detached cleanup host running seven Git invocations exceeded 5 s on the hosted runner). Native-host suite 29 of 29 and the hosted job green at the head. Follow-ups in `#2670`: a vacuous registration assertion, a 500 ms settle that cannot detect a wrongly scheduled cleanup, and a 10 s askpass readiness deadline. Exact head `eb9a67f39`. + +- **Not shipped reality:** Open at the time of writing on the alpha lane: the `#1949` AC4 route-affordance inventory (PR `#2673`, open in review at the time of writing; a closed inventory of 12 routes with 2 to 5 affordances each, a 21-route exclusion map with reasons, a vitest guard equating inventory plus exclusions to the named `/workspace` routes, and one Playwright walk budgeted at four tests in the required E2E Smoke lane; the lane's measured duration is reported on `#1872`). Held behind another session's open PRs, which the D-12 interim rule leaves to their author: `#2629` on `PaperReviewView.vue` and its spec (blocking the second `#2214` slice, items 2, the malformed-id copy and the explicit-path toast, the Paper-side `#2215` residuals, `#2638` item 1, and the two `#1940` residuals), and `#2654` on `PaperTriageTable.vue` (blocking `#1999` item 3 and the picker E2E leg). Waiting on the maintainer's K-section rulings: `#2007` AC2 and AC3 (D-5), `#1284` AC4 (D-7), `#2009`'s palette anchors and the 1.4.11 question (D-6; the three measured candidates are on the issue), `#1999` item 2 (Triaged-row edit in Paper, handed to the coordinator on 2026-09-05). Tracked non-blocking residuals opened this wave: `#2636` (keys pressed with focus inside a shell surface still reach page-level listeners), `#2638` (the decision rail's inert sticky rule; recovery text retired by explicit loads), `#2640` (closed by `#2659`), plus the notes on `#2214`, `#2090` (control localization, global collapse key, the 248 px card cap, the pending-navigation residual) and `#1940`. Control-plane PRs stay parked under SC-10 for the maintainer. + +Corrections to earlier blocks in this file: The platform lane's `#2639` re-measurement (2026-09-05) found that the activate-time forced static-cache sweep from PR `#2416` does run (three of three lifecycle paths in Chromium); the earlier "window still open" reading came from a test that asserted before the sweep finished, so the `#2416` entries are not overstated on that point. The three corrections to the `#2416` block from PR `#2660` (merge `58c5a3bfe`) follow in this paragraph verbatim from that lane's merge packet. The sentence "The parked PR's missing real-browser proof ran and found two defects the mocked unit tests had hidden: the cleanup script's `activate` listener never received the event under vite-plugin-pwa's asynchronous AMD wrapper, and the install-time sweep that replaced it left a re-poisoning window before activation while the old worker still controlled the page." now reads "The parked PR's missing real-browser proof ran and found the defect the mocked unit tests had hidden: the install-time sweep left a re-poisoning window before activation while the old worker still controlled the page. Its second reading, that the cleanup script's `activate` listener never received the event under vite-plugin-pwa's asynchronous AMD wrapper, did not reproduce on re-measurement (PR `#2660`: the listener fires 3 of 3 on three Chromium lifecycle paths); what was real is that the listeners were attached inside the AMD factory's promise continuation, an ordering no specification promises, and PR `#2660` removes that dependency by hoisting the emitted `importScripts` call to the top of `dist/sw.js`."; the clause "plus a forced re-sweep inside `waitUntil` at activation that rejects activation on failure;" now reads "plus a forced re-sweep inside `waitUntil` at activation that completes before `clients.claim()`; a rejected activate `waitUntil` does not abort activation, so on failure the old entries stay in place and `clients.claim()` is skipped;"; and "**Not verified:** a browser-level regression for the install-to-activation window (`#2475`)." now reads "The browser-level regression for the install-to-activation window (`#2475`) landed in PR `#2660`: it waits for `registration.active.state === 'activated'` and is red only when the forced sweep is removed." Three shipped-reality gaps found by the lane's 2026-09-05 read-only audits, recorded here because the earlier blocks never carried them: (a) PR `#2426` (merge `ea2ab3e68`, 2026-09-03, `#1999` item 4) shipped the stalled-detail Close control on the Paper Inbox (a `capture-history-loading-close` control while a detail load is pending, focus restored to the opener, a late payload rejected by item id) and nothing in this file said so; (b) PRs `#2158` (Paper board column-width presets, persisted per browser with a total type guard) and `#2165` (merge `a90971162`, accessible column collapse with focus return, counts, drag order and realtime-safe keying by column id) shipped on `#2090`, and the 2026-08-28 checkpoint sentence "`#2090` remains Blocked behind parked PR `#2165`" is stale as a present-tense claim, the maintainer having merged `#2165`; titles-only density shipped later that day in PR `#2666`, recorded above; (c) PR `#2166` (the three Review disclosures: confidence, provenance, similar past) is the baseline the `#1940` honest-empty slice corrects and was never recorded either. + v0.3.0-rc.1 SHIPPED (2026-08-30, annotated tag `3fc9f6e8e` peels to `9d2ea3c7c`): - **The public v0.3.0-rc.1 pre-release exists**, cut by the agent under the maintainer's v0.3 RC deck reply q-1 A (2026-08-30; map `map:v1:bec0a8dd…dd9138`; record `#1947`) and the repository's declared authority. The GitHub Release is `prerelease=true`, `draft=false`, published 2026-08-30T02:26:06Z with three assets — `taskdeck-v0.3.0-rc.1-win-x64.zip` (53,916,746 bytes), its `.sha256` sidecar, and `taskdeck-v0.3.0-rc.1-provenance.txt` — and a composed page (download badge first, RC banner, SHA-256, quick-start link, `## Breaking changes` lifted from UPGRADING, `## Highlights` from `docs/releases/notes/v0.3.0-rc.1.md`, grouped `## What's changed`). `/releases/latest` still resolves to `v0.2.0`. - **Tag workflows:** CI Release 33287786328, Release Security 33287786318, Release Container 33287786267, Release Desktop 33287786253 — all four success. **GHCR:** `ghcr.io/chris0jeky/taskdeck:0.3.0-rc.1` published (`sha256:d47bdf2d…2db67`), `latest` and `0.2` both still `sha256:e4915d72…8c752`, and no `0.3` alias exists — the floating `latest` / `0.2` index digest is unchanged from the pre-tag capture (`sha256:e4915d72…8c752`) and no `0.3` alias was created, which is the live proof of `#2217`/PR `#2223` that the threat-model row was waiting for.