* test(server): a revalidation tick that errors leaves the collab conn writable (BUG-3474, red)
A real fault: workspace_members is renamed, so the tick's membership lookup
fails with a plain error. After the first failing tick, a frame still
persists. Red 3/3 on SQLite.
* fix(server): a collab revalidation tick that errors makes the conn read-only, not closed (BUG-3474)
Lead ruling: fail closed on WRITE, keep the connection. Both error branches
of the tick (GetItem, and a plain error from authorizeCollabAccess) now call
SetConnWritable(false) before logging. The next clean tick restores write if
the role still allows it. The test faults each branch for real (renamed
workspace_members, renamed items).
* test(server): a credential re-check store error keeps the collab conn writable; recovery is checked by content (BUG-3474, codex r1, red)
* fix(server): an unknown credential re-check makes the collab conn read-only and skips the tick (BUG-3474, codex r1)
streamCredentialStillValid reads a store error as valid (right for SSE,
which has no write), so the collab tick went on to set write from the role.
The collab loop now switches on credentialLiveness: invalid closes as
before; unknown sets read-only, logs at warn and re-arms.
* 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)
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.
* 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.
* 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
* feat(decision): log each evaluation's spend (TASK-3119)
One info line per evaluation that reached the provider: set, workspace,
item ref and id, questions, calls, input and output tokens, truncated,
outcome (stored|failed). Counts and identifiers only: no question, answer,
item text or error message. A failed call's tokens are counted too.
The dogfood box's /metrics is loopback-only and :7777 binds the LAN IP, so
the server log is where its spend is readable.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* test(decision): a liveness stop between split calls still logs the call made (TASK-3119)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* 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
* wip(decision): conventions set (TASK-3119 U1a)
* chore(eval): executable-conventions eval through the production question path (TASK-3119 U0)
Measures, before any chip exists, how often the decision provider says a
hand-labelled item breaks a convention at the 0.9 threshold: per
convention precision and recall (all cases and real-only), the false
'Breaks' rate of a behaviour convention, and the calls and tokens spent.
State is decision.BuildItemState, the question a decision.Noul worded as
the U1 contract says, the model decision.DefaultModel.
The hand labels were posted to TASK-3119 before any score existed; the
trail comment carrying them is dropped from every state, and the dry run
refuses a state that still carries them. Reads TYPESAFE_API_KEY only.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* chore(eval): -links and -only for the conventions eval (TASK-3119 U1a)
-links builds each state as the conventions set does, with the item's
links (from the dump: the parent link only; blocking links are not in
pad item show's JSON). -only runs one convention. Used for the lead's
U1a gate: CONVE-1286 rerun with links.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(decision): liveness before EACH split call (TASK-3119 U1a, codex r1)
A split set makes several provider calls; the liveness check ran once
before them, so an item or workspace deleted between calls was still
sent in the later ones. It now runs immediately before every call. A
test deletes the item from inside the first call and the second is not
sent; checking only once fails it.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(decision): decision_check has no default; allow-list the eval's decode (TASK-3119 U1a)
A default "on" was written into every new convention: imports gained a
decision_check field (artifact import parity) and an activated built-in
no longer matched its seed (TASK-3462 would read every one as diverged).
Absent already reads as on, so the field carries no default, in the
template and both migrations.
The conventions eval's readJSON is a raw decode the reserved-field guard
counts; allow-listed with its reason, like the attention harness.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
The copy preflight decoded carried field values with plain json.Unmarshal
into `any`, so a number above 2^53 became a float64 and the dry run
previewed 9007199254740992 for a stored 9007199254740993. The copy itself
keeps the stored digits (server-side), so the preview misled exactly
where a user would check. Decode with models.DecodeJSONKeepingNumbers,
as BUG-3448 did for item show and the MCP resource.
The mutating copy's response is left on json.Unmarshal: it prints no
carried values (the item's fields are a string), and a decode error
there would misreport a committed copy.
Tests: the client keeps the digits, and the rendered dry run through the
real client shows them; on the old decode it renders
"= 9007199254740992".
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* test: pad init keeps an edited skill and does not downgrade a newer one (BUG-3466)
* fix: pad keeps an edited /pad skill and never downgrades a newer one; skill files carry a stamp, and --force replaces (BUG-3466)
* test: workspace init updates an older unedited skill, agent update reports a keep, --list names --force for a newer skill, and semver numbers have no size limit (BUG-3466, codex r1)
* fix: workspace init routes installed skills through the decision, agent update and --list report kept files with --force, and semver numbers compare at any size (BUG-3466, codex r1)
* test: --list flags an untracked edited skill, and agent update counts an untracked current one (BUG-3466, codex r2)
* fix: --list asks the decision for local skills, and every skill present after a write is recorded and counted (BUG-3466, codex r2)
* test: invalid SemVer never refuses as a downgrade; the frozen list pins a released text (BUG-3466, codex r3)
* fix: the frozen legacy list holds released output only, and SemVer is validated strictly before comparing (BUG-3466, codex r3)
* test: every unstamped release's skill output must be in the frozen list; the release workflow (which has the tags) fails loudly otherwise (BUG-3466, lead)
* test: whitespace after the stamp does not hide it; pinned real copies and per-tag formatter checks guard the frozen list independently (BUG-3466, codex r5)
* fix: whitespace after the stamp no longer hides it (BUG-3466, codex r5)
* test: a newer pad's identical text reads as current, not as a downgrade (BUG-3466, codex r6 declined with reason)
* chore: regenerate the legacy skill hashes with v0.18.0-rc.8 (BUG-3466)
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
* 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)
A moved-in backlog of open tasks and bugs with no priority set, bugs in
their initial status `new`, answered "No ready items found": only the
literal status `open` counted, and an open item outside an active plan
needed a high/critical priority (BUG-1082's "without flooding", though the
list is capped at three).
Such items now fill the slots the existing tiers leave empty, from a
fallback tier below every item admitted before, so a workspace that
filled three slots gets the same list. The tier admits items whose
collection is not a system collection and declares `priority` or
`severity` (lead-ruled; across every built-in template that is Tasks,
Features, Backlog and Bugs), whose done field is not terminal, that are
not in progress and not blocked. It ranks by priority, else severity,
then age.
`ready`'s help and the MCP `ready` description said it returns the full
actionable backlog; it returns the same capped top three as `next`, and
now says so. The empty state says why and points at `pad project stale`.
ListCollectionsMinimal also returns is_system.
ToolSurfaceVersion 0.70 (behavior bump on the v0.49 grounds).
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* test: onboarding an existing project asks where its open work lives and brings it in after saying there is no importer and confirming (TASK-3454)
* feat: the onboard playbook asks where a project's open work lives and offers to bring it in, saying there is no importer and confirming first (TASK-3454)
* test: open work is untrusted data, its destination is confirmed when B2 did not run, and the paste fallback does not assume gh (TASK-3454, codex r1)
* fix: open-work text is data not instructions; the destination is confirmed when B2 did not run; the paste fallback is keyed on being able to list issues (TASK-3454, codex r1)
* test: a full page of issues is reported as possibly truncated (TASK-3454, codex r2)
* fix: a full page of issues is reported as possibly truncated rather than silently cut (TASK-3454, codex r2)
* 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
* 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)
⌘] 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
A version in plugin.json pins every installed copy until the string
changes (code.claude.com/docs/en/plugins/loading, "Versions and
updates"). 0.3.3 stood from 2026-08-27 while 8 commits changed plugin/,
so marketplace installs kept the August skills. With no version, a
relative-path plugin in a git-hosted marketplace is versioned by its
commit, and `claude plugin update` picks up every change.
Measured in a scratch CLAUDE_CONFIG_DIR against a git marketplace with a
commit to plugin/ after install: pinned 0.3.3 answered "already at the
latest version (0.3.3)" and kept the old copy; unpinned updated
be0122112e5a -> d6312f1d8c6d with the change in the cache.
A guard test refuses a version in plugin.json or the marketplace entry.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* 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)
Every remote route that needs a workspace and lacks one (routeTable
mappers, special routes, link routes, the assign/role resolvers) now
answers the structured no_workspace envelope with available_workspaces
and a hint, the one stdio has always given, instead of validation_failed
"workspace is required". Mappers return one sentinel the dispatcher
converts centrally; special routes call noWorkspaceResult directly.
The single-workspace default now runs before the assign/role resolvers,
so a one-workspace caller who omits workspace is not refused. The
no_workspace hint is never empty.
ToolSurfaceVersion 0.69 (behavior bump on the v0.42/v0.45 grounds).
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
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
* 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)
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
* test: markdown stays body-verbatim and names an empty body; no agent-facing text loads an item with it (TASK-3451)
* fix: skills and playbooks load items with --agent; --format markdown stays body-verbatim and names an empty body on stderr (TASK-3451)
* test: the guard covers every flag spelling and agent-read doc; a stale empty body is not claimed empty (TASK-3451, codex r1)
* fix(cli): the empty-body hint is not printed over a stale body (TASK-3451, codex r1)
* fix: remaining skill load lines use --agent; the guard exempts only round-trip descriptions and covers quoted refs and the read alias (TASK-3451, codex r2)
* fix(skill): the convention-body fallback loads with --agent (TASK-3451, codex r3)
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
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
* test(store): an invite replaced right after it committed still returns its own invitation (BUG-3460)
* fix(store): CreateInvitation reads its row back inside its transaction, so a concurrent re-invite cannot make a successful replace report an error (BUG-3460)
* test(cli): json fields print as JSON, the metadata block is labelled for the item's kind, the update summary sizes structured values (BUG-3448)
* fix(cli): json fields print as JSON, the metadata block is named for the item's kind, the update summary sizes structured values (BUG-3448)
* test: large numbers, null, the MCP resource and unguessed labels (BUG-3448, codex r1)
* fix: numbers keep their digits, null prints as null, the MCP item resource prints json fields as JSON, metadata labels are not guessed (BUG-3448, codex r1)
* test(cli): the agent projection keeps large numbers (BUG-3448, codex r2)
* fix(cli): the agent projection keeps large numbers (BUG-3448, codex r2)
* chore(mcp): ToolSurfaceVersion 0.68, a BEHAVIOR bump: the item resource renders json fields as JSON (BUG-3448, lead ruling)
* 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)
* 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
* 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)
* 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)
* 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
* 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)
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
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
* test(store): relationship reads exclude soft-deleted collections; the unparented rule stays structural (BUG-3434, red)
* fix(store): relationship reads (links, parent, parent map, lineage, blocks edges) exclude soft-deleted collections; getItemLinkQ, the write paths' read-back, stays inclusive (BUG-3434)
* test(store): exercise both ends of parent and blocks links (BUG-3434)
* test(store): the parent filter ignores a parent in a soft-deleted collection (BUG-3434, codex r1, red)
* fix(store): the parent filter treats a parent in a soft-deleted collection as no parent, on both list paths (BUG-3434, codex r1)
TestSupervisorIdleTimerDuringJob staged 'the idle timer fires while a job
runs' with a 300ms timer, a 200ms pause and a 400ms job. On a loaded macOS
runner the pause stretched past the timer, the worker went idle BETWEEN the
jobs, and the test failed with '2 spawns'. Widening the pause to 320ms
reproduces that exact failure 3/3.
Now nothing fires on its own (a one-hour idle). A new script command,
await:<path>, holds the job inside the worker until the test releases it;
the test fires the timer itself (back-dating lastJob, Reset(0)) once the
job has provably started, and waits on a new test hook, onIdleBusy, which
idleStop calls when it finds the slot taken. Then it releases the job,
checks one spawn and that the job's end re-armed the timer on the same
worker, and fires once more to see the worker stop.
Mutants: idleStop proceeding without the slot check (red 10/10), and
armIdle not recording the job's end (red). 30/30 green with every CPU
saturated, and under -race.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* test(server): the epoch tracks the live collection set; the guard ignores deleted collections' children (BUG-3428 phase 2, red)
* fix: the access epoch fingerprints the live collection set; the index and delta drop deleted collections' rows; the open-children guard ignores them (BUG-3428 phase 2)
* test(server): an item door's epoch is never newer than its rows (BUG-3428 phase 2, codex r1, red)
* fix(server): item doors read the live collection set before their rows, so a response's epoch is never newer than its rows (BUG-3428 phase 2, codex r1)
* test(server): a grant-filtered delta's epoch is never newer than its rows (BUG-3428 phase 2, codex r2, red)
* fix(server): the delta door resolves its live grant set before its rows too (BUG-3428 phase 2, codex r2)
A caller holding item grants is filtered by guestResourceFilter's fullCollIDs
and grantedItemIDs instead of visibleCollectionIDs, and only
visibleCollectionIDs applied an app request's read ceiling (SPEC-6 U6a). So an
app principal with item grants would have been served rows outside its
ceiling on /items-index, /items-changes, the SSE snapshot and every other
grant-path door. Unreachable today (app tokens are refused on /api/v1 and the
app router exposes none of those doors), so this is hardening.
guestResourceFilterCoreQ now narrows both lists for an app request: full
collections to the ceiling, granted items to those whose collection is in it
(store.ItemIDsInCollectionsQ). nil/nil stays nil/nil, and a nil fullCollIDs
beside grants is not widened to the ceiling.
The test drives the shared filter with an app context: control (no app
context) shows the fixture's out-of-ceiling rows; with a ceiling they are
gone and the in-ceiling grant stays. Mutants: dropping the ceiling, and
skipping only the item filter, each go red.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* test(store): search, tags, children and progress must exclude soft-deleted collections (BUG-3428, red)
* fix(store): global search, tags, children and progress exclude soft-deleted collections; the open-children guard keeps them (BUG-3428)
* test(store): bare-number search excludes soft-deleted collections (BUG-3428)
* test(store): has_children agrees with the children list; tighter progress and facet assertions (BUG-3428, codex r1, red)
* fix(store): has_children leaves out children in soft-deleted collections (BUG-3428, codex r1)
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
* test(store): rebindQuery must not bind inside SQL comments or quoted identifiers (BUG-3430, red)
* fix(store): rebindQuery skips SQL comments and quoted identifiers (BUG-3430)
* fix(store): rebindQuery counts nested block comments, as Postgres does; edge-case tests (BUG-3430, codex r1)
* docs(store): rebindQuery names the Postgres string forms it does not lex (BUG-3430, codex r2)
* test(store): fail loudly when store SQL adds a $$ or E'' string, which rebindQuery does not lex (BUG-3430)
Dave's ruling (2026-10-06). A symlinked node_modules tests a worktree
against whatever the main checkout last installed. The lock file changes
every few days, and day 86 hit it twice: stale Tiptap, and a materializer
bundle built from it. SvelteKit 3 also writes its generated tsconfig
inside node_modules with relative paths (TASK-3423), so a symlinked
worktree would type-check the main checkout's tree. npm ci from the warm
cache takes about 5s. The rule for legacy symlinked worktrees stays:
never npm ci through the link; remove it first.
Claude-Session: https://claude.ai/code/session_01J9famMX8HvArgW2uo8mPRT
* test(store): ListItems, its search and the graph edges must exclude soft-deleted collections (BUG-3425, red)
* test(server): graph nodes exclude soft-deleted collections; the account export keeps them (BUG-3425)
* fix(store): ListItems, its search and the graph exclude soft-deleted collections; the account export opts back in (BUG-3425)
* test(store): role counts exclude soft-deleted collections; search honours the opt-out (BUG-3425, codex r1, red)
* fix(store): role counts exclude soft-deleted collections, as the role lanes do (BUG-3425, codex r1)
* docs(store): doneFiltersForWorkspace's comment no longer names GetRoleBreakdown as counting deleted collections (BUG-3425, codex r2)
* fix(store): keep SQL comments out of the role-count query (an apostrophe broke PG rebind); BUG-3333 expects a deleted collection's doc unlisted (BUG-3425)
libopenapi 0.41 moved its Node type to github.com/pb33f/go-yaml. Marshalling
a MediaType example with go.yaml.in/yaml/v4 saw a plain struct, and the
example block no longer rendered the author's YAML (TestConvertOpenAPI_Petstore:
markdown missing "name: Rex"). The petstore markdown is byte-identical to
libopenapi 0.40's output with this change. The test now pins the whole
example block.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
A widened delegated-access row told the owner the change was recorded and
granted nothing until delegated access is enabled (TASK-3399), but U5b-2
opened delegated sign-in. appAdmitDelegated grants the lesser of what the
person consented to and what the manifest offers now, and a consent is
capped at the level offered when it was given, so the row now says that
existing sign-ins keep their consented access and people who sign in after
approval can consent to the new level. Stale 'refused until TASK-3399'
comments in app_api.go and install_clients.go are corrected.
Tests pin the detail string and the behaviour behind it (a read consent
stays read after the widening; a write consent given after it writes).
Mutants: the old string, and admission granting the manifest's level, each
go red.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS