Commit Graph

1127 Commits

Author SHA1 Message Date
xarmian ede34e9086 fix(web): the balanced fields grid follows the item body, not the viewport (TASK-2209) (#1897)
In the docked pane (~400px on a 1440px screen) the balanced layout's
two-column fields grid stayed two columns, because it collapsed only on a
viewport media query, and its bare 1fr tracks let a select's and the tag
input's min-content push the grid to 476px: the right column ended at
x=1492 on a 1440px viewport.

.item-body.layout-balanced is now an inline-size query container and the
grid drops to one column below 560px of body width, with minmax(0, 1fr)
tracks. Measured: pane widths 312-461px one column with no overflow,
583px+ two columns, full page at 1440 two columns of 444px, mobile one
column; the 559.98/560 edge is clean; a BottomSheet opened inside still
covers the viewport.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 13:55:45 -04:00
xarmian 6735b43450 feat(web): cap the dashboard's attention and plan lists, with a Show-all toggle (TASK-2210) (#1896)
On a mature workspace "Needs Attention" and "Active Plans" rendered every
row (76 and 23 on the docapp workspace), burying Up Next and pushing
every later section screens down. Attention now shows its 6 most urgent
rows (overdue, blocked, needs a human, stalled, plan done, no parent)
and plans its first 5, each with a "Show all (N)" toggle that is
remembered per workspace in this browser. The API still returns the
full lists.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-08 13:51:19 -04:00
xarmian 63e2830c75 fix: the Live badge reflects a dead SSE connection, and the reconnect catches up (TASK-2197) (#1895)
* fix: the Live badge reflects a dead SSE connection within 75s or at once on offline, and the reconnect catches up (TASK-2197)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): never open the SSE stream while the browser is offline; pin heartbeats on a busy stream (TASK-2197 codex r1)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 13:41:39 -04:00
xarmian 25dca945e5 fix(web): list and table j/k step the rows on screen (BUG-3492) (#1894)
The page stepped `filteredItems` (updated order) while the list renders
each group in its sort mode's order and the table in its sort mode or
column sort. After an edit to an older item the two disagreed, and j
landed on the edited item instead of the row below. ListView and
TableView now report the ids they render (collapsed groups and drag
shadows excluded), and j/k step that order through listKeyNav, the way
the board steps BoardView's columns.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-08 13:40:12 -04:00
xarmian f2a3a28e46 fix(web): offline edits are protected honestly: the badge says they live in this tab, closing or leaving asks, a discarding reload hands them back (TASK-2199) (#1892)
* fix(web): offline edits are protected honestly: the badge says they live in this tab, closing or leaving asks, and a reload that discards them hands them back to copy (TASK-2199)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): a copy that resolves after an item switch clears only the version it copied (TASK-2199 codex r1)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 13:11:16 -04:00
xarmian 34d17e44f1 fix(web): an item load no longer waits for the index to catch up (BUG-3473) (#1887)
* fix(web): an item load no longer waits for the index to catch up (BUG-3473)

ItemDetail awaited localIndex.bootstrap so wiki-links resolve against a
populated index (BUG-1461). On an index that is already ready with a
resync pending, bootstrap returns the reconcile, which reads
/items-changes until a page comes back empty; in a workspace others
are writing to, that chased new rows for seconds while the pane sat on
its skeleton. The call is still made (it schedules the catch-up) but is
awaited only when it is what populates the index.

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

* test(e2e): a doc this tab just created resolves as a link in a pane opened straight after (BUG-3473)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-08 12:17:51 -04:00
xarmian 1ed71b0ecf feat(web): the playbook editor and collection dialogs keep a draft; Save stays; the arguments section is edited in place (TASK-2191) (#1882)
* feat(web): the playbook editor and Create Collection keep a draft; Save stays; the arguments section is edited in place (TASK-2191)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* feat(web): the Edit Collection dialog keeps edits; classify its baseline continuation (TASK-2191)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): a close during an in-flight save is not a discard; playbook e2e specs name Save or Save and close (TASK-2191)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): a CRLF playbook body keeps its arguments section intact and its line endings (TASK-2191 codex r1)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): a save re-baselines from what it sent, so an edit typed during it stays unsaved (TASK-2191 codex r2)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(web): the playbook identity gate re-reviews save() for the sent-form baseline (TASK-2191)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(e2e): TASK-2186's close leg accepts the discard confirm an edited form now raises (TASK-2191)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 11:30:54 -04:00
xarmian ee288cbebd fix: item lists refuse a filter key that names no field (BUG-3480) (#1880)
* fix: item lists refuse a filter key that names no field (BUG-3480)

The item list endpoints read every query parameter they did not know as
a field filter, so a parameter the server never had (the web Library
page's `all`, WebMCP's `collection`) answered 200 with an empty list.
A filter key must now be declared by a schema in scope (the
collection's, or any visible collection's on the workspace-wide list),
or be stored by an item in scope, which is honoured and named in the
X-Pad-Undeclared-Filter-Keys header (printed to stderr by the CLI).
Otherwise 400 validation_error, "invalid list filter ...". A key that
is not a field key is refused too; the store used to drop it and
answer the unfiltered list. WebMCP pad_item:list now lists a
collection through its path. ToolSurfaceVersion 0.73.

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

* test: type the listByCollection mock so svelte-check can index its calls (BUG-3480)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-08 10:43:33 -04:00
xarmian 7b68125a30 feat: schema edits say what they do to items; removing an option is never a rename (TASK-2188, TASK-2187) (#1877)
* feat: schema edits say what they do to items, and removing an option is never a rename (TASK-2188, TASK-2187)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix: import the field-usage type; keep the collection tool description inside the tools/list budget (TASK-2188)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix: the counts landing no longer close an open schema-edit confirm; pin that an option added after a removal is no rename (TASK-2188 codex r1)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(e2e): the BUG-2129 migration-race spec confirms the rename before its PATCH (TASK-2188)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 08:29:44 -04:00
xarmian c81b5c0726 fix: owners cannot lock themselves out by mistake; accounts with no password can set one and turn 2FA off (TASK-2190) (#1879)
* fix: owners cannot lock themselves out by mistake; accounts with no password can set one and turn 2FA off (TASK-2190)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(web): the settings identity gate re-reviews the member handlers; PadApiError on its own import line (TASK-2190)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 07:41:35 -04:00
xarmian 03a3617dbb fix(web): Library, Playbooks, Conventions and the playbook editor find their collections by trait (BUG-3481) (#1876)
* fix(web): Library and Playbooks pages find their collections by trait, so a renamed conventions/playbooks collection still works (BUG-3481)

The web half of BUG-2702: both pages addressed the conventions and playbooks
collections by their default slugs, so a workspace that renamed either (a
documented onboarding step) got a Library page listing nothing Active with
Activate hidden, and a Playbooks page listing nothing with Create and Import
hidden. A small resolver (lib/collections/artifactSlug.ts) finds each by its
artifact_kind trait, as the server has since TASK-2657, falling back to the
default slug until the collection store holds the workspace; both pages key
their loads on it, so an unrenamed workspace still loads once.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): Conventions page and playbook editor by trait too; slug-aware stale guards; Playbooks page test (BUG-3481)

codex r1: the Conventions page and the playbook editor addressed the
collections by literal too; the Playbooks page's loads had no guard against
a superseded slug's response. All now resolve by the artifact_kind trait,
drop a response whose slug moved, and the editor ensures the collection
list before refusing a non-playbook.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(web): identity gates re-reviewed for the trait-resolved slugs (BUG-3481)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(web): renamed-collection coverage for the conventions create and the playbook editor check (BUG-3481)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 05:23:07 -04:00
xarmian 15c9f76366 feat: archived collections can be listed, restored and undone; the archive confirm shows the count (TASK-2189) (#1874)
* feat: archived collections can be listed and restored (TASK-2189, server, CLI, MCP v0.72)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* feat(web): archive shows the item count, guards big collections, and can be undone or restored (TASK-2189)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix: list-archived is read-only, an unknown count needs typing, instructions say v0.72; store tests on both dialects incl. a restore race (TASK-2189)

codex r1.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test: README tool-surface version 0.72; classify the archive Undo's restore continuations (TASK-2189)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 03:59:26 -04:00
xarmian 28501155b1 feat(web): review and accept built-in updates from the Library page and the item pane (TASK-3462 U3b) (#1873)
* feat(web): the Library page matches built-ins by key and badges an update on offer (TASK-3462 U3b)

GET /builtins joins the page's load. An entry is Active when an item made
from it exists (by key, so a rename no longer re-offers it), or by the old
title match for items with no recorded origin (legacy, until U4). An entry
with an update on offer shows 'Update available' (unedited copy) or
'Library changed' (edited), linking to the item. The identity gate rows
are re-reviewed, and builtinEntries is page data replaced on both arms.

* feat(web): the item pane offers a built-in's newer library text for review (TASK-3462 U3b)

BuiltinUpdateNotice reads GET /items/{ref}/builtin for items in a system
collection and, when an update is on offer, shows a badge that opens a review
dialog:
- an unedited copy: its text against the library's;
- an edited copy: what the library changed and what you changed, side by side;
- unknown origin: a plain two-way comparison that says it may have been edited.
It lists the settings accepting would replace and says only the body survives
in history.

Accept sends the previewed expected_seq. content_pending_flush turns the
button into an explicit discard; update_conflict refreshes the preview.
Nothing updates on its own.

The notice is fenced with identityFence(), joins the child-population guard,
and is mounted under the identity key with onUpdated refusing a stale key.
Behaviour tests: 8, with two mutants killed.

* fix(web): built-in update review never reloads over the pane, refuses with unsaved edits, and stays on its item (TASK-3462 U3b, codex r1)

- No callback into the pane after Accept. A loadData() there flushed a
  pending raw draft over the accepted text, and cancelled the collab editor's
  materialization flush. The item's own item_updated refreshes the pane, and
  the notice re-reads when seq moves.
- Accept refuses while the pane holds unsaved edits (a pending raw draft, a
  save in flight), asked synchronously at the press.
- A response for an item the pane has left commits nothing: the press checks
  its ref, and the mount is keyed by item id.
- The dialog closes when its offer goes away, so it is never left empty.
- After content_pending_flush the copy says the discarded edits are not kept.
- The Library page offers unknown_origin too ("Library version differs").

The four new behaviour tests fail on the previous component.

* fix(web): built-in update review reloads raw mode only, waits for unsynced collab edits, and survives a rename (TASK-3462 U3b, codex r2)

- Raw mode keeps its body on an SSE refresh, so an accepted update now
  reloads the pane there. The pane decides: not in collab mode, where a reload
  cancels the editor's flush, and not over a pending raw draft. The callback
  refuses a stale identity key (class-C table row).
- Unsaved edits now include local collab edits not yet flushed (localDirty)
  and a collab provider that is not synced: edits the server's
  content_pending_flush cannot see.
- A press tracks the item by id, so a rename during the request no longer
  leaves Accept disabled.

* fix(web): built-in update guard uses the provider's sync state, not localDirty; a conflict refresh uses the live ref (TASK-3462 U3b, codex r3)

localDirty is not cleared when a collab flush is deduplicated, so it could
block Accept until a reload. A synced provider's typing is already in the
op-log, where the server's content_pending_flush refusal sees it; an unsynced
one (buffered pre-anchor, or dropped offline) is what the client must refuse.
After a rename, the update_conflict refresh now reads by the current ref.

* fix(web): Accept saves the open collab editor first and stops if that changed the text (TASK-3462 U3b, codex r4)

A synced socket proves nothing about typing the server has stored: a frame in
flight is not in the op-log, so content_pending_flush cannot see it. Accept
now runs a foreground editor flush first (new CollabFlusher.flushAndWait).
- flushed: the editor held text the preview did not show, so nothing is
  replaced and the preview is read again;
- failed or skipped: nothing is replaced;
- deduped: the update proceeds.
The review dialog is a native modal, so no typing can follow the flush. The
not-synced refusal stays, for buffered frames that would land after the
update. Both new tests fail on the previous component.

* test(server): the built-in state carries the item's current text with its seq (TASK-3462 U3b, codex r5, red)

* fix: the built-in review compares against the item text read with its seq (TASK-3462 U3b, codex r5)

GET /items/{ref}/builtin now carries current: {content, fields}, the item's
own text from the same read as seq (additive, omitempty, present when an
update is on offer). The dialog compares against it, falling back to the
pane's copy for an older server. So after a 'flushed' stop, the re-read
shows the edits that caused it, and a fresh seq is never paired with a stale
body.

* fix(web): the built-in notice renders for any item the server says is built-in; document current (TASK-3462 U3b, codex r6)

The is_system gate missed built-ins moved, or activated, into a non-system
collection of the right kind: a Library badge then led to a page with no
review. The server's not_builtin answer now decides. CLAUDE.md's endpoint
bullet documents current.

* fix(web): the Library page's title fallback works again; the permission walk keeps an unactivated playbook (TASK-3462 U3b, CI)

The page listed conventions and playbooks with ?all=true. The server has no
such parameter and reads an unknown one as a field filter, so both lists came
back empty and the title match never matched. On main every library card
offers Activate, seeded ones included. Dropped.

U3b's key match then (correctly) marks all four template-seeded playbooks
Active, which left the permission walk's Library leg with nothing to
activate. Its setup now deletes one seeded playbook. It also creates a
convention with a library title and no origin (a legacy item), which the
leg asserts reads Active for every account that can see it. That assertion
failed with ?all=true and passes without it.
2026-10-08 02:53:41 -04:00
xarmian 79922d4bab feat: conventions checked on each comment alone, with the chip on its card (TASK-3119 U2b) (#1872)
* feat: conventions checked on each comment alone, with the chip on its card (TASK-3119 U2b)

A new decision set, conventions_comments (QuestionSet.PerComment): the
conventions questions asked about each of an item's last 10 live comments,
one comment per state (BuildCommentState: the item's title and collection,
and the comment, 16k-rune cap), rows keyed conv:<ref>@<comment-id>. Gate 2
measured this subject: 7 of 7 comment-borne breaks caught, 0 of 9 false, a
credential past the old 2k trail clip caught at 0.99.

- Asked vs current (lead ruling): only the newest 10 comments are asked or
  re-asked, but an answer stays current while its comment is unchanged,
  however many newer comments arrive. Edited, deleted or moved: not current.
  A title edit re-asks the window (the title is in the state, as measured).
- Idempotent per comment: a new comment costs one call, an item write none.
- Web: DecisionChips publishes per-comment chips from its existing read into
  a small store keyed by item; TimelineCommentCard shows 'Possibly breaks
  CONVE-N' on the comment (and on a reply) at p >= 0.9, full-reload link, no
  compliance state. Cleared when the item goes.
- CLAUDE.md: the third set.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): a late decisions response cannot repopulate cleared comment chips (TASK-3119 U2b)

codex r1.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 01:43:43 -04:00
xarmian b8c9b0ea6b fix(web): the import dialog shows progress, stops on a stall, and says what was kept (BUG-3475) (#1869)
* fix(web): the import dialog shows progress, stops on a stall, and says what was kept (BUG-3475)

The dialog sat on 'Importing...' forever when the upload stalled: the upload
was a bare fetch, with no progress and no way to notice a stall.

- The upload goes over XMLHttpRequest (lib/api/importUpload.ts): bytes sent
  of total, then 'Finishing the import...'. No upload progress for 60s, or
  no response 120s after the last byte, ends it; 'Stop upload' and closing
  the dialog cancel it. Both bounds are pinned against the server's 60s
  per-Read idle window, read from the Go source.
- Each attempt carries a fresh import key (getRandomValues: randomUUID needs a
  secure context, and http on a LAN IP is not one). When no response
  arrives, the dialog asks GET /workspaces/import-status for up to 75s and
  says exactly one of: finished (Open workspace), partial workspace kept
  (Open workspace), nothing kept (Try again), or unknown (check your
  workspace list). An unknown key is never 'nothing was kept'.
- A new open of the dialog ends any earlier operation, so a late outcome
  cannot paint onto it; it is closed through the store too, which close()
  never sees.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(e2e): a failed import upload is resolved through import-status (BUG-3475)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): import dialog fences Open workspace, closing stops the outcome poll, callbacks cannot wedge the upload (BUG-3475)

codex r1 on the web half.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): Open workspace does not navigate after the dialog was closed (BUG-3475)

codex r2 on the web half.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-07 22:05:11 -04:00
xarmian 84ddb3f693 fix(web): the list cursor no longer snaps back while the pane follows j/k (BUG-3204) (#1863)
* test(e2e): a list change while the pane follows j does not snap the cursor back (BUG-3204)

* fix(web): a list change while the pane follows j/k no longer snaps the cursor back (BUG-3204)

The collection route keeps the row highlight on the open pane's item: an
effect sets the cursor to ?item='s row whenever the list changes. j/k move the
cursor at once and the pane follows 140ms later, so a list change inside that
window snapped the cursor back to the old item, and the follow snapped it
forward again: a visible flicker, and attachment-viewer-owners' owner-2 flake
(CI: alpha, alpha, BRAVO, alpha after one j).

While a follow is pending the cursor is the newer intent, so the effect
yields; the follow's own ?item= change re-runs it.

Lever (e2e/bug-3204-cursor-snapback.spec.ts holds the /items-changes fetch a
rename triggers until j is pressed): main 10/10 fail, this 20/20 pass.

* fix(web): the cursor follows a reactive pending target, and a click supersedes a pending follow (BUG-3204, codex r1)

Codex r1: the first fix gated the snap-back on the non-reactive timer handle,
so a follow that ended without navigating (target deleted, already open)
left the cursor blank with nothing to re-run the effect, and a click during
the debounce was overridden by the follow.

pendingFollow {id, fromRef} is $state. While set, the snap-back targets that
row by id. It clears when ?item= reaches the row, when the follow opens
nothing, or when ?item= moves by another door, which also cancels the follow.

The spec freezes the page clock and holds the list fetch that carries the
change, so each case is deterministic. Main: case 1 (snap-back) and case 3
(click overridden) fail 10/10, and case 2 passes. Fix: 180/180 at 8 workers.

* fix(web): pendingFollow is per-session state; the follow timer keeps its identity fence first (BUG-3204)

The collection identity gate caught both: a new $state the identity reset
did not clear, and a timer whose identityHeld check had moved into a helper.
resetPerSessionState clears pendingFollow; the timer checks identityHeld
before anything else; the gate rows are re-reviewed.

* fix(web): a follow the controller would drop, or that never lands, releases its pending target (BUG-3204, codex r2)

follow() reports false when a pane navigation is in flight, since the
controller silently drops the open then. A follow that navigates arms a
fenced settle timer that drops a target still pending after 1s, covering a
goto superseded without ?item= moving. The identity gate's timer inventory
and rows are updated. Case 2 of the spec now claims only what it exercises.

* fix(web): the settle check shares the follow's timer handle, so teardown clears it (BUG-3204, codex r3)
2026-10-07 17:42:35 -04:00
xarmian 412ea6f568 test(e2e): sidebar-footer-row waits for Settings before measuring the row (BUG-3472) (#1865)
The Settings button renders only once workspaceStore.current has loaded
({#if wsSlug && !isGuest}). The test waited for the footer row, which renders
earlier, and measured a row missing Settings when the workspace GET was slow.
Wait for the button the assertions are about.

Lever: delaying the workspace GET 2.5s failed the old wait 3/10 (Settings
absent, []) and passed the new one 10/10.
2026-10-07 16:55:14 -04:00
xarmian d6f1a06508 test(e2e): task-3413 handlers tolerate teardown closing the context mid-fetch (BUG-3471) (#1864)
* test(e2e): task-3413 route handlers tolerate teardown closing the context mid-fetch (BUG-3471)

The route handlers fetch, read JSON and fulfil. When the test ends while one is
in flight, teardown closes the context and Playwright disposes the fetched
response, so apiResponse.json() throws 'Response has been disposed' and the
test fails after its assertions passed. Swallow only that abandoned-request
family; any other error still fails the test.

Lever: closing the context mid-handler fails the old handler 3/3 with CI's
exact error and passes the guarded one 3/3.

* test(e2e): guard the third task-3413 handler; tolerate only the two teardown messages (BUG-3471)

Codex: the long-name test's handler had the same fetch/json/fulfil race, and
'Route is already handled' means two handlers raced, a real defect, not an
abandoned request. Re-measured with the narrowed pattern: context close fails
the old handler 5/5 and passes the guarded one 5/5.
2026-10-07 16:49:10 -04:00
xarmian e95135c499 fix(web): links to the /-/r/ resolver open their target (TASK-3119) (#1862)
* fix(web): links to the /-/r/ resolver open their target (TASK-3119)

/-/r/{ws}/{ref} is a server redirect route with no client route behind it.
Three doors handed it to the SvelteKit router, which rendered it as
[username]=- / [workspace]=r: 'Could not load item / Workspace not found'.

- The 'Possibly breaks CONVE-N' chip: data-sveltekit-reload.
- Cross-workspace wiki-links rendered from markdown: rel="external" (already
  on the sanitizer allowlist, so the allowlist does not widen).
- Editor link clicks: planHrefClick sends a resolver href to its full-load
  branch instead of goto. Its old test pinned the goto.

Found by clicking the live chip on :7777. The new e2e clicks the chip and a
comment's wiki-link and asserts where the browser lands; both fail with the
fixes reverted.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(web): the resolver click-through e2e cleans up its fixture items (TASK-3119)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-07 15:32:36 -04:00
xarmian 10f722e9fe feat(web): "Possibly breaks CONVE-N" chips on the item page (TASK-3119 U1b) (#1860)
* feat(web): "Possibly breaks CONVE-N" chips on items (TASK-3119 U1b)

The item page's decision chips gain the conventions set: a current
conv:<ref> answer at or above 0.9 is a chip reading "Possibly breaks
CONVE-N", linking to the convention through the ref resolver
(/-/r/{ws}/{ref}, so a renamed conventions collection still resolves).

The U0 eval measured the check as precise but shy, so a missing chip
means nothing: no "complies" state, no green, and an answer below the
threshold produces no chip at all. Same fetch, same switched-item and
identity fences as the attention chips.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(web): convention chip title makes no compliance claim (TASK-3119 U1b)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-07 13:39:38 -04:00
xarmian 6903eeeea5 feat(web): choose monthly or annual Pad Pro at checkout (TASK-3468) (#1856)
The billing page offers Monthly ($8 / month) and Annual ($80 / year),
with a local-currency note, above both Upgrade buttons, sharing one
choice. Monthly is the default. The checkout sends {interval}.

The page opens Stripe only for a session priced at the picked interval
(decideCheckout, against the sidecar's echo, pad-cloud #62). A missing
echo means a sidecar from before TASK-3367, which sells monthly: a
monthly pick still proceeds, an annual pick is refused. A differing echo
is refused. A 503 for the interval shows a plain message, disables that
option and moves the choice to the other; other failures stay toasts.

The picker shows prices, so it renders only under commerceAllowed and
joins noCommerceOutsideGate's gated list; that guard also gains a
price-copy pattern, so a price outside the gate fails it.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-07 10:21:54 -04:00
xarmian 9b7ffdf897 fix(web): long Active Plans titles stay inside their card at every width (BUG-3440) (#1855)
* test(e2e): Active Plans rows hold a long title at desktop through 375px in both themes (BUG-3440)

* fix(web): a long Active Plans title clips with an ellipsis and its full text on hover, and takes its own line on a phone (BUG-3440)
2026-10-07 09:33:37 -04:00
xarmian 13fa0c99e9 fix(web): quick-add is c, and one switch turns every single-key shortcut off (BUG-3465) (#1851)
* fix(web): quick-add is `c`, and one switch turns every single-key shortcut off (BUG-3465)

Mod+N reaches a page in no browser tab: Chromium reserves IDC_NEW_WINDOW
(IsReservedCommandOrKey), Firefox declares accel+N reserved="true", and
Safari reportedly does not dispatch it. Quick-add moves to `c` (Linear,
GitHub and Jira's create key), outside editable fields and with no
modifier held. Mod+N stays bound for installed-app windows but is no
longer advertised.

WCAG 2.1.4 needs a way to turn character-only shortcuts off, and the app
already shipped `?` and j/k/h/l without one. characterKey() is now the
single gate: `c`, `?`, j/k/h/l (mapped onto the arrow cases, so Ctrl+J
no longer moves the list or swallows the browser's Downloads key) and
the image viewer's + = - 0 read their key through it. A per-device
"Single-key shortcuts" switch sits in the shortcuts modal and on the
account settings page (the way back once `?` is off).

Shortcut labels follow the platform: ⌘ on macOS, Ctrl elsewhere, in the
modal and every tooltip.

A source census fails on any raw single-character key comparison outside
characterKey(), with a reasoned allow-list for focus-scoped input
triggers; it is red on the unconverted bindings.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(e2e): the palette's dark-theme axe scan waits for the theme's transitions (BUG-3465)

The scan ran the instant data-theme flipped, while the filter chips were
still fading their colours (`transition: all 0.15s`), so axe read
in-between values: #9b9baa on #3b3b44, 4.04:1, failing #1851's CI twice
while passing 13/13 locally. Neither colour belongs to either theme.

The scan now waits until no CSS transition is running. With every
transition stretched to 2s, the old order fails with CI's assertion and
the new one passes.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-07 06:51:38 -04:00
xarmian c17865462f feat: built-ins record their origin; an item made from one can be offered Pad's current text (TASK-3462 U1+U2) (#1852)
* feat: built-ins record their origin, and an item made from one can be offered Pad's current text (TASK-3462 U1+U2)

U1: every convention and playbook Pad ships has a stable key and a content
hash; template seeding and the new server-side POST /library/activate record
{key, seed hash, seed text} in item_builtin_origin (migration 128 / pg 102).
CLI, remote MCP and the web Library page activate through the endpoint;
seeded and activated copies of an entry store the same fields. Export/import
carry the origin; copies get none; a request body cannot claim one.

U2: GET /items/{ref}/builtin derives current / update_available / diverged /
unknown_origin / unknown_entry; POST /items/{ref}/builtin/update takes the
library text through the ordinary PATCH path (expected_seq required, status
and title untouched, a forced version row keeps a diverged item's edits);
GET /builtins lists states in one join, showing only fully visible
collections.

* fix: the origin's seed text is NUL-protected (trigger migration 129); the MCP activate test asserts the endpoint call; activation follows the declared kind server-side (TASK-3462)

* fix: the built-in seed moves in the update's own transaction; a NUL-bearing imported origin is skipped; the listing computes visibility once; numbers hash by value (TASK-3462, codex r1)

* fix: on the applier path the built-in seed is recorded only after the apply confirms; a library change that only drops a field is offered (TASK-3462, codex r2)

* fix: a field the library added that the user already set reads diverged; an update clears convention metadata the library dropped (TASK-3462, codex r3)

* test: the lifecycle test finds the imported item by slug (the copy shares its title); ScanNUL's cost figures count the two new columns (TASK-3462)

* test(e2e): Library activate posts the entry to /library/activate and the item records its built-in origin (TASK-3462)
2026-10-07 06:40:06 -04:00
xarmian 35ca07bd6e fix(web): drop the ⌘] binding that cancelled browser Forward for nothing (BUG-2666) (#1849)
⌘] toggled uiStore.detailPanelOpen, which no component has read since the
first release; the real detail pane is URL-derived (?item=). Chrome does
not reserve Back/Forward (IsReservedCommandOrKey in Chromium's
browser_command_controller.cc), so it delivered the key, and the handler's
preventDefault took Forward away in Chrome, Safari and Firefox on macOS
and Firefox on Linux while changing nothing visible.

Removes the binding, the dead state (detailPanelOpen, its toggle/open/
close, the pad-detail-panel key, the mobile-crossing force-close, the
unused --detail-panel-width var) and the e2e probes that measured it. A
layout test pins that Mod+[ / Mod+] are never prevented, with either
modifier; it is red on main.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-07 04:45:59 -04:00
xarmian a7c559e431 fix(web): the command palette is an accessible combobox, axe-clean in both themes (TASK-2234) (#1847)
* test(e2e): the command palette is a keyboard- and screen-reader-usable combobox, axe-clean with results and recents (TASK-2234); adds @axe-core/playwright

* fix(web): the command palette is an ARIA combobox (listbox options, active descendant, announced count) and the keyboard works on recent searches; status pills clear 4.5:1 (TASK-2234)

* test(e2e): reopen the palette only once the item page has settled (TASK-2234)

* test: navigation follows the grouped render order; a body-only match is announced; the e2e reads aria-controls as a list and really scans dark (TASK-2234, codex r1)

* fix(web): palette navigation walks the grouped results in rendered order; the announcement counts content matches (TASK-2234, codex r1)

* test: the selection follows its item across a grouped append; nothing is announced while the content search runs (TASK-2234, codex r2)

* fix(web): the palette selection follows its item across Load more; the live region waits for the content search (TASK-2234, codex r2)
2026-10-07 03:22:58 -04:00
xarmian f392847024 fix(web): no field suppresses its focus ring any more (TASK-3461) (#1845)
WIP: 40 component rules removed outline under :focus on a field, which
beat the global :focus-visible ring (TASK-2262). Removed from all 40; the
danger input's ring takes its red. A vitest guard
(src/lib/a11y/fieldFocusIndicator.test.ts) refuses a new one unless the
rule gives another indicator; red on the old code, green now.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-07 03:02:53 -04:00
xarmian 56f8a49fe0 fix(web): SSE bursts coalesce their reconcile and collections reload; the timeline ignores other items' comments (TASK-2224) (#1844)
* test(e2e): a tab's request budget for external updates, a 20-update burst and comments on another item (TASK-2224)

* fix(web): SSE bursts coalesce their reconcile and collections reload; the timeline ignores comments on other items (TASK-2224)

* fix(web): a cancelled coalesced run settles its callers; a solitary call is capped; the budget gate bursts concurrently and proves the last update was reconciled (TASK-2224, codex r1)

* test(web): an SSE callback whose coalesced run was cancelled by destroy stops (TASK-2224, codex r2)

* fix(web): a cancelled SSE callback stops; the budget gate scales with the burst's duration and waits out the reconcile queue (TASK-2224, codex r2)

* test(web): cancelling a coalesced run already in flight rejects its callers (TASK-2224, codex r3)

* fix(web): cancelling the coalescer abandons runs in flight too; the budget gate requires a burst short enough to discriminate (TASK-2224, codex r3)

* test(web): a synchronously throwing coalesced function rejects its callers (TASK-2224, codex r4)

* fix(web): a synchronous throw rejects the coalesced callers; the budget gate counts until the tab is quiet and skips only the budget on a box too slow to discriminate (TASK-2224, codex r4)

* test(e2e): the budget's windows come from when the page received the 20 events (TASK-2224, codex r5)

* test(e2e): the budget counts only once no API request is in flight (TASK-2224, codex r6)

* test(e2e): a budget run that never goes quiet is skipped, not counted (TASK-2224, codex r7)

* test(e2e): room for the budget's waits (TASK-2224)
2026-10-07 02:13:33 -04:00
xarmian 7222505d41 fix(web): every field shows a focus ring, and the sidebar add buttons reach 3:1 (TASK-2262) (#1843)
The 2026-07-20 audit's C9 / C10, re-measured on current main.

- --text-muted already meets AA: a runtime survey of every element rendered
  in the muted colour against its effective background, 13 pages x 2
  themes, 458 elements each, has a minimum of 5.05:1 dark and 5.09:1
  light (the audit-era #666666 measures 2.94 with the same instrument). No
  token change; the dead --status-archived token (0 uses) is removed.
- Focus (C10): app.css stripped every field's outline and gave only input
  and textarea a 1px border shift; selects had NO indicator (settings >
  Members invite selects, measured with a keyboard-focus probe). A global
  :is(input, textarea, select):focus-visible 2px ring, with the
  pseudo-class doubled so it beats components' plain outline resets; fields
  showing the ring went from 0 to 10 of 14 probed, none left without an
  indicator. The 44 per-component ':focus { outline: none }' field rules
  are their own task (lead ruling).
- The sidebar's icon-only '+' controls rendered muted at opacity 0.5:
  2.29:1 dark, 2.12:1 light. At 0.8: 3.94 / 3.76 (WCAG 1.4.11, 3:1).

e2e task-2262-focus-and-contrast: 4/4 green here, 4/4 red on main.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-07 00:19:35 -04:00
xarmian 1d022040e6 fix(web): the field editor's advanced-section sync no longer depends on the Svelte runtime to stay alive (TASK-2186) (#1841)
The 2026-07-20 audit's critical C1: typing the first letter of a new
field's label wedged both collection modals (key stuck at one letter,
add-field dead, unclosable). FieldEditor's $effect.pre read and wrote
lastSyncedKey as $state, the self-invalidating class BUG-1687 fixed in
CreateCollectionModal. Svelte 5.56 stopped the wedge (b759898d5, #1156,
bumped it from 5.55.8 incidentally); the code was unchanged and only safe
by that. lastSyncedKey is now a plain variable.

Measured with the new e2e (type a label, then key auto-sync, a second add,
Escape, no page errors):
- audit code 3725d968 + Svelte 5.55.8: RED (key stuck at 's');
- the same, Svelte raised to 5.56.8 or 5.57.1: green;
- the same, Svelte 5.55.8, with this change only: green 3/3;
- main with this change: Create and Edit modals green 3/3.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-06 23:40:35 -04:00
xarmian 4fdc6ed625 feat: in-app tutorials on Pad Cloud, links to getpad.dev/learn elsewhere (TASK-3452) (#1839)
Design and rulings on TASK-3452 (lead, day 89).

- Placement: a first-time card on /console's empty state (Start a new
  project) and in the workspace launchpad's step 2 (Set up your workspace
  with /pad onboard); each vanishes with its screen. The stable way back is
  a Tutorials entry in the user menu's Resources block, right after Docs.
- Dismissal: users.ui_dismissals, a JSON set of keys behind an allow-list
  (models.UIDismissalKeys), read-modify-written under the users row lock,
  so a dismissal holds on every device. GET/PUT /me/ui-dismissals, web only.
- Pad Cloud: the server fetches https://www.getpad.dev/learn/catalog.json
  (a constant; 1h cache, last good copy kept, 5 min back-off) and serves it
  same-origin at /tutorials, with posters proxied at /tutorials/posters/
  {slug} from www.getpad.dev only. The page CSP gains frame-src for
  youtube-nocookie, Cloud only. /console/tutorials hosts the click-to-load
  player and chapters; transcripts link to getpad.dev.
- Self-hosted: both routes 404, no outbound request, no CSP change; every
  surface links to getpad.dev/learn.

Migration 127 / pg 101, after #1837's 126 / 100 (rides rc.7). No CLI or MCP surface.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-06 22:23:29 -04:00
xarmian 1d94d038a4 fix: the first-run launchpad follows an onboarding agent live (BUG-3447) (#1837)
* test: collection create publishes an event; the launchpad and connect banner follow agent writes live (BUG-3447)

* fix(web,server): collection create publishes collection_updated; the launchpad and connect banner follow agent writes live (BUG-3447)

* test(web): a debounce armed in one workspace does not fire into the next (BUG-3447, codex r1)

* fix(web): the live reload and the banner re-check fire only for the workspace they were armed in (BUG-3447, codex r1)

* test(web): an event during the first load or check still triggers the live refresh (BUG-3447, codex r2)

* fix(web): an event before the first load or check commits still triggers the live refresh (BUG-3447, codex r2)

* test(web): a failed quiet re-check that overtook the first check leaves the banner showing (BUG-3447, codex r3)

* fix(web): a failed quiet re-check from the unknown state falls back to showing the banner (BUG-3447, codex r3)

* test: an agent's collection is agent activity (dashboard, banner, e2e incl. the Agent-connected step) (BUG-3447, lead)

* fix: a collection records its source; an agent-created collection is agent activity for the launchpad and the banner (BUG-3447, lead)

* test(e2e): the launchpad follows an onboarding agent from outside the tab, red on main for each outcome (BUG-3447, lead)

* test(store): collection provenance survives export/import; the census sees the new column (BUG-3447, codex r5)

* fix(store): collection provenance travels in the workspace bundle, validated on import; census records the column (BUG-3447, codex r5)

* test(store): an unrecognised bundle source is stored as '' (BUG-3447)
2026-10-06 18:31:29 -04:00
xarmian a91fcce7f9 fix: blank workspaces accept the library's own triggers and scopes (BUG-3446) (#1836)
* fix: a blank workspace's conventions and playbooks take the library's own triggers and scopes (BUG-3446)

The blank template seeds Conventions with trigger [always] / scope [all]
(Playbooks [manual] / [all]) and nothing widened them, so all 18 library
conventions were refused on every door: CLI and MCP library activate, the
web Activate button, and the onboard playbook's literal B3 create.

A create in a system collection declaring the convention or playbook
artifact kind now adds a missing trigger/scope value to the select's
options when the LIBRARY itself uses that word
(collections.LibraryOptionVocabulary, computed from the library). The write
goes through UpdateCollection with its concurrency token, ahead of
validation, and is reported in warnings.options_added (CLI: stderr) and in
the created activity's metadata, which the item timeline renders. Any other
value is refused as before. Editors may trigger it (lead ruling). The
onboard playbook's B3 now says how an owner adds an invented trigger.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* chore(mcp): ToolSurfaceVersion 0.67 for BUG-3446 (additive)

Lead ruling: a create in a system conventions/playbooks collection now
adds a library trigger/scope word instead of refusing it, and its result
gains an omitempty warnings.options_added. No tool name, enum or param
moved; tools/list golden bytes are unchanged, initialize moves for the
version and the instructions note.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-06 16:48:50 -04:00
xarmian f74f626fcb fix(server): register-with-code claims the invitation atomically; a gone invitation refuses and rolls the account back (BUG-3438) (#1834)
* test(server): register-with-code through an invitation gone before its claim joins nothing (BUG-3438)

* fix(server): register-with-code claims the invitation in the membership's transaction; a gone invitation refuses and rolls the account back (BUG-3438)

* test(server): a cloud signup refused for a gone invitation is rolled back; a failed claim beside a membership is not a join (BUG-3438, codex r1)

* fix(server): the signup rollback removes the account's verification tokens with it; a membership the claim did not write is not a join (BUG-3438, codex r1)

* test(server): a session minted in the window does not block the signup rollback; a refused signup keeps the emailed proof for the retry (BUG-3438, codex r2)

* fix(server): the signup rollback removes the account's sessions under its row lock and restores a spent invitation proof (BUG-3438, codex r2)

* test(server): a reset token minted in the window does not block the signup rollback (BUG-3438, codex r3)

* fix(server): the signup rollback is the full account deletion; a proof whose consume reported an error counts as possibly spent (BUG-3438, codex r3)

* fix(server): an invitation proof whose consume errored is never restored from what the caller sent (BUG-3438, codex r4)

* test(server): an invited signup reports the role the account holds (BUG-3438, codex r5)

* fix(server): an invited signup reports the role the claim kept (BUG-3438, codex r5)

* fix(server): the signup rollback kicks the account's connections, as every account deletion does (BUG-3438, codex r6)

* fix(server): the signup rollback kicks even when the deletion reports an error (BUG-3438, codex r7)

* test(server): on open registration a refused invited signup keeps the account, joins nothing and says why (BUG-3438, lead ruling)

* test(web): a registration kept without its invitation says so on the join page and lands nowhere (BUG-3438)

* fix(server,web): a refused invited signup is rolled back only when the invitation was its admission; on open registration the account is kept and told (BUG-3438, lead ruling)

* test(server): a claim refused before commit needs no reconcile read; a failed rollback is not reported as one (BUG-3438, codex r9)

* fix(server): a claim refused before commit skips the reconcile read; a failed rollback answers 500; docs name the admin case (BUG-3438, codex r9)
2026-10-06 15:48:30 -04:00
xarmian b831e9fe09 feat(web): + badge, Decline, and an accept/decline card on invitation links (BUG-2136 U2) (#1833)
* test(web): red tests for the invitation badge, Decline, and the join-page consent card (BUG-2136 U2)

* feat(web): pending-invitation badge on +, Decline in the list, and an accept/decline card on /join (BUG-2136 U2)

* test(web): a list fetch issued before a decline cannot resurface the row; accept locks Decline (BUG-2136 U2)

* fix(web): a decline orders out list fetches already in flight (BUG-2136 U2)

* test(e2e): the + badge counts and clears, and a decline from + joins nothing (BUG-2136 U2)

* docs: invitations are pending until accepted; the CLI, + menu and join link (BUG-2136)

* test(web): stale list responses across an identity change or an accept; the 2FA sign-in lands on the card (BUG-2136 U2, codex r1)

* fix(web): list responses are fenced by identity and ordered out by an accept; 2FA button says Verify; decline routes documented (BUG-2136 U2, codex r1)

* test(web): /join accept and decline refresh the + badge; the 2FA button reads exactly Verify (BUG-2136 U2, codex r2)

* fix(web): /join accept and decline force-refresh the + badge (BUG-2136 U2, codex r2)

* test(web): the TopBar mount refresh skips the badge throttle (BUG-2136 U2, codex r3)

* fix(web): the TopBar mount refresh is forced (BUG-2136 U2, codex r3)

* test(web): /join follows a code change under the mounted page, drops a late preview, and announces results (BUG-2136 U2, codex r4)

* fix(web): /join restarts its flow per code and acts on the code it shows; results are announced; docs name the CLI (BUG-2136 U2, codex r4)

* test(web): an accept, decline or sign-in in flight across a /join code change does not land on the new code's page (BUG-2136 U2, codex r5)

* fix(web): /join fences every action continuation by flow, and a late sign-in restarts the current flow (BUG-2136 U2, codex r5)

* refactor(web): /join accept keeps one fence, at the landing; a spent proof is cleared whichever page shows (BUG-2136 U2)

* test(web): a /join code change empties the form, keeps an in-flight sign-in busy, and a late signup clears its proof (BUG-2136 U2, codex r6)

* fix(web): a /join code change empties the form and keeps an in-flight sign-in busy; a late signup clears its proof; the username check is fenced (BUG-2136 U2, codex r6)

* test(web): the + list answers the badge under a token reserved before it fetched (BUG-2136 U2, codex r7)

* fix(web): the + list and the badge refresh share one order (BUG-2136 U2, codex r7)

* test(web): the + rows follow the badge store's one order (BUG-2136 U2, codex r8)

* fix(web): the + rows render from the badge store, so one order decides both (BUG-2136 U2, codex r8)

* test(web): the TopBar store double carries the list the + surface renders (BUG-2136 U2)
2026-10-06 13:46:05 -04:00
xarmian 2f3cf402b1 fix(timeline): 'commented on update' only for a comment an update carried (BUG-3437) (#1832)
* fix(timeline): 'commented on update' only for a comment an update carried (BUG-3437)

TimelineCommentCard showed 'commented on update' whenever the comment had
an activity_id, and every comment has one: a comment or reply written on
its own links to its own 'commented' activity, so a plain
'pad item comment' read as if it were attached to a change.

The store's comment list join (the one that already reads the agent name
off the linked activity) now also reads its action into
Comment.LinkedActivityAction (json:"-"), and the timeline sets an
additive, omitempty TimelineEntry.comment_on_update when that action is
anything but 'commented'. The card keys the label on it. The comments
API, the CLI and MCP keep their shape (the new field is never serialized
outside the timeline, which MCP does not expose).

Tests: the server test writes a standalone comment, a reply and an update
carrying a comment through the real routes (web, CLI and MCP share the
comments route) and checks the flag, with the old rule and an always-false
mutant each red; the web test pins the card and the entry list's
pass-through, each red with its half reverted.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(timeline): a comment that arrives live keeps 'commented on update' (BUG-3437)

The label comes from the server-set comment_on_update. Comments that arrive
while the timeline is open come through the SSE refresh, which re-reads the
timeline endpoint (nothing builds a comment entry client-side), so they carry
the flag. This pins it: red if the entry list stops passing the flag.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-06 12:57:14 -04:00
xarmian 8c8deeeb89 fix: an invite asks an existing user instead of adding them; decline; CLI invitations (BUG-2136 U1) (#1831)
* test(server): an invite asks an existing user; re-invite replaces; decline by id and by code (BUG-2136, red)

* fix(server): an invite asks an existing user instead of adding them; a re-invite replaces the pending invitation; decline by id and by code (BUG-2136)

* feat(cli,mcp): pad workspace invitations/accept/decline; invite output says pending; ToolSurfaceVersion 0.66 (BUG-2136)

* test(e2e): invitees accept their invitation before the specs use the membership (BUG-2136)

* test(store): concurrent re-invites of one address leave one pending invitation (BUG-2136)

* test(store): an accept that loses to a decline adds no membership (BUG-2136, codex r1, red)

* test(server): an unverified invitee can decline by code, as they can accept (BUG-2136, codex r1, red)

* fix: an accept that loses to a decline or replace adds no membership; decline by code passes the verified-email gate; honest oracle comment (BUG-2136, codex r1)

* fix(server): the verified-email exemption matches exactly /invitations/{code}/accept|decline (BUG-2136, codex r2)

* test(server): drop the plan-limit harness's now-unused newUser (BUG-2136)
2026-10-06 12:16:32 -04:00
xarmian 7aa5fa67f7 fix(web): an open mobile sheet stops page pull-to-refresh, and a pull from its list top closes it (BUG-3386) (#1830)
With the mobile Workspace sheet open, a downward swipe in the collection
list did not close it, and at the list's top it pull-to-refreshed the page.
DockedSheet put overscroll-behavior: contain on .ds-panel, which never
scrolls, so it did nothing: the list (.ds-content) is the scroller, and its
overscroll chained to the page.

- .ds-content gets overscroll-behavior: contain;
- while any DockedSheet is open the root is overscroll-behavior-y: none
  (html/body :has(.ds-panel)), so a drag on the backdrop, the grip, or a
  list too short to scroll cannot refresh the page either;
- a downward drag that starts in the list with the list at its top closes
  the sheet, the grip's existing close gesture continued into the content.
  A drag on a scrolled list, or one that moves up first, stays a scroll.

Applies to all three DockedSheets (Workspace, You, Quick capture).

Unit tests cover the content pull (close, snap back, scrolled list,
up-first, viewer in front); mutants removing the handler, the scrollTop
check and the up-first check each turn one red. The mobile-chromium e2e
drives a real CDP touch drag: red on main (root overscroll still auto
under the open sheet) and red on a mutant without the content handler
(the sheet stays open).

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-06 10:59:53 -04:00
xarmian d39cb9f00a fix(web): the selected tab is marked by an accent, not a background step (BUG-3420) (#1829)
In dark mode the active workspace tab was hard to tell from the others:
its background is the page colour (the tab flows into the page), which
measured 1.07:1 against the strip and its neighbours (1.09:1 in light),
with the same text colour and weight. It now carries a 2px accent line
along its top edge (inset box-shadow): 4.85:1 / 5.20:1 in dark, 5.70:1 /
5.24:1 in light, against the strip and the tab.

A survey of every selected-state control with siblings, across 12 pages
in both themes, found three tab-like segmented toggles failing the same
way (background steps of 1.05-1.16:1): the item pane's Rich / Markdown
toggle, the tag page's view toggle and the Insights window selector. Each
selected segment gets a 1px inset accent ring (>= 4.47:1). Underline tab
bars (item pane, settings, library, admin) already met 3:1 and are
unchanged.

The e2e pins the requirement for the tab bar in both colour schemes: the
active tab's indicator reaches 3:1 against an inactive tab and the strip.
Red on the old build (no indicator) and on a mutant drawing the line in
the border grey.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-06 10:49:45 -04:00
dependabot[bot] 28cc004686 chore(deps)(deps): bump source-map-js from 1.2.1 to 1.2.2 in /web (#1819)
Bumps [source-map-js](https://github.com/7rulnik/source-map-js) from 1.2.1 to 1.2.2.
- [Release notes](https://github.com/7rulnik/source-map-js/releases)
- [Changelog](https://github.com/7rulnik/source-map-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/7rulnik/source-map-js/compare/v1.2.1...v1.2.2)

---
updated-dependencies:
- dependency-name: source-map-js
  dependency-version: 1.2.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 00:40:14 -04:00
xarmian b454f17928 build(web): move the SvelteKit and Svelte config into vite.config.ts (TASK-3423, PR A) (#1823)
SvelteKit 3 refuses svelte.config.js, and kit 2.62+ already takes its config
through sveltekit({...}), so the move lands on kit 2.70.3 first. Kit's own
keys (adapter) stay with kit; dynamicCompileOptions is forwarded to
vite-plugin-svelte.

The vitest jsdom project used a bare svelte(), which got runes mode only by
loading svelte.config.js itself. Both configs now import dynamicCompileOptions
from one module, svelteCompileOptions.ts, and the jsdom plugin passes
configFile: false. Deleting the file without that would have dropped runes
mode from component tests silently.

Proofs:
- runesMode.svelte.test.ts: importing a component with a legacy $: statement
  is refused in the jsdom project; red without the vitest change.
- svelteCompileOptions.test.ts: resolving the real vite.config.ts yields
  vite-plugin-svelte options carrying the forcing ({runes: true} for ours,
  undefined for node_modules) and configFile false; red without the forward.

The unused @sveltejs/adapter-auto is removed.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 23:38:37 -04:00
xarmian e62188ed15 docs: fresh 2x product screenshots (dark and light) and a tightened README (TASK-3426) (#1816) 2026-10-05 19:03:24 -04:00
xarmian 73fd8d5111 test(e2e): assert each list-navigation step on its own (BUG-3204) (#1815)
* test(e2e): assert each list-navigation step on its own, with a named expected card (BUG-3204)

* test(e2e): key cursor steps by slug and accept a row that arrives mid-step (BUG-3204, codex r1)

* test(e2e): the return step compares against the card read just before its press (BUG-3204, codex r2)
2026-10-05 18:44:09 -04:00
xarmian f0c65fe08c chore(web): adopt Tiptap 3.31.4 and regenerate the materializer corpus (TASK-3421) (#1811)
* chore(web): adopt Tiptap 3.31.4 and regenerate the materializer corpus (TASK-3421)

3.31.4's TableCell/TableHeader renderHTML stop rendering a default
colspan="1" / rowspan="1". parseHTML is unchanged, so a stored body
carrying them parses to the same document. Replaying all 50 old corpus
op-logs through the 3.31.4 bundle reproduces the old output exactly once
those attributes are removed (and fails on 28 cases without removing them).

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(web): pin that a pre-3.31.4 table body still dedupes on open (TASK-3421)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 15:52:54 -04:00
xarmian 78f3c08240 test(e2e): a real installed app, end to end over https (TASK-3415) (#1804)
* feat(e2e): a test-build-only door for the app origin's CA (TASK-3415)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(e2e): a real install end to end over an https front and app origin (TASK-3415)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(e2e): the install review waits for a real manifest fetch (TASK-3415)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* chore: ignore the e2etest pad binary (TASK-3415)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(e2e): discovered https endpoints, awaited teardown, OS-assigned ports, an explicit timeout (codex r1 on TASK-3415)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* test(e2e): teardown survives a spawn failure, pad's port retries, the front drops hop-by-hop headers, cert setup cleans up (codex r2 on TASK-3415)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 10:21:28 -04:00
xarmian 8b8124ab5c feat(apps): item-pane link-out for installed apps' item actions (TASK-3413, SPEC-6 U9d) (#1794)
* feat(apps): the item pane's app actions: one link-out button per action, the tab opened inside the click with no opener, only web URLs followed, refusals and sign-outs close it (TASK-3413, SPEC-6 U9d)

* test(apps): e2e for the item action: new tab with no opener at the minted URL, a refused mint closes it, no button without actions (real server) (TASK-3413, U9d)

* fix(apps): an item action whose pane moved on before the mint answered closes its tab; the referrer the app sees is documented (TASK-3413, U9d codex r1)

* fix(apps): the item action fences with authStore.identityFence() and joins ItemDetail's reviewed child population; its tab cleanup is classified (BUG-3095 guard; TASK-3413, U9d)
2026-10-05 10:15:06 -04:00
xarmian 83ca50f861 fix(apps): the apps UI tolerates null lists from the server (BUG-3417, client half) (#1795)
* fix(apps): every list the apps UI reads is made an array where its response lands, so a null list from the server no longer crashes the install or upgrade review (BUG-3417)

* test(apps): a changed artifact with null lists in the upgrade review (BUG-3417)
2026-10-05 07:50:11 -04:00
xarmian d5c2ecfe08 feat(apps): 'via <App>' attribution on items, comments and history, and the members Apps section (TASK-3413, SPEC-6 U9c UI) (#1793)
* feat(apps): the 'via <App>' label on items, comments, replies and history, history keeping a person's own edits apart from their edits through an app, and the members list's separate Apps section (TASK-3413, SPEC-6 U9c)

* test(apps): the via <App> label on comments and replies, and history's writer rules with an app (TASK-3413, U9c)

* test(apps): history labels writes through an app and keeps them apart from direct edits (TASK-3413, U9c mutant V5)

* test(apps): e2e for the via <App> label on an item and its comment, and the members Apps section, real rows with the app fields added through route.fetch (TASK-3413, U9c)

* fix(apps): app names are bidi-isolated in the Members section and wrap in every via label (TASK-3413, U9c codex r1)

* test(apps): the long-name leg checks each via label ends inside the viewport; the page-width check could not fail, an ancestor clips (TASK-3413, U9c)

* fix(apps): WorkspaceApp gets its own import line, so the identity gate re-reviews only load() and handleInvite(), whose new apps writes sit beside members behind the same checks (TASK-3413, U9c)
2026-10-05 07:28:37 -04:00
xarmian 9ae4a0be45 feat(apps): upgrade review and draft activation in Settings → Apps (TASK-3413, SPEC-6 U9b) (#1789)
* feat(apps): the install view lists the items its pack provisioned (source_pack = origin@version), for draft activation (TASK-3413, SPEC-6 U9b)

* refactor(apps): the per-artifact review is its own component, shared by install and upgrade (TASK-3413, U9b)

* feat(apps): the upgrade review (diff by review/auto, released companions, new drafts, the reviewed hash) and the drafts panel with per-item activation (TASK-3413, SPEC-6 U9b)

* test(apps): each upgrade diff line sits in exactly one group (TASK-3413, U9b mutant U2)

* test(apps): e2e for the upgrade review and draft activation through page.route (TASK-3413, U9b)

* fix(apps): Activate addresses the item by id, not slug; the origin prefix is compared in characters; the one-live-install-per-origin rule is documented (TASK-3413, U9b codex r1)

* fix(apps): Activate moves focus to its row and announces it; the draft list retries after a failure; the upgrade's loading line is a status (TASK-3413, U9b codex r2)

* fix(apps): activation moves focus only when it was lost, and to the panel heading when the row is gone (TASK-3413, U9b codex r3)

* test(apps): an app token, service or a delegated owner's, cannot activate its own draft artifact (§2 step 7; TASK-3413, U9b lead ruling)
2026-10-05 06:09:12 -04:00
xarmian 48c3ca066c fix(test): test temp dirs stop leaking into /tmp (BUG-3412) (#1786)
* fix(test): template and fixture temp dirs no longer leak into /tmp (BUG-3412)

The store test templates are named for the owning pid, and each build
first removes the template dirs of processes that are gone (and pid-less
legacy dirs over a day old), so a test binary killed before TestMain's
Cleanup no longer leaves its template on the tmpfs for good. The fence
source tests remove their fixture dirs when the file finishes.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(test): never sweep pid-less template dirs; age proves no owner gone (codex r1 on BUG-3412)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 04:50:02 -04:00