Commit Graph

2366 Commits

Author SHA1 Message Date
xarmian 4532eecffd feat(decision): conventions ask about the item without its comment trail, 12 questions a call (TASK-3119 U2a) (#1871)
* chore(eval): TASK-3119 U2 gates in one command; decision.BuildCommentState

-gates runs three passes through the production question path:
A  U0's population on the U2a item state (links in, trail out), gated on
   precision not dropping below U0's on any convention that fired;
B  comment-borne breaks, each comment asked alone with its item's title
   and collection, gated on precision >= 0.9;
C  a measurement of 12+ questions in one call (MaxPerCall 12).

BuildCommentState is the comment subject's one serialisation, shared by
the harness now and the runner in U2b. Comments are clipped at the body
bound (16k runes), not the trail's 2k.

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

* feat(decision): the conventions set asks about the item without its comment trail, 12 questions a call (TASK-3119 U2a)

The conventions state carried the last 10 comments, clipped at 2,000 runes
each: on docapp that was most of every call's input (15.7k tokens for one
write across 3 calls), 720 items had a clipped comment, and a break past the
clip was invisible anyway. Comments get their own subject in U2b.

- QuestionSet.NoTrail leaves the trail out; the conventions set sets it. The
  state is exactly what the U2 gates measured (recent_trail [], links in):
  precision held at 100% on every convention that fires, recall rose (1286
  40->80%, 2693 70->90%), 0 truncated states, ~954 input tokens per call.
- MaxPerCall 5 -> 12, from measure C: up to 13 questions in one call gave
  identical precision and recall, 0 of 116 answers moved by more than 0.2.
- A comment write no longer changes this set's state, so it costs no call.
- Every conventions answer computed before this reads not-current until the
  item's next write (the state bytes changed): a fleet-wide re-ask, by design.
- CLAUDE.md: the registered sets (it still said attention was the only one,
  since U1a) and the per-set state.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-08 00:01:26 -04:00
xarmian daed6bb441 fix(server): removeUnusableWorkspace reports a failed delete as kept (BUG-3479) (#1870)
Its contract is nil when the workspace is gone, an error when it was kept,
but the branch where its own soft-delete FAILED returned nil. Every caller
then reported a removal that had not happened while a live, ownerless
workspace held its slug.

- That branch now returns an error naming the failed removal.
- Cloud auto-create (both arms) logs that the workspace could NOT be removed
  instead of leaving 'is being removed' as the last word.
- Create workspace's seed failure says in its 500 that the half-made
  workspace survived and holds its name.
- addOwnerOrCompensate's ABSENT arm now returns the KEPT error, which its
  callers already treat as a failure; the bundle keep path reads the row back
  (BUG-2709) and the interrupted-import rollback does too (BUG-3475).

A test per caller with a SQLite trigger refusing the soft-delete; each fails
on the old nil.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-07 23:39:39 -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 a23edba856 feat: built-in update count in the bootstrap, and accepted updates record replaced fields (TASK-3462 U3a) (#1868)
* test(server): the bootstrap counts built-ins with an update on offer (TASK-3462 U3a, red)

* feat(server): bootstrap carries builtin_updates, a count of built-ins with an update on offer (TASK-3462 U3a)

update_available plus diverged, among the items the caller sees in full:
the library listing's own visibility, through one shared helper. omitempty,
so a workspace with nothing on offer is byte-identical to before.

* feat(mcp): agents mention built-in updates once and never apply them; tool surface 0.71 (TASK-3462 U3a)

The skill (both copies) and the MCP server instructions describe the new
builtin_updates bootstrap count: mention it once per session, and leave each
update for a person to accept. ADDITIVE bump to 0.71, since the bootstrap
every door returns gained a member and the instructions changed. Golden wire
bytes regenerated: only the version and the instructions text moved.

* test(server): an accepted built-in update records the field values it replaced (TASK-3462 U3a, red)

* feat(server): an accepted built-in update names the field values it replaced (TASK-3462 U3a)

Version history keeps bodies only, so the summary of the update now lists
each non-body field whose value it changes, as key: old → new (JSON,
bounded), including typed convention metadata. Fields the update leaves
unchanged are not listed.

* test(server): every template seed and every library activation reads current at once (TASK-3462 U3a)

The guard the night-43 review ran by hand: the registry's hash must equal
what each seeding door stores. A seed that stores drifted text turns it red
(update_available straight after seeding).

* test(server): replaced-fields summary ignores numerically equal values and truncates on a rune boundary (TASK-3462 U3a, codex r1, red)

* fix(server): replaced-fields summary compares canonical numbers and cuts on a rune boundary (TASK-3462 U3a, codex r1)
2026-10-07 21:29:37 -04:00
xarmian 250b5c2c03 fix(server): an interrupted import rolls back, and its outcome can be asked for (BUG-3475) (#1867)
* fix(server): an interrupted import rolls back, and its outcome can be asked for (BUG-3475)

The web import dialog hung on 'Importing...' when the upload stalled. The
server already bounds a stalled body (60s per-Read deadline, BUG-3184), but
it then KEPT the partial workspace and said so in a 400 that cannot reach a
client whose connection died; a lost 201 read as a failure whose retry made
a duplicate.

- A transport failure reading the body (deadline, reset, client gone) rolls
  the partial workspace back and answers 400 import_interrupted with the
  cause. A mid-stream DATA error still keeps it (TASK-896): its 400 reaches
  a live client. Told apart by a tracker on the raw body, beneath the size
  cap: a truncated file that arrives in full reads to a clean EOF.
- ?import_key= names an attempt; GET /workspaces/import-status?key= answers
  running/complete/kept/removed/not_created/unknown for the caller's own key.
  In memory, 1h TTL, bounded; an unknown key is UNKNOWN, never 'nothing was
  created'. A running key cannot be reused (409).
- The validation-reject rollback and the new one share rollBackPartialImport.

The web dialog's progress, stall abort and outcome display follow in PR B.

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

* test(server): body-reader census counts the import's transport tracker (BUG-3475)

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

* test(mcp): import-status is a known static sibling of /{slug} (BUG-3475)

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

* fix(server): a running import key is never evicted, and a panic records unknown (BUG-3475)

codex r1: eviction at the bound could drop a RUNNING entry, letting its key
begin again beside the upload still using it; it now drops only the oldest
settled entry, and an attempt over a bound full of running ones goes
untracked (reads unknown). A panic after mint skipped every report and left
the key running until the TTL; the handler now records unknown before
re-panicking, since the keep door does not say which way it went.

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

* fix(server): a running import key outlives the settled TTL (BUG-3475)

codex r2: the TTL sweep expired RUNNING entries too, and an import may read
for up to its 1h ceiling, so a long upload's key could begin again beside it.
Running entries now last importOutcomeRunningTTL (2h), pinned above the read
ceiling.

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

* fix(server): an import rollback reads back whether the workspace is gone (BUG-3475)

codex r3: removeUnusableWorkspace returns nil even when its delete fails, so
'removed' was inferred from a return that does not mean it. rollBackPartialImport
now reads the row back; a workspace still live reports unknown and the 400
says it could not be removed. Tested with a trigger refusing the soft-delete,
red without the read-back.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-07 21:03:24 -04:00
xarmian 698fe2ff6e fix(server): a collab revalidation tick that errors makes the conn read-only (BUG-3474) (#1866)
* 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.
2026-10-07 19:45:14 -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 cc1aa6902c feat(decision): log each evaluation's spend (TASK-3119) (#1861)
* 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
2026-10-07 13:43:42 -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 deec62235e feat(decision): executable conventions, the server half (TASK-3119 U1a) (#1859)
* 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
2026-10-07 12:42:32 -04:00
xarmian 90a304765f fix(cli): copy --dry-run previews a big number with every digit (BUG-3456) (#1858)
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
2026-10-07 12:37:49 -04:00
xarmian 3aff8e388f fix: pad keeps an edited /pad skill and never downgrades a newer one (BUG-3466) (#1857)
* 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)
2026-10-07 12:26:28 -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
v0.18.0-rc.8
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 b3cf4f5292 fix(server): an ordinary open backlog fills Up next and pad project ready (BUG-3453) (#1854)
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
2026-10-07 08:36:58 -04:00
xarmian 1c23a1bcbc feat: onboarding asks where an existing project's open work lives and offers to bring it in (TASK-3454) (#1853)
* 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)
2026-10-07 08:18:53 -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 cba6379c4d test: the stalled-transport test waits for the worker to be inside Publish before clearing the transport (BUG-3464) (#1850) 2026-10-07 05:09:33 -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 e774268d63 fix(plugin): drop the pinned version so installs follow the commit (BUG-3463) (#1848)
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
2026-10-07 03:33:30 -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 0f932b5880 fix(mcp): remote routes answer a missing workspace with no_workspace and the list (TASK-2314) (#1846)
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
2026-10-07 03:14:22 -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 05c039f131 fix: agents load items with --agent; --format markdown stays body-verbatim and names an empty body (TASK-3451) (#1842)
* 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)
2026-10-07 00:02:09 -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 a27df265bc fix(store): concurrent re-invites no longer report a successful replace as an error (BUG-3460) (#1840)
* 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)
v0.18.0-rc.7
2026-10-06 21:23:38 -04:00
xarmian c204dd3683 fix(cli): json fields print as JSON, metadata labelled for the item's kind; MCP 0.68 (BUG-3448) (#1838)
* 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)
2026-10-06 18:54:37 -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
xarmian f9d79aea94 fix(store): relationship reads exclude soft-deleted collections (BUG-3434) (#1828)
* 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)
2026-10-06 09:59:38 -04:00
xarmian b67e78343a test(materialize): fire the idle timer mid-job on purpose, not by wall-clock luck (BUG-3435) (#1827)
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
2026-10-06 09:25:18 -04:00
xarmian 10a1683b83 fix: the access epoch tracks the live collection set; deleted collections leave the local index; the open-children guard ignores them (BUG-3428 phase 2) (#1826)
* 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)
2026-10-06 07:59:20 -04:00
xarmian e3668f0fea fix(server): the item-grant filter keeps an app request's read ceiling (BUG-3424) (#1825)
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
2026-10-06 01:33:24 -04:00
xarmian a7f98c01fc fix(store): search, tags, children and progress exclude soft-deleted collections (BUG-3428, phase 1) (#1824)
* 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)
2026-10-06 01:04:29 -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 1e75d19d1f fix(store): rebindQuery skips SQL comments and quoted identifiers (BUG-3430) (#1822)
* 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)
2026-10-05 22:54:52 -04:00